webpack选择性编译

项目背景

  项目中有些地方引用了本地的mock数据,这些数据全部在一个mock.js的文件中,由于数据较多,比较占位置,所以想在生产环境下移除该部分代码。具体实现还得靠webpack插件。以这个demo为例进行讲解。

关键点

  • webpack.DefinePlugin 定义工程中的全局变量,本项目中定义了ISDEBUG来判断运行环境是否是生产环境
  • terser-webpack-plugin 压缩代码,与uglifyjs-webpack-plugin功能一样,甚至参数都相差不大。但是对于ES6的支持程度比后者更好。该插件优化项之一是去除项目中无效代码,这是核心依赖项
  • webpack-bundle-analyzer 分析打包结果
  • 项目中mock.js的引用方式要使用Commonjs标准的require(‘mock’)

项目目录结构

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
├─config // webpack配置
│ ├─entry.js
│ ├─logConfig.js
│ ├─server.js
│ ├─webpack.common.js
│ ├─webpack.dev.js
│ └─webpack.pro.js
├─dist // 打包输出目录
├─entries // 多页入口
├─public // 模板目录
└─src
├─mock // 本地mock数据,将要在生产环境下移除的部分
│ └─mock.js
├─service // 引用mock数据的地方,这里要用require动态引入mock.js
│ ├─details.js
│ ├─life.js
│ └─menu.js
└─views // 业务逻辑页面
├─detail
│ └─index.vue
├─life
│ └─index.vue
│─menu
│ └─index.vue
└─Home.vue

项目中service下的每个文件都会引用mock.js文件。优化前,这些文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// mock.js
const mockData = {
code: 0,
data: {
name: '菲尼克斯',
job: '净化者',
position: "圣堂武士",
profession: "净化者",
duty: "执政官"
}
};

export default mockData;

1
2
3
4
5
6
7
8
9
10
11
// service下的js文件
import mockData from 'mockDataPath';

const ISDEBUG = process.env.NODE_ENV !== 'production';

if (ISDEBUG) {
result = mockData;
}

export default result;

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
<!-->views下的页面文件<-->
<template>
<div>
<p class="test-text">
Detail
</p>
<div>
{{ data }}
</div>
</div>
</template>

<script>
import mockData from '@/service/details';

export default {
name: 'Detail',
data() {
return {
data: ''
};
},
mounted () {
this.data = mockData;
}
};
</script>

<style lang="scss" scoped>
.test-text {
font-size: 50px;
}
</style>

没有优化的打包结果:

noMock

优化步骤

webpack增加配置:terser-webpack-plugin

mock.js文件中的数据只是本地调试时使用的,所以,我们的目的是在生产环境下移除这部分代码。在上文我们提到的简化代码的插件terser-webpack-plugin,它使用terser.js来优化代码,并且有一项功能,就是打包时移除无效代码:

mock

什么是无效代码,就像这种:

1
2
3
if (false) {
console.log('这是一段无效代码')
}

所以我们需要对mock文件引用方式做一个调整

更新本地文件引用方式

require与import的区别

  在这里需要说一下require与import引入模块的一些区别。因为我们需要把项目中引用模块的方式由ES6的import全部改成Commonjs标准的require。原因如下:

  • import是ES6的模块引入方式,它是在JS引擎编译阶段执行的。在代码运行前,遇到import就会生成一个只读引用,然后在运行阶段,碰到有用到引入模块值的再去引用模块取值,可能项目中并没有引用引入模块的值,但是已经声明了,就会执行。所以无法在生产环境下移除该部分代码,
  • require是运行时执行,可以做到按需加载。
  • 以后import可能也会支持按需引入,在ES2020提案中,可以使用import(‘module’)来按需加载。目前可以使用babel的@babel/plugin-syntax-dynamic-import插件进行兼容

优化结果

修改文件引用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
// mock.js
const mockData = {
code: 0,
data: {
name: '菲尼克斯',
job: '净化者',
position: "圣堂武士",
profession: "净化者",
duty: "执政官"
}
};

module.exports = mockData;
1
2
3
4
5
6
7
8
// service下的js文件
let result = '';
if (ISDEBUG) { // 全局变量ISDEBUG控制代码是否有效
const mockData = require('mockDataPath'); // require按需加载
result = mockData;
}

export default result;
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
// webpack部分增加配置
webpackChainConfig
.plugin('DefinePlugin')
.use(webpack.DefinePlugin, [{
ISDEBUG: process.env.NODE_ENV !== 'production' // 定义全局变量ISDEBUG
}])
webpackChainConfig
.optimization
.minimizer('terser')
.use(TerserPlugin, [{
exclude: /\.min\.js$/, // 排除已压缩项
cache: true,
parallel: true,
sourceMap: false,
extractComments: false, // 不生成注释文档
terserOptions: {
warnings: false,
compress: {
booleans: true,
if_return: true,
sequences: true,
unused: true, // 移除无效代码
drop_debugger: true,
pure_funcs: ['console.log'] // 移除console.log
},
output: {
comments: false // 移除注释
}
}
}]
)

优化结果如下,mock.js已经没有了

mock

方案更新

  • 使用webpack内置插件ProvidePlugin全局引入公共模块,需要使用时即可直接使用,无需每次手动require引入

尝试方案

  在这之前,还试过webpack属性external,但无法达成目的。因为它的作用是打包时排除某一个已经采用CDN或其他方式引用的库或文件。关键点是已经设置了CDN等别的引入方式。因为打包之后该引用的地方还是会继续引用,区别是本地环境下,可能从本地node_modules引入。打包时,webpack会自动排除该文件,改从已经设置好的CDN引入了,这样可以极大的减少打包体积,但是会增加网络请求量,使用时也要权衡利弊。这个属性之前没怎么用过,不然也不会在这里采坑~

总结

  1. 使用webpack.DefinePlugin定义全局变量ISDEBUG控制代码是否有效;
  2. 使用require按需引入,或用ProvidePlugin全局引入;
  3. 使用terser-webpack-plugin优化代码,移除无效代码

参考内容

ECMAScript 6 入门——阮一峰