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

532 阅读2分钟

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

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

要求点:

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

需求分析:

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

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

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

难点分析:

  1. 难点1:内部10+平台都接入了国际化,我另一篇juejin.cn/post/697385… ,所以导致源代码内的中文并不是简单中文,都是带了$t的(用i18n库),比如

    <p>{{$t('请输入查询语句')}}</p>
    
    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国的这个分支内,配置去掉这个警告
        const i18n = new VueI18n({
          ...
          silentTranslationWarn: true // 关掉控制台的i18n警告
        })
        
  2. 难点2:一些第三方库也会被打包,第三方库的中文不一定清除干净了。比如iview的min版,里面也还有中文。比如一些自己写的组件库

    解决思路:看下面,和 难点3 一起解决

  3. 难点3:可能还会有遗漏的中文? 最终还需要把打包后的dist扫一遍

    解决思路:写一个webpack的plugin。

    1. 在打包的生命周期到了”emit阶段:生成资源到 output 目录之前“的这个时间点,去执行plugin(此时已经把注释都去掉了)。此时就能保证是扫描打包后的dist。
    2. 然后把中文的漏网之鱼给打印到终端(告诉开发者)。同时也可以配一个map,传入插件内,中文就会转换成对应value,或者不处理,就直接把中文变成空
      const map = {
          日: 'day',
          月: 'month',
      }
      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,依托于工具平台: 内部前端工具平台搭建

// 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

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