业务背景
嗯。。。事情是这样的。
前段时间新启动的一个紧急项目经过我们小而美的团队不懈的努力,终于顺利上线了。后面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的时候,就是突破天花板的过程~~~