
以下配置是在webpack 4.41.6+测试
可用于生产环境:
不可用于生产环境的:
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader?cacheDirectory', // 开启缓存
include: path.resolve(__dirname, 'src'), // 明确范围
// 排除范围,include和exclude两者选一个就行
// exclude: path.resolve(__dirname, 'node_modules')
}
]
}这里的?cacheDirectory放在babel-loader后面,把语法转换的代码缓存下来。只要ES6代码没有改变的,第二次编译的时候,这些ES6没有改动的部分就不会重新编译,直接使用缓存,编译速度更快。
或者这样写
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
// 开启babel缓存
// 第二次构建时,会读取之前的缓存
cacheDirectory: true
}
},一般来说,一个loader写成loader:"babel-loader"这种字符串的形式,多个loader写成use:["babel-loader", "eslint-loader"]字符串数组的形式
HappyPack是一个通过多线程来提升Webpack打包速度的工具,不是多进程,很多博客写的多进程,为此我查阅github的happypack插件说明明确说到是多线程。
在打包过程中有一项非常耗时的工作,就是使用loader将各种资源进行转译处理
我们可以简单地将代码转译的工作流程概括如下:
1)从配置中获取打包入口; 2)匹配
loader规则,并对入口模块进行转译; 3)对转译后的模块进行依赖查找(如a.js中加载了b.js和c.js); 4)对新找到的模块重复进行步骤2)和步骤3),直到没有新的依赖模块。 不难看出从步骤2)到步骤4)是一个递归的过程,Webpack需要一步步地获取更深层级的资源,然后逐个进行转译。
这里的问题在于Webpack是单线程的,假设一个模块依赖于几个其他模块,Webpack必须对这些模块逐个进行转译。虽然这些转译任务彼此之间没有任何依赖关系,却必须串行地执行。HappyPack恰恰以此为切入点,它的核心特性是可以开启多个线程,并行地对不同模块进行转译,这样就可以充分利用本地的计算资源来提升打包速度。
HappyPack适用于那些转译任务比较重的工程,当我们把类似babel-loader和ts-loader迁移到HappyPack之上后,一般都可以收到不错的效果,而对于其他的如sass-loader、less-loader本身消耗时间并不太多的工程则效果一般。
每次webpack解析模块时,HappyPack都会获取它及其所有依赖项,并将这些文件分发到多个工作程序“线程”。如下图

