这段时间一直在做国际化的项目,需要对界面上的文本都对各种语言做翻译,利用了vue-i18n这个库,它可以在切换语言时无刷新的更新页面上的文本翻译。 本文主要用来记录阅读这个库时的心得体会。
如何使用
按照官网教程安装完后,经过很简单的几句代码就可以使用了:
1 | import VueI18n from 'vue-i18n'; |
结构
整个插件的结构如下:
入口
先看看Vue.use
是如何工作的,代码位于vue/src/core/global-api/use.js
:
1 | Vue.use = function(plugin: Function | Object) { |
接下来就是VueI18n.install
干了些什么,代码位于vue-i18n/src/install.js
1 | export function install(_Vue) { |
如何实现无刷新更新语言
这是我们最为关心的问题了,也是这个插件最核心的逻辑,其他都是为了让插件更好用。
此处逻辑放在了Vue.mixin(mixin)
里,这里面主要是给 Vue 实例或组件添加了 2 个生命周期钩子beforeCreate
和beforeDestroy
。 在beforeCreate
钩子里实现了对语言、文本等变量的监听,如果发现了变更,就会手动触发 Vue 实例或组件的$forceUpdate
方法强制更新。在beforeDestroy
中注销所有监听。
beforeCreate
这个就是核心代码了,看看怎么实现的:
1 | beforeCreate (): void { |
如果在当前组件没有注册 i18n 插件,会首先去根组件找,然后再去父组件找。只要找到了就说明如果翻译变量变化了,当前组件可以执行更新。
watchI18nData、subscribeDataChanging
为了能够监听到翻译变量的变化,vuei18n 做了一个巧妙的事:
- 它在内部实例化了一个 vue 实例,然后把所有相关翻译变量当做
data
选项传进去, - 最后再
watch
整个data
,只要触发了watch
就说明翻译变量发生了变化。
首先,在执行插件的构造函数时,有一个_initVM
函数,这里就是用来实例化内部 vm 的:
1 | _initVM(data: { |
可以看到监听的有语言、默认语言、翻译词包、时间格式化函数、数字格式化函数
然后在上述的mixin
逻辑中,有一个watchI18nData
函数,它就是用来watch
整个data
对象的:
1 | watchI18nData(): Function { |
_dataListeners
是什么? 这里是一个简单的观察者模式
,_dataListeners
是一个数组,存放所有对data
感兴趣的回调函数。回调函数在哪里注册的呢? 就是上面的
1 | this._i18n.subscribeDataChanging(this); // this指向vue组件或vue实例 |
最后只要看看$forceUpdate
干了什么,整个插件的主体脉络就梳理清楚了,应该就是执行页面更新了.这个函数在vue
源码中的vue/src/core/instance/lifecycle.js
:
1 | Vue.prototype.$forceUpdate = function() { |
至于vm._watcher.update()
干了些什么事,以后再研究。
综上: 我们可以在页面上设置一个切换语言功能,在其中只要更改语言locale
就可以最终执行组件的update
了,或者动态的更新词包也行。
beforeDestroy
上面也说到,这里会卸载watch
和_dataListeners
,具体看看怎么做的:
1 | beforeDestroy (): void { |
代码很简单,略。。。。
至此,插件的核心逻辑就讲完了,其他的都是锦上添花的功能,我们也挨个来看下。
$t
、$tc
在install
函数中执行了这个extend(Vue)
,它会往vue
实例上挂载各种各样的便利格式化函数:
1 | Object.defineProperty(Vue.prototype, '$t', { |
这里重点关注最常用的$t
和$tc
.
$t
跟踪它的代码调用,会发现最后会到一个_render
函数,他负责将词条连同各种参数翻译成最终的文本:
1 | // message是待翻译词条,values是参数如插值或单复数的实际值 |
这个函数又调用了_formatter.interpolate
,formatter
负责解析切割词条,返回文本片段数组。
formatter
分为了两步:parse
和compile
,
parse
负责把待翻译词条解析成特定格式的数组,数组每个元素格式如下:
1 | { |
整个过程其实并不难,就是挨个处理字符:
1 | const RE_TOKEN_LIST_VALUE: RegExp = /^(\d)+/; |
比如hello , my name is {name}, and you?
就会被解析为:
1 | [ |
这里的name
变量真正的值就存储在_render
函数的values
参数中。下一步compile
就会拿到真正的值并替换name
变量.
compile
同样比较简单,最后也会返回一个数组
1 | // values参数就是_render函数的values |
其实只要把compiled
数组join
就是最终$t
的结果了,为什么不这么干呢? 是因为想把formatter
处理的结果给$tc
复用。
$tc
它比较『无耻』,先直接拿到$t
的劳动果实,再从数组中挑一个想要的。
1 | _tc(key: Path, _locale: Locale, messages: LocaleMessages, host: any, choice?: number, ...values: any): any { |
fetchChoice
的逻辑并不难,就是根据choice
的值取数组中挑一个元素回来,各位看官自行去了解。
v-t
这是一个全局指令,
这是一个全局指令,只有bind
和update
两个选项,前者会在绑定 DOM 元素时将翻译后的词条当做元素的textContent
,后者执行更新逻辑。
bind
1 | export function bind(el: any, binding: Object, vnode: any): void { |
应该来说,这里的t
函数做的事情和挂载到Vue
实例上的$t
、$tc
类似,实际代码确实也是这样:
1 | function t(el: any, binding: Object, vnode: any): void { |
update
更新时,如果语言没变且绑定到指令上的值没有变化则什么也不做;否则执行跟bind
相同的逻辑:
1 | export function update(el: any, binding: Object, vnode: any, oldVNode: any): void { |
这里的looseEqual
比较有意思:如果都是数组,那么挨个比较每个数组元素;如果两个参数都是对象,那么递归比较每个属性值;如果不是对象,那么把它俩转为字符串对比:
1 | export function looseEqual(a: any, b: any): boolean { |
<i18n>
这是VueI18n
提供的一个函数式组件,业务中用的比较少,暂且没看。。。。 它的作用是可以方便的混合普通文本翻译与 html 模板,在一些稍微复杂的情形下会比较有用。
Vue.config.optionMergeStrategies
看到install
方法里最后有这样两句:
1 | // use object-based merge strategy |
关于选项合并策略,vue 的 mixin也有描述,但具体的源码是怎样的呢?mixin
是在什么时间点起作用的呢?
我们先回答后一个问题:
mixin 的作用时间点
在Vue
的构造函数(位于/vue/src/core/instance/index.js
)中只有很短的几句:
1 | function Vue(options) { |
其中的_init
函数是在initMixin
中定义的:
1 | export function initMixin(Vue: Class<Component>) { |
看到有一句
1 | vm.$options = mergeOptions( |
这个mergeOptions
就是处理mixin
的过程了,可以看到这个过程非常早,早于组件初始化生命周期钩子。再来看看mergeOptions
1 | /** |
好,我们已经知道mixin
的合并时机非常早,并且会在合并组件其他选项之前就会合并mixin
的选项。正因为这样,才有官网上说的『如果组件自身的选项与 mixin 冲突,最后会以组件自身的选项为准』。
对于i18n
选项的合并,通过代码我们已经知道他是使用Vue.config.optionMergeStrategies.methods
,其实对于各种合并策略,无非就是怎么处理参数中的两个对象,我们不妨把所有提供提供的合并策略挨个了解下,代码都位于vue/src/core/util/options.js
。
components、directives 和 filters 的合并策略都是 mergeAssets,大体就是组件选项覆盖 mixin 选项。
1 | function mergeAssets(parentVal: ?Object, childVal: ?Object, vm?: Component, key: string): Object { |
strats.props、strats.methods、strats.inject 、strats.computed
这几个都是使用相同的一个叫extend
的函数:
1 | strats.props = strats.methods = strats.inject = strats.computed = function( |
上述代码逻辑很简单,如果parent
或child
为空,直接返回不为空的那个;否则执行extend
合并二者的选项,extend
同样很简单:
1 | // from中的第一层元素覆盖to的第一层元素 |
所以这几个合并策略都是最多只合并一层属性。
strats.data
1 | strats.data = function(parentVal: any, childVal: any, vm?: Component): ?Function { |
注意methods
策略直接返回的合并后对象,而data
策略返回的是一个函数,原因在代码注释里已经说的很清楚。接下来关注下其中的mergeData
函数:
1 | /** |
注意到这里是递归合并所有选项,而不是像官网上说的那样:
数据对象在内部会进行浅合并 (一层属性深度),在和组件的数据发生冲突时以组件数据优先。
我们也使用以下代码也可以验证data
的合并不是只合并一层属性。
1 | var mixin = { |
综上,data
合并策略返回一个函数,所有深层次的选项都会被合并。
strats.watch
他会把所有 watch 选项最后合并成数组。
1 | /** |
strats.propsData、strats.el
这两个选项只能在非生产环境下使用,
1 | /** |
这俩都是采用类似短路求值的方式,优先使用childVal
.