18. 不同环境下的配置
创建不同的环境配置的方式主要有两种。第一种是在配置文件中添加相应的判断条件,然后根据环境导出不同的配置。第二种是为不同的环境对应一个配置文件,确保每一个环境下面都会有一个对应的配置文件。
首先来看配置文件中添加判断的方式,webpack的配置文件支持导出一个函数,在函数中返回所需要的配置对象。函数接收两个参数,第一个是env也就是通过cli传递的环境名参数,第二个是argv,指运行cli过程中所传递的所有参数。
可以将开发模式配置定义在config变量中,再去判断一下env是不是等于production,这里约定生产环境的env就是production。如果是生产环境的就将mode属性的字段设置为production,然后再将devtool设置为false,再添加cleanWebpackPlugin和CopyWebpackPlugin这两个插件。
const webpack = require('webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = (env, argv) => {
const config = {
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
publicPath: 'dist/'
},
devtool: 'cheap-eval-module-source-map',
devServer: {
hot: true,
contentBase: 'public'
},
module: {
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /.png$/,
use: 'url-loader'
},
{
test: /.html$/,
use: {
loader: 'html-loader',
options: {
attrs: ['img:src', 'a:href']
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'webpack',
template: './src/index.html'
}),
new webpack.HotModuleReplacementPlugin(),
]
}
if (env === 'production') {
config.mode = 'production';
config.devtool = false;
config.plugins = [
...config.plugins,
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
]
}
return config;
}
yarn webpack
yarn webpack --env producton
通过判断环境名参数返回不同的配置对象这种方式只适用于中小型项目,因为一旦项目变得复杂配置文件也会变得复杂起来。所以对于大型的项目还是建议使用不同环境对应不同配置文件的方式实现,一般这种方式至少会有三个webpack配置文件。
其中两个用来适配不同环境,另外一个是公共配置,开发环境和生产环境并不是所有的配置都完全不同,需要一个公共的文件来抽象两者之间相同的配置。
首先在项目的跟目录下新建webpack.common.js存储公共配置。
module.exports = {
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
publicPath: 'dist/'
},
devtool: 'cheap-eval-module-source-map',
devServer: {
hot: true,
contentBase: 'public'
},
module: {
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /.png$/,
use: 'url-loader'
},
{
test: /.html$/,
use: {
loader: 'html-loader',
options: {
attrs: ['img:src', 'a:href']
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'webpack',
template: './src/index.html'
}),
new webpack.HotModuleReplacementPlugin(),
]
}
然后新建webpack.dev.js和webpack.prod.js分别去用来为开发和生产定义特殊的配置。
生产环境的配置当中(webpack.prod.js)先导入公共的配置对象,这里可以使用webpack-merge方法把公共配置对象复制到这个配置对象中,通过最后一个对象覆盖公共配置中的一些配置。
yarn webpack-merge --dev
const common = require('./webpack.common');
const merge = require('webpack-merge');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = merge(common, {
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin()
]
})
同理webpack.dev.js文件当也可以通过这样一个方式实现一些额外的配置这里就不重复了。
运行webpack时需要通过--config参数指定所使用的配置文件。
yarn webpack --config webpack.prod.js
19. DefinePlugin
webpack4x中新增的production模式内部自动开启了很多通用的优化功能。第一个是一个叫做define-plugin的插件,用来为代码注入全局成员。在production模式下,默认这个插件就会启用并且往代码当中注入了一个process.env.NODE_ENV的常量。很多第三方模块都是通过这个成员判断当前的运行环境,从而去决定是否执行一些操作。
define-plugin是一个内置的插件,先要导入webpack模块,plugins这个数组当中添加这个插件,插件的构造函数接收一个对象,对象中每一个键值都会被注入到代码中。
这里在定义API_BASE_URL用来为代码注入api服务地址,值是字符串https://api.github.com。
const webpack = require('webpack');
module.exports = {
mode: 'node',
entry: './src/main.js',
output: {
filename: 'bundle.js'
},
plugins: [
new webpack.DefinePlugin({
API_BASE_URL: 'https://api.github.com'
})
]
}
代码中把API_BASE_URL打印出来。运行webpack打包,可以发现define-plugin其实就是把注入成员的值直接替换到了代码中。define-plugin的设计并只是用来替换数据,所传递的字符串实际上是一个代码片段,也就是一段符合js语法的代码。
const webpack = require('webpack');
module.exports = {
mode: 'node',
entry: './src/main.js',
output: {
filename: 'bundle.js'
},
plugins: [
new webpack.DefinePlugin({
API_BASE_URL: JSON.stringify('https://api.github.com')
})
]
}
20. Tree Shaking
Tree-shaking字面意思是摇树,一般伴随着摇树动作树上的枯树枝和树叶就会掉落下来。不过这里摇掉的是代码当中那些没有用到的部分,更专业的叫未引用代码(dead-code)。
webpack生产模式优化可以自动检测出代码中那些未引用的代码然后移除他们。比如components.js文件中导出了一些函数,每一个函数分别模拟一个组件。其中button组件函数中,在return过后还执行了一个console.log语句,很明显这就属于未引用代码。
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)
}
index.js文件中导入了components,只是导入了components当中的button这成员。这就会导致代码中很多的地方用不到,这些用不到的地方对于打包结果是冗余的,Tree-shaking的作用就是移除这些冗余的代码。
import { Button } from './components'
document.body.appendChild(Button())
yarn webpack --mode production
打包完成bundle.js中可以看到冗余的代码根本就没有输出,tree-shaking这个功能会在生产模式下自动开启。
需要注意的是tree-shaking并不是webpack中某一个配置选项,他是一组功能搭配使用过后的效果。
回到命令行终端运行webpack打包,不过这一次使用none也就是不开启任何内置功能和插件。打包完成过后输出的bundle.js文件中link函数和heading函数虽然外部并没有使用,但仍然是导出了。
yarn webpack
很明显这些导出是没有意义的,可以借助一些优化功能去掉,打开webpack的配置文件添加optimization的属性。这个属性是集中配置webpack内部的一些优化功能。可以先开启usedExports选项,表示在输出结果中只导出那些外部使用了的成员。
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
optimization: {
usedExports: true
}
}
此时components模块所对应的函数就不会导出link和heading两个函数了,可以开启webpack的代码压缩功能,压缩掉这些没有用到的代码。配置文件在optimization中开启minimize。
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
optimization: {
usedExports: true,
minimize: true
}
}
此时bundle.js中未引用的代码就都被移除掉了,这就是tree-shaking的实现。整个过程用到了两个优化功能,一个是usedExports另一个是minimize。
如果说真的把代码看做一棵大树的话,可以理解成usedExports就是用来在这个大树上标记哪些是枯树叶枯树枝,然后minimize就是负责把这些枯树叶树枝全都摇下来。
普通的打包结果是将每一个模块放在一个单独的函数中,如果模块很多也就意味着在输出结果中会有很多模块函数。concatenteModules可以将所有的模块打包到一个函数中。配置文件中开启concatenateModules, 为了可以更好的看到效果先去关掉minimize重新打包。
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
optimization: {
usedExports: true,
concatenateModules: true,
// minimize: true
}
}
此时bundle.js中就不再是一个模块对应一个函数了,而是把所有的模块都放到了同一个函数当中,concatnateModules的作用就是尽可能将所有的模块全部合并到一起然后输出到一个函数中。这样的话既提升了运行效率又减少了代码体积。这个特性被称之为Scope Hoisting也就是作用域提升,他是webpack3中添加的特性,如果此时再去配合minimize代码体积就会又减小很多。
由于早期webpack早期发展非常快,变化也就比较多,所以去找资料时得到的结果并不一定适用于当前所使用的版本,对于tree-shaking的资料更是如此。很多资料中都表示如果使用了babel-loader就会导致tree-shaking失效。针对于这个问题统一说明一下。
首先需要明确一点的是tree-shaking的实现,他的前提必须要使用ES Modules组织代码,也就是交给webpack处理的代码必须使用ES Modules的方式实现的模块化。
因为webpack在打包所有模块之前,先是将模块根据配置交给不同的loader处理,最后将所有loader处理过后的结果打包到一起。为了转换代码中ECMAScript的新特性很多时候我会选择babel-loader处理js。而在babel转换代码时有可能处理掉代码中ES Modules转换成Commonjs,当然这取决于有没有使用转换ES Modules的插件。
例如在项目中所使用的的@babel/preset-env插件集合里面就有这个插件,所以说preset-env插件集合工作的时候,代码中ES Modules的部分会被转换成Commonjs。webpack再去打包时拿到的代码是以Commonjs的方式组织的代码所以tree-shaking不能生效。
为了可以更容易分别结果,这里只开启usedExports,重新打包查看bundle.js。
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
modules: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
optimization: {
usedExports: true,
// concatenateModules: true,
// minimize: true
}
}
这里结果并不是像刚刚说的那样,usedExports功能正常工作了,也就说明如果开启压缩代码的话,那这些未使用的代码依然会被移除tree-shaking并没有失效。
在最新版本的babel-loader中已经关闭了ES Modules转换的插件,可以在node_modules中找到babel-loader模块,他在injectcaller这个文件中已经标识了当前的环境是支持ES Modules的。然后再找到的preset-env模块,在200多行可以发现,这里根据injectcaller中的标识禁用了ES Module的转换。
所以webpack最终打包时得到的还是ES Modules代码,tree-shaking自然也就可以正常工作了,当然这里只是定位的找到了源代码当中相关的一些信息。如果需要仔细了解这个东西的话可以去翻看一下babel的源代码。
可以尝试在babel的preset配置当中强制开启这个插件来去试一下。不过给preset添加配置的方式比较特别,需要把预设这个数组中的成员再次定义成一个数组,然后这个数组当中的第一个成员就是所使用的的preset的名称。第二个成员是给这个preset定义的对象,这里不能搞错是数组套数组。
将对象的modules属性设置为commonjs,默认这个属性值是auto也就是根据环境去判断是否开启ES Module插件,设置为commonjs也就表示需要强制使用babel的ES Modules插件把代码中的ES Moudle转换为commonjs。
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js'
},
modules: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', { modules: 'commonjs'}],
]
}
}
}
]
},
optimization: {
usedExports: true,
// concatenateModules: true,
// minimize: true
}
}
打包后就会发现刚刚所配置的usedExports没办法生效了。即便开启压缩代码tree-shaking也是没有办法正常工作的。
21. sideEffects
weboack4中还新增了一个叫做sideEffects的新特性。允许通过配置的方式标识代码是否有副作用,从而为tree-shaking提供更大的压缩空间。副作用是指模块执行时候除了导出成员是否还做了一些其他的事情,这样一个特性一般只有在开发npm模块时才会用到。
因为官网中把sideEffects的介绍跟tree-shaking混到了一起,所以很多人误认为他俩是因果关系。其实他俩真的没有那么大的关系。
这里设计一下能够让side Effects发挥效果的一个场景,基于上面的案例把components拆分出了多个组件文件。然后在index.js中集中导出便于外界导入。回到入口文件中导入components中的button成员。
这样就会出现一个问题,因为在这载入的是components目录下的index.js,index.js中又载入了所有的组件模块。这就会导致只想载入button组件,但是所有的组件模块都会被加载执行。查看打包结果会发现所有组件的模块确实都被打包了,side effects特性就可以用来解决此类问题。
打开webpack的配置文件在optimization中开启属性sideEffects: true,这个特性在production模式下会自动开启。
{
optimization: {
sideEffects: true
// usedExports: true,
// concatenateModules: true,
// minimize: true
}
}
开启过后webpack在打包时就会先检查当前代码所属的这个package.json当中有没有sideEffects的标识,以此来判断这个模块是否有副作用。
如果说这个模块没有副作用,那这些没有用到的模块就不再会打包。可以打开package.json添加sideEffects的字段设置为false。
{
"sideEffects": false
}
打包过后查看bundle.js文件,此时那些没有用到的模块就不再会被打包进来了,这就是sideEffects的作用。使用sideEffects功能的前提是确定代码没有副作用,否则在webpack打包时就会误删掉那些有副作用的代码。
例如有一个extend.js文件。在这个文件中没有向外导出任何成员。仅仅是在Number这个对象的原型上挂载了一个pad方法,用来为数字去添加前面的导0。
Number.prototype.pad = functuon(size) {
let result = this + '';
while (result.lengtj < size>) {
result = '0' + result
}
return result;
}
回到入口文件导入extend.js,然后就可以使用他为Number所提供的扩展方法。
import './extend.js';
console.log((8).pad(3));
这里为Number做扩展方法的操作就属于extend模块的副作用,因为在导入了这个模块过后,Number的原型上就多了一个方法这就是副作用。
此时如果还标识项目中所有代码都没有副作用的话,打包之后就会发现刚刚的扩展操作是不会被打包进来的。除此之外还有再代码中载入的css模块,也都属于副作用模块,同样会面临刚刚这样一种问题。
解决的办法就是在package.json中关掉副作用标识,或者是标识一下当前这个项目当中哪一些文件是有副作用的,这样的话webpack就不会去忽略这些有副作用的模块了。
可以打开package.json把sideEffects的false改成一个数组。然后添加extend.js文件路径,还有global.css文件的路径,当然了这里可以使用路径通配符的方式来去配置。*.css。
{
"sideEffects": [
"./src/extend.js",
"./src/global.css"
]
}
22. 代码分割
合理的打包方案应该是把打包结果按照一定的规则分离到多个bundle当中,根据应用的运行按需加载这些模块。这样的话就可以大大提高应用的响应速度以及运行效率。为了解决这样的问题webpack支持分包的功能,通过把模块按照所设计的规则打包到不同的bundle当中,从而提高应用的响应速度。
目前webpack实现分包的方式主要有两种,第一种是根据业务去配置不同的打包入口,也就是会有多个打包入口同时打包,这时候就会输出多个打包结果。第二种是采用ES Module的动态导入功能实现模块的按需加载,这个时候webpack会自动的把需要动态导入的这个模块单独的输出到一个bundle中。
1. 多入口打包
多入口打包一般适用于传统的多页应用程序,最常见的划分规则就是一个页面对应一个打包入口,对于不同页面之间的公共部分,再去提取到公共的结果当中。这种方式使用起来非常简单。
这里有两个页面分别是index和album,index.js负责实现index页面所有功能,album.js负责实现album页面所有功能,global.css和fetch.js都是公共部分。
一般配置文件中的entry属性只会配置一个文件名路径,也就是说只会配置一个打包入口,如果我们配置多个入口的话,可以把entry定义成一个对象,对象中一个属性就是一路入口,那我们属性名就是这个入口的名称,值就是这个入口所对应的文件路径。
一但这里配置为多入口,输出的文件名也需要修改,两个入口也就意味着会有两个打包结果,可以为filename属性添加[name]动态输出文件名最终就会被替换成入口的名称index和album。
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = allModes.map(item => {
return {
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({
template: './src/index.html',
filename: `index.html`
})
new HtmlWebpackPlugin({
template: './src/album.html',
filename: `album.html`
})
]
}
})
配置文件当中输出html的插件需要指定输出的html所使用的bundle,可以使用chunk属性设置,每一个打包入口就会形成一个独立的chunk,分别为这两个页面配置不同的chunk。
[
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: `index.html`,
chunk: ['index']
})
new HtmlWebpackPlugin({
template: './src/album.html',
filename: `album.html`,
chunk: ['album']
})
]
2. 提取公共模块
多入口打包本身非常容易理解也非常容易使用,但是也存在一个小问题,不同的打包入口中一定会有一些公共的部分。例如index入口和album入口中就共同使用了global.css和fetch.js这两个公共的模块。所以需要把这些公共的模块提取到一个单独的bundle当中。webpack中实现公共模块提取的方式非常简单,只需要在优化配置中开启splitChunks的功能就可以了。
配置文件中在optimization中添加splitChunks属性,这个属性需要配置chunks属性设置为all,表示会把所有的公共模块都提取到单独的bundle当中。
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = allModes.map(item => {
return {
mode: 'none',
entry: {
index: './src/index.js',
album: './src/album.js',
},
output: {
filename: `[name].bundle.js`,
},
optimization: {
splitChunks: {
chunks: 'all'
}
}
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: `index.html`,
chunk: ['index']
})
new HtmlWebpackPlugin({
template: './src/album.html',
filename: `album.html`,
chunk: ['album']
})
]
}
})
打包后我dist目录下就会生成额外的一个js文件,在这个文件中就是index和album这两个入口公共的模块部分了。
3. 动态导入
按需加载是开发浏览器应用中非常常见的需求,一般常说的按需加载指的是加载数据。那这里所说的按需加载指的是应用运行过程中需要某个模块时才去加载这个模块,这种方式可以极大地节省我们的带宽和流量。
webpack中支持使用动态导入的方式实现模块的按需加载,所有动态导入的模块都会自动被提取到单独的bundle中从而实现分包,相比于多入口的这种方式动态导入他更为灵活,可以通过代码的逻辑控制需不需要加载某个模块,或者是什么时候加载某个模块。分包的目的就是要让模块实现按需加载来提高应用的响应速度。
这里设计好了一个可以发挥按需加载作用的场景,在这个页面的主体区域,如果访问的是文章页的话得到的就是一个文章列表,如果访问的是相册页面显示的就是相册列表。文章列表所对应的是post组件,相册列表对应的是album组件,在打包入口中同时导入这两个模块。这里的逻辑是当锚点发生变化时,根据锚点的值决定显示哪个组件。
动态导入使用的就是ES Module标准当中的动态导入,需要动态导入的地方通过import函数导入指定的路径。这个方法返回的是一个Promise,在Promise的then方法中就可以拿到模块对象。
// import posts from './posts/posts';
// import album from './album/album';
const render = () => {
const hash = locaton.hash || '#posts';
const mainElement = document.querySelector('.main');
mainElement.innerHTML = '';
if (hash === '#posts') {
// mainElement.appendChild(posts());
import('./posts/posts').then(({ default: posts}) => {
mainElement.appendChild(posts());
})
} else if (hash === '#album') {
// mainElement.appendChild(album());
import('./album/album').then(({ default: album}) => {
mainElement.appendChild(album());
})
}
}
render();
window.addEventListener('hashchange', render);
打包过后dist目录会多出三个js文件,这三个js文件实际上就是由动态导入自动分包所产生的。这三个文件分别是刚刚导入的两个模块以及这两个模板当中公共的部分所提取出来的bundle。整个过程无需配置任何一个地方只需要按照ES Module动态导入成员的方式去导入模块就可以了webpack内部会自动处理分包和按需加载。
4. 魔法注释
默认通过动态导入产生的bundle文件,他的名称只是一个序号,这并没有什么不好的,因为在生产环境大多数时候是不用关心资源文件的名称是什么的。但是如果你需要给这些bundle命名可以使用webpack所特有的模板注释来去实现。
具体的使用就是在调用import函数的参数位置添加一个行内注释,这个注释有一个特定的格式/* webpackChunkName: 名称 */这样的话就可以给分包所产生的bundle起上名字了。
// import posts from './posts/posts';
// import album from './album/album';
const render = () => {
const hash = locaton.hash || '#posts';
const mainElement = document.querySelector('.main');
mainElement.innerHTML = '';
if (hash === '#posts') {
// mainElement.appendChild(posts());
import(/* webpackChunkName: posts */'./posts/posts').then(({ default: posts}) => {
mainElement.appendChild(posts());
})
} else if (hash === '#album') {
// mainElement.appendChild(album());
import(/* webpackChunkName: album */'./album/album').then(({ default: album}) => {
mainElement.appendChild(album());
})
}
}
render();
window.addEventListener('hashchange', render);
如果chunkName是相同的,那相同的chunkName最终就会被打包到一起。
// import posts from './posts/posts';
// import album from './album/album';
const render = () => {
const hash = locaton.hash || '#posts';
const mainElement = document.querySelector('.main');
mainElement.innerHTML = '';
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());
})
}
}
render();
window.addEventListener('hashchange', render);
23. MiniCssExtractPlugin
MiniCssExtractPlugin是一个可以将css代码从打包结果中提取出来的插件,通过这个插可以实现css模块的按需加载。
yarn add mini-css-extract-plugin
打开webpack的配置文件需要先导入这个插件,导入过后可以将这个插件添加到配置对象的plugins数组中。MiniCssExtractPlugin在工作时会自动提取代码中的css到一个单独文件中。
除此以外目前所使用的样式模块是先交给css-loader解析,然后再交给style-loader处理。style-loader的作用就是将样式代码通过style标签的方式注入到页面当中,从而使样式可以工作。使用MiniCssExtractPlugin的话,样式会单独存放到文件当中也就不需要style标签,而是直接通过link的方式去引入,所以就不再需要style-loader了,取而代之的是MiniCssExtractPlugin中提供一个loader,实现样式文件通过link标签的方式去注入。
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = allModes.map(item => {
return {
mode: 'none',
entry: {
main: './src/index.js',
},
output: {
filename: `[name].bundle.js`,
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
// 'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: `index.html`
}),
new MiniCssExtractPlugin()
]
}
})
重新打包dist目录中看到提取出来的样式文件了。如果样式文件体积不是很大的话不建议提取他到单个文件当中,一般css文件超过了150kb左右才需要考虑是否将他提取到单独文件当中,否则css嵌入到代码当中减少了一次请求效果可能会更好。
24. OptimizeCssAssetsWebpackPlugin
使用MiniCssExtractPlugin过后样式文件可以被提取到单独的css文件中但是这里同样会有一个小问题。命令行以生产模式去运行打包。
yarn webpack --mode production
那按照之前的了解,在生产模式下webpack会自动压缩输出的结果。但是这里会发现样式文件根本没有任何的变化。这是因为webpack内置的压缩插件,紧紧是针对于js文件压缩,对于其他资源文件压缩,需要额外的插件支持。webpack官方推荐optimize-css-assets-webpack-plugin压缩样式文件。
yarn add optimize-css-assets-webpack-plugin
配置文件中需要先导入这个插件,导入过后把这个插件添加到plugins数组当中。
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = allModes.map(item => {
return {
mode: 'none',
entry: {
main: './src/index.js',
},
output: {
filename: `[name].bundle.js`,
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
// 'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: `index.html`
}),
new MiniCssExtractPlugin(),
new OptimizeCssAssetsWebpackPlugin()
]
}
})
这时打包的样式文件就以压缩文件的格式输出了。不过这里还有一个额外的小点,在官方文档中这个插件并不是配置在plugins数组中的而是添加到optimization属性的minimize属性中。如果把这个插件配置到plugins数组中,这个插件在任何情况下都会正常工作。而配置在minimize数组中只会在minimize特性开启时才会工作。
webpack建议这种压缩类的插件应该配置到minimize数组当中,以便于可以通过minimize选项统一控制。
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = allModes.map(item => {
return {
mode: 'none',
entry: {
main: './src/index.js',
},
output: {
filename: `[name].bundle.js`,
},
optimization: {
minimize: [
new OptimizeCssAssetsWebpackPlugin()
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
// 'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: `index.html`
}),
new MiniCssExtractPlugin(),
new OptimizeCssAssetsWebpackPlugin()
]
}
})
此时如果没有开启压缩功能这个插件就不会工作,如果说我们以生产模式打包minimize属性就会自动开启,压缩插件就会自动工作。
但是这么配置也有一个小小的缺点,看一下输出的js文件会发现,原本可以自动压缩的js确不能自动压缩了。这是因为这里设置了minimize数组,webpack认为如果配置了这个数组,就是要自定义所使用的的压缩器插件内部的js压缩器就会被覆盖掉,所以这里需要手动的把他添加回来。内置的js压缩插件叫做terser-webpack-plugin需要安装这个模块,
yarn add terser-webpack-plugin --dev
安装完成后把这个插件手动的添加到minimize数组中。
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');
module.exports = allModes.map(item => {
return {
mode: 'none',
entry: {
main: './src/index.js',
},
output: {
filename: `[name].bundle.js`,
},
optimization: {
minimize: [
new OptimizeCssAssetsWebpackPlugin(),
new TerserWebpackPlugin()
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
// 'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: `index.html`
}),
new MiniCssExtractPlugin(),
new OptimizeCssAssetsWebpackPlugin()
]
}
})
25. Hash文件名
一般部署前端资源时会启用服务器的静态资源缓存,这样对用户的浏览器而言,可以缓存住应用中的静态资源。后续就不再需要请求服务器。整体应用的响应速度就有大幅度的提升。
不过开启静态资源的客户端缓存,会有一些小小的问题,如果缓存策略中缓存失效时间设置的过短效果就不是特别明显。如果过期时间设置的比较长,一但应用发生了更新又没有办法及时更新到客户端。
为了解决这样一个问题建议在生产模式下,给输出的文件名中添加Hash值,这样一旦资源文件发生改变,文件名称也可以跟着一起变化。
对于客户端而言,全新的文件名就是全新的请求,那也就没有缓存的问题,可以把服务端缓存策略的缓存时间设置的非常长,也不用担心文件更新过后的问题。
webpack中的filename属性和绝大多数插件的filename属性都支持通过占位符的方式为文件名设置hash,不过这里支持三种hash效果各不相同。
普通的hash可以通过[hash]设置,这个hash实际上是整个项目级别的,也就是说一旦项目中任何一个地方发生改动,打包过程中的hash值都会发生变化。
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');
module.exports = allModes.map(item => {
return {
mode: 'none',
entry: {
main: './src/index.js',
},
output: {
filename: `[name]-[hash].bundle.js`,
},
optimization: {
minimize: [
new OptimizeCssAssetsWebpackPlugin(),
new TerserWebpackPlugin()
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
// 'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: `index.html`
}),
new MiniCssExtractPlugin({
filename: '[name]-[hash].bundle.css'
}),
]
}
})
其次是chunkhash,这个hash是chunk级别的,只要是同一路的打包chunkhash都是相同的。
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');
module.exports = allModes.map(item => {
return {
mode: 'none',
entry: {
main: './src/index.js',
},
output: {
filename: `[name]-[chunkhash].bundle.js`,
},
optimization: {
minimize: [
new OptimizeCssAssetsWebpackPlugin(),
new TerserWebpackPlugin()
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
// 'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: `index.html`
}),
new MiniCssExtractPlugin({
filename: '[name]-[chunkhash].bundle.css'
}),
]
}
})
这里虽然只配置了一个打包入口index,但是在代码中通过动态导入的方式分别形成了两路chunk分别是posts和album。样式文件是从代码中单独提取出来的,所以他并不是单独的chunk,main、posts、album三者chunkhash各不相同。
css和所对应的js文件他们二者的chunkhash是完全一样的因为他们是同一路。当index发生修改重新打包会发现,只有main.bundle文件名发生了变化,其他的文件都没有变。在posts.js文件中做一些修改,输出的js和css都会发生变化,因为他们是属于同一个chunk。
至于main.bundle也发生变化的原因是posts所生成的js文件和css文件的文件名发生了变化,入口文件中引入他们的路径也会发生变化所以mian.chunk算是被动的改变。
contenthash是文件级别的hash,根据输出文件的内容生成的hash值,也就是说只要是不同的文件就有不同的hash值。
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');
module.exports = allModes.map(item => {
return {
mode: 'none',
entry: {
main: './src/index.js',
},
output: {
filename: `[name]-[contenthash].bundle.js`,
},
optimization: {
minimize: [
new OptimizeCssAssetsWebpackPlugin(),
new TerserWebpackPlugin()
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
// 'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: `index.html`
}),
new MiniCssExtractPlugin({
filename: '[name]-[contenthash].bundle.css'
}),
]
}
})
那相比于前两者contenthash是解决缓存问题最好的方式,因为他精确的定位到了文件级别的hash,只有当这个文件发生了变化才有可能更新文件名,实际上是最适合解决缓存问题的。
webpack允许指定hash的长度,可以在占位符里面通过冒号跟一个数组[:8]的方式指定hash的长度。
new MiniCssExtractPlugin({
filename: '[name]-[contenthash:8].bundle.css'
})
总的来说如果是控制缓存8位的contenthash应该是最好的选择了。