我是瘦子

Webpack 优化总会让你不得不爱

加油

在家的日子不能出去玩,不能出去吃,的确是很不开心的,不过也是真的增加了好多空闲时间

与其在家呆着无聊,不如安安静静的学习一下

疫情期间,不管怎样,心态不能崩,武汉加油,中国加油

闲言少叙,现在我们就开始一起学习吧

优化是个好词

优化,就是加以改变或选择使优良,在工作当中是让提升效率的好办法。
当然,webpack 优化千千万,但我觉得这些就够了
首当其冲的,就是工作中那些用不到的样式,可能是由于历史遗留原因已经忘记哪些是没有使用的样式了,一一排查太过耗时费力了
于是,purgecss-webpack-pluginglob 它俩就登场了,它的作用就是解决上面提到的问题,让我们来看看如何使用吧
插件千万个,安装第一步:

1
npm i purgecss-webpack-plugin glob -D

去除无用的样式

工欲善其事必先利其器,装备好了,我们也来看一眼实际的情况,然后再进行有效的配置吧
webpack 配置
上图中就有一个没有被使用到的类名logo,所以如果在打包抽离出来的css文件里,肯定是不希望看到它的
那么,不废话,解决它,我们开始配置吧

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
// webpack.config.js文件

const path = require('path');
// html模板
const HtmlWebpackPlugin = require('html-webpack-plugin');
// 从js中抽离出css
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

// 去除无用的样式
const glob = require('glob');
const PurgecssWebpackPlugin = require('purgecss-webpack-plugin');

module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve('dist')
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader'
}
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
}
]
},
plugins: [
new HtmlWebpaclPlugin({
template: './src/index.html'
}),
new MiniCssExtractPlugin(),
// 去除无用的样式
new PurgecssWebpackPlugin({
paths: glob.sync('./src/**/*', {nodir: true})
})
]
};

配置完毕了,上面包含了一些基本的配置。大家可以把重点放在注释为去除无用的样式代码部分即可了

