初探webpack之编写plugin

webpack通过plugin机制让其使用更加灵活,以适应各种应用场景,当然也大大增加了webpack的复杂性,在webpack运行的生命周期中会广播出许多事件,plugin可以hook这些事件,在合适的时机通过webpack提供的API改变其在处理过程中的输出结果。

描述

webpack是一个现代JavaScript应用程序的静态模块打包器module bundler,当webpack处理应用程序时,它会递归地构建一个依赖关系图dependency graph,其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个bundle
使用webpack作为前端构建工具通常可以做到以下几个方面的事情:

  • 代码转换: TypeScript编译成JavaScriptSCSS编译成CSS等。
  • 文件优化: 压缩JavaScriptCSSHTML代码,压缩合并图片等。
  • 代码分割: 提取多个页面的公共代码、提取首屏不需要执行部分的代码让其异步加载。
  • 模块合并: 在采用模块化的项目里会有很多个模块和文件,需要构建功能把模块分类合并成一个文件。
  • 自动刷新: 监听本地源代码的变化,自动重新构建、刷新浏览器页面,通常叫做模块热替换HMR
  • 代码校验: 在代码被提交到仓库前需要校验代码是否符合规范,以及单元测试是否通过。
  • 自动发布: 更新完代码后,自动构建出线上发布代码并传输给发布系统。

webpack应用中有两个核心:

  • 模块转换器,用于把模块原内容按照需求转换成新内容,可以加载非js模块;
  • 扩展插件,在webpack构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要的事情。

本文编写的就是编写一个简单的webpack插件,设想一个简单的场景,假如我们实现了一个多页的Vue应用,每个打包的页面都会共享一个相同的头部和底部,也就是顶部navigation bar和底部的footer。因为类似于Vue这种框架都是在运行时才会加载出来头部与底部,而这部分代码实际上完全可以作为一个独立的公用子项目去开发,没必要在多页应用的每个页面都引用一次组件再让框架去解析组件。另外在多页应用页面之间跳转时,如果编写一个头部组件在每个页面组件内部去引用的话,很容易因为需要加载解析JS的时间比较长从而出现导航栏闪烁的问题。
如果要解决上边提到的问题的话,可以采用的一个方案就是使用静态页面片,我们可以将头部和底部的页面片在webpack打包的时候将其注入到要打包完成的html页面中,这样的话不但可以节省一些框架解析组件的JS消耗,而且还可以有更好的SEO表现。虽然只是一个头部与底部并未承载多少信息,但是如果是在SSR场景下大量的重复CPU任务,提升一点对于整体来说还是有一个比较大的提高的,就像图形学中画线的算法一样,架不住运算次数太多。此外这样可以比较好的解决组件头部闪烁的问题,因为其是随着HTML一并返回的,所以能立即渲染在页面上不需要JS的加载解析,同样对于骨架屏而言也是可以采用webpack注入页面片的这种方案加载,文中涉及到的所有代码都在https://github.com/WindrunnerMax/webpack-simple-environment

实现

搭建环境

初探webpack,那么便从搭建简单的webpack环境开始,首先是初始化并安装依赖。

$ yarn init -y
$ yarn add -D webpack webpack-cli cross-env

首先可以尝试一下webpack打包程序,webpack可以零配置进行打包,目录结构如下:

webpack-simple
├── package.json
├── src
│   ├── index.js
│   └── sum.js
└── yarn.lock
// src/sum.js
export const add = (a, b) => a + b;
// src/index.js
import { add } from "./sum";
console.log(add(1, 1));

之后写入一个打包的命令。

// package.json
{// ..."scripts": {"build": "webpack"},// ...
}

执行npm run build,默认会调用node_modules/.bin下的webpack命令,内部会调用webpack-cli解析用户参数进行打包,默认会以src/index.js作为入口文件。

$ npm run build

执行完成后,会出现警告,这里还提示我们默认modeproduction,此时可以看到出现了dist文件夹,此目录为最终打包出的结果,并且内部存在一个main.js,其中webpack会进行一些语法分析与优化,可以看到打包完成的结构是。

// src/main.js
(()=>{"use strict";console.log(2)})();

配置webpack

当然我们打包时一般不会采用零配置,此时我们就首先新建一个文件webpack.config.js。既然webpack说默认modeproduction,那就先进行一下配置解决这个问题,因为只是一个简单的webpack环境我们就不区分webpack.dev.jswebpack.prod.js进行配置了,简单的使用process.env.NODE_ENVwebpack.config.js中区分一下即可。在这里我们主要关心dist打包过后的文件,在这里就不进行dev环境的处理以及webpack-dev-server的搭建了,cross-env是用以配置环境变量的插件。

