Webpack进阶

Webpack进阶

Tree Shaking

::: tip 理解
Tree Shaking是一个术语,通常用于描述移除js中未使用的代码。
:::
::: warning 注意
Tree Shaking 只适用于ES Module语法(既通过export导出,import引入),因为它依赖于ES Module的静态结构特性。
:::

在正式介绍Tree Shaking之前,我们需要现在src目录下新建一个math.js文件,它的代码如下:

1
2
3
4
5
6
export function add(a, b) {
console.log(a + b);
}
export function minus(a, b) {
console.log(a - b);
}

接下来我们对index.js做一下处理,它的代码像下面这样,从math.js中引用add方法并调用:

1
2
import { add } from './math'
add(1, 4);

在上面的.js改动完毕后,我们最后需要对webpack.config.js做一下配置,让它支持Tree Shaking,它的改动如下:

{8,9,10}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const path = require('path');
module.exports = {
mode: 'development',
devtool: 'source-map',
entry: {
main: './src/index.js'
},
optimization: {
usedExports: true
},
output: {
filename: 'main.js',
path: path.resolve(__dirname,'dist')
}
}

在以上webpack.config.js配置完毕后,我们需要使用npx webpack进行打包,它的打包结果如下:

1
2
3
4
5
6
7
8
9
10
11
// dist/main.js
"use strict";
/* harmony export (binding) */
__webpack_require__.d(__webpack_exports__, "a", function() { return add; });
/* unused harmony export minus */
function add(a, b) {
console.log(a + b);
}
function minus(a, b) {
console.log(a - b);
}

打包结果分析:虽然我们配置了 Tree Shaking,但在开发环境下,我们依然能够看到未使用过的minus方法,以上注释也清晰了说明了这一点,这个时候你可能会问:为什么我们配置了Tree Shakingminus方法也没有被使用,但依然还是被打包进了main.js中?

其实这个原因很简单,这是因为我们处于开发环境下打包,当我们处于开发环境下时,由于source-map等相关因素的影响,如果我们不把没有使用的代码一起打包进来的话,source-map就不是很准确,这会影响我们本地开发的效率。

看完以上本地开发Tree Shaking的结果,我们也知道了本地开发Tree Shaking相对来说是不起作用的,那么在生产环境下打包时,Tree Shaking的表现又如何呢?

在生产环境下打包,需要我们对webpack.config.js中的mode属性,需要由development改为production,它的改动如下:

{3}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const path = require('path');
module.exports = {
mode: 'production',
devtool: 'source-map',
entry: {
main: './src/index.js'
},
optimization: {
usedExports: true
},
output: {
filename: 'main.js',
path: path.resolve(__dirname,'dist')
}
}

配置完毕后,我们依然使用npx webpack进行打包,可以看到,它的打包结果如下所示:

1
2
3
4
5
6
7
8
9
// dist/main.js
([function(e,n,r){
"use strict";
var t,o;
r.r(n),
t=1,
o=4,
console.log(t+o)
}]);

打包代码分析:以上代码是一段被压缩过后的代码,我们可以看到,上面只有add方法,未使用的minus方法并没有被打包进来,这说明在生产环境下我们的Tree Shaking才能真正起作用。

SideEffects

::: tip 说明
由于Tree Shaking作用于所有通过import引入的文件,如果我们引入第三方库,例如:import _ from 'lodash'或者.css文件,例如import './style.css' 时,如果我们不
做限制的话,Tree Shaking将起副作用,SideEffects属性能帮我们解决这个问题:它告诉webpack,我们可以对哪些文件不做 Tree Shaking
:::

1
2
3
4
5
6
7
8
// 修改package.json
// 如果不希望对任何文件进行此配置,可以设置sideEffects属性值为false
// *.css 表示 对所有css文件不做 Tree Shaking
// @babael/polyfill 表示 对@babel/polyfill不做 Tree Shaking
"sideEffects": [
"*.css",
"@babel/polyfill"
],

小结:对于Tree Shaking的争议比较多,推荐看:point_right:你的Tree Shaking并没有什么卵用,看完你会发现我们对Tree Shaking的了解还需要进一步加深。

区分开发模式和生产模式