下面我们来简单分析分析:

  • glob是用来查找文件的

    1
    2
    3
    4
    glob.sync('./src/**/*', {nodir: true}
    // 同步查找src目录下的任意文件夹下的任意文件
    // 返回一个数组,如['真实路径/src/css/style.css','真实路径/src/index.js',...]
    // {nodir: true}表示不包含文件夹,加快查找速度
  • purgecss-webpack-plugin是去除无用的css

    1
    2
    3
    4
    5
    new PurgecssWebpackPlugin({
    // paths表示指定要去解析的文件名数组路径
    // Purgecss会去解析这些文件然后把无用的样式移除
    paths: glob.sync('./src/**/*', {nodir: true})
    })

webpack 配置2

大功告成,进入下一环节!!!

动态添加CDN

html 文件中引入cdn文件,在 webpack 配置 externals,这样就不会打包引入的cdn的库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// index.html文件

<body>
<div id="root"></div>
<!-- 引入jquery的cdn -->
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
</body>

// webpack.config.js文件

module.exports = {
externals: {
'jquery': '$'
}
}

这样写完后,在js文件中我们就可以不用再导入jquery也能直接使用$操作符了
But,这只是个过渡而已,下面有请主角登场
由于每次都需要在index.html模板中手动引入需要的cdn文件,然后还要在webpack里配置,有点繁琐了
So, html-webpack-externals-plugin 这样的插件就应运而生了
安装步骤我就略过了,直接看代码

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
// webpack.config.js文件

// 动态添加CDN
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');

module.exports = {
plugins: [
new HtmlWebpackExternalsPlugin({
externals: [
{ // 引入的模块
module: 'jquery',
// cdn的地址
entry: 'https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js',
// 挂载到了window上的名称
// window.jQuery就可以全局使用
global: 'jQuery'
},
{
module: 'vue',
entry: 'https://cdn.bootcss.com/vue/2.6.10/vue.min.js',
global: 'Vue'
}
]
})
]
};

webpack 配置3

Tree-shaking

这是一个webpack内置的优化能力,webpack很好很强大,哈哈
在生产环境下,Tree-shaking会进行自动删除的操作
如果通过ES6的import引用的方式就会把没有用到的代码给删除掉
那么在打包的时候,就不会打包那些未引用的方法了
接下来,我们看个栗子:
在src目录下新建一个common.js,然后在里面随便写点东东

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/common.js文件

export const flatten = arr => {
console.log('my-flatten');
return arr.reduce((all, cur) => {
if (Array.isArray(cur) {
return [...all, ...flatten(cur)];
} else {
return [...all, cur];
}
}, []);
};

export const myBind = (fn, context) => {
console.log('my-bind');
let args = [].slice.call(arguments, 2);
return function() {
let newArgs = [].slice.call(arguments);
fn.apply(context, args.concat(newArgs));
}
};

import 引用一下

1
2
3
4
5
6
// index.js文件

import { flatten } from './common';

let arr = [1,[3,[4, 5]], [2, [20]]];
console.log(flatten(arr));

webpack 配置4
因为只在生产环境下有效果,可以去dist文件中检查一下,是没有 console.log('my-bind') 的代码的,这就说明完美的去掉了没有引用到的代码了
完美,完美,完美了

高兴的太早了,是药三分毒,有副作用的

副作用

现在我们在src目录再创建一个test.js文件,让大家再感受一下副作用

1
2
3
4
5
6
7
// src/test.js文件

export default test = () => {
console.log('test');
}

test();

此时,简单修改一下index.js

1
2
3
4
5
6
7
// index.js文件

import { flatten } from './common';
import test from './test';

let arr = [1,[3,[4, 5]], [2, [20]]];
console.log(flatten(arr));

尽管引入了test但是并没有使用,可是打包后的bundle.js文件却变化了,请看大屏幕
webpack 配置5

可想而知,副作用出现了,去dist目录下再看看bundle.js的内容,发现了打印test字段的代码。
肿么办?没引用却打包进去了,表慌,让我们来将文件标记为无副作用的
干掉副作用
需要配合 package.json 文件,在里面添加一个 sideEffects 属性,赋值为 false 就把这些副作用给干掉了不会再打包进去了,So Easy

1
2
3
4
5
// package.json文件
{
...省略
"sideEffects": false
}

webpack 配置6

不过貌似,没有看到打包出来的css文件呢,回头看之前的截图,是打包出3个文件的,其中就包括 main.css
CSS去哪儿了?
很好理解,因为我们在js中引入css文件是 import './style.css' 这样,所以就出现了副作用,引用却没使用的尴尬
也是因为 sideEffects: false 一股脑的全给标记过滤掉了
现在修改一下 sideEffects 的值就可以,给它一个去除副作用的范围

1
2
3
4
5
6
// package.json文件

{
...省略
"sideEffects": ["./src/**/*.css"]
}

过滤掉引入的css文件产生的副作用,这样走丢的css文件就又找回来了
webpack 配置7
至此,webpack内置的Tree-shaking就说完了,是不是很有意思,webpack大法好
下面一鼓作气继续说个内置的插件,非常实用,come on baby

DllPlugin动态链接库

很多时候我们在开发时无论是用React还是Vue,我们都不希望这个开发的主力框架每次都被打包一遍,这样也是费时费力的事情
所以,出现了DllPlugin这种插件,它纯属webpack内置的,放心大胆的用
作用:

在第一次打包的时候就把打包用到的开发框架直接打包好,然后会生成一个 manifest.json 文件
再打包的时候,只要有 import React from 'react' 这样的引用,它就会先去所谓的缓存文件里找,找到了就直接用,也不用再进行对react打包了
当然,如果没找到的话,再对框架打包一遍也无伤大雅

说多了,都是泪,看代码更实在,重写index.js

1
2
3
4
5
6
7
8
9
10
// index.js文件

import React from 'react';
import { render } from 'react-dom';
import './style.css';

render(<React.Fragment>
<h1 className="title">听妈妈的话-周杰伦</h1>
<button className="btn">显示歌词</button>
</React.Fragment>, window.root);

把index.js重写后我们再来npm run build打包一下看看
webpack 配置8

但是这很不科学,因为很多时候没有必要把开发框架也打包到我们写的逻辑代码中
所以,接下来我们来看看DllPlugin会帮我们怎么处理吧

创建动态链接库

根目录下创建一个 webpack.dll.js 文件,用来打包出dll文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// webpack.dll.js文件

const path = require('path');
// 引入webpack
const webpack = require('webpack');

module.exports = {
entry: ['react', 'react-dom'],
output: {
filename: 'react.dll.js',
path: path.resolve('dll'),
library: 'react' // 打包后被引用的变量名
},
plugins: [
// 动态链接库
new webpack.DllPlugin({
name: 'react',
path: path.resolve('dll', 'manifest.json')
})
]
};

代码写完了,npm run dll,之后会出现一个dll的文件夹,里面会包含你打包出来的文件,如下图
webpack 配置9
打包完成后,接下来轮到我们引用的时刻到了

引用动态链接库

回到我们的主战场webpack.config.js中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// webpack.config.js文件

const path = require('path');
// 引入webpack
const webpack = require('webpack');

module.exports = {
plugins: [
// 引用对应的动态链接库的manifest.json文件
// 这样以后再引入react的时候就会优先在json文件里去寻找
new webpack.DllReferencePlugin({
manifest: path.resolve('dll', 'manifest.json')
})
]
};

写到这里还不算完,还需要在src目录下的index.html模板中引入一下
<script src="../dll/react.dll.js"></script>
插一句:之所以,会新建一个dll目录,因为在 npm run dev 开发环境编译的时候,dist目录的内容都在内存中了,是找不到react.dll.js文件的
好了,现在让我们看看效果吧,npm run dev 启动一下
webpack 配置10

当然,也许有人会问,如果需要引入的文件比较多怎么办?
每次在index.html中手动引入毕竟不是长久之计,那么接下来就再看一个好东东

动态引入js

通过 add-asset-html-webpack-plugin 插件就可以完成这样的需求,来看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// webpack.config.js文件

const webpack = require('webpack');
// 添加资源到html文件
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');

module.exports = {
plugins: [
// 引用打包好的react,不会打包到bundle里
new webpack.DllReferencePlugin({
manifest: path.resolve('dll', 'manifest.json')
}),
// 直接将打包好的react.dll.js添加到html模板
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve('dll', 'react.dll.js')
})
]
};

