代码语言:javascript复制
❝本文由作者 xfz 授权发布 ❞
webpack本质:理解为是一种基于事件流的编程范例,一系列的插件运行
命令行
- 通过 npm scripts 运行 webpack
- 开发环境 npm run dev
- 生产环境 npm run build
- 通过 wepback直接运行
- webpack entry.js bundle.js
这个过程发生了什么
运行命令后 npm让命令行工具进入node_modules/.bin目录查找是否存在webpack.sh或者webpack.cmd文件 如果存在,则执行,不存在,抛出错误(node_modules/wepback/bin/wepback.js)
启动后的结果:wepback最终找到wepback-cli(webpack-command)包,并且执行cli
代码语言:javascript复制// 正常执行返回
process.exitCode = 0;
// 运行某个命令
const runCommand = (command, args) => {
const cp = require("child_process");
return new Promise((resolve, reject) => {
const executedCommand = cp.spawn(command, args, {});
executedCommand.on("error", error => reject(error););
// code 为 0 则说明成功,resolve,否则reject
executedCommand.on("exit", code => {})
});
}
// 判断某个包是否安装
onst isInstalled = packageName => {
try {
require.resolve(packageName);
return true;
} catch (err) {
return false;
}
};
// wepback 可用的 cli:webpck-cli和webpack-command
const installedClis = CLIs.filter(cli => cli.installed)
// 判断两个cli是否安装,根据安装数量处理
if (installedClis.length === 0) {}
else if (installedClis.length === 1) {}
else {}
webpack-cli
- 引入 yargs,对命令行进行定制
- 分析命令行参数,对各个参数进行转换,组成编译配置项
- 引用webpack,根据配置项进行编译和构建
// wepback-cli处理不需要经过编译的命令
const NON_COMPILATION_ARGS = [
"init", // 创建一份webpack配置文件
"migrate", // 进行webpack版本迁移
"add", // 往webpack配置文件中增加属性
"remove", // 从webpack配置文件中删除属性
"serve", // 运行webpack-serve
"generate-loader", // 生成webpack loader 代码
"generate-plugin", // 生成webpack plugins 代码
"info" // 返回与本地环境相关的一些信息
];
const NON_COMPILATION_CMD = process.argv.find(arg => {
if (arg === "serve") {
global.process.argv = global.process.argv.filter(a => a !== "serve");
process.argv = global.process.argv;
}
return NON_COMPILATION_ARGS.find(a => a === arg);
});
if (NON_COMPILATION_CMD) {
return require("./prompt-command")(NON_COMPILATION_CMD, ...process.argv);
}
// 通过yargs,提供命令和分组参数,动态生成help帮助信息
const yargs = require("yargs").usage(`webpack-cli ${
require("../package.json").version
}
// 将输入的命令传递给config-yargs
require("./config-yargs")(yargs);
// 对命令行参数进行解析
yargs.parse(process.argv.slice(2), (err, argv, output) => {}
// 生成 options webpack参数配置对象
let options = require("./convert-argv")(argv);
// 将参数设置对象交给webpack执行
let compiler = webpack(options);
- webpack-cli 使用 args 分析,参数分组,将命令划分为9类:
- Config options: 配置相关参数(文件名称、运行环境)
- Basic options: 基础参数(entry、debug、watch、devtool)
- Module options: 模块参数,给loader设置扩展
- Output options: 输出参数(输出路径、输出文件名称)
- Advanced options: 高级用法(记录设置、缓存设置、监听频率、bail等)
- Resolving options: 解析参数(alias和解析的文件后缀设置)
- Optimizing options: 优化参数
- Stats options: 统计参数
- options: 通用参数(帮助命令、版本信息)
- webpack-cli执行结果
- webpack-cli对配置文件和命令行参数进行转换最终生成配置选项参数options,最终会根据配置参数实例花webpack对象,然后交给webpack执行构建流程(complier)
Tapable插件架构和Hooks设计
- compiler extends Tapable -> compilation extends Tapable
- Tapable 是一个类似Nodejs的EventEmitter的事件库,主要控制钩子函数的发布与订阅,控制着webpack插件系统,Tapable暴露了很多Hook(钩子)类,为插件提供挂载的钩子
- SyncHook: 同步钩子
- SyncBailHook: 同步熔断钩子
- SyncWaterfallHook: 同步流水钩子
- SyncLoopHook: 同步循环钩子
- AsyncParallelHook: 异步并发钩子
- AsyncParallelBailHook: 异步并发熔断钩子
- AsyncSeriesHook: 异步串行钩子
- AsyncSeriesBailHook: 异步串行熔断钩子
- AsyncSeriesWaterfallHokk: 异步穿行流水钩子
- Tapable Hooks 类型
- Hook:所有钩子的后缀
- Waterfall:同步方法,但是它会传值给下一个汉顺
- Bail:熔断:当函数有任何返回值,就会在当前执行函数停止
- Loop:监听函数返回true表示继续循环,返回undefined表示结束循环
- Sync:同步方案
- AsyncSeries:异步串行钩子
- AsyncParallel:异步并发执行钩子
- Tapable暴露出来的都是类方法,new一个类方法获得我们需要的钩子
- 异步:callAsync/promise
- 同步:call
- 异步:tapAsync/tabPromise/tap
- 同步:tap
- class接受数组参数options,非必传,类方法会根据传参,接受同样数量的参数
- 绑定/订阅:
- 执行/发布:
// 创建钩子
const hook = new SyncHook(['arg1', 'arg2', 'arg3'])
// 绑定事件到webpack事件流
hook.tap('hook1', (arg1, arg2, arg3) => {console.log(arg1, arg2, arg3)})
// 执行
hook.call(1, 2, 3);// 1, 2, 3
Tapable与webpack联系起来
代码语言:javascript复制if (Array.isArray(options)) {
compiler = new MultiCompiler(options.map(options => webpack(options)));
} else if (typeof options === "object") {
options = new WebpackOptionsDefaulter().process(options);
compiler = new Compiler(options.context);
compiler.options = options;
new NodeEnvironmentPlugin().apply(compiler);
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
plugin.apply(compiler);
}
}
compiler.hooks.environment.call(); // hook了
compiler.hooks.afterEnvironment.call();
compiler.options = new WebpackOptionsApply().process(options, compiler);
} else {
throw new Error("Invalid argument: options");
}
if (callback) {
// ....
compiler.run(callback);
}
return compiler;
};
webpack整体流程
流程图
过程分析
- webpack编译按照钩子调用顺序执行
- webbpack 本质上就是一个 JS Module Bundler,用于将多个代码模块进行打包。bundler 从一个构建入口出发,解析代码,分析出代码模块依赖关系,然后将依赖的代码模块组合在一起,在JavaScriptbundler中,还需要提供一些胶水代码让多个代码模块可以协同工作,相互引用
- 分析出依赖关系后,webpack 会利用JavaScript Function的特性提供一些代码来将各个模块整合到一起,即是将每一个模块包装成一个JS Function,提供一个引用依赖模块的方法,如下面例子中的__webpack__require__,这样做,既可以避免变量相互干扰,又能够有效控制执行顺序
// 分别将各个依赖模块的代码⽤ modules 的⽅式组织起来打包成⼀个⽂件
================================entry======================================
// entry.js
import { bar } from './bar.js'; // 依赖 ./bar.js 模块
// bar.js
const foo = require('./foo.js'); // 依赖 ./foo.js 模块
递归下去,直至没有更多的依赖模块,最终形成一颗模块依赖树
================================moudles======================================
// entry.js
modules['./entry.js'] = function() {
const { bar } = __webpack__require__('./bar.js')
}
// bar.js
modules['./bar.js'] = function() {
const foo = __webpack__require__('./foo.js')
};
// foo.js
modules['./foo.js'] = function() {
// ...
}
================================output===========================
// 已经执⾏的代码模块结果会保存在这⾥
(function(modules){
const installedModules = {}
function __webpack__require__(id) {
// 如果 installedModules 中有就直接获取
// 没有的话从 modules 中获取 function 然后执⾏,
//将结果缓存在 installedModules 中然后返回结果
}
})({
"./entry.js": (function(__webpack_require__){
var bar = __webpack_require__(/*code内容*/)
}),
"./bar.js": (function(){}),
"./foo.js": (function(){}),
})
其实webpack就是把AST分析树 转化成 链表
- webpackOptionsApply
- output.library -> LibraryTemplatePlugin
- externals -> ExternalsPlugin
- 将素有配置options参数转换成webpack内部插件
- 使用默认列表,例如
- 模块构建和chunk生成阶段
compiler hooks
- 流程相关
- (before-)run
- (before-/after-)compiler
- make
- (after-)emit
- done
- 监听相关
- watch-run
- watch-close
compilation
- compiler 调用 compilation 生命周期方法
- addEntry -> addModuleChain
- finish(上报模块错误)
- seal
ModuleFactory
- NormalModuleFactory
- ContextModuleFactory
Module
- NormalModule: 普通模块
- ContextModule: ./src/a ./src/b
- ExternalModule: module.exports = jQuery
- DelegatedModule: manifest
- MultiModule: entry: ['a', 'b']
build
- 使用 loader-runner 运行loaders
- 通过 Parser 解析(内部是acron)
- ParserPlugins 添加依赖
Compilation hooks
- 模块相关
- build-module
- failed-module
- succeed-module
- 资源生成相关
- module-asset
- chunck-asset
优化和seal相关
- (after-)seal
- optimize
- optimize-modules(-basic/advanced)
- after-optimize-modules
- after-optimize-chunks
- after-optimize-tree
- optimize-chunk-modules(-basic/advanced)
chunk生成算法
- 1.webpack先将entry中对应的module都生成一个新的chunk
- 2.遍历module的依赖列表,将依赖的module也加入到chunk
- 3.如果一个依赖module是动态引入的模块,那么就会根据这个module创建一个新的chunk,继续遍历依赖
- 4.重复上面过程,直到得到所有的chunks
全剧终
经过一周的时间,重新对这几年使用webpack4的感悟进行整理,是时候和 webpack4 说再见了,希望以后不要再见了...