My Little World

webpack优化

优化开发体验

目的是提升开发效率

优化构建速度

解决项目庞大时构建的耗时加长的问题

缩小文件查找范围

1.由于Loader对文件转换很耗时,所以应该让尽可能少的文件被处理,适当调整项目目录结构,在配置loader时通过【include】属性缩小处理命中文件范围
2.当安装第三方模块都放在项目根目录的./node_module目录下时,通过指明【resolve.modules】为存放第三方模块的绝对路径,减少寻找第三方模块的递归查找
3.在项目中所有第三方模块都采用main字段去描述入口文件时,只给【resolve.mainFileds】配置main字段,不使用默认值,减少搜索步骤
4.使用【resolve.alias】进行路径映射时,将导入模块的语句,替换成直接使用模块中完整文件的语句,减少耗时的对于lib中文件的解析工作
5.优化【resolve.extension】配置,减少尝试次数
a.后缀列表尽可能小,不要将项目中不可能存在的情况写到后缀列表中
b.频率出现最高的文件后缀要优先放在最前面,以做到尽快退出寻找过程
c.在源码中写导入语句时,尽可能带上后缀,从而避免寻找过程
6.合理使用【module.noparse】属性,排除不需要进行模块解析处理的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1.
rules:{
//只对项目根目录下src目录中文件采用配置的loader
include:path.resolve(__dirname,'src')
}
2.
resolve:{
//使用绝对路径指明第三方模块存放的位置,减少搜索步骤
modules:[path.resolve(__dirname,'node_module')]
}
3.
resolve:{
//减少入口查找
mainFilds:['main']
}
4.
resolve:{
//减少第三方模块递归解析
alias:{
'react':path.resolve(__dirname,'./node_module/react/dist/react.min.js')
}
}

使用DLLPlugin

一个动态链接库文件可以包含为其他模块调用的函数和数据
提升构建速度的原理:
包含大量复用模块的动态链接库只需被编译一次,在之后的构建过程中被动态链接库包含的模块将不会重新编译,而是直接使用动态链接库的代码,
由于动态链接库中大多数包含的是常用的第三方模块,例如react,react-dom,所以只要不升级这些模块的版本,动态连接库就不用重新编译

DllPlugin:打包出一个个单独的动态链接库文件
DllReferencePlugin:用于在主要的配置文件中引入DllPlugin打包好的动态连接库文件

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
//webpack_dll.config.js
const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');

module.exports = {
// JS 执行入口文件
entry: {
// 把 React 相关的放到一个单独的动态链接库
react: ['react', 'react-dom'],
// 把项目需要所有的 polyfill 放到一个单独的动态链接库
polyfill: ['core-js/fn/object/assign', 'core-js/fn/promise', 'whatwg-fetch'],
},
output: {
// 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称,也就是 entry 中配置的 react 和 polyfill
filename: '[name].dll.js',
// 输出的文件都放到 dist 目录下
path: path.resolve(__dirname, 'dist'),
// 存放动态链接库的全局变量名称,例如对应 react 来说就是 _dll_react
// 之所以在前面加上 _dll_ 是为了防止全局变量冲突
library: '_dll_[name]',
},
plugins: [
// 接入 DllPlugin
new DllPlugin({
// 动态链接库的全局变量名称,需要和 output.library 中保持一致
// 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
// 例如 react.manifest.json 中就有 "name": "_dll_react"
name: '_dll_[name]',
// 描述动态链接库的 manifest.json 文件输出时的文件名称
path: path.join(__dirname, 'dist', '[name].manifest.json'),
}),
],
};

webpack_dll.config.js文件中,DllPlugin中的name参数必须和output

//webpack.config.js
const path = require('path');
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');

module.exports = {
entry: {
// 定义 入口 Chunk
main: './main.js'
},
output: {
// 输出文件的名称
filename: '[name].js',
// 输出文件都放到 dist 目录下
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
// 项目源码使用了 ES6 和 JSX 语法,需要使用 babel-loader 转换
test: /\.js$/,
use: ['babel-loader'],
exclude: path.resolve(__dirname, 'node_modules'),
},
]
},
plugins: [
// 告诉 Webpack 使用了哪些动态链接库
new DllReferencePlugin({
// 描述 react 动态链接库的文件内容
manifest: require('./dist/react.manifest.json'),
}),
new DllReferencePlugin({
// 描述 polyfill 动态链接库的文件内容
manifest: require('./dist/polyfill.manifest.json'),
}),
],
devtool: 'source-map'
};

