<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【源碼】Vue-i18n: 你知道國際化是怎么實現(xiàn)的么?

          共 25459字,需瀏覽 51分鐘

           ·

          2021-06-22 01:24

          Vue-i18n 簡單介紹以及使用

          大家好,我是 Gopal。目前就職于 Shopee,一家做跨境電商的公司,因為業(yè)務(wù)涉及到多個國家,所以我們各個系統(tǒng)都會涉及到國際化翻譯。Vue I18n 是 Vue.js 的國際化插件,它可以輕松地將一些本地化功能集成到你的 Vue.js 應(yīng)用程序中。

          本文的源碼閱讀是基于版本 8.24.4 進行

          我們來看一個官方的 demo

          <!DOCTYPE html>
          <html>
            <head>
              <meta charset="utf-8">
              <title>ES modules browser example</title>
              <script src="../../dist/vue-i18n.js"></script>
            </head>
            <body>
              <div id="app">
                <p>{{ $t('message.hello') }}</p>
              </div>
              <script type="module">
                // 如果使用模塊系統(tǒng) (例如通過 vue-cli),則需要導(dǎo)入 Vue 和 VueI18n ,然后調(diào)用 Vue.use(VueI18n)。
                import Vue from 'https://unpkg.com/[email protected]/dist/vue.esm.browser.js'
                Vue.use(VueI18n)

                new Vue({
                  // 通過 `i18n` 選項創(chuàng)建 Vue 實例
                  // 通過選項創(chuàng)建 VueI18n 實例
                  i18nnew VueI18n({
                    locale'zh'// 設(shè)置地區(qū)
                    // 準(zhǔn)備翻譯的語言環(huán)境信息
                    // 設(shè)置地區(qū)信息
                    messages: {
                      en: {
                        message: {
                          hello'hello, I am Gopal'
                        }
                      },
                      zh: {
                        message: {
                          hello'你好,我是 Gopal 一號'
                        }
                      }
                    }
                  })
                }).$mount('#app')
              
          </script>
            </body>
          </html>

          使用上是比較簡單的,本文我們深入了解 Vue-i18n 的工作原理,探索國際化實現(xiàn)的奧秘。包括:

          • 整體的 Vue-i18n 的架構(gòu)是怎樣的?
          • 上述 demo 是如何生效的?
          • 我們?yōu)槭裁纯梢灾苯釉谀0逯惺褂?$t?它做了什么?
          • 上述 demo 是如何做到不刷新更新頁面的?
          • 全局組件 <i18n> 和全局自定義指令的實現(xiàn)?

          代碼結(jié)構(gòu)以及入口

          我們看一下 Vue-18n 的代碼結(jié)構(gòu)如下

          ├── components/
          │   ├── interpolation.js // <i18n> 組件的實現(xiàn)
          │   └── number.js
          ├── directive.js // 全局自定義組件的實現(xiàn)
          ├── extend.js // 拓展方法
          ├── format.js // parse 和 compile 的核心實現(xiàn)
          ├── index.js // 入口文件
          ├── install.js // 注冊方法
          ├── mixin.js // 處理各個生命周期
          ├── path.js
          └── util.js

          關(guān)于 Vue-18n 的整體架構(gòu),網(wǎng)上找到了一個比較貼切的圖,如下。其中左側(cè)是 Vue-i18n 提供的一些方法、組件、自定義指令等能力,右側(cè)是 Vue-i18n 對數(shù)據(jù)的管理

          入口文件為 index.js,在 VueI18n 類中的 constructor 中先調(diào)用 install 方法注冊

          // Auto install if it is not done yet and `window` has `Vue`.
          // To allow users to avoid auto-installation in some cases,
          // this code should be placed here. See #290
          /* istanbul ignore if */
          if (!Vue && typeof window !== 'undefined' && window.Vue) {
            install(window.Vue)
          }

          install 方法中,主要做了幾件事,如下代碼注釋,后面還會提到,這里有一個大致的印象

          // 在 Vue 的原型中拓展方法,代碼在 extend.js 里
          extend(Vue)
          // 在 Vue 中通過 mixin 的方式混入
          Vue.mixin(mixin)
          // 全局指令
          Vue.directive('t', { bind, update, unbind })
          // 全局組件
          Vue.component(interpolationComponent.name, interpolationComponent)
          Vue.component(numberComponent.name, numberComponent)

          注冊完成后,會調(diào)用 _initVM,這個主要是創(chuàng)建了一個 Vue 實例對象,后面很多功能會跟這個 this._ vm 相關(guān)聯(lián)

          // VueI18n 其實不是一個 Vue 對象,但是它在內(nèi)部建立了 Vue 對象 vm,然后很多的功能都是跟這個 vm 關(guān)聯(lián)的
          this._initVM({
            locale,
            fallbackLocale,
            messages,
            dateTimeFormats,
            numberFormats
          })

          _initVM (data: {
                   locale: Locale,
                   fallbackLocale: FallbackLocale,
                   messages: LocaleMessages,
                   dateTimeFormats: DateTimeFormats,
                   numberFormats: NumberFormats
                   }): void {
            // 用來關(guān)閉 Vue 打印消息的
            const silent = Vue.config.silent
            Vue.config.silent = true
            this._vm = new Vue({ data }) // 創(chuàng)建了一個 Vue 實例對象
            Vue.config.silent = silent
          }

          全局方法 $t 的實現(xiàn)

          我們來看看 Vue-i18n 的 $t 方法的實現(xiàn),揭開國際化翻譯的神秘面紗

          在 extent.js 中,我們看到在 Vue 的原型中掛載 $t 方法,這是我們?yōu)槭裁茨軌蛑苯釉谀0逯惺褂玫脑颉?/p>

          // 在 Vue 的原型中掛載 $t 方法,這是我們?yōu)槭裁茨軌蛑苯釉谀0逯惺褂玫脑?/span>
          // 把 VueI18n 對象實例的方法都注入到 Vue 實例上
          Vue.prototype.$t = function (key: Path, ...values: any): TranslateResult {
            const i18n = this.$i18n
            // 代理模式的使用
            return i18n._t(key, i18n.locale, i18n._getMessages(), this, ...values)
          }

          看到的是調(diào)用 index.js 中的 $t 的方法

          // $t 最后調(diào)用的方法
          _t (key: Path, _locale: Locale, messages: LocaleMessages, host: any, ...values: any): any {
            if (!key) { return '' }
            const parsedArgs = parseArgs(...values)
            // 如果 escapeParameterHtml 被配置為 true,那么插值參數(shù)將在轉(zhuǎn)換消息之前被轉(zhuǎn)義。
            if(this._escapeParameterHtml) {
              parsedArgs.params = escapeParams(parsedArgs.params)
            }
            const locale: Locale = parsedArgs.locale || _locale
            // 翻譯
            let ret: any = this._translate(
              messages, locale, this.fallbackLocale, key,
              host, 'string', parsedArgs.params
            )
          }

          _interpolate

          回到主線,當(dāng)調(diào)用 _translate 的時候,接著調(diào)用

          this._interpolate(step, messages[step], key, host, interpolateMode, args, [key])

          并返回

          this._render(ret, interpolateMode, values, key)

          _render 方法中,可以調(diào)用自定義方法去處理插值對象,或者是默認的方法處理插值對象。

          _render (message: string | MessageFunction, interpolateMode: string, values: any, path: string): any {
            // 自定義插值對象
            let ret = this._formatter.interpolate(message, values, path)

            // If the custom formatter refuses to work - apply the default one
            if (!ret) {
              // 默認的插值對象
              ret = defaultFormatter.interpolate(message, values, path)
            }

            // if interpolateMode is **not** 'string' ('row'),
            // return the compiled data (e.g. ['foo', VNode, 'bar']) with formatter
            return interpolateMode === 'string' && !isString(ret) ? ret.join('') : ret
          }

          我們主要來看看默認的方法處理,主要是在 format.js 中完成

          format.js 中的 parse 和 compile

          format.js 實現(xiàn)了 BaseFormatter 類,這里使用 _caches 實現(xiàn)了一層緩存優(yōu)化,也是常見的優(yōu)化手段。下面的 沒有插值對象的話,就直接返回 [message],就完成使命了。

          export default class BaseFormatter {
            // 實現(xiàn)緩存效果
            _caches: { [key: string]: Array<Token> }

            constructor () {
              this._caches = Object.create(null)
            }

            interpolate (message: string, values: any): Array<any> {
              // 沒有插值對象的話,就直接返回
              if (!values) {
                return [message]
              }
              // 如果存在 tokens,則組裝值返回
              let tokens: Array<Token> = this._caches[message]
              if (!tokens) {
                // 沒有存在 tokens,則拆分 tokens
                tokens = parse(message)
                this._caches[message] = tokens
              }
              return compile(tokens, values)
            }
          }

          當(dāng)遇到如下的使用方式的時候

          <p>{{ $t('message.sayHi', { name: 'Gopal' })}}</p>

          主要涉及兩個方法,我們先來看 parse,代碼比較直觀,可以看到本質(zhì)上是遍歷字符串,然后遇到有 {} 包裹的,把其中的內(nèi)容附上類型拿出來放入到 tokens 里返回。

          // 代碼比較直觀,可以看到本質(zhì)上是遍歷字符串,然后遇到有 {} 包裹的,把其中的內(nèi)容附上類型拿出來放入到 tokens 里返回。
          export function parse (format: string): Array<Token{
            const tokens: Array<Token> = []
            let position: number = 0

            let text: string = ''
            while (position < format.length) {
              let char: string = format[position++]
              if (char === '{') {
                if (text) {
                  tokens.push({ type'text'value: text })
                }

                text = ''
                let sub: string = ''
                char = format[position++]
                while (char !== undefined && char !== '}') {
                  sub += char
                  char = format[position++]
                }
                const isClosed = char === '}'

                const type = RE_TOKEN_LIST_VALUE.test(sub)
                  ? 'list'
                  : isClosed && RE_TOKEN_NAMED_VALUE.test(sub)
                    ? 'named'
                    : 'unknown'
                tokens.push({ value: sub, type })
              } else if (char === '%') {
                // when found rails i18n syntax, skip text capture
                if (format[(position)] !== '{') {
                  text += char
                }
              } else {
                text += char
              }
            }

            text && tokens.push({ type'text'value: text })

            return tokens
          }

          以上的 demo 的返回 tokens 如下:

          [
              {
                  "type""text",
                  "value""hi, I am "
              },
              {
                  "value""name",
                  "type""named"
              }
          ]

          還有 parse,就是將上述的組裝起來

          // 把一切都組裝起來
          export function compile (tokens: Array<Token>, values: Object | Array<any>): Array<any{
            const compiled: Array<any> = []
            let index: number = 0

            const mode: string = Array.isArray(values)
              ? 'list'
              : isObject(values)
                ? 'named'
                : 'unknown'
            if (mode === 'unknown') { return compiled }

            while (index < tokens.length) {
              const token: Token = tokens[index]
              switch (token.type) {
                case 'text':
                  compiled.push(token.value)
                  break
                case 'list':
                  compiled.push(values[parseInt(token.value, 10)])
                  break
                case 'named':
                  if (mode === 'named') {
                    compiled.push((values: any)[token.value])
                  } else {
                    if (process.env.NODE_ENV !== 'production') {
                      warn(`Type of token '${token.type}' and format of value '${mode}' don't match!`)
                    }
                  }
                  break
                case 'unknown':
                  if (process.env.NODE_ENV !== 'production') {
                    warn(`Detect 'unknown' type of token!`)
                  }
                  break
              }
              index++
            }

            return compiled
          }

          以上 demo 最后返回 ["hi, I am ", "Gopal"],最后再做一個簡單的拼接就可以了,至此,翻譯就可以成功了

          Vue-i18n 是如何避免 XSS ?

          上面 _t 方法中有一個 _escapeParameterHtml 。這里談?wù)?escapeParams,其實是 Vue-i18n 為了防止 xss 攻擊做的一個處理。如果 escapeParameterHtml 被配置為 true,那么插值參數(shù)將在轉(zhuǎn)換消息之前被轉(zhuǎn)義。

          // 如果escapeParameterHtml被配置為true,那么插值參數(shù)將在轉(zhuǎn)換消息之前被轉(zhuǎn)義。
          if(this._escapeParameterHtml) {
            parsedArgs.params = escapeParams(parsedArgs.params)
          }
          /**
           * Sanitizes html special characters from input strings. For mitigating risk of XSS attacks.
           * @param rawText The raw input from the user that should be escaped.
           */

          function escapeHtml(rawText: string): string {
            return rawText
              .replace(/</g'&lt;')
              .replace(/>/g'&gt;')
              .replace(/"/g'&quot;')
              .replace(/'/g'&apos;')
          }

          /**
           * Escapes html tags and special symbols from all provided params which were returned from parseArgs().params.
           * This method performs an in-place operation on the params object.
           *
           * @param {any} params Parameters as provided from `parseArgs().params`.
           *                     May be either an array of strings or a string->any map.
           *
           * @returns The manipulated `params` object.
           */

          export function escapeParams(params: any): any {
            if(params != null) {
              Object.keys(params).forEach(key => {
                if(typeof(params[key]) == 'string') {
                  // 處理參數(shù),防止 XSS 攻擊
                  params[key] = escapeHtml(params[key])
                }
              })
            }
            return params
          }

          如何做到無刷新更新頁面

          我們發(fā)現(xiàn),在 demo 中,我無論是修改了 locale 還是 message 的值,頁面都不會刷新,但頁面也是會更新數(shù)據(jù)。這個功能類似 Vue 的雙向數(shù)據(jù)綁定,它是如何實現(xiàn)的呢?

          new

          這里 Vue-i18n 采用了觀察者模式,我們上面提到過的 _initVM 方法中,我們會將翻譯相關(guān)的數(shù)據(jù) data 通過 new Vue 傳遞給 this._vm 實例。現(xiàn)在要做的就是去監(jiān)聽這些 data 的變化

          Vue-i18n 的這一塊的邏輯主要是在 mixin.js 文件中,在 beforeCreate 中調(diào)用 watchI18nData 方法,這個方法的實現(xiàn)如下:

          // 為了監(jiān)聽翻譯變量的變化
          watchI18nData (): Function {
            const self = this
            // 使用 vue 實例中的 $watch 方法,數(shù)據(jù)變化的時候,強制刷新
            // 組件的 data 選項是一個函數(shù)。Vue 在創(chuàng)建新組件實例的過程中調(diào)用此函數(shù)。它應(yīng)該返回一個對象,然后 Vue 會通過響應(yīng)性系統(tǒng)將其包裹起來,并以 $data 的形式存儲在組件實例中
            return this._vm.$watch('$data', () => {
              self._dataListeners.forEach(e => {
                Vue.nextTick(() => {
                  e && e.$forceUpdate()
                })
              })
            }, { deeptrue })
          }

          其中 _dataListeners,我理解是一個個的實例(但我沒想到具體的場景,在系統(tǒng)中使用 vue-18n new 多個實例?)。subscribeDataChangingunsubscribeDataChanging 就是用來添加和移除訂閱器的函數(shù)

          // 添加訂閱器,添加使用的實例
          subscribeDataChanging (vm: any): void {
            this._dataListeners.add(vm)
          }

          // 移除訂閱器
          unsubscribeDataChanging (vm: any): void {
            remove(this._dataListeners, vm)
          }

          它們會在 mixin.js 中的 beforeMountbeforeDestroy 中調(diào)用

          // 精簡后的代碼  
          // 在保證了_i18n 對象生成之后,beforeMount 和 beforeDestroy 里就能增加移除監(jiān)聽了
          beforeMount (): void {
            const options: any = this.$options
            options.i18n = options.i18n || (options.__i18n ? {} : null)

            this._i18n.subscribeDataChanging(this)
          },


            beforeDestroy (): void {
              if (!this._i18n) { return }
              const self = this
              this.$nextTick(() => {
                if (self._subscribing) {
                  // 組件銷毀的時候,去除這個實例
                  self._i18n.unsubscribeDataChanging(self)
                  delete self._subscribing
                }
              })
          }

          總結(jié)一下,在 beforeCreate 會去 watch data 的變化,并在 beforeMount 中添加訂閱器。假如 data 變化,就會強制更新相應(yīng)的實例更新組件。并在 beforeDestroy 中移除訂閱器,防止內(nèi)存溢出,整體流程如下圖所示

          全局自定義指令以及全局組件的實現(xiàn)

          在 extent.js 中,我們提到了注冊全局指令和全局組件,我們來看下如何實現(xiàn)的

          // 全局指令
          Vue.directive('t', { bind, update, unbind })
          // 全局組件
          Vue.component(interpolationComponent.name, interpolationComponent)
          Vue.component(numberComponent.name, numberComponent)

          全局指令 t

          關(guān)于指令 t 的使用方法,詳情參考官方文檔

          以下是示例:

          <!-- 字符串語法:字面量 -->
          <p v-t="'foo.bar'"></p>

          <!-- 字符串語法:通過數(shù)據(jù)或計算屬性綁定 -->
          <p v-t="msg"></p>

          <!-- 對象語法: 字面量 -->
          <p v-t="{ path: 'hi', locale: 'ja', args: { name: 'kazupon' } }"></p>

          <!-- 對象語法: 通過數(shù)據(jù)或計算屬性綁定 -->
          <p v-t="{ path: greeting, args: { name: fullName } }"></p>

          <!-- `preserve` 修飾符 -->
          <p v-t.preserve="'foo.bar'"></p>

          directive.js 中,我們看到實際上是調(diào)用了 t 方法和 tc 方法,并給 textContent 方法賦值。(textContent 屬性表示一個節(jié)點及其后代的文本內(nèi)容。)

          // 主要是調(diào)用了 t 方法和 tc 方法
          if (choice != null) {
            el._vt = el.textContent = vm.$i18n.tc(path, choice, ...makeParams(locale, args))
          else {
            el._vt = el.textContent = vm.$i18n.t(path, ...makeParams(locale, args))
          }

          在 unbind 的時候會清空 textContent

          全局組件 i18n

          i18n 函數(shù)式組件 使用如下:

          <div id="app">
            <!-- ... -->
            <i18n path="term" tag="label" for="tos">
              <a :href="url" target="_blank">{{ $t('tos') }}</a>
            </i18n>

            <!-- ... -->
          </div>

          其源碼實現(xiàn) src/components/interpolation.js,其中 tag 表示外層標(biāo)簽。傳 false, 則表示不需要外層。

          export default {
            name'i18n',
            functionaltrue,
            props: {
              // 外層標(biāo)簽。傳 false,則表示不需要外層
              tag: {
                type: [StringBooleanObject],
                default'span'
              },
              path: {
                typeString,
                requiredtrue
              },
              locale: {
                typeString
              },
              places: {
                type: [ArrayObject]
              }
            },
            render (h: Function, { data, parent, props, slots }: Object) {
              const { $i18n } = parent

              const { path, locale, places } = props
              // 通過插槽的方式實現(xiàn)
              const params = slots()
              // 獲取到子元素 children 列表
              const children = $i18n.i(
                path,
                locale,
                onlyHasDefaultPlace(params) || places
                  ? useLegacyPlaces(params.default, places)
                  : params
              )

              const tag = (!!props.tag && props.tag !== true) || props.tag === false ? props.tag : 'span'
              // 是否需要外層標(biāo)簽進行渲染
              return tag ? h(tag, data, children) : children
            }
          }

          注意的是:places  語法會在下個版本進行廢棄了

          function useLegacyPlaces (children, places{
            const params = places ? createParamsFromPlaces(places) : {}

            if (!children) { return params }

            // Filter empty text nodes
            children = children.filter(child => {
              return child.tag || child.text.trim() !== ''
            })

            const everyPlace = children.every(vnodeHasPlaceAttribute)
            if (process.env.NODE_ENV !== 'production' && everyPlace) {
              warn('`place` attribute is deprecated in next major version. Please switch to Vue slots.')
            }

            return children.reduce(
              everyPlace ? assignChildPlace : assignChildIndex,
              params
            )
          }

          總結(jié)

          總體 Vue-i18n 代碼不復(fù)雜,但也花了自己挺多時間,算是一個小挑戰(zhàn)。從 Vue-i18n 中,我學(xué)習(xí)到了

          • 國際化翻譯 Vue-i18n 的架構(gòu)組織和 $t 的原理,當(dāng)遇到插值對象的時候,需要進行 parse 和 compile
          • Vue-i18n 通過轉(zhuǎn)義字符避免 XSS
          • 通過觀察者模式對數(shù)據(jù)進行監(jiān)聽和更新,做到無刷新更新頁面
          • 全局自定義指令和全局組件的實現(xiàn)

          參考

          • https://zhuanlan.zhihu.com/p/110112552
          • https://hellogithub2014.github.io/2018/07/17/vue-i18n-source-code/


          瀏覽 171
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲成人黄色电影 | 91无码人妻一区二区 | 豆花AV在线入口 | 成人淫色免费视频 | 影音先锋熟女av 永久免费看黄网址 |