文件列表插件
/*
最终结果
## 文件名 资源大小
- bundle.js 32157
- bundle.js.map 30826
- index.html 317
*/
class FileListPlugin{
constructor({filename}) {
this.filename = filename;
}
apply(compiler) {
compiler.hooks.emit.tap("FileListPlugin", (compilation, cb) => {
// 所有的资源都挂在compilation.assets属性上, 可以在属性上再加一个文件 这样就会被打包生成出来
let assets = compilation.assets;
let content = `## 文件名 资源大小\r\n`;
Object.entries(assets).forEach(element => {
let [filename, statObj] = element;
content += `- ${filename} ${statObj.size()}\r\n`
})
assets[this.filename] = {
source(){
return content;
},
size() {
return content.length;
}
}
})
}
}
module.exports = FileListPlugin;
内联webpack插件
// 将外链标签变成内联
const HtmlWebpackPlugin = require("html-webpack-plugin");
class InlineSourcePlugin{
constructor({match}) {
this.reg = match; // 正则
}
apply(compiler) {
// 要通过htmlwebpackplugin实现这个功能
compiler.hooks.compilation.tap("InlineSourcePlugin", (compilation) => {
HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync("alterPlugin", (data,cb) => {
data = this.processTags(data, compilation); // 因为资源内容都放在compilation.assets上
cb(null,data);
})
})
}
processTags(data, compilation) { // 处理引入标签的数据
let headTags = [];
let bodyTags = [];
data.headTags.forEach(headTag => {
headTags.push(this.processTag(headTag, compilation))
})
data.bodyTags.forEach(bodyTag => {
bodyTags.push(this.processTag(bodyTag, compilation))
})
return {...data, headTags, bodyTags};
}
processTag(tag, compilation) { // 处理某一个标签
let newTag, url;
if(tag.tagName === "link" && this.reg.test(tag.attributes.href)){
newTag = {
tagName: "style",
attributes:{type:"text/css"}
};
url = tag.attributes.href;
}
if(tag.tagName === "script" && this.reg.test(tag.attributes.src)){
newTag = {
tagName: "script",
attributes:{type:"application/javascript"}
}
url = tag.attributes.src;
}
if(url) {
newTag.innerHTML = compilation.assets[url].source(); // 文件的内容 放到innerHTML属性上
delete compilation.assets[url]; // 删除掉原资源 否则还是会被单独打包生成文件
return newTag;
}
return tag;
}
}
module.exports = InlineSourcePlugin;
tapable
- tapable是插件的核心,也是webpack工作流的核心
- webpack实现插件机制的大体方式是
- 创建
- 注册
- 调用
- tapable分类
- 同步
- SyncHook
- SyncBailHook
- SyncWaterfallHook
- SyncLoopHook
- 异步
- AsyncParallelHook
- AsyncParallelBailHook
- AsyncSeriesHook 异步串行
- AsyncSeriesBailHook
- AsyncSeriesWaterfallHook 异步串行钩子,会传递返回的参数
- bail
- 执行每一个事件函数,遇到第一个结果 result !== undefined 则返回 不再继续执行
- 有返回结果就结束
- waterfall
- 如果前一个事件函数的结果result !== undefined,则result会作为后一个事件函数的第一个参数
- loop
- 不停的循环执行事件函数,直到所有函数结果result == undefined
- 返回值不为undefined 就重新从头开始执行
- tapable同步钩子用法
- 实例化钩子函数
- tap注册
- call触发
const { SyncHook } = require("tapable");
// SyncHook
// 参数是一个数组,参数的长度有用,代表取真实的call的参数的个数
// 数组中字符串的名字随意
const syncHook = new SyncHook(["name", "age"]);
// 注册事件函数
syncHook.tap("1", (name, age) => {
console.log(1, age, name);
});
syncHook.tap("2", (name, age) => {
console.log(2, age, name);
});
syncHook.tap("3", (name, age) => {
console.log(3, age, name);
});
// 触发事件函数
syncHook.call("zhuzhu", "28");
const { SyncBailHook } = require("tapable");
// SyncBailHook与SyncHook都是顺序执行,唯一的不同是SyncBailHook如果有返回值就不再继续执行
// 参数是一个数组,参数的长度有用,代表取真实的call的参数的个数
// 数组中字符串的名字随意
const syncBailHook = new SyncBailHook(["name", "age"]);
// 注册事件函数
syncBailHook.tap("1", (name, age) => {
console.log(1, age, name);
});
syncBailHook.tap("2", (name, age) => {
console.log(2, age, name);
return 2;
// 下面的事件函数不再执行
});
syncBailHook.tap("3", (name, age) => {
console.log(3, age, name);
});
// 触发事件函数
syncBailHook.call("zhuzhu", "28")
const { SyncWaterfallHook } = require("tapable");
// 事件函数中的name参数的值是往上找,离得最近的return的非undefined的值
const SyncWaterfallHook = new SyncWaterfallHook(["name", "age"]);
// 注册事件函数
SyncWaterfallHook.tap("1", (name, age) => {
console.log(1, age, name);
return 1
});
SyncWaterfallHook.tap("2", (name, age) => {
console.log(2, age, name);
});
SyncWaterfallHook.tap("3", (name, age) => {
console.log(3, age, name);
});
// 触发事件函数
SyncWaterfallHook.call("zhuzhu", "28")
// SyncWaterfallHook
// 不停的循环执行事件函数 知道函数返回的结果都是undefined为止
// 每次循环都是从头开始
- tapable异步钩子用法
- 实例化钩子函数
- Async异步钩子注册方式更加多样化 有3种
- tap注册 callAsync触发
- tapAsync注册 callAsync触发
- tapPromise注册 promise触发
- 异步会等待 什么时候调用callback 事件函数才结束
const { AsyncParallelHook } = require("tapable");
// 事件函数中的name参数的值是往上找,离得最近的return的非undefined的值
const AsyncParallelHook = new AsyncParallelHook(["name", "age"]);
// 注册事件函数
AsyncParallelHook.tap("1", (name, age) => {
console.log(1, age, name);
return 1
});
AsyncParallelHook.tap("2", (name, age) => {
console.log(2, age, name);
});
AsyncParallelHook.tap("3", (name, age) => {
console.log(3, age, name);
});
// 触发事件函数
AsyncParallelHook.callAsync("zhuzhu", "28",(err)=>{
console.log('err', err);
})
// tapAsync注册
// 注册的异步函数同时执行 全部执行完成后执行最终的回调函数
const { AsyncParallelHook } = require("tapable");
// 事件函数中的name参数的值是往上找,离得最近的return的非undefined的值
const hook = new AsyncParallelHook(["name", "age"]);
// 注册事件函数
hook.tapAsync("1", (name, age, callback) => {
setTimeout(() => {
console.log(1, name, age);
callback()
}, 1000)
});
hook.tapAsync("2", (name, age, callback) => {
setTimeout(() => {
console.log(2, name, age);
callback()
}, 2000)
});
hook.tapAsync("3", (name, age) => {
setTimeout(() => {
console.log(3, name, age);
callback()
}, 3000)
});
// 触发事件函数
hook.callAsync("zhuzhu", "28", (err) => {
console.log('err', err);
})
// promise方式
const { AsyncParallelHook } = require("tapable");
// 事件函数中的name参数的值是往上找,离得最近的return的非undefined的值
const hook = new AsyncParallelHook(["name", "age"]);
// 注册事件函数
hook.tapPromise("1", (name, age, ) => {
return new Promise((resolve, reject) => {
console.log(1, name, age);
resolve();
})
});
hook.tapPromise("2", (name, age, ) => {
return new Promise((resolve, reject) => {
console.log(2, name, age);
resolve();
})
});
hook.tapPromise("3", (name, age) => {
return new Promise((resolve, reject) => {
console.log(3, name, age);
resolve();
})
});
// 触发事件函数
hook.promise("zhuzhu", "28").then(result => {
console.log(result);
})
// AsyncParallelBailHook 有返回值就停止
callback(null, "返回值");
类似于Promise.race 有一个有返回值就结束了 然后执行最终回调
resolve('返回值')
插件
-
常见的插件对象
- compiler
- compilation
- module factory
- module
- parser
- template
-
创建插件
- 插件是一个类 里面有apply方法
-
compiler和compilation
- compiler对象代表了完整的webpack环境配置,这个对象在启动webpack时被一次性建立,并配置好所有可操作的设置,包括options/loader/plugin。当在webpack环境中应用一个插件时,插件将收到此compiler对象的引用。可以使用它来访问webpack的主环境
- compilation 对象表示了一次资源版本构建。当运行webpack开发环境中间件时,每当检测到一个文件变化,就会创建一个新的compilation,从而生成一组新的编译资源。一个compilation对象表现了当前的模块资源,编译生成资源,变化的文件,以及被跟踪依赖的状态信息。compilation对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用
-
compiler插件
// compiler插件
class DonePlugin{
constructor(options) {
this.options = options;
}
apply(compiler) {
compiler.hooks.done.tapAsync('DonePlugin',(stats, callback) => {
console.log("hello", this.options.name);
callback();
})
}
}
module.exports = DonePlugin;
- compilation插件
- 使用compiler对象时,可以绑定提供了编译compilation引用的回调函数,然后拿到每次新的compilation对象。这些compilation对象提供了一些钩子函数,来购入到构建流程的很多步骤中
- compilation里面放着编译的过程和编译的结果
class AssetPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
// compiler只有一个,每当监听到文件的变化,就会创建一个新的compilaton
// 每当compiler开启一次新的编译,就会创建一个新的compilation,触发一次compilation事件
compiler.hooks.compilation.tap('AssetPlugin', (compilation) => {
compilation.hooks.chunkAsset.tap('AssetPlugin', (chunk, filename) => {
console.log(chunk, filname);
})
})
}
}
module.exports = AssetPlugin;
- compiler一般用来监听编译的流程 如开始,编译结束等
- compilation一般用来监听编译过程中的一些资源
zip-plugin插件
- 把编译之后的文件放到一个文件夹
const path = require("path");
const JSZip = require("jszip");
const {RawSource} = require("webpack-sources");
class ZipPlugin{
constructor(options) {
this.options = options;
}
apply(compiler) {
// 把打包后的文件全部打包在一起生成一个文件包,压缩包
compiler.hooks.emit.tapAsync('ZipPlugin', (compilation, callback) => {
let zip = new JSZip();
for(let filename in compilation.assets) {
// 文件源代码
const source = compilation.assets[filename].source();
zip.file(filename, source);
}
// 异步生成压缩包
zip.generateAsync({type: 'nodebuffer'}).then(content => {
compilation.assets[this.options.filename] = new RawSource(content);
callback();
})
})
}
}
module.exports = ZipPlugin;
自动外链插件
- 使用外部类库(之前的做法)
-
- 手动指定externals:{jquey:"$"}
-
- 手动引入 script cdn地址
- 使用 import $ from 'jquery' 会自动在window上找 window.jquery
-
- 实现思路
- 解决import自动处理externals和script,需要怎么实现?
- 依赖当检测到有import该library时,将其设置为不打包 类似于externals,并在指定模版中加入script。那么如何检测import?用到parser
- webpack的externals是通过externalsPlugin实现的,ExternalsPlugin通过tap NormalModuleFactory在每次创建Module的时候判断是否是ExternalModule
- webpack4加入了模块类型之后,parser获取需要指定类型moduleType,一般使用javascript/auto即可
1. 自动实现externals
2. 自动向产出的html文件中插入cdn脚本
// 使用
plugins: [
new AutoExternalPlugin({
jquery: {
expose: '$', // jquery模块要从window上的哪个全局变量取值
url: 'https://cdn.bootcss.com/jquery/3.1.0/jquery.js', // 要向HTML中插入的脚本
}
})
]
// 实现
1. 通过ast语法树检测当前项目脚本中引入了那些模块 是不是引入了jquery
2. 如果发现引入了,则要自动插入cdn脚本
const {externalModule} = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
class AutoExternalPlugin{
constructor(options) {
this.options = options;
this.externalModules = Object.keys(this.options); // ['jquery', 'lodash']
this.importedModules = new Set(); // 存放着所有导入的外部依赖模块
}
apply(compiler){
// 每种模块都有一个对应的模块工厂来创建这个模块,普通模块对应的工作就是普通模块工厂
compiler.hooks.normalModuleFactory.tap('AutoExternalFactory', (normalModuleFactory) => {
normalModuleFactory.hooks.parser
.for('javascript/auto')
// 把源代码转成抽象语法树,然后进行遍历
// 遍历到不同类型的节点会触发不同的钩子 执行钩子对应的时间函数
.tap('AutoExternalPlugin', parser => {
// 拦截import
parser.hooks.import.tap('AutoExternalPlugin', (statement, source) => {
// console.log(statement, source);
if(this.externalModules.includes(source)) { // jquery
this.importedModules.add(source);
}
})
// 拦截对require的方法调用
parser.hooks.call.for("require").tap('AutoExternalPlugin', (expression) => {
console.log(expression);
let value = expression.arguments[0].value;
if(this.externalModules.includes(value)) {
this.importedModules.add(value);
}
})
})
// 改造创建模块的过程
// factorize 是一个 AsyncSeriesBailHook
normalModuleFactory.hooks.factorize.tapAsync('AutoExternalPlugin', (resolveData, callback) => {
let request = resolveData.request; // jquery
if(this.externalModules.includes(request)) {
let expose = this.options[request].expose;
// 创建一个外部模块并返回 jquery = window.jquery
callback(null, new externalModule(expose, 'window', request))
}else{
callback(); // 如果是正常模块 直接调用callback向后执行
}
})
})
compiler.hooks.compilation.tap('AutoExternalPlugin', (compilation) => {
HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tapAsync('AutoExternalPlugin', (htmlData, callback)=>{
let {assetTags} = htmlData;
let importedExternalModules = Object.keys(this.options).filter(item => this.importedModules.has(item));
importedExternalModules.forEach(key => {
assetTags.script.unshift({
tagsName: 'script',
voidTag: false,
attributes:{
src: this.options[key].url,
defer: false
}
})
})
callback(null, htmlData);
}
})
}
}
module.exports = AutoExternalPlugin;