// package.json
{// ..."scripts": {"build": "cross-env NODE_ENV=production webpack --config webpack.config.js"},// ...
}
const path = require("path");
module.exports = {mode: process.env.NODE_ENV,entry: "./src/index.js",output: {filename: "index.js",path:path.resolve(__dirname, "dist")}
}

不过按照上边的需求来说,我们不光是需要处理js文件的,还需要处理html文件,这里就需要使用html-webpack-plugin插件。

$ yarn add -D html-webpack-plugin

之后在webpack.config.js中进行配置,简单配置一下相关的输入输出和压缩信息,另外如果要是想每次打包删除dist文件夹的话可以考虑使用clean-webpack-plugin插件。

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");module.exports = {mode: process.env.NODE_ENV,entry: "./src/index.js",output: {filename: "index.js",path:path.resolve(__dirname, "dist")},plugins:[new HtmlWebpackPlugin({title: "Webpack Template", filename: "index.html", // 打包出来的文件名 根路径是`module.exports.output.path`template: path.resolve("./public/index.html"),hash: true, // 在引用资源的后面增加`hash`戳minify: {collapseWhitespace: true,removeAttributeQuotes: true,minifyCSS: true,minifyJS: true,},inject: "body", // `head`、`body`、`true`、`false`scriptLoading: "blocking" // `blocking`、`defer`})]
}

编写插件

之后到了正文环节,此时我们要编写一个插件去处理上边提到的需求,具体实现来看,我们需要的是首先在html中留下一个类似于<!-- inject:name="head" -->的标记注释,之后在webpack打包时对于html文件进行一次正则匹配,将注释相关的信息替换成页面片,通过name进行区分到底要加载哪一个页面片。另外个人感觉实际上编写webpack插件的时候还是首先参考其他人编写的webpack插件的实现,自己去翻阅文档成本查阅各种hook的成本有点高。
对于这个插件我们直接在根目录建立一个static-page-slice.js,插件由一个构造函数实例化出来,构造函数定义apply方法,在webpack处理插件的时候,apply方法会被webpack compiler调用一次。apply方法可以接收一个webpack compiler对象的引用,从而可以在回调函数中访问到compiler对象。一个最基础的Plugin的结构是类似于这样的:

class BasicPlugin{// 在构造函数中获取用户给该插件传入的配置constructor(options){this.options = options || {};}// `Webpack`会调用`BasicPlugin`实例的`apply`方法给插件实例传入`compiler`对象apply(compiler){compiler.hooks.someHook.tap("BasicPlugin", (params) => {/* ... */});}
}// 导出 Plugin
module.exports = BasicPlugin;

在开发plugin时最常用的两个对象就是compilercompilation,它们是pluginwebpack之间的桥梁,compilercompilation的含义如下:

  • compiler对象包含了webpack环境所有的的配置信息,包含optionsloadersplugins这些信息,这个对象在webpack启动时候被实例化,它是全局唯一的,可以简单地把它理解为webpack实例。
  • compilation对象包含了当前的模块资源、编译生成资源、变化的文件等,当webpack以开发模式运行时,每当检测到一个文件变化,一次新的compilation将被创建,compilation对象也提供了很多事件回调供插件做扩展,通过compilation也能读取到compiler对象。

compilercompilation的区别在于: compiler代表了整个webpack从启动到关闭的生命周期,而compilation只是代表了一次新的编译,与之相关的信息可以参考https://webpack.docschina.org/api/compiler-hooks/
webpack就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果,这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理,插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理,webpack通过tapable来组织这条复杂的生产线https://github.com/webpack/tapable
在这里我们选择在compiler钩子的emit时期处理资源文件,即是在输出assetoutput目录之前执行,在此时要注意emit是一个AsyncSeriesHook也就是异步的hook,所以我们需要使用TapabletapAsync或者tapPromise,如果选取的是同步的hook,则可以使用tap

class StaticPageSlice {constructor(options) {this.options = options || {};}apply(compiler) {compiler.hooks.emit.tapPromise("StaticPageSlice", compilation => {return new Promise(resolve => {console.log("StaticPageSlice is being called")resolve();})});}
}module.exports = StaticPageSlice;

接下来我们正式开始处理逻辑,首先此处我们需要先判断这个文件的类型,我们只需要处理html文件,所以我们需要先一下是否为html文件,之后就是一个正则匹配的过程,匹配到注释信息以后,将其替换为页面片,这里的页面片我们就直接在此处使用Promise模拟一下异步过程就好,之后便可以在webpack中引用并成功打包了。

