记录前端xx国部署,如何去掉源代码内的所有中文?
背景:由于政治问题,需要去xx国国内部署内部已有的平台(10+个)
要求点:
- 源代码内不能有跟国内本公司有关的所有关键字和图片,所有域名也要替换成xx国的
- 源代码内不能有中文
需求分析:
- 第一点好解决,分一个xx国的分支,代码内全局搜索,替换就完了
- 第二点人工替换的话,太笨了,成本也太高(有10+平台)
问题主要来到,如何去掉源代码内的所有中文?
- 首先用npm run build,打包出来的dist文件,能把所有的注释去掉(注释去掉是webpack可配置的,不是本文的重点,可自行查找配置方法)
- 此时,我们需要处理的就是,源代码内的除了注释之外的中文了。
难点分析:
- 难点1:内部10+平台都接入了国际化,参考文章【xxxx】,所以导致源代码内的中文并不是简单中文,都是带了$t的(用i18n库),比如
解决思路: ```{{$t(‘请输入查询语句’)}}
content: this.$t(‘sql不能为空’)
- 写一个webpack的loader,根据en.json,把所有的中文转成对应的英文,比如:$t('错误') 换成 $t('error')
// en.json是一个json串, key是中文,value是英文 { “错误”: “error”, “确定”: “confirm”, … }
- 另外,给i18n的配置中,把引入的zh.json和en.json等其他语言包都注释了。因为$t('abc') 没找到对应value时,会直接显示abc
- 不过此时在终端会有i18n的警告,因为$t(key)没找到对应的key-value,可以在xx国的这个分支内,配置去掉这个警告
```javascript
const i18n = new VueI18n({
...
silentTranslationWarn: true // 关掉控制台的i18n警告
})
- 难点2:一些第三方库也会被打包,第三方库的中文不一定清除干净了。比如iview的min版,里面也还有中文。比如一些自己写的组件库
解决思路:看下面,和 难点3 一起解决 - 难点3:可能还会有遗漏的中文? 最终还需要把打包后的dist扫一遍
解决思路:写一个webpack的plugin。- 在打包的生命周期到了”emit阶段:生成资源到 output 目录之前“的这个时间点,去执行plugin(此时已经把注释都去掉了)。此时就能保证是扫描打包后的dist。
- 然后把中文的漏网之鱼给打印到终端(告诉开发者)。同时也可以配一个map,传入插件内,中文就会转换成对应value,或者不处理,就直接把中文变成空
const map = {
日: 'day',
月: 'month',
}
new myWebpackPlugin(map)
为什么一下用loader,一下用plugin? 下面会有源代码,看完之后,发现他们的特点和区别之后,就能理解了
开始写loader和plugin:
- 写loader
- 写plugin
1. 写loader
写loader非常简单,就是写一个js。然后在webpack.config.js内以loader的规范引入。
- webpack会给loader内的回调函数传入一个source参数,最后你需要return这个source出去。函数内,可以随意处理source
先配置引入loader,依托于工具平台: 内部前端工具平台搭建
// webpack.config.js内
...
// 在production下,加入以下, 注意path参数,要是当前项目内的en.json,最好用动态路径(如果是线上打包)
if (env.NODE_ENV === 'production') {
baseConfig.module.rules.push({
test: /\.vue$/,
use: {
loader: '@myCompany/feTools/bin/lan/zhToEn-loader.js', // 需先安装 npm i -D @myCompany/feTools
options: {
// 注意path参数,要是当前项目内的en.json,最好用动态路径(如果是线上打包),请确认好自己的en.json的路径位置
path: path.resolve(__dirname, 'src/langs/en.json')
}
}
})
}
...
在写自己的loader: your-loader.js,并放在工具平台上(内部前端工具平台搭建)(本地开发调试,可以绝对路径引入)
- 放在工具平台是为了其他组员 可以轻松使用
// your-loader.js
// 帮助获取webpack.config.js 内传过来的option参数
const getOptions = require('loader-utils').getOptions
module.exports = function (source) {
const options = getOptions(this)
const path = options.path || 'src/langs/en.json'
const map = require(path) // 拿到en.json的map
// 把 $t('错误') => $t('error')
const reg = /\$t\(.*?\)/g
source = source.replace(reg, word => { // 把 $t('错误') => $t('error')
if (["'", '"'].includes(word[3])) {
const str = word
word = word.slice(4).slice(0, -2)
const val = map[word]
if (val) {
word = map[word]
return "$t('" + word + "')"
} else {
console.log('未找到对应的英文 或 不规范:', str)
}
} else {
return word
}
})
return source
}
另外:多个loader的执行顺序是:先执行后面的,从后到前
2. 写plugin
写loader会比较简单一点,因为loader已经把文件类型给过滤好了(比如:test: /.vue$/,)
写plugin要比loader复杂一些,plugin的api也要更强大一些,可以配置在不同的打包生命周期。
- 下面会稍微详细讲一下如何配置打包生命周期的钩子。
先在 webpack.config.js 内引入插件
...
// 这个是node_modules的路径引入文件(依托于工具平台(https://juejin.cn/post/6973845836891586591/)),需要先安装npm i -D @company/feTools
const YourPlugin = require('@company/feTools/bin/lan/your-plugin.js')
...
// 在production下的plugin处
if (env.NODE_ENV === 'production') {
...
const map = {
月: 'month',
日: 'day'
}
baseConfig.plugins = (baseConfig.plugins || []).concat([
...
new YourPlugin(map) // 可以加入传参,会根据key-value把扫描到的中文,替换成英文
])
}
...
开始写plugin
首先是写一个插件的主结构
// your-plugin.js 这是webpack插件的固有写法,可以先参考这写
class YourPlugin {
apply (compiler) {
compiler.hooks.emit.tapAsync('YourPlugin', (compilation, callback) => {
...
})
}
}
module.exports = YourPlugin
// 生命周期钩子函数,是由 compiler 暴露,可以通过如下方式访问:
compiler.hooks.someHook.tap(/* ... */);
分析上YourPlugin内的 compiler.hooks.emit.tapAsync()
tapAsync:是异步模式,tap是同步,还有tapPromise模式,细节可查看:https://github.com/webpack/tapable#hook-types
emit:就是生命周期的钩子位
还有一些生命周期 如:具体可以参考官网: https://v4.webpack.docschina.org/api/compiler-hooks/
entryOption
在 webpack 选项中的 entry 配置项 处理过之后,执行插件。
afterPlugins
设置完初始插件之后,执行插件。
参数:compiler
afterResolvers
resolver 安装完成之后,执行插件。
参数:compiler
environment
environment 准备好之后,执行插件。
afterEnvironment
environment 安装完成之后,执行插件。
beforeRun
compiler.run() 执行之前,添加一个钩子。
参数:compiler
...
// 另外,参数compilation 也有类似compiler的生命周期钩子,可以参考官网 https://v4.webpack.docschina.org/api/compilation-hooks
class YourPlugin {
apply (compiler) {
// make 可以触发 compilation.hooks
compiler.hooks.make.tapAsync('YourPlugin', (compilation, callback) => {
compilation.hooks.buildModule.tap('YourPlugin', module => {
console.log('module.resource', module.resource);
console.log('module.loaders', module.loaders);
console.time('YourPlugin');
});
compilation.hooks.succeedModule.tap('YourPlugin', module => {
console.timeEnd('YourPlugin');
});
callback(); // 配置了tapAsync,是异步的,才需要callback(),告知异步已经执行完了
});
}
}
module.exports = YourPlugin
上完整代码
/**
* 为什么这里不用loader? 因为要等到一些plugin (TerserPlugin 压缩, 去掉注释等等) 执行完在扫描
* @作用 扫描中文, 此处主要解决 js
* 另外 css 文件 用插件 CssMinimizerPlugin 去掉注释等等
*/
const path = require('path')
const obj = {
提示: 'prompt',
确定: 'confirm',
取消: 'cancel'
}
// 扫描出漏网之鱼的中文, 然后根据 mapMerge 对象, 用key-value去替换掉中文, 没有对应key的中文, 直接替换成空. 确保源代码没有1个中文
const replaceZh = (optionsMap={}, source, name) => {
const mapMerge = Object.assign(optionsMap, obj)
const reg = /[\u4e00-\u9fa5]{1,}/g
source = source && source.replace(reg, word => {
const val = mapMerge[word]
if (val) {
console.log(`${word}替换成${val}`)
return val
} else {
console.log(name, word)
return ''
}
})
return source
}
class YourPlugin {
constructor (options) {
this.options = options // 拿到给插件的传参
}
apply (compiler) {
// emit: 生成资源到 output 目录之前。
compiler.hooks.emit.tapAsync('YourPlugin', (compilation, callback) => {
const map = compilation.assets // 直接修改里面的值 就能生效
for (const name in map) {
if (['.js'].includes(path.extname(name))) {
// 直接修改map[name]._value, 影响输出的字符串
map[name]._value = replaceZh(this.options, map[name]._value, name)
}
}
callback()
})
}
}
module.exports = YourPlugin
码字不易,点点小赞鼓励~