Skip to main content

webpack4.0进阶(一)

webpack4.0进阶学习

Tree Shaking按需打包文件

import { used } from './moment';

used();

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

image-20200525151309388

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

caution
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值,防止浏览器使用缓存。