自定义webpack loader和babel plugin,替换国际化Key,减小打包体积
我正在参加「掘金·启航计划」
前言
上篇文章 使用Top-level await新特性,按需加载国际化资源文件,提高首屏效率 介绍了如何通过Top-level await
特性,拆分国际化资源文件,达到按需加载,减小了浏览器请求js size体积。
在实际项目中,还会存在另一种情况,也会导致打包后的js sizet体积过大。
比如国际化资源文件是以json
格式储存,如果项目比较大,词条量就会很大,然后业务开发时,定义的I18N Key
也不规范,会定义的很长很长,这些Key
在打包后,都会以字符串格式打包到js里,就会导致打完包的js里有大量国际化Key
格式的字符串,导致js很大,从而影响首屏效率。
// i18n/en/a.en.json
{
"I18!N_A_Key1": "value1",
"I18N_A_Key2": "value2",
"I18N_A_Key3": "value3"
}
// i18n/en/b.en.json
{
"I18N_B_Key1": "en b value1",
"I18N_B_Key2": "en b value2",
"I18N_B_Key3": "en b value3",
// 很多Key会定义的很长
"I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key0": "i18n en value 0",
"I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key_I18N_B_Key1": "i18n en value 1",
",
...数量也会很大
}
打包后会以json字符串形式编译在js里。
思路
想法是,在打包时通过一些命令,在编译阶段,把I18N key值转换成较短的字符串,比如a.en.json
里替换为a{自增序号int}
这种,保证了唯一性,字符串长度也会很短。
解决方案
编译js\jsx
打包用的是Webpack
,先研究下Webpack是怎么编译文件的,用到了各种loader以及babel,编译js和jsx用的是babel-loader
,这里就可以考虑写一个自定义的Babel plugin插件,通过解析AST,来转义词条Key字符串。
Babel 插件手册:github.com/jamiebuilds…
{
test: /\.(jsx|js)$/,
exclude: /(node_modules|bower_components)/,
use: 'babel-loader',
}
下面是一个简单plugin例子,把所有js\jsx文件中的所有字符串,都替换为new key
,所以这里需要加些判断,判断是I18N Key才需要转换。
// babel.config.js
module.exports = {
"presets": [
"@babel/preset-env",
[
"@babel/preset-react", { "runtime": "automatic" }
]
],
"plugins": [
["./my-babel-plugin.js"]
],
};
// my-babel-plugin.js
module.exports = function () {
return {
visitor: {
StringLiteral(path) {
// 把所有js\jsx文件中的所有字符串,都替换为'new key'
path.node.value = 'new key';
}
},
};
}
编译json
尝试把json也用上babel-loader
,但是不支持,babel只支持js系列文件的转换,webpack内部已经内置了json-loader
,不需要配置,所以得自己写一个loader来转换json格式文件。
Webpack官方自定义loader教程:webpack.docschina.org/contribute/…
尝试写了个例子测试了下,可以达到转换效果:
// webpack.config.js
module: {
rules: [
...
{
test: /\.(json)$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: path.resolve(__dirname, "./my-loader.js"),
}
},
// my-loader.js
module.exports = function (source) {
const json = typeof source === "string" ? JSON.parse(source) : source;
const result = {};
Object.entries(json).forEach(([k, v], i) => {
result['newkey' + i] = v;
});
return JSON.stringify(result);
};
上面例子就可以把json文件的对象属性名,都改成了newkey+{index}
格式了,从打包后的js里可以看出来。
实践
解决方案和技术方向已经研究好了,就可以开始实践了。
1. 加node脚本,把新旧Key的mapping关系确定好了
写一个node程序 scan-i18n-map.js
,用 fast-glob 读取i18n json文件,然后用文件名+序号
作为新key,最后把新旧Key mapping输出到i18n.map.json
文件里。
// scan-i18n-map.js
const fg = require('fast-glob');
const fs = require('fs');
const map = {};
// 读取所有en词条文件
const files = fg.sync('./src/i18n/en/*.en.json');
for (const file of files) {
const path = file; // 文件路径
const name = path.split('/').at(-1).split('.').at(0); // 取文件前缀,比如a.en.json取到的是a
const content = fs.readFileSync(file, 'utf-8'); // 读取内容
const json = JSON.parse(content); // 转成json对象
Object.entries(json).forEach(([k, v], i) => {
let key = name + i;
map[k] = key; // 把新旧key关系存到map上
});
};
// 最后写到json文件里
fs.writeFileSync('./i18n.map.json', JSON.stringify(map, null, 2), { encoding: 'utf8' }, (err) => {
if (err) throw err;
});
执行node scan-i18n-map.js
,生成i18n.map.json
文件:
2. 自定义webpack loader,编译i18n json文件,替换Key
重新自定义一个webpack loader,文件名叫i18n-loader.js
,然后里面读取上一步生成的i18n.map.json
,然后进行替换。
// i18n-loader.js
let map = null
// 只在prod build生效
if (process.env.NODE_ENV === "production") {
map = require('./i18n.map.json'); // 读取新旧key mapping关系
}
module.exports = function (source) {
if (!map) return source;
const json = typeof source === "string" ? JSON.parse(source) : source;
const result = {}; // 替换后的json
Object.entries(json).forEach(([k, v]) => {
let key = map[k]; // 新key
if (key) {
// 取到对应的新key,替换
result[key] = v;
}
});
return JSON.stringify(result); // 最后输出新json string
};
3. 自定义babel plugin,编译js\jsx文件,替换Key string
重新自定义一个babel plugin,文件名叫i18n-babel-plugin.js
,然后依然是读取mapping json,然后判断js\jsx里所有字符串是否完全匹配旧key,再替换成新key。
// i18n-babel-plugin.js
let map = null
// 只在prod build生效
if (process.env.NODE_ENV === "production") {
map = require('./i18n.map.json'); // 读取新旧key mapping关系
}
module.exports = function () {
if (!map) return {};
return {
visitor: {
// 解析字符串语法
StringLiteral(path) {
// 取到所有字符串
const value = path.node.value;
// 判断字符串是否是mapping里的旧key
const key = map[value];
if (key) {
// 取到对应的新key,替换
path.node.value = key;
}
}
},
};
}
4. 最后整合,执行
- 把生成mapping关系的node脚本
scan-i18n-map.js
添加到package.json
scripts pre 指令里,这样每次执行npm run prod
时会自动先执行脚本,生成mapping json文件。
// package.json
"scripts": {
"preprod": "node scan-i18n-map.js",
"prod": "cross-env NODE_ENV=production webpack --config webpack.config.js"
},
- 把webpack loader
i18n-loader.js
,加到webpack.config.js
里。
// webpack.config.js
test: /\.(json)$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: path.resolve(__dirname, "./i18n-loader.js"),
}
- 把babel plugin
i18n-babel-plugin.js
加到babel.config.js
里。
// babel.config.js
"plugins": [
["./i18n-babel-plugin.js"]
],
执行npm run prod
后查看打包文件:
可以看到国际化json、业务jsx编译后的文件里,i18n key都被替换了,
这里用到了webpack拆包,所以是两个js,感兴趣可以参考之前的文章:使用Top-level await新特性,按需加载国际化资源文件,提高首屏效率
对比了下前后文件size,肉眼可见提升。测试例子放了100个左右比较长的Key,如果真实项目,词条量多了就更可观了。
缺点及优化
也存在一些缺点,比如要求业务使用get词条时,词条key必须已完整字符串定义,不能有字符拼接。
也可以看看市面上比较成熟的国际化解决方案,比如 i18next formatjs,找找有没有类似需求的功能或扩展。
总结
本文介绍了如何通过自定义webpack loader以及babel plugin,实现自定义文件打包编译逻辑,用来替换国际化Key字符串,最终达到减小打包文件体积,提高首屏加载效率。
以后如果遇到类似的webpack打包编译需求,也可以参考这个路子来,做一些简单的替换字符串逻辑,还是很简单的。
转载自:https://juejin.cn/post/7233711648745652279