• 包裹Context
    • 不依赖context的另外一种实现

    包裹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)