记录前端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国的这个分支内,配置去掉这个警告```javascriptconst 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/feToolsoptions: {// 注意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').getOptionsmodule.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\(.*?\)/gsource = source.replace(reg, word => { // 把 $t('错误') => $t('error')if (["'", '"'].includes(word[3])) {const str = wordword = 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/feToolsconst 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-typesemit:就是生命周期的钩子位还有一些生命周期 如:具体可以参考官网: https://v4.webpack.docschina.org/api/compiler-hooks/entryOption在 webpack 选项中的 entry 配置项 处理过之后,执行插件。afterPlugins设置完初始插件之后,执行插件。参数:compilerafterResolversresolver 安装完成之后,执行插件。参数:compilerenvironmentenvironment 准备好之后,执行插件。afterEnvironmentenvironment 安装完成之后,执行插件。beforeRuncompiler.run() 执行之前,添加一个钩子。参数:compiler...// 另外,参数compilation 也有类似compiler的生命周期钩子,可以参考官网 https://v4.webpack.docschina.org/api/compilation-hooksclass YourPlugin {apply (compiler) {// make 可以触发 compilation.hookscompiler.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,}/gsource = 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
码字不易,点点小赞鼓励~