通过上面的改造后,就动态的把react.dll.js文件添加到html文件中了。

我们之前在index.html模板里手动引入js的那一行就可以删除掉了

懒加载

说到懒加载必然是一种很好的优化网页或应用的方式,那么在webpack中也是通过ES6的 import() 语法来引入的。
虽然这个 import() 语法目前还处在草案的第三阶段,不过并不影响大家对它的一致好评
用过Vue-Router的同学都知道,大家写的路由通过 component: () => import() 的方式,也是可以进行懒加载的
So,好饭不怕晚,早晚会成为正式一员的。下面还是用实际栗子演示一下吧

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
// index.js文件

import './style.css';
import React from 'react';
import { render } from 'react-dom';

// 写个辅助的类
class Music extends React.Component {
constructor() {
super();
this.state = { lrc: [] };
}
showLrc() {
// 通过ES6的import()方法实现了懒加载功能,实际上是利用了jsonp去动态导入了
import('./lrc').then(data => {
let lrc = data.default.split('\n').filter(item => item !== '');
this.setState({ lrc });
});
}
render() {
return (
<div>
<button className="btn" onClick={() => this.showLrc()}>显示歌词</button>
<div className="lrc-box">
{this.state.lrc && this.state.lrc.map((item, index) => (
<p className="lrc" key={index}>{item}</p>
))}
</div>
</div>
)
}
}

render(<React.Fragment>
<h1 className="title">听妈妈的话-周杰伦</h1>
<Music></Music>
</React.Fragment>, window.root);

npm run dev 让我们看看是不是有了懒加载的效果
webpack 配置11
通过上面的gif图可以看到,当点击显示歌词的时候,加载了一个0.bundle.js文件
这个文件就是我们通过懒加载导入的lrc.js文件,由此可见,懒加载功能验证成功,撒花

接下来再来个老生常谈的优化,请继续往下看,不要停

抽取公共代码

开发的时候,经常会有不同的模块引用了同一个第三方包。
这里举个栗子,比如有两个js文件,一个是index.js另一个是lrc.js,它们都引用了著名的实用工具库lodash,代码如下下下

1
2
3
4
5
6
7
8
9
// index.js文件

import _ from 'lodash';
console.log(_.xor([2, 1], [2, 3]));


// lrc.js文件
import _ from 'lodash';
console.log(_.flatten([1,[3, 4, 5, [2]]]));

看完以上代码就明白了,他们的公共部分就是都引了lodash,这样会分别打包到他们所在的文件中去,这样打包的js文件就会很大了

所以,必须得把lodash提取出来,废话不多说,看招

抽取第三方模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// webpack.config.js文件

module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
chunks: 'initial',
minSize: 0,
minChunks: 2,
test: /node_modules/,
priority: 1
}
}
}
}
};