在实际使用时,要用HappyPack提供的loader来替换原有loader,并将原有的那个通过HappyPack插件传进去。请看下面的例子
单个loader的优化(一般不用这个方式,都是使用多个loader的优化,多个loader只写一个就是单个loader)
// 初始Webpack配置(使用HappyPack前)
module.exports = {
//...
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: ['react'],
},
}
],
},
};
// 使用HappyPack的配置
const HappyPack = require('happypack');
module.exports = {
//...
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'happypack/loader',
}
],
},
plugins: [
new HappyPack({
loaders: [
{
loader: 'babel-loader',
options: {
presets: ['react'],
},
}
],
})
],
};在module.rules中,我们使用happypack/loader替换了原有的babel-loader,并在plugins中添加了HappyPack的插件,将原有的babel-loader连同它的配置插入进去即可。
多个loader的优化
提高构建速度,利用好多核CPU
1.安装happyPack
2.引入const HappyPack = require('happypack')
3.使用
在使用HappyPack优化多个loader时,需要为每一个loader配置一个id,否则HappyPack无法知道rules与plugins如何一一对应。
module: {
rules: [
{
test: /\.js$/,
// 把对 .js 文件的处理转交给 id 为 babel123 的 HappyPack 实例
loader: 'happypack/loader?id=babel123',
include: path.resolve(__dirname, 'src'), // 明确范围
// 排除范围,include和exclude两者选一个就行
// exclude: path.resolve(__dirname, 'node_modules')
},
{
test: /\.ts$/,
include: path.resolve(__dirname, 'src'), // 明确范围
loader: 'happypack/loader?id=ts',
}
]
},
plugins: [
// ...省略其他代码
// happyPack 开启多线程打包
new HappyPack({
// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
id: 'babel123',
// 如何处理 .js 文件,用法和 Loader 配置中一样
use: ['babel-loader?cacheDirectory']
// 这里写成loaders: ['babel-loader?cacheDirectory']也可以
// 这里必须用数组形式
}),
new HappyPack({
id: 'ts',
use: [{
loader: 'ts-loader',
options: {}, // ts options
}],
})
],如果你的happyPack的id对应不上就会报如下错误
AssertionError [ERR_ASSERTION]: HappyPack: plugin for the loader 'babel123' could not be found! Did you forget to add it to the plugin list?...
在使用多个HappyPack loader的同时也就意味着要插入多个HappyPack的插件,每个插件加上id来作为标识。同时我们也可以为每个插件设置具体不同的配置项,如使用的线程数、是否开启debug模式等。
现在的webpack内置Uglify工具压缩js,只要你是生产环境就会自动压缩js(当然你webpack版本太旧是不能自动在生产环境压缩的),因为JS是单线程的,开启多线程会压缩的更快。
1.安装webpack-parallel-uglify-plugin
2.引入const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')
plugins: [
// ...省略部分无关代码
new ParallelUglifyPlugin({
// 传递给 UglifyJS 的参数
// (还是使用 UglifyJS 压缩,只不过帮助开启了多进程)
uglifyJS: {
output: {
beautify: false, // 最紧凑的输出
comments: false, // 删除所有的注释
},
compress: {
// 删除所有的 `console` 语句,可以兼容ie浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
}
}
})
],热更新:新代码生效,网页不刷新,状态不丢失
自动网页刷新状态会丢失
自动刷新会用到devServer
1.引入const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');
2.在plugins加入配置
plugins: [
// ...省略其他无关代码
new HotModuleReplacementPlugin()
],3.在devServer加入hot: true
devServer: {
port: 8080,
progress: true, // 显示打包的进度条
contentBase: distPath, // 根目录
open: true, // 自动打开浏览器
compress: true, // 启动 gzip 压缩
hot: true, // ======在这里加入热更新配置=============
// 设置代理
proxy: {
// 将本地 /api/xxx 代理转发到 http://localhost:3000/api/xxx
'/api': 'http://localhost:3000',
// 将本地 /api2/xxx 代理转发到 http://localhost:3000/xxx
'/api2': {
target: 'http://localhost:3000',
pathRewrite: {
'/api2': '' // 将路径中的'/api2'变为''空串
}
}
}
},举例子:
这里开启devServer,如果不是热更新,我们修改代码会自动刷新整个网页。如果每次刷新都会有网络请求,增加了后台负担;如果填写都表单有数据,网页刷新表单数据会丢失;如果你进了路由都子路由的子路由,层级比较深,而刷新后又回到了根路由…
开启热更新之后,需要热更新部分加上监听
// 增加,开启热更新之后的代码逻辑
if (module.hot) {
module.hot.accept(['./math.js'], () => {
const sumRes = sum(10, 30)
console.log('sumRes in hot', sumRes)
})
}那么你只要修改了math.js里面的代码,就只会热更新,执行这里module.hot.accept的第二个参数----回调函数中的内容。
并且这里不会清空你在Console中定义的变量值,不会清空你在input框里面的值,因为它并不会刷新整个网页,仅仅只是针对math.js里面的东西作出响应。
module: {
rules: [
// ...省略无关代码
// 图片 - 考虑 base64 编码的情况
{
test: /\.(png|jpg|jpeg|gif)$/,
use: {
loader: 'url-loader',
options: {
// 小于 5kb 的图片用 base64 格式产出
// 否则,依然延用 file-loader 的形式,产出 url 格式
limit: 5 * 1024,
// 打包到 img 目录下
outputPath: '/img1/',
// 设置图片的 cdn 地址(也可以统一在外面的 output 中设置,那将作用于所有静态资源)
// publicPath: 'http://cdn.abc.com'
}
}
},
]
},这个例子,小于5kb以base64产出,url-loader处理,打包到了对应js,这样就不会单独打包成图片,减少网络请求的耗时。
太大对图片就单独打包成图片,避免js文件过大,下载太耗时导致页面渲染卡住。
output: {
// filename: 'bundle.[contentHash:8].js', // 打包代码时,加上 hash 戳
filename: '[name].[contentHash:8].js', // name 即多入口时 entry 的 key
path: path.join(__dirname, '..', 'dist'),
// publicPath: 'http://cdn.abc.com' // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
},加上contentHash是因为只要文件js内容不变,这个contentHash值就不会变,这样上线之后用户发起请求可以命中缓存,直接取本地缓存,当内容变化之后contentHash变化,缓存失效,再发起请求拉去新的文件。
为什么不用[hash]而是[contentHash],因为webpack每次打包都会有一个hash,而且每次不一样,这样每次还是回去请求新的文件,没有利用到缓存,失去了意义。
后面对:8是取contentHash值的前8位。
CSS操作也是一样,css-loader是将css文件变成commonjs模块加载js中,里面内容是样式字符串,这样CSS文件就放在了打包后的JS文件中,当多个JS引入相同的CSS的时候,如果这样操作,每个打包出来的CSS文件都放在不同的JS文件中,而这些CSS又是重复的样式,所以需要把CSS提取出来减小JS体积,我们一般会对CSS文件命名,这里也是加上了[contentHash:8]
plugins: [
// ...省略无关代码
// 抽离 css 文件
new MiniCssExtractPlugin({
filename: 'css/main.[contentHash:8].css'
})
],比较大的文件用懒加载(异步加载)
document.getElementById('btn').onclick = function() {
// 懒加载~:当文件需要使用时才加载~
// 预加载 prefetch:会在使用之前,提前加载js文件
// 正常加载可以认为是并行加载(同一时间加载多个文件)
// 预加载 prefetch:等其他资源加载完毕,浏览器空闲了,再偷偷加载资源
import(/* webpackChunkName: 'test', webpackPrefetch: true */'./test').then(({ mul }) => {
console.log(mul(4, 5));
});
};为什么选择懒加载呢?
这样可以提高代码覆盖率。也就是一个js里面的代码使用率提高。我们可以在MAC电脑使用快捷键command+shift+P,输入coverage–>选择Show Coverage,然后如下图所示点击