执行构建流程:
先编译出动态链接库相关的文件,执行webpack –config webpack_dll.config.js
在确保动态链接库存在时才能正常编译入口文件,webpack.config.js中的DllReferencePlugin会依赖动态链接库相关文件,执行webpack命令

使用HappyPack

提升构建速度原理:
将任务分解给多个子进程去并发执行,子进程处理完后再将结果发送给主进程,从而让webpack在同一时刻处理多个任务
实际表现就是
所有通过Loader处理的文件都先交给happypack/loader去处理,在收集到这些文件的处理权后,HappyPack就可以统一分配了
每通过new HappyPack() 去实例化一个HappyPack,其实就是告诉HappyPack核心调度器如何通过一系列Loader去转换一类文件,
并且可以指定如何为这类转换操作分配子进程
核心调度器的逻辑代码在主进程中,也就是运行着webpack的进程中,核心调度器会将一个个任务分配给当前空闲的子进程,
子进程处理完毕后将结果发送给核心调度,他们之间的数据交换是通过进程间的通信API实现的
核心调度器收到来自子进程处理完毕的结果后,会通知WebPack该文件已处理完毕

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
50
51
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HappyPack = require('happypack');

module.exports = {
// JS 执行入口文件
entry: {
main: './main.js',
},
output: {
// 把所有依赖的模块合并输出到一个 bundle.js 文件
filename: '[name].js',
// 输出文件都放到 dist 目录下
path: path.resolve(__dirname, './dist'),
},
module: {
rules: [
{
test: /\.js$/,
// 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例
use: ['happypack/loader?id=babel'],
// 排除 node_modules 目录下的文件,node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换
exclude: path.resolve(__dirname, 'node_modules'),
},
{
// 把对 .css 文件的处理转交给 id 为 css 的 HappyPack 实例
test: /\.css$/,
use: ExtractTextPlugin.extract({
use: ['happypack/loader?id=css'],
}),
},
]
},
plugins: [
new HappyPack({
// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
id: 'babel',
// 如何处理 .js 文件,用法和 Loader 配置中一样
loaders: ['babel-loader?cacheDirectory'],
}),
new HappyPack({
id: 'css',
// 如何处理 .css 文件,用法和 Loader 配置中一样
loaders: ['css-loader'],
}),
new ExtractTextPlugin({
filename: `[name].css`,
}),
],
devtool: 'source-map' // 输出 source-map 方便直接调试 ES6 源码
};

在实例化 HappyPack 插件的时候,除了可以传入 id 和 loaders 两个参数外,HappyPack 还支持如下参数:
threads: 代表开启几个子进程去处理这一类型的文件,默认是3个,类型必须是整数。
verbose: 是否允许 HappyPack 输出日志,默认是 true。
threadPool :代表共享进程池,即多个 HappyPack 实例都使用同一个共享进程池中的子进程去处理任务,以防止资源占用过多

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
const HappyPack = require('happypack');
// 构造出共享进程池,进程池中包含5个子进程
const happyThreadPool = HappyPack.ThreadPool({ size: 5 });

module.exports = {
plugins: [
new HappyPack({
// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
id: 'babel',
// 如何处理 .js 文件,用法和 Loader 配置中一样
loaders: ['babel-loader?cacheDirectory'],
// 使用共享进程池中的子进程去处理任务
threadPool: happyThreadPool,
}),
new HappyPack({
id: 'css',
// 如何处理 .css 文件,用法和 Loader 配置中一样
loaders: ['css-loader'],
// 使用共享进程池中的子进程去处理任务
threadPool: happyThreadPool,
}),
new ExtractTextPlugin({
filename: `[name].css`,
}),
],
};

使用ParallelUglifyPlugin

