webpack进阶
loader
loader 用于转换某些类型的模块
loader 用于对某些导入的资源进行特定处理 例如 css image...
原理
loader本质上是一个函数
手写一个 babelLoader
webpack.config.js
const path = require('path');
module.exports = {
module: {
rules: [
{
test: /\.js$/,
loader: 'babelLoader', // 自定义的 babelLoader
options: {
presets: [
'@babel/preset-env'
]
}
}
]
},
// 配置 loader 路径解析规则 babelLoader 先去 node_modules 中找找不到再去 "./loaders" 下找
resolveLoader: {
modules: [
'node_modules',
path.resolve(__dirname, 'loaders')
]
}
}
babelLoader.js
const { getOptions } = require('loader-utils');
const { validate } = require('schema-utils');
const babel = require('@babel/core');
const util = require('util');
const babelSchema = require('./babelSchema.json');
// babel.transform用来编译代码的方法 是一个普通异步方法
// util.promisify将普通异步方法转化成基于promise的异步方法
const transform = util.promisify(babel.transform);
module.exports = function (content, map, meta) {
// 获取loader的options配置
const options = getOptions(this) || {};
// 校验babel的options的配置
validate(babelSchema, options, {
name: 'Babel Loader' // 报错的提示名称
});
// 创建异步 callback 将内容传给下一个 loader
const callback = this.async();
// 使用babel编译代码
transform(content, options)
.then(({code, map}) => callback(null, code, map, meta))
.catch((e) => callback(e))
}
babelSchema.json 验证规则
{
// 需要验证的参数的类型
"type": "object",
"properties": {
// 参数名称
"presets": {
// 参数类型
"type": "array"
}
},
// 是否允许添加新的属性
"addtionalProperties": true
}
Plugins
Plugin 插件可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量。
原理
Plugin 实例 插件是一个具有 apply 方法的 JavaScript 对象。apply 方法会被 webpack compiler 调用,并且在 整个 编译生命周期都可以访问 compiler 对象。
ConsoleLogOnBuildWebpackPlugin.js
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';
class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
compiler.hooks.run.tap(pluginName, (compilation) => {
console.log('webpack 构建正在启动!');
});
}
}
module.exports = ConsoleLogOnBuildWebpackPlugin;
compiler 钩子
Compiler 可以看做是 运行时的webpack对象
Compiler.hooks 上有 webpack 运行时生命周期的各个 hook, 这些 hook(extends)自 Tapable 类
Tapable 使用
// tabpable 提供一系列创建钩子和调用钩子的方法 (发布者订阅者模式)
const tapable = require('tapable');
const {
// 同步执行 串行
SyncHook,
// 同步执行 串行 遇到返回值 退出执行
SyncBailHook,
// 异步并行 并行行1个钩子出错 不影响其他钩子触发
AsyncParallelHook,
// 异步串行 串行1个钩子出错 后面的钩子就不再触发
AsyncSeriesHook,
} = tapable;
class HookTest {
constructor() {
this.hooks = {
// hooks 的容器
go: new SyncHook(['arg1', 'arg2']), // 参数数组的长度 是 回调函数的所能接受参数的长度
asyncGo: new AsyncParallelHook(['arg1']),
};
}
// 向容器里添加对应的钩子函数 (添加订阅者)
tap() {
// tap 在 go (SyncHook)容器(相当于一个map) 添加键值--- key -> synchook1, value -> 回调函数
this.hooks.go.tap('synchook1', (...args) => {
console.log('synchook1 触发了 args:', args);
return 123;
});
this.hooks.go.tap('synchook2', (...args) => {
console.log('synchook2 触发了 args:', args);
});
// tapAsync 添加一个异步执行的函数 函数内部接受一个 callback , callback 调用代表 函数执行完毕
this.hooks.asyncGo.tapAsync('asynchook1', (...args) => {
let callback = args[args.length - 1];
setTimeout(() => {
console.log('asynchook1 回调异步 触发了 arg1:', args[0]);
// 异步函数执行完毕 callback 第一参数 是错误
callback(null, 'asynchook1');
}, 3000);
});
// tapPromise 添加一个异步执行的函数 函数返回一个 promise
this.hooks.asyncGo.tapPromise('asynchook2', (arg1) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('asynchook2 Promise 异步 触发了 arg1:', arg1);
// 异步函数执行完毕
resolve('asynchook2');
}, 1000);
});
});
}
// 触发所有钩子中的函数 (发布订阅)
call() {
this.hooks.go.call('这是同步钩子调用的参数');
// 最后一个传入的参数 就是回调函数 callback
// 当所有 钩子都执行完了,(或有一个出错误时) 就会触发这个 函数
this.hooks.asyncGo.callAsync('这是异步钩子调用的参数', function (err, res) {
// 代表所有leave容器中的函数触发完了,才触发
console.log('异步钩子 end~~~');
});
}
}
const test = new HookTest();
test.tap();
test.call();
Compilation
Compilation
compilation 由 Compiler 创建, 包含所有模块和对应依赖 , 同样也有各个生命周期的 hook 同 compiler.hooks
手写一个 CopyWebpackPlugin
CopyWebpackPlugin.js
const { validate } = require('schema-utils');
const schema = require('./schema').CopyWebpackPlugin;
const globby = require('globby');
const path = require('path');
const { readFile } = require('fs').promises;
const { RawSource } = require('webpack').sources;
class CopyWebpackPlugin {
constructor(options) {
this.options = options;
validate(schema, options);
this.compiler = null;
this.compilation = null;
}
apply(compiler) {
// compiler 初始化 compilation
this.compiler = compiler;
compiler.hooks.thisCompilation.tap('CopyWebpackPlugin', (compilation) => {
this.compilation = compilation;
// 为 compilation 创建额外 asset
compilation.hooks.additionalAssets.tapPromise(
'CopyWebpackPlugin',
this.hanleCopy.bind(this),
);
});
}
async hanleCopy() {
let { from, ignore, to = '.' } = this.options;
const context = this.compiler.options.context; // process.cwd()
// 判断是 from 否为绝对路径
from = path.isAbsolute(from) ? from : path.resolve(process.cwd(), from);
// window 下有路径问题
from = from.replace(/\\/g, '/');
// 1. 获取 from 文件列表
let files = await globby(from, {
ignore,
});
for (const file of files) {
// 2. 读取文件
let filename = path.join(to, path.basename(file));
let buf = await readFile(file);
// 3. 写入文件
let source = new RawSource(buf);
this.compilation.emitAsset(filename, source);
}
}
}
module.exports = CopyWebpackPlugin;
webpack.config.js
const CopyWebpackPlugin = require('../plugins/CopyWebpackPlugin');
module.exports = {
mode: 'development', // production
plugins: [
new CopyWebpackPlugin({
to: '.',
from: 'public',
ignore: ['*/index.html'],
}),
],
};
schema
module.exports = {
CopyWebpackPlugin: {
type: 'object', // options 类型
properties: {
to: {
type: 'string',
},
from: {
type: 'string',
},
ignore: {
type: 'array',
},
},
additionalProperties: false, // 可以添加更多key
},
};
手写简易版 webpack
参考
- mini webpack 将多个文件打包到一个文件中