從vue-i18n來分析vue插件是如何工做的

故事背景

vue-i18n是vue代碼貢獻量第二的vue core team的一位日本小哥寫的, 雖是第三方插件, 用起來內心也舒服. github裏搜了vue i18n, 結果有很多, 有一些很粗糙的, 甚至用jquery的lib都有六七十個star. (阻斷吐槽). 厲害的人明顯在設計上代碼上都高不少檔次吧.html

今天的故事的主角repo是: vue-i18niView. 在使用他們的時候報錯了, 查看了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()作了些什麼
  • 上面的代碼爲何避免了iViewvue-i18n集成使用的錯誤
  • Vue.mixin()作了些什麼
  • vue-i18n的差值表達式$t方法是哪裏來的(由於我只用了這個方法)

    下面開始咱們的故事.vuex

Vue.use

(接文章開頭的故事), 之前使用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.jsapp

/* @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處理方法, 怪不到不按照文檔的規定來也不會報錯了.

Vue.mixin

那麼咱們傳入的方法是(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. (這個邏輯沒有看代碼, 看的是文檔).

$t是如何運做的

進行了加載之後, 只須要在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

上文已經提到過, 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
  • _t方法是寫在哪裏被加載進Vue的

帶着問題, 咱們繼續看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實例的.

Vue實例構造過程當中加載的VueI18n實例

切取一段來自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源碼, 只能下回分解了.

原文地址

相關文章
相關標籤/搜索