webpack本身可以定义为事件驱动、基于插件的打包器。插件是webpack生态系统的关键拼图,使得社区开发者可以hook到关键事件中、可以侵入到webpack的编译过程的每一切面。

AOP

AOP(Aspect-Oriented Programming):面向切面编程,是对面向对象编程的扩充,在实现对关注点模块化扩展的同时,保证了与原系统的低耦合性。即使你没了解过这个思想,在日常的开发工作中,像表单验证、埋点日志收集、路由钩子、装饰器decorators等场景,都有这种模式的影子。

webpack的tapable就是遵循AOP模式的一个实现。

Tapable

Tapable作为webpack扩展功能的骨架与核心,与Nodejs的EventEmitter类似。扩展tapable的类和对象,我们称之为tapable实例,即是我们通常所说的插件。借助tapable提供的钩子,我们可以侵入到编译阶段的每个过程中。 Tapable skeleton

通过一个简单的demo,看一下Tapable的基本用法👇:

// 为了测试拦截器的loop,我们选用loopHook
const {SyncLoopHook} = require('tapable');

const syncLoopHook = new SyncLoopHook(['name', 'age']);

// tap第一个插件,传入上下文
syncLoopHook.tap({
    name: 'firstPlugin',
    context: true
}, (context, name, age) => console.log({name, age, ...context}));

// tap第零个插件,放在第一个前面
syncLoopHook.tap({
    name: 'zeroPlugin',
    before: 'firstPlugin'
}, (name, age) => console.log({name, age}));

// 添加上下文,观察call、register、loop、tap四个阶段
syncLoopHook.intercept({
    context: true,
    call: (context, ...args) => console.log('arguments: ', ...args),
    register: tap => console.log('tap name: ', tap.name) || tap,
    loop: (context, ...args) => console.log('loop args: ', ...args),
    tap: (context, tap) => {
        console.log('tap event: ', tap);
        if (context) {
            context.name = 'John';
            context.age = 36;
        }
    }
});

// call the hook
syncLoopHook.call('David', 25);

通过控制台的输出,可以看到Tapable整个流程为register➡️call➡️loop➡️tap➡️tap callback。

网上介绍这个API的文章很多,在这儿就不赘述了,建议直接看github的README

webpack工作流

要写好一个插件,必须了解webpack插件的这些切面是如何工作的。

通用模块打包器的大致流程是依赖解析➡️模块映射➡️打包,在webpack中,为了使用nodeJS的文件系统,第一个被这样处理的就是NodeEnvironmentPlugin

下面是webpack的七大模块: webpack arch

开机键Compiler

Compiler的作用可以用两行伪代码来表示:

const webpack = require('webpack');
const compiler = webpack(someConfig);

作为插件开发者,需要从webpack机制/流程/事件发生的时间点来切入,添加你想实现的功能及特性,compiler作为top-level实例,同时也是webpack runtime, 正担任这个角色。它控制着webpack的启动与停止,使你能用上run、emit这些钩子。

藏宝图Compilation

compilation作为compiler的产物,描绘了整个app依赖关系的深度遍历藏宝图,webpack通过compilation掌握你的代码依赖全貌。webpack的load, seal, optimize, chunk, hash都在这一阶段完成,因此具有optimize-modules、seal及optimize-chunk-assets等hook。

webpack-compilaton

寻路resolver

类似于nodejs的resolver处理文件路径,webpack的resolver由enhanced-resolver创建。我们也可以通过resolveLoader或者自己编写插件自定义模块解析策略。

webpack-resolver

同声传译loaders

在resolve文件依赖进行build过程中,肯定会寻找到非JS的文件。这时就需要loader根据ruleSet将!@#$%$^&变成标准模块,加到chunk中。

模块工厂Module Factory

ModuleFactory将resolver、loaders和源模块实例零件进行黏合加工,产出模块对象到内存中。 除了Normal类型之外,还有ContextFactory用于处理上下文的动态依赖。

webpack-module-factory

寻宝科学家Parser

AST是计算机与人类的桥梁,Parser是Module与bundle Template的桥梁。webpack parser使用的是acorn引擎实现AST。

webpack-parser

圣诞树的外衣Template

template顾名思义作为文件输出的数据模版,将template subclass组合到一起,生成最后打包文件的框架结构。当然,由于模块类型的不同,template有多种类型,包括:

  • MainTemplate: 运行时bundle的wrapper
  • ChunkTemplate: 控制chunk wrapper的形式和格式
  • ModuleTemplate: 模块模版
  • DependencyTemplate: 依赖模版
  • RuntimeTemplate: 运行时模版

webpack-template

总结

当我们开启热更新的时候,webpack会按照这种运作机制不断地解析文件、生成依赖图、输出bundle文件。正是因为webpack采用基于切面设计的插件系统和基于插件的运作体系,我们才能够编写优秀的自定义插件实现增量更新,才能够根据差异性的应用场景持续添加特性,达成科学快速的工程化解决方案。

相关资料

  1. artsy-webpack-tour
  2. The Contributors Guide to webpack — Part 2
  3. webpack4核心模块tapable源码解析
  4. Creating a Custom webpack Plugin
  5. 头图来自:从Webpack源码探究打包流程,萌新也能看懂~
  6. 插图部分来自:everything-is-a-plugin