很多时候前端项目需要实现国际化,刚开始语言数量不多、项目规模不大时,可能不怎么会考虑性能优化,直接一把梭全部import进来。但是随着项目的迭代,越来越多的语言需要支持,词库体积也越来越大,这个时候就不得不考虑优化加载性能了。常规思路也就是使用动态import(),结合打包工具的自动分割chunk的功能,即可完成优化工作,这也是网上大部分优化方案的思路。
但是笔者在实际项目中遇到的问题,用上面的优化思路是行不通的,因为代码中有许多地方是在同步代码块中调用了国际化函数,国际化函数执行的时候词库并未加载,所以我们需要换一种思路。
所以我们可以自己实现一个vite插件,对index.html编辑一下,让我们的词库先于入口文件加载即可
/** * 堆代码 duidaima.com * 以参数的形式接收语言包文件的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', }, // 构造modulepreload标签,提前加载入口文件,减少整体加载时间 { tag: 'link', attrs: { rel: 'modulepreload', href: `${config.base}${fileName}`, }, injectTo: 'head', }, ], }; }, /** * 记录热更新的时间戳 * @param {*} param0 * @returns */ handleHotUpdate({ timestamp, file, modules }) { hotUpdateTimestamp = timestamp; return [...modules]; }, }; }2. vite.config.js
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. 语言包代码
const messages = { hello: { world: '你好,世界', vue: '你好,Vue', }, }; window.messages = messages; export default messages;4. i18n代码
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插件
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. 目录结构如下