编写webpack loader 和 plugin
# 编写webpack loader 和 plugin
# webpack打包过程
打包过程可以分为3个阶段:初始化阶段、打包阶段、输出阶段。
初始化配置:根据配置文件和命令行配置参数得到最终的配置对象。
加载插件:根据配置对象初始化compiler对象,加载所有插件,调用compiler实例的run方法执行编译。
确定打包入口:根据配置的entry找到入口文件,对文件中的依赖调用对应的loader进行递归转移成可用模块。
完成模块编译:根据AST分析出依赖关系,得到模块最终被转译后的内容。
输出资源:根据入口和模块的关系,组装成一个个包含多个模块的chunk,再把每个chunk转换成单独的文件传入到输出列表。
输出完成:在确定号输出内容之后,根据配置的路径和文件名,把内容写入到产物中。如果是non-initial-chunk,就是动态导入的模块,会使用它唯一的id命名,也可以使用模式注释命名。
# loader和plugin的作用
loader的作用根据文件的不同类型,使用对应的loader将文件转换成可读取的模块。
plugin (opens new window)可以执行很多loader无法解决的工作,它可以应用在webpack打包的各个生命周期。webpack 插件是一个具有 apply (opens new window) 方法的 JavaScript 对象。
compiler.hooks.someHook.tap('MyPlugin', (params) => {
/* ... */
});
# 准备工作
yarn init
yarn add webpack webpack-cli -D -S
基本配置webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js',
},
};
尝试打包
npx webpack --config webpack.config.js
# 编写loader
loader 是导出为一个函数的 node 模块。该函数在 loader 转换资源的时候调用。给定的函数将调用 Loader API (opens new window),并通过 this 上下文访问。
在配置loader时需要注意调用顺序,它是从右向左调用loader的,前一个loader的输出将会作为下一个loader的输入。
比如:
less-loader: 将.less文件编译为.css文件
css-loader: 对@import和url()进行处理,转换为commonjs
style-loader: 用于把css插入到DOM中,即将样式通过<style>插入到head中。
我们实现一个添加作者信息注释和一个取出console.log()的loader.
module.exports = function (source) {
const message = `
/**
*author: ${this.getOptions().author}
*date: ${this.getOptions().date}
**/
`
return message + source
}
module.exports = function (source) {
source = source.replace(new RegExp(/(console.log\()(.*)(\))/g), "");
console.log(source);
return source;
}
配置一下:
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js',
},
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: path.resolve(__dirname, './loader/clear-console-loader.js'),
options: {
author: 'William',
data: new Date()
}
},
{
loader: path.resolve(__dirname , './loader/add-authorinfo-loader.js')
}
]
}
]
}
};
# 编写plugin
通过编写plugin (opens new window)我们可以在 webpack 构建流程中引入自定义的行为。
webpack 插件由以下组成:
- 一个 JavaScript 命名函数或 JavaScript 类。
- 在插件函数的 prototype 上定义一个
apply方法。 - 指定一个绑定到 webpack 自身的事件钩子 (opens new window)。
- 处理 webpack 内部实例的特定数据。
- 功能完成后调用 webpack 提供的回调。
插件是由「具有 apply 方法的 prototype 对象」所实例化出来的。这个 apply 方法在安装插件时,会被 webpack compiler 调用一次。apply 方法可以接收一个 webpack compiler 对象的引用,从而可以在回调函数中访问到 compiler 对象。
通过compiler对象的hooks属性可以访问到各个生命周期钩子。
# tapAsync
当我们用 tapAsync 方法来绑定插件时,必须调用函数的最后一个参数 callback 指定的回调函数。
# tapPromise
当我们用 tapPromise 方法来绑定插件时,_必须_返回一个 pormise ,异步任务完成后 resolve 。
# 简单例子
module.exports = class {
constructor(options) {
// 在new的时候可以传入一个配置对象
this.options = Object.prototype.toString.call(options) === '[object Object]' ? options : {}
}
apply(compiler) {
compiler.hooks.emit.tapAsync(
'MyPlugin',
(compilation, callback) => {
console.log(compilation)
console.log(this.options);
callback()
}
)
}
}
# compiler钩子
通过如下方式访问
compiler.hooks.someHook.tap('MyPlugin', (params) => {
/* ... */
});
列举一些钩子:
| 钩子 | 类型 | 何时调用 | 回调参数 | 调用方式 |
|---|---|---|---|---|
| emit | AsyncSeriesHook | 输出 asset 到 output 目录之前执行。 | compilation | |
| environment | SyncHook | 在编译器准备环境时调用,时机就在配置文件中初始化插件之后。 | ||
| entryOption | SyncBailHook | 在 webpack 选项中的 entry 被处理过之后调用。 | context, entry | tap |
# 实现一个简单plugin
我们实现一个plugin,它的功能是创建一个Index.html并把main.js的内容嵌入到script中。
module.exports = class {
constructor(options) {
// 在new的时候可以传入一个配置对象
this.options = Object.prototype.toString.call(options) === '[object Object]' ? options : {}
}
apply(compiler) {
// compiler.hooks.entryOption.tap(
// 'MyPlugin',
// (context, entry) => {
// console.log(context, entry)
// }
// )
compiler.hooks.emit.tapAsync(
'MyPlugin',
(compilation, callback) => {
const source = compilation.assets['main.js'].source()
compilation.assets['index.html'] = {
source: function () {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script>
${source}
</script>
</head>
<body>
</body>
</html>
`
}
}
callback()
}
)
}
}
配置一下
const path = require('path');
const MyPlugin = require('./plugin/my-plugin');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js',
},
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: path.resolve(__dirname, './loader/clear-console-loader.js'),
options: {
author: 'William',
data: new Date()
}
},
{
loader: path.resolve(__dirname , './loader/add-authorinfo-loader.js')
}
]
}
]
},
plugins: [new MyPlugin({ a: 1 , b: 2})]
};