【初学者笔记】前端工程化必须要掌握的 webpack
安装
现在学习的是 webpack4
的最新版,新建文件夹,npm init -y
初始化一下,然后执行下面命令进行安装,需要同时安装 webpack
和 webpack-cli
。
npm install webpack@4.43.0 webpack-cli@3.3.12 -D
不建议全局安装 webpack
, 因为全局安装会锁定版本,多个项目之间可能依赖的 webpack
版本不同, 所以还是建议项目内安装。
可以根目录下新建 .npmrc
文件,指定当前项目安装依赖的源地址
registry=https://registry.npm.taobao.org
webpack 的零配置
webpack
宣传的一个噱头就是零配置构建, 也就是什么配置都不写就可以进行打包,虽然确实是可以执行,但其实是满足不了实际开发需要的。
先来随便跑一下,新建 src/index.js
文件。我们在 index.js
中 只写一行
console.log("hello, webpack !")
然后执行
npx webpack -v
npx
在当前项目下查找并启动 webpack
。可以看到根目录下生成 dist/main.js
文件。
建议在 package.json
文件中,添加构建命令,便于添加构建参数,也更加易读。
"scripts": {
"dev": "webpack"
},
虽然我只写了一行代码,但是打包出来却有这么多字符,看下面的进度条就知道后面还有很多了。
是因为 webpack
默认给做了很多兼容,这也是相对于其他构建工具,出现了很多的冗余代码。
但其实问题不大, webpack5
进行了一些优化,但是当模块变的越来越多时候,就没有什么太大改变了。
构建信息
看一下构建时都输出了什么信息
打包出来的文件内容有很多,但是主结构很简单,其实就是一个自执行函数,如下
(function(modules) {
// todo
})({
"./src/login.js": (function(module, exports) {
eval("xxxxx")
})
})
在这个结构中,可以看到自执行函数的参数是一个对象,对象的每个 key
的 value
都包含一个 chunk
,也就是 代码片段。
也可以包含多个 chunk
, 被称为 chunks
, chunk组
,还可以被称为 chunk Names
。
这一整个对象中可以包含多个 key:value
,被称为依赖图谱。
构建后产生的资源文件, 称呼为 bundle
文件 。
几者遵循以下关系:
- 一个
bundle
对应至少一个chunks
- 一个
chunks
对应至少一个module
- 一个
module
对应至少一个chunk
一句话总结:
module
,chunk
和 bundle
其实就是同一份逻辑代码在不同转换场景下的取了三个名字:
我们直接写出来的是 module
,webpack
处理时是 chunk
,最后生成浏览器可以直接运行的 bundle
。
webpack的配置文件
如果没有添加配置文件,会走它的默认配置,也就是所谓的零配置,如果添加了配置文件 webpack
会按照配置文件里的配置进行打包。
通常情况下 webpack
的配置文件的文件名叫做 webpack.config.js
,其实也可以通过配置修改成别的,但是基本上没什么必要。
修改 package.json
文件,在 script
中 新建命令即可
--config
指定配置文件
"warbler": "webpack --config ./warbler.config.js"
由于配置文件的内容是非常多的,语法也很难一时全记下来,所以我们可以通过一点点小技巧,就可以让配置文件具有语法提示。
首先安装 @types/webpack
。
npm i -D @types/webpack
然后再配置文件中注明类型就可以了。
// webpack.config.js
/**
* @type {import('webpack').Configuration}
*/
module.exports = {
}
babel.config.js
也是支持同样方式的。
npm i -D @types/babel__core
/**
* @type {import('@babel/core').TransformOptions}
*/
vue.config.js
也是可以的,且不用安装其他包。
/**
* @type {import('@vue/cli-service').ProjectOptions}
*/
webpack的配置参数
webpack
是基于 nodejs
的, 所有的 nodejs
核心模块的 api
都可以直接引入使用。
entry
执行打包任务的入口,默认是 src/index
,支持相对路径, 也支持绝对路径。
entry: "./src/index.js"
支持 string
, arr
, object
如果传入的是字符串,也会在构建期间被转换成对象结构,上面的代码和下面的代码结果是一样的。
entry: {
main: "./src/index.js"
},
支持 spa
(单入口) 和 mpa
(多入口)。多入口会输出多个 bundle
文件。
entry: {
index: "./src/index.js",
login: "./src/login.js",
home: "./src/home.js"
},
output
输出资源文件的信息 , 包括 存储位置 , 文件名称
path
: 存储位置,打包出来的文件放在哪里 默认是dist
要求是绝对路径;filename
: 文件名称,打包出来的文件叫什么。
output: {
path: path.resolve(__dirname, './dist'),
filename: "main.js"
},
但是如果 entry
设置了多入口的话,就会产生多个 main.js
这样就会报错。
解决方法就是使用 占位符,语法如下
filename: "[name].js"
作用就是 entry
中的 key
叫什么,打包后的 filename
就叫什么,多个入口就会对应多个出口了。
mode
打包模式,默认是生产模式,生产模式下会做代码压缩,摇树等操作。
development
开发模式production
生产模式none
默认
plugins
插件,插件的本质就是一个类。后面会单独讲解各种常用插件的配置。
plugins
的执行顺序是从上到下的。
plugins:[]
module
模块,webpack
默认只支持 .js
, .json
文件,像我们平时常用的 .vue
, .png
, .scss
, .css
, .ts
等都是不支持的。
所以如果想要 webpack
支持其他类型的文件,就需要不同类型的 loader
进行解析。
下面这个配置的意思就是,当检测(test)到 .css
后缀文件的时候,使用(use) 什么 loader
来进行处理。
// webpack.config.js
module: {
rules: [
{
test: /\.css$/,
use: ["style-loader", "css-loader"]
},
]
}
- css-loader :把
css
模块序列化,让webpack
认识css
语法并可以生成chunk
。 - style-loader :把
css
整合在html
文件的head
标签的style
中。
当多个 loader
作用于 一个模块的时候, 是按照自右向左的顺序执行的,也就是先执行 css-loader
在执行 style-loader
。
其实一个 loader
也可以搞定上面两件事,但是建议一个 loader
只做一件事情,也就是遵循 单一职责 原则。
也可以给 loader
书写配置,这个时候就要用对象的结构了。
// webpack.config.js
module: {
rules: [
{
test: /\.less$/,
use: [
{
loader: 'style-loader',
},
{
loader: "css-loader",
options: {
modules: true
}
},
{
loader: 'postcss-loader',
},
{
loader: 'less-loader',
}
]
}
]
}
- less-loader :把
less
语法 转化为css
语法。 - postcss-loader :
postcss
是一个 工具集 ,这个就十分强大了,postcss
对css
的意义,就等同于babel
对于js
的意义,他自身作为插件,还可以携带插件。
resolveLoader
解析 loader
, 告知 webpack
如何匹配 loader
。当我们自定义一些 loader
的时候。可以通过绝对路径的方式导入,但是过于繁琐,当我们通过这个字段指定文件夹的时候,就可以像使用第三方 loader
一样直接写 loader
的名字了。
// webpack.config.js
resolveLoader: {
// 默认是 node_modules
modules: ["node_modules", "./myLoaders"]
},
常用插件的常用配置
html-webpack-plugin
版本:4.5.2
- template:使用指定模板生成
html
。 - filename:打包后的
html
文件名。 - chunks:打包后的
html
会包含哪些chunk
。
// webpack.config.js
plugins: [
// 自动生成 html 文件 , 引入 bundle 文件, 压缩 html
new htmlWebpackPlugin({
// 模板匹配
template: "./src/index.html",
filename: "index.html",
chunks: ["index", 'login'],
}),
]
如果要生成多个 html
,只需要创建多个实例即可。
// webpack.config.js
plugins: [
// 自动生成html 文件 ,引入bundle文件,压缩html
new htmlWebpackPlugin({
// 模板匹配
template: "./src/index.html",
filename: "index.html",
chunks: ["index", 'login'],
}),
// 自动生成html 文件 ,引入bundle文件,压缩html
new htmlWebpackPlugin({
// 模板匹配
template: "./src/index.html",
filename: "login.html",
chunks: ["login"],
}),
// 自动生成html 文件 ,引入bundle文件,压缩html
new htmlWebpackPlugin({
// 模板匹配
template: "./src/index.html",
filename: "home.html",
chunks: ["home"],
}),
]
但是作为能懒则懒得程序员,是万万不可能写这种代码的,因为 webpack
的配置文件本身就是一个对象。所以我们当然可以利用一些方法来自动生成配置项。
首先要规定好结构,本例中每个文件夹中存放一个 index.html
和 index.js
,结构如下。当然可以自己随意定义结构,然后自己编写对应的函数体就可以了。
├── src
├── list
│ ├── index.html
│ └── index.js
├── login
│ ├── index.html
│ └── index.js
├── detail
│ ├── index.html
│ └── index.js
├── index
│ ├── index.html
│ └── index.js
配置如下,利用 setMap
方法自动生成 entry
和 htmlWebpackPlugins
,这样每次添加文件都不用我们修改配置了。
// webpack.config.js
const path = require('path')
const htmlWebpackPlugin = require("html-webpack-plugin")
// 模糊匹配路径
const glob = require('glob')
// 自动生成 entry 和 htmlWebpackPlugins
const setMap = () => {
const entry = {};
const htmlWebpackPlugins = []
// 模糊匹配 src 目录下 任意目录下的 index.js 返回的是文件的绝对路径
const entryFiles = glob.sync(path.join(__dirname, "./src/*/index.js"))
// 遍历匹配到的结果
entryFiles.forEach((entryFile) => {
// 获取到文件名
const pageName = entryFile.match(/src\/(.*)\/index\.js$/)[1]
// 生成 entry
entry[pageName] = entryFile
// 生成 htmlWebpackPlugins
htmlWebpackPlugins.push(
new htmlWebpackPlugin({
template: `./src/${pageName}/index.html`,
filename: `${pageName}.html`,
chunks: [pageName]
}))
})
return {
entry,
htmlWebpackPlugins
}
}
const { entry, htmlWebpackPlugins } = setMap()
// 配置文件
module.exports = {
entry,
// 输出资源文件的信息
output: {
// 存储位置
path: path.resolve(__dirname, './dist'),
// 文件名称
filename: " [name].js"
},
// 打包模式
mode: "development",
// 插件
plugins: [
...htmlWebpackPlugins,
],
}
postcss
postcss
也有他自己的配置文件,根目录下新建 postcss.config.js
,也是个模块,通过require("autoprefixer")
安装插件。
// postcss.config.js
module.exports = {
plugins: [
require("autoprefixer"),
require("cssnano")
]
}
postcss的插件autoprefixer
autoprefixer
自动给 css
属性加前缀,使用 can i use
的数据。
但是这样还不会生效,需要设置 browserslist
,目标浏览器集合 ,也就是 需要兼容的浏览器版本,用到浏览器集合的工具会根据 browserslist
的描述, 针对性的输出兼容性的代码。
browserslist
的设置有两种方式。
你可以直接在 package.json
文件中添加属性如下
"browserslist": [
">1%",
"last 2 versions"
]
也可以根目录下新建 .browserslistrc
文件,直接写,什么括号都不用。
// .browserslistrc
>1%
last 2 versions
- >1% : 全球市场占有率 大于1%的浏览器。
- last 2 versions : 兼容浏览器的最近两个大版本。
可以使用 npx browserslist
查询兼容的浏览器版本。
postcss的插件cssnano
压缩 css
代码。
mini-css-extract-plugin
把 css
代码抽离出来单独放到一个文件里,可以指定文件名,支持绝对路径,会自动生成文件夹。需要在写 loader
的位置, 使用 minicss.loader
替换 style-loader
。
// webpack.config.js
const minicss = require("mini-css-extract-plugin")
module.exports = {
plugins: [
new minicss({
filename: 'style/index.css'
})
],
module: {
rules: [
{
test: /\.less$/,
use: [
minicss.loader,
{
loader: "css-loader",
options: {
modules: true
}
},
{
loader: 'postcss-loader',
},
{
loader: 'less-loader',
}
]
}
]
}
}
clean-webpack-plugin
每次打包之前清理打包目录。
// webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
plugins: [
new CleanWebpackPlugin()
],
}
实现自定义loader
loader的引用
根目录下新建 myLoaders/a-loader.js
文件,然后在 module
中需要使用绝对路径的方式引入。
// webpack.config.js
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: path.resolve(__dirname, './myLoaders/a-loader.js'),
},
]
},
]
}
loader的结构
loader
就是一个函数。接收一个参数 source
,source
就是符合 test
中正则的模块内容,本例中就是 .js
后缀的文件内容。
- 这个函数不可以是 箭头函数,因为
webpack
提供了loader api
都是挂载到this
上的。 - 这个函数必须有 返回值,否则会报错。
// a-loader.js
module.exports = function(source) {
console.log('🚀🚀~ source:', source);
return source
}
src/index.js
文件中只输出一行日志。
// src/index.js
console.log('hello, world');
把 source
打印出来看一下,source
就是 src/index.js
这个文件还没有经过 webpack
编译的内容。
loader的编写
在 a-loader.js
中,我们把 hello
替换成 你好
,再返回。
// a-loader.js
module.exports = function(source) {
const _source = source.replace('hello', '你好' )
return _source
}
看一下打包后的 chunk
, 变成了 console.log('你好, world')
,说明我们自定义的 loader
已经生效了。
在我们使用第三方 loader
的时候,通常会定义一个配置项 options
,那么我们的自定义 loader
怎么设置这个 options
呢?
首先还是像其它 loader
一样在 use
中设置一个 options
属性。
// webpack.config.js
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: path.resolve(__dirname, './myLoaders/a-loader.js'),
options: {
name: "一尾流莺"
}
},
]
},
]
}
然后在自定义 loader
中通过 this.query
获取到 options
的内容,这也就说明了为什么 loader
不能是个箭头函数的原因了。
把 world
替换成我们设置的 options
中的 name
。
// a-loader.js
module.exports = function(source) {
const _source = source.replace('world', this.query.name )
return _source
}
好,成功替换。
在 loader
中进行异步操作是很正常的事,那么我们需要使用 this.callback
,一个可以同步或者异步调用的可以返回多个结果的函数。
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
);
- 第一个参数必须是
Error
或者null
- 第二个参数是一个
string
或者Buffer
。 - 可选的:第三个参数必须是一个可以被这个模块解析的
source map
。 - 可选的:第四个选项,会被
webpack
忽略,可以是任何东西(例如一些元数据)。
我们还需要通过 this.async
告诉 loader
的解析器这个 loader
将会异步地回调。
新建 myLoaders/b-loader.js
文件,使用 setTimeout
来模拟一段异步操作。
// b-loader.js
module.exports = function(source) {
const _callback = this.async();
const _source = source.replace('world', this.query.name)
setTimeout(() => {
_callback(null, _source)
}, 3000)
}
然后引用 loader
。
// webpack.config.js
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: path.resolve(__dirname, './myLoaders/b-loader.js'),
options: {
name: "一尾流莺"
}
},
]
},
]
}
看一下构建时间,确实是三秒多。
结果也如我们所愿,把 world
替换成了我的名字 一尾流莺。
异步 loader
不会影响其他模块的 loader
,但是会影响多个 loader
作用于一个模块的 loader
。
一个 loader
执行完,才会交给下一个 loader
直到没有 loader
,最后交给 webpack
。
我们来测试一下两个 loader
作用于同一个模块的情况,由于 loader
的运行顺序是从右到左,所以我们先写 b-loader
,后写 a-loader
。
// webpack.config.js
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: path.resolve(__dirname, './myLoaders/b-loader.js'),
options: {
name: "一尾流莺"
}
},
{
loader: path.resolve(__dirname, './myLoaders/a-loader.js'),
},
]
},
]
}
// b-loader.js
module.exports = function(source) {
const _callback = this.async();
const _source = source.replace('world', this.query.name)
setTimeout(() => {
_callback(null, _source)
}, 3000)
}
// a-loader.js
module.exports = function(source) {
const _source = source.replace('hello', '你好')
return _source
}
可以看到,两个 loader
的作用都生效了。
但是有个问题,我们自定义的 loader
用绝对路径的引用方式,实在是太繁琐了,也不已读。
解决方案就是上面提到过的配置参数 resolveLoader
,这样就可以像使用第三方 loader
一样直接写 loader
的名字了。
// webpack.config.js
resolveLoader: {
// 默认是 node_modules
modules: ["node_modules", "./myLoaders"]
},
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: "b-loader",
options: {
name: "一尾流莺"
}
},
{
loader: "a-loader",
},
]
},
]
}
练习手写 loader
my-style-loader
module.exports = function(source) {
return `
const tag = document.createElement("style")
tag.innerHTML = ${source}
document.head.appendChild(tag)
`
}
my-css-loader
module.exports = function(source) {
return JSON.stringify(source)
}
my-less-loader
const less = require("less")
module.exports = function(source) {
less.render(source, (error, output) => {
const cssInfo = output.css
this.callback(error, cssInfo)
})
}
处理静态资源
前面提到过除了 .js
和 .json
之外的文件类型,webpack
是不认识的,所以我们处理静态资源的时候,也是需要使用不同的 loader
的。
接下来我们将学习新的 loader
来处理图片资源。
file-loader
作用:导出一个资源,并返回路径。
配置项(options
):
- name :图片名称,可以使用占位符。
[name]
名称,[ext]
后缀。 - outputPath :输出资源的存储位置,默认相对于
dist
目录。 - publicPath :资源的使用位置,
publicPath
+name
=css中图片的完整使用路径
。
// webpack.config.js
module: {
rules: [
{
test: /\.(jpe?g|png|gif|webp)$/,
use: [
{
loader: "file-loader",
options: {
name: "[name].[ext]",
outputPath: "images",
publicPath: "../images",
}
},
]
},
]
}
url-loader
作用: 依赖 file-loader
,把图片转成 base64
编码,可以减少图片请求。
配置项(options
):
- name :图片名称,可以使用占位符。
[name]
名称,[ext]
后缀。 - outputPath :输出资源的存储位置,默认相对于
dist
目录。 - publicPath :资源的使用位置,
publicPath
+name
=css中图片的完整使用路径
。 - limit : 超过指定大小的图片不会被转化成
base64
,单位字节。
// webpack.config.js
module: {
rules: [
{
test: /\.(jpe?g|png|gif|webp)$/,
use: [
{
loader: "url-loader",
options: {
name: "[name].[ext]",
outputPath: "images",
publicPath: "../images",
limit: 4 * 102,
}
},
]
},
]
}
image-webpack-loader
这个 loader
需要使用 cnpm
才能安装成功。
cnpm i image-webpack-loader -D
作用:图片压缩
// webpack.config.js
module: {
rules: [
{
test: /\.(jpe?g|png|gif|webp)$/,
use: [
{
loader: 'image-webpack-loader',
}
]
},
]
}
看一下源文件和输出后的文件对比,效果还是很显著的。
webpack5中的图片处理方式
// webpack.config.js
module: {
rules: [
{
test: /\.(jpe?g|png|gif|webp)$/,
// asset 通用配置 默认以 8kb 为界限 超过使用 asset/resource 否则使用 asset/inline
// asset/resource 等同于 file-loader 的作用
// asset/inline 等同于 url-loader 的作用
type: "asset/resource",
// 只有在 asset/resource 才可以设置 generator 属性
generator: {
filename: "images/[name][ext]",
}
parser: {
dataUrlCondition: {
// 只有在 asset 设置 maxSize 属性才会生效
maxSize: 1 * 1024
}
}
}
]
}
字体文件的处理
字体文件是这样引用的。
// index.less
@font-face {
font-family: 'webfont';
font-display: swap;
src: url('../font/webfont.eot'); /* IE9 */
src: url('../font/webfont.eot?#iefix') format('embedded-opentype'),
/* IE6-IE8 */ url('../font/webfont.woff2') format('woff2'),
url('../font/webfont.woff') format('woff'),
/* chrome、firefox */ url('../font/webfont.ttf') format('truetype'),
/* chrome、firefox、opera、Safari, Android, iOS 4.2+*/
url('../font/webfont.svg#webfont') format('svg'); /* iOS 4.1- */
}
body {
div {
height: 100px;
background: blue;
display: flex;
background: url(../images/main.jpg) 0 0 no-repeat;
font-family: 'webfont' !important;
}
}
字体文件的处理和图片是一样的,用的 loader
也是一样的。
字体文件也可以转成 base64
格式的,所以也可以用 url-loader
。
// webpack.config.js
module: {
rules: [
{
test: /\.(eot|svg|ttf|woff|woff2)$/,
use: [
{
loader: "file-loader",
options: {
name: "[name].[ext]",
outputPath: "font",
publicPath: "../font",
}
},
]
},
]
}
处理JavaScript模块
babel
首先要同时安装下面两个,babel-loader
依赖 @babel-core
。
npm i @babel-core babel-loader -D
babel
跟 postcss
很像,是一个工具集,他自己不会做任何事情,需要依靠他自身的插件。
babel
也是一个可以用到浏览器集合的工具。
@babel/preset-env
用来处理 es6+
语法的兼容问题。
我们先写点 es6
的新东西。
// index.js
const arr = [new Promise(() => { })]
然后配置插件。
// webpack.config.js
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'babel-loader',
options: {
presets: ["@babel/preset-env"]
}
}
]
},
]
}
打包看看结果。
可以看到,const
已经被转成了 var
, 但是 promise
并没有被转化。因为上面已经说过了,这个插件只用于转化 es6+
语法,而新特性是管不到的。
那怎么办呢?当然还要依赖其他的小伙伴了。
@babel/polyfill
这个包是在运行时起作用的,所以要安装到生产依赖里。
npm i @babel/polyfill -S
这个包里包含了所有新特性,要在所有代码之前引用。
// index.js
import "@babel/polyfill"
const arr = [new Promise(() => { })]
在 babel7.4
以后的版本中,建议使用新的方式。
// index.js
import "core-js/stable";
import "regenerator-runtime/runtime";
const arr = [new Promise(() => { })]
但是由于这个包过大,所以我们还需要配置按需引入,这还是要通过 @babel/preset-env
的 options
来配置。
// webpack.config.js
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'babel-loader',
options: {
presets: [
["@babel/preset-env", {
// 单独指定浏览器集合
targets: {},
// 指定core-js的版本 需要安装 npm i core-js@3 -S
corejs: 3,
/**
* false 当我们引入polyfill,不会进行按需引入,bundle体积会很大
* entry 需要在入口文件里导入 import "@babel/polyfill" babel会帮我们按需引入
* usage 全自动检测 不需要导入polyfill 即可达到按需引入的目的
*/
useBuiltIns: "usage"
}]
]
}
}
]
},
]
}
babel
也可以有自己的配置文件 。 根目录下新建 babel.config.js
文件,把 options
中的内容挪过来即可,当然也要作为模块导出。
// babel.config.js
module.exports = {
presets: [
["@babel/preset-env", {
targets: {},
corejs: 3,
useBuiltIns: "usage"
}]
]
}
集成react
集成 react
,安装依赖。
npm i react react-dom -S
npm i @babel/preset-react -D
配置 babel.config.js
。
// babel.config.js
module.exports = {
presets: [
["@babel/preset-react", {}]
]
}
集成vue
集成 vue
,安装依赖。这个其实跟 babel
没啥关系,顺便在这写了。
要注意 vue
和 vue-template-compiler
的版本要保持一致。
npm i vue vue-loader vue-template-compiler -D
配置 webpack.config.js
。
// webpack.config.js
const { VueLoaderPlugin } = require('vue-loader')
module.exports = {
module: {
rules: [
// ... 其它规则
{
test: /.vue$/,
loader: 'vue-loader'
}
]
},
plugins: [
// 请确保引入这个插件!
new VueLoaderPlugin()
]
}
实现自定义plugin
plugin的结构
根目录下新建 myPlugins/my-plugin.js
。
plugin
的实质就是一个类。通过构造函数接收 options
配置。每个 plugin
必须内置一个 apply
方法,该方法接收一个参数 compiler
,就是实例化的 webpack
,其中包含配置等信息。
// my-plugin.js
class MyPlugin {
constructor(options) {
console.log('🚀🚀~ options:', options);
}
apply(compiler) {
// console.log('🚀🚀~ : my-plugin');
}
}
module.exports = MyPlugin
plugin的引用
新建实例的方式引用,可以在创建实例的时候传递 options
。
// webpack.config.js
const MyPlugin = require("./myPlugins/my-plugin")
module.exports = {
plugins: [
new MyPlugin({
name: '一尾流莺'
})
]
}
plugin的编写
怎么确定 plugin
的执行时机?
其实 webpack
也是有生命周期钩子的。我们可以来看一下都有哪些钩子。
// 跟目录下新建 webpack.js 然后通过 node webpack.js 启动
// 引入 webpack
const webpack = require("webpack")
// 引入配置文件
const config = require("./webpack.config.js")
// 根据配置文件 生产 webpack 实例
const compiler = webpack(config)
// 打印 webpack 的 生命周期钩子
Object.keys(compiler.hooks).forEach((hookName) => {
compiler.hooks[hookName].tap("xxx", (compilation) => {
console.log('🚀🚀~ hookName:', hookName);
})
})
// 执行 编译
compiler.run()
所有的生命周期钩子都在 compiler.hooks
属性上,所以我们遍历打印出来的结果如下:
可以看到 webpack
的生命周期钩子实在是太多了。
所以我们的 plugin
的事件只需要注册在对应的生命周期钩子上就可以了,然后 webpack
就会在恰当的时机通过我们自己的 plugin
了。
class MyPlugin {
constructor(options) {
}
// 接收一个参数 compiler 就是实例化的webpack 包含配置等信息
apply(compiler) {
// 注册事件 同步钩子用tap 异步钩子用tapAsync注册
// 事件名称可以为任意值,建议和插件名称保持语义一致
compiler.hooks.emit.tapAsync('xxx', (compilation, cb) => {
// compilation 半成品
console.log('🚀🚀~ compilation:', compilation.assets);
const content = 'hello,plugin xxxxx'
// 添加静态资源
compilation.assets['warbler.txt'] = {
source: function() {
return content
},
// 只是用来查看的,并不会影响文件真实大小
size: function() {
return content.length
}
}
cb()
})
}
}
module.exports = MyPlugin
通过 apply
方法的 compiler
参数进行事件的注册,可以注册在任何一个生命周期中,但是有的生命周期是同步的,有的是异步的,具体的还是要查看官网。
同步钩子用 tap
注册事件, 异步钩子用 tapAsync
注册事件。
如果是异步钩子的话, 需要在事件的回调中传递一个 cb
参数,并在最后调用一下。
事件名称可以为任意值,建议和插件名称保持语义一致,方便阅读源码,理解功能。
事件回调的 compilation
参数就是上一个钩子执行过后的半成品,属性有很多,可以阅读官网。
转载自:https://juejin.cn/post/7078461343711363103