webpack 是前端非常常用的一种项目打包工具,它可以在很大程度上方便项目构建、调试和打包。而其中比较重要的是它的 loader 和 plugin 的使用。
webpack 工作流程
工作流程:
- 组装 option 参数,添加默认插件
validation(configOption)校验 option 参数compiler.hooks.environment.call()应用 Nodejs 风格的文件系统到 compiler 对象applyWebpackOptionDefaults(options)添加默认参数compiler = new Compiler(options, context)实例化 CompilerWebpackOptionsApply().process(options, compiler)添加插件,依次调用 apply 方法,同时传入 compiler 实例
- 编译
compiler.hooks.beforeRun清除缓存compiler.hooks.run启动一次新的编译 runcompiler.hooks.thisCompilation备份一次 thisCompilationcompiler.hooks.compilation构建 compilation 对象,包含当前模块资源、编译生成资源、变化的文件compiler.hooks.beforeCompile执行之前定义的 beforeCompilecompiler.hooks.compile插件一次新的编译将要启动compilation = this.newCompilation(params)创建 compilation 对象compiler.hooks.make根据 entry,通过 acorn 将 JS 转成 AST,递归 AST 节点,resolve 依赖路径,处理loader,把所有文件组装到 modules 数组
- 处理编译后的文件,合并 module、chunk
- build 完成,组装编译后的内容,把 module 组合成一个个的 chunk
- 通过 Template 生成代码,保存到 compilation、assets
- emitAssets 生成文件到 dist 目录
其中,compiler对象包含了 webpack 环境所有的配置信息,而compilation 包含了其中的模块资源、编译生成资源、变化的文件等,他们的主要区别是:
- compiler 代表了整个 webpack 从启动都关闭的生命周期,compilation 只代表某一次的编译过程
- compiler 和 compilation 都暴露出很多钩子,可以自定义处理
loader
首先 loader 是指加载器,这是 webpack 用来加载一些文件的时候使用的。它在 module.rules 中配置,是作为解析模块规则而存在的。
loader 的类型是数组,其中每一项就是一个 Object,描述了文件的类型、loader、options 等。
同时,loader 是链式传递的,对文件资源从上一个 loader 传递到下一个,而 loader 的处理遵循着从上到下的顺序(webpack 中是这样的),它有一定的开发规则:
- 单一原则。每个 loader 负责一件事,方便维护和使用
- 链式调用。Webpack 会按照顺序链式调用每个 loader
- 统一原则。遵循 webpack 规范,输入输出都应是字符串,并且各个 loader 相互独立
- 无状态原则。在转换不同模块时,不应该在 loader 中保存状态
尝试手写 less-loader
我们在处理 source 时,可能会用到异步操作,此时可以用两种方法:
- async/await,阻塞操作执行
- 用 loader 本身的回调函数 callback()
我们可以尝试手写一下自己的 less-loader:
const less = require("less")
fucntion loader(source){
const callback = this.async()
less.render(source,{sourceMap:{}}, function(err,res){
let {css,map} = res
callback(null, css,map)
})
}
module.exports = loader
其中 callback 函数接收四个参数:
callback({
error: Error | null, //无法翻译时返回 Error
content: string | buffer, //转换后的内容
sourceMap?: SourceMap, //转换后依然得到原内容的 SourceMap
abstractSyntaxTree?: AST //原内容生成 AST 语法树
})
然后我们需要将 loader 文件加载到 webpack 配置中去:
module.exports = {
module:{
rules:[{
test: /\.less/,
use:[
{
loader: './loader/style-loader.js',
},
{
loader: path.resolve(__dirname,"loader","less-loader"),
},
],
}]
}
}
当然,写到这里还没算完,我们还需要对 loader 的传递参数进行一定的配置,这一般都是通过 options 属性来传递的。Webpack 也提供了相应的 query 属性来传参数,但是较不稳定,不是很推荐。这里我们使用官方的 laoder-utils 来处理,用 getOptions 返回处理后的参数:
const {
getOptions,
parseQuery,
stringifyRequest
} = require("loader-utils")
module.exports = function(source, map){
const options = getOptions(this)
parseQuery("?param1=foo")
stringifyRequest(this, "test/lib/index.js")
}
//如果是字符串的话就调用如下方法:
const parseQuery = require('./parseQuery');
function getOptions(loaderContext) {
const query = loaderContext.query;
if (typeof query === 'string' && query !== '') {
return parseQuery(loaderContext.query);
}
if (!query || typeof query !== 'object') {
return {};
}
return query;
}
module.exports = getOptions
而在获取到参数之后,还需要对参数的完整性进行检验:
const { getOptions } = require("loader-utils");
const { validate } = require("schema-utils");
const schema = require("./schema.json");
module.exports = function (source, map) {
const options = getOptions(this);
const configuration = { name: "Loader Name"};
validate(schema, options, configuration);
...
}
//其中 validate 并没有返回值,需要引入另一个 schema.json:
{
"type":"object",
"properties":{
"source":{
"type":"boolean"
},
"name":{
"type":"string"
}
},
"additionalProperties": false //代表是否允许有额外属性
}
plugin
至于 plugin 则可以理解为插件,它在 plugins 中配置,类型也是数组,并且每一项都是一个 plugin 实例,参数通过构造函数直接传入。常见的有 html-webpack-plugin、define-plugin 等。
尝试手写 plugin
其实 plugin 的本质也可以理解为一个类,可以先手写一个简单的 plugin:
class MyPlugin {
constructor(){
console.log('Plugin has been constructed')
}
apply(compiler){}
}
module.exports = Mylugin
而在 webpack 中的配置如下:
const MyPlugin = require('./plugins/Myplugin')
module.exports = {
plugins:[
new Myplugin()
]
}
当然,我们可以做一些改动,让它支持插件传参:
class MyPlugin {
constructor(options){
console.log('Plugin has been constructed')
console.log(options)
this.options = options
}
apply(compiler){}
}
module.exports = {
plugins:[
new Myplugins({title: 'Myplugin'})
]
}
其中, apply 函数会在 webpack 运行时被调用,并赋予 compiler 对象:
- webpack 启动,执行 new Myplugin() 来初始化插件并获取实例
- 初始化 compiler 对象,抵用 MyPlugin.apply()给插件传入 compiler
- 插件实例获取 compiler,通过 compiler 监听 webpack 时间,通过 compiler 操作 webpack
接下来可以尝试手写一个 FileListPlugin:
class FileListPlugin {
apply(compiler){
compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, callback)=>{
var filelist = 'In this build:\n\n';
for (var filename in compilation.assets) {
filelist += '- ' + filename + '\n';
}
compilation.assets['filelist.md'] = {
source: function() {
return filelist;
},
size: function() {
return filelist.length;
}
};
callback();
})
}
}
module.exports = FileListPlugin
这样就可以依次读出文件夹中的文件了
参考
本篇总结参考了谢小飞的《Webpack 手写 loader 和 plugin》,对更多细节有兴趣的朋友可以访问原博客。