本文将主要讲述以下内容:
webpack-chain
的基本使用;webpack loader
;plugin
的工作原理,tapable
的用法及自定义plugin
;Demo
版本的Webpack
;webpack
启动流程、运行流程及源码调试技巧。为什么要使用chainWebpack
的方式修改webpack
配置
webpack.config.js
配置单个项目没有什么问题,webpack-chain
尝试通过提供可链式和流式的API
创建和修改webpack
配置。webpack-chain
由两部分组成 configureWebpack
和chainWebpack
类似于JavaScript Map
为链式和生成配置提供一些便利,如果一个属性被标记为ChainedMap
,则它将具有如下的API
和方法:除非另有说明,否则这些方法将返回ChainedMap
,允许链式调用这些方法。
webpack-chain
有一定的认知和学习成本,推荐先跑一跑官网例子, 切记别上来就尝试修改cli
配置(知识的掌握效率低)。(中学做实验有个控制变量法,编程有种设计思想叫做职责单一,学习技术同样如此)
webpack-chain
修改配置示例
javascriptmodule.exports = {
chainWebpack: (config) => {
// 一般是新增规则写在这里
config.module
.rule('cssOptimize')
.test(/\.(sc|c)ss$/i)
.include
.add(path.resolve(__dirname, './src'))
.end()
.use('removeCssComments')
.loader(path.resolve(__dirname, './loaders/removeCssCommets.js'))
// 2.1 修改loader示例
config.module
.rule('eslint')
.set('exclude', /node_module/)
.use('eslint-loader')
.loader('eslint-loader')
.tap((options) => {
options.fix = true;
options.formatter = require('eslint-friendly-formatter');
return options;
});
// 3.1 修改plugin示例
if (process.env.NODE_ENV !== 'development') {
config.plugin('BannerPlugin')
.use(require('webpack').BannerPlugin, [{
banner: `${getVersionInfo()}`,
include: /manifest/,
}]).end();
}
// 3.2 修改plugin示例2 修改 HtmlWebpackPlugin
config.plugin('html').tap(args => {
// 官网没说那么细,一开始我以为args是一个对象,其实它的配置是数组的第一个元素
Object.assign(args[0], {
title: '标题',
versionInfo: new Date().toLocalString(),
/*。。。。*/
});
return args;
}).end();
},
// configureWebpack: {},
configureWebpack: (config) => {
const htmlPlugin = config.plugins.find((item) => (item.options || {}).filename === 'index.html');
htmlPlugin.options.title = 'custome title';
return config
}
}
js/* 1.指定loader的绝对路径 */
module.exports = {
module: {
rules: [
{
test: /\.js$/,
// 在这里配置绝对路径
use: path.resolve(__dirname, 'loaders/myLoader.js')
}
]
}
}
/* 2.resolveLoader里配置alias别名 */
module.exports = {
resolveLoader: {
alias: {
myLoader: path.resolve(__dirname, 'loaders/myLoader.js')
}
},
module: {
rules: [
{
test: /\.js$/,
use: 'myLoader'
}
]
}
}
/* 3.resolveLoader 里配置 modules 属性 */
module.exports = {
// xxx
resolveLoader: {
// 配置 resolveLoader.modules
modules: ['node_modules', path.resolve(__dirname, 'loaders']
},
module: {
rules: [
{
test: /\.js$/,
use: 'myLoader'
}
]
}
}
/* 4. npm link */
1.首先看一个极简的loader
jsmodule.exports = function (source) {
// const result = source.replace("XXX", "xxx");
// console.log(result);
return source;
// 下面是完整写法, 如果有错误传入第一个参数,
// return this.callback(null, source);
};
2.处理一个异步的loader
less-loader
举例jsconst less = require("less");
module.exports = function (source) {
less.render(source, (e, output) => {
// console.log(output.css);
this.callback(e, output.css);
});
};
style-loader
jsmodule.exports = function (source) {
return `const ele = document.createElement('style');
ele.innerHTML = ${JSON.stringify(source)};
document.head.appendChild(ele)
`;
};
js{
test: /\.js$/i,
use: [
{
test: /\.js$/i,
loader: "mybabel-loader",
options: {
presets: [
"@babel/preset-env"
]
}
}
]
}
jsconst babel = require("@babel/core");
const { getOptions } = require("loader-utils");
module.exports = function(content) {
// 设置为异步的loader
const callback = this.async();
// 获取传入的参数
const options = getOptions(this);
// 补充知识点: 如果需要校验参数可以使用 schema-utils
// 对源代码进行转换
babel.transform(
content,
options, /* todo把这里参数与配置文件merge合并 */
(err, result) => {
if (err) {
callback(err);
} else {
callback(null, result.code)
}
})
}
module.rules
js{
test: /\.md$/i,
use: [
// "html-loader",
"mymd-loader"
]
},
mymd-loader.js
jsconst marked = require('marked');
const hljs = require('highlight.js');
module.exports = function(content) {
marked.setOptions({
highlight: function(code, lang) {
return hljs.highlight(lang, code).value;
}
})
const htmlContent = marked(content);
const innerContent = "`" + htmlContent + "`";
const moduleCode = `var code=${innerContent}; export default code;`
return moduleCode;
}
src/index.js
jsimport code from "./doc.md";
import "highlight.js/styles/default.css";
document.body.innerHTML = code;
doc.md
文件,增加一些内容打包试试看loader
的执行循序是从后往前webpack.config.js
module.rules
配置js{
test: /\.js$/i,
use: [
'second_exec-loader',
'first_exec_loader',
]
}
pitch loader
与normal loader
webpack.config.js
loader
配置如下js{
test: /\.js$/i,
use: [
'second_exec-loader',
'first_exec_loader',
]
}
first_exec_loader.js
js// normal loader
module.exports = function(content) {
console.log("first_exec-loader");
return content;
}
// pitch loader
module.exports.pitch = function() {
console.log("first_exec-loader pitch");
}
second_exec-loader.js
jsmodule.exports = function(content) {
console.log("second_exec-loader");
return content;
}
module.exports.pitch = function() {
console.log("second_exec-loader pitch");
}
输出顺序为
enforce
改变loader
顺序js{
test: /\.js$/i,
use: 'second_exec-loader',
enforce: "pre",
},
{
test: /\.js$/i,
use: 'first_exec_loader'
}
enforce
字段,上述配置与案例1的执行顺序一致enforce
的执行循序为总结: loader
配置enforce
后执行顺序为
pitch loader
post
, inline
, normal
, pre
;normal loader
pre
, normal
, inline
, post
;inline
是什么?
import "style-loader!css-loader!./css/style.css";
- webpack像是一条生产线,要经过一系列的处理流程才能将源代码转换成输出结果
- 这条生产线上的处理流程都是职责单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。
- 插件就像插入生产线的一个功能,在特定的时机对生产线上的资源做处理。
- webpack通过tabable来组织这条复杂的生产线,webpack在运行过程中会广播事件,插件只需要监听它所需要的事件,就能加入到这条生产线中,去改变生产线的运作。
- webpack通过事件机制保证了插件的有序性,使得整个系统扩展性很好
——「深入浅出 Webpack」
Plugin作用
在学习webpack plugin
之前要先了解下tapable
,然后再学习webpack plugin
就是水到渠成的事了。tapable
本质是一个高级版本的事件机制,我们先来看一下EventEmmitter
javascriptclass EventEmmitter {
events = {}
on(name, fn) {
const events = this.events[name] ??= [];
events.push(fn);
}
emit(name, data) {
const events = this.events[name] ??= [];
events.forEach(fn => fn(data));
}
off(name, fn) {
const events = this.events[name] ??= [];
if(!fn) {
this.events[name].length = 0;
} else {
this.events[name] = events.filter(
item => ![item, item.cb].includes(fn)
)
}
}
once(name, fn) {
const once = (arg) => {
fn(arg);
this.off(name, once);
}
once.cb = fn;
this.on(name, once)
}
}
补充说明: EventEmmitter
仅仅短短几十行代码,却在前端领域的应用非常广泛, 比如nodejs
、vue
、react
都有它的身影。
因为eventEmmitter
的应用如此广泛,我这里也不在啰嗦eventEmitter
的使用了,接下来我们来看一下tapable
的使用。
javascriptconst { SyncHook } = require('tapable');
const syncHook = new SyncHook(['name', 'age']);
syncHook.tap('event1', function(name, age) {
console.log('event1 of syncHook execed', name, age);
});
syncHook.tap('event2', function(name, age) {
console.log('event2 of syncHook execed', name, age);
});
syncHook.call('张三', 25);
/**
* 输出
* - event1 of syncHook execed 张三 25
* - event2 of syncHook execed 张三 25
*
* 与 eventEmmitter 的区别
* - 创建实例的时候需要声明参数个数,
* - 声明几个参数 .tap事件的回调函数就能收到几个参数
**/
const { SyncBailHook } = require('tapable');
const bailHook = new SyncBailHook(['name', 'age']);
bailHook.tap('event1', function(name, age) {
console.log('event1 of SyncBailHook execed', name, age);
// return false;
});
bailHook.tap('event2', function(name, age) {
console.log('event2 of SyncBailHook execed', name, age);
});
bailHook.call('李四', 32);
/**
* SyncBailHook与 SyncHook的区别
* - 如果前一个事件有返回值(返回值不为undefined)则后续事件不再执行
* - 类似 DOM事件的 stopImmediatePropagation()
*/
const {SyncWaterfallHook} = require('tapable');
const waterfallHook = new SyncWaterfallHook(['name', 'age']);
waterfallHook.tap('event1', function(name, age) {
console.log('event1 of SyncWaterfallHook execed', name, age);
return '王小二';
});
waterfallHook.tap('event2', function(name, age) {
console.log('event2 of SyncWaterfallHook execed', name, age);
});
waterfallHook.call('王五', 32);
/**
* 输出
* - event1 of SyncWaterfallHook execed 王五 32
* - event2 of SyncWaterfallHook execed 王小二 32
*
* SyncWaterfallHook 与 SyncHook 的 区别
* - 如果前一个事件有返回值(返回值不为undefined),则传给下一个事件
*/
const {SyncLoopHook} = require('tapable');
const loopHook = new SyncLoopHook();
let count = 0;
loopHook.tap('event1', function() {
count++;
console.log('event1 of SyncLoopHook execed', count);
if(count < 2) {
return true;
}
// return '王小二';
});
loopHook.tap('event2', function() {
count++;
console.log('event2 of SyncLoopHook execed', count);
if(count < 4) {
return true;
}
});
loopHook.call();
/**
* 输出
* - event1 of SyncLoopHook execed 1
* - event1 of SyncLoopHook execed 2
* - event2 of SyncLoopHook execed 3
* - event1 of SyncLoopHook execed 4
* - event2 of SyncLoopHook execed 5
*
* 与 SyncHook 的区别
* - 如果某个事件有返回值(返回值不为undefined)则循环执行包含该事件的之前的所有事件
*/
const { AsyncSeriesHook } = require('tapable');
const seriesHook = new AsyncSeriesHook(['name', 'age']);
seriesHook.tapAsync('event1', (name, age, cb) => {
console.log('event1 of AsyncSeriesHook execed', name, age);
setTimeout(() => {
cb()
}, 1000)
});
seriesHook.tapPromise('event1', (name, age) => new Promise(
(resolve) => {
console.log('event2 of AsyncSeriesHook execed', name, age)
setTimeout(resolve, 2000)
}
));
seriesHook.callAsync('王二小', 12, () => {
console.log('all events execed done')
})
/**
* 输出
* - event1 of AsyncSeriesHook execed 王二小 12
* 间隔1s
* - event2 of AsyncSeriesHook execed 王二小 12
* 间隔2s
* - all events execed done
*
* AsyncSeriesHook 与 SyncHook 的区别
* - AsyncSeriesHook是异步串行
* - 使用 tapAsync 和 tapPromise 代替 tap 这样也是更好的语义化
* - 使用 callAsync 代替 call
*/
const { AsyncParallelHook } = require('tapable');
const parallelHook = new AsyncParallelHook(['name', 'age']);
parallelHook.tapAsync('event1', (name, age, cb) => {
setTimeout(() => {
console.log('event1 of AsyncParallelHook execed done', name, age);
cb();
}, 2000)
});
parallelHook.tapPromise('event2', (name, age) => new Promise(
(resolve) => {
setTimeout(() => {
console.log('event2 of AsyncParallelHook execed done', name, age)
resolve();
}, 1000);
}
));
parallelHook.callAsync('王二小', 12, () => {
console.log('all events execed done')
})
/**
* 输出
* 间隔1s
* - event2 of AsyncParallelHook execed done 王二小 12
* 在间隔1s
* - event1 of AsyncParallelHook execed done 王二小 12
* - all events execed done
*
* AsyncParallelHook 与 SyncHook 的区别
* - AsyncParallelHook 是异步并行
*/
/**
* 理解了 sync 与 async,
* bail 与 waterfall、loop,
* parallel 与 Series 基本上就掌握了hook
* 还剩一些组合情况,不需要解释了
*/
const {
AsyncParallelBailHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook,
AsyncSeriesLoopHook,
} = require('tapable')
钩子的本质就是:事件。为了方便我们直接介入和控制编译过程,webpack 把编译过程中触发的各类关键事件封装成事件接口暴露了出来,这些接口被很形象地称做:hooks(钩子)。开发插件,离不开这些钩子。
Tapable 为 webpack 提供了统一的插件接口(钩子)类型定义,它是 webpack 的核心功能库。webpack 中目前有十种 hooks,在 Tapable 源码中可以看到,他们是:
javascript// https://github.com/webpack/tapable/blob/master/lib/index.js
exports.SyncHook = require("./SyncHook");
exports.SyncBailHook = require("./SyncBailHook");
exports.SyncWaterfallHook = require("./SyncWaterfallHook");
exports.SyncLoopHook = require("./SyncLoopHook");
exports.AsyncParallelHook = require("./AsyncParallelHook");
exports.AsyncParallelBailHook = require("./AsyncParallelBailHook");
exports.AsyncSeriesHook = require("./AsyncSeriesHook");
exports.AsyncSeriesBailHook = require("./AsyncSeriesBailHook");
exports.AsyncSeriesLoopHook = require("./AsyncSeriesLoopHook");
exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook");
Tapable 还统一暴露了三个方法给插件,用于注入不同类型的自定义构建行为:
一个 webpack plugin 由如下部分组成:
一个基本的 plugin 代码结构大致长这个样子:
javascript// plugins/MyPlugin.js
class MyPlugin {
apply(compiler) {
compiler.hooks.done.tap('My Plugin', (stats) => {
console.log('Bravo!');
});
}
}
module.exports = MyPlugin;
speed-measure-webpack-plugin
npx webpack --env product --profile --json=stats.json
webpack-bundle-analyer
尝试写一个简单webpck构建过程
javascript// src/index.js
import a from "./a.js";
import b from "./b.js";
console.log(a + "hello,webpack-bundle!");
// src/a.js
import c from "./c.js";
export default "haha" + c;
// src/b.js
export default "xixi";
// src/c.js
export default "c";
javascriptconst path = require("path");
module.exports = {
entry: "./src/index.js",
mode: "development",
output: {
path: path.resolve(__dirname, "./dist"),
filename: "main.js",
},
};
3.编写demo版webpack.js
javascriptconst fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const { transformFromAst } = require("@babel/core");
module.exports = class webpack {
constructor(options) {
const { entry, output } = options;
this.entry = entry;
this.output = output;
this.modules = [];
}
run() {
//开始分析入口模块的内容
const info = this.parse(this.entry);
//递归分析其他的模块
// console.log(info);
this.modules.push(info);
for (let i = 0; i < this.modules.length; i++) {
const item = this.modules[i];
const { dependencies } = item;
if (dependencies) {
for (let j in dependencies) {
this.modules.push(this.parse(dependencies[j]));
}
}
}
const obj = {};
this.modules.forEach((item) => {
obj[item.entryFile] = {
dependencies: item.dependencies,
code: item.code,
};
});
// console.log(obj);
this.file(obj);
}
parse(entryFile) {
const content = fs.readFileSync(entryFile, "utf-8");
const ast = parser.parse(content, {
sourceType: "module",
});
const dependencies = {};
traverse(ast, {
ImportDeclaration({ node }) {
// "./a.js" => "./src/a.js"
const newPathName =
"./" + path.join(path.dirname(entryFile), node.source.value);
// console.log(newPathName);
dependencies[node.source.value] = newPathName;
},
});
const { code } = transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
});
return {
entryFile,
dependencies,
code,
};
}
file(code) {
//创建自运行函数,处理require,module,exports
//生成main.js = >dist/main.js
const filePath = path.join(this.output.path, this.output.filename);
console.log(filePath);
//require("./a.js")
// this.entry = "./src/index.js"
const newCode = JSON.stringify(code);
const bundle = `(function(graph){
function require(module){
function reRequire(relativePath){
return require(graph[module].dependencies[relativePath])
}
var exports = {};
(function(require,exports,code){
eval(code)
})(reRequire,exports,graph[module].code)
return exports;
}
require('${this.entry}')
})(${newCode})`;
fs.writeFileSync(filePath, bundle, "utf-8");
}
};
node bundle.js
javascriptconst options = require("./webpack.config.js");
const webpack = require("./lib/webpack.js");
new webpack(options).run();
javascript(function (graph) {
function require(module) {
function reRequire(relativePath) {
return require(graph[module].dependencies[relativePath])
}
var exports = {};
(function (require, exports, code) {
eval(code)
})(reRequire, exports, graph[module].code)
return exports;
}
require('./src/index.js')
})({
"./src/index.js": {
"dependencies": {"./a.js": "./src/a.js", "./b.js": "./src/b.js"},
"code": "\"use strict\";\n\nvar _a = _interopRequireDefault(require(\"./a.js\"));\n\nvar _b = _interopRequireDefault(require(\"./b.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_a[\"default\"] + \"hello,webpack-bundle!\");"
},
"./src/a.js": {
"dependencies": {"./c.js": "./src/c.js"},
"code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nvar _c = _interopRequireDefault(require(\"./c.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nvar _default = \"haha\" + _c[\"default\"];\n\nexports[\"default\"] = _default;"
},
"./src/b.js": {
"dependencies": {},
"code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\nvar _default = \"xixi\";\nexports[\"default\"] = _default;"
},
"./src/c.js": {
"dependencies": {},
"code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\nvar _default = \"c\";\nexports[\"default\"] = _default;"
}
})
Q:webpack
与webpack-cli
之间的关系
A:webpack
是核心通过nodejs
代码进行webpack
打包,webpack-cli
提供命令行参数合并及打包的功能,它能解析命令行参数merge到配置文件中,最终调用webpack
进行打包
npx webpack
node ./node_modules/bin/webpack
webpack-cli
,没有就安装,安装后继续执行runCli
执行webpack-cli
bin.webpack-cli
字段 最终相当于执行npx webpack-cli
lib/bootstrap.js
--> new WebpackCli().run()
webpack-cli.js
构造函数 和run
方法run
方法做了些命令行参数文档,然后检查webpack
是否安装,没安装就执行安装,然后this.webpack = require('webpack')
this.makeCommand
--> this.makeOption
解析命令行参数merge
到配置文件buildCommand
--> this.runWebpack
(watch实现) --> this.createCompiler
this.createCompiler()
有一行关键代码 compiler = this.webpack(config.options, callback)
(new Webpack(config.options)).run(callback)
config.options
即合并命令行参数后的完整webpack配置webpack-cli
就没有关系了前面分析了webpack启动流程,简单概括下
cli做了一些列的检查工作,最后使用执行Webpack
构造函数传入参数对象,生成compiler
对象,再调用compiler.run()
方法。
jsconst compiler = new Webpack(config.options)
compiler.run(callback); // 返回this 即compiler对象
综上我可以手动创建一个配置文件webpack.config.js
和一个脚本build.js
,脚本代码如下
jsconst webpack = require("../lib/webpack");
const config = require("./webpack.config");
const compiler = webpack(config);
compiler.run((err, stats) => {
if (err) {
console.error(err);
} else {
console.log(stats);
}
});
../lib/webpack
文件哪里来的?
这个就是github webpack
源码包的一个文件。可以把仓库down下来本地调试
控制台执行 node build.js
即可对webpack源码进行断点调试了
webpack-5.24.3.tar.gz 这里是一个加了注释的源码包,有兴趣的同学可以下载下来调试下
一次完整的 webpack 打包大致是这样的过程:
webpack
配置文件 合并、解析得到参数对象。webpack
执行得到 Compiler
对象。Compiler
的 run
方法开始编译。每次执行 run
编译都会生成一个 Compilation
对象。Compiler
的 make
方法分析入口文件,调用 compilation
的 buildModule
方法创建主模块对象。compilation
的 seal
方法对每个 chunk
进行整理、优化、封装。Compiler
的 emitAssets
方法把生成的文件输出到 output
的目录中。注: 创建compiler对象后 立即开始注册所有插件plugin.call(compiler, compiler)
or plugin.apply(compiler)
;除了配置文件中的plugin
所有配置项都会转换为Plugin
Compiler
对象, 并且在webpack
的整个生命周期都会存在,除非重启,否则该对象一致保持不变。webpack的整个生命周期
before
-run
-beforeCompiler
-compile
-make
-finishMake
-afterCompiler
-done
本文作者:郭敬文
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!