如果不使用懒加载,你的代码属于Unused Bytes,使用了之后,你的代码是是属于Used Bytes,我们的目的就是提高Used Bytes,这样就提高了代码覆盖率。优化首先考虑代码覆盖率再才会考虑缓存。
这里写了/* webpackChunkName: 'test', webpackPrefetch: true */
表示这里的回调函数的内容会打包到chunkName为test到js中,默认entry我们是单入口文件,比如
entry: './src/js/index.js',实际上等同于
entry: {
main: './src/js/index.js' // 这个默认的main就是默认的webpackChunkName
}webpackChunkName是main,当我们把/* webpackChunkName: 'test' */之后就指定webpackChunkName是test,所以console.log(mul(4, 5));会打包到test.[contentHash:8].js中
当然,你的输出文件名仍然是可以在output修改的
output: {
filename: 'js/[name].[contenthash:10].js',
path: resolve(__dirname, 'build'),
chunkFilename: 'js/[name].[contenthash:10]_chunk.js' // 这个[name]是你/* webpackChunkName: 'xxx'*/指定的,打包出来就是js/xxx.[contentHash:10]_chunk.js
// 如果你不指定webpackChunkName,这里就会输出js/[id].[contentHash:10]_chunk.js,以从0开始的数字往后命名,看你webpack打包日志的chunks这一项是什么数字,这个[id]就会显示多少
},
这个就不多说了,不然篇幅太长。
这里还提到了/* webpackPrefetch: true */,懒加载是等用到的时候再去发起请求获取数据,而预加载是等网络带宽空闲时去加载,比如这里的test.[contentHash:8].js是现在不需要的,后面可能会用到,但是这里是在onclick监听事件里面才import(),属于宏任务,宏任务一定会在尝试一次DOM渲染之后才执行, 所以在这个例子中是渲染一次完成了再去加载,然后当你点击触发获取test.[contentHash:8].js的时候就不用再发起请求了,直接在本地加载,速度看起来更快。预加载目前在一些浏览器和移动端可能不支持。
举个例子:比如网页登录按钮点击之后弹出提示登录的操作,很显然我们需要懒加载这个登录界面,那么如果我点击按钮之后才去请求这个js(创建DOM结点操作显示界面),会不会有点慢,让人感觉会卡顿一下?那么这里的预加载就是很好的一种方案了。在网络带宽空闲的时候会去把这个预加载的js下载下来,再次加载的时候之后从缓存请求这个js,速度就非常快了。
有人可能会问了,这里在onlick事件里面,我没去点击按钮,没触发这个回调你怎么知道我回调函数里面有个预加载或者懒加载?因为DOM事件是宏任务,在你的同步代码执行完=>微任务=>尝试DOM渲染=>宏任务,按照这样的执行顺序来的。如果你不了解JS异步,可以看看这里JS 异步进阶【想要进大厂,更多异步的问题等着你】
optimization: {
splitChunks: {
// initial 入口chunk,对于异步导入的文件不处理
// async 异步chunk,只对异步导入的文件处理
// all 全部chunk
chunks: 'all', // 默认是async
// 为什么默认是async呢?上面说过了,懒加载会提高代码覆盖率,而拆分同步代码只是利用缓存,优化十分有限,所以默认拆分懒加载的代码,为async!!!!
// 下面是默认值,可以不写~
minSize: 30 * 1024, // 分割的chunk最小为30kb
maxSize: 0, // 最大没有限制
minChunks: 1, // 要提取的chunk最少被引用1次
maxAsyncRequests: 5, // 按需加载时并行加载的文件的最大数量
maxInitialRequests: 3, // 入口js文件最大并行请求数量
automaticNameDelimiter: '~', // 名称连接符
name: true, // 可以使用命名规则
// === 以上为公共规则 ==========
cacheGroups: {
// 分割chunk的组的规则
// node_modules文件会被打包到 vendors 组的chunk中。--> vendors~xxx.js,这个~是名称链接符
// 满足上面的公共规则,如:大小超过30kb,至少被引用一次。比如vue、vue-router等等
vendors: {
test: /[\\/]node_modules[\\/]/,
// 优先级
priority: -10
},
default: {
// 要提取的chunk最少被引用2次
minChunks: 2,
// 优先级
priority: -20,
// 如果当前要打包的模块,和之前已经被提取的模块是同一个,就会复用,而不是重新打包模块
reuseExistingChunk: true
}
}
},
// 将当前模块的记录其他模块的hash单独打包为一个文件 runtime
// 解决:修改a文件导致b文件的contenthash变化,做代码分割一定要加上runtimeChunk,否则导致缓存失效
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}`
},
minimizer: [
// 配置生产环境的压缩方案:js和css,4.26以上的webpack压缩js使用terser-webpack-plugin
// 压缩js
new TerserWebpackPlugin({
// 开启缓存
cache: true,
// 开启多进程打包
parallel: true,
// 启动source-map
sourceMap: true
}),
// 压缩css
new OptimizeCSSAssetsPlugin({})
]
}// output配置
entry: path.join(__dirname, 'src/index.js'),
output: {
filename: 'test.js',
path: path.resolve(__dirname, '../dist'),
chunkFilename: '[name]_chunk.js'
},optimization.splitChunks就是默认配置,和上面一样。
optimization: {
splitChunks: {
chunks: 'all' // 这里要写,不然就是只分割异步代码
// 后面不写,都是默认配置
}
}代码中自定义webpackChunkName为vConsole
import _ from 'lodash'// 这个在node_modules里面,分割时属于vendors组的规则
import myjs from './lib/myjs' // 这个在我新建的lib文件夹下的目录
// promise方式
if (process.env.NODE_ENV === 'development') {
let VConsole;
import(/* webpackChunkName: 'vConsole' */'vconsole/dist/vconsole.min.js').then((module) => {
VConsole = module.default;
new VConsole();
});
}
// 或者async/await方式
async created() {
if (process.env.NODE_ENV === 'development') {
try {
const { default: VConsole } =
await import(/* webpackChunkName: 'vConsole' */ 'vconsole/dist/vconsole.min.js');
new VConsole();
} catch (err) {
console.log(err);
}
}
},打包出来的vendors~vConsole_chunk.js、main_chunk.js、vendors~main_chunk.js
vendors~vConsole_chunk.js的文件说明
vendors是缓存组cacheGroups的组名字,~是默认的automaticNameDelimiter名称链接符,vConsole _chunk.js就是output中的chunkFilename规则[name]_chunk.js,这里[name]就是由于魔法注释(magic comment)/* webpackChunkName: 'vConsole' */变成vConosle。这里如果打生产包是不会把vconsole打进去的,因为process.env.NODE_ENV === 'development'为false
main_chunk.js的文件说明
入口文件默认
chunk名为main,这里面有lodash的映射关系,但是lodash库的js不在这。 不过myjs在这里,所以引入的js或者库文件只要不是node_modules目录下的js都会打包在这里
vendors~main_chunk.js的文件说明
默认
cacheGroups里面的test: /[\\/]node_modules[\\/]/, 所以node_modules里面的文件都会满足这个拆分规则,[name]为vendors,所以node_modules里的lodash包拆分到这里来了,~是默认的automaticNameDelimiter名称链接符,因为入口文件默认chunkName为main,chunkFilename规则[name]_chunk.js,连起来就是main_chunk.js
默认配置的一些属性说明:
chunks: 'all', // 默认是async
为什么默认是async呢?上面说过了,懒加载会提高代码覆盖率,而拆分同步代码只是利用缓存,优化十分有限,所以默认拆分懒加载的代码,为async!!!!
注意:
terser-webpack-plugin插件压缩js,而不是uglifyjs-webpack-plugin,在webpack4.26+就用terser-webpack-plugin去压缩js,因为uglifyjs-webpack-plugin不再维护了。
缓存组cacheGroups里面default组里有一个reuseExistingChunk: true,解释一下,比如文件c.js里引入a.js和b.js,而a.js里面又引入里b.js,打包的时候设置reuseExistingChunk: true,则会忽略第二次引入b.js,这样就避免了重复引入b.js
从webpack 5开始就不支持{cacheGroup}.name了,即
optimization: {
splitChunks: {
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/]/,
- name: 'vendors', // 这里不支持
chunks: 'all'
}
}
}
}这里块名称是commons,那么分割出的包名就是commons.js,name命名无效,默认就是那个组名称。
这里为什么写/[\\/]node_modules[\\/]/而不是/node_modules/?
webpack在处理文件路径时,默认在Unix是/,在Windows是\,[\\/]避免在跨平台使用时出现问题
分割chunk组规则里的优先级priority有什么用?
当满足公共规则的时候,比如提取出引入的第三方jquery,既满足vendors组的规则(因为在node_modules路径下),也满足default组的规则的时候,谁的优先级高就匹配对应组的规则,这里-10 > -20,所以打包出来的[name]是vendors而不是default
我们建议webpack都升级到4.0以上,如果你还是webpack 4.0以下,那就不得不说一下runtimeChunk,这是为了防止修改a文件导致b文件的contenthash变化,做代码分割一定要加上runtimeChunk,否则可能导致缓存失效。
我们先看现象,再讲解原因
// a.js
export function add(x, y) {
return x + y;
}// main.js
import(/* webpackChunkName: 'a' */'./a.js').then(({ add }) => {
console.log(add(1, 2));
});举个例子,还是上面的默认分割规则,没有配置runtimeChunk的时候,打包出来如下

在main.[contentHash:10].js中存在映射关系,包含了a.[contentHash:10].js文件映射,在html文件的<script>标签只会引入main.e9bc442f61.js文件,而main.e9bc442f61.js会动态创建<script>标签并引入a.ad13327f26_chunk.js。

如果我修改a.js文件的内容,打包后a.js的contentHash会变化,因为映射关系要对应,从而会导致main.js的contentHash会变化,那么客户端根据缓存发现哈希值不一致,会重新下载。所以我们需要把映射关系从main.[contentHash:10].js提取出来,加上runtimeChunk配置之后,打包如下

打包之后 html文件中,<script>只引入main.[contentHash:10]_chunk.js和runtime-main.[contentHash:10].js,映射关系跑到了runtime-main.[contentHash:10].js里面去了,而打开runtime-main.[contentHash:10].js会发现是管理着映射关系,会动态创建<script>标签然后引入a.[contentHash:10]_chunk.js。
所以再次修改a.js,就只是runtime-main.[contentHash:10].js和a.[contentHash:10]_chunk.js去变化,main.[contentHash:10]_chunk.js就不会改变。这里main也能变成chunk块了,匹配output.chunkFilename:[name]_chunk.js规则。这样客户端只会拉取下载runtime-main.[contentHash:10].js,main.[contentHash:10]_chunk.js利用缓存还可以继续使用。
实际上,平时写代码的时候,main.[contenthash:10].js是业务逻辑,vendors.[contenthash:10].js放的是库文件,业务逻辑和库文件的映射关系代码叫做manifest,默认manifest存在于main.[contenthash:10].js中,也存在于vendors.[contenthash:10].js里面,manifest在旧版webpack中打包可能会有差异,正是这种差异导致在旧版中哪怕内容没改变,contenthash值也会发生改变,原因在于包之间的关系或者js之间的关系嵌套在main和vendors文件里面,打包的时候会发生变化。我们把manifest里关联关系的代码抽离出来放在runtime文件里去。这样的话,main里面就是业务相关的代码,vendors就是库文件代码,关联关系代码放在runtime文件,这样打包后main文件的contenthash和vendors文件的contenthash都不会变,这样新旧版本都实现了每次打包只要内容不变,contenthash就不改变的情况。
在项目中可能有几处体积占用较大的库,其中一个便是moment.js这个日期处理库。对于一个日期处理的功能,为何这个库会占用如此大的体积,仔细查看发现当引用这个库的时候,所有的locale文件都被引入,而这些文件甚至在整个库的体积中占了大部分,因此当webpack打包时移除这部分内容会让打包文件的体积有所减小。
webpack自带的两个库可以实现这个功能:
IgnorePlugin
ContextReplacementPluginIgnorePlugin的使用方法如下:
// 插件配置
plugins: [
// 忽略moment.js中所有的locale文件
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
],
// 使用方式
const moment = require('moment');
// 引入zh-cn locale文件
require('moment/locale/zh-cn');
moment.locale('zh-cn');复制代码ContextReplacementPlugin的使用方法如下:
// 插件配置
plugins: [
// 只加载locale zh-cn文件
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /zh-cn/),
],
// 使用方式
const moment = require('moment');
moment.locale('zh-cn');复制代码通过以上两种方式,moment.js的体积大致能缩减为原来的四分之一。
你要引入一个库,但是这个库的在线js比较慢,你可以放到CDN。
如果你最终是在线页面,你会把这些资源包上传到公司的CDN或者自己的CDN,你可以这么写
output: {
// filename: 'bundle.[contentHash:8].js', // 打包代码时,加上 hash 戳
filename: '[name].[contentHash:8].js', // name 即多入口时 entry 的 key
path: path.join(__dirname, '..', 'dist'),
publicPath: 'http://cdn.abc.com' // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
},这里的publicPath写为你公司的CDN或者自己的CDN,打包之后是这样的

如果不写,那么publicPath默认是相对路径,相对于根目录

如果你最终是会变成下载下来的本地包加载,那么就不用写在线CDN的URL了,直接写上publicPath: '/'或者publicPath: './',根据你的的资源最后打包出来的路径选择
这个publicPath也可以写在loader的options里面,比如写在url-loader里面,去解析图片,这样打包出来的东西大于指定范围limit的东西会变成file-loader处理输出,outputPath决定输出路径,而publicPath的可以改变在线CDN的前缀路径。
vue、react等会自动删掉调试代码(如开发环境的warning)Tree Shaking(1. 必须使用ES6模块化import引入 2. 开启production环境)说一下Tree Shaking摇树,如果是开发环境,如果JS中有很多函数,而我只import了一个函数,打包的时候会把所有的函数代码打包进去,而生产环境,就只会引入你用到的那个函数。
形象比喻:树上很多果子代表函数,你只要一个果子,生产环境就是就会把整个树上无用的果子摇掉,简称“摇树Tree Shaking”
为什么必须使用ES6模块化import引入才能Tree Shaking呢?
ES6 Module是静态引入,编译时引入Commonjs是动态引入,执行时引入ES6 Module才能静态分析,实现Tree Shaking
Commonjs执行的时候才知道哪个函数需要哪个不需要,Commonjs就不能实现编译的时候摇树commonjs可以加上条件判断去引入,因为动态执行的时候根据条件变化可以执行,而ES6 Module静态编译的时候无法确定条件,会直接报错告诉你Module parse failed: 'import' and 'export' may only appear at the top level只能出现在最外层,外层不能再加条件判断了。
const flag = true
if (flag) {
import test from './test
} // 会直接报错const flag = true
if (flag) {
require('./test')
} // 完全没问题创建函数作用域更少,体积更小,可读性更好,现在的webpack自动集成了这一功能
以前引入一个js,默认打包的时候就会产生一个新的作用域,当引入文件比较多的时候就产生了很多作用域,现在的webpack将这些代码优化在了一个作用域,减小了体积。