返回 登录
0

浅析Webpack插件化设计

阅读6133

本文为原创投稿文章,未经允许,请勿转载。
作者:郭祥,饿了么前端工程师,Node.js、PHP开发者,目前参与饿了么外卖平台移动端业务,业余研究Hybrid方案,Docker及服务器运维,崇尚用代码去解决生活中的一切问题。
责编:陈秋歌,寻求报道或者投稿请发邮件至chenqg#csdn.net,或加微信:Rachel_qg。
了解更多前沿技术资讯,获取深度技术文章推荐,请关注CSDN研发频道微博

文章摘要:在事件机制驱动下,通过良好的API约定,简洁的插件系统设计,Webpack在完成自身繁重的构建任务同时,还提供了良好的拓展性及可测试性。本文以Webpack的2.3.3版本为例进行演示,带读者走进 Webpack的本体设计中去,从宏观的角度观察其内部的运行和实现。

前言

在我们之前写的《从Bundle文件看Webpack模块机制》一文中,可以了解到经过Webpack构建之后的代码是如何工作的,学习到其模块化的实现原理,本文将带大家走进Webpack的本体设计中去,从宏观的角度观察其内部的运行和实现。

Webpack代码较为复杂,其在内部高度使用事件通信和插件化的机制来实现代码解耦及工作流程的控制。本文将以Webpack的2.3.3版本为例进行相关演示。

事件系统

说起事件,自然少不了发布/订阅模式,Webpack的基础组件之一Tapable是为其量身定做的“EventEmitter”,但它不只是单纯的事件中枢,还相应补充了对事件流程的控制能力,增加了如waterfall/series/parallel系列方法,实现了同步/异步、顺序/并行等事件流的控制能力。

图1截选了实例的部分API,具体可参阅源码:tapable/Tapable.js at master · webpack/tapable · GitHub,仅有三百余行。

总体来说,可分为三类方法:

  • apply提供给插件的注册使用。
  • plugins注册事件监听,接受事件名称和对应的回调函数。
  • applyPlugins[xx]系列方法用于emit事件。类似applyPlugins0、applyPlugins1……这样后面的数字用来限制对应事件函数形参个数,类似参数限制的声明保证了接口声明的一致性。

事件注册相关源码如下,注册的事件维护在_plugins对象中。

Tapable.prototype.plugin = function plugin(name, fn) {
    if(Array.isArray(name)) {
        name.forEach(function(name) {
            this.plugin(name, fn);
        }, this);
        return;
    }
    if(!this._plugins[name]) this._plugins[name] = [fn];
    else this._plugins[name].push(fn);
};

Tapable.prototype.apply = function apply() {
    for(var i = 0; i < arguments.length; i++) {
        arguments[i].apply(this);
    }
};

Webpack的核心对象compiler/compilation都是Tapable的子类,各自分管自己的plugins。由compiler提供的Event Hook往往也对应着Bundle过程的各生命周期,从中可以获取到对应阶段的compilation对象引用,compilation是主要的执行者,在相应周期中负责各项子任务,并触发更细粒度的事件,同时保持着对处理结果的引用。对于开发者编码主要集中在compilation hook的处理上,用于捕获事件结果进行二次改造。

插件化设计

Webpack的插件与事件是紧密相连的,插件的设计让代码高度职能化,事件如同丝线连接,完成插件与主体(主要为compiler和compilation)间的流程和数据的协作。

插件定义

插件的接口也是非常简单,仅需要实现一个apply方法,这与Tapable的apply方法相对应,相关文档可参考官方的《How to write a plugin?》部分。

我们每天都在使用各种Webpack Plugins完成项目的构建,有时也需要自己为项目量身定制。Webpack内部插件与这些日常使用插件完全相同,不仅遵循一致的API设计,也共享相同的事件发布者。也就是说,我们可以通过外部插件触及到Bundle过程中的每个过程,高度的拓展性也是Webpack社区繁荣的基础之一。

插件注册
插件注册的实质是插件内部事件的注册。日常使用中,我们将插件实例化以后声明在配置的plugins数组中,由compiler的apply方法接管插件实例的注册:

if(options.plugins && Array.isArray(options.plugins)) {
    compiler.apply.apply(compiler, options.plugins);
}

compiler.apply顺序调用各插件的apply方法并传入compilation运行时对象作为唯一的参数,方法内部调用compiler/compilation的plugin方法完成事件监听的注册。

执行实例

下面我们选取Webpack模块热更新这一过程为例,了解一下事件与插件系统在开发实践中的应用与表现。

要开启热更新,需要在配置对象中声明webpack.HotModuleReplacementPlugin()的实例。可以发现,热更新功能同样作为一个内置插件拓展在本体中,相应文件位于 /lib/HotModuleReplacementPlugin.js。Webpack watch文件变动后触发一轮新的buildModule,生成chunk后再次调用compilation.createChunkAssets方法更新assets对象,完成后触发“chunk-assets”事件,紧跟着会触发“additional-chunk-assets”事件,目前源
码中仅有HotModuleReplacementPlugin监听“additional-chunk-assets”事件,在此代码执行权转移给插件。在插件内部,通过compilation对象获取到本轮build的chunk信息,筛选出更新和移除的module交由Template对象生成hot-update.js源码作为新的chunk加入到assets中。

值得一提的是,在系统设计考量上,Webpack并不只局限于满足自身的实现,还尽可能站在系统拓展的角度把控。如前文所述的“chunk-assets”事件其自身并没有注册相应的回调函数,但仍然保留这一事件的触发,传递当前阶段chunk对象的引用和对应的chunk文件名,有需求的开发者可以通过这个hook对assets进行二次开发。

总结

在事件机制驱动下,通过良好的API约定,简洁的插件系统设计,Webpack在完成自身繁重的构建任务同时,还提供了良好的拓展性及可测试性。然而事件机制并不是万金油,通过事件维系的代码如果没有明确的索引关系将增加代码跟踪和调试的复杂度。本文作为简明地分析,希望能抛砖引玉,如有不当之处还望指正。


欢迎加入“CSDN前端开发者”群,与更多专家、技术同行进行热点、难点技术交流。请扫描以下二维码加群主微信,申请入群,务必注明「姓名+公司+职位+工作年限」,否则不予通过,谢谢。
图片描述

评论