webpack4中自带了抽取公共代码的方法,通过optimization里的splitChunks来做到
webpack 配置12
抽离第三方模块有两点好处

  • 不会和业务逻辑打包在一起
  • 增加缓存 304

抽取公共模块

当然了,项目中很多js文件不仅仅会引用第三方模块来开发,我们也会使用写好的公共模块,那么是不是也可以提取出来呢?

1
2
3
4
5
6
7
// index.js文件
import { flatten } from './common';
console.log('index',flatten([1,[33, 4, 5, [34]]]));

// lrc.js文件
import {flatten} from './common';
console.log(flatten([1,[33, 4, 5, [34]]]));

上面在两个js文件中都引入了common.js中写好的flatten方法,既然它们都引用到了common.js,所以想当然的也可以把它抽取出来的

那么参照提取第三方代码的实现来写一下吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// webpack.config.js文件

module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
utils: {
chunks: 'initial',
minSize: 0,
minChunks: 2
}
}
}
}
};

webpack 配置13
以上就是抽取公共代码部分了,到此为止了

热更新

热更新对于开发来说可以说是非常高效的,而且webpack现在也自带插件支持热更新了

通过 devServer 来启动热更新

☆:devServer的使用需要在项目中提前安好 webpack-dev-server

那么我们先来看看配置部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// webpack.config.js文件

// 引入webpack
const webpack = require('webpack');

module.exports = {
devServer: {
hot: true, // 启动热更新
port: 8080,
contentBase: './dist'
},
plugins: [
// webpack支持热更新插件
new webpack.HotModuleReplacementPlugin(),
// 打印更新了的文件路径
new webpack.NamedModulesPlugin()
]
};

热更新只适合在开发环境下来搞,所以配置好后,再执行 npm run dev

现在让我们回到index.js文件里去,让我们感受一下

// index.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
// 热更新
// 在src目录下新创建了audio.js文件
// 需要导入这个文件,不然热更新失效
import audio from './audio';

// 检查是否支持热更新
if (module.hot) {
// 接收热更新的模块
module.hot.accept('./audio.js', (path) => {
console.log(path);
console.log('audio文件更新了');
});
}

这样就完成了热更新操作,接收了audio.js文件,所以在audio.js文件内部如果进行修改保存后,会在控制台里展示对应的更新信息,如下图

webpack 配置14

热更新现在可是开发中的老朋友,很多IDE都配置了热更新操作,比如像Android Studio这样的编译器,通过热更新来加快编译速度
好了,不啰嗦了,下面还是干货满满的,Enjoy it

跨域请求

严格意义上来讲,通过webpack来做跨域请求实际上也不能算在优化里。不过在开发中,这种跨域的情况还是很多的,了解了也不吃亏,一起看看吧

还是通过老朋友devServer来实现的,上代码

第一种跨域方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// webpack.config.js文件

module.exports = {
devServer: {
proxy: {
// 可以这样写
// '/api': 'http://localhost:3000',
// 也可以这样写,多配置
'/api': {
target: 'http://localhost:3000',
pathRewrite: {
'^/api': ''
}
}
}
}
};

通过devServer提供的proxy属性,可以完成我们想要的跨域请求,下面看看参数都是干什么的

  • target
    指定要跨域请求的url
    比如你请求/api/userInfo就会代理到http://localhost:3000/api/userInfo

  • pathRewrite
    顾名思义,重写路径
    接口不可能都是/api开头的,所以如果遇到个接口是/getSongs
    而不是/api/getSongs的接口地址,会把以/api重写改为空字符
    最后就可以访问http://localhost:3000/getSongs

第二种跨域方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// webpack.config.js文件

module.exports = {
devServer: {
// 利用node来写
before(app) {
// 相当于直接写了后端的接口,哈哈
app.get('/api/info', (req, res) => {
res.json({
nickname: '我滴个大榴莲啊',
level: 8,
src: 'https://music.163.com/song/media/outer/url?id=1382794914.mp3'
});
});
}
}
};

当然,以上两种方式大家知道即可了,第一种也是最普遍的实现方式,妈妈再也不用担心我的跨域问题了

IgnorePlugin

作用: 忽略打包第三方模块指定的目录

为什么要忽略呢? 通过下面的栗子来看一下

相信很多人应该或多或少的都听过moment这个时间库,不知道也没关系,我来演示一波

先安装moment: npm i moment -S

1
2
3
4
5
6
7
8
9
10
// index.js文件

// 导入moment
import moment from 'moment';

