loader原理
loader作用:处理webpack本身不能处理的模块
loader执行顺序
1、分类
- pre: 前置 loader
- normal: 普通 loader
- inline: 内联 loader
- post: 后置 loader
2、执行顺序
- 4 类 loader 的执行优级为:
pre > normal > inline > post。 - 相同优先级的 loader 执行顺序为:
从后往前。
// 此时loader执行顺序:loader1 - loader2 - loader3
module: {
rules: [
{
enforce: "pre",
test: /.js$/,
loader: "loader1",
},
{
// 没有enforce就是normal
test: /.js$/,
loader: "loader2",
},
{
enforce: "post",
test: /.js$/,
loader: "loader3",
},
],
},
使用 loader 的方式
- 配置方式:在
webpack.config.js文件中指定 loader。(pre、normal、post loader) - 内联方式:在每个
import语句中显式指定 loader。(inline loader)
inline loader:
用法:import Styles from 'style-loader!css-loader?modules!./styles.css';
含义:
- 使用
css-loader和style-loader处理styles.css文件 - 通过
!将资源中的 loader 分开
inline loader 可以通过添加不同前缀,跳过其他类型 loader。
!跳过 normal loader。
import Styles from '!style-loader!css-loader?modules!./styles.css';
-!跳过 pre 和 normal loader。
import Styles from '-!style-loader!css-loader?modules!./styles.css';
!!跳过 pre、 normal 和 post loader。
import Styles from '!!style-loader!css-loader?modules!./styles.css';
loader 分类
同步loader:
module.exports = function (content, map, meta) {
// 传递map,让source-map不中断
// 传递meta,让下一个loader接收到其他参数
this.callback(null, content, map, meta);
return; // 当调用 callback() 函数时,总是返回 undefined
};
异步loader:
module.exports = function (content, map, meta) {
const callback = this.async();
// 进行异步操作
setTimeout(() => {
callback(null, result, map, meta);
}, 1000);
};
// 由于同步计算过于耗时,在 Node.js 这样的单线程环境下进行此操作并不是好的方案,建议尽可能地使loader 异步化。但如果计算量很小,同步 loader 也是可以的。
Raw Loader:
// 默认情况下,资源文件会被转化为 UTF-8 字符串,然后传给 loader。通过设置 raw 为 true,loader 可以接收原始的 Buffer。 ---- 处理图片、字体图标等资源
module.exports = function (content) {
// content是一个Buffer数据
return content;
};
module.exports.raw = true; // 开启 Raw Loader
Pitching Loader:
module.exports = function (content) {
return content;
};
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
console.log("do somethings");
};
// webpack 会先从前到后执行 loader 链中的每个 loader 上的 pitch 方法(如果有),然后再从后到前执行 loader 链中的每个 loader 上的普通 loader 方法。
// 在这个过程中如果任何 pitch 有返回值,则 loader 链被阻断。webpack 会跳过后面所有的的 pitch 和 loader,直接进入上一个 loader 。
流程图:
loader1 《--- loader2 《--- loader3 《----- file
pitch1 ----》 pitch2 ----》 pitch3 ---》 file
loader重点API:
| this.async | 异步回调 loader。返回 this.callback | const callback = this.async() |
|---|---|---|
| this.callback | 可以同步或者异步调用的并返回多个结果的函数 | this.callback(err, content, sourceMap?, meta?) |
| this.getOptions(schema) | 获取 loader 的 options | this.getOptions(schema) |
| this.emitFile | 产生一个文件 | this.emitFile(name, content, sourceMap) |
| this.utils.contextify | 返回一个相对路径 | this.utils.contextify(context, request) |
| this.utils.absolutify | 返回一个绝对路径 | this.utils.absolutify(context, request) |
手写babel-loader
const schema = require("./schema.json");
const babel = require("@babel/core");
module.exports = function (content) {
const options = this.getOptions(schema);
// 使用异步loader
const callback = this.async();
// 使用babel对js代码进行编译
babel.transform(content, options, function (err, result) {
callback(err, result.code);
});
};
plugin原理
作用:拓展webpack、自定义构建行为;
plugin工作原理
webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。webpack 通过 Tapable 来组织这条复杂的生产线。 webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。
也就是:webpack 在编译代码过程中,会触发一系列 Tapable 钩子事件,插件所做的,就是找到相应的钩子,往上面挂上自己的任务,也就是注册事件,这样,当 webpack 构建的时候,插件注册的事件就会随着钩子的触发而执行了。
钩子的本质就是事件。为了方便我们直接介入和控制编译过程,webpack 把编译过程中触发的各类关键事件封装成事件接口暴露了出来。这些接口被很形象地称做:hooks(钩子)。开发插件,离不开这些钩子。
Tapable 为 webpack 提供了统一的插件接口(钩子)类型定义,它是 webpack 的核心功能库。
Tapable 统一暴露了3个方法给插件,用于注入不同类型的自定义构建行为:
tap:一般用于注册同步钩子。tapAsync:回调方式注册异步钩子。tapPromise:Promise 方式注册异步钩子。
Plugin 构建对象
Compiler
compiler 对象中保存着完整的 Webpack 环境配置,每次启动 webpack 构建时它都是一个独一无二,仅仅会创建一次的对象。
这个对象会在首次启动 Webpack 时创建,我们可以通过 compiler 对象上访问到 Webapck 的主环境配置,比如 loader 、 plugin 等等配置信息。
它有以下主要属性:
compiler.options可以访问本次启动 webpack 时候所有的配置文件,包括但不限于 loaders 、 entry 、 output 、 plugin 等等完整配置信息。compiler.inputFileSystem和compiler.outputFileSystem可以进行文件操作,相当于 Nodejs 中 fs。compiler.hooks可以注册 tapable 的不同种类 Hook,从而可以在 compiler 生命周期中植入不同的逻辑。
它有以下主要属性:
compilation.modules可以访问所有模块,打包的每一个文件都是一个模块。compilation.chunkschunk 即是多个 modules 组成而来的一个代码块。入口文件引入的资源组成一个 chunk,通过代码分割的模块又是另外的 chunk。compilation.assets可以访问本次打包生成所有文件的结果。compilation.hooks可以注册 tapable 的不同种类 Hook,用于在 compilation 编译模块阶段进行逻辑添加以及修改。
生命周期简述
创建compiler对象(保存着webpack的完整配置) -----》 compiler.run() ------》 compiler.compilation()
------》 compiler.make() ----》 执行compilation(一次资源的完整构建过程)中的各个钩子 -----》
compiler.afterCompile() ------》 compiler.emit() -----》 compiler.emitAssets()
插件执行:
/**
* 1、webpack 会加载 webpack.config.js 中所有配置,此时就会 new TestPlugin() ,执行 constructor
* 2、webpack 创建 compiler 对象
* 3、遍历plugins中所有插件,调用插件的apply方法
* 4、执行剩下的编译流程(触发各个hook事件)
*/
class TestPlugin {
constructor() {
console.log('TestPlugin constructor');
}
apply(compiler) {
debugger;
console.log('compiler: ', compiler);
console.log('TestPlugin apply');
// tap注册同步钩子environment(在编译器准备环境时调用,时机就在配置文件中初始化插件之后)
compiler.hooks.environment.tap('TestPlugin', () => {
console.log('TestPlugin environment hook');
})
// emit 在输出asset到output目录之前执行 (asyncSeriesHook 异步串行钩子)
compiler.hooks.emit.tap('TestPlugin', compilation => {
console.log('compilation: ', compilation);
console.log('TestPlugin emit hook 111');
})
compiler.hooks.emit.tapAsync('TestPlugin', (compilation, callback) => {
setTimeout(() => {
console.log('TestPlugin emit hook 222');
callback()
}, 2000);
})
compiler.hooks.emit.tapPromise('TestPlugin', compilation => {
return new Promise(resolve => {
setTimeout(() => {
console.log('TestPlugin emit hook 333');
resolve()
}, 1000);
})
})
// make 在compilation结束之前执行(asyncParallelHook 异步并行钩子)
compiler.hooks.make.tapAsync('TestPlugin', (compilation, callback) => {
// seal 在 compilation对象 停止接收新的模块时触发(封存)
compilation.hooks.seal.tap('TestPlugin', () => {
console.log('TestPlugin -> make -> compilation -> seal');
})
setTimeout(() => {
console.log('TestPlugin make 111');
callback()
}, 3000);
})
compiler.hooks.make.tapAsync('TestPlugin', (compilation, callback) => {
setTimeout(() => {
console.log('TestPlugin make 222');
callback()
}, 1000);
})
compiler.hooks.make.tapAsync('TestPlugin', (compilation, callback) => {
setTimeout(() => {
console.log('TestPlugin make 333');
callback()
}, 2000);
})
}
}
module.exports = TestPlugin
手写analyze-webpack-plugin
class AnalyzeWebpackPlugin {
apply(compiler) {
compiler.hooks.emit.tap('AnalyzeWebpackPlugin', compilation => {
// 遍历所有即将输出的文件,得到其大小 {k1: v1, k2: v2} ===> [[k1, v1], [k2, v2]]
const assets = Object.entries(compilation.assets)
/*
【md表格语法】:
| 资源名称 | 资源大小 |
| --- | --- |
| xxx.js | 10kb |
*/
let content = `| 资源名称 | 资源大小 |\n| --- | --- |\n`;
assets.forEach(([filename, file]) => {
content += `| ${filename} | ${ (file.size() / 1024).toFixed(2) }kb |`
})
// 追加 生成一个md文件
compilation.assets['analyze.md'] = {
source() {
return content;
},
size() {
return content.length;
}
}
})
}
}
module.exports = AnalyzeWebpackPlugin;