Vue-i18n国际化的内部实现原理

1.简单实现

我们看一个示例:

<!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">
      // 如果使用模块系统 (例如通过 vue-cli),则需要导入 Vue 和 VueI18n ,然后调用 Vue.use(VueI18n)。
      import Vue from 'https://unpkg.com/vue@2.6.10/dist/vue.esm.browser.js'
      Vue.use(VueI18n)

      new Vue({
        // 通过 `i18n` 选项创建 Vue 实例
        // 通过选项创建 VueI18n 实例
        i18n: new VueI18n({
          locale: 'zh', // 设置地区
          // 准备翻译的语言环境信息
          // 设置地区信息
          messages: {
            en: {
              message: {
                hello: 'hello, I am Gopal'
              }
            },
            zh: {
              message: {
                hello: '你好,我是 Gopal 一号'
              }
            }
          }
        })
      }).$mount('#app')
    </script>
  </body>
</html>

使用上是比较简单的,本文我们深入了解 Vue-i18n 的工作原理,探索国际化实现的奥秘。包括:

  • 整体的 Vue-i18n 的架构是怎样的?
  • 上述 demo 是如何生效的?
  • 我们为什么可以直接在模板中使用 $t?它做了什么?
  • 上述 demo 是如何做到不刷新更新页面的?
  • 全局组件 <i18n> 和全局自定义指令的实现?

2.代码结构和入口

├── components/
│   ├── interpolation.js // <i18n> 组件的实现
│   └── number.js
├── directive.js // 全局自定义组件的实现
├── extend.js // 拓展方法
├── format.js // parse 和 compile 的核心实现
├── index.js // 入口文件
├── install.js // 注册方法
├── mixin.js // 处理各个生命周期
├── path.js
└── util.js

关于 Vue-18n 的整体架构,网上找到了一个比较贴切的图,如下。其中左侧是 Vue-i18n 提供的一些方法、组件、自定义指令等能力,右侧是 Vue-i18n 对数据的管理:
在这里插入图片描述
入口文件为 index.js,在 VueI18n 类中的 constructor 中先调用 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)

注册完成后,会调用_initVM,这个主要是创建了一个 Vue 实例对象,后面很多功能会跟这个this._ vm相关联:

// VueI18n 其实不是一个 Vue 对象,但是它在内部建立了 Vue 对象 vm,然后很多的功能都是跟这个 vm 关联的
this._initVM({
  locale,
  fallbackLocale,
  messages,
  dateTimeFormats,
  numberFormats
})

_initVM (data: {
         locale: Locale,
         fallbackLocale: FallbackLocale,
         messages: LocaleMessages,
         dateTimeFormats: DateTimeFormats,
         numberFormats: NumberFormats
         }): void {
  // 用来关闭 Vue 打印消息的
  const silent = Vue.config.silent
  Vue.config.silent = true
  this._vm = new Vue({ data }) // 创建了一个 Vue 实例对象
  Vue.config.silent = silent
}

3.全局方法$t的实现

我们来看看 Vue-i18n 的$t方法的实现,揭开国际化翻译的神秘面纱

在 extent.js 中,我们看到在 Vue 的原型中挂载 $t 方法,这是我们为什么能够直接在模板中使用的原因。

// 在 Vue 的原型中挂载 $t 方法,这是我们为什么能够直接在模板中使用的原因
// 把 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)
}

看到的是调用 index.js 中的$t的方法:

// $t 最后调用的方法
_t (key: Path, _locale: Locale, messages: LocaleMessages, host: any, ...values: any): any {
  if (!key) { return '' }
  const parsedArgs = parseArgs(...values)
  // 如果 escapeParameterHtml 被配置为 true,那么插值参数将在转换消息之前被转义。
  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
  )
}

3.1 _interpolate

回到主线,当调用 _translate 的时候,接着调用

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

并返回

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

在 _render 方法中,可以调用自定义方法去处理插值对象,或者是默认的方法处理插值对象。

_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 中完成。

3.2 format.js中的parse和compile

format.js 实现了 BaseFormatter 类,这里使用 _caches 实现了一层缓存优化,也是常见的优化手段。下面的 没有插值对象的话,就直接返回 [message],就完成使命了。

export default class BaseFormatter {
  // 实现缓存效果
  _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)
  }
}

当遇到如下的使用方式的时候:

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

