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