提升构建速度原理:
当webpack有多个JS文件需要输出和压缩时,原本会使用UglifyJS去一个一个压缩再输出,但是ParallelUglifyPlugin会开启多个子进程,
将对多个文件的压缩工作分配给多个子进程完成,每个子进程其实还是通过UglifyJS去压缩代码,单变成了并行执行

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
const path = require(‘path’);
const DefinePlugin = require(‘webpack/lib/DefinePlugin’);
const ParallelUglifyPlugin = require(‘webpack-parallel-uglify-plugin’);

module.exports = {
plugins: [
// 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
new ParallelUglifyPlugin({
// 传递给 UglifyJS 的参数
uglifyJS: {
output: {
// 最紧凑的输出
beautify: false,
// 删除所有的注释
comments: false,
},
compress: {
// 在UglifyJs删除没有用到的代码时不输出警告
warnings: false,
// 删除所有的 console 语句,可以兼容ie浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
}
},
}),
],
};

在通过 new ParallelUglifyPlugin() 实例化时,支持以下参数:
test:使用正则去匹配哪些文件需要被 ParallelUglifyPlugin 压缩,默认是 /.js$/,也就是默认压缩所有的 .js 文件。
include:使用正则去命中需要被 ParallelUglifyPlugin 压缩的文件。默认为 []。
exclude:使用正则去命中不需要被 ParallelUglifyPlugin 压缩的文件。默认为 []。
cacheDir:缓存压缩后的结果,下次遇到一样的输入时直接从缓存中获取压缩后的结果并返回。cacheDir 用于配置缓存存放的目录路径。默认不会缓存,想开启缓存请设置一个目录路径。
workerCount:开启几个子进程去并发的执行压缩。默认是当前运行电脑的 CPU 核数减去1。
sourceMap:是否输出 Source Map,这会导致压缩过程变慢。
uglifyJS:用于压缩 ES5 代码时的配置,Object 类型,直接透传给 UglifyJS 的参数。

其中的 test、include、exclude 与配置 Loader 时的思想和用法一样
UglifyES 是 UglifyJS 的变种,专门用于压缩 ES6 代码,它们两都出自于同一个项目,并且它们两不能同时使用。
UglifyES 一般用于给比较新的 JavaScript 运行环境压缩代码,例如用于 ReactNative 的代码运行在兼容性较好的 JavaScriptCore 引擎中,为了得到更好的性能和尺寸,采用 UglifyES 压缩效果会更好。
ParallelUglifyPlugin 同时内置了 UglifyJS 和 UglifyES,也就是说 ParallelUglifyPlugin 支持并行压缩 ES6 代码。
如果设置 cacheDir 开启了缓存,在之后的构建中会变的更快。

优化使用体验

通过自动化手段完成一些重复工作,让我们专注于解决问题本身

优化文件监听

通过配置watchOption进行优化
ignored:/node_module/ 不监听node_module下文件 监听更少文件
agregatetionTimeout:值越大,降低构建频率
poll:值越小,降低检查频率
后面两项的配置会使监听模式的反应和灵敏度降低

优化自动刷新性能

1.使用devserver开启inline时,devserver因为不知道某个网页依赖哪几个Chunk,所以会向每个输出的chunk中注入代理客户端代码
当项目输出多个chunk时,就会导致构建缓慢,因此关闭inline进行优化
如果不想以iframe方式去访问,但同时想让网页保持自动刷新功能,则需要手动向网页中注入代理客户端的脚本,
向index.html中注入webpack-dev-server.js,但要注意发布到线上时要删掉这段用于开发的代码

2.开启热替换时,控制台打印信息不能标明模块信息,可以使用NameModulesPlugin插件解决,从而在控制台打印出被修改的模块名称
注意关闭默认inline模式并手动注入客户端的方法,不能用于模块热替换的情况,原因在于模块热替换的运行依赖每个chunk中都包含代理客户端的代码

优化输出质量

目的是为用户呈现体验更好的网页

减少用户能够感知到的加载时间(首屏时间)

区分环境

问题:
开发过程会涉及到调试的代码,没必要发到线上给到用户
开发环境和线上接口地址不同,使用不同环境数据
线上代码会进行压缩,开发代码不需要、

