提起前端工程化,就绕不开这个基于事件流的架构设计。
它借助把繁杂的构建流程解析成一个个单独的环节,运用广播制度使其串联起来,达成了高度的扩展性以及灵活性,使得开发者能够精确把控代码从源码直至产物的每一步。
初始化流程
构建过程的第一步是参数的收集与合并。
系统会从项目默认的配置文件里读取配置项,系统还会从用户执行命令时添加的语句中读取配置项,之后系统会把两者进行合并,系统还会把两者进行覆盖,最终得出适用于本次构建所使用的一套完整参数。
配置文件的默认名字一般是 webpack.config.js,搞开发的人员呢 有时候也能够凭借命令参数去选定其他的文件。
这个配置文件的核心作用在于激活项目所需的各种加载项和插件。
系统会对文件内容进行解析,把像入口文件路径、输出目录规则、模块解析规则这类配置项,逐个进行拷贝,然后存储到一个全局的配置对象内。
等完成了这些基础信息的载入工作之后,用户自行定义的插件才会被正式地加载进来。
var path = require('path');
var node_modules = path.resolve(__dirname, 'node_modules');
var pathToReact = path.resolve(node_modules, 'react/dist/react.min.js');
module.exports = {
// 入口文件,是模块构建的起点,同时每一个入口文件对应最后生成的一个 chunk。
entry: './path/to/my/entry/file.js',
// 文件路径指向(可加快打包过程)。
resolve: {
alias: {
'react': pathToReact
}
},
// 生成文件,是模块构建的终点,包括输出文件与输出路径。
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].js'
},
// 这里配置了处理各模块的 loader ,包括 css 预处理 loader ,es6 编译 loader,图片处理 loader。
module: {
loaders: [
{
test: /.js$/,
loader: 'babel',
query: {
presets: ['es2015', 'react']
}
}
],
noParse: [pathToReact]
},
// webpack 各插件对象,在 webpack 的事件流中执行对应的方法。
plugins: [
new webpack.HotModuleReplacementPlugin()
]
};
编译器对象
参数解析完成之后,系统着手开始行动,去创建操控整个构建过程的掌控者,也就是编译器对象。
此对象本身不运行具体的代码转换工作,它主要的职责,在于搞全局的调度以及管理。
class Compiler extends Tapable {
constructor(context) {
super();
this.hooks = {
beforeCompile: new AsyncSeriesHook(["params"]),
compile: new SyncHook(["params"]),
afterCompile: new AsyncSeriesHook(["compilation"]),
make: new AsyncParallelHook(["compilation"]),
entryOption: new SyncBailHook(["context", "entry"])
// 定义了很多不同类型的钩子
};
// ...
}
}
function webpack(options) {
var compiler = new Compiler();
...// 检查options,若watch字段为true,则开启watch线程
return compiler;
}
...
它是从一个更为基础的类那里继承而来的,在初始化的这个阶段,其内部会事先定义好那些贯穿于整个生命周期的钩子函数。
这些钩子函数就是系统事件机制的基础。
它们仿若一个个预先设定好的广播站,分别散布于启动这个关键节点,编译这个关键节点,还有输出这个关键节点。
对于插件,或者是其他的扩展功能而言,仅仅只需要去监听自身所关心的钩子,便能够 在特定的某个时机之中,被唤醒,进而执行代码,凭借如此,从而介入,或者是改变构建流程。
编译启动
module.exports = {
entry: './src/file.js'
}
初始化工作准备妥当之后,构建过程借着调用编译器对象的 run 方法正式开启。
执行完 run 方法后,首先会触发一个起着关键性作用的钩子,且是在这个进程当中构建出一个处于核心地位的对象。
这个新对象是编译阶段的实际执行者,负责后续复杂的模块处理。
那个对象的主要职责能够归纳为,先去执行模块创建,接着进行依赖关系收集,然后开展代码块封装,最后实行最终打包等核心任务。
它把从单个文件到最终产出物之间的所有中间步骤串联起来了,它是构建流程里最为繁忙的那一部分,后续所有的编译工作都会围绕这个对象去展开。
模块编译

编译过程从入口文件开始。
入口文件的依赖路径会被系统接收,对应的工厂函数会被系统调用,以此来生成其中一个空的模块对象,是这样的情况。
这个模块对象,之后会被存到编译对象的依赖列表中,还会被存到编译对象的模块列表中,这标志着,对其管理的正式开端。
_addModuleChain(context, dependency, onModule, callback) {
...
// 根据依赖查找对应的工厂函数
const Dep = /** @type {DepConstructor} */ (dependency.constructor);
const moduleFactory = this.dependencyFactories.get(Dep);
// 调用工厂函数NormalModuleFactory的create来生成一个空的NormalModule对象
moduleFactory.create({
dependencies: [dependency]
...
}, (err, module) => {
...
const afterBuild = () => {
this.processModuleDependencies(module, err => {
if (err) return callback(err);
callback(null, module);
});
};
this.buildModule(module, false, null, null, err => {
...
afterBuild();
})
})
}
随后,系统便会进入真正的模块内容构建阶段。
在构建模块内容时,系统会调用配置文件中指定的各类加载器。
各种语言或者框架代码,像是TypeScript、Vue、Sass等,是由开发者编写的,这些代码会被这些加载器转化成标准的JavaScript模块。
转换达成之后,系统会运用解析器(像 acorn 这样的)去生成相应的抽象语法树,从而为后续针对代码开展静态分析以及处理提供便利。
依赖收集
有了抽象语法树,系统便开始分析模块之间的依赖关系。
从配置的入口模块开始出发,系统会对其语法树展开遍历,当遭遇 import、require 这类导入其他模块的语句之际,便会把这些导入路径予以记录,并且添加到模块的依赖列表之中。
这个过程是递归进行的。
对于每一个新被发现的依赖模块,系统就会去重复进行编译的操作,并且还会重复语法分析的操作,一直到找出项目里所有被引用到的模块为止。
借由这般方式,系统能够明晰地架构出一幅完备的模块依赖关系图,弄明白全部文件之间的关联网络,给后续的代码封装奠定根基。
输出资源
在完全掌握了所有模块及其依赖关系后,流程进入输出阶段。
有个名为seal的方法,它承担着对模块依赖图予以优化的职责,还会把它们组合成为一个或者多个代码块(chunk)。
每一个入口文件,同其相关联的模块,会相互组合,形成一个独立的 chunk。而那些经动态导入的模块,有可能性被单独拆分,进而成为新的 chunk。
完成组装之后,每一个chunk 都会转变成一个独自存在的文件,并且会被增添到最终的输出列表里头。
在文件切实被写进磁盘之前,emit 钩子会被执行,这给予了开发者最后一回修改产出内容的时机。
至此,整个从源码读取到打包输出的串联流程宣告结束。
阅读完这篇文章之后,你有没有已然领会这套依托事件以及钩子的机制,是怎样使得插件如同乐高积木那般灵活地介入构建进程的呢?
于你开展项目实践期间,是否发生借由自定义插件处理某个特定构建问题的经历了呢问号!
热忱相邀于评论区域之中去分享你所拥有的案例,同时还请进行点赞的行为以及分享这篇文章,以此使得更多的开发者能够知晓这一套精妙无比的设计。
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].js'
}

Comments NOTHING