包裹Context

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

  1. // dependencies.js
  2. export default {
  3. data: {},
  4. get(key) {
  5. return this.data[key];
  6. },
  7. register(key, value) {
  8. this.data[key] = value;
  9. }
  10. }

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

  1. import dependencies from './dependencies';
  2. dependencies.register('title', 'React in patterns');
  3. class App extends React.Component {
  4. getChildContext() {
  5. return dependencies;
  6. }
  7. render() {
  8. return <Header />;
  9. }
  10. }
  11. // 我们还可以对context中的数据做类型校验
  12. App.childContextTypes = {
  13. data: PropTypes.object,
  14. get: PropTypes.func,
  15. register: PropTypes.func
  16. };

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

  1. // Title.jsx
  2. export default class Title extends React.Component {
  3. render() {
  4. return <h1>{ this.context.get('title') }</h1>
  5. }
  6. }
  7. Title.contextTypes = {
  8. data: PropTypes.object,
  9. get: PropTypes.func,
  10. register: PropTypes.func
  11. };

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

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

  1. // Title.jsx
  2. import wire from './wire';
  3. function Title(props) {
  4. return <h1>{ props.title }</h1>;
  5. }
  6. export default wire(Title, ['title'], function resolve(title) {
  7. return { title };
  8. });

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

下面是一种可能的实现:

  1. export default function wire(Component, dependencies, mapper) {
  2. class Inject extends React.Component {
  3. render() {
  4. var resolved = dependencies.map(this.context.get.bind(this.context));
  5. var props = mapper(...resolved);
  6. return React.createElement(Component, props);
  7. }
  8. }
  9. Inject.contextTypes = {
  10. data: PropTypes.object,
  11. get: PropTypes.func,
  12. register: PropTypes.func
  13. };
  14. return Inject;
  15. };

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

不依赖context的另外一种实现

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

  1. var dependencies = {};
  2. export function register(key, dependency) {
  3. dependencies[key] = dependency;
  4. }
  5. export function fetch(key) {
  6. if (key in dependencies) return dependencies[key];
  7. throw new Error(`"${ key } is not registered as dependency.`);
  8. }
  9. export function wire(Component, deps, mapper) {
  10. return class Injector extends React.Component {
  11. constructor(props) {
  12. super(props);
  13. this._resolvedDependencies = mapper(...deps.map(fetch));
  14. }
  15. render() {
  16. return (
  17. <Component
  18. {...this.state}
  19. {...this.props}
  20. {...this._resolvedDependencies}
  21. />
  22. );
  23. }
  24. };
  25. }

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

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

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

  1. // app.jsx
  2. import Header from './Header.jsx';
  3. import { register } from './di.jsx';
  4. register('my-awesome-title', 'React in patterns');
  5. class App extends React.Component {
  6. render() {
  7. return <Header />;
  8. }
  9. }
  1. // Header.jsx
  2. import Title from './Title.jsx';
  3. export default function Header() {
  4. return (
  5. <header>
  6. <Title />
  7. </header>
  8. );
  9. }
  1. // Title.jsx
  2. import { wire } from './di.jsx';
  3. var Title = function(props) {
  4. return <h1>{ props.title }</h1>;
  5. };
  6. export default wire(Title, ['my-awesome-title'], title => ({ title }));

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