解决:
当代码中使用了process模块的语句时,webpack会自动打包加入process模块代码来支持非nodejs运行环境
当代码中没有使用时,就不会打包加入
可以在源码中使用process.env.NODE_ENV环境变量去判断执行开发/线上环境的代码
环境变量的设置通过DefinePlugin设置
设置后,环境变量的值在webpack处理过程中会被代入源码中,替换掉process.env.NODE_ENV
访问proces的语句被替换,webpack也就不会在打包时加入process模块了

注意:DefinedPlugin定义的环境变量只对Webpack需要处理的代码有效,而不会影响Nodejs运行时的环境变量

1
2
3
4
5
6
7
8
9
10
11
12
const DefinePlugin = require('webpack/lib/DefinePlugin');

module.exports = {
plugins: [
new DefinePlugin({
// 定义 NODE_ENV 环境变量为 production
'process.env': {
NODE_ENV: JSON.stringify('production')//环境变量的值需要一个由双引号包裹的字符串'"production"'
}
}),
],
};

压缩代码

提升网页加载速度,减少网络传输流量,混淆代码以防有人下载代码进行代码分析和改造
通过插件形式引入UglifyJs,利用UglifyJs分析JS代码语法树,理解代码含义,
从而去掉无效代码,去掉日志输出代码,缩短变量名等优化,仅用于es5
UglifyJsPlugin:封装UglifyJs实现压缩
ParallelUglifyPlugin:多进行并行处理压缩
uglifyjsWebpackPlugin:引入UglifyES,压缩es6,要去掉.babelrc文件中的babel-preset-env,否则会将es6转es5
css-loader?minimize:压缩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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
1.UglifyJsPlugin
const UglifyJSPlugin = require('webpack/lib/optimize/UglifyJsPlugin');//使用内置插件
module.exports = {
plugins: [
new UglifyJSPlugin({
compress: {
// 在 UglifyES 删除没有用到的代码时不输出警告
warnings: false,
// 删除所有的 `console` 语句,可以兼容ie浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
},
output: {
// 最紧凑的输出,默认会保留空格
beautify: false,
// 删除所有的注释
comments: false,
}
}),
],
};

2.uglifyjs-webpack-plugin
const UglifyESPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
plugins: [
new UglifyESPlugin({
uglifyOptions: {//相比UglifyJSPlugin 多了一层
compress: {
// 在 UglifyES 删除没有用到的代码时不输出警告
warnings: false,
// 删除所有的 `console` 语句,可以兼容ie浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
},
output: {
// 最紧凑的输出
beautify: false,
// 删除所有的注释
comments: false,
}
}
}),
],
};

3.压缩CSS
const path = require('path');
const {WebPlugin} = require('web-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
module: {
rules: [
{
test: /\.css/,// 增加对 CSS 文件的支持
// 提取出 Chunk 中的 CSS 代码到单独的文件中
use: ExtractTextPlugin.extract({
use: ['css-loader?minimize'] // 压缩 CSS 代码
}),
},
]
},
plugins: [
// 用 WebPlugin 生成对应的 HTML 文件
new WebPlugin({
template: './template.html', // HTML 模版文件所在的文件路径
filename: 'index.html' // 输出的 HTML 的文件名称
}),
new ExtractTextPlugin({
filename: `[name]_[contenthash:8].css`,// 给输出的 CSS 文件名称加上 hash 值
}),
],
};

使用CDN加速

webpack接入CDN需要满足:
1.静态资源的导入URL需要变成指向CDN服务的绝对路径URL,而不是相对于HTML文件的URL
2.静态资源的文件名需要带上由文件内容算出来的Hash值,以防止被缓存
3.将不同类型资源放到不同域名CDN服务上,以防止资源的并行加载阻塞
实现主要依赖publicPath设置存放静态资源的CDN目录URL

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
const path = require('path');
const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const DefinePlugin = require('webpack/lib/DefinePlugin');
const {WebPlugin} = require('web-webpack-plugin');

