一不小心就写了一个webpack plugin

1,708 阅读9分钟

业务背景

嗯。。。事情是这样的。 前段时间新启动的一个紧急项目经过我们小而美的团队不懈的努力,终于顺利上线了。后面review代码的时候发现这样的目录结构

container里面都是react的容器组件。定睛一看咋这么多。其实还没完呢,其中Doctor,Dss,Operator,Patient,RegionManager以及Seller都是项目中的角色,每个角色看到的页面是不一样的,就意味着每个角色对应的文件夹里面还有其他容器组件,于是看到的router的文件就是这样的

真是令人窒息,一屏还不能截全。

这时候拒绝复制粘贴的前端开发们就要偷懒了,我可不想开发的时候每新增一个container就要在router文件中更新一次路由,这不得烦死啊。于是我就写了一个基本可以覆盖大部分开发场景的webpack插件 -- RouteFromContainerPlugin,还可以支持懒加载哦。

plugin功能简介

RouteFromContainerPlugin负责的核心功能就是读取项目中的container文件夹(读取路径可以通过参数配置),BFS遍历文件夹下面的所有文件,直到找到后缀为jsx或者tsx的文件,然后记住他的遍历路径,就是该容器组件的文件路径。然后就可以顺利的写入我们的router文件了。

遍历图中container文件夹下面的所有容器组件,将生成的内容通过提前定义好的模板一起填充到App.jsx中。

这就是最终生成的App.jsx文件内容

同时插件支持配置懒加载和需要匹配的容器组件的文件夹路径(默认就是./src/container)

使用起来就是这样的,在webpack.config.js文件中配置plugins:

plugins: [
  new RouteFromContainerPlugin({
    lazy: true // 使用懒加载,
    containerFolder: './src/container' // 默认值
  })
]

如上,一个满足日常开发需求的webpack插件就完成了。下面我们就一起来看看如何一步步的去写webpack插件。

如何开发一个Plugin

虽然 webpack 的官方文档写的不咋地,但是不得不说 plugin 的开发真是超级方便。

webpack的打包本质上是一种事件流的机制,它的原理是将各个插件串联起来,而实现这一切的核心是tapable。并且在webpack中负责编译的Compiler和负责创建bundles的Compilation都是tapable构造函数的实列。

想深入了解tapable的同学可以移步 这篇文章

根据官方文档的说法,大家把下面的模板代码复制到你的开发文件中,你就完成了50%的工作,哈哈哈哈。

class XXXPlugin { // 定义插件名
  constructor(options) {
    this.options = options; // 通过options传递参数
  }
  apply(compiler) { // 必须定义这个方法,webpack执行插件的时候会依次执行所有插件的apply函数
    compiler.hooks.任何生命周期.tap( // 在你需要处理的生命周期中处理插件逻辑
      "XXXPlugin", // 绑定的插件名
      (compiler或者compilation, callback) => {
        // blablabla 业务逻辑
        callback(); // 如果是异步的生命周期需要执行callback函数
      }
    );
  }
}

上面提到了两个相似的名字,Compiler 和 Compilation。而理解这两个概念是开发完美插件的关键。

Compiler 和 Compilation

compiler 对象包含了 Webpack 环境所有的配置信息,包含options,loaders, plugins这些项,这个对象在webpack启动时候被实例化,它是全局唯一的。我们可以把它理解为webpack的实列。

compilation 对象包含了当前的模块资源、编译生成资源、文件的变化等。当webpack在开发模式下运行时,每当检测到一个文件发生改变的时候,那么一次新的 Compilation将会被创建。从而生成一组新的编译资源。

他们两个最大的区别是:Compiler代表了是整个webpack从启动到关闭的生命周期。Compilation 对象只代表了一次新的编译。

webpack在运行的过程中通过观察者模式会广播事件,插件只需要关心自己监听的生命周期事件,就可以执行插件的功能。

因此,前面的模板中“任何生命周期”就是插件需要关心的那个时间节点。在webpack触发该时间节点的时候,就会执行插件相应的逻辑。

官方文档中定义了所有的生命周期,以及该生命周期是同步还是异步(具体 tapable 中有定义同步异步的问题),回调函数的参数等。

具体回调函数中的参数我在官方文档中还没有找到有相关的介绍,开发的时候都是自己摸索着在命令行中一个个打印看的。哈哈哈,有哪位同学知道更好的办法可以指教。

开发插件

