从被测试大佬鄙视出发,封装一个wepback插件

495 阅读5分钟

前言

前段时间上线活动,测试大佬经常找我,我说发生肾么事了?
原来是出现了好几次前端 mock 数据带到测试环境的问题,使得测试大佬仇恨值++,差点要来给我一套化挥发闪电五连鞭.
每次build的时候,心里都很慌,生怕手写的mock数据带到测试环境.
我寻思,作为一个程序猿,这种事我应该耗子尾汁,不应该每次都手动的去检查这个问题,所以也一直在思考怎么去解决这个问题. 经过多方面咨询和自己的思考,最终得到两个比较好的思路:

  1. 通过严谨,高度的封装,将所有的请求封装在一起,然后统一在请求层面去配置mock=true ,使用mock接口,build的时候,强制所有的接口都自动使用正式数据.

  2. mock数据处,手动增加约定的特殊注释,然后写一个webpack插件在build的时候去检测是否存在这种注释,如果存在,则直接抛出异常禁止打包.

两者对比下来,其实方法一更好些,因为不需要额外去搞webpack插件,而且代码写起来很好看.
但是我还是采用的方法二.因为我这边业务不太合适集中把接口封装起来.而且我还有点小私心,想熟悉下wepback的插件开发.

理解webpack工作流程

翻了很多资料,搞懂了webpack的大致的工作流程,分享下比较精髓的一段话:

下面这段引用自Webpack 从零入门到工程化实战 26章

Webpack 可以看做是一个工厂车间, pluginloader是车间中的两类机器,工厂有一个车间主任和一个生产车间。车间主任叫Compiler,负责指挥生产车间机器Compilation进行生产劳动,Compilation会首先将进来的原材料(entry)使用一种叫做loader的机器进行加工,生产出来的产品就是ChunkChunk生产出来之后,会被组装成Bundle,然后通过一类plugin的机器继续加工,得到最后的Bundle,然后运输到对应的仓库(output)。这个工厂的生产线就是 Tapable,厂子运作的整个流程都是生产线控制的,车间中有好几条生产线,每个生产线有很多的操作步骤(hook),一步操作完毕,会进入到下一步操作,直到生产线全流程完成,再将产出传给下一个产品线处理。整个车间生产线也组成了一条最大的生产线。

初步尝试

通过上边的理解,我大概有了思路,我要做的事是在build前拿到由代码组成的字符串,然后对这个字符串通过正则进行遍历,如果查出了约定好的特殊mock注释,则抛出警告打断build,于是写出以下代码:

//webpack.config.js
// 使用插件
const WebpackMocWarnkPlugin = require('webpack-mock-warn')
module.exports = {
  // ...
  plugins: [
    new WebpackMocWarnkPlugin(),
  ],
}
// webpackMocWarnkPlugin/index.js 插件代码

module.exports = class WebpackMockWarnPlugin {
  apply(compiler) {
    compiler.plugin('emit', (compilation, callback) => {
      // compilation.chunks存放着所有的代码块
      compilation.chunks.forEach((chucnk) => {
        const chunkCode = chunk.entryModule._source._value
        //.... 对chunkCode进行遍历匹配
      })
    })
  }
}

但是这样写发现webpack警告说compiler.plugin这个api已经不建议使用,需要更换api了.

再次尝试

上边webpack工作流程中已经说了,现在(webpack4)应该先去使用compiler合适的钩子,拿到compilation,然后再去用compilation中合适的钩子去拿到代码块,钩子这块还挺绕的,看了好久资料,最后还是求助大佬才解决了疑惑.于是又有写出了以下代码:

// webpackMocWarnkPlugin/index.js 

module.exports = class WebpackMockWarnPlugin {
  apply(compiler) {

    // compilation 这个钩子表示compilation创建成功之后的回调,参数就是热乎乎的compilation
    compiler.hooks.compilation.tap('webpackMockWarnPlugin', (compilation) => {
      compilation.hooks.afterChunks.tap('af', (chunks) => {
        this.testChunk(chunks) // 遍历处理chunks的代码
      })
    })
    // 完成编译和封存编译产出之后的回调
    compiler.hooks.afterCompile.tap('afcompile', this.throwWarn) //如果发现存在特殊标记则抛出错误
  }
}

解释下上边使用的compiler.hooks.afterCompile,因为可能会存在多个特殊标记,所以这里需要等所有的代码块都处理完,才能拿到所有的结果,然后在这个钩子里去判断,一起抛出

进行优化

主要问题大致解决了,剩余一些边边角角,尽量的去优化一下,贴出最后实现的代码:

