# 源码分析总结
# JS Module Bundler
webpack 本质上就是一个 JS Module Bundler,用于将多个代码模块进行打包。 bundler 从一个构建入口触发,解析代码,分析出代码模块的依赖关系,然后将依赖的代码 模块组合在一起,在 bundler 中,提供了一些胶水代码让多个代码模块可以协同工作,互相引用。如下举一些简单的例子:
// entry.js
import {bar} from './bar.js' //依赖 ./bar.js 模块
// bar.js
const foo = require('./foo.js') //依赖 ./foo.js 模块
递归下去,直至没有更多的依赖模块,最终形成一个模块依赖树
分析出依赖关系后,webpack 会利用 JavaScript Function 的特性提供一些代码来将各个模块整合到一起,即是将每一个模块包装成一个 JS Function,提供一个引用依赖模块的方法,比如 webpack_require,这样做,即可以避免变量互相干扰,又能够有效控制执行顺序
// 分别将各个模块依赖的代码用 modules 的方法打包成一个文件
// entry.js
modules['./entry.js'] = function(){
const {bar} = _webpack_require_('./bar.js')
}
// bar
modules['./bar.js'] = function(){
const {bar} = _webpack_require_('./foo.js')
}
modules['./entry.js'] = function(){
// ...
}
已经执行的代码 模块结果会保存在 installedModules 里面
const installedModules = {}
function __webpack_require__(moduleId) {
console.log(moduleId);
// 检查 module 是否在缓存中
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 创建一个 module
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// 执行 module 里面的方法
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 设置加载状态
module.l = true;
// 返回 module
return module.exports;
}
具体可以查看 源码分析一
# 构建过程
Compiler webpack 的运行入口, compiler 对象代表了完整的 webpack 环境配置. 这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options、loader 和 plugin. 当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用,可以使用它来访问 webpack 的主环境
Compilation 对象代表了一次资源版本的构建. 当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源. 一个 compilation 对象表现了当前的 模块资源,编译生成资源,变化的文件,以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键步骤的回调,以供插件做自定义处理时选择使用
Chunk,即用于表示 chunk 的类,对于构建时需要的 chunk 对象由 Compilation 创建后保存管理
Module,用来表示代码模块的基础类,衍生出很多子类用于处理不同的情况 NormalModule,关于代码模块的所有信息都会存在 Module 实例中,如 dependencies 记录代码模块的依赖等。当一个 Module 实例被创建后,比较重要的一步四执行 compilation.buildModule 这个方法,它会调用 Module 实例的build 方法来创建 Module 实例需要的一些东西,然后调用自身的 runLoaders 方法,执行对于的 loaders,将代码源码内容一一 交 由配置中指定的 loader 处理后,再把处理的结果 保存起来
Parser:基于 acorn 来分析 AST 语法树,解析出代码模块的依赖
Dependency:解析是用于保存代码模块对应的依赖使用的对象。Module 实例 build 方法在执行完对应的loader时,处理完模块带啊自身的转换后,继续调用 Parser 的实例来解析自身依赖的模块,解析后的结果存放在 module.dependenuies 中,首先保存的是依赖的路径,后续会经过 compilation.processModuleDepencies 方法,再来处理各个依赖模块,递归地去建立 整个依赖
Template,生成最终代码要使用到的代码模版,打包完的代码就是用对应的 Template 来生成
# 编译顺序
webpack的编译都按照下面的钩子调用顺序执行
# 编写自己的插件
webpack 实现插件机制的大体方式如下:
- 创建 -- webpack 在其内部对象上创建各种钩子
- 注册 -- 插件将自己的方法注册到对应的钩子上,交给 webpack
- 调用 -- webpack 编译过程中,会适时的触发相应的钩子,因此也就触发了插件的方法
如下一个简单例子🌰
const pluginName = 'ConsoleLogBuildPlugin'
class ConsoleLogBuildPlugin {
apply(compiler) {
// 在 compiler 的hooks中注册一个方法,当执行到该阶段会调用
compiler.hooks.run.tap(pluginName, (compilation) => {
console.log('The webpack build process is starting');
})
}
}
module.exports = ConsoleLogBuildPlugin
- 事件钩子会有不同的类型 SyncBailHook,AsyncSeriesHook,SyncHook 等
- 如果是异步事件钩子,那么可以使用 tapPromise 或者 tapAsync 来注册事件函数,tapPromise 要求方法返回 Promise 以便处理异步,而 tapAsync 则需要用 callback 来返回结果
- 除了同步和异步的,名称带有 parallel 的,注册的事件函数会并行调用,名称带有 bail 的,注册的事件函数会被顺序调用,直至一个处理方法有返回值名称带有 waterfall的,每个注册的事件函数,会将上一个方法的返回结果作为输入参数.
异步事件钩子
tapPromise
class HelloAsyncPlugin {
apply(compiler) {
compiler.hooks.emit.tapPromise('HelloAsyncPlugin', compilation => {
// 返回一个 Promise,在我们的异步任务完成时 resolve……
return new Promise((resolve, reject) => {
setTimeout(function() {
console.log('异步工作完成……');
resolve();
}, 1000);
});
});
}
}
module.exports = HelloAsyncPlugin;
tapAsync
class HelloAsyncPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('HelloAsyncPlugin', (compilation, callback) => {
// 做一些异步的事情……
setTimeout(function() {
console.log('Done with async work...');
callback();
}, 1000);
});
}
}
module.exports = HelloAsyncPlugin;
# 编写自己的Loader
loader 会就是接收字符串(或者buffer),再返回处理完的字符串(或者buffer)的过程. webpack 会将加载的资源作为参数传入 loader方法,交与 loader 处理,再返回 如下 编写一个简单的 loader:
const loaderUtils = require('loader-utils')
const path = require('path')
// 就是文件内容
module.exports = function (source) {
// 获取loader配置
onst loaderOptions = loaderUtils.getOptions(this);
return source.replace('xikun', 'myloader')
}
在转换步骤是异步时,可以这样
module.exports = function(source) {
// 告诉 Webpack 本次转换是异步的,Loader 会在 callback 中回调结果
var callback = this.async();
setTimeout(source, function(err, result, sourceMaps, ast) {
// 通过 callback 返回异步执行后的结果
callback(err, result, sourceMaps, ast);
},1000);
};
处理二进制数据 Webpack传给Loader的原内容都是UTF-8格式编码的字符串。 但有些场景下Loader不是处理文本文件,而是处理二进制文件,例如file-loader,就需要Webpack给Loader传入二进制格式的数据。
module.exports = function(source) {
// 在 exports.raw === true 时,Webpack 传给 Loader 的 source 是 Buffer 类型的
source instanceof Buffer === true;
// Loader 返回的类型也可以是 Buffer 类型的
// 在 exports.raw !== true 时,Loader 也可以返回 Buffer 类型的结果
return source;
};
// 通过 exports.raw 属性告诉 Webpack 该 Loader 是否需要二进制数据
module.exports.raw = true;