像上一节那样,如果我们要区分Tree Shaking的开发环境和生产环境,那么我们每次打包的都要去更改webpack.config.js文件,有没有什么办法能让我们少改一点代码呢? 答案是有的!
::: tip 说明
区分开发环境和生产环境,最好的办法是把公用配置提取到一个配置文件,生产环境和开发环境只写自己需要的配置,在打包的时候再进行合并即可,**webpack-merge** 可以帮我们做到这个事情。
:::

首先,我们效仿各大框架的脚手架的形式,把 Webpack 相关的配置都放在根目录下的build文件夹下,所以我们需要新建一个build文件夹,随后我们要在此文件夹下新建三个.js文件和删除webpack.config.js,它们分别是:

  • webpack.common.js:Webpack 公用配置文件
  • webpack.dev.js:开发环境下的 Webpack 配置文件
  • webpack.prod.js:生产环境下的 Webpack 配置文件
  • webpack.config.js删除根目录下的此文件

新建完webpack.common.js文件后,我们需要把公用配置提取出来,它的代码看起来应该是下面这样子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
entry: {
main: './src/index.js'
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader','css-loader']
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader"
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
new CleanWebpackPlugin()
],
output: {
filename: '[name].js',
path: path.resolve(__dirname,'dist')
}
}