module.exports = {
entry: {
// Chunk app 的 JS 执行入口文件
app: './main.js'
},
output: {
// 给输出的 JavaScript 文件名称加上 Hash 值
filename: '[name]_[chunkhash:8].js',
path: path.resolve(__dirname, './dist'),
// 指定存放 JavaScript 文件的线上目录
publicPath: '//js.cdn.com/id/',
},
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
// 排除 node_modules 目录下的文件,node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换
exclude: path.resolve(__dirname, 'node_modules'),
},
{
// 增加对 CSS 文件的支持
test: /\.css/,
// 提取出 Chunk 中的 CSS 代码到单独的文件中
use: ExtractTextPlugin.extract({
// 压缩 CSS 代码
use: ['css-loader?minimize'],
// 指定存放 CSS 中导入的资源(例如图片)的线上目录
publicPath: '//img.cdn.com/id/'
}),
},
{
// 增加对 PNG 文件的支持
test: /\.png/,
// 给输出的 PNG 文件名称加上 Hash 值
use: ['file-loader?name=[name]_[hash:8].[ext]'],
},
]
},
plugins: [
// 使用 WebPlugin 自动生成 HTML,会根据publicPath用线上地址替换原来相对地址
new WebPlugin({
// HTML 模版文件所在的文件路径
template: './template.html',
// 输出的 HTML 的文件名称
filename: 'index.html',
// 指定存放 CSS 文件的线上目录
stylePublicPath: '//css.cdn.com/id/',
}),
new ExtractTextPlugin({
// 给输出的 CSS 文件名称加上 Hash 值
filename: `[name]_[contenthash:8].css`,
}),
new DefinePlugin({
// 定义 NODE_ENV 环境变量为 production 去除开发时才需要的部分
'process.env': {
NODE_ENV: JSON.stringify('production')
}
}),
// 压缩输出的 JS 代码
],
};

使用tree Shaking剔除用不上的代码

Tree Shaking可以分析出那些代码被用上了,哪些没有
Tree Shaking正常工作的前提是,提交给webpack的js代码必须采用了ES6的模块化语法,因为es6模块化语法是静态的
(在导入,导出语句中的路径必须是静态字符串,而且不能放入其他代码块中),这让webpack可以简单地分析出那些export被import了。
实现:
1.修改.babelrc文件,保留ES6语法

1
2
3
4
5
6
7
8
9
10
{
"presets":[
[
"env",
{
"module":false//关闭模块转换功能,保留ES6语法
}
]
]
}

可以使用webpack –display-used-exports查看分析结果
2.使用uglifyJS剔除用不上的代码
可以在直接在配置文件中配置
也可以使用命令行执行
webpack –display-used-exports –optimize-minimize

处理但第三方库时,利用mainField告诉webpack采用那份入口文件,
当指定入口于文件采用es6模块化语法时,从而可以使用treeShaking进行代码优化

提取公共代码

问题:
相同资源被重复加载,浪费用户的流量和服务器成本
每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验

解决:
将多个页面公共代码部分抽离成单独文件,用户在第一次访问后,公共文件代码被浏览器缓存
在用户切换其他页面时,则不会再重新加载存放公共代码的文件,而是直接从缓存中获取
从而
减少网络传输流量,降低服务器成本
虽然用户第一次打开网站的速度得不到优化,但之后访问其他页面的速度将提高

实现:
–根据网站所使用的技术栈,找出网站所有页面都需要用到的基础库(第三方库),将他们提取到一个单独的文件base.js中
该文件包含了所有网页的基础运行环境,用于长期缓存,提高响应速度

–剔除了各个页面中被base.js包含的部分代码后,再找出所有页面都依赖的公共部分的代码,将他们提取到commom.js

–再为每个网页都生成一个单独的文件,不包含base.js和common.js中包含的部分,只包含各页面单独需要的部分代码

– 依赖CommonsChunkPlugin插件
CommonsChunkPlugin实例会生成一个新的Chunk,这个chunk中包含了被提取的代码
通过name属性告诉插件新生成的chunk的名称,chunks属性指明从哪些已有chunk中提取公共部分

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
const path = require('path');
const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const DefinePlugin = require('webpack/lib/DefinePlugin');
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
const {AutoWebPlugin} = require('web-webpack-plugin');

// 使用 AutoWebPlugin,自动寻找 pages 目录下的所有目录,把每一个目录看成一个单页应用
const autoWebPlugin = new AutoWebPlugin('pages', {
template: './template.html', // HTML 模版文件所在的文件路径
// 提取出所有页面公共的代码
commonsChunk: {
name: 'common',// 提取出公共代码 Chunk 的名称
},
});

