Appearance
生产环境优化
webpack开发体验解决却带来了生产环境的新问题
在我们提升开发体验的时候,我们的webpack打包也越来越臃肿,因为webpack为了实现那些特性,会在打包结果中添加额外的代码实现各自的功能(例如:source map和HMR)。
这些代码对于生产环境来讲是冗余的。生产环境注重运行效率,开发环境中注重开发效率,解决这些问题webpack4提出了mode(模式)的用法。
webpack也建议我们为不同的工作环境创建不同的配置,便于让我们的打包结果适用于不同的环境
生产环境值得优化的地方 —— 不同环境下的配置
M1:配置文件根据环境不同导出不同配置
- webpack.config.js中编写
js
/**
* webpack配置支持导出一个函数,里面return的对象就是我们的配置对象
* @param {string} env cli传递的环境名参数
* @param {*} argv 运行中传递的所有参数
*/
module.exports = (env, argv) => {
// 开发模式下的选项配置
const config = {
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output')
},
devtool: 'eval',
devServer: {
...
},
mode: 'none',
module: {
...
},
plugins: [
...
]
}
// 判断是生产环境
if(env === 'production') {
// 模式修改为生产环境
config.mode = 'production'
// 禁用source map
config.devtool = false
// 上线打包的时候用的插件
config.plugins = {
...config.plugins,
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
}
}
return config
}
- 在命令行中运行
npm run build
(webpack
)不加任何参数可以看到是正常打包 - 在package.json中添加
js
"scripts": {
"prod": "webpack --env production"
}
命令行中运行npm run prod
(webpack --env production
)可以返回生产模式下的配置
M2:一个环境对应一个配置文件
上面的M1方法只适用于中小型项目,由于项目的复杂,我们的配置也会跟着复杂。对于大型项目还是推荐不同环境对应不同配置文件。
一般项目中会有三个配置文件,两个是用于不同环境的,还有一个是公共配置。
- 创建
webpack.common.js
用于放公共配置
js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output')
},
module: {
rules: [{
test: /\.js/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},{
test:/\.png$/,
use: {
loader:'url-loader',
options: {
limit: 10 * 1024
}
}
},{
test: /\.html$/,
use: {
loader: 'html-loader',
options: {
attributes: {
list: [
{
tag: 'img',
attribute: 'src',
type: 'src'
},
{
tag: 'a',
attribute: 'href',
type: 'src'
}
]
}
}
}
},{
test: /\.md$/,
use: [
'html-loader',
'./markdown-loader'
]
}]
},
plugins: [
new HtmlWebpackPlugin({
title: 'webpack plugin sample',
hello: 'hello~',
meta: {
viewport: 'width=device-width'
},
template: './src/index.ejs'
}),
new HtmlWebpackPlugin({
filename: 'about.html',
title: 'About'
})
]
}
- 创建
webpack.dev.js
用于开发环境配置
js
// 先导入公共的配置模块
const common = require('./webpack.common')
const webpack = require('webpack')
const { merge } = require('webpack-merge')
module.exports = merge(common, {
mode: 'development',
devtool: 'cheap-eval-module-source-map',
devServer: {
contentBase: './public',
proxy: {
'/api': {
target:'https://api.github.com',
pathRewrite: {
'^/api': ''
},
changeOrigin: true
}
},
hotonly: true
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
})
- 创建
webpack.prod.js
用于生产环境配置 - 不在
webpack.prod.js
中使用Object.assign进行合并的原因是因为plugins里面的东西不需要完全覆盖,所以安装社区中官方提供的npm i webpack-merge --save-dev
到依赖中,并且使用
js
// 先导入公共的配置模块
const common = require('./webpack.common')
// 使用webpack-merge中的merge函数
const { merge } = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = merge(common, {
mode: 'production',
devtool: false,
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [{
from: 'public'
}]
})
]
})
- 在命令行终端执行
webpack --config webpack.prod.js
来指定我们运行的配置文件
其他优化功能
webpack4的production模式下面内部开启了很多优化功能,对于使用者而言这种开箱即用的方式是很方便的,但是对于学习者而言会忽略掉很多东西,以致于出现问题之后无从下手。
如果要深入了解webpack的使用,需要单独了解每个配置背后的作用,这里先学习几个主要的优化配置,了解一下webpack是如何优化打包结果的。
DefinePlugin
definePlugin是webpack内置插件,是为代码注入全局成员的,production模式下这个插件会默认启用起来,并且在代码中注入了process.env.NODE_ENV的常量,很多第三方模块都是通过这个常量判断当前的运行环境,从而去决定是否打印日志等操作。
- webpack.config.js中写
js
const webpack = require('webpack')
plugins: [
// 接收一个对象,每一个键值都会注入到代码中,里面可以自定义常数变量,还可以根据环境自己定义
new webpack.DefinePlugin({
// 值不是字符串,而是JS代码片段,所以应该在内容里面自己加双引号说明这是一个字符串
API_BASE_URL: '"https://api.example.com"',
})
]
小技巧
如果我们注入的是一个值的话,我们可以用JSON.stringify转换成一个表示值的代码片段,这样就不容易错了
API_BASE_URL: JSON.stringify('https://api.example.com')
- 在main.js中使用
js
console.log(API_BASE_URL)
- 在命令行中打包
npm run build
可以看到bundle.js中解析出了对应的代码。
Tree-shaking
字面意思就是摇树,摇掉代码中未引用代码(dead-code)。webpack可以自动检测出未引用的代码然后移除掉。其并不是某个功能选项,而是一组功能搭配使用后的优化效果,这种功能会在production模式下自动开启。
举个例子:
代码中写下列代码然后生产环境打包,在bundle.js中会找不到显示的未引用部分的代码。
js
// index.js
import { Button } from './components'
document.body.appendChild(Button())
// components.js
export const Button = () => {
return document.createElement('button')
// 未引用部分
console.log('dead-code')
}
// 未引用部分
export const Link = () => {
return document.createElement('a')
}
// 未引用部分
export const Heading = level => {
return document.createElement('h' + level)
}
在其他模式下如何开启Tree-shaking?
因为目前官方对tree-shaking的介绍有些混乱,所以我们通过在其他模式下一步步开启Tree-shaking,了解其工作过程和其他的优化功能。
- usedExports 负责标记【枯树叶】
- minimize 负责【摇掉】它们
- 下载模板 webapck-tree-shaking-temp,命令行中执行
npm run build
打包可以在bundle.js中找到模块有对link函数等进行模块导出
- 在webpack.config.js中添加
js
// webpack.config.js
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
// 集中配置webpack内部优化功能
optimization: {
// 只导出外部使用了的成员
usedExports: true
}
}
设置之后命令行执行npm run build
发现bundle.js中不再对模块进行导出,但是还是有定义的link等函数
- 再对webpack.config.js进行添加
js
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
optimization: {
usedExports: true,
// 进行压缩,未被引用的部分会被弃用
minimize: true
}
}
命令行执行npm run build
发现bundle.js中的代码已经找不到了未引用的link等函数,完美~
Tree Shaking & Babel
因为早期webpack发展很快,所以我们找到的文档可能并不适用于当前版本,很多文档说:
如果我们使用babel-loader,就会导致tree-shaking失效?!
对于这现象的解释: Tree Shaking前提是使用ES Modules去组织我们的代码,即我们交给webpack打包的代码必须使用ESM。 webpack在打包前先将文件交给不同的loader进行处理,最后将处理后的结果打包到一起,我们在处理ES6新特性的时候会用babel-loader,babel-loader在处理的使用会把ES Modules转化成CommonJS,最后会不会失效这取决于我们有没有使用转换ES Modules的插件(@babel/preset-env就使用了)
验证
- 安装
npm i babel-loader @babel/core @babel/preset-env --save-dev
,在webpack.config.js中引用
js
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
module: {
rules: [{
test: /\.js/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}]
},
optimization: {
usedExports: true
}
}
发现tree-shaking并没有失效
原因分析
因为在最近版本的babel-loader中已经自动关闭了ES Modules转换的插件。
在babel中可以看到其支持ESModules
在preset-env中自动禁用了的转换
所以webpack在打包的时候得到的还是ESModules的代码。
强制开启插件
js
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
module: {
rules: [{
test: /\.js/,
use: {
loader: 'babel-loader',
options: {
// 这里是数组套数组,注意!
presets: [
// 第二个参数是一个对象,默认是auto,根据环境判断是否支持ES Modules
// 指定modules是commonjs,强制只用ESM的转换插件
// 如果不确定的话,那么将modules强制设定为false,就绝对不会开启ES Modules转换的插件
['@babel/preset-env', { modules: 'commonjs' }]
]
}
}
}]
},
optimization: {
usedExports: true
}
}
这时候打包看确实tree-shaking失效了。
concatenateModules
concatenateModules —— 合并模块函数,这个特性又被成为Scope Hoisting(作用域提升),是webpack3中添加的一个特性。
普通的打包结果是将我们的模块最终放在一个单独的函数中,这样的话如果我们的模块很多,就意味着我们在输出结果中有很多的模块函数。使用concatenateModules可以将所有模块合并到一个函数中,既提升了运行效率,又减少了代码的体积。
js
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
optimization: {
usedExports: true,
// 开启合并模块函数特性
concatenateModules: true
}
}
这样所有的模块都合并到一个函数中。
sideEffects
webpack4中新增了一个新特性,允许我们通过配置的方式标识我们的代码是否有副作用,从而为tree-shaking提供更大的压缩空间
副作用:模块执行时除了导出成员之外所做的事情。
ps: sideEffects一般用于npm包开发标记是否有副作用。因为官网中把sideEffects和tree-shaking混到了一起,很多人误认为他们是因果关系,其实他们没有什么关系。
遇到的问题
下载模板文件 webpack-sideEffects-temp ,src/components的index.js文件把所有的模块都引入了,那些模块除了引用并没有别的操作,但是我们在src/index.js中只想引用Button,却把其他文件也引用进来了,命令行npm run build
打包文件中可以看到
我们需要的是我们虽然有引入模块但是并没有别的操作,我们就可以把这些模块去掉。
解决此类问题
- 设置
weboack.config.js
开启功能
js
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
// 集中配置webpack内部优化功能
optimization: {
// 此特性在production模式下会自动开启
// 会先检查当前代码所属的package.json中有没有sideEffects标识,以此来判断这个模块是否有副作用。如果没有副作用,那些没用的模块就不会打包
sideEffects: true,
}
}
- 在package.json中配置标识
js
{
// 标识当前项目的代码,都没有副作用,一旦没有引用的模块没有副作用,就会被移除掉
"sideEffects": false
}
- 重新命令行打包
npm run build
可以看到没有引用的代码不会打包进去
如果文件有副作用怎么办?
使用sideEffcts的前提是确保代码真的没有副作用,否则在打包的时候就会误删掉有副作用的代码。
模板中有一个extend.js的代码,这段代码就是副作用代码。
js
// 为 Number 的原型添加一个扩展方法
Number.prototype.pad = function (size) {
// 将数字转为字符串 => '8'
let result = this + ''
// 在数字前补指定个数的 0 => '008'
while (result.length < size) {
result = '0' + result
}
return result
}
在index.js中直接引用
js
// 副作用模块
import './extend'
console.log((8).pad(3))
这个时候我们还运行没有副作用的话,就不会被打包进去。同样如果引用css模块,也是副作用代码,也面临同样的问题,
js
// 样式文件属于副作用模块
import './global.css'
解决途径:
- 关闭掉没有副作用的标识
- 标识一下当前项目的哪些文件标识为有副作用的文件,webpack就不会忽略了
js
// package.json
"sideEffects": [
// 值可以是一个数组,元素是对应的路径
"./src/extend.js",
// 也可以使用通配符的方式
"*.css"
]
命令行运行一下看到bundle.js文件中对有副作用的代码并没有删除
webpack优化内置属性的介绍,这些特性都是为了弥补javascript早期在设计的遗留问题,随着webpack技术的发展,javascript越来越好。
Code Splitting
Code Splitting是代码分包,代码分割。为了解决应用很复杂,模块很多,导致生成bundle体积过大的问题。
原因描述
通过webpack将所有的代码最终都被打包到一起,会造成bundle体积过大,事实上并不是每个模块在启动时都是必要的,因为这些模块被打包到一起,我们只用其中一个,就要把所有的模块全部加载进去。应用一般运行在浏览器端,这就意味着会浪费掉很多流量和带宽。
这个是否和之前的模块合并有冲突?
并不会有冲突,模块打包是必要的,HTTP1.1版本,本身有很多的缺陷,我们并不能同时对同一个域名下发起很多次的并行请求。而且每一次请求都会有一定的延迟,还会有额外的请求头和响应头,会造成很多流量带宽的浪费。
但是物极必反,我们需要在模块变大之后灵活处理合并和分包的问题。
最佳实践
将模块根据不同的规则打包到多个bundle中,按需加载。可以大大提高响应速度和运行效率。
M1:多入口打包(Multi Entry)
多个打包入口同时打包,会输出多个打包结果。一般适用于传统的多页应用程序,最常见的就是一个页面对应一个打包入口,对于公共部分单独提取。
- 下载模板 webpack-multiple-entry-temp, 修改webpack.config.js
js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'none',
// 这里定义的是一个对象而不是数组,如果是数组就是多个文件打包到一起,最终还是一个入口
// 对象的键就是打包的入口,键就是入口名称,值就是文件路径
entry: {
index: './src/index.js',
album: './src/album.js'
},
output: {
// 两个入口有两个文件名,这里用[name]占位符的方式动态输出文件名,最终会被替换成入口名称
filename: '[name].bundle.js'
},
module: {
...
},
plugins: [
...
]
}
- 命令行输入
npm run build
可以看到dist目录下确实生成了不同的文件。但是他们的html中还是两个文件都引入了
html
<script type="text/javascript" src="index.bundle.js"></script><script type="text/javascript" src="album.bundle.js"></script></body>
- HtmlWebpackPlugin生成插件,这个插件默认是产生一个自动输入所有打包结果的html,继续修改webpack.config.js
js
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/index.html',
filename: 'index.html',
// 添加chunks属性,确定引入的bundle文件
chunks: ['index']
}),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/album.html',
filename: 'album.html',
// 添加chunks属性,确定引入的bundle文件
chunks: ['album']
})
]
- 再运行命令行打包的结果就一个html引入一个bundle.js文件了。
- 不同入口中肯定会有公共模块,所以在不同的打包结果中会有相同的模块出现(例如:项目中都运用了vue 、jquery之类的模块),所以还是需要对公共模块进行提取(Split Chunks)。
js
// webpack.config.js
optimization: {
splitChunks: {
// 把所有的公共模块都提取到bundle中
chunks: 'all'
}
},
- 命令行运行
npm run build
可以看到多出了一个打包文件
M2:采用ES Modules的动态导入(Dynamic Imports)
通过动态导入,实现按需加载。webpack会把动态导入的模块自动单独输出到一个bundle中。
相比于多入口,这个更加的灵活,我们可以根据代码的逻辑控制是否需要或者何时加载模块。更加适用于单页应用程序。
- 下载模板webpack-dynamic-import-temp,里面有album模块和posts模块,现在是启动首页就加载了两个模块,很多人如果不进行锚点切换的时候其实可以不用去加载别的模块,这里进行优化
js
// src/index.js
// 将两个模块的导入注释掉,我们一开始不导入这两个模块
// import posts from './posts/posts'
// import album from './album/album'
const render = () => {
const hash = window.location.hash || '#posts'
const mainElement = document.querySelector('.main')
mainElement.innerHTML = ''
if (hash === '#posts') {
// 这里使用动态导入,参数是参入的路径,返回一个promise对象,then是导入成功之后执行的函数
// then的返回参数是module,这里posts是默认导出所以将default赋值成posts别名
import('./posts/posts').then(({ default: posts }) => {
mainElement.appendChild(posts())
})
} else if (hash === '#album') {
// 这里同上
import('./album/album').then(({ default: album }) => {
mainElement.appendChild(album())
})
}
}
render()
window.addEventListener('hashchange', render)
- 命令行打包
npm run build
可以看到dist/中多出来三个bundle,其中有两个是两个模块,还有一个是两个中公共的部分提取的bundle
如果使用的是vue单页应用,那么通过路由映射组件动态导入的方式就可以实现按需加载
锦上添花 —— 魔法注释
默认用动态导入的文件,文件只是一个序号,一般没啥。如果想要命名的话,可以使用webpack特有的魔法注释(Magic Comments)实现:
- 在import里面添加行内注释,格式为
js
if (hash === '#posts') {
import(/* webpackChunkName: 'posts' */'./posts/posts').then(({ default: posts }) => {
mainElement.appendChild(posts())
})
} else if (hash === '#album') {
import(/* webpackChunkName: 'album' */'./album/album').then(({ default: album }) => {
mainElement.appendChild(album())
})
}
- 运行命令行文件
npm run build
,可以看到打包的文件变成了我们要的名字
- 如果webpackChunkName是相同的,那么他们就会被打包到一起。
js
// src/index.js
if (hash === '#posts') {
// mainElement.appendChild(posts())
import(/* webpackChunkName: 'components' */'./posts/posts').then(({ default: posts }) => {
mainElement.appendChild(posts())
})
} else if (hash === '#album') {
// mainElement.appendChild(album())
import(/* webpackChunkName: 'components' */'./album/album').then(({ default: album }) => {
mainElement.appendChild(album())
})
}
- 命令行运行
npm run build
可以看到生成的打包文件
这样我们就可以灵活的控制我们要打包的文件了。
MiniCssExtractPlugin
可以将css代码从打包文件中提取的插件,通过这个插件我们可以做到css模块的按需加载
- 安装插件
npm i mini-css-extract-plugin --save-dev
- 在
webpack.config.js
中导入插件
js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
...
module: {
rules: [
{
test: /\.css$/,
use: [
// style-loader的作用将样式通过style标签注入html中,因为加入插件后css会单独放一个文件,所以不需要style标签,这里的style-loader就不需要了
// 'style-loader',
// 这里会使用link的方式导入css文件,所以使用MiniCssExtractPlugin插件自带的loader处理
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
...
// 在plugins中引用,就可以单独放在一个文件中,不需要style标签,而是通过link的方式去引入
new MiniCssExtractPlugin()
]
- 在命令行运行
npm run build
可以看到dist文件中有css文件单独形成。
注意:
这个功能适用于css文件比较大而言
- css超过150kb之后可以单独加载
- css小于150kb,不单独加载放在代码中减少一次请求会更好
OptimizeCssAssetsWebpackPlugin
压缩输出的css文件
单独的css文件提取之后,即使运行生产模式代码都没有被压缩,是因为webpack内置只对js文件进行压缩处理,其他文件就需要额外的工具进行处理
- 安装
npm i optimize-css-assets-webpack-plugin --save-dev
webpack.config.js
中进行配置
js
// 引入插件
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = {
...
plugins: [
...
// 在plugins中初始化
new OptimizeCssAssetsWebpackPlugin()
]
}
- 在命令行运行
npm run build
可以看到生成的css文件已经经过了压缩。
将其配置到minimizer中
官方推荐我们把这个插件配置到minimize中而不是plugins,原因是因为配置到plugins,文件一直会被压缩,如果配置到optimization的minimizer中,可以通过开启关闭minimizer对文件是否要压缩进行控制
- 在
webpack.config.js
中修改
js
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = {
...
// 在这个里面添加配置,自定义压缩器插件,webpack内置的压缩器就会被覆盖
optimization: {
minimizer: [
new OptimizeCssAssetsWebpackPlugin()
]
},
...
plugins: [
...
// 把这个注释掉
// new OptimizeCssAssetsWebpackPlugin()
]
}
- 在命令行中运行
npm run build
可以看到其不会进行压缩 - 配置在生产环境中运行
npm run prod
,下面是package.json中的配置,可以看到css代码自动进行了压缩
js
"scripts": {
"build": "webpack",
"prod": "webpack --mode production"
}
解决原生js压缩被覆盖的情况 —— terser-webpack-plugin
- 但是这样会把webpack内置的压缩器覆盖掉,我们要进行手动配置,安装
npm i terser-webpack-plugin --save-dev
(目前安装5.0.3会报错,所以跟着例子安装2.2.1可以正常工作) - 在webpack.config.js中配置
js
const TerserWebpackPlugin = require("terser-webpack-plugin")
module.exports = {
...
optimization: {
minimizer: [
// 替代原生压缩插件初始化
new TerserWebpackPlugin(),
new OptimizeCssAssetsWebpackPlugin()
]
},
plugins: [
...
]
}
- 命令行运行
npm run prod
可以看到js和css一起压缩了。
Hash
一般部署前端资源文件的时候都会启动服务器的静态资源缓存,这样对于用户的浏览器而言,就可以缓存到应用的静态资源,后续不需要再请求服务器。整体的响应速度会提升,如果在缓存策略中,缓存时长设置的过短,效果并不是很明显。如果缓存时间过长,一旦应用发生更新,又没有办法及时更新到客户端。为了解决这个问题:
在生产模式下,为输出的文件名添加Hash,一旦名称发生改变,全新的文件名就是全新的请求,不用担心不刷新的问题,那么缓存时间就可以设置的很长。
webpack中filename属性都支持通过占位符的方式支持Hash,不过他们支持三种Hash,效果各不相同。
M1:hash
js
module.exports = {
...
output: {
filename: '[name]-[hash].bundle.js'
},
plugins: [
...
new MiniCssExtractPlugin({
filename: "[name]-[hash].bundle.css"
})
]
}
项目级别的hash,所有文件生成一个hash值
如果项目中有任何的改动,生成的所有文件都会生成一个新的hash
M2:chunkhash(推荐)
js
module.exports = {
...
output: {
filename: '[name]-[chunkhash].bundle.js'
},
plugins: [
...
new MiniCssExtractPlugin({
filename: "[name]-[chunkhash].bundle.css"
})
]
}
组件级别的hash,相同组件的hash是一样的
单独修改js文件,只有js文件的hash修改
修改posts.js文件,其整个组包括main的hash都会改变,其他的文件不会改变。
contenthash
js
module.exports = {
...
output: {
filename: '[name]-[contenthash].bundle.js'
},
plugins: [
...
new MiniCssExtractPlugin({
filename: "[name]-[contenthash].bundle.css"
})
]
}
文件级别的hash,根据文件的内容生成的hash值,只要是不用的文件,就有不同的hash值
修改posts文件,posts文件和其被引用文件都会更新hash
这种方式是解决缓存问题最好的方式
修改hash长度
22位的hash长度过长,可以自己指定,冒号后面跟长度即可。
js
module.exports = {
...
output: {
filename: '[name]-[contenthash:8].bundle.js'
},
plugins: [
...
new MiniCssExtractPlugin({
filename: "[name]-[contenthash:8].bundle.css"
})
]
}