提取完 Webpack 公用配置文件后,我们开发环境下的配置,也就是webpack.dev.js中的代码,将剩下下面这些:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const webpack = require('webpack');
module.exports = {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
devServer: {
contentBase: 'dist',
open: true,
port: 3000,
hot: true,
hotOnly: true
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
}

而生产环境下的配置,也就是webpack.prod.js中的代码,可能是下面这样子的:

1
2
3
4
5
6
7
module.exports = {
mode: 'production',
devtool: 'cheap-module-source-map',
optimization: {
usedExports: true
}
}

在处理完以上三个.js文件后,我们需要做一件事情:

  • 当处于开发环境下时,把webpack.common.js中的配置和webpack.dev.js中的配置合并在一起
  • 当处于开发环境下时,把webpack.common.js中的配置和webpack.prod.js中的配置合并在一起

针对以上问题,我们可以使用webpack-merge进行合并,在使用之前,我们需要使用如下命令进行安装:

1
$ npm install webpack-merge -D

安装完毕后,我们需要对webpack.dev.jswebpack.prod.js做一下手脚,其中webpack.dev.js中的改动如下(代码高亮部分):

{2,3,4,18}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const webpack = require('webpack');
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common');
const devConfig = {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
devServer: {
contentBase: 'dist',
open: true,
port: 3000,
hot: true,
hotOnly: true
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
}
module.exports = merge(commonConfig, devConfig);

相同的代码,webpack.prod.js中的改动部分如下(代码高亮):

{1,2,3,10}
1
2
3
4
5
6
7
8
9
10
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common');
const prodConfig = {
mode: 'production',
devtool: 'cheap-module-source-map',
optimization: {
usedExports: true
}
}
module.exports = merge(commonConfig, prodConfig);

聪明的你一定想到了,因为上面我们已经删除了webpack.config.js文件,所以我们需要重新在package.json中配置一下我们的打包命令,它们是这样子写的:

1
2
3
4
"scripts": {
"dev": "webpack-dev-server --config ./build/webpack.dev.js",
"build": "webpack --config ./build/webpack.prod.js"
},

配置完打包命令,心急的你可能会马上开始尝试进行打包,你的打包目录可能长成下面这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|-- build
| |-- dist
| | |-- index.html
| | |-- main.js
| | |-- main.js.map
| |-- webpack.common.js
| |-- webpack.dev.js
| |-- webpack.prod.js
|-- src
| |-- index.html
| |-- index.js
| |-- math.js
|-- .babelrc
|-- postcss.config.js
|-- package.json

问题分析:当我们运行npm run build时,dist目录打包到了build文件夹下了,这是因为我们把Webpack 相关的配置放到了build文件夹下后,并没有做其他配置,Webpack 会认为build文件夹会是根目录,要解决这个问题,需要我们在webpack.common.js中修改output属性,具体改动的部分如下所示:

{3}
1
2
3
4
output: {
filename: '[name].js',
path: path.resolve(__dirname,'../dist')
}

那么解决完上面这个问题,赶紧使用你的打包命令测试一下吧,我的打包目录是下面这样子,如果你按上面的配置后,你的应该跟此目录类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|-- build
| |-- webpack.common.js
| |-- webpack.dev.js
| |-- webpack.prod.js
|-- dist
| |-- index.html
| |-- main.js
| |-- main.js.map
|-- src
| |-- index.html
| |-- index.js
| |-- math.js
|-- .babelrc
|-- postcss.config.js
|-- package.json

代码分离(CodeSplitting)

::: tip 理解
Code Splitting 的核心是把很大的文件,分离成更小的块,让浏览器进行并行加载。
:::
常见的代码分割有三种形式:

  • 手动进行分割:例如项目如果用到lodash,则把lodash单独打包成一个文件。
  • 同步导入的代码:使用 Webpack 配置进行代码分割。
  • 异步导入的代码:通过模块中的内联函数调用来分割代码。

手动进行分割

手动进行分割的意思是在entry上配置多个入口,例如像下面这样:

1
2
3
4
5
6
module.exports = {
entry: {
main: './src/index.js',
lodash: 'lodash'
}
}

这样配置后,我们使用npm run build打包命令,它的打包输出结果为:

1
2
3
4
5
6
        Asset       Size  Chunks             Chunk Names
index.html 462 bytes [emitted]
lodash.js 1.46 KiB 1 [emitted] lodash
lodash.js.map 5.31 KiB 1 [emitted] lodash
main.js 1.56 KiB 2 [emitted] main
main.js.map 5.31 KiB 2 [emitted] main

它输出了两个模块,也能在一定程度上进行代码分割,不过这种分割是十分脆弱的,如果两个模块共同引用了第三个模块,那么第三个模块会被同时打包进这两个入口文件中,而不是分离出来。

所以我们常见的做法是关心最后两种代码分割方法,无论是同步代码还是异步代码,都需要在webpack.common.js中配置splitChunks属性,像下面这样子:

1
2
3
4
5
6
7
8
module.exports = {
// 其它配置
optimization: {
splitChunks: {
chunks: 'all'
}
}
}

你可能已经看到了其中有一个chunks属性,它告诉 Webpack 应该对哪些模式进行打包,它的参数有三种:

  • async:此值为默认值,只有异步导入的代码才会进行代码分割。
  • initial:与async相对,只有同步引入的代码才会进行代码分割。
  • all:表示无论是同步代码还是异步代码都会进行代码分割。

同步代码分割

在完成上面的配置后,让我们来安装一个相对大一点的包,例如:lodash,然后对index.js中的代码做一些手脚,像下面这样:

1
2
import _ from 'lodash'
console.log(_.join(['Dell','Lee'], ' '));

就像上面提到的那样,同步代码分割,我们只需要在webpack.common.js配置chunks属性值为initial即可:

{5}
1
2
3
4
5
6
7
8
module.exports = {
// 其它配置
optimization: {
splitChunks: {
chunks: 'initial'
}
}
}

webpack.common.js配置完毕后,我们使用npm run build来进行打包, 你的打包dist目录看起来应该像下面这样子:

1
2
3
4
5
6
|-- dist
| |-- index.html
| |-- main.js
| |-- main.js.map
| |-- vendors~main.js
| |-- vendors~main.js.map

打包分析main.js使我们的业务代码,vendors~main.js是第三方模块的代码,在此案例中也就是_lodash中的代码。

异步代码分割

由于chunks属性的默认值为async,如果我们只需要针对异步代码进行代码分割的话,我们只需要进行异步导入,Webpack会自动帮我们进行代码分割,异步代码分割它的配置如下:

{5}
1
2
3
4
5
6
7
8
module.exports = {
// 其它配置
optimization: {
splitChunks: {
chunks: 'async'
}
}
}

注意:由于异步导入语法目前并没有得到全面支持,需要通过 npm 安装 @babel/plugin-syntax-dynamic-import 插件来进行转译

1
$ npm install @babel/plugin-syntax-dynamic-import -D

安装完毕后,我们需要在根目录下的.babelrc文件做一下改动,像下面这样子:

{6}
1
2
3
4
5
6
7
{
"presets": [["@babel/preset-env", {
"corejs": 2,
"useBuiltIns": "usage"
}]],
"plugins": ["@babel/plugin-syntax-dynamic-import"]
}

配置完毕后,我们需要对index.js做一下代码改动,让它使用异步导入代码块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 点击页面,异步导入lodash模块
document.addEventListener('click', () => {
getComponent().then((element) => {
document.getElementById('root').appendChild(element)
})
})

function getComponent () {
return import(/* webpackChunkName: 'lodash' */'lodash').then(({ default: _ }) => {
var element = document.createElement('div');
element.innerHTML = _.join(['Dell', 'lee'], ' ')
return element;
})
}

写好以上代码后,我们同样使用npm run build进行打包,dist打包目录的输出结果如下:

1
2
3
4
5
6
|-- dist
| |-- 1.js
| |-- 1.js.map
| |-- index.html
| |-- main.js
| |-- main.js.map

我们在浏览器中运行dist目录下的index.html,切换到network面板时,我们可以发现只加载了main.js,如下图:



当我们点击页面时,才 真正开始加载 第三方模块,如下图(1.js):

SplitChunksPlugin配置参数详解

在上一节中,我们配置了splitChunks属性,它能让我们进行代码分割,其实这是因为 Webpack 底层使用了 splitChunksPlugin 插件。这个插件有很多可以配置的属性,它也有一些默认的配置参数,它的默认配置参数如下所示,我们将在下面为一些常用的配置项做一些说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
module.exports = {
// 其它配置项
optimization: {
splitChunks: {
chunks: 'async',
minSize: 30000,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
};

chunks参数

此参数的含义在上一节中已详细说明,同时也配置了相应的案例,就不再次累述

minSize 和 maxSize

::: tip 说明
minSize默认值是30000,也就是30kb,当代码超过30kb时,才开始进行代码分割,小于30kb的则不会进行代码分割;与minSize相对的,maxSize默认值为0,为0表示不限制打包后文件的大小,一般这个属性不推荐设置,一定要设置的话,它的意思是:打包后的文件最大不能超过设定的值,超过的话就会进行代码分割。
:::
为了测试以上两个属性,我们来写一个小小的例子,在src目录下新建一个math.js文件,它的代码如下:

1
2
3
export function add(a, b) {
return a + b;
}

新建完毕后,在index.js中引入math.js:

1
2
import { add } from './math.js'
console.log(add(1, 2));

打包分析:因为我们写的math.js文件的大小非常小,如果应用默认值,它是不会进行代码分割的,如果你要进一步测试minSizemaxSize,请自行修改后打包测试。

minChunks

::: tip 说明
默认值为1,表示某个模块复用的次数大于或等于一次,就进行代码分割。
:::
如果将其设置大于1,例如:minChunks:2,在不考虑其他模块的情况下,以下代码不会进行代码分割:

1
2
3
// 配置了minChunks: 2,以下lodash不会进行代码分割,因为只使用了一次 
import _ from 'lodash';
console.log(_.join(['Dell', 'Lee'], '-'));

maxAsyncRequests 和 maxInitialRequests

  • maxAsyncRequests:它的默认值是5,代表在进行异步代码分割时,前五个会进行代码分割,超过五个的不再进行代码分割。
  • maxInitialRequests:它的默认值是3,代表在进行同步代码分割时,前三个会进行代码分割,超过三个的不再进行代码分割。

automaticNameDelimiter

这是一个连接符,左边是代码分割的缓存组,右边是打包的入口文件的项,例如vendors~main.js

cacheGroups

::: tip 说明
在进行代码分割时,会把符合条件的放在一组,然后把一组中的所有文件打包在一起,默认配置项中有两个分组,一个是vendorsdefault
:::

vendors组: 以下代码的含义是,将所有通过引用node_modules文件夹下的都放在vendors组中

1
2
3
4
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
}

default组: 默认组,意思是,不符合vendors的分组都将分配在default组中,如果一个文件即满足vendors分组,又满足default分组,那么通过priority的值进行取舍,值最大优先级越高。

1
2
3
4
5
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}

reuseExistingChunk: 中文解释是复用已存在的文件。意思是,如果有一个a.js文件,它里面引用了b.js,但我们其他模块又有引用b.js的地方。开启这个配置项后,在打包时会分析b.js已经打包过了,直接可以复用不用再次打包。

1
2
3
4
5
6
7
// a.js
import b from 'b.js';
console.log('a.js');

// c.js
import b from 'b.js';
console.log('c.js');

自定义文件名

我们如果不对代码分隔后的文件进行配置的话,那么在vendors组里面的文件名,默认会按vendors+main(入口)的形式命名,例如:vendors~main.js,如果我们想要自定义配置文件名的话,则需要分情况:

  • 同步代码分隔:使用filename命名。
  • 非同步代码分隔:使用name来命令。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // 同步代码分隔
    module.exports = {
    // 其它配置略
    splitChunks: {
    chunks: 'initial',
    vendors: {
    test: /[\\/]node_modules[\\/]/,
    priority: -10,
    filename: 'vendors.js'
    }
    }
    }

    // 非同步代码分隔
    module.exports = {
    // 其它配置略
    splitChunks: {
    chunks: 'async',
    vendors: {
    test: /[\\/]node_modules[\\/]/,
    priority: -10,
    name: 'vendors'
    }
    }
    }

Lazy Loading懒加载

::: tip 理解
Lazy Loading懒加载的理解是:通过异步引入代码,这里说的异步,并不是在页面一开始就加载,而是在合适的时机进行加载。
:::
Lazy Loading懒加载的实际案例我们已经在上一小节书写了一个例子,不过我们依然可以做一下小小的改动,让它使用async/await进行异步加载,它的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 页面点击的时候才加载lodash模块
document.addEventListener('click', () => {
getComponet().then(element => {
document.body.appendChild(element);
})
})
async function getComponet() {
const { default: _ } = await import(/* webpackChunkName: 'lodash' */ 'lodash');
var element = document.createElement('div');
element.innerHTML = _.join(['1', '2', '3'], '**')
return element;
}

以上懒加载的结果与上一小节的结果类似,就不在此展示,你可以在你本地的项目中打包后自行测试和查看。

PreLoading 和Prefetching

::: tip 理解
在以上Lazy Loading的例子中,只有当我们在页面点击时才会加载lodash,也有一些模块虽然是异步导入的,但我们希望能提前进行加载,PreLoadingPrefetching可以帮助我们实现这一点,它们的用法类似,但它们还是有区别的:Prefetching不会跟随主进程一起下载,而是等到主进程加载完毕,带宽释放后才进行加载,PreLoading会随主进程一起加载。
:::
实现PreLoading或者Prefetching非常简单,我们只需要在上一节的例子中加一点点代码即可(参考高亮部分):

{8}
1
2
3
4
5
6
7
8
9
10
11
12
// 页面点击的时候才加载lodash模块
document.addEventListener('click', () => {
getComponet().then(element => {
document.body.appendChild(element);
})
})
async function getComponet() {
const { default: _ } = await import(/* webpackPrefetch: true */ 'lodash');
var element = document.createElement('div');
element.innerHTML = _.join(['1', '2', '3'], '**')
return element;
}

改写完毕后,我们使用npm run dev或者npm run build进行打包,在浏览器中点击页面,我们将在network面板看到如下图所示:

相信聪明的你一定看到了0.js,它是from disk cache,那为什么?原因在于,Prefetching的代码它会在head头部,添加像这样的一段内容:

1
<link rel="prefetch" as="script" href="0.js">

这样一段内容追加到head头部后,指示浏览器在空闲时间里去加载0.js,这正是Prefetching它所能帮我们做到的事情,而PreLoading的用法于此类似,请自行测试。

CSS代码分割

::: tip 理解
当我们在使用style-loadercss-loader打包.css文件时会直接把CSS文件打包进.js文件中,然后直接把样式通过<style></style>的方式写在页面,如果我们要把CSS单独打包在一起,然后通过link标签引入,那么可以使用mini-css-extract-plugin插件进行打包。
:::
::: warning
截止到写此文档时,此插件还未支持HMR,意味着我们要使用这个插件进行打包CSS时,为了开发效率,我们需要配置在生产环境下,开发环境依然还是使用style-loader进行打包

此插件的最新版已支持HMR
:::
在配置之前,我们需要使用npm install进行安装此插件:

1
$ npm install mini-css-extract-plugin -D

安装完毕后,由于此插件已支持HMR,那我们可以把配置写在webpack.common.js中(以下配置为完整配置,改动参考高亮代码块):

{4,15,16,17,18,19,36,37,38}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: {
main: './src/index.js'
},
module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
hmr: true,
reloadAll: true
}
},
'css-loader'
]
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader"
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: '[name].css'
})
],
optimization: {
splitChunks: {
chunks: 'all'
}
},
output: {
filename: '[name].js',
path: path.resolve(__dirname,'../dist')
}
}

