vite项目集成i18n,实现语言包懒加载【原创】

前言

很多时候前端项目需要实现国际化,刚开始语言数量不多、项目规模不大时,可能不怎么会考虑性能优化,直接一把梭全部import进来。但是随着项目的迭代,越来越多的语言需要支持,词库体积也越来越大,这个时候就不得不考虑优化加载性能了。

常规思路也就是使用动态import(),结合打包工具的自动分割chunk的功能,即可完成优化工作,这也是网上大部分优化方案的思路。

但是笔者在实际项目中遇到的问题,用上面的优化思路是行不通的,因为代码中有许多地方是在同步代码块中调用了国际化函数,国际化函数执行的时候词库并未加载,所以我们需要换一种思路。

整体思路

简单地说,就是要保证项目中的同步代码块运行时能正常调用国际化函数,就必须保证词库的加载时机在整个项目的入口文件之前。

以vite为例,入口文件是在index.html中的<script type="module" src="..."></script>完成加载的

即使使用的不是vite而是webpack等其他打包工具,这个思路也是适用的

所以我们可以自己实现一个vite插件,对index.html编辑一下,让我们的词库先于入口文件加载即可

代码实现

1. plugin代码

javascript 复制代码
/**
 * 以参数的形式接收语言包文件的moduleId
 * @param {Record<string, string>} messageModules key为locale,value为对应的module路径
 * @returns
 */
export default function (messageModules) {
  // 记录语言包的moduleId和对应的url
  const localeToChunkUrl = {};
  // 记录当前模式,build或dev
  let mode;
  // 保存vite的config,这个例子中暂时只用到了config.base
  let config;
  // 记录dev模式热更新的时间戳
  let hotUpdateTimestamp;
  // chunkName和locale的映射关系
  const chunkNameToLocale = {};
  return {
    name: 'vite-plugin-auto-import-messages',
    /**
     * 保存vite的config
     * @param {*} resolvedConfig
     */
    configResolved(resolvedConfig) {
      config = resolvedConfig;
    },
    /**
     * vite开始构建时会触发的hook,根据mode的不同,做不同的处理
     */
    buildStart() {
      // 记录当前的mode
      mode = this.environment.mode;
      if (mode === 'build') {
        // build模式下,手动将语言包文件emit出去,否则会被tree-shaking掉,无法生成对应的chunk
        Object.entries(messageModules).forEach(([locale, moduleId]) => {
          const chunkName = `language-${locale}`;
          chunkNameToLocale[chunkName] = locale;
          this.emitFile({
            name: chunkName,
            type: 'chunk',
            id: moduleId,
          });
        });
      } else if (mode === 'dev') {
        // dev模式下,直接将语言包文件的moduleId映射到localeToChunkUrl中
        Object.entries(messageModules).forEach(([locale, moduleId]) => {
          localeToChunkUrl[locale] = moduleId;
        });
      }
    },
    /**
     * 仅在build模式下触发的hook,用于记录语言包文件的chunkUrl
     * @param {*} options
     * @param {*} bunlde
     */
    generateBundle(options, bunlde) {
      Object.values(bunlde).forEach((chunk) => {
        const { name, fileName } = chunk;
        // 判断chunk.name是否在chunkNameToLocale存在,如果存在说明当前chunk是语言包的chunk
        const locale = chunkNameToLocale[name];
        if (!locale || !messageModules[locale]) {
          return;
        }
        // 记录locale对应的chunkUrl
        localeToChunkUrl[locale] = `${config.base}${fileName}`;
      });
    },
    transformIndexHtml(html, ctx) {
      let fileName;
      // 入口文件名
      if (mode === 'build') {
        fileName = ctx.chunk.fileName;
      } else {
        // dev模式下,默认入口文件名为main.js,可以根据实际情况修改
        fileName = 'src/main.js';
      }
      // 匹配入口文件的script标签
      const entryRegExp = new RegExp(
        `<script .* src="${config.base}${fileName}".*</script>`
      );
      // 从html中移除入口文件的script标签
      html = html.replace(entryRegExp, '');
      // 动态加载语言包的script标签的src
      let messageScriptSrc = 'messageChunks[locale]';
      // dev模式下添加热更新的时间戳
      if (mode === 'dev' && hotUpdateTimestamp) {
        messageScriptSrc += `+"?t=${hotUpdateTimestamp}"`;
      }
      let scriptContent = [
        // 为了避免全局变量污染,使用IIFE包裹代码
        `(function () {`,
        // 注入语言包的chunkUrl
        `const messageChunks = ${JSON.stringify(localeToChunkUrl)};`,
        // 根据实际情况修改获取locale的逻辑
        `const locale = localStorage.getItem("locale") || "zh_CN";`,
        // 动态创建script标签加载语言包
        `const scriptTag = document.createElement('script')`,
        // 可能会出现messageChunks[locale]为undefined的情况,根据实际需求做相应处理即可
        `scriptTag.src = ${messageScriptSrc};`,
        `scriptTag.type = 'module';`,
        `document.body.insertAdjacentElement('beforeend', scriptTag);`,
        `const entryScriptTag = document.createElement('script')`,
        `entryScriptTag.type = 'module';`,
        `entryScriptTag.crossorigin = true;`,
        `entryScriptTag.src = "${config.base}${fileName}";`,
        // 在语言包加载完毕后再加载入口文件
        `const onFinish = function() {document.body.insertAdjacentElement('beforeend', entryScriptTag);}`,
        `scriptTag.onload = onFinish;`,
        `scriptTag.onerror = onFinish;`,
        `})();`,
      ];
      scriptContent = scriptContent.map((line) => `      ${line}`);

      return {
        html,
        tags: [
          {
            tag: 'script',
            children: `\n${scriptContent.join('\n')}\n`,
            injectTo: 'body',
          },
        ],
      };
    },
    /**
     * 记录热更新的时间戳
     * @param {*} param0
     * @returns
     */
    handleHotUpdate({ timestamp, file, modules }) {
      hotUpdateTimestamp = timestamp;
      return [...modules];
    },
  };
}

