Webpack知识整理———插件
概述
在开发中为了增强webpack自动化的能力,我们需要安装一些插件来辅助开发。
Loader 是为了解决资源加载的问题,而Plugin是为了解决其他自动化的问题。
基本使用
在webpack.config.js 文件中添加 plugins属性 该属性是一个数组,在这个数组填入插件的实例即可。
plugins: [
插件1,
插件2,
…
]
Webpack 常用插件
自动清除输出目录
clean-webpack-plugin 通过安装使用该插件 来实现打包时自动清除上一次打包成果的功能
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
plugins:[
new CleanWebpackPlugin ()
]
}
自动生成引用打包结果的HTML
如果html文件是由我们自己手动引入打包后的资源,那么会存在硬编码问题,即,如果修改打包配置修改文件名或者文件路径,就需要我们手动再去修改html中的文件引用
我们可以借助 html-webpack-plugin 来实现自动生成html 并自动完成打包后文件的注入,这样再运行打包之后,dist目录中就会出现html文件 并且自动引入对应的js文件了
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
plugins:[
new HtmlWebpackPlugin()
]
}
我们可以自定义输出的html文件内容,可以在new HtmlWebpackPlugin中添加配置对象,如果需要自定义的内容比较复杂,我们还可以使用template设置模板
plugins:[
new HtmlWebpackPlugin({
title:'Hello World!',
meta: {
viewport: 'width=device-width'
},
template:'./src/index'
})
]
如果要输出多个页面就继续添加 多个实例即可
plugins:[
new HtmlWebpackPlugin({
title:'Hello World!',
meta: {
viewport: 'width=device-width'
},
filename:'home.html',
template:'./src/index'
}),
new HtmlWebpackPlugin({
filename:'about.html'
}),
]
把静态资源目录拷贝到打包后的目录中
项目中的静态资源目录,比如 public 目录 想要在打包后也放在打包后的目录中,可以借助于这个 copy-webpack-plugin 插件来完成
const CopyWebpackPlugin = require('copy-webpack-plugin')
plugins:[
new CopyWebpackPlugin({
patterns:[ { from:'目标',to:'生成到哪去' } ]
})
]
插件工作原理
webpack的插件工作机制就是我们在软件开发中常见的钩子机制来实现。
webpack要插件必须是一个函数,或者包含apply方法的对象。
我们来实现一个去掉 /******/ 这种注释的插件
class MyPlugin {
apply (compiler) {
console.log('MyPlugin 启动')
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation => 可以理解为此次打包的上下文
for (const name in compilation.assets) {
if (name.endsWith('.js')) {
const contents = compilation.assets[name].source()
const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
compilation.assets[name] = {
source: () => withoutComments,
size: () => withoutComments.length
}
}
}
})
}
}
提高开发体验
总结一下,我们使用webpack进行项目打包的流程:
四个步骤,依次进行,但是这种手动的方式过于原始,也会让开发的体验很差,我们就需要想办法提高开发的体验,使一些工作能够自动完成,让我们专注于代码编写上。
希望能够有以下几个方向的优化,来提高开发体验:
- 以 Http Server 来运行项目,而不是文件的方式打开
- 修改完代码保存后,能够自动编译 + 自动刷新页面
- 为了方便代码调试,还希望拥有 Source Map 支持 这样就可以进行断点调试
自动编译
使用watch 工作模式,监听文件变化,自动重新打包。
执行 webpack --watch
这样webpack就会开始监听文件变化了
webpack-dev-server
该工具提供一个 HTTP Server,并且这个工具集成了 「自动编译」和「自动刷新页面」 等功能
- 启动一个HTTP Server 服务器运行项目
- 以watch的方式运行打包,监听文件变化 自动打包编译
- 代码更新后会自动刷新页面
- devServer 打包生成的文件存在内存中,HTTP Server也是从内存中获取文件进行执行
- 因为生成的文件在内存中 所有减少了 磁盘读写 提高了执行效率
运行方式: webpack-dev-server
可以在 npm script 中添加该执行 并传入cli 参数
"scripts": {
"dev": "webpack-dev-server --open --host 127.0.0.1 --port 8888"
}
然后执行 npm run dev 即可
webpack5之后 webpack-dev-server 执行cli 修改为 webpack serve
需要执行 webpack serve 指令来启动
在package.json应做如下配置
"dev": "webpack serve"
再执行npm run start就正常启动。
使用contentBase 指定静态资源目录
我们在开发的时候 CopyWebpackPlugin 一般只在最后添加 所以为了防止dev server访问不到静态资源
为 dev-server 指定要访问的静态资源目录,在 webpack.config.js中添加
devServer: {
contentBase: './public'
}
利用devServer 在开发环境下启用代理
前端在发起接口请求时,由于浏览器的同源问题,会出现跨域问题。我们在开发的过程中可以利用devServer配置代理api 来绕开同源策略。当项目上线之后 交给运维人员去配置反向代理
devServer: {
contentBase: './public',
proxy: {
// 匹配以 /api 开头的请求
'/api': {
// http://localhost:8080/api/users -> https://api.github.com/api/users
target: 'https://api.github.com',
// 重写地址,不希望其中出现/api
// http://localhost:8080/api/users -> https://api.github.com/users
pathRewrite: {
'^/api': ''
},
// 不能使用 localhost:8080 作为请求 GitHub 的主机名
// changeOrigin默认是false:请求头中host仍然是浏览器发送过来的host
// 如果设置成true:发送请求头中host会设置成target
changeOrigin: true
}
}
},
使用Source Map来调试代码
因为运行代码都是经过打包编译之后的,跟编写的不同,不方便我们调试。Source Map就解决了这个问题 ,它利用map文件 映射出原代码,方便开发人员进行调试。
webpack配置SourceMap
source map 基本使用
devtool:"source-map"
选择不同的模式
- 开发环境下 可以使用 cheap-module-eval-source-map 模式
注意在webpack 5 之下 该模式更名为 eval-cheap-module-source-map,
也就是要这样设置
devtool:"eval-cheap-module-source-map"
- 生成环境下 可以使用 none 这样不会使我的开发原代码暴露
自动刷新的问题
devServer可以在我们修改过代码后,自动刷新页面,但是这样也存在一个问题。如果页面中有一些表单元素,输入内容到一般,修改代码自动刷新就会出现输入的内容丢失。就又需要再次输入比较麻烦。
我们需要解决这个问题,这里可以使用到的就是 Hot Module Replacement (模块热更新)
HMR 指的是在程序运行过程中可以实时的替换掉某个模块,并且程序的运行状态不受影响.
在webpack5 中,dev-server 可能不会出现自动刷新的问题,在配置中添加target:"web"属性
开启HMR ( 热更新 )
热更新集成在了 webpack-dev-server 中 不需要额外安装其他模块。
可以通过cli 参数 运行 webpack serve --hot
添加 --hot来开启
也可以通过配置文件开启, 这里注意 修改的样式文件要是js 文件中引入的样式文件 这样才会触发热更新
注意 : 针对于 html js 图片文件 需要手动去设置hmr的逻辑,社区还提供许多其他 loader 和示例,可以使 HMR 与各种框架和库平滑地进行交互。
"scripts": {
"serve":"webpack serve --hot --open"
},
生产环境优化
1. 配置文件根据环境不同导出不同配置
webpack的配置文件除了支持导出一个对象,还支持导出一个函数,函数接收两个形参: env 和 argv, 在这个函数中 返回我们需要的配置对象。
env 是我们通过cli(终端指令)传递的环境名参数
argv 是指我们运行cli过程中传递的所有参数
默认约定 生产环境的env的值是 “production” 比如 webpack --env production
module.exports = (env, argv) => {
const config = "具体配置对象"
if (env === 'production') {
config.mode = 'production'
config.devtool = false
config.plugins = [
...config.plugins,
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
]
}
return config
}
2. 一个环境对应一个配置文件
- 创建一个公共的配置文件 webpack.common.js
- 创建开发模式下的配置文件 webpack.dev.js
- 创建生产环境下的配置文件 webpack.pro.js
- 在开发和生产 环境对应的文件中使用
webpack-merge
来合并公共配置
const webpack = require('webpack')
const merge = require('webpack-merge')
const common = require('./webpack.common')
module.exports = merge(common, {
mode: 'development',
devtool: 'cheap-eval-module-source-map',
devServer: {
hot: true,
contentBase: 'public'
}
})
- 在scripts中配置对应的cli
"scripts": {
"build": "webpack --config webpack.prod.js",
"serve": "webpack serve --hot"
},
Webpack DefinePlugin
这是webpack内置的一个插件,它可以为我们的代码注入全局成员
比如 我要注入api的公共域名
const webpack = require('webpack')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js'
},
plugins: [
new webpack.DefinePlugin({
// 值要求的是一个代码片段
// API_BASE_URL 就可以在全局使用了
API_BASE_URL: JSON.stringify('https://api.example.com')
})
]
}
Tree Sharking
顾名思义 “摇树” 去掉枯叶,在webpack里它的作用是「摇掉」代码中未引用的部分,未引用代码( dead-code ),生产模式中,webpack会自动检测未引用的代码 然后移除掉它们。
Tree Sharking 不是指某一个配置选项,它是一组功能搭配使用过后的优化效果,在生产模式中webpack会自动启动它
如果需要在开发模式中使用,我们可以通过两个配置来实现
- usedExports — 负责标记 「枯树叶」
- minimize — 负责 「摇掉」它们
optimization: {
usedExports: true,
minimize: true
}
tree sharking 要想生效,关键点在于 由webpack 打包的代码必须使用ESM,而我们有时候使用的babel版本比较低的话 就会把ESM 转化为 commonJS 规范 这样就会导致 tree sharking 失效,但是在最新版的babel中已经不存在这个问题了
webpack 合并模块
concatenateModules 合并模块,原本webpack打包会把一个模块生产一个函数,使用了concatenateModules 可以把所有的模块都合并到一个函数中,进一步压缩体积。
sideEffects
开启了sideEffects配置后,webpack在打包时就会先检查当前代码所属的package.json中有没有sideEffects的标识,以此来判断这个模块是不是又副作用。如果这个模块没有副作用,这些没被用到的模块就不会被打包。(这个特性在production模式下会自动开启)
副作用: 模块执行时除了导出成员之外所作的事情。
sideEffects 一般用于 npm 包标记是否有副作用
比如说 一个模块中的代码 除了导出成员之外,还为Object 扩展了原型方法那么这就是副作用,我们需要标记这些有副作用的文件模块,不然它们是不会被webpack打包进来的。
使用sideEffects的前提就是确定你的代码真的没有副作用,否则的话,在webpack打包时,就会误删掉那些有副作用的代码。
代码分割
webpack 可以把模块代码打包成一个文件,这样也存在一些弊端。如果应用非常复杂,模块很多,那么最终生成的文件就会特别的大。然而,在实际情况中,并不是每个模块在启动时都是必要的。
最理想的情况是,把代码分离到多个文件中,分包,根据需要按需加载。
当然这样有人会有疑问,就是说一开始模块就是分离的,既然要分离干脆别打包啊?
理由很简单,单一文件太大这样肯定不行,而如果文件数量太多太碎,也是一样不行的。
我们可以按照不同的业务功能来对模块进行分包,实现的方式有以下两种:
- 多入口打包
- 动态导入
多入口打包
一般适用于传统的多页应用程序,最常见的是一个页面对应一个打包入口,公共部分单独提取。
- entry 的值要修改成对象,每一个属性就对应一个打包入口
- output 中filename 的 [name] 就是entry里的属性名
- HtmlWebpackPlugin默认会在生成的html文件中引入所有打包好的js文件,这里我们需要单独引入对应的js文件 所以需要添加chunks属性 里面的值就是要引入的包名
module.exports = {
mode: 'none',
entry: {
index: './src/index.js',
album: './src/album.js'
},
output: {
filename: '[name].bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/index.html',
filename: 'index.html',
chunks: ['index']
}),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/album.html',
filename: 'album.html',
chunks: ['album']
})
]
}
公共模块提取
optimization: {
splitChunks: {
// 自动提取所有公共模块到单独 bundle
chunks: 'all'
}
},
动态导入
动态导入的模块会被自动分包。
而实现动态导入模块的方法,其实就是利用ESM中动态导入模块的写法即可。只要使用动态导入的模块webpack就会自动实现打包后生成多个分包文件,并提取公共部分
import(./posts/posts').then(({ default: posts }) => {
})
而默认打包后的文件名字只是在前面添加了序号,我们可以在动态导入里添加一行魔法注释,这样生成的分包文件的名字就会使用注释中的名字了,并且添加了相同注释的动态导入会被打包到一起。
这一点在我们开发vue项目时,配置动态路由的时候尤为明显。
import(/* webpackChunkName: 'components' */'./posts/posts').then(({ default: posts }) => {
})
webpack MiniCssExtractPlugin
如果项目中的css文件体积过大,我们可以将css从js中抽离出来 还是以单独文件的形式引入,这就需要使用 MiniCssExtractPlugin这个插件了
module: {
rules: [
{
test: /\.css$/,
use: [
// 'style-loader', // 将样式通过 style 标签注入
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
}
optimize-css-assets-webpack-plugin
webpack 生产模式下 只能对js文件进行压缩,如果需要对样式文件进行压缩,就需要借助于插件
optimization: {
minimizer: [
new OptimizeCssAssetsWebpackPlugin()
]
},