Skip to main content

生产构建

Tree Shaking 按需打包文件

Tree Shaking 用于打包时去除没有使用到的代码,能够优化打包产物体积大小。

import { used } from './moment';

used();

上面这个例子中我们只使用到了 moment 中的used,但是打包后连同notUsed一起被打包进了 main.js 文件中

image-20200525151309388

Tree Shaking 可以帮我们解决这个问题。

开启方式
optimization: {
usedExports: true,
}

// production模式是会自动配置好,可写可不写
warning
  • 开启后针对 副作用文件 需要单独声明避免被删除
  • 只支持 ES Module 语法(import),不支持 CommonJs
  • Tree Shaking 默认只在 production 模式下生效。
package.json
"sideEffects": [
"**/*.css",
"**/*.scss",
"./esnext/index.js",
"./esnext/configure.js"
],

意思是对这些文件不进行 tree shaking 处理 例如 import './common.css'; 虽然我们没有使用 common.css 的一些东西,但是它起到了样式的作用的,如果不在 sideEffect 中设置的话,webpack 是不会将它打包到产物中的。

或者 "sideEffects": false

开发环境和生产环境配置文件

由于开发环境需要调试代码所以会引入devServer之类的插件,那么这部分插件在生产环境中是不需要使用到的,我们可以对开发环境和生产环境分别设置不同的配置文件。

首先安装插件webpack-merge用来将拼接 common 配置

npm i webpack-merge -D

目录如下:

webpacktest
├── package.json
├── src
│ ├── index.html
│ ├── index.js
│ └── moment.js
├── webpack.common.js
├── webpack.dev.js
└── webpack.prod.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
mode: 'development', // 默认为 production
entry: {
main: './src/index.js' // 打包入口文件
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/, // 不对node_modules下的js文件处理
loader: 'babel-loader'
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
output: {
// 输出文件配置
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
},
optimization: {
usedExports: true
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new CleanWebpackPlugin()
]
};

配置好后,开发环境使用npm run dev进行打包,生产环境用npm run build进行打包。

代码分割

代码分割有利于性能的优化。何为代码分割:

有这么一个场景,我在index.js中使用到了一些公共代码库/工具库(lodash),index.js中的代码是依赖于 lodash 中的一些工具的。当我们打包时,lodash 也是被打包到了main.js文件中,并且一旦index.js中的业务代码改变了,连同 lodash 也要一同重新打包加载,但是我们一般是不会去改动 lodash 这类工具库的。于是我们可以借助代码分割来将业务代码和 lodash 进行分割,这样下次再修改业务代码时,我们就无需重新加载 lodash 的内容了。

先安装 lodash

npm i lodash -S

这里为了便于查看打包后的文件内容,我们加上一条 npm script"start": "webpack --config webpack.dev.js"(由于 devServer 不会生成打包内容)

index.js
import _ from 'lodash';

console.log(_.compact([0, 1, false, 2, '', 3]));

npm run start打包,发现打包后的 main.js 文件中包含 lodash 内容。

那如何实现代码分割呢,只需配置 webpack.common.js 文件

optimization: {
splitChunks: {
chunks: 'all';
}
}

再次打包,发现打包后的文件中多了一个vendor~main.js,webpack 自动将 lodash 内容打包进去了,而 main.js 文件中就没有了 lodash 的内容了。

dist
├── index.html
├── main.js
└── vendors~main.js

上面介绍的是同步代码分割,下面看一下异步代码分割index.js,可以实现懒加载

async function createElement() {
const { default: _ } = await import(/* webpackChunkName: "lodash" */ 'lodash');
const element = document.createElement('div');
element.innerHTML = _.compact([0, 1, false, 2, '', 3]);
return element;
}

document.addEventListener('click', () => {
createElement().then(element => {
document.body.appendChild(element);
});
});

/* webpackChunkName: "lodash" */设置打包后的文件名为vendors~lodash.js,打开浏览器可以看到只有点击页面时,才会引入vendors~lodash.js,实现了懒加载

打包后的目录

dist
├── index.html
├── main.js
├── vendors~lodash.js
└── vendors~main.js

当然可以通过其他配置来设置打包后的文件名称。

tip

代码分割更多配置

打包分析工具

我们在生产环境打包后需要对产物体积进行分析优化,这时候就可以使用 bundle-analysis 来分析。

首先要拿到status.json文件,具体获取方式只需配置 npm script 即可

