作者:张新
日期:2017年09月18日
简单来说,我们需要提供一套 API ,根据当前 locale 而显示不同的文本消息,日期时间,货币等等。
本文不涉及基础概念问题(如什么是i18n,l10n),请自行Google。
由于我们的需求,以及下面所列种种原因,最终导致我们没有选择 react-intl 作为我们的国际化解决方案。😦
做为前端工程师,我们的核心任务并不是去 Build 更多的 UI 组件库,而是需要更好的去服务我们的 Business。长话短说就是,尽可能选择开源的解决方案,长远来说,这必将会降低成本和减轻风险。

我们是用 Webpack 来作为打包工具的,Webpack 天生就支持代码分割,按需异步加载代码块,所以无论是 Webpack@1 中的 require-ensure,还是其后续者 Webpack@2 中的 import() 语法,都可以帮助我们做到根据当前 locale 异步加载 i18n 资源。
默认选择的是 import(),首先它是符合 ECMAScript 规范的,另外,从 Webpack@2.4 起,Webpack 也支持通过 webpackChunkName 来自定义每个区块名,已经可以完全取代 require.ensure。
Context #首先我们是基于 React 的组件模型的,它提供了一套很好的父子组件的通信方式。一般常见方式是,父组件可以通过 props 传递数据给子组件,而子组件可以通过调用 callback prop 来回传数据,这样一来,数据流就很清晰,代码也易读并且容易维护。当出现问题时,也很容易找出问题所在,因为你知道是谁传递的这个 callback,是谁调用了这个 callback,也知道是哪两个父子组件正在通信。
所以,设计思路时的第一个想法就是,我们可以通过这样单向传递的方式来逐级传递 i18n 资源的。但是,问题也随之而来了,随着组件层级的逐级加深,每个组件不得不显示的将 props 传递给它的子组件(即使有可能没有用到它),更槽糕的是,有些中间层组件,根本不在我们的控制之内,有可能是第三方的组件库,导致我们根本没有办法保证 i18n 资源可以逐级下传至下层组件。
这里,你会突然醒悟,我们需要的一种机制,可以帮助我们做到透传。而React Context完美的解决了这个问题。
React Context: 当一个组件在它的 Context 上定义了一些数据后,任何它的后代子组件都可以访问这些数据。
这也就意味着,任何在组件树中的子组件都可以访问所在 Context 中的数据,而并不需要通过 prop 来传递。具体使用方法,请参考官方文档。
shouldComponentUpdate -> Observer Pattern #那么我们现在知道是在 Context 中放我们的 i18n 资源数据,那么 Context 是什么时候会更新呢?
The
getChildContextfunction will be called when the state or props changes. In order to update data in the context, trigger a local state update with this.setState. This will trigger a new context and changes will be received by the children.
但是 Context 更新了之后,在组件树中的后代子组件能够同步获取最新值吗?如果可以,又是如何获取的呢?
这就取决于中间层组件的 shouldComponentUpdate 实现。
任何一个 React 组件都可以定义自己的 shouldComponentUpdate 实现(或者继承自 PureComponent ),如果它返回 false,那就表明该组件及其子组件都不需要重新渲染。但是,如果某一个中间层组件的 shouldComponentUpdate 也返回 false,那么该组件的子组件也就不会更新,即使是当前组件树中的 Context 已经变了。😃
一图胜千言:
高层组件(包含 context.color 定义)
中间层组件(`shouldComponentUpdate` 返回 false)
...
....
子组件(从当前 context 中获取数据)
...
...
以上图为例,如果 context.color 更新了,子组件是不会重新渲染的,因为它的父级组件的 shouldComponentUpdate 返回了 false。这样直接导致的结果就是 - 全乱套了,UI 和状态之间无法同步,从而导致各种莫名其妙的 Bugs,这也是大家常常不愿意用它的另外一个重要原因。
那么,怎么解决呢?答案就是 Context + Observer Pattern。
首先,我们会有二个假设:
Context 是 immutable 的,并且由当前组件自己维护自己的内部状态Context,之后 Context 的更新都能通过订阅获取设计方案有了,接下来,我们来看看如何设计我们的 API。
L10n #它主要用于:
L10nMessage,L10nDateTime,L10nCurrency 或是其它通过 injecctL10n 注入的组件formatMessage,formatDateTime,formatCurrency 等等L10nProvider #它主要用于:
L10n 对象,通过 Context 暴露给子级组件Provider ),其它 L10nXXX 组件不能脱离它独立使用injectL10n #它主要用于:
Context 中的 l10n 对象,生成一个新的 Bounded 的 l10n 对象,然后以 props 的形式自动注入到绑定组件中l10n,并且订阅 l10n 资源的更新,一旦有任何更新,进而会调用 forceUpdate 强制更新组件getBoundL10n = () => {
const l10n = this.context...
const boundFuncs = ...
const boundConfig = ...
return {
[l10nName]: {
...l10n,
...boundConfig,
...boundFuncs,
},
}
}
这样就保证在 locale 或是其它资源(如 messages )变化后,被注入的组件会有 props 的变化,从而会触发组件的重新渲染,进而重新获取 l10n 中的数据
L10nMessage #L10nMessage 是 l10n.formatMessage 方法的组件封装,它根据当前传入的 id,找到对应的文本信息,同时也支持字符串插值。

L10nDateTime #L10nDateTime 是 l10n.formatDateTime 的组件封装,它根据当前传入的 date 对象和日期时间的 format,转换回对应 format 的字符串形式。
比如,en_US 的长日期时间格式为 MMMM d, yyyy h:mm a,然后传入当前的 date 后,就会转换为 2017/09/31 11:30 a.m.
L10nCurrency #L10nCurrency 是 l10n.formatCurrency 的组件封装,它根据传入的数值 amount 和货币代码 code (如 USD ),转换回对应格式的字符串表示形式。同时,用户可以指定以下选项:
{
integerOnly: false, // 是否是整数形式
separationCount: 3, // 多少字符做分隔
separator: ',', // 数字之间的分隔符
negativeMark: '-', // 是否需要添加负号标记
}
那么,有了这些 API 组件后,它们之间是如何协同工作的呢?
以下面这个典型的组件树为例:
<L10nProvider locale='zh_CN'>
...
<UpdateBlocker>
<L10nMessage id='save' />
...
</UpdateBlocker>
<Form>
<Field
placeholder={l10n.formatMessage('save')}
/>
</Form>
</L10nProvider>
UpdateBlocker组件意味着该组件的shouldComponentUpdate生命周期方法始终返回false
L10nProvider 中,会首先初始化一个 L10n 对象或是传入的 L10n 对象DateTimeSymbols,currenciesConfig 和 en_US 的 i18n 文本消息,会默认绑定到上一步生成的 L10n 对象上L10nProvider 的 componentDidMount 生命周期方法中,会去动态加载指定 locale 的文本消息(此处是 zh_CN )this.l10n.messages 合并injectL10n 实现自动注入和订阅l10n的更新的组件,如 L10nMessage )Context 中,拿到 immutable 的 L10n 对象后,重新生成一个新的 L10n 对象字面量,然后以 prop 的形式注入到绑定组件中通过实现 i18n 组件让我们更加深入的了解了 React 的组件模型,以及组件之间的通信方式。对于组件化的编程模型有了更加深入的思考和探索,这样的经历,又何乐而不为呢?😃
网站内容许可证:公共领域(public domain)