// 设置中文
moment.locale('zh-cn');
let time = moment().endOf('day').fromNow();

window.root.innerHTML += time;

webpack 配置15

页面上展示的一点毛病都没有,不过如果看一下打包的情况就会发现有瑕疵了,你看
webpack 配置16

设置了中文,却把整个语言包都打包进去了,这样很不好
这是神马原因呢,其实是因为moment被导入的时候,附赠了整个locale语言包,这种买一赠一的行为就不用提现在代码世界了,吃不消了
webpack 配置17

我们需要用中文包,但是不想打包全部语言包,就让IgnorePlugin出马了

1
2
3
4
5
6
7
8
9
10
// webpack.config.js文件

const webpack = require('webpack');

module.exports = {
plugins: [
// 忽略moment目录下的locale文件夹
new webpack.IgnorePlugin(/\.\/locale/, /moment/)
]
};

配置改写后,再回到index.js中单独导入中文语言包就好了

1
2
3
4
5
6
7
8
9
10
// index.js文件

// 利用IgnorePlugin把只需要的语言包导入使用就可以了,省去了一下子打包整个语言包
import moment from 'moment';
// 单独导入中文语言包
import 'moment/locale/zh-cn';

let time = moment().endOf('day').fromNow();

window.root.innerHTML += time;

再重新 npm run build 打包后,体积瞬间减少了278k啊啊啊啊,下图显示
webpack 配置18

noParse

noParse的作用是不去解析你所使用的第三方库中的依赖库

废话不多说,直接上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// webpack.config.js文件

module.exports = {
module: {
// 不去解析jquery或lodash中的依赖库
noParse: /jquery|lodash/,
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader'
}
}
]
}
}

在工作中,忽略大型的库可以提高构建性能,可以从构建时间上看出来速度的提升,如上面代码中提到的jquery和lodash

resolve

从这个英文就能看出来,它就是配置模块如何解析用的,配置太多也没必要一一介绍了,还是直接说重点写出常用的配置吧

resolve常用配置

  • modules
    指定解析第三方包的目录位置
  • alias
    指定import导入时的别名,简化引入
  • extensions
    自动解析确定好的扩展名
    默认会把js和json当做扩展名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// webpack.config.js文件

const { resolve } = require('path');

module.exports = {
resolve: {
modules: [resolve('node_modules')],
alias: {
Utils: resolve(__dirname, 'src/utils/'),
'@': resolve(__dirname, 'src')
},
extensions: ['.js', '.css', '.json']
}
}

此刻,我们往src目录下创建一个utils文件夹,然后新建一个parse-url.js文件

1
2
3
4
5
6
7
8
9
10
11
12
// src/utils/parse-url.js文件

// 就简单导出一下
export default '我是解析url的方法';



// src/index.js文件

import parseUrl from 'Utils/parse-url';

console.log(parseUrl); // 打印:我是解析url的方法

上面代码为我们展示了alias别名的效果,真的是很有用很方便的,哈哈

include和exclude

  • include: 包含指定目录下的文件解析
  • exclude: 排除指定目录不进行解析
    二者使用一个即可了,想必这个优化的点大家并不陌生的,看眼代码吧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader'
},
exculde: /node_modules/, // 二选一
include: path.resolve('src') // 二选一
}
]
}
}

happypack

webpack在Node环境下运行所以也是单线程操作,一件一件的去处理事情。这样很不nice,本着现在cpu都那么威猛的情况下,运用多核运算完全是小儿科的
于是乎,就有了happypack的用武之地了,它的作用就是可以实现多进程打包操作
下面我们再来看下是如何配置的
配置前先来安装一下 npm i happypack -D

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
// webpack.config.js文件

const Happypack = require('happypack');

module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: 'Happypack/loader?id=js'
},
{
test: /\.css$/,
use: 'Happypack/loader?id=css'
}
]
},
plugins: [
new Happypack({
id: 'js',
use: [
{
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-react'
]
}
}
]
}),
new Happypack({
id: 'css',
use: ['style-loader', 'css-loader']
})
]
}

差不多了,大家真的辛苦了,哈哈,坚持看下来的小伙伴,真是太给力了

简单几句

优化的内容有多种多样,也不会全部都包含,上面提到的也都是比较常见的一些优化方式,各取所需、各取所用就好了

还是那句话,疫情总会过去的,没什么好怕的,一起努力吧!!!

感谢大家,886

文章来源:Webpack 优化总会让你不得不爱