记录前端xx国部署,如何去掉源代码内的所有中文?

背景:由于政治问题,需要去xx国国内部署内部已有的平台(10+个)

要求点:

  1. 源代码内不能有跟国内本公司有关的所有关键字和图片,所有域名也要替换成xx国的
  2. 源代码内不能有中文

需求分析:

  1. 第一点好解决,分一个xx国的分支,代码内全局搜索,替换就完了
  2. 第二点人工替换的话,太笨了,成本也太高(有10+平台)

问题主要来到,如何去掉源代码内的所有中文?

  1. 首先用npm run build,打包出来的dist文件,能把所有的注释去掉(注释去掉是webpack可配置的,不是本文的重点,可自行查找配置方法)
  2. 此时,我们需要处理的就是,源代码内的除了注释之外的中文了。

难点分析:

  1. 难点1:内部10+平台都接入了国际化,参考文章【xxxx】,所以导致源代码内的中文并不是简单中文,都是带了$t的(用i18n库),比如
    解决思路: ```

    {{$t(‘请输入查询语句’)}}

content: this.$t(‘sql不能为空’)

  1. - 写一个webpackloader,根据en.json,把所有的中文转成对应的英文,比如:$t('错误') 换成 $t('error')

// en.json是一个json串, key是中文,value是英文 { “错误”: “error”, “确定”: “confirm”, … }

  1. - 另外,给i18n的配置中,把引入的zh.jsonen.json等其他语言包都注释了。因为$t('abc') 没找到对应value时,会直接显示abc
  2. - 不过此时在终端会有i18n的警告,因为$t(key)没找到对应的key-value,可以在xx国的这个分支内,配置去掉这个警告
  3. ```javascript
  4. const i18n = new VueI18n({
  5. ...
  6. silentTranslationWarn: true // 关掉控制台的i18n警告
  7. })
  1. 难点2:一些第三方库也会被打包,第三方库的中文不一定清除干净了。比如iview的min版,里面也还有中文。比如一些自己写的组件库
    解决思路:看下面,和 难点3 一起解决
  2. 难点3:可能还会有遗漏的中文? 最终还需要把打包后的dist扫一遍
    解决思路:写一个webpack的plugin。
    1. 在打包的生命周期到了”emit阶段:生成资源到 output 目录之前“的这个时间点,去执行plugin(此时已经把注释都去掉了)。此时就能保证是扫描打包后的dist。
    2. 然后把中文的漏网之鱼给打印到终端(告诉开发者)。同时也可以配一个map,传入插件内,中文就会转换成对应value,或者不处理,就直接把中文变成空
      1. const map = {
      2. 日: 'day',
      3. 月: 'month',
      4. }
      5. new myWebpackPlugin(map)

      为什么一下用loader,一下用plugin? 下面会有源代码,看完之后,发现他们的特点和区别之后,就能理解了

开始写loader和plugin:

  1. 写loader
  2. 写plugin

1. 写loader

写loader非常简单,就是写一个js。然后在webpack.config.js内以loader的规范引入。

  • webpack会给loader内的回调函数传入一个source参数,最后你需要return这个source出去。函数内,可以随意处理source

先配置引入loader,依托于工具平台: 内部前端工具平台搭建

  1. // webpack.config.js内
  2. ...
  3. // 在production下,加入以下, 注意path参数,要是当前项目内的en.json,最好用动态路径(如果是线上打包)
  4. if (env.NODE_ENV === 'production') {
  5. baseConfig.module.rules.push({
  6. test: /\.vue$/,
  7. use: {
  8. loader: '@myCompany/feTools/bin/lan/zhToEn-loader.js', // 需先安装 npm i -D @myCompany/feTools
  9. options: {
  10. // 注意path参数,要是当前项目内的en.json,最好用动态路径(如果是线上打包),请确认好自己的en.json的路径位置
  11. path: path.resolve(__dirname, 'src/langs/en.json')
  12. }
  13. }
  14. })
  15. }
  16. ...

在写自己的loader: your-loader.js,并放在工具平台上(内部前端工具平台搭建)(本地开发调试,可以绝对路径引入)

  • 放在工具平台是为了其他组员 可以轻松使用
  1. // your-loader.js
  2. // 帮助获取webpack.config.js 内传过来的option参数
  3. const getOptions = require('loader-utils').getOptions
  4. module.exports = function (source) {
  5. const options = getOptions(this)
  6. const path = options.path || 'src/langs/en.json'
  7. const map = require(path) // 拿到en.json的map
  8. // 把 $t('错误') => $t('error')
  9. const reg = /\$t\(.*?\)/g
  10. source = source.replace(reg, word => { // 把 $t('错误') => $t('error')
  11. if (["'", '"'].includes(word[3])) {
  12. const str = word
  13. word = word.slice(4).slice(0, -2)
  14. const val = map[word]
  15. if (val) {
  16. word = map[word]
  17. return "$t('" + word + "')"
  18. } else {
  19. console.log('未找到对应的英文 或 不规范:', str)
  20. }
  21. } else {
  22. return word
  23. }
  24. })
  25. return source
  26. }

另外:多个loader的执行顺序是:先执行后面的,从后到前

2. 写plugin

写loader会比较简单一点,因为loader已经把文件类型给过滤好了(比如:test: /.vue$/,)

