包裹Context

相比于单纯的数据对象,将context包装成一个提供一些方法的对象会是更好的实践。因为这样能提供一些方法供我们操作context里面的数据。

// dependencies.js
export default {
  data: {},
  get(key) {
    return this.data[key];
  },
  register(key, value) {
    this.data[key] = value;
  }
}

经过了包装的Context,可以通过类似于下面的这种方法使用。

import dependencies from './dependencies';
dependencies.register('title', 'React in patterns');

class App extends React.Component {
  getChildContext() {
    return dependencies;
  }
  render() {
    return <Header />;
  }
}

// 我们还可以对context中的数据做类型校验
App.childContextTypes = {
  data: PropTypes.object,
  get: PropTypes.func,
  register: PropTypes.func
};

这样我们的Title组件就能直接从Context中获取数据了。

// Title.jsx
export default class Title extends React.Component {
  render() {
    return <h1>{ this.context.get('title') }</h1>
  }
}
Title.contextTypes = {
  data: PropTypes.object,
  get: PropTypes.func,
  register: PropTypes.func
};

一般来说,我们不需要每次在使用context的地方都对context内的数据做类型校验。这种功能完全可以借由一个高阶组件派生出来。我们甚至可以使用高阶组件来作为我们操作context的代理,来替代我们对于context的直接操作。

比如: 我们可以使用一个高阶组件来替代我们直接对于this.context.get('title')方法的调用。

// Title.jsx
import wire from './wire';

function Title(props) {
  return <h1>{ props.title }</h1>;
}

export default wire(Title, ['title'], function resolve(title) {
  return { title };
});

wire这个函数的接收一个React Element作为第一个参数。第二个参数为一个数组,数组内容为组件所依赖的数据在context中的key。第三个参数我把它叫做mapper,mapper这个函数会将context里面的原始数据进行处理,然后以对象的形式返回组件所需要的数据。在我们的Title组件的例子中,我们在只需要在第二个参数中传入title作为我们依赖的描述,mapper就会返回在context中的title。 但是在实际应用中,我们所需要的数据可能会很多,依赖的描述形式也可能千变万化,可能是一堆数据的集合,也可能是读自某个配置文件。但是我们都可以通过将依赖的描述传入wire函数,让wire函数去帮我们将所有必要的依赖传入我们的组件,而不是传入所有的context。

下面是一种可能的实现:

export default function wire(Component, dependencies, mapper) {
  class Inject extends React.Component {
    render() {
      var resolved = dependencies.map(this.context.get.bind(this.context));
      var props = mapper(...resolved);

      return React.createElement(Component, props);
    }
  }
  Inject.contextTypes = {
    data: PropTypes.object,
    get: PropTypes.func,
    register: PropTypes.func
  };
  return Inject;
};

Inject 是一个能访问context并且获取所有在dependency数组中列出的数据的高阶组件。mapper是一个能接受context数据作为输入,将所有需要的数据从context中取出转换成组件props的函数。

不依赖context的另外一种实现

我们使用一个单例来注册/获取所有的依赖

var dependencies = {};

export function register(key, dependency) {
  dependencies[key] = dependency;
}

export function fetch(key) {
  if (key in dependencies) return dependencies[key];
  throw new Error(`"${ key } is not registered as dependency.`);
}

export function wire(Component, deps, mapper) {
  return class Injector extends React.Component {
    constructor(props) {
      super(props);
      this._resolvedDependencies = mapper(...deps.map(fetch));
    }
    render() {
      return (
        <Component
          {...this.state}
          {...this.props}
          {...this._resolvedDependencies}
        />
      );
    }
  };
}

我们把dependencies这个对象存放在全局范围(不是应用全局范围,而是包全局范围)。同时我们export出register和fetch两个函数用于读写我们的dependencies对象。(有点类似javascript class里面getter和setter的实现)。至此,wire函数的实现就已经完成了。wire函数接受一个React组件作为输入,输出一个高阶组件。

我们在这个返回的高阶组件中去处理我们的依赖并将其转化为props,在render函数中传入子组件(即我们传入想真正渲染的组件)

遵循上面这个模式,我们实现了以下代码。di.jsx这个helper帮助我们将我们应用的所有以来注册好,并且通过这个helper我们可以在整个应用的域里面随时取得我们想需要的依赖。

// app.jsx
import Header from './Header.jsx';
import { register } from './di.jsx';

register('my-awesome-title', 'React in patterns');

class App extends React.Component {
  render() {
    return <Header />;
  }
}
// Header.jsx
import Title from './Title.jsx';

export default function Header() {
  return (
    <header>
      <Title />
    </header>
  );
}
// Title.jsx
import { wire } from './di.jsx';

var Title = function(props) {
  return <h1>{ props.title }</h1>;
};

export default wire(Title, ['my-awesome-title'], title => ({ title }));

如果我们仔细观察Title.jsx的话,我们会发现实际上用到的component和wiring后的component的可以来自于不同的文件,这样的话,所有的这些代码都是可以被很容易的测试的。(因为可以很容易被mock)

results matching ""

    No results matching ""