配置完毕以后,我们来在src目录下新建一个style.css文件,它的代码如下:

1
2
3
body {
color: green;
}

接下来,我们改动一下index.js文件,让它引入style.css,它的代码可以这样写:

1
2
3
import './style.css';
var root = document.getElementById('root');
root.innerHTML = 'Hello,world'

使用npm run build进行打包,dist打包目录如下所示:

1
2
3
4
5
6
|-- dist
| |-- index.html
| |-- main.css
| |-- main.css.map
| |-- main.js
| |-- main.js.map

::: warning 注意
如果发现并没有打包生成main.css文件,可能是Tree Shaking的副作用,应该在package.json中添加属性sideEffects:['*.css']
:::

CSS压缩

::: tip 理解
CSS压缩的理解是:当我们有两个相同的样式分开写的时候,我们可以把它们合并在一起;为了减少CSS文件的体积,我们需要像压缩JS文件一样,压缩一下CSS文件。
:::
我们再在src目录下新建style1.css文件,内容如下:

1
2
3
body{
line-height: 100px;
}

index.js文件中引入此CSS文件

1
2
3
4
import './style.css';
import './style1.css';
var root = document.getElementById('root');
root.innerHTML = 'Hello,world'

使用打包npm run build打包命令,我们发现虽然插件帮我们把CSS打包在了一个文件,但并没有合并压缩。