有了上面的背景介绍以及模板代码和相关知识,我们现在就一起来一步步实现自动生成 app.jsx 文件的需求。

  • 摆上我们的模板代码

      class XXXPlugin { // 定义插件名
        constructor(options) {
          this.options = options; // 通过options传递参数
        }
        apply(compiler) { // 必须定义这个方法,webpack执行插件的时候会依次执行所有插件的apply函数
          compiler.hooks.任何生命周期.tap( // 在你需要处理的生命周期中处理插件逻辑
            "XXXPlugin", // 绑定的插件名
            (compiler或者compilation, callback) => {
              // blablabla 业务逻辑
              callback(); // 如果是异步的生命周期需要执行callback函数
            }
          );
        }
      }
    
  • 修改一些变量名

      class RouteFromContainerPlugin { // 定义插件名
        constructor(options) {
          // 通过options传递参数。我们暂时定义用户可以配置容器组件的文件夹路径。以及是否支持按需加载
          // options = options || {
          //	containerFolderPath: path.resolve("./src/container"), // 默认从.src/container文件夹开始遍历
          //    lazy: false // 默认不支持按需加载
          // }
          this.options = options;
        }
        apply(compiler) { 
          compiler.hooks.watchRun.tap( // 找到对应的生命周期。每次容器文件有变动就去重新生成app.jsx。因此应该监听开发阶段的重新编译阶段,根据文档找到该阶段为watchRun
            "RouteFromContainerPlugin", // 绑定的插件名
            (compiler, callback) => { // 根据文档,watchRun阶段的回调函数参数为compiler和callback
              // blablabla 业务逻辑
              callback(); // 如果是异步的生命周期需要执行callback函数
            }
          );
        }
      }
    
  • 接下来就可以写业务逻辑了

    • 首先根据 containerFolderPath bfs遍历查找所有文件后缀为jsx或者tsx的文件,记录下他们的文件路径,并保存。

    • 根据我们提前定义好的模板写入app.jsx文件中

    class RouteFromContainerPlugin { // 定义插件名
      constructor(options) {
      	// 通过options传递参数。我们暂时定义用户可以配置容器组件的文件夹路径。以及是否支持按需加载
      	// options = options || {
        //	containerFolderPath: path.resolve("./src/container"), // 默认从.src/container文件夹开始遍历
        //    lazy: false // 默认不支持按需加载
        // }
        this.options = options;
      }
      apply(compiler) { 
        compiler.hooks.watchRun.tap( // 找到对应的生命周期。每次容器文件有变动就去重新生成app.jsx。因此应该监听开发阶段的重新编译阶段,根据文档找到该阶段为watchRun
          "RouteFromContainerPlugin", // 绑定的插件名
          (compiler, callback) => { // 根据文档,watchRun阶段的回调函数参数为compiler和callback
            const routePathList = findAllDir(containerFolder); // 根据入口地址遍历得到所有容器组件的路径
            writeAppFile(appPath, routePathList, this.options.lazy); // 根据appPath路径生成app.jsx文件,将routePathList 写入我们提前定义好的模板中
            callback(); // 执行回调函数
          }
        );
      }
    }

现在将我们的 plugin 加入 webpack 中试一下。npm start 走起。

  // webpack.config.js
  plugins: [
      new RouteFromContainerPlugin({
        lazy: true
      })
  ]

boom ~~~ 停不下来了。一直有信息在重复打印。我们赶快ctrl + c 停止运行。去看看代码哪里出了问题。 仔细分析代码我们发现,npm start 之后通过container文件确实找到了所有的容器组件,也确实重写了app.jsx文件。问题就在这里,重写了app.jsx之后是不是又触发了 watchRun 这个阶段,然后就又是一次重复上述过程,所以才会造成死循环的结果。 找到问题后我们就可以修改代码逻辑了,只需要加一次小小的判断。

    class RouteFromContainerPlugin { // 定义插件名
      constructor(options) {
      	// 通过options传递参数。我们暂时定义用户可以配置容器组件的文件夹路径。以及是否支持按需加载
      	// options = options || {
        //	containerFolderPath: path.resolve("./src/container"), // 默认从.src/container文件夹开始遍历
        //    lazy: false // 默认不支持按需加载
        // }
        this.options = options;
        this.lastRoutePathList = []; // 添加变量,表示上一次遍历得到的容器组件的路径数组
      }
      apply(compiler) { 
        compiler.hooks.watchRun.tap( // 找到对应的生命周期。每次容器文件有变动就去重新生成app.jsx。因此应该监听开发阶段的重新编译阶段,根据文档找到该阶段为watchRun
          "RouteFromContainerPlugin", // 绑定的插件名
          (compiler, callback) => { // 根据文档,watchRun阶段的回调函数参数为compiler和callback
            const routePathList = findAllDir(containerFolder); // 根据入口地址遍历得到所有容器组件的路径
            if (!isEqual(this.lastRoutePathList, routePathList)) {  // 判断这次生成的路径list和上一次是否相同
              writeAppFile(appPath, routePathList, this.options.lazy); // 根据appPath路径生成app.jsx文件,将routePathList 写入我们提前定义好的模板中
              this.lastRoutePathList = routePathList; // 更新lastRoutePathList
            }
            callback(); // 执行回调函数
          }
        );
      }
    }

这样一个功能完整的webpack plugin插件就写好啦。全部代码还请各位同学移步 github 查看。欢迎大家多多提 issue 。

总结

其实刚开始做前端工作的时候就听前辈说过一句话:“懒得人往往技术会相对好一些”,刚开始可能并没有很理解这句话的含义,最近几年网上总是有各种声音说前端天花板低,对于业务的参与度不够,抱怨工作没什么挑战。。。等等。结合前辈说的那句话,我觉得对于技术的追求就是一个主观能动性的问题,当你觉得某项任务不够智能、重复率高的时候有的人可能就在写一个工具来帮助他,而你却一边在抱怨技术没有成长一边做着复制粘贴的工作。

所以,当我们大家都开始认真“思考”每天的工作,认真“打磨”每一个feature的时候,就是突破天花板的过程~~~