// static-page-slice.js
const simulateRemoteData = key => {const data = {header: "<div>HEADER</div>",footer: "<div>FOOTER</div>",}return Promise.resolve(data[key]);
}class StaticPageSlice {constructor(options) {this.options = options || {}; // 传递参数}apply(compiler) {compiler.hooks.emit.tapPromise("StaticPageSlice", compilation => {return new Promise(resolve => {const cache = {};const assetKeys = Object.keys(compilation.assets);for (const key of assetKeys) {const isLastAsset = key === assetKeys[assetKeys.length - 1];if (!/.*\.html$/.test(key)) {if (isLastAsset) resolve();continue;}let target = compilation.assets[key].source();const matchedValues = target.matchAll(/<!-- inject:name="(\S*?)" -->/g); // `matchAll`函数需要`Node v12.0.0`以上const tags = [];for (const item of matchedValues) {const [tag, name] = item;tags.push({tag,name,data: cache[name] ? cache[name] : simulateRemoteData(name),});}Promise.all(tags.map(item => item.data)).then(res => {res.forEach((data, index) => {const tag = tags[index].tag;const name = tags[index].name;if (!cache[name]) cache[name] = data;target = target.replace(tag, data);});}).then(() => {compilation.assets[key] = {source() {return target;},size() {return this.source().length;},};}).then(() => {if (isLastAsset) resolve();});}});});}
}module.exports = StaticPageSlice;
// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const StaticPageSlice = require("./static-page-slice");module.exports = {mode: process.env.NODE_ENV,entry: "./src/index.js",output: {filename: "index.js",path:path.resolve(__dirname, "dist")},plugins:[new HtmlWebpackPlugin({title: "Webpack Template", filename: "index.html", // 打包出来的文件名 根路径是`module.exports.output.path`template: path.resolve("./public/index.html"),hash: true, // 在引用资源的后面增加`hash`戳minify: {collapseWhitespace: true,removeAttributeQuotes: true,minifyCSS: true,minifyJS: true,},inject: "body", // `head`、`body`、`true`、`false`scriptLoading: "blocking" // `blocking`、`defer`}),new StaticPageSlice({url: "https://www.example.com/"})]
}

之后便可以看到打包前后的html文件的差别了。

<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title><%= htmlWebpackPlugin.options.title %></title></head><body><!-- inject:name="header" --><div id="app"></div><!-- inject:name="footer" --><!-- built files will be auto injected --></body>
</html>
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><title>Webpack Template</title></head><body><div>HEADER</div><div id=app></div><div>FOOTER</div><!-- built files will be auto injected --><script src=index.js?7e2c7994f2e0891ec351></script></body></html>

webpack5对于hooks有一次更新,使用上边的插件会提示:

(node:5760) [DEP_WEBPACK_COMPILATION_ASSETS] DeprecationWarning: Compilation.assets will be frozen in future, all modifications are deprecated.
BREAKING CHANGE: No more changes should happen to Compilation.assets after sealing the Compilation.Do changes to assets earlier, e. g. in Compilation.hooks.processAssets.Make sure to select an appropriate stage from Compilation.PROCESS_ASSETS_STAGE_*.

所以我们可以根据其提示提前将资源进行处理,可以实现同样的效果。