1
2
3
4
5
6
body {
color: green;
}
body{
line-height: 100px;
}

要实现CSS的压缩,我们需要再安装一个插件:

1
$ npm install optimize-css-assets-webpack-plugin -D

安装完毕后我们需要再一次改写webpack.common.js的配置,如下:

{1,8,9,10}
1
2
3
4
5
6
7
8
9
10
11
12
const optimizaCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
// 其它配置
optimization: {
splitChunks: {
chunks: 'all'
},
minimizer: [
new optimizaCssAssetsWebpackPlugin({})
]
}
}

配置完毕以后,我们再次使用npm run build进行打包,打包结果如下所示,可以看见,两个CSS文件的代码已经压缩合并了。

1
body{color:red;line-height:100px}

Webpack和浏览器缓存(Caching)

在讲这一小节之前,让我们清理下项目目录,改写下我们的index.js,删除掉一些没用的文件:

1
2
3
4
5
import _ from 'lodash';

var dom = document.createElement('div');
dom.innerHTML = _.join(['Dell', 'Lee'], '---');
document.body.append(dom);

清理后的项目目录可能是这样的:

1
2
3
4
5
6
7
8
9
|-- build
| |-- webpack.common.js
| |-- webpack.dev.js
| |-- webpack.prod.js
|-- src
|-- index.html
|-- index.js
|-- postcss.config.js
|-- package.json