主要涉及两个方法,我们先来看 parse,代码比较直观,可以看到本质上是遍历字符串,然后遇到有 {}包裹的,把其中的内容附上类型拿出来放入到 tokens 里返回。

// 代码比较直观,可以看到本质上是遍历字符串,然后遇到有 {} 包裹的,把其中的内容附上类型拿出来放入到 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"],最后再做一个简单的拼接就可以了,至此,翻译就可以成功了

3.3 Vue-i18n是如何避免XSS?

上面 _t方法中有一个 _escapeParameterHtml。这里谈谈escapeParams,其实是Vue-i18n为了防止 xss 攻击做的一个处理。如果escapeParameterHtml被配置为 true,那么插值参数将在转换消息之前被转义。

// 如果escapeParameterHtml被配置为true,那么插值参数将在转换消息之前被转义。
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') {
        // 处理参数,防止 XSS 攻击
        params[key] = escapeHtml(params[key])
      }
    })
  }
  return params
}

4.如何做到无刷新更新页面

我们发现,在 demo 中,我无论是修改了 locale 还是 message 的值,页面都不会刷新,但页面也是会更新数据。这个功能类似 Vue 的双向数据绑定,它是如何实现的呢?

在这里插入图片描述
这里 Vue-i18n 采用了观察者模式,我们上面提到过的 _initVM方法中,我们会将翻译相关的数据 data 通过 new Vue 传递给 this._vm实例。现在要做的就是去监听这些 data 的变化

Vue-i18n 的这一块的逻辑主要是在 mixin.js 文件中,在 beforeCreate 中调用 watchI18nData 方法,这个方法的实现如下:

// 为了监听翻译变量的变化
watchI18nData (): Function {
  const self = this
  // 使用 vue 实例中的 $watch 方法,数据变化的时候,强制刷新
  // 组件的 data 选项是一个函数。Vue 在创建新组件实例的过程中调用此函数。它应该返回一个对象,然后 Vue 会通过响应性系统将其包裹起来,并以 $data 的形式存储在组件实例中
  return this._vm.$watch('$data', () => {
    self._dataListeners.forEach(e => {
      Vue.nextTick(() => {
        e && e.$forceUpdate()
      })
    })
  }, { deep: true })
}

其中 _dataListeners,我理解是一个个的实例(但我没想到具体的场景,在系统中使用 vue-18n new 多个实例?)。subscribeDataChanging 和 unsubscribeDataChanging 就是用来添加和移除订阅器的函数

// 添加订阅器,添加使用的实例
subscribeDataChanging (vm: any): void {
  this._dataListeners.add(vm)
}

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

它们会在 mixin.js 中的 beforeMount 和 beforeDestroy 中调用

// 精简后的代码  
// 在保证了_i18n 对象生成之后,beforeMount 和 beforeDestroy 里就能增加移除监听了
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
      }
    })
}

总结一下,在 beforeCreate 会去 watch data 的变化,并在 beforeMount 中添加订阅器。假如 data 变化,就会强制更新相应的实例更新组件。并在 beforeDestroy 中移除订阅器,防止内存溢出,整体流程如下图所示
在这里插入图片描述

5.全局自定义指令以及全局组件的实现

在 extent.js 中,我们提到了注册全局指令和全局组件,我们来看下如何实现的

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

5.1 全局指令t

关于指令 t 的使用方法,详情参考官方文档

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

<!-- 字符串语法:通过数据或计算属性绑定 -->
<p v-t="msg"></p>

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

<!-- 对象语法: 通过数据或计算属性绑定 -->
<p v-t="{ path: greeting, args: { name: fullName } }"></p>

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

在 directive.js 中,我们看到实际上是调用了 t 方法和 tc 方法,并给 textContent 方法赋值。(textContent 属性表示一个节点及其后代的文本内容。)

// 主要是调用了 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

5.2 全局组件i18n

i18n 函数式组件 使用如下:

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

其源码实现 src/components/interpolation.js,其中 tag 表示外层标签。传 false, 则表示不需要外层。

export default {
  name: 'i18n',
  functional: true,
  props: {
    // 外层标签。传 false,则表示不需要外层
    tag: {
      type: [String, Boolean, Object],
      default: 'span'
    },
    path: {
      type: String,
      required: true
    },
    locale: {
      type: String
    },
    places: {
      type: [Array, Object]
    }
  },
  render (h: Function, { data, parent, props, slots }: Object) {
    const { $i18n } = parent

    const { path, locale, places } = props
    // 通过插槽的方式实现
    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'
    // 是否需要外层标签进行渲染
    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
  )
}