module.exports = {
// AutoWebPlugin 会找为寻找到的所有单页应用,生成对应的入口配置,
// autoWebPlugin.entry 方法可以获取到生成入口配置
entry: autoWebPlugin.entry({
// 这里可以加入你额外需要的 Chunk 入口
base: './base.js'
}),
output: {
filename: '[name]_[chunkhash:8].js',// 给输出的文件名称加上 hash 值
path: path.resolve(__dirname, './dist'),
},
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
// 排除 node_modules 目录下的文件,node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换
exclude: path.resolve(__dirname, 'node_modules'),
},
{
test: /\.css/,// 增加对 CSS 文件的支持
// 提取出 Chunk 中的 CSS 代码到单独的文件中
use: ExtractTextPlugin.extract({
use: ['css-loader?minimize'] // 压缩 CSS 代码
}),
},
]
},
plugins: [
autoWebPlugin,
// 为了从 common 中提取出 base 也包含的部分,减小common体积
new CommonsChunkPlugin({
// 从 common 和 base 两个现成的 Chunk 中提取公共的部分
chunks: ['common', 'base'],
// 把公共的部分放到 base 中
name: 'base'
}),
new ExtractTextPlugin({
filename: `[name]_[contenthash:8].css`,// 给输出的 CSS 文件名称加上 hash 值
}),
new DefinePlugin({
// 定义 NODE_ENV 环境变量为 production 去除 react 代码中的开发时才需要的部分
'process.env': {
NODE_ENV: JSON.stringify('production')
}
}),
// 压缩输出的 JS 代码
],
};

common.js没有内容的时候解决
1.CommonsChunkPlugin的minChunks属性表示文件要被提取出来时需要在指定的Chunks中出现的最小次数,值越小,被提到common中的文件越多
2.根据各个页面之间的相关性选取其中的部分页面时,可用CommonChunkPlugin提取这部分被选出的页面的公共部分,
而不是提取所有页面的公共部分,而且这样的操作可以叠加多次,缺点是配置复杂,需要根据页面之间关系去思考如何配置
但该方法不通用

分割代码按需加载

问题
单页面应用一次性加载所有功能代码,实际在每个阶段只可能使用其中一部分

解决
将整个网站划分成一个个小功能,再按照每个功能的相关程度将他们分成几类
将每一类合并成一个Chunk,按需加载对应的Chunk
不要按需加载用户首次打开网站时需要看到的画面所对应的功能,将其放到执行入口所在的chunk中,
以减少用户能感知的网页加载时间
对于不依赖大量代码的功能点,可对其进行按需加载

实现

1
2
3
4
5
6
7
//oneName.js
module.exports= function(){}

//加载语句
import(/* webpackChunkName:"oneName" */ './oneName').then((oneName)=>{
oneName()
})

webpack内置了对import(*)语句的支持,当webpack遇到类似的语句时会这样处理
以oneName.js为入口重新生成一个Chunk
当代码执行到import所在的语句时才去加载由Chunk对应生成的文件
import返回一个Promise,当文件加载成功时可以在Promise的then方法中获取oneName.js导出的内容

/* webpackChunkName:”oneName” */ 含义是为动态生成的Chunk赋予一个名称,以方便我们追踪和调试代码
如果不指定,则其默认的名称将会是[id].js
同样需要在webpack中配置chunkFilename属性,来指定动态生成的Chunk在输出时文件名称

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
const path = require('path');

module.exports = {
// JS 执行入口文件
entry: {
main: './main.js',
},
output: {
// 为从 entry 中配置生成的 Chunk 配置输出文件的名称
filename: '[name].js',
// 为动态加载的 Chunk 配置输出文件的名称
chunkFilename: '[name].js',
path: path.resolve(__dirname, './dist'),
},
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
exclude: path.resolve(__dirname, 'node_modules'),
},
]
},
devtool: 'source-map' // 输出 source-map 方便直接调试 ES6 源码
};

