自定义plugin及loader简述:
- 自定义loader是定义一个js文件导出函数,只要在配置文件中符合test正则的文件就会进入loader对应的js文件(也就是函数),会经过loader文件中的函数处理了文件内容后返回处理之后的文件
- 自定义plugin是定义一个js文件导出类,在webpack配置文件中new一个plugin就会进入plugin对应的类,类中的apply方法允许将该plugin插件及对应的处理代码挂载到compiler的钩子函数(生命周期函数)上,也就是不会立即执行plugin的处理代码,只有webpack执行到对应的钩子函数才会触发这个钩子函数上挂载的plugin
- 自定义loader处理的是某一类满足test的文件,而plugin可以处理webpack执行阶段的所有文件及资源
项目目录
loader
loader执行顺序:从下到上,从右到左
loader简介–同步loader与异步loader
同步loader
//同步loader(当前loader内不能执行异步操作,同步loader不会等待异步操作被执行后再执行下一个loader)
//content 需要loader处理的源文件的内容
//map SourceMap 数据
//meta 传向下一个loader的数据,可以是任何内容
module.exports = function (content, map, meta) {
console.log('test1');
//return content
//this.callback 方法则更灵活,因为它允许传递多个参数,而不仅仅是 content
//执行this.callback即代表当前loader执行完毕,将参数传递给下一个loader执行
//this.callback的第一个参数是error,代表错误
this.callback(null, content, map, meta);
return; // 当调用 callback() 函数时,总是返回 undefined
}
异步loader
//异步loader(当前loader内能执行异步操作,异步loader会等待异步操作被执行后再执行下一个loader)
module.exports = function (content, map, meta) {
//this.async将loder转换为异步loader,返回this.callback
const callback = this.async();
// 进行异步操作(当前loader会等待异步操作执行结束再执行下一个loader)
setTimeout(() => {
console.log('test2');
//callback会将参数传入下一个loader(callback中的参数和下一个loader中接收的参数对应)
callback(null, content, map, meta);
}, 1000);
};
自定义loader
项目根目录新建loader文件夹存放自定义loader文件
自定义babel-loader
自定义babel-loader:使用babel提供的预设和工具来实现es6转es5
新建loader/babel-loader
文件夹新建loader/babel-loader/index.js
存放自定义babel-loader逻辑代码
//loader/babel-loader/index.js
//使用babel提供的预设和工具来实现es6转es5
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) {
if (err) callback(err);
else callback(err, result.code);
});
};
由于loader可以传参数,自定义loader可以通过json文件约束参数信息新建loader/babel-loader/schema.json
存放babel-loader参数信息
{
//传入参数类型
"type": "object",
"properties": {
//约束传入参数信息
"presets": {
"type": "array"
}
},
//是否可以自定义传入参数
"additionalProperties": true
}
自定义banner-loader
自定义banner-loader:在文件内容前加上描述信息(作者信息等)
新建loader/banner-loader
文件夹新建loader/banner-loader/index.js
存放自定义banner-loader逻辑代码
//loader/banner-loader/index.js
//在文件内容前加上描述信息(作者信息等)
const schema = require('./schema.json')
module.exports = function (content) {
//this.getOptions获取传入的options参数,需要对webpack.config.js中loader的options传入的参数进行校验
//在this.getOptions中传入校验规则,校验规则需要满足JSON规则
//schema.json规则的type代表传入参数的类型,properties代表对象类型的属性的规则配置,additionalProperties表示是否可以添加属性
const options = this.getOptions(schema)
const prefix = `
/*
* Author: ${
options.author}
*/
`
return `${
prefix} \n ${
content}`
}
新建loader/banner-loader/schema.json
存放banner-loader参数信息
{
"type": "object",
"properties": {
"author": {
"type": "string"
}
},
"additionalProperties": false
}
自定义file-loader
自定义file-loader:打包图片资源
新建loader/file-loader
文件夹新建loader/file-loader/index.js
存放自定义file-loader逻辑代码
//loader/file-loader/index.js
//打包图片资源
const loaderUtils = require("loader-utils");
const schema = require('./schema.json')
module.exports = function (content) {
const options = this.getOptions(schema)
// 1. 根据文件内容生成带hash值文件名(webpack提供的loaderUtils处理loader的一些工具)
let interpolatedName = loaderUtils.interpolateName(this, "[hash].[ext][query]", {
content,
});
//修改图片存放位置
interpolatedName = `${
options.filename}/${
interpolatedName}`
// console.log(interpolatedName);
// 2. this.emitFile方法将文件输出出去(输出到对应目录)
this.emitFile(interpolatedName, content);
// 3. 返回:module.exports = "文件路径(文件名)"
return `module.exports = "${
interpolatedName}"`;
};
// 需要处理图片、字体等文件。它们都是buffer数据
// 需要使用raw loader才能处理buffer数据
module.exports.raw = true;
新建loader/file-loader/schema.json
存放file-loader参数信息
{
"type": "object",
"properties": {
"filename": {
"type": "string"
}
},
"additionalProperties": false
}
自定义style-loader
自定义style-loader:处理css样式文件
新建loader/style-loader
文件夹新建loader/style-loader/index.js
存放自定义style-loader逻辑代码
//loader/style-loader/index.js
module.exports = function (content) {
/*
1. 直接使用style-loader,只能处理样式
不能处理样式中引入的其他资源
use: ["./loaders/style-loader"],
2. 借助css-loader解决样式中引入的其他资源的问题
use: ["./loaders/style-loader", "css-loader"],
问题是css-loader暴露了一段js代码,style-loader需要执行js代码,得到返回值,再动态创建style标签,插入到页面上
不好操作
3. style-loader使用pitch loader用法
*/
// const script = `
// const styleEl = document.createElement('style');
// styleEl.innerHTML = ${JSON.stringify(content)};
// document.head.appendChild(styleEl);
// `;
// return script;
};
module.exports.pitch = function (remainingRequest) {
// remainingRequest 剩下还需要处理的loader
// console.log(remainingRequest); // C:\Users\86176\Desktop\webpack\source\node_modules\css-loader\dist\cjs.js!C:\Users\86176\Desktop\webpack\source\src\css\index.css
// 1. 将 remainingRequest 中绝对路径改成相对路径(因为后面只能使用相对路径操作)
const relativePath = remainingRequest
.split("!")
.map((absolutePath) => {
// 返回相对路径
return this.utils.contextify(this.context, absolutePath);
})
.join("!");
// console.log(relativePath); // ../../node_modules/css-loader/dist/cjs.js!./index.css
// 2. 引入css-loader处理后的资源(内联loader)
// 3. 创建style,将内容插入页面中生效
const script = `
import style from "!!${
relativePath}";
const styleEl = document.createElement('style');
styleEl.innerHTML = style;
document.head.appendChild(styleEl);
`;
// 中止后面loader执行
return script;
};
自定义clean-log-loader
自定义clean-log-loader:清除所有js文件中的console.log()代码
新建loader/clean-log-loader.js
存放自定义clean-log-loader逻辑代码
//loader/clean-log-loader.js
//清除所有js文件中的console.log语句
module.exports = function (content) {
//content是传入的js文件内的内容,对其做正则表达式全局替换console.log再返回就实现了清除所有js文件中的console.log语句了
//正则表达式的.()等都需要\来进行转义处理
return content.replace(/console.log(.*);?/g, "")
}
plugin
plugin简介-apply、compiler、compilation
- 在整个webpack打包构建过程中compiler对象只有一个,而compilation可以有多个,每一个对应一块资源
- compiler对象和compilation都提供了很多的钩子(生命周期),可以在不同的钩子上挂载不同的插件,在执行到对应钩子时就会执行插件代码
/*
1. webpack加载webpack.config.js中所有配置,此时就会new TestPlugin(), 执行插件的constructor
2. webpack创建compiler对象
3. 遍历所有plugins中插件,调用插件的apply方法
4. 执行剩下编译流程(触发各个hooks事件)
*/
class TestPlugin {
constructor() {
console.log("TestPlugin constructor");
}
//参数传入compiler对象
//在整个webpack打包构建过程中compiler对象只有一个,而compilation可以有多个,每一个对应一块资源
//compiler对象和compilation都提供了很多的钩子(生命周期),可以在不同的钩子上挂载不同的插件,在执行到对应钩子时就会执行插件代码
apply(compiler) {
//console.log("compiler", compiler);
console.log("TestPlugin apply");
// 由文档可知,compiler的environment是同步钩子(插件按顺序执行),所以需要使用tap注册
//注册在钩子上的第一个参数是类名(即插件名)
compiler.hooks.environment.tap("TestPlugin", () => {
console.log("TestPlugin environment");
});
// 从文档可知, emit 是 AsyncSeriesHook, 也就是异步串行钩子,特点就是异步任务顺序执行
compiler.hooks.emit.tap("TestPlugin", (compilation) => {
//console.log("compilation", compilation);
console.log("TestPlugin emit 111");
});
// 使用tapAsync、tapPromise注册,进行异步操作会等异步操作做完再继续往下执行
compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {
setTimeout(() => {
console.log("TestPlugin emit 222");
callback();
}, 2000);
});
compiler.hooks.emit.tapPromise("TestPlugin", (compilation) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("TestPlugin emit 333");
resolve();
}, 1000);
});
});
// 从文档可知, make 是 AsyncParallelHook, 也就是异步并行钩子, 特点就是异步任务同时执行
// 可以使用 tap、tapAsync、tapPromise 注册。
// 如果使用tap注册的话,进行异步操作是不会等待异步操作执行完成的。
compiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => {
// 需要在compilation hooks触发前注册才能使用
compilation.hooks.seal.tap("TestPlugin", () => {
console.log("TestPlugin 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;
自定义plugin
项目根目录新建plugin文件夹存放自定义plugin文件
自定义AnalyzeWebpackPlugin
自定义AnalyzeWebpackPlugin:生成打包文件信息(名称、文件大小)
新建plugin/analyze-webpack-plugin.js
存放自定义AnalyzeWebpackPlugin逻辑代码
//plugin/analyze-webpack-plugin.js
class AnalyzeWebpackPlugin {
apply(compiler) {
compiler.hooks.emit.tap("AnalyzeWebpackPlugin", (compilation) => {
// 1. 遍历所有即将输出文件,得到其大小
/*
将对象变成一个二维数组:
对象:
{
key1: value1,
key2: value2
}
二维数组:
[
[key1, value1],
[key2, value2]
]
*/
const assets = Object.entries(compilation.assets);
/*
md中表格语法:
| 资源名称 | 资源大小 |
| --- | --- |
| xxx.js | 10kb |
*/
let content = `| 资源名称 | 资源大小 |
| --- | --- |`;
// //根据文件大小排序(冒泡排序-大->小)
for (let j = assets.length - 1; j >= 0; j--) {
for (let i = 0; i < j; i++) {
if (Math.ceil(assets[i][1].size() / 1024) < Math.ceil(assets[i + 1][1].size() / 1024)) {
let temp = assets[i]
assets[i] = assets[i + 1]
assets[i + 1] = temp
}
}
}
assets.forEach(([filename, file]) => {
content += `\n| ${
filename} | ${
Math.ceil(file.size() / 1024)}kb |`;
});
// 2. 生成一个md文件
compilation.assets["analyze.md"] = {
source() {
return content;
},
size() {
return content.length;
},
};
});
}
}
module.exports = AnalyzeWebpackPlugin;
自定义BannerWebpackPlugin
自定义BannerWebpackPlugin:在生产环境中在css和js资源打包的文件中加上头部注释(作者信息等)
新建plugin/banner-webpack-plugin.js
存放自定义BannerWebpackPlugin逻辑代码
//plugin/banner-webpack-plugin.js
//在生产环境中在css和js资源打包的文件中加上头部注释(作者信息等)
//之前自定义的banner-loader不能在生产环境中使用,在生产环境中使用会把loader添加的注释压缩舍弃掉
//所以banner-webpack-plugin插件需要在compiler打包之后,输出之前的emit钩子函数上挂载头部注释插件
class BannerWebpackPlugin {
//定义的插件都是类,在webpack配置文件new插件传入的参数就会进入constructor构造函数中,要想插件中使用就加上this
constructor(options = {
}) {
this.options = options
}
apply(compiler) {
compiler.hooks.emit.tapAsync("BannerWebpackPlugin", (compilation, callback) => {
//compilation.assets可以获取到打包的资源文件,是对象形式,键为文件名,值为文件信息
//步骤:1、将对象的键(即文件名)使用filter过滤,只获取指定的以css和js后缀的文件
//2、将获取到的文件名组成的数组遍历,通过compilation.assets[遍历的文件名]获取文件信息
//3、文件信息是一个对象,对象的source方法可以获取文件内容,size方法可以获取文件大小
//4、获取到文件内容之后和需要加上的头部注释拼接,然后将这个文件重新赋值一个新对象,包含source方法和size方法,再返回
const extensions = ['css', 'js']
//头部信息
const content = `/*
* Author: ${
this.options.author}
*/
`;
//获取满足后缀条件的文件名
const nameList = Object.keys(compilation.assets).filter((asset) => {
const list = asset.split('.')
//是否为正确的文件后缀名
return extensions.includes(list[list.length - 1])
})
nameList.forEach((item) => {
//获取原本的文件内容
const oldfile = compilation.assets[item].source()
//拼接新文件内容
const newfile = content + oldfile
//重新将该文件的信息整合替换
compilation.assets[item] = {
//文件内容
source() {
return newfile
},
//文件大小(即内容的长度)
size() {
return newfile.length
}
}
})
callback()
})
}
}
module.exports = BannerWebpackPlugin
自定义CleanWebpackPlugin
自定义CleanWebpackPlugin:打包之前清除上一次的打包文件夹dist
新建plugin/clean-webpack-plugin.js
存放自定义CleanWebpackPlugin逻辑代码
//plugin/clean-webpack-plugin.js
//打包之前清除上一次的打包文件夹dist
class CleanWebpackPlugin {
apply(compiler) {
//需要在输出之前清空文件夹,因为如果在刚打包就清空上一次的文件夹,如果遇到打包错误终止了webpack,上一次的dist依旧会被删除
//所以需要在输出之前emit上挂载插件
//1、获取打包输出目录:通过 compiler 对象。
//2、通过文件操作清空内容:通过 compiler.outputFileSystem 操作文件。
// 获取操作文件的对象
const fs = compiler.outputFileSystem;
// emit是异步串行钩子
compiler.hooks.emit.tapAsync("CleanWebpackPlugin", (compilation, callback) => {
// 获取输出文件目录
const outputPath = compiler.options.output.path;
// 删除目录所有文件
const err = this.removeFiles(fs, outputPath);
// 执行成功err为undefined,执行失败err就是错误原因
callback(err);
});
}
//删除文件夹需要将文件夹下的文件全部清空才能删除掉,所以需要使用递归遍历文件夹下的所有文件
removeFiles(fs, path) {
try {
// 读取当前目录下所有文件
const files = fs.readdirSync(path); //['images','js','index.html']
// 遍历文件,删除
files.forEach((file) => {
// 获取文件完整路径
const filePath = `${
path}/${
file}`; // dist/images
// 分析文件
const fileStat = fs.statSync(filePath);
// 判断是否是文件夹
if (fileStat.isDirectory()) {
// 是文件夹需要递归遍历删除下面所有文件
this.removeFiles(fs, filePath);
} else {
// 不是文件夹就是文件,直接删除
fs.unlinkSync(filePath);
}
});
// 最后删除当前目录
fs.rmdirSync(path);
} catch (e) {
// 将产生的错误返回出去
return e;
}
}
}
module.exports = CleanWebpackPlugin
自定义InlineChunkWebpackPlugin
自定义InlineChunkWebpackPlugin:将打包生成的小的js文件转换为html的内联js代码,不使用script标签引入,可以减少请求的发送(比如runtime文件)
新建plugin/inline-chunk-webpack-plugin.js
存放自定义InlineChunkWebpackPlugin逻辑代码
//plugin/inline-chunk-webpack-plugin.js
//将打包生成的小的js文件转换为html的内联js代码,不使用script标签引入,可以减少请求的发送(比如runtime文件)
// plugins/inline-chunk-webpack-plugin.js
const HtmlWebpackPlugin = require("safe-require")("html-webpack-plugin");
class InlineChunkWebpackPlugin {
constructor(tests) {
this.tests = tests;
}
apply(compiler) {
compiler.hooks.compilation.tap("InlineChunkWebpackPlugin", (compilation) => {
//htmlwebpackplugin插件内置了几个钩子函数
//由于htmlwebpackplugin插件是生成html文件且自动引入依赖文件
//所以在htmlwebpackplugin插件执行插入依赖时将需要转换为内联代码的文件的内容插入到html中形成文件转内联代码
const hooks = HtmlWebpackPlugin.getHooks(compilation);
//在htmlpluginwebpack插件提供的钩子函数上注册插件(alterAssetTagGroups表示生成了标签分组的过程)
hooks.alterAssetTagGroups.tap("InlineChunkWebpackPlugin", (assets) => {
//assets.headTags获取head中的标签,assets.bodyTags获取body中的标签
assets.headTags = this.getInlineTag(assets.headTags, compilation.assets);
assets.bodyTags = this.getInlineTag(assets.bodyTags, compilation.assets);
});
//htmlwebpackplugin执行结束之后删除runtime文件(afterEmit表示插件执行结束的过程)
hooks.afterEmit.tap("InlineChunkHtmlPlugin", () => {
Object.keys(compilation.assets).forEach((assetName) => {
if (this.tests.some((test) => assetName.match(test))) {
delete compilation.assets[assetName];
}
});
});
});
}
//找出所有要插入html中的标签中需要转换为内联代码的文件标签,将文件内容代码插入,而不是插入引入文件的标签
getInlineTag(tags, assets) {
/*
目前tags:[
{
tagName: 'script',
voidTag: false,
meta: { plugin: 'html-webpack-plugin' },
attributes: { defer: true, type: undefined, src: 'js/runtime~main.js.js' }
},
]
修改为:
[
{
tagName: 'script',
innerHTML: runtime文件的内容
closeTag: true
},
]
*/
return tags.map((tag) => {
//判断是否是script标签
if (tag.tagName !== "script") return tag;
const scriptName = tag.attributes.src;
//判断是否满足正则表达式的文件名
if (!this.tests.some((test) => scriptName.match(test))) return tag;
//以上都不满足则代表找到了目标文件,将目标文件内容以script标签插入为内联代码
return {
tagName: "script",
innerHTML: assets[scriptName].source(),
closeTag: true
};
});
}
}
module.exports = InlineChunkWebpackPlugin;
webpack配置项使用上面自定义的plugin和loader
项目根目录新建webpack.config.js
配置webpack
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const BannerWebpackPlugin = require('./plugin/banner-webpack-plugin')
const CleanWebpackPlugin = require('./plugin/clean-webpack-plugin')
const AnalyzeWebpackPlugin = require('./plugin/analyze-webpack-plugin')
const InlineChunkWebpackPlugin = require('./plugin/inline-chunk-webpack-plugin')
module.exports = {
mode: "production",
entry: path.resolve(__dirname, './src/main.js'),
output: {
path: path.resolve(__dirname, './dist'),
filename: 'js/[name].js',
clean: true
},
module: {
//loader执行顺序;从下到上,从右到左(如果不使用oneof就会把所有test匹配的loader都执行)
rules: [
{
test: /\.js$/,
//use: ['./loader/demo/test1.js','./loader/demo/test2.js'],
loader: "./loader/clean-log-loader.js"
},
// {
// test: /\.js$/,
// loader: "./loader/banner-loader",
// //传入loader的参数,必须要满足该loader的参数校验规则,不然会报错
// options: {
// author: "minus"
// }
// },
{
test: /\.js$/,
loader: './loader/babel-loader',
options: {
//传入babel的预设
presets: ["@babel/preset-env"],
}
},
{
test: /\.png|jpe?g|gif$/,
loader: './loader/file-loader',
type: "javascript/auto", // 阻止webpack5默认处理图片资源,只使用自己配置的file-loader处理
options: {
filename: "images"
}
},
{
test: /\.css$/,
use: ['./loader/style-loader', 'css-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './public/index.html')
}),
//new TestPlugin(),
//传参进入插件类的构造函数中
new BannerWebpackPlugin({
author: 'minus'
}),
new CleanWebpackPlugin(),
new AnalyzeWebpackPlugin(),
//通过传参的方式可以指定需要转为内联代码的文件
new InlineChunkWebpackPlugin([/runtime(.*)\.js$/])
],
optimization: {
//分包
splitChunks: {
chunks: "all",
},
//生成runtime文件(由于runtime文件体积很小,现在需要将runtime文件转换为内联js代码,减少请求发送)
runtimeChunk: {
name: (entrypoint) => `runtime~${
entrypoint.name}.js`,
},
},
}