写plugin要比loader复杂一些,plugin的api也要更强大一些,可以配置在不同的打包生命周期。

  • 下面会稍微详细讲一下如何配置打包生命周期的钩子。

先在 webpack.config.js 内引入插件

  1. ...
  2. // 这个是node_modules的路径引入文件(依托于工具平台(https://juejin.cn/post/6973845836891586591/)),需要先安装npm i -D @company/feTools
  3. const YourPlugin = require('@company/feTools/bin/lan/your-plugin.js')
  4. ...
  5. // 在production下的plugin处
  6. if (env.NODE_ENV === 'production') {
  7. ...
  8. const map = {
  9. 月: 'month',
  10. 日: 'day'
  11. }
  12. baseConfig.plugins = (baseConfig.plugins || []).concat([
  13. ...
  14. new YourPlugin(map) // 可以加入传参,会根据key-value把扫描到的中文,替换成英文
  15. ])
  16. }
  17. ...

开始写plugin

首先是写一个插件的主结构

  1. // your-plugin.js 这是webpack插件的固有写法,可以先参考这写
  2. class YourPlugin {
  3. apply (compiler) {
  4. compiler.hooks.emit.tapAsync('YourPlugin', (compilation, callback) => {
  5. ...
  6. })
  7. }
  8. }
  9. module.exports = YourPlugin
  10. // 生命周期钩子函数,是由 compiler 暴露,可以通过如下方式访问:
  11. compiler.hooks.someHook.tap(/* ... */);
  12. 分析上YourPlugin内的 compiler.hooks.emit.tapAsync()
  13. tapAsync:是异步模式,tap是同步,还有tapPromise模式,细节可查看:https://github.com/webpack/tapable#hook-types
  14. emit:就是生命周期的钩子位
  15. 还有一些生命周期 如:具体可以参考官网: https://v4.webpack.docschina.org/api/compiler-hooks/
  16. entryOption
  17. webpack 选项中的 entry 配置项 处理过之后,执行插件。
  18. afterPlugins
  19. 设置完初始插件之后,执行插件。
  20. 参数:compiler
  21. afterResolvers
  22. resolver 安装完成之后,执行插件。
  23. 参数:compiler
  24. environment
  25. environment 准备好之后,执行插件。
  26. afterEnvironment
  27. environment 安装完成之后,执行插件。
  28. beforeRun
  29. compiler.run() 执行之前,添加一个钩子。
  30. 参数:compiler
  31. ...
  32. // 另外,参数compilation 也有类似compiler的生命周期钩子,可以参考官网 https://v4.webpack.docschina.org/api/compilation-hooks
  33. class YourPlugin {
  34. apply (compiler) {
  35. // make 可以触发 compilation.hooks
  36. compiler.hooks.make.tapAsync('YourPlugin', (compilation, callback) => {
  37. compilation.hooks.buildModule.tap('YourPlugin', module => {
  38. console.log('module.resource', module.resource);
  39. console.log('module.loaders', module.loaders);
  40. console.time('YourPlugin');
  41. });
  42. compilation.hooks.succeedModule.tap('YourPlugin', module => {
  43. console.timeEnd('YourPlugin');
  44. });
  45. callback(); // 配置了tapAsync,是异步的,才需要callback(),告知异步已经执行完了
  46. });
  47. }
  48. }
  49. module.exports = YourPlugin

上完整代码

  1. /**
  2. * 为什么这里不用loader? 因为要等到一些plugin (TerserPlugin 压缩, 去掉注释等等) 执行完在扫描
  3. * @作用 扫描中文, 此处主要解决 js
  4. * 另外 css 文件 用插件 CssMinimizerPlugin 去掉注释等等
  5. */
  6. const path = require('path')
  7. const obj = {
  8. 提示: 'prompt',
  9. 确定: 'confirm',
  10. 取消: 'cancel'
  11. }
  12. // 扫描出漏网之鱼的中文, 然后根据 mapMerge 对象, 用key-value去替换掉中文, 没有对应key的中文, 直接替换成空. 确保源代码没有1个中文
  13. const replaceZh = (optionsMap={}, source, name) => {
  14. const mapMerge = Object.assign(optionsMap, obj)
  15. const reg = /[\u4e00-\u9fa5]{1,}/g
  16. source = source && source.replace(reg, word => {
  17. const val = mapMerge[word]
  18. if (val) {
  19. console.log(`${word}替换成${val}`)
  20. return val
  21. } else {
  22. console.log(name, word)
  23. return ''
  24. }
  25. })
  26. return source
  27. }
  28. class YourPlugin {
  29. constructor (options) {
  30. this.options = options // 拿到给插件的传参
  31. }
  32. apply (compiler) {
  33. // emit: 生成资源到 output 目录之前。
  34. compiler.hooks.emit.tapAsync('YourPlugin', (compilation, callback) => {
  35. const map = compilation.assets // 直接修改里面的值 就能生效
  36. for (const name in map) {
  37. if (['.js'].includes(path.extname(name))) {
  38. // 直接修改map[name]._value, 影响输出的字符串
  39. map[name]._value = replaceZh(this.options, map[name]._value, name)
  40. }
  41. }
  42. callback()
  43. })
  44. }
  45. }
  46. module.exports = YourPlugin

码字不易,点点小赞鼓励~