2. vite.config.js

javascript 复制代码
import { defineConfig } from 'vite';
import autoImport from './src/plugins/auto-import-messages';
// https://vite.dev/config/
export default defineConfig({
  plugins: [
    // ...
    autoImport({
      zh_CN: './src/util/i18n/zh_CN.js',
      en_US: './src/util/i18n/en_US.js',
    }),
  ],
  // ...
});

3. 语言包代码

javascript 复制代码
const messages = {
  hello: {
    world: '你好,世界',
    vue: '你好,Vue',
  },
};
window.messages = messages;
export default messages;

4. i18n代码

javascript 复制代码
import { createI18n } from 'vue-i18n';

const getLang = () => {
  // 可以根据实际情况修改,常见的可能有localStorage、或者路由参数等
  const lang = localStorage.getItem('locale') || 'zh_CN';
  return lang;
};

const i18n = createI18n({
  locale: getLang(),
  messages: {},
});

const $t = i18n.global.t.bind(i18n.global);

if (window.messages) {
  i18n.global.setLocaleMessage(getLang(), window.messages);
  delete window.messages;
} else {
  console.error(`${getLang()} messages not found`);
}

export { i18n, $t };

5. main.js集成i18n插件

javascript 复制代码
import { createApp } from 'vue';
import App from './App.vue';
import { i18n, $t } from './util/i18n';

const vue = createApp(App);
vue.use(i18n);
vue.mount('#app');

// 测试同步代码块调用i18n函数是否生效
console.log($t('hello.world'));

6. 目录结构如下


本文完,如果有其他思路也欢迎探讨 未经允许禁止转载

相关推荐
kkkkkkkkira4 天前
VUE3+VITE简单的跨域代理配置
vue.js·vite·proxy配置
随便起的名字也被占用4 天前
封装一个自己的JS或TS库,并发布到npm上
开发语言·javascript·npm·vite
叶浩成5206 天前
goview——vue3+vite——数据大屏配置系统
vue3·vite·goview
weixin_1897 天前
‌Vite和Webpack区别 及 优劣势
前端·webpack·vue·vite
蟾宫曲8 天前
在 Vue3 项目中实现计时器组件的使用(Vite+Vue3+Node+npm+Element-plus,附测试代码)
前端·npm·vue3·vite·element-plus·计时器
vivo互联网技术8 天前
主打一个“小巧灵动”:Vite + Svelte
vite·性能·svelte·轻量·研发效率
正小安10 天前
Vite系列课程 | 11. Vite 配置文件中 CSS 配置(Modules 模块化篇)
前端·vite
学前端的小朱11 天前
Echarts实现大屏可视化
websocket·echarts·nodejs·vue3·vite·koa·cors