这两天手头的一个任务是给一个五六年的老项目添加多语言。这个项目庞大且复杂,早期是用jQuery实现的,两年前引入Vue并逐渐用组件替换了以前的Mustache风格模板。要添加多语言,不可避免存在不少文本替换的工做,这么庞杂的一个项目,怎么才能使文本替换变得高效且不会引入bug是这篇文章主要要写的东西。html
vue-i18n是一个vue插件,主要做用就是让项目支持国际化多语言。首先咱们引入这个插件:vue
import Vue from 'vue' import VueI18n from 'vue-i18n' Vue.use(VueI18n)
这里注意的就是vue插件的使用方法,经过全局方法 Vue.use()
使用插件。node
插件一般会为 Vue 添加全局功能。插件的范围没有限制——通常有下面几种:添加全局方法或者属性;添加全局资源:指令/过滤器/过渡等;经过全局 mixin 方法添加一些组件选项;添加 Vue 实例方法,经过把它们添加到 Vue.prototype 上实现。
Vue.js 的插件应当有一个公开方法 install, 经过代码能够更直观的看出插件提供的功能:webpack
MyPlugin.install = function (Vue, options) { // 1. 添加全局方法或属性 Vue.myGlobalMethod = function () { // 逻辑... } // 2. 添加全局资源 Vue.directive('my-directive', { bind (el, binding, vnode, oldVnode) { // 逻辑... } ... }) // 3. 注入组件 Vue.mixin({ created: function () { // 逻辑... } ... }) // 4. 添加实例方法 Vue.prototype.$myMethod = function (methodOptions) { // 逻辑... } }
了解vue插件的install方法对咱们等会查看i18n源码有很大帮助。git
咱们先看官方提供的最简单的使用模板:github
//HTML <div id="app"> <p>{{ $t("message.hello") }}</p> </div> //JAVASCRIPT const messages = { en: { message: { hello: 'hello world' } }, ja: { message: { hello: 'こんにちは、世界' } } } const i18n = new VueI18n({ locale: 'ja', // set locale messages, // set locale messages }) new Vue({ i18n }).$mount('#app') //OUTPUT <div id="#app"> <p>こんにちは、世界</p> </div>
能够看到,咱们在实例化Vue的时候,将i18n当作一个option传了进去。以后咱们就能够在vue的组件里使用i18n了,使用方法主要是两种:web
$t()
方法this.$i18n.t()
方法上节的messages
是一个包含了多语言的的对象,它就像咱们的字典。既然是字典,我但愿它只有一本。因此我只会new VueI18n()
一次,并将实例化获得的i18n对象做为惟一的字典。json
因此新建一个locales文件夹,存放全部跟多语言相关的代码。目前包含三个文件:index.js, en.json, zh.json。浏览器
en.json和zh.json就是咱们的语言包,是一个json形式。这里为了对照方便,咱们必须保证语言包的内容是一一对应的。而后咱们在index.js中完成设置。app
import Vue from 'vue' import VueI18n from 'vue-i18n' Vue.use(VueI18n) const DEFAULT_LANG = 'zh' const LOCALE_KEY = 'localeLanguage' const locales = { zh: require('./zh.json'), en: require('./en.json'), } const i18n = new VueI18n({ locale: DEFAULT_LANG, messages: locales, }) export const setup = lang => { if (lang === undefined) { lang = window.localStorage.getItem(LOCALE_KEY) if (locales[lang] === undefined) { lang = DEFAULT_LANG } } window.localStorage.setItem(LOCALE_KEY, lang) Object.keys(locales).forEach(lang => { document.body.classList.remove(`lang-${lang}`) }) document.body.classList.add(`lang-${lang}`) document.body.setAttribute('lang', lang) Vue.config.lang = lang i18n.locale = lang } setup() export default i18n
咱们对外提供了一个setup()
的方法,给使用者修改当前使用语种的能力。同时,咱们在setup里还作了三件事:
将当前语种存到 localStorage中,保存用户的使用习惯;给body添加语种相关的class,由于不一样语言可能致使排版出现差别,咱们须要适配;将当前语种存到Vue的全局配置中,以便将来可能的使用。
最后咱们在main.js
中引入这个Index.js便可。
import Vue from 'vue' import App from './app.vue' import store from './store' import router from './router' ... import i18n from '@crm/locales' ... new Vue({ i18n, router, store, render: h => h(App), }).$mount('#app')
这样看起来,咱们的国际化已经完成了,然而以后立刻就有新的问题出现了!
前面说到,vue实例中咱们可使用this.$i18n.t
,这里的this是vue的实例。那项目中不少js代码在vue的实例以外,咱们要怎么办?
最简单的解决方法是这样的,咱们的locales/index.js这个文件已经export了i18n
这个对象,那咱们只须要在每次要使用的时候手动将i18n导入进来就能够了。
<script> import i18n from '@crm/locales' //const test = "测试数据" const test = i18n.t('message.test') </script>
但是这样一来,咱们以后作诸如上面的文本替换时,就得当心翼翼的区别是否在vue实例中。若是是,咱们用this.$i18n.t
,不然先import而后用i18n.t
。这显然增长了复杂性!
为了解决这个问题,只直接的解决办法就是将i18n挂到window下,变成全局变量。咱们就没必要再Import进来,同时只使用统一方法:i18n.t
。
咱们在main.js中添加这行代码:
import Vue from 'vue' import App from './app.vue' import store from './store' import router from './router' ... import i18n from '@crm/locales' ... window.i18n = i18n new Vue({ i18n, router, store, render: h => h(App), }).$mount('#app')
而后咱们兴高采烈的将组件中的import i18n
全去掉,并将this.$i18n.t
改成i18n.t
。而后项目跑起来就报错了:i18n is not defined。
问题出在哪里?显示是组件调用i18n的时候,i18n尚未挂载到window上,因此是执行顺序出了问题。咱们先来看一下下面代码的执行顺序:
//假设webpack的入口文件是```main.js``` //main.js import moduleA from 'moduleA' console.log(1) import moduleB from 'moduleB' console.log(2) //moduleA.js console.log(3) //moduleB.js console.log(4) //最终在浏览器中打印出的数字顺序是: 3 4 1 2
为何会这样呢?跟ES6 module的机制有关系。import命令具备提高效果,会提高到整个模块的头部,首先执行。这种行为的本质是,import命令是编译阶段执行的,在代码运行以前。
这样咱们就找出以前报错的缘由了,咱们先import了App, router这些视图,而后Import的i18n并挂载到window。因此组件的script中的代码会最早执行,而此时i18n并未开始。因此咱们首先将window.i18n = i18n
移到locales/index中,而后调整main.js中import的顺序:
//locales/index ... setup() window.i18n = i18n export default i18n //main.js import Vue from 'vue' import i18n from '@crm/locales' import App from './app.vue' import store from './store' import router from './router' ...
前面咱们在main.js的new Vue({i18n, ...})
中将i18n做为option放了进去,但很快我发现这个项目并只有一个Vue的实例。全局搜索发现一共有70多个。
项目中很的诸如弹窗之类的组件,都是直接本身实例化一个Vue而后本身$mount()
到DOM中。这些组件在实例化的过程当中并无混入i18n选项,因此他们的template上天然找不到$t()
方法。
怎么办?难道给每个new Vue()都手动添加i18n选项吗?确定不行,首先咱们要给添加70屡次,其次若是将来又有人写了新的new Vue()忘了添加Ii8n,那又回致使报错。因此咱们要想一个万全的法子。
官方文档里找不到解决办法,看来咱们得hack一下了。首先咱们来查vue-i18n的源码,找到$t()
方法是怎么工做的。
全局搜索$t,找到定义它的地方:
Object.defineProperty(Vue.prototype, '$t', { get: function get () { var this$1 = this; return function (key) { var values = [], len = arguments.length - 1; while ( len-- > 0 ) values[ len ] = arguments[ len + 1 ]; var i18n = this$1.$i18n; return i18n._t.apply(i18n, [ key, i18n.locale, i18n._getMessages(), this$1 ].concat( values )) } } });
能够看到$t挂载在Vue.prototype上,每当咱们在实例中调用$t时,其实咱们是在调用this.$i18n对象上的_t方法。如今问题变成,实例上的$i18n是什么是时候定义的。
全局搜索$i18n,咱们找到了前面提到过的每一个插件必须提供的install方法:
function install (_Vue) { Vue = _Vue; ... Object.defineProperty(Vue.prototype, '$i18n', { get: function get () { return this._i18n } }); extend(Vue); Vue.mixin(mixin); Vue.directive('t', { bind: bind, update: update }); Vue.component(component.name, component); // use object-based merge strategy var strats = Vue.config.optionMergeStrategies; strats.i18n = strats.methods; }
能够看到$i18n一开始就被定义在了Vue.prototype上,每次调用的时候其实咱们是在调用this._i18n,因此如今问题变成实例的_i18n在哪里。同时能够看到在Install中咱们还混入了mixin, directive, component,这些在上面都有提过它的做用。
var mixin = { beforeCreate: function beforeCreate () { var options = this.$options; options.i18n = options.i18n || (options.__i18n ? {} : null); if (options.i18n) { if (options.i18n instanceof VueI18n) { ... this._i18n = options.i18n;
咱们在mixin中找到了this._i18n的来源,前面提到mixin会被注入到组件中。在每一个组件建立前,咱们将this.$options的i18n给了this._i18n。
这个this.$options是什么?它的使用方式是Vue.mixin(mixin)
,因此咱们看一下vue的文档:全局混入
// 为自定义的选项 'myOption' 注入一个处理器。 Vue.mixin({ created: function () { var myOption = this.$options.myOption if (myOption) { console.log(myOption) } } }) new Vue({ myOption: 'hello!' }) // => "hello!"
因此this.$options就是咱们new Vue时提供的选项对象。
因此问题的根源就是除了main.js中的new Vue外,其他70多个new Vue咱们没有混入i18n这个选项。怎样才可让每次new Vue时自动将i18n混入选项呢?看上去咱们只能修改Vue的源码了。
function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword'); } this._init(options); }
能够看到每次Vue实例化时,会调用_init方法,这个方法从哪里来呢?
function initMixin (Vue) { Vue.prototype._init = function (options) { ...
在Vue.prototype上,因此咱们只须要修改Vue.prototype就行了。
//locales/index const init = Vue.prototype._init Vue.prototype._init = function(options) { init.call(this, { i18n, ...options, }) }
这样咱们在任什么时候候new Vue()就自动添加了i18n选项,问题解决!