vue-i18n是vue代碼貢獻量第二的vue core team的一位日本小哥寫的, 雖是第三方插件, 用起來內心也舒服. github裏搜了vue i18n, 結果有很多, 有一些很粗糙的, 甚至用jquery的lib都有六七十個star. (阻斷吐槽). 厲害的人明顯在設計上代碼上都高不少檔次吧.html
今天的故事的主角repo是: vue-i18n與iView. 在使用他們的時候報錯了, 查看了issue, 在issue中得到到一段代碼, 不明真相地解決了問題:vue
Vue.use(iView, { i18n: (key, value) => i18n.vm._t(key, value) })
這多是我第一次知道Vue.use
能夠傳第二個參數, 因此想知道發生了什麼.jquery
先說結果: 是由於iView
作了對vue-i18n
的集成, 是沒有仔細看文檔而使用不當致使的問題. 研究期間又看了element ui的代碼. 發現iView
的對vue-i18n
的集成是抄他們的. (阻斷吐槽). git
來講一下看完這篇文章能明白哪些幾點:github
Vue.use()
作了些什麼iView
和vue-i18n
集成使用的錯誤Vue.mixin()
作了些什麼$t
方法是哪裏來的(由於我只用了這個方法)下面開始咱們的故事.vuex
(接文章開頭的故事), 之前使用Vue.use()
的場景都是Vue.use(vuex)
, Vue.use(router)
等. 那麼此次在第二個參數傳入了i18n: (key, value) => i18n.vm._t(key, value)
之後發生了什麼事組織了程序報錯呢.api
首先要明白Vue.use()
是幹什麼用的, 接受的各個參數是幹嗎的. 開始看vue的代碼, 本文看的Vue的版本爲2.5.2, 貼個代碼, 文件位置: src/core/global-api/use.js
app
/* @flow */ import { toArray } from '../util/index' export function initUse (Vue: GlobalAPI) { Vue.use = function (plugin: Function | Object) { // 如下4行: 判斷這個插件是否已經被加載, 防止重複加載 const installedPlugins = (this._installedPlugins || (this._installedPlugins = [])) if (installedPlugins.indexOf(plugin) > -1) { return this } // additional parameters // 如下2行: 製造一串參數等待調用, 製造結果爲: 把Vue代替接收到的第一個參數 const args = toArray(arguments, 1) args.unshift(this) // 如下4行: 兼容兩種api, 而後調用插件中的安裝方法. if (typeof plugin.install === 'function') { plugin.install.apply(plugin, args) } else if (typeof plugin === 'function') { plugin.apply(null, args) } // 把插件記錄在內部, 以便下次判斷重複加載 installedPlugins.push(plugin) return this } }
代碼的語法解釋已經寫在註釋中, 如今來直白的解釋一下, 假設插件名字爲Cwj
:iview
// 使用的時候 Vue.use(Cwj) // 內部實際執行 Cwj.install(Vue) // 另外一種api, 不推薦, 由於正規的lib中都有install方法 Cwj(Vue) // 有參數的使用: Vue.use(Cwj, { foo: () => bar }) // 內部實際執行 Cwj.install(Vue, { foo: () => bar })
總結: Vue.use()
的行爲: 執行插件的install()
方法, 第一個參數爲Vue, 剩餘的參數爲Vue.use()
接受的第二個及之後的參數.dom
另外, 大部分ui組件的install方法大部分都在執行Vue.component()
, 哈哈.
知道了Vue.use()
幹了什麼, 那麼咱們要到iView
的代碼裏去找install()
方法了. 我這裏看的iView的版本爲2.5.0-beta.1, 在src/index.js
中找到了install方法:
const install = function(Vue, opts = {}) { locale.use(opts.locale); locale.i18n(opts.i18n); Object.keys(iview).forEach(key => { Vue.component(key, iview[key]); }); Vue.prototype.$Loading = LoadingBar; Vue.prototype.$Message = Message; Vue.prototype.$Modal = Modal; Vue.prototype.$Notice = Notice; Vue.prototype.$Spin = Spin; };
很明是第二行和第三行進行了第二個參數的操做, 那麼看一下src/locale/index.js
,
export const use = function(l) { lang = l || lang; }; export const i18n = function(fn) { i18nHandler = fn || i18nHandler; };
哇, 原來如此, 若是傳了i18n
方法, 就會在iView組件裏調用傳入的方法, 而不是預約義的i18n處理方法, 怪不到不按照文檔的規定來也不會報錯了.
那麼咱們傳入的方法是(key, value) => i18n.vm._t(key, value)
, 這裏的i18n.vm._t
是哪裏來的, 看一下在個人項目中出現問題的文件是如何加載他們的:
Vue.use(VueI18n) const i18n = new VueI18n({ locale: 'cn', messages }) Vue.use(iView, { i18n: (key, value) => i18n.vm._t(key, value) }) new Vue({ components: {App}, router, store, i18n, template: '<App/>' }).$mount('#app')
原來如此, 這個i18n
正是被傳入Vue跟組件的VueI18n
的實例, 實例裏帶着了語言包的信息, 以此推斷翻譯的時候也是調用了i18n.vm._t
方法, 那麼就忍不住要看一下vue-i18n
的代碼了, 我查看的vue-i18n的版本爲7.3.1, 看一下src/install.js
:
import { warn } from './util' import extend from './extend' import mixin from './mixin' import component from './component' import { bind, update } from './directive' export let Vue export function install (_Vue) { Vue = _Vue // 下面都是作一些必要的判斷, 不是咱們要看的運行機制 const version = (Vue.version && Number(Vue.version.split('.')[0])) || -1 /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && install.installed) { warn('already installed.') return } install.installed = true /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && version < 2) { warn(`vue-i18n (${install.version}) need to use Vue 2.0 or later (Vue: ${Vue.version}).`) return } // 這裏開始業務邏輯, 下面把_i18n賦給Vue.$i18n Object.defineProperty(Vue.prototype, '$i18n', { get () { return this._i18n } }) // 下面4句是加載核心 extend(Vue) Vue.mixin(mixin) Vue.directive('t', { bind, update }) Vue.component(component.name, component) // 下面是配置merge策略 // use object-based merge strategy const strats = Vue.config.optionMergeStrategies strats.i18n = strats.methods }
一樣地, 解釋也都寫在註釋中了, 那麼4句install的核內心個人mixin方法不熟悉, 接下來咱們來了解一下Vue.mixin()
方法作了些什麼:
/* @flow */ import { mergeOptions } from '../util/index' export function initMixin (Vue: GlobalAPI) { Vue.mixin = function (mixin: Object) { this.options = mergeOptions(this.options, mixin) return this } }
字面意思就merge配置, options也就是new Vue()
的時候傳入的參數, 因此在mixin裏傳入的會被全部Vue的子組件做爲options. (這個邏輯沒有看代碼, 看的是文檔).
進行了加載之後, 只須要在dom的插值表達中調用就能夠翻譯, 相似: $t('hello')
, 那麼$t
方法是如何被加載到全部Vue的子組件中的呢. 咱們須要從新開始理一下.
以前的章節對於一些加載的方法有了瞭解, 那麼如今從vue-i18n
安裝的時候開始分析, 以查出$t
是如何進行翻譯爲目的來跟着vue-i18n
的源碼兜一圈.
先看vue-i18n是如何被加載進來的.
Vue.use(VueI18n) const i18n = new VueI18n({ locale: 'cn', messages }) new Vue({ components: {App}, router, store, i18n, template: '<App/>' }).$mount('#app')
這裏分紅兩塊:
Vue.use(VueI18n)
, 上面說過, 這裏是執行了install方法new Vue({ i18n: new VueI18n(options)})
, 這裏是把一個vue-i18n
的實例設爲了咱們跟組件的options, 咱們須要分析這個實例有些什麼東西, 並在什麼地方何時調用了他.上文已經提到過, install裏的核心四個方法:
Object.defineProperty(Vue.prototype, '$i18n', { get () { return this._i18n } }) extend(Vue) Vue.mixin(mixin) Vue.directive('t', { bind, update }) Vue.component(component.name, component)
directive與component分別是註冊指令和註冊組件, 這裏先不展開, 咱們的目標是分析$t
.
來看extend.js中關於$t
的代碼: (文件中其餘代碼沒有貼出來)
/* @flow */ export default function extend (Vue: any): void { Vue.prototype.$t = function (key: Path, ...values: any): TranslateResult { const i18n = this.$i18n return i18n._t(key, i18n.locale, i18n._getMessages(), this, ...values) } }
原來如此, 咱們調用的$t('hello')
的來源是Vue.$t
, 而且調用了Vue.$i18n._t
方法. 對比文章開頭的i18n.vm._t
, i18n是VueI18n
的實例, 被註冊到Vue的options的i18n
這個字段裏, 調用了一樣的_t()
方法, 那麼如今浮現的問題是:
i18n.vm._t
是如何被加載成爲Vue.$i18n._t
的帶着問題, 咱們繼續看mixin.js. 在mixin.js裏只有兩個方法, 是beforeCreate和beforeDestroy, 我大體看了下beforeCreate, 做用是創建當前component的Vue._i18n變量, 這個變量就是Vue.$i18n的getter的指向, 爲何要寫getter緣由也出來了, 由於i18n-loader容許在單文件裏寫本地語言包, 因此要merge一下, 產生本地的語言環境.
那麼在mixin中是如何獲取初始語言包的呢, 源碼裏: const options: any = this.$options
, 也就是取了Vue.$options, 那麼下一章來說一講Vue實例構建的時候是如何把vue-i18n實例加載進入Vue實例的.
切取一段來自src/core/instance/init.js
的代碼:
Vue.prototype._init = function (options?: Object) { const vm: Component = this // a uid vm._uid = uid++ let startTag, endTag /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { startTag = `vue-perf-start:${vm._uid}` endTag = `vue-perf-end:${vm._uid}` mark(startTag) } // a flag to avoid this being observed vm._isVue = true // merge options if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created') /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { vm._name = formatComponentName(vm, false) mark(endTag) measure(`vue ${vm._name} init`, startTag, endTag) } if (vm.$options.el) { vm.$mount(vm.$options.el) } } }
(粗糙地一看), 就是Vue吧參數判斷了一下而後塞進了本身的.$options
屬性. 也就是Vue.$options.i18n
如今是一個VueI18n實例.
準備看一下VueI18n的構造吧. 代碼有600行, 初始化的時候仍是執行了
const silent = Vue.config.silent Vue.config.silent = true this._vm = new Vue({ data }) Vue.config.silent = silent
好像vuex也是這麼寫的, _t方法就寫在這個文件裏, 可是如何加載的還得看vue源碼, 只能下回分解了.