我们使用npm run build打包命令,打包我们的代码,可能会生成如下的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|-- build
| |-- webpack.common.js
| |-- webpack.dev.js
| |-- webpack.prod.js
|-- dist
| |-- index.html
| |-- main.js
| |-- main.js.map
| |-- vendors~main.js
| |-- vendors~main.js.map
|-- src
|-- index.html
|-- index.js
|-- package.json
|-- postcss.config.js

我们可以看到,打包生成的dist目录下,文件名是main.jsvendors~main.js,如果我们把dist目录放在服务器部署的话,当用户第一次访问页面时,浏览器会自动把这两个.js文件缓存起来,下一次非强制性刷新页面时,会直接使用缓存起来的文件。


假如,我们在用户第一次刷新页面和第二次刷新页面之间,我们修改了我们的代码,并再一次部署,这个时候由于浏览器缓存了这两个.js文件,所以用户界面无法获取最新的代码。


那么,我们有办法能解决这个问题呢,答案是[contenthash]占位符,它能根据文件的内容,在每一次打包时生成一个唯一的hash值,只要我们文件发生了变动,就重新生成一个hash值,没有改动的话,[contenthash]则不会发生变动,可以在output中进行配置,如下所示:

1
2
3
4
5
6
7
// 开发环境下的output配置还是原来的那样,也就是webpack.common.js中的output配置
// 因为开发环境下,我们不用考虑缓存问题
// webpack.prod.js中添加output配置
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js'
}

