1.Loader
1.1 Loader作用
把js和json外的其它文件转为Webpack可以识别的模块
1.2 Loader简介
1.2.1 Loader类型
1.总类型
pre: 前置loader
normal: 普通loader
inline: 内联loader
post: 后置loader
2.默认类型
默认为normal类型
3.修改类型
配置时可以通过enforce修改pre,normal,post类型。
{ enforce: 'post', test: /\.js$/, loader: 'loader' }
1.2.2 Loader顺序
1.总顺序
类型顺序 > 配置顺序
举例:
配置loader:
[A, B, C]
,执行顺序为:C -> B -> A
配置loader:
[A(enforce: pre), B, C]
,执行顺序为A -> C -> B
2.类型顺序
pre > noraml > inline > post
3.配置顺序
从右到左,从下到上(即配置的链表的逆序)
1.2.3 Loader使用
1.配置Loader
在webpack.config.js中配置Loader将处理哪些类型的文件
配置方法: 见“Webpack学习记录”
2.内联Loader
在引入某个文件时指定使用的Loader
内联方法:
Loader: 多个Loader间用!隔开
参数: 和URL一样用?和&传参给Loader
文件: 文件和Loader间用!隔开
优先级: 类似于配置Loader中的enforce。
!: 跳过普通Loader
-!: 跳过前置Loader和普通Loader
!!: 跳过前置Loader和普通Loader和后置Loader
注意:内联Loader在每次引入文件时使用,写的内容太多太分散,且不利于排查问题。不推荐使用。
import test from 'B-loader!A-loader?mode=txt&type=run!./test.txt'
module.exports = function(content) { // 通过loaders获取内联的每个loader的具体信息,包括查询参数 console.log(this.loaders) return content }
3.脚手架Loader
配置方法: 了解即可。下面是对.jade和.css文件使用对应的loader
webpack --module-bind jade-loader --module-bind 'css=style-loader!css-loader'
1.3 Loader开发
1.3.1 Loader模式
1.开发Loader基本概念
函数: Loader是一个函数,涉及this调用,不要使用箭头函数
函数参数:
content: 文件内容
map: 代码映射
meta: 传递给下一个Loader的内容
链式调用: 不论采用简洁模式还是普通模式,后续loader会通过callback或返回值获取前一个loader处理内容继续处理
2.同步Loader
注意:同步Loader中不应该存在异步操作
简洁模式
module.exports = function(content, map, meta) { return content }
普通模式
module.exports = function(content, map, meta) { this.callback(null, content, map, meta) }
3.异步Loader
注意:虽然异步Loader中有异步操作,但是链式调用时只有异步操作完成,才能继续链式调用
简洁模式
module.exports = function(content, map, meta) { return new Promise((res) => { setTimeout(() => { res(content) }, 1000) }) }
普通模式
module.exports = function(content, map, meta) { const callback = this.async() setTimeout(() => { callback(content, map, meta) }, 1000) }
3.Raw Loader
用途: Raw Loader配置raw为true即可,表示接收Buffer格式的文件二进制数据,通常用于处理图片,音视频等。
模式: 同步和异步模式Raw Loader都可以使用。
module.exports.raw = true module.exports = function(content, map, meta) { return content }
4.Pitch Loader
用途: Pitch Loader配置pitch为函数即可,表示提前执行pitch函数,可以在函数中返回一个非undefined值来中断链式调用中后续Loader的执行。
模式: 同步和异步模式Pitch Loader都可以使用。
中断: Pitch函数返回值中断后,会导致无法读取文件,后续执行的Loader函数的文件来源是中断Pitch函数的返回值。
顺序:
- Pitch阶段: 按照配置的Loader的链表的正序执行它们的Pitch函数。Pitch一旦有返回值,立即执行上一个Pitch对应的Loader并终止链式调用。
- 读取文件: Pitch阶段结束后Loader读取文件准备执行Loader函数。
- Normal阶段: Normal阶段包括pre,normal,inline,post。Normal阶段晚于Pitch阶段。按照配置的Loader的链表的逆序执行它们的Loader函数。
参数:
- remainingRequest: 当前Loader之后要使用的Loader,以内联Loader格式显示。
- precedingRequest: 当前Loader之前要使用的Loader,显示Loader位置。
- data: 用于同一对Pitch和Loader通信。设置data上的属性,Loader可以在this.data中获取到。
module.exports.pitch = (remainingRequest, precedingRequest, data) => { // pitch和loader间通信 data.x = 123 // 有返回值提前中断 return 'result' } module.exports = function(content, map, meta) { // pitch和loader间通信 console.log(this.data.x) return content }
1.3.2 Loader方法
常用方法
方法 描述 用法 this.callback 描述Loader返回结果 this.callback(null, content, map, meta) this.async 标记Loader为异步Loader并返回callback const callback = this.async() this.getOptions 获取webpack.config.js中配置的Loader的options (注意:schema对象,用于描述校验规则,类似于props-type库) const options = this.getOptions(schema) this.emitFile 输出文件到打包后的文件夹中 (注意:通常在处理webpack默认解析不了的文件,并且想让它输出到打包后文件夹中的场景中使用) this.emitFile(name, content, sourceMap) this.utils.contextify 产生一个相对路径 (注意:Path模块产生的路径可能不满足某些Loader的需求,因此一般使用该方法) this.utils.contextify(content, request) this.utils.absolutify 产生一个绝对路径 this.utils.absolutify(content, request)
1.3.3 clean-log-loader
实现一个清除所有console.log语句的loader
module.exports = function(content) { return content.repalce(/console\.log\(.*\);?/g, '') }
1.3.4 banner-loader
实现一个添加作者信息的loader,并支持options配置
banner-loader.js
const schema = { // options类型 type: "object", // options属性 properties: { name: { type: "string", }, }, // options是否可以追加属性 additionalProperties: false, }; module.exports = function (content) { const options = this.getOptions(schema); const prefix = ` /* * Author: ${options?.name || 'Your Name'} */ `; return prefix + content; };
webpack.config.js
module.exports = { module: { rules: [ { test: /\.js$/, loader: './loader/banner-loader', options: { name: 'Danny' } } ] } }
1.3.5 babel-loader
实现一个babel-loader,做一个控制传参与调用的中间层,转译模块调用第三方模块。
const babel = require("@babel/core"); const schema = { type: "object", properties: { presets: { type: "array", }, }, }; module.exports = function (content) { const callback = this.async(); const options = this.getOptions(schema); babel.transform(content, options, function (err, result) { if (err) callback(err); else callback(null, result.code); }); };
1.3.6 file-loader
实现一个file-loader,让webpack能够处理png资源
注意:回顾一下,通常配置webpack的Loader时对于这种资源只配置
type: 'asset'
即可,不用指定Loader
- 重写文件名: 生成带有Hash值的文件名称
- 输出文件: 输出资源到打包后文件夹
- 导出文件: 配置资源导出方式
const loaderUtils = require("loader-utils"); // 处理图片,音视频,字体等文件,需要处理二进制文件 module.exports = function (content) { // 生成哈希值文件名 const interpolatedName = loaderUtils.interpolateName( this, "[hash].[ext][query]", { content } ); // 输出文件 this.emitFile(interpolatedName, content); // 文件输出方式修改 return `module.exports = '${interpolatedName}'` }; module.exports.raw = true;
webpack.config.js
module.exports = { module: { rules: [ { test: /\.png$/, loader: './loader/file-loader', // 禁止webpack默认处理文件资源,只使用我们自定义的loader type: 'javascript/auto' } ] } }
1.3.7 style-loader
注意:style-loader的实现是一种利用pitch loader解决特殊链式调用的解决方案
实现一个style-loader,配合css-loader使用。在实现时请注意这些问题:
style-loader的作用: style-loader实现时把样式作为集成到style标签中插入文档。
css-loader的作用: css-loader帮助我们解决了依赖引入等问题,例如背景图需要使用图片。
css-loader的返回值: css-loader返回一段JavaScript脚本,包含导入导出语句,因此你无法用eval执行获取结果。这和其它大部分Loader在链式调用中返回文件内容不同。
// Imports import ___CSS_LOADER_API_NO_SOURCEMAP_IMPORT___ from "../node_modules/css-loader/dist/runtime/noSourceMaps.js"; import ___CSS_LOADER_API_IMPORT___ from "../node_modules/css-loader/dist/runtime/api.js"; import ___CSS_LOADER_GET_URL_IMPORT___ from "../node_modules/css-loader/dist/runtime/getUrl.js"; var ___CSS_LOADER_URL_IMPORT_0___ = new URL("./assets/development.png", import.meta.url); var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_NO_SOURCEMAP_IMPORT___); var ___CSS_LOADER_URL_REPLACEMENT_0___ = ___CSS_LOADER_GET_URL_IMPORT___(___CSS_LOADER_URL_IMPORT_0___); // Module ___CSS_LOADER_EXPORT___.push([module.id, `.test { width: 100%; height: 100%; background-image: url(${___CSS_LOADER_URL_REPLACEMENT_0___}) } `, ""]); // Exports export default ___CSS_LOADER_EXPORT___;
注意:其实问题就是在style-loader中怎么引入css-loader返回的脚本的导出值。在Loader函数中你无法通过入参和this上的属性来获取导出值的路径
module.exports = function () {}; module.exports.pitch = function (remainingRequest) { // 通过Pitch Loader获取内联Loader调用目标文件的形式 // 通过引用内联Loader,你可以获取css-loader脚本的导出值 const relativePath = remainingRequest .split("!") .map((str) => this.utils.contextify(this.context, str)) .join("!"); // 获取css-loader脚本的导出值,在Loader函数中获取不到内联函数调用形式 const script = ` import result from '!!${relativePath}' const style = document.createElement('style'); style.innerHTML = result; document.head.append(style) `; // 终止后续loader执行 return script; };
2.Plugin
2.1 Plugin作用
扩展Webpack的功能。
2.2 Plugin简介
2.2.1 Plugin原理
Plugin在Webpack工作流程插入操作来扩展Webpack功能。
2.2.2 Webpack钩子
1.钩子
Webpack暴露若干种钩子,表示Webpack工作的不同阶段。
Plugin可以通过注册钩子插入Webpack工作流程。
2.Tapable
Tapable是Webpack内部引用的一个库,帮助Webpack使用钩子。
Tapable对开发者无感知,Webpack包装了Tapable的某些方法供开发者注册钩子。
tap
: 注册同步钩子和异步钩子tapAsync
: 回调方式注册异步钩子tapPromise
: Promise方式注册异步钩子
2.2.3 Webpack构建对象
1.Compiler
Webpack工作时创建Compiler对象,保存了webpack.config.js等配置信息。
Compiler在API形式定制Webpack和Plugin开发时会用到。
compiler.options
: webpack.config.js中的配置信息compiler.inputFileSystem
和compiler.outputFileSystem
: 进行文件操作,类似fscompiler.hooks
: 注册钩子到整个打包过程
2.Compilation
Webpack工作时创建Compilation对象,保存了对模块编译的信息。
Compilation在Plugin开发时会用到。
compilation.modules
: 访问遇到的模块(文件)compilation.chunks
: 访问遇到的chunkscompilation.assets
: 访问遇到的资源文件compilation.hooks
: 注册钩子到编译过程
2.2.4 Webpack生命周期
Webpack生命周期可以通过钩子描述,下面结合Compiler和Compilation的常用钩子描述Webpack生命周期
compiler.initialize: 初始化。
compiler.run: 开始构建。
compiler.compilation: 创建编译实例。
compiler.make: 开始一次编译 (每个文件编译时会触发,包括下述红色部分)。
compilation.buildModule: 构建模块。
compilation.seal: 构建完成。
compilation.optimize: 模块优化。
compiler.afterCompile: 所有文件编译完成。
compiler.emit: 输出资源。
compiler.done: 构建过程完成。
2.3 Plugin开发
2.3.1 Plugin模式
调用方式: Plugin以构造函数调用
核心方法: Plugin的核心方法是constructor和apply
- constructor: Webpack加载webpack.config.js的配置时调用每个plugin的constructor
- apply: Webpack生成配置对象compiler后调用plugin实例的apply方法
class TestPlugin { constructor() { console.log('TestPlugin constructor') } apply(compiler) { console.log('TestPlugin apply') } } module.exports = TestPlugin
2.3.2 Plugin钩子
- 注册方式: Plugin钩子在apply中注册
- 钩子执行: 钩子执行分为同步,异步串行,异步并行。执行方式由钩子说明文档决定。
1.异步串行
注:每个钩子执行完毕后才能执行下一个钩子,钩子的执行会阻塞Webpack的构建过程
// compiler.emit钩子文档描述是异步串行 compiler.hooks.emit.tap("TestPlugin", (compilation) => { // 参数是compilation,可以以此注册compilation钩子 console.log("TestPlugin emit sync"); }); compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => { setTimeout(() => { console.log("TestPlugin emit async"); callback(); }, 10000); }); compiler.hooks.emit.tapPromise("TestPlugin", (compilation) => { return new Promise((resolve) => { setTimeout(() => { console.log("TestPlugin emit promise"); resolve(); }, 2000) }); });
2.异步并行
注:每个钩子同时触发
// compiler.make钩子文档描述是异步并行 compiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => { setTimeout(() => { console.log("TestPlugin make async1"); callback(); }, 3000); }); compiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => { setTimeout(() => { console.log("TestPlugin make async2"); callback(); }, 1000); });
2.3.3 banner-webpack-plugin
- 获取文件资源: 通过
compilation.assets
对象获取键名来得知文件名- 追加内容: 设置
compilation.assets[key]
为对象,实现source
方法和size
方法来描述输出文件// 给文件添加注释的插件 class BannerWebpackPlugin { apply(compiler) { compiler.hooks.emit.tapAsync( "BannerWebpackPlugin", (compilation, callback) => { // 1.获取输出的文件资源。只保留js和css资源 const assets = Object.keys(compilation.assets).filter((assetPath) => { const extensions = ["css", "js"]; const typeName = assetPath.split(".").slice(-1).join(""); return extensions.includes(typeName); }); // 资源内容上追加内容 const prefix = ` /* Author: xxx */ `; // 2.在文件上追加内容 assets.forEach((asset) => { // 获取原来内容 const source = compilation.assets[asset].source(); // 新内容 const content = prefix + source; compilation.assets[asset] = { // 最终资源输出时调用source方法 source() { return content; }, // 资源大小 size() { return content.length; }, }; }); callback(); } ); } } module.exports = BannerWebpackPlugin;
2.3.4 clean-webpack-plugin
webpack4中有该插件,但是webpack5内置了该功能。在此尝试实现clean-webpack-plugin。
- 获取webpack.config.js配置:
compiler.options
- 获取文件操作工具:
fs = compiler.outputFileSystem
- 获取目录下文件和文件夹:
fs.readdirSync
- 获取文件状态:
fs.statSync
- 判断文件是否是目录:
fs.isDirectory
- 删除文件:
fs.unlinkSync
// 清理上次打包内容插件 class CleanWebpackPlugin { apply(compiler) { // 打包输出目录 const outputPath = compiler.options.output.path; // 类似于fs const fs = compiler.outputFileSystem; compiler.hooks.emit.tap("CleanWebpackPlugin", (compilation) => { this.removeFiles(fs, outputPath); }); } removeFiles(fs, filePath) { // 读取目录下所有文件和文件夹 const files = fs.readdirSync(filePath); files.forEach((file) => { const path = `${filePath}/${file}`; // 分析文件状态 const fileStat = fs.statSync(path); // 判断是否是文件夹(是文件夹则先删除子文件) if (fileStat.isDirectory()) { this.removeFiles(fs, path); } else { // 同步删除方法 fs.unlinkSync(path); } }); } } module.exports = CleanWebpackPlugin;
2.3.5 analyze-webpack-plugin
实现一个文件大小分析插件。实现需要API可以参考banner-webpack-plugin。
// 分析文件资源大小插件 class AnalyzeWebpackPlugin { apply(compiler) { compiler.hooks.emit.tap("AnalyzeWebpackPlugin", (compilation) => { // 1.分析输出资源大小 const assets = Object.entries(compilation.assets); // 2.生成分析md文件 const baseContent = `| 资源名称 | 资源大小 |\n | --- | --- |`; const content = assets.reduce((content, [filename, file]) => { return content + `\n| ${filename} | ${file.size()} |`; }, baseContent); // 3.输出分析md文件 compilation.assets["analyze.md"] = { source() { return content; }, size() { return content.length; }, }; }); } } module.exports = AnalyzeWebpackPlugin;
2.3.6 inlineChunk-webpack-plugin
作用
配置了runtimeChunk后webpack打包生成的runtime文件可能非常小,可以考虑直接内联注入到index.html入口中。
思路
因为输出index.html是html-webpack-plugin。因此需要借助这个插件向index.html中追加内容
实现
- 内联runtime文件中的内容到index.html
- 删除打包后产生的runtime.js文件
const HtmlWebpackPlugin = require("safe-require")("html-webpack-plugin"); class InlineChunkWebpackPlugin { apply(compiler) { compiler.hooks.compilation.tap( "InlineChunkWebpackPlugin", (compilation) => { // 1.获取html-webpack-plugin内部的自定义hooks const hooks = HtmlWebpackPlugin.getHooks(compilation); // 2.根据其文档注册alterAssetTagGroups钩子(此时要标签已经分好组),将runtime内容内联到index.html hooks.alterAssetTagGroups.tap("InlineChunkWebpackPlugin", (assets) => { /* headTags [ { tagName: 'script', voidTag: false, meta: { plugin: 'html-webpack-plugin' }, attributes: { defer: true, src: 'jsruntime~main.js.js' } } ] */ // 处理头部标签和身体标签(不确定runtime.js会被html-webpack-plugin放到哪个部分) assets.headTags = this.getInlineChunk( assets.headTags, compilation.assets ); assets.bodyTags = this.getInlineChunk( assets.bodyTags, compilation.assets ); }); // 3.根据其文档注册afterEmit钩子(此时已经输出资源),将产生的runtime.js删除 hooks.afterEmit.tap("InlineChunkWebpackPlugin", () => { Object.keys(compilation.assets).forEach((filePath) => { if (/runtime(.*)\.js$/g.test(filePath)) delete compilation.assets[filePath]; }); }); } ); } getInlineChunk(tags, assets) { return tags.map((tag) => { // 不是script标签不处理 if (tag.tagName !== "script") return tag; // 获取文件资源路径 const filePath = tag.attributes.src; if (!filePath) return tag; // 不是runtime文件不处理 if (!/runtime(.*)\.js$/g.test(filePath)) return tag; return { tagName: "script", // assets是通过compilation获取文件资源 innerHTML: assets[filePath].source(), closeTag: true, }; }); } } module.exports = InlineChunkWebpackPlugin;
3.调试
在构建工具中调试
1.构建工具代码中设置debugger断点
2.配置调试指令
- -brk: 在第一行代码停下来
- cli.js: 运行cli.js通过webpack脚手架启动webpack
{ "scripts": { "debug": "node --inspect-brk ./node_modules/webpack-cli/bin/cli.js" } }