"start": "webpack --profile --json > status.json --config webpack.dev.js",

打包后会生成status.json文件。

然后使用官网提供的一些工具就可以可视化分析打包结果了。

在写代码时,我们要尽可能的使用异步引入,这样可以提高代码的使用率,提升性能,减少加载不必要的代码。

查看代码使用率的方法,浏览器控制台按下 ctrl+shift+p,输入 show coverage

代码优化

现在有一个优化场景,我有一个登录按钮,当点击按钮后弹出登录框。这里的优化思路是,页面加载时只加载登录按钮的代码,当按钮代码加载完后。利用空闲时间去加载登录框的代码。这样既可以优化首屏加载速度,还可以解决因使用懒加载登录框(也就是点击按钮后再去加载)而带来的用户体验较差的问题。

具体代码:(只需要在 import 中加入/_ webpackPrefetch: true _/)

index.js

document.addEventListener('click', () => {
import(/* webpackPrefetch: true */ './loginModal.js').then(({ default: login }) => {
login();
});
});

loginModal.js

export default function () {
alert('loginModal');
}

css 文件处理

MiniCssExtractPlugin

这个插件会将 css 打包到一个 css 文件中,通过 link 链接到 html 中

This plugin should be used only on production builds without style-loader in the loaders chain, especially if you want to have HMR in development.

官方推荐不要在开发环境中使用,因为不支持 HMR,不利于提高开发效率。

npm install --save-dev mini-css-extract-plugin
import './style.css';
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
mode: 'development', // 默认为production
entry: {
main: './src/index.js' // 打包入口文件
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/, // 不对node_modules下的js文件处理
loader: 'babel-loader'
}
]
},
output: {
// 输出文件配置
filename: '[name].js',
chunkFilename: '[name].chunk.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new CleanWebpackPlugin()
],
optimization: {
usedExports: true,
splitChunks: {
chunks: 'all'
}
}
};

也可以使用**optimize-css-assets-webpack-plugin**来压缩 css 代码

配置 output 解决缓存问题

当我们为 js 文件设置缓存时,在我们第一次加载 main.js 后,浏览器会保有 main.js 的缓存,下次再加载时就直接从缓存获取了。但是当我们下次发布新版本时(修改了 main.js 文件),浏览器还是使用以前缓存的 main.js 内容,所以显示的内容并不是最新的,为了解决这个问题我们可以在每次打包时使用不同的文件名来消除缓存带来的影响。

设置 output 的 filename,添加[contenthash]占位符。

output: {
// 输出文件配置
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js',
},

或者自定义

const now = new Date();
const date = `${now.getFullYear()}${now.getMonth()}${now.getMonth()}${now.getDate()}`;

output: {
// 输出文件配置
filename: `[name]-${date}.js`,
chunkFilename: `[name]-${date}.js`,
},

shimming

看一个场景,假设 library.js 是一个比较老的第三方库。

import './style.css';
import { createText } from './library';

createText();

npm run dev打包后浏览器报错

warning

Uncaught ReferenceError: _ is not defined

如果library.js是我们自己写的库那还好说,直接自己手动引入 lodash 就可以了。但是由于是第三方库,源文件是在 node_module 中的,不利于修改,这个时候就可以用shimming来解决了。

配置 webpack.common.js

// 记得引入const webpack =require('webpack');
// 下面代码的意思:当遇到_时,会自动为我们添加下面代码
// import _ from 'lodash'
plugins: [
new webpack.ProvidePlugin({
_: 'lodash'
})
],

更多配置参考shimming

细粒度 shimming

试着在 index.js 中打印出this,发现this其实是指向模块本身。那如何把 this 指向window呢,这里要借助imports-loader

npm i imports-loader -D

配置好 loader

rules: [
{
test: /\.js$/,
exclude: /node_modules/, // 不对node_modules下的js文件处理
use: [
{
loader: 'babel-loader'
},
{
loader: 'imports-loader?this=>window'
}
]
}
];

总结

  • Tree Shaking 可以实现对 js 文件的按需打包,只在 production 下生效。
  • 为生产和开发环境分别创建不同配置文件方便管理扩展。
  • 利用代码分割实现懒加载(利用魔法注释)和预加载。
  • 利用 status.json 来分析打包过程。
  • 单独生成 css 文件,减少 main.js 体积。
  • 为 output 文件设置 hash 值,解决缓存问题。