使用npm run build进行打包,dist打包目录的结果如下所示,可以看到每一个.js文件都有一个唯一的hash值,这样配置后就能有效解决浏览器缓存的问题。

1
2
3
4
5
6
|-- dist
| |-- index.html
| |-- main.8bef05e11ca1dc804836.js
| |-- main.8bef05e11ca1dc804836.js.map
| |-- vendors~main.4b711ce6ccdc861de436.js
| |-- vendors~main.4b711ce6ccdc861de436.js.map

Shimming

有时候我们在引入第三方库的时候,不得不处理一些全局变量的问题,例如jQuery的$,lodash的_,但由于一些老的第三方库不能直接修改它的代码,这时我们能不能定义一个全局变量,当文件中存在$或者_的时候自动的帮他们引入对应的包。
::: tip 解决办法
这个问题,可以使用ProvidePlugin插件来解决,这个插件已经被 Webpack 内置,无需安装,直接使用即可。
:::
src目录下新建jquery.ui.js文件,代码如下所示,它使用了jQuery$符号,创建这个文件目的是为了来模仿第三方库。

1
2
3
export function UI() {
$('body').css('background','green');
}

创建完毕后,我们修改一下index.js文件, 让它使用刚才我们创建的文件:

1
2
3
4
5
6
7
8
import _ from 'lodash';
import $ from 'jquery';
import { UI } from './jquery.ui';

UI();

var dom = $(`<div>${_.join(['Dell', 'Lee'], '---')}</div>`);
$('#root').append(dom);

接下来我们使用npm run dev进行打包,它的结果如下:

问题: 我们发现,根本运行不起来,报错$ is not defined

解答: 这是因为虽然我们在index.js中引入的jquery文件,但$符号只能在index.js才有效,在jquery.ui.js无效,报错是因为jquery.ui.js$符号找不到引起的。

以上场景完美再现了我们最开始提到的问题,那么我们接下来就通过配置解决,首先在webpack.common.js文件中使用ProvidePlugin插件:
::: tip 说明
配置$:'jquery',只要我们文件中使用了$符号,它就会自动帮我们引入jquery,相当于import $ from 'jquery'
:::

{3,13,14,15,16}
1
2
3
4
5
6
7
8
9
10
const webpack = require('webpack');
module.exports = {
// 其它配置
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
_: 'lodash'
})
]
}

打包结果: 使用npm run dev进行打包,打包结果如下,可以发现,项目已经可以正确运行了。

处理全局this指向问题

我们现在来思考一个问题,一个模块中的this到底指向什么,是模块自身还是全局的window对象

1
2
// index.js代码,在浏览器中输出:false
console.log(this===window);

如上所示,如果我们使用npm run dev运行项目,运行index.html时,会在浏览器的console面板输出false,证明在模块中this指向模块自身,而不是全局的window对象,那么我们有什么办法来解决这个问题呢?
::: tip 解决办法
安装使用imports-loader来解决这个问题
:::

1
$ npm install imports-loader -D

安装完毕后,我们在webpack.common.js加一点配置,在.js的loader处理中,添加imports-loader

{13}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module.exports = {
// ... 其它配置
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader'
},
{
loader: 'imports-loader?this=>window'
}
]
}
]
}
}

配置完毕后使用npm run dev来进行打包,查看console控制台输出true,证明this这个时候已经指向了全局window对象,问题解决。

编辑