6.总结

总体 Vue-i18n 代码不复杂,但也花了自己挺多时间,算是一个小挑战。从 Vue-i18n 中,我学习到了

  • 国际化翻译 Vue-i18n 的架构组织和$t的原理,当遇到插值对象的时候,需要进行 parse 和 compile
  • Vue-i18n 通过转义字符避免 XSS
  • 通过观察者模式对数据进行监听和更新,做到无刷新更新页面
  • 全局自定义指令和全局组件的实现
<think>我们正在讨论Vue 3中优化i18n国际化)的使用,特别是如何避免在每个页面或组件中重复导入`useI18n`。 根据引用[1]的建议,我们可以总结出: 1. 避免在Pinia store的顶层使用`useI18n()`,因为这样可能导致i18n实例未正确初始化或上下文丢失。 2. 创建一个专门的辅助函数来访问i18n实例。 3. 在store的方法中使用辅助函数,而不是直接使用`useI18n()`。 4. 确保i18n插件在Vue应用挂载之前被正确注册。 但是,用户的问题是关于如何在每个页面(组件)中避免重复导入`useI18n`。我们可以借鉴引用中的思路,但需要调整到组件层面。 解决方案: 1. 提供一个全局的i18n实例,这样在组件中可以直接访问这个实例,而不必每次都调用`useI18n`。 2. 使用Vue插件机制,在插件中全局注入i18n实例,这样在组件中可以通过`this`访问或者通过全局属性访问。 3. 使用Composition API的全局注入(provide/inject),在根组件提供i18n实例,在子组件中注入使用。 然而,值得注意的是,`useI18n`是Vue I18n提供的组合式API,它依赖于当前的组件实例上下文(尤其是当使用全局注册的i18n实例时)。因此,在组件外直接调用`useI18n`是不安全的(比如在普通的js文件中),但在组件内调用是标准的。 为了避免在每个组件中重复导入和调用`useI18n`,我们可以考虑以下方法: 方法一:创建全局混合(mixin)或自定义组合函数 -Vue 3中,我们可以创建一个自定义的组合函数,封装`useI18n`,然后返回需要的方法(如`t`翻译函数)。然后在每个组件中引入这个自定义函数。但这仍然需要在组件中导入,不过可以封装更多逻辑。 方法二:全局属性(推荐) - 在创建Vue应用实例时,将i18n实例的`t`方法或其他方法挂载到全局属性(app.config.globalProperties)。 - 这样在模板中可以直接使用`$t`,在选项式API中可以通过`this.$t`访问。 - 但是,在组合式API的setup函数中,我们无法直接访问`this`,因此这种方法对于组合式API的组件来说,在setup内部并不直接可用。不过,在模板中仍然可以避免使用`useI18n`。 方法三:使用provide/inject在组合式API组件中全局注入 - 在根组件(App.vue)中,使用`provide`提供i18n实例(或者提供`useI18n`返回的内容)。 - 在子孙组件中,使用`inject`来获取i18n实例。但是,这需要每个组件都写一次`inject`,并不比使用`useI18n`简洁。 方法四:创建自定义插件 - 我们可以创建一个Vue插件,将i18n的方法挂载到全局属性,同时也可以提供一个组合式函数来简化在setup中的使用。 然而,针对组合式API,Vue I18n v9的设计初衷是让我们在需要国际化的组件中使用`useI18n`。如果每个组件都使用国际化,那么在每个组件中调用`useI18n`是标准做法。 但是,如果用户觉得在每个组件中写`import { useI18n } from 'vue-i18n'`和`const { t } = useI18n()`很繁琐,我们可以考虑以下优化: 自定义组合函数(推荐): 我们可以创建一个模块,导出一个自定义的`useTranslation`函数,它内部调用`useI18n`并返回我们需要的部分(比如只返回`t`函数)。这样做的好处是: 1. 集中管理:如果需要调整i18n的使用方式,只需修改这个函数。 2. 简化使用:在组件中只需要引入这个自定义函数,然后调用即可。 例如: // 在某个文件中,比如`src/composables/useTranslation.js` import { useI18n } from 'vue-i18n' export function useTranslation() { const { t, ...rest } = useI18n() // 可以在这里添加一些自定义逻辑 return { t, ...rest } } 然后在组件中: import { useTranslation } from '@/composables/useTranslation' const { t } = useTranslation() 这样虽然还是要导入,但是导入的路径更短,而且如果以后需要修改,比如添加默认选项,只需要修改`useTranslation`函数。 全局挂载组合式API函数(不推荐,因为可能违反组合式API的使用原则): 目前没有官方推荐的方法将组合式API函数全局注入到每个组件的setup中。 另一种思路:使用全局注入的辅助函数(在模板中使用) 如果我们大部分翻译都是在模板中,那么我们可以通过全局属性来提供`t`方法,这样在模板中可以直接使用`$t`。这样就不需要在组件的setup中调用`useI18n`(除非在setup中也需要翻译)。 在创建app时: app.use(i18n) // 这是vue-i18n的标准注册,它会自动将$t注入到每个组件的实例中(包括模板和选项式API的this上下文) // 此外,我们还可以手动添加: app.config.globalProperties.$t = i18n.global.t 这样,在模板中,我们可以直接: <div>{{ $t('message.key') }}</div> 但是,如果我们在setup脚本中需要翻译,我们仍然需要调用`useI18n`(或者在选项式API的方法中,我们可以使用`this.$t`,但在组合式API的setup中,我们无法使用`this`)。因此,对于组合式API,如果我们在setup中有翻译需求,还是需要调用`useI18n`。 总结:针对用户的问题,我们可以提供两种优化方式: 1. 在模板中使用全局的`$t`方法(通过Vue I18n的默认注入),这样在模板中不需要调用`useI18n`。 2. 在setup脚本中,我们可以封装一个自定义的`useTranslation`组合函数来简化导入和调用。 另外,对于Pinia store,我们按照引用[1]的建议,使用辅助函数来获取i18n实例(例如在store的方法中需要翻译时,我们可以调用这个辅助函数,这个辅助函数内部可以获取当前组件的i18n实例,但是需要注意,在store中无法直接使用组合式API,所以这个辅助函数可能需要在组件调用store方法时传入当前组件的i18n实例,或者通过其他方式获取到i18n实例)。 下面我们详细说明: 步骤1:确保i18n正确安装 在创建Vue应用之前,先创建i18n实例并安装: ```javascript import { createApp } from 'vue' import { createI18n } from 'vue-i18n' import App from './App.vue' const i18n = createI18n({ // 配置 }) const app = createApp(App) app.use(i18n) app.mount('#app') ``` 步骤2:在模板中使用全局注入的$t(无需在组件中额外导入) 在任意组件的模板中,都可以使用`$t`来翻译。 步骤3:在组合式API的setup中,使用自定义组合函数(简化版) 创建`useTranslation.js`: ```javascript import { useI18n } from 'vue-i18n' export function useTranslation() { return useI18n() // 或者只返回t函数 // const { t } = useI18n() // return t } ``` 然后在组件中: ```javascript import { useTranslation } from '@/composables/useTranslation' // 在setup中 const { t } = useTranslation() // 如果上面返回的是t函数,则直接: // const t = useTranslation() ``` 这样我们就可以在组件中使用`t`函数。 步骤4:在Pinia store中避免直接使用useI18n 因为store不是组件,没有组件实例上下文,所以不能直接使用`useI18n`。我们可以这样做: - 方法A:在调用store方法时,将当前组件的t函数作为参数传入(适用于从组件中调用store方法的情况)。 - 方法B:在store中直接使用i18n的全局实例(注意:全局实例可能无法处理局部翻译,如果项目有局部翻译,这种方法可能不适用)。例如: ```javascript // 在创建i18n实例的地方导出i18n实例 export const i18n = createI18n({ ... }) // 然后在store中导入这个实例 import { i18n } from '@/i18n' const t = i18n.global.t ``` 但是,在store中直接使用全局实例可能无法响应语言切换(因为全局实例的t函数可能不是响应式的,或者响应式更新不会触发store的重新渲染?)。而且,如果我们在组件中使用了局部作用域(通过`useI18n`的参数指定了局部翻译),那么在store中无法访问到这个局部作用域。 因此,更推荐方法A:将t函数作为参数传入store的方法。 根据用户的真正需求,他们想避免在每个页面(组件)中导入`useI18n`。我们的解决方案是: - 模板部分:使用全局的`$t`,无需导入。 - setup脚本部分:使用自定义的`useTranslation`函数,虽然还是要导入,但这是一个集中封装的函数,简化了调用(并且可以统一管理)。 另外,还有一种极端做法:创建一个高阶组件(HOC)或者使用SFC中的`<script setup>`的自动导入功能(通过构建工具自动导入)。例如,使用unplugin-auto-import这样的工具,可以自动导入`useI18n`(这样在组件中就不用写import语句了)。但是,这需要配置构建工具,并且可能带来其他问题(如IDE支持,代码可读性等)。 综合考虑,我们给出以下推荐方案: 1. 在模板中使用`$t`进行翻译,这样可以完全不使用`useI18n`。 2. 在组件的setup脚本中,如果确实需要使用翻译,则使用自定义组合函数`useTranslation`(集中封装,便于维护)。 3. 对于Pinia store,避免使用`useI18n`,而是通过参数传入t函数,或者使用全局实例(根据项目情况选择)。 最后,我们生成相关问题: 相关问题: 1. 在Vue 3的组合式API中,如何全局注入一个组合函数? 2. 如何配置Vue I18n实现多语言切换? 3. 在Pinia store中访问i18n实例的最佳实践是什么? 4. 使用自动导入工具(如unplugin-auto-import)来省略组合式API的导入语句是否可行? </think>在 Vue 3 中优化 `useI18n` 重复导入的解决方案如下,基于最佳实践和引用建议: ### 1. **创建全局访问辅助函数** ```typescript // src/utils/i18n-helper.ts import { useI18n } from 'vue-i18n' export function useGlobalI18n() { return useI18n() } ``` - **使用方式**: ```vue <script setup> import { useGlobalI18n } from '@/utils/i18n-helper' const { t } = useGlobalI18n() </script> ``` - **优势**:封装后只需导入更短路径的辅助函数,统一管理 i18n 逻辑[^1] ### 2. **全局注入翻译方法** ```typescript // main.ts import { createApp } from 'vue' import App from './App.vue' import { createI18n } from 'vue-i18n' const i18n = createI18n({ /* 配置 */ }) const app = createApp(App) // 注入全局 $t 方法 app.config.globalProperties.$t = i18n.global.t app.use(i18n).mount('#app') ``` - **模板直接使用**: ```vue <template> <div>{{ $t('greeting') }}</div> </template> ``` - **注意事项**:此方法仅在模板中生效,JS 逻辑仍需通过辅助函数 ### 3. **自动导入工具配置(推荐)** 使用 `unplugin-auto-import` 自动注入: ```bash npm install -D unplugin-auto-import ``` ```typescript // vite.config.ts import AutoImport from 'unplugin-auto-import/vite' export default defineConfig({ plugins: [ AutoImport({ imports: [ { 'vue-i18n': ['useI18n'] } ], dts: 'src/auto-imports.d.ts' }) ] }) ``` - **效果**:所有组件无需手动导入,可直接调用 `const { t } = useI18n()` - **原理**:构建时自动注入 API 声明[^1] ### 4. **Pinia Store 专用优化** ```typescript // stores/localeStore.ts import { i18n } from '@/main' export const useLocaleStore = defineStore('locale', () => { const changeLanguage = (lang: string) => { i18n.global.locale = lang } return { changeLanguage } }) ``` - **关键点**:直接导入注册后的 i18n 实例,避免在 Store 顶层调用 `useI18n()`[^1] ### 5. **插件级封装方案 ```typescript // plugins/i18n.ts export default { install(app) { app.provide('i18n', useI18n()) } } // 组件内使用 import { inject } from 'vue' const { t } = inject('i18n')! ``` ### 最佳实践总结 1. **组件层**:优先使用自动导入工具(方案3)或全局辅助函数(方案1) 2. **模板层**:直接使用注入的 `$t` 方法(方案2) 3. **Store 层**:导入预注册的 i18n 实例(方案4) 4. **生命周期**:确保 i18nVue 挂载前完成初始化[^1] > 采用自动导入+全局注入的组合方案,可减少 90% 的手动导入操作。实际项目中方案3实施成本最低且效果显著,大型项目推荐配合方案4管理状态。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值