跳到主要内容

webpack4.0进阶(一)

webpack4.0 进阶学习

Tree Shaking 按需打包文件

import { used } from './moment';

used();

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

image-20200525151309388

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

警告
1.Tree Shaking只在production模式下生效。
2.只支持ES Module 语法(import),不支持CommonJs

{% tabs 2 %}

"sideEffects": false,
或者
"sideEffects": [
"**/*.css",
"**/*.scss",
"./esnext/index.js",
"./esnext/configure.js"
],
意思是对这些文件不进行tree shaking处理
例如
import './common.css';
虽然我们没有使用common.css的一些东西,但是它起到了样式的作用的,如果不在sideEffect中设置的话,webpack是不会对它进行打包的。
optimization: {
usedExports: true,
}
// production模式是会自动配置好,可写可不写

{% endtabs%}

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

由于开发环境需要调试代码所以会引入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

webpack.common.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()
]
};

webpack.dev.js

const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');

const devConfig = {
mode: 'development', // 默认为production
devtool: 'cheap-module-eval-source-map',
devServer: {
contentBase: './dist',
open: true, // 自动打开浏览器
port: 3001, // 服务器端口号
hot: true // 开启HRM
}
};

module.exports = merge(commonConfig, devConfig);

webpack.prod.js

const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');

const prodConfig = {
mode: 'development', // 默认为production
devtool: 'cheap-module-source-map'
};

module.exports = merge(commonConfig, prodConfig);

npm script

"scripts": {
"dev": "webpack-dev-server --config webpack.dev.js",
"build": "webpack --config webpack.prod.js"
},

配置好后,开发环境使用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

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

{%note success, 代码分割更多配置%}

打包分析工具

首先要拿到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

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

index.js

import './style.css';

style.css

body {
background: #e65;
}

{% tabs 3 %}

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'
}
}
};
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');

const devConfig = {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
devServer: {
contentBase: './dist',
open: true, // 自动打开浏览器
port: 3001, // 服务器端口号
hot: true // 开启HRM
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
};

module.exports = merge(commonConfig, devConfig);
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

const prodConfig = {
mode: 'development',
devtool: 'cheap-module-source-map',
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
}
]
},
plugins: [new MiniCssExtractPlugin({})]
};

module.exports = merge(commonConfig, prodConfig);
"sideEffects": [
"*.css"
],

{% endtabs %}

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

配置 output 解决浏览器 cache 问题

浏览器是有缓存功能的,在我们第一次加载 main.js 后,浏览器会保有 main.js 的缓存,下次再加载时就直接从缓存获取了。但是当我们下次发布新版本时(修改了 main.js 文件),浏览器还是使用以前缓存的 main.js 内容,所以显示的内容并不是最新的。

处理方法

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

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

shimming

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

{% tabs 6 %}

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

createText();
// 这个第三方库中使用到了loadsh,但是并没有引入lodash
export function createText() {
document.getElementById('root').innerHTML = _.compact([0, 1, false, 2, '', 3]);
}

{% endtabs %}

npm run dev打包后浏览器报错

{%note error, 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 文件,减少 mian.js 体积。
  • 为 output 文件设置 hash 值,防止浏览器使用缓存。