//在.babellrc中需要引入组件来识别import(*)语法
{
"presets":[
"env",
"react"
],
"plugins":[
"syntax-dynamic-import"
]
}
//实际应用
import React, {PureComponent, createElement} from 'react';
import {render} from 'react-dom';
import {HashRouter, Route, Link} from 'react-router-dom';
import PageHome from './pages/home';

/**
* 异步加载组件
* @param load 组件加载函数,load 函数会返回一个 Promise,在文件加载完成时 resolve
* @returns {AsyncComponent} 返回一个高阶组件用于封装需要异步加载的组件
*/
function getAsyncComponent(load) {
return class AsyncComponent extends PureComponent {

componentDidMount() {
// 在高阶组件 DidMount 时才去执行网络加载步骤
load().then(({default: component}) => {
// 代码加载成功,获取到了代码导出的值,调用 setState 通知高阶组件重新渲染子组件
this.setState({
component,
})
});
}

render() {
const {component} = this.state || {};
// component 是 React.Component 类型,需要通过 React.createElement 生产一个组件实例
return component ? createElement(component) : null;
}
}
}

// 根组件
function App() {
return (
<HashRouter>
<div>
<nav>
<Link to='/'>Home</Link> | <Link to='/about'>About</Link> | <Link to='/login'>Login</Link>
</nav>
<hr/>
<Route exact path='/' component={PageHome}/>
<Route path='/about' component={getAsyncComponent(
// 异步加载函数,异步地加载 PageAbout 组件
() => import(/* webpackChunkName: 'page-about' */'./pages/about')
)}
/>
<Route path='/login' component={getAsyncComponent(
// 异步加载函数,异步地加载 PageAbout 组件
() => import(/* webpackChunkName: 'page-login' */'./pages/login')
)}
/>
</div>
</HashRouter>
)
}

// 渲染根组件
render(<App/>, window.document.getElementById('app'));

提升流畅度

提升代码性能
1.使用prepack优化代码运行时效率
通过在编译阶段预先执行源码来得到执行结果,再直接将运行结果放到编译后的代码中,而不是在代码运行时才去求值

prepack工作原理和流程大致如下:
通过Babel将JS源码解析成抽象语法树(AST),以更细粒度地分析源码
PrePack实现了一个JS解释器,用于执行源码,借助这个解释器,Prepack才能理解源码具体是如何执行的
并将执行过程中的结果返回到输出中

因为还处于开发阶段,不能识别DOM api和部分nodejs API 代码,优化后文件尺寸可能大大增加,性能可能更差所以目前还不适合用于处理线上代码

2.开启Scope Hoisting
分析模块之间的依赖关系,尽可能将被打散的模块合并到一个函数中,大前提是不能造成代码冗余,
因此只有那些被引用了一次的模块才能被合并
由于Scope Hosting需要分析模块之间的依赖关系,因此源码必须采用ES6语句,不然它将无法生效
对于非ES6模块化语法的代码,webpack会降级处理且不使用Scope Hoisting优化。
为了知道webpack对哪些代码做了降级处理,可以在启动webpack时带上–display-optimization-bailout参数
这样输出的日志就会告知是什么原因导致了降级处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const path = require('path');
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');

module.exports = {
// JS 执行入口文件
entry: './main.js',
output: {
// 把所有依赖的模块合并输出到一个 bundle.js 文件
filename: 'bundle.js',
// 输出文件都放到 dist 目录下
path: path.resolve(__dirname, './dist'),
},
resolve: {
// 针对 Npm 中的第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法的文件
mainFields: ['jsnext:main', 'browser', 'main']
},
plugins: [
// 开启 Scope Hoisting
new ModuleConcatenationPlugin(),
],
};

好处:
代码体积更小,因为函数申明语句会产生大量代码
代码在运行时因为创建的函数作用域变少,所以内存开销也变小了

输出分析

执行命令
webpack –profile –json >stats.json
可以将构建相关的信息输出到stats.json文件中
1.打开http:\//webpack.github.io/analyse/ 上传stats文件,使用官方Webpack Analyse分析输出结果
2.使用webpack-bundle-analyzer进行分析
安装webpack-bundle-analyzer到全局
在项目根目录中执行webpack-bundle-analyzer,浏览器会打开对应网页并展现打包结果

侧重优化输出质量的配置
侧重优化开发体验的配置