作者:张新
日期: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
getChildContext
function 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)