// webpackMocWarnkPlugin/index.js 
const { red, cyan, yellow } = require('colorette') //这个插件是用来在控制台上输出多种颜色代码的,这个也是看wepbakc-cli的源码发现的 还是挺好玩的
const wanrns = []
module.exports = class WebpackMockWarnPlugin {

  // 这里允许使用者去自定义特殊标记 或者直接去自己写正则
  constructor(options = {}) {
    this.mockReg = options.mockReg
    this.mockFlag = options.mockFlag || 'mock'
  }
  apply(compiler) {
    compiler.hooks.compilation.tap('webpackMockWarnPlugin', (compilation) => {
      compilation.hooks.afterChunks.tap('af', (chunks) => {
        this.testChunk(chunks)
      })
    })
    compiler.hooks.afterCompile.tap('afcompile', this.throwWarn)
  }
  testChunk(chunks){
    chunks.forEach((chunk) => {
      const reg =
        this.mockReg ||
        new RegExp(`(\/\*)\s*${this.mockFlag}|\/\/ *${this.mockFlag}`, 'g')
      const chunkCode = chunk.entryModule._source._value
      // 先生成行数数组.每个元素的index表示当前行数,每个元素的index表示所在行,内容是所在的index
      const rows = [0]
      for (let i = 0; i < chunkCode.length; i++) {
        if (chunkCode[i] === '\n') rows.push(i)
      }
      var regExec = null
      while ((regExec = reg.exec(chunkCode)) !== null) {
        // 若匹配到mock,则取出行数 / 上一行,下三行之内的代码方便检阅
        if (regExec[0]) {
          // 循环遍历所在行数
          for (let index in rows) {
            if (rows[index] >= regExec.index) {
              let content = ''
              let contentWrap = 5
              let endIndex = chunkCode.length
              let startIndex = 0
              for (let i = regExec.index; i >= 0; i--) {
                if (chunkCode[i] === '\n') {
                  startIndex = i
                  break
                }
              }
              for (let i = regExec.index; i <= chunkCode.length - 1; i++) {
                if (contentWrap === 0) {
                  endIndex = i
                  break
                }
                if (chunkCode[i] === '\n') {
                  contentWrap--
                }
              }
              content = chunkCode.slice(startIndex, endIndex)
              wanrns.push({
                row: index || 1,
                path: chunk.entryModule._source._name,
                content,
              })
              break
            }
          }
        }
      }
    })
  }
  throwWarn(){
    if (wanrns.length !== 0) {
      let errorStr = `${red(
        `检测到存在${wanrns.length}处mock数据,请删除后再次尝试操作:
        如果你想使用自定义捕获mock标记,请配置 mockFlag , 也可以配置 mockReg 自定义捕获mock正则
        `
      )}\n`
      for (let el of wanrns) {
        errorStr += yellow(
          `path: ${el.path}\nrow: ${el.row}\ncontent: ${cyan(el.content)}\n\n`
        )       
      }
      console.error(red(errorStr))
      process.exit(1);//结束进程抛出错误
    }
  }
}

发布至npm

修改下pagejson:

{
  "name": "xxx",
  "version": "1.0.0",
  "description": "When build, it detects the mock data and throws a warning",
  "main": "./src/index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "publishConfig": {
    "registry": "http://registry.npmjs.org"
  },
  "keywords": [
    "webpack",
    "mock"
  ],
  "author": "xxx <xxxx.com>",
  "license": "ISC",
  "dependencies": {
    "colorette": "^1.2.1",
    "webpack-cli": "^4.2.0"
  }
}

再编辑下readme文件
npm publish
大功告成!

结语

其实到最后,还遗留了2个问题:

  1. 现在最后是调用nodeprocess.exit(1)直接在命令行抛出错误,这很不优雅,很难被称为warn,我其实理想的是直接终止掉webpack的打包而不抛出错误,但是没找到好的解决方案.
  2. html/css注释进行适配,这个暂时没有需求,所以没有费力去搞,而且中间会涉及到loader转义后的html/css,而不是编译前的,应该也要费一番力气.

这个小东西也花费了我好几天的摸鱼时间,开发过程中遇到的问题,比上边描述的要多得多.

我个人研究东西总是要有一个源头,就是说我要先知道我想干嘛,然后再去由这个进一步去研究.如果直接去学习某种技术,总是会迷失.从业务问题出发,到开发webpack插件去解决问题.这种体验还是挺好的.

当然,以小见大,理解了webpack的工作流程,也算有所收获.

深刻感觉到了,提出一个好问题,或许比解决这个问题更重要.

我是菜菜驴,江湖人称驴渣.感谢你阅读我的分享!