// static-page-slice.js
const simulateRemoteData = key => {const data = {header: "<div>HEADER</div>",footer: "<div>FOOTER</div>",};return Promise.resolve(data[key]);
};class StaticPageSlice {constructor(options) {this.options = options || {}; // 传递参数}apply(compiler) {compiler.hooks.thisCompilation.tap("StaticPageSlice", compilation => {compilation.hooks.processAssets.tapPromise({name: "StaticPageSlice",stage: compilation.constructor.PROCESS_ASSETS_STAGE_ADDITIONS,additionalAssets: true,},assets => this.replaceAssets(assets, compilation));});}replaceAssets(assets, compilation) {return new Promise(resolve => {const cache = {};const assetKeys = Object.keys(assets);for (const key of assetKeys) {const isLastAsset = key === assetKeys[assetKeys.length - 1];if (!/.*\.html$/.test(key)) {if (isLastAsset) resolve();continue;}let target = assets[key].source();const matchedValues = target.matchAll(/<!-- inject:name="(\S*?)" -->/g); // `matchAll`函数需要`Node v12.0.0`以上const tags = [];for (const item of matchedValues) {const [tag, name] = item;tags.push({tag,name,data: cache[name] ? cache[name] : simulateRemoteData(name),});}Promise.all(tags.map(item => item.data)).then(res => {res.forEach((data, index) => {const tag = tags[index].tag;const name = tags[index].name;if (!cache[name]) cache[name] = data;target = target.replace(tag, data);});}).then(() => {compilation.assets[key] = {source() {return target;},size() {return this.source().length;},};}).then(() => {if (isLastAsset) resolve();});}});}
}module.exports = StaticPageSlice;

每日一题

https://github.com/WindrunnerMax/EveryDay

参考

https://webpack.docschina.org/concepts/
https://juejin.cn/post/6854573216108085261
https://webpack.docschina.org/api/plugins/
https://juejin.cn/post/6844903942736838670
https://segmentfault.com/a/1190000012840742
https://segmentfault.com/a/1190000021821557
https://webpack.docschina.org/api/compilation-hooks/
https://webpack.docschina.org/api/normalmodulefactory-hooks/

初探webpack之编写plugin相关推荐

  1. 初探webpack之从零搭建Vue开发环境

    初探webpack之搭建Vue开发环境 平时我们可以用vue-cli很方便地搭建Vue的开发环境,vue-cli确实是个好东西,让我们不需要关心webpack等一些繁杂的配置,然后直接开始写业务代码, ...

  2. n 如何编写html,webpack4系列教程,如何编写plugin处理html代码逻辑?

    本博客不欢迎:各种镜像采集行为,请尊重知识产权法律法规.大家都是程序员,不要闹得不开心. 在上一篇文章中,利用不同位置的publicPath,对html中的cdn地址,进行了处理.但是,遗留了一个小问 ...

  3. webpack 打包(plugin、loader 工作原理)

    模块化工具 由来 ES Modules 存在环境兼容问题 模块文件过多,网络请求频繁 所有的前端资源都需要模块化 概要 Webpack 作为 模块打包器(Module bundler),可以把零散的文 ...

  4. webpack入门(四)——webpack loader 和plugin

    什么是loader loaders是你用在app源码上的转换元件.他们是用node.js运行的,把源文件作为参数,返回新的资源的函数.  例如,你可以用loaders告诉webpack加载 coffe ...

  5. Webpack4 学习笔记一初探Webpack

    前言 此内容是个人学习笔记,以便日后翻阅.非教程,如有错误还请指出 Webpack 打包文件 支持JS模块化 模式: production(0配置默认), development(生产环境) 更详细的 ...

  6. webpack的一些plugin,怎么使用webpack对项目进行优化

    构建优化 1.减少编译体积 ContextReplacementPugin.IgnorePlugin.babel-plugin-import.babel-plugin-transform-runtim ...

  7. 实现前端项目自动化部署(webpack+nodejs)

    前言: 一般来说,我们前端是不需要关心部署的事情的,只需要把打包后的文件直接丢给后台去部署就可以了.但是呢,如果频繁修改一点东西就要叫后台进行部署,这样后台会很烦(毕竟人家还有其他工作嘛),我们也会很 ...

  8. Webpack中Loader和Plugin的区别和编写思路

    由于webpack基于发布订阅模式,在运行的生命周期中会广播出许多事件,插件通过监听这些事件,就可以在特定的阶段执行自己的插件任务 一.区别 前面两节我们有提到Loader与Plugin对应的概念,先 ...

  9. [转] webpack之plugin内部运行机制

    简介 webpack作为当前最为流行的模块打包工具,几乎所有的主流前端开发框架(React.Vue等)都会将其作为默认的模块加载和打包工具.通过简单的配置项,使用各种相关的loader和plugin, ...

最新文章

  1. asp.net ajax删除数据,Asp.net MVC 2 使用Ajax删除数据
  2. 网络品牌推广带大家了解网站中有哪些常见的URL优化手段?
  3. android combobox控件,Android中的组合框
  4. Hybris DDIC type and its counterpart model class
  5. viso图插入Word中大片空白解决办法
  6. DDD~领域事件应用篇(订单处理变得更清晰)
  7. 如何设计领域特定语言,实现终极业务抽象?
  8. 蓝牙版本avrcp怎么选_「科技犬」除了苹果AirPods,真无线蓝牙耳机到底怎么选?...
  9. 关于C#中Remoting的使用
  10. 日常学习笔记-RGB配色和颜色转换
  11. Java学习笔记第七天:极其基础的家庭记账系统
  12. Aria2基础使用教程
  13. 软件定义汽车下的整车开发
  14. html5加号展开减号缩减,CSS3 linear-gradient线性渐变生成加号和减号的方法
  15. excel公式编辑器_Excel如何用函数公式制作随机抽奖小程序
  16. 图像分解python_利用奇异值分解(SVD)进行图像压缩-python实现
  17. 合上笔记本屏幕 Ubuntu 20.04 不休眠
  18. 状态栏和导航栏重叠,解决办法
  19. ionic3 使用QR Scaner 扫描
  20. 2018年高德地图POI全国数据下载

热门文章

  1. Linus 07年在 Google讲座介绍Git的特点和设计思路
  2. Nacos数据库配置
  3. 深入理解JVM逃逸分析
  4. nacos服务注册与发现
  5. 用javascript完成pos机的输入输出
  6. windows 下安装 elasticsearch 以及 head 管理插件
  7. 升级到AKU3.3 v1.1,感受智能手机的VGA模式
  8. PgSQL · 应用案例 · 经营、销售分析系统DB设计之共享充电宝
  9. 如何在GPT分区上安装WIN7
  10. UIKit 框架之UIAlertController