前言
很多时候前端项目需要实现国际化,刚开始语言数量不多、项目规模不大时,可能不怎么会考虑性能优化,直接一把梭全部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. 目录结构如下
本文完,如果有其他思路也欢迎探讨 未经允许禁止转载