webpack 学习笔记之基础篇

596 阅读16分钟

本篇分为基础篇、进阶篇、原理篇和实战篇。让我们对webpack有一个比较全面的了解。本文的教程和例子主要针对webpack4

1. 基本介绍

1.1 为什么要使用构建工具

  • 转换ES6语法
  • 转换JSX
  • CSS前缀的自动补全以及CSS的预处理器
  • 代码压缩混淆
  • 图片压缩
  • ......

(补充点:less、sass这种预处理器,通过这种预处理器编写出来的CSS代码具备可编程性,代码便于维护,编写代码更加方便,效率更高。)

下图是浏览器对es6语法的支持情况:红色表示不支持

image.png

1.2 为什么选择webpack

前端构建工具演变之路:

image.png

image.png

从图中可以看出 webpack

  • 社区生态丰富
  • 配置灵活和插件化扩展
  • 官方更新迭代速度快。

1.3 配置文件名称及配置组成

默认的配置文件是 webpack.config.js

可以通过 webpack --config xxx 来指定特定的配置文件,比如

webpack --config webpack.dev.config.js // 在开发环境使用webpack.dev.config.js配置文件 
webpack --config webpack.prod.config.js // 在生产环境使用webpack.prod.config.js配置文件

webpack配置组成:

module.exports = {
    entry: './src/index.js', // 打包的入口文件
    output: './dist/main.js', // 打包的输出
    mode: 'production', // 环境,有 development 和 production
    module: {
            rules: [ // Loader配置
                    {
                            test: /\.txt$/,
                            use: 'raw-loader'
                    },
            ],
    },
    plugins: [ // 插件配置
            new HtmlWebpackPlugin({
                    template: 'index.html',
                    inject: true,
                    favicon: path.resolve('favicon.ico'),
                    minify: {
                            collapseWhitespace: true,
                    },
            }),
    ]
}

webpack4.0之后出的零配置webpack

默认设置了entryoutput,对于其他的mode、module、plugins没做设定。

module.exports = {
    entry: './src/index.js', // 指定默认的entry为 ./src/index.js
    output: './dist/main.js', // 指定默认的output为 ./dist/main.js
}

1.4 安装 webpack

因为 webpack的运行依赖于node.js环境,所以需要安装node.js。

1. 环境搭建:安装 Node.js 和 NPM

安装 Node.jsNPM

  • nvm install 版本号
  • 检查是否安装成功:node -v, npm -v

nvmnode版本管理工具,可惜目前只支持linuxOS系统,windows下可以下载 nvm windows来使用。

2. 安装webpackwebpack-cli

mkdir my - project // 新建一个文件夹
cd my - project // 进入到 my-project文件夹
npm init - y // 生成初始的package.json文件
npm install webpack webpack-cli --save-dev // 安装webpack和webpack-cli
npm install webpack@4.31.0 --save-dev // 安装指定版本的webpack
./node_modules/.bin/webpack -v; // 运行这个命令,查看安装的webpack的版本,webpack放在node_modules/.bin文件下面的

3. 运行一个简单的例子

接着上一步,再在根目录下创建一个 webpack.config.js 文件,其代码如下所示:

const path = require('path'); // path模块是nodejs的核心模块,用于操作文件路径。

module.exports = {
    mode: 'production',
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'), // __dirname表示当前目录
        filename: 'bundle.js'
    }
}

再新建 src/index.jssrc/helloWorld.js,代码如下:

// src/index.js
import { helloWorld } from './helloWorld'; 

document.write(helloWorld());
// src/helloWorld.js
export function helloWorld() {  return 'hello world'; }

执行webpack打包命令,

./node_modules/.bin/webpack // 打包后的文件放在 dist文件夹下

如果出现报错:[webpack-cli] TypeError: compiler.getInfrastructureLogger is not a function【webpack与webpack-cli不兼容】,那就移除当前安装的webpack版本,重新换一个版本安装,如:

npm uninstall webpack webpack-cli --save; // 首先将之前的安装的版本删除 
npm install webpack@4.16.5 webpack-cli@3.3.11 --save-dev; // 重新指定版本安装

如果出现的是以下信息,就表示打包好了:

image.png

然后,创建index.html, 引入 ./dist/bundle.js脚本文件, 代码如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello webpack</title>
</head>

<body>
    <script src='./dist/bundle.js' type='text/javascript'></script>
</body>

</html>

然后在浏览器中打开 index.html 页面。

在上面的例子中, 每次通过 ./node_modules/.bin/webpack 来执行打包太麻烦了,可以在package.json中的script来设置,因为 package.json 默认会读取到 node_modules/.bin目录下的配置的:

{
  "name""learn-webpack",
  "version""1.0.0",
  "description""",
  "main""index.js",
  "scripts": {
    "test""echo \"Error: no test specified\" && exit 1",
    "build""webpack"
  },
  "keywords": [],
  "author""",
  "license""ISC",
  "devDependencies": {
    "webpack""^4.39.3",
    "webpack-cli""^3.3.8"
  }
}

删除上次的dist文件夹: rm -rf dist, 然后执行 npm run build,打包成功。

4. 补充

1. path.joinpath.resolve的区别

path模块是nodejs的核心模块,用于操作文件路径。

path.join([path1][, path2][, ...])

path.join()方法可以连接任意多个路径字符串。要连接的多个路径可做为参数传入。path.join()方法在接路径的同时也会对路径进行规范化。例如:

var path = require('path'); 
//合法的字符串连接 
path.join('/foo', 'bar', 'baz/asdf', 'quux', '..') // 连接后 '/foo/bar/baz/asdf' 

//不合法的字符串将抛出异常 
path.join('foo', {}, 'bar')  // 抛出的异常 TypeError: Arguments to path.join must be strings'

// 如果连接后的路径字符串是一个长度为零的字符串,则返回 '.',表示当前工作目录。
path.join('foo', 'bar', '..', '..')   // 返回结果为'.'

对于以/开始的路径片段,path.join只是简单的将该路径片段进行拼接,而path.resolve将以/开始的路径片段作为根目录,在此之前的路径将会被丢弃,就像是在terminal中使用cd命令一样。

而且path.resolve()方法可以将多个路径解析为一个规范化的绝对路径。

path.resolve('/a', '/b') // '/b'
path.resolve('./a', './b') // '/Users/username/Projects/webpack-demo/a/b'

path.resolve('/foo/bar', './baz') // 输出结果为 '/foo/bar/baz' 
path.resolve('/foo/bar', '/tmp/file/') // 输出结果为 '/tmp/file' 

path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif') 
// 当前的工作路径是 /home/itbilu/node,则输出结果为 '/home/itbilu/node/wwwroot/static_files/gif/image.gif'

2. 为什么配置文件moduls.exports换成export default后运行就有问题?而在页面中使用export default则正常?

是因为 webpack 的这个配置没有经过 babel 转换。而这个项目的src内的代码经过了 babel 转换。

2. 基础篇

2.1 入口 entry

通过 entry 来指定打包的入口,因为 webpack 是模块打包器, 它会把代码或者图片、字体依赖等非代码都当做模块来处理。

入口起点(entry point)指示 webpack 应该使用哪个模块来作为构建其内部依赖图的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。

image.png

entry有单入口和多入口两种用法,代码演示如下:

// 单入口
entry: './path/to/my/entry/file.js' 

// 单入口其实是下面语法的简写 
entry: { 
    main: './path/to/my/entry/file.js' 
}

// 多入口:(多页应用, entry是一个object,object里面是以键值对形式存在的)
entry: { 
    pageOne: './src/pageOne/index.js', 
    pageTwo: './src/pageTwo/index.js', 
    pageThree: './src/pageThree/index.js' 
}

2.2 出口 output

output 是用来告诉 webpack 如何将编译后的文件输出到磁盘。

output 的用法:单入口配置 ,(单入口时,output的filename可以写死):

module.exports = {
    entry: './path/to/my/entry/file.js' 
    output: {
        filename: 'bundle.js', 
        path: __dirname + '/dist'
    }
};

output 的用法:多入口配置 (多入口时,outputfilename要使用占位符[name]来显示,这样打包之后,dist输出文件有2个,分别是app.js、search.js):

module.exports = { 
    entry: { 
    	app: './src/app.js', 
    	search: './src/search.js' 
    }, 
    output: { 
    	filename: '[name].js', 
    	path: __dirname + '/dist' 
    } 
};

2.3 loaders

  • webpack开箱即用只支持JSJSON 两种文件类型,通过loaders 去支持其它文件类型并且把它们转化成有效的模块,并且可以添加到依赖图中。
  • loaders本身是一个函数,接受源文件作为参数,返回转换的结果。

常见的loaders有哪些?如下表格所示:

名称描述
babel-loader转换ES6、ES7等JS新特性语法
css-loader支持.css文件的加载和解析
less-loader将less文件转换成css
ts-loader将TS转成JS
file-loader进行图片、字体等的打包
raw-loader将文件以字符串的形式导入
thread-loader多进程打包JS和CSS

loaders的用法:

test 指定匹配规则 ,use 指定使⽤用的 loader 名称

注意:loaders 调用是链式调用,其执行顺序是从右到左执行的。

const path = require('path');

module.exports = { 
    output: { 
    	filename: 'bundle.js' 
    }, 
    module: { 
        rules: [ { 
            test: /\.txt$/, 
            use: 'raw-loader',
            exclude: /node_modules/,
        }]
     } 
};

2.4 plugins

  • 用于打包后的bundle文件的优化,资源管理和环境变量注⼊,是webpack的增强功能,实现很多loaders实现不了的功能。
  • 作用于整个构建过程,从构建开始到构建结束都可以使用plugins

常见的plugins有:

名称描述
CommonsChunkPlugin通常用于多页面打包,将chunks中相同的js模块代码提取成公共的js
CleanWebpackPlugin清理构建目录
ExtractTextWebpackPlugin将CSS从bundle文件里提取成一个独立的CSS文件
CopyWebpackPlugin将文件或者文件夹拷贝到构建的输出目录
HtmlWebpackPlugin创建html文件去承载输出的bundle
UglifyisWebpackPlugin压缩JS
ZipWebpackPlugin将打包出的资源生成一个zip包

plugins的用法:

const path = require('path');

module.exports = { 
// 将plugins放到该数组里面
    plugins: [ 
    	new HtmlWebpackPlugin({template: './src/index.html'}),
     ] 
};

2.5 mode

mode ⽤用来指定当前的构建环境是:production、development 还是 none 。设置 mode 可以使用 webpack 内置的函数,默认值为 production

image.png

2.6 解析ECMAScript6React JSX

1. 解析ES6

image.png

image.png

步骤1:安装babel插件

npm i @babel/core @babel/preset-env babel-loader -D; // -D 等价于 --save-dev

步骤2: 创建 .babelrc 文件,代码如下:

{
  "presets": [
    "@babel/preset-env"
  ]
}

步骤3: 在webpack.config.js 文件中增加代码:

module: {
    rules: [
        {
            test: /.js$/,
            use: 'babel-loader'
        }
    ]
}

最后,在控制台运行 rm -rf distnpm run build 即可。

2. 解析 React JSX

image.png

步骤1: 安装 React 相关插件

npm i react react-dom @babel/preset-react -D

步骤2: 在 .babelrc 文件中增加 @babel/preset-react 配置,更新后的代码如下:

  {
    "presets": [
      "@babel/preset-env",
      "@babel/preset-react"
    ]
  }

然后写一个React组件进行测试:

import React from 'react';
import ReactDOM from 'react-dom';

class Index extends React.Component {
    render() {
        return (
            <div>hello world</div>
        )
    }
}

ReactDOM.render(
    <Index />,
    document.getElementById('app')
)

3. 解析 CSS、Less、Sass

image.png

  • css-loader:用于加载和解析.css文件
  • style-loader:将样式通过<style>标签插入到head中 步骤1: 安装css相关插件
npm i style-loader css-loader -D

步骤2: 修改 配置文件webpack.config.js,增加css-loader相关配置代码:

// 注意顺序,从右到左执行,先执行 css-loader ,将执行完的结果传给 style-loader 执行
module: {
    rules: [
        {
            test:/.css$/,
            use: [
                'style-loader',
                'css-loader'
            ]
        }
    ]
}

可以新建一个xxx.css文件进行css-loader的测试。

但是,在日常项目开发中,使用less更方便,所以,在安装css相关插件的基础上,再结合less-loader来处理css。 image.png

步骤1:安装 less 相关插件

npm i less less-loader -D

步骤2: 修改 配置文件webpack.config.js,增加less-loader相关配置代码:

module: {
    rules: [
        {
            test:/.less$/,
            use: [
                'style-loader',
                'css-loader',
                'less-loader'
            ]
        }
    ]
}

可以新建一个xxx.less文件进行less-loader的测试。

4. 解析图片和字体 file-loader 或 url-loader

image.png 步骤1:安装 file-loader 插件

npm i file-loader -D

步骤2: 修改 配置文件webpack.config.js,增加file-loader相关配置代码:

module: {
    rules: [
        {
            test:/.(png|jpg|gif|jpeg)$/,
            use: 'file-loader'
        }
    ]
}

image.png 步骤1:首先需要下载字体,放到项目代码文件里

// index.css文件
@font-face{ // 定义字体 
  font-family:'HansHandItalic'url('fonts/hanshand-webfont.ttf')format('truetype');
}

p {
    font-family: 'HansHandItalic';
}

步骤2:在css文件中定义该字体,并使用该字体

module: { 
    rules: [ 
        { 
            test: /\.(woff|woff2|eot|ttf|otf)$/, 
            use: 'file-loader'
        }
    ] 
}

除此之外,可以使用url-loader来处理图片和字体,url-loader内置了file-loader,并且还能设置较小资源进行base64的转化。

image.png

// 10240表示10K,当资源小于10K时,webpack打包时将这些资源自动转成base64
use: [
    { 
        loader: 'url-loader’, 
        options: { limit: 10240 } 
    }
] 

5. webpack中的文件监听

⽂件监听是在发现源码发⽣变化时,⾃动重新构建出新的输出⽂件。

webpack 开启监听模式,有两种⽅式:

  • 启动 webpack 命令时,带上 --watch 参数
  • 在配置 webpack.config.js 中设置 watch: true

那我们现在在package.json中设置快捷命令:

"scripts": {
    "watch": "webpack --watch"
  },

然后运行npm run watch即可监听文件,唯一缺陷是:每次需要手动刷新浏览器。

文件监听的原理分析:

  • 轮询判断⽂件的最后编辑时间是否变化
  • 某个⽂件发⽣了变化,并不会⽴刻告诉监听者,⽽是先缓存起来,等 aggregateTimeout
module.export = {
    //默认 false,也就是不开启
    watch: true,
    
    //只有开启监听模式时,watchOptions才有意义
    wathcOptions: {
        ignored: /node_modules/, //默认为空,不监听的文件或者文件夹,支持正则匹配
        aggregateTimeout: 300, //监听到变化发生后会等300ms再去执行,默认300ms
        poll: 1000, //判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次
    }
}

6. webpack中的热更新及原理分析

注意:生产环境不要使用热更新。

方式1: 使用 webpack-dev-server 结合 HotModuleReplacementPlugin 插件进行热更新

image.png

  • webpack-dev-server(WDS)的功能提供 bundle server的能力,就是生成的 bundle.js 文件可以通过 localhost://xxx 的方式去访问,另外 WDS 也提供 livereload(浏览器的自动刷新)。
  • hot-module-replacement-plugin 的作用是提供 HMRruntime,并且将 runtime 注入到 bundle.js 代码里面去。一旦磁盘里面的文件修改,那么 HMR server 会将有修改的 js module 信息发送给 HMR runtime,然后 HMR runtime 去局部更新页面的代码。因此这种方式可以不用刷新浏览器。
  • 单独写两个包也是出于功能的解耦来考虑的。
  • 简单来说就是:hot-module-replacement-plugin 包给 webpack-dev-server 提供了热更新的能力。

步骤1:在 package.json文件中添加运行命令:

// package.json
  "scripts": {
    "dev": "webpack-dev-server --open"; // --open 每次构建完之后,自动开启一个浏览器
  },

步骤2: 修改配置文件webpack.config.js,增加热更新相关配置代码:

const { webpack } = require('webpack');

plugins: [
    new webpack.HotModuleReplacementPlugin()
],
devServer: {
    contentBase: './dist',
    hot: true,
}

方式2: 使用 webpack-dev-middleware 进行热更新

这种方式适用于灵活的定制场景,通常需要结合node-server一起使用,一般是使用 express 或者 koa 来创建 node-server

image.png

热更新的原理分析

  • Webpack Compile: 将 JS 编译成 Bundle
  • HMR Server: 将热更新的⽂件输出给 HMR Rumtime
  • Bundle server: 提供⽂件在浏览器的访问,支持你通过 localhost:8080/xxx进行访问
  • HMR Rumtime: 会被注⼊到浏览器, 更新⽂件的变化。浏览器端的 bundle.js 和服务器端建立连接,通常这个连接是 websocket
  • bundle.js: 构建输出的⽂件

image.png 热更新有两个过程:

  • 过程1:启动阶段

将初始的代码经过 webpack 编译打包,将打包好的文件传输给 Bundle Server,Bundle Server其实就是一个服务器。

  • 过程2: 更新阶段

在本地开发时,若有文件发生变化,这个代码还是会经过 webpack 编译,编译好之后,会将代码发送给服务端的HMR ServerHMR Server就会知道哪些模块发生了改变,并通知客户端的HMR RuntimeHMR Runtime 就会更新我们的代码,并且不需要刷新浏览器。

7. 文件指纹策略:chunkhash、contenthashhash

文件指纹:是打包后输出的文件名的后缀。

image.png

文件指纹如何生成:

  • Hash:和整个项⽬的构建相关,只要项⽬⽂件有修改,整个项⽬构建的 hash 值就会更改
  • Chunkhash:和 webpack 打包的 chunk 有关,不同的 entry 会⽣成不同的 chunkhash 值
  • Contenthash:根据⽂件内容来定义 hash ,⽂件内容不变,则 contenthash 不变

JS的文件指纹设置:

image.png 其实,js、css 的文件指纹都可以设置为 contenthashcontenthash:8 指的是取 hash 值的前8位。

CSS的文件指纹设置:

image.png

注意:MiniCssExtractPluginstyle-loader 是冲突的,不能一起用。因为 MiniCssExtractPlugin 是将 css 提取成一个独立的文件。

首先,安装 MiniCssExtractPlugin 插件,npm i mini-css-extract-plugin -D;

其次,修改 webpack 配置文件,增加代码:

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module: {
    rules: [
        {
            test: /.css$/,
            use: [
                MiniCssExtractPlugin.loader,
                'css-loader'
            ]
        }
    ]
},
plugins: [
    new MiniCssExtractPlugin({
        filename:'[name]_[contenthash:8].css'
    })
],

JS的文件指纹设置: image.png

8. HTML、CSSJavaScript代码压缩

代码压缩分为:HTML 压缩、 CSS 压缩、 JS 压缩。

  • JS 文件的压缩: webapck4 内置了 uglifyjs-webpack-plugin 插件,默认打包出来的 JS 文件是压缩过了,我们不需要再做其他操作。当然,我们也可以手动安装 uglifyjs-webpack-plugin 插件,然后设置自己想要的参数对 JS 进行压缩。
  • CSS 文件的压缩: 使⽤ optimize-css-assets-webpack-plugin ,并同时依赖于 cssnano 插件。 image.png
  • HTML 文件的压缩: image.png

3. 进阶篇

3.1 自动清理构建目录产物

当前构建存在的问题:每次构建时不会清理目录,导致构建的输出目录 output 文件越来越多。 解决方法可以分为两种:

  • 方法1: 通过 npm scripts 清理构建⽬录
// package.json 文件
  "scripts": {
    "clean": "rimraf dist",
    "build": "npm run clean && webpack",
  },
  
  或者
  "scripts": {
    "clean": "rm -rf dist",
    "build": "npm run clean && webpack",
  },
  • 方法2: 使⽤ clean-webpack-plugin 插件自动清理构建目录
npm i clean-webpack-plugin -D
// webpack.config.js
const CleanWebpackPlugin = require("clean-webpack-plugin");

module.exports = {
    plugins: [
        new CleanWebpackPlugin(),
    ],
};

3.2 PostCSS 插件 autoprefixer 自动补全 CSS3 前缀

CSS前缀:由于浏览器众多,各大浏览器的标准不统一,因此需要兼容不同的浏览器。

使⽤ autoprefixer 插件,根据 Can I Use 规则( caniuse.com/ )。

image.png

3.3 移动端 CSS px 自动转成 rem

为了适配各种浏览器的分辨率,我们需要对CSS进行处理。

方法1 :CSS 媒体查询实现响应式布局

缺陷:需要写多套适配样式代码

@media screen and (max-width: 980px) {
    .header {
            width: 900px;
    }
}

@media screen and (max-width: 480px) {
    .header {
            height: 400px;
    }
}

@media screen and (max-width: 350px) {
    .header {
            height: 300px;
    }
}

方法2: 移动端 CSSpx 自动转成 rem

  • W3Crem 的定义: font-size of the root element
  • rempx 的对⽐: rem 是相对单位, px 是绝对单位
  • 使⽤ px2rem-loader插件: 页面渲染时计算根元素的 font-size

3.4 静态资源内联

资源内联(inline resource),就是将一个资源以内联的方式嵌入进另一个资源里面。

资源内联的意义:代码层面的意义 + 请求层面的意义。

  • 代码层面

1、初始化脚本或内联html

如果存在 es6 代码需要增加 babel-loader 进行转换

npm i raw-loader@0.5.1 -D

${require('...')} 读取出内容,然后进行插入

raw-loader 内联html ${require('raw-loader!./meta.html')}

raw-loader 内联js <script>$(require('raw-loader!babel-loader!./node_modules/lib-flexible'))</script>

2、上报点:js/css的加载中、加载完成

3、css内联避免页面闪动:

如:首屏的样式内联入 html 中,防止页面闪动

image.png

  • 请求层面:减少 HTTP 网络请求数

小图片或者字体内联进页面(使用 url-loader,设置 limit

了解更多,请参考:

  1. webpack4 中如何实现资源内联?
  2. webapck5 学习9 -- 静态资源文件内联

3.5 多页面应用打包通用解决方案

多⻚⾯应⽤(MPA)概念:每⼀次⻚⾯跳转的时候,后台服务器都会给返回⼀个新的 html ⽂档, 这种类型的⽹站也就是多⻚⽹站,也叫做多⻚应⽤。

多⻚⾯打包基本思路:每个⻚⾯对应⼀个 entry,⼀个 html-webpack-plugin
缺点:每次新增或删除⻚⾯需要改 webpack 配置。

image.png

多⻚⾯打包通⽤⽅案:利⽤ glob.sync 插件动态获取 entry 和设置 html-webpack-plugin 数量。

假设在我们的项目中,我们将多页面统一放入 src 文件夹内,比如 src/indexsrc/search 文件夹,各自的入口文件统一命名 index.js ,模板文件统一命名 index.html。那么,webpack 的配置可以这么写:

// webpack.config.js
const glob = require('glob');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const setMPA = () => {
    const entry = {}
    const htmlWebpackPlugins = []
    // 匹配 src/pages 内文件夹内有 index.js 的文件
    const entryFiles = glob.sync(path.join(__dirname, './src/pages/*/index.js'))
    
    entryFiles.map((entryFile) => {
        // 通过正则匹配出文件夹名,也就是页面名
        const matchRes = entryFile.match(/src\/pages\/(.*)\/index\.js$/)
        const pageName = matchRes && matchRes[1]
        
        entry[pageName] = path.resolve(__dirname, `./src/pages/${pageName}/index.js`)
        htmlWebpackPlugins.push(new HtmlWebpackPlugin({
            template: path.join(__dirname, `./src/pages/${pageName}/index.html`),
            filename: `${pageName}.html`,
            chunks: [pageName], // 需要的 chunks
            inject: true, // 打包后的 chunks 是否自动注入
            minify: {
                html5: true,
                collapseWhitespace: true,
                preserveLineBreaks: false,
                minifyCSS: true,
                minifyJS: true,
                removeComments: false
            }
        }))
    })
    return {
        entry,
        htmlWebpackPlugins
    }
}
const {entry, htmlWebpackPlugins} = setMPA()

module.exports = {
    entry,
    plugins: [].concat(htmlWebpackPlugins)
}

3.6 使用 sourcemap

  • 作⽤:通过 source map 定位到源代码;
  • 开发环境开启,线上环境关闭;
    线上排查问题的时候可以将 sourcemap 上传到错误监控系统

sourcemap 关键字:

  • eval: 使⽤ eval 包裹模块代码
  • source map: 产⽣ .map ⽂件
  • cheap: 不包含列信息
  • inline: 将.map作为 DataURI 嵌⼊,不单独⽣成.map⽂件
  • module: 包含 loadersourcemap

source map 类型:

image.png

配置代码如下:

// webpack.config.js
module.exports = { 
    entry: { ... }, 
    devtool: 'source-map' 
}

3.7 提取页面公共资源

方法1:基础库分离

image.png

方法2: 利用 SplitChunksPlugin 进行公共脚本分离
SplitChunksPluginWebpack4 内置的,替代 Webpack3CommonsChunkPlugin 插件。

module.exports = {
    optimization: {
        splitChunks: {
            chunks: 'async',
            minSize: 30000,
            maxSize: 0,
            minChunks: 1,
            maxAsyncRequests: 5,
            maxInitialRequests: 3,
            automaticNameDelimiter: '~',
            name: true,
            cacheGroups: {
                vendors: {
                    test: /[\\/]node_modules[\\/]/,
                    priority: -10
                }
            }
        }
    }
};

chunks 参数说明:

  • async: 异步引⼊的库进⾏分离(默认)
  • initial: 同步引⼊的库进⾏分离
  • all: 所有引⼊的库进⾏分离(推荐)

image.png 利⽤ SplitChunksPlugin 分离⻚⾯公共⽂件,如图:

image.png

参数说明: minChunks- 设置最⼩引⽤次数为2次,minSize- 分离的包体积的⼤⼩

module.exports = {
    //...
    optimization: {
        splitChunks: {
            // async:异步引入的库进行分离(默认),  initial: 同步引入的库进行分离, all:所有引入             // 的库进行分离(推荐)
            chunks: 'async',
            minSize: 30000, // 30K,抽离的公共包最小的大小,单位字节
            maxSize: 0, // 最大的大小
            minChunks: 1, // 资源使用的次数(在多个页面使用到), 大于1, 最小使用次数
            maxAsyncRequests: 5,  // 并发请求的数量
            maxInitialRequests: 3, // 入口文件做代码分割最多能分成3个js文件
            automaticNameDelimiter: '~', // 文件生成时的连接符
            automaticNameMaxLength: 30, // 自动自动命名最大长度
            name: true, //让cacheGroups里设置的名字有效
            cacheGroups: { //当打包同步代码时,上面的参数生效
                vendors: {
                    test: /[\\/]node_modules[\\/]/,  //检测引入的库是否在node_modlues目录下的
                    priority: -10, //值越大,优先级越高.模块先打包到优先级高的组里
                    filename: 'vendors.js'//把所有的库都打包到一个叫vendors.js的文件里
                },
                default: {
                    minChunks: 2, // 上面有
                    priority: -20,  // 上面有
                    reuseExistingChunk: true //如果一个模块已经被打包过了,那么再打包时就忽略这个模块
                }
            }
        }
    }
};

3.8 Tree Shaking 的使用和原理分析

Tree Shaking:摇树优化。

概念:一个模块可能有多个⽅法,只要其中的某个⽅法使⽤到了,则整个⽂件都会被打到 bundle ⾥⾯去,tree shaking 就是只把⽤到的⽅法打⼊ bundle ,没⽤到的⽅法会在 uglify 阶段被擦除掉。

使⽤webpack 默认⽀持,production mode的情况下默认开启,或者在 .babelrc ⾥设置 modules: false 即可 。

要求:必须是 ES6 的语法,CJS 的⽅式不⽀持。

CJScommon js的缩写,语法如下:

// importing
const doSomething = require('./doSomething.js')

// exporting
module.exports = function doSomething(n) {
    // do something
}

image.png

Tree-shaking 原理:

  • 利⽤ ES6 模块的特点:
  1. 只能作为模块顶层的语句出现
  2. import 的模块名只能是字符串常量
  3. import binding 是 immutable的
  • 代码擦除: uglify 阶段删除⽆⽤代码

3.9 Scope Hoisting 使用和原理分析

存在问题:构建后的代码存在⼤量闭包代码,会导致⼤量作⽤域包裹代码,导致体积增⼤(模块越多越明显),运⾏代码时创建的函数作⽤域变多,内存开销变⼤。

image.png

image.png

image.png

image.png scope hoisting 使⽤

  • webpack modeproduction 默认开启
  • 必须是 ES6 语法,CJS 不⽀持
  • 或者手动安装插件webpack.optimize.ModuleConcatenationPlugin来使用 scope hoisting

image.png

3.10 代码分割和动态 import

代码分割的意义

对于⼤的 Web 应⽤来讲,将所有的代码都放在⼀个⽂件中显然是不够有效的,特别是当你的某些代码块是在某些特殊的时候才会被使⽤到。webpack 有⼀个功能就是将你的代码库分割成 chunks(语块),当代码运⾏到需要它们的时候再进⾏加载。

适⽤的场景

  • 抽离相同代码到⼀个共享块
  • 脚本懒加载,使得初始下载的代码更小

懒加载 JS 脚本的两种方式

    1. CommonJS:require.ensure
    1. ES6:动态 import(⽬前还没有原⽣⽀持,需要 babel 转换)

如何使用动态 import?

  • 步骤1. 安装 babel 插件,npm install @babel/plugin-syntax-dynamic-import --save-dev
  • 步骤2. ES6 的动态 import 需要 babel 转换,在.babelrc文件中增加配置
{ 
    "plugins": ["@babel/plugin-syntax-dynamic-import"], 
    ... 
}

例子演示

// index.js
import React from 'react';
import ReactDOM from 'react-dom';

class Search extends React.Component {
    constructor() {
        super(...arguments);
        this.state = {
            Text: null
        };
    }

    // 点击之后进行懒加载
    loadComponent() {
        import('./text.js').then((Text) => {
            this.setState({
                Text: Text.default
            });
        });
    }

    render() {
        const { Text } = this.state;
        return <p onClick={ this.loadComponent.bind(this) }>点击这里才去引入 text.js 文件</p>;
    }
}

ReactDOM.render(
    <Search />,
    document.getElementById('root')
);

3.11 在 webpack 中使用 ESLint

ESLint 的必要性

2017年4⽉13⽇,腾讯⾼级⼯程师⼩明在做充值业务时,修改了苹果 iap ⽀付配 置,将 JSON 配置增加了重复的 key 。代码发布后,有⼩部分使⽤了 vivo ⼿ 机的⽤户反馈充值⻚⾯⽩屏,⽆法在 Now app 内进⾏充值。最后问题定位是: vivo ⼿机使⽤了系统⾃带的 webview ⽽没有使⽤ X5 内核,解析 JSON 时遇到 重复 key 报错,导致⻚⾯⽩屏。

行业里面优秀的 ESLint 规范实践

Airbnb: eslint-config-airbnb、 eslint-config-airbnb-base

腾讯:
·alloyteam团队 eslint-config-alloy(github.com/AlloyTeam/e…)
·ivweb 团队:eslint-config-ivweb(github.com/feflow/esli…)

制定团队的 ESLint 规范

  • 不重复造轮⼦,基于 eslint:recommend 配置并改进
  • 能够帮助发现代码错误的规则
  • 帮助保持团队的代码风格统⼀,⽽不是限制开发体验

ESLint 如何执行落地?

  • 和 CI/CD 系统集成
  • 和 webpack 集成

方案一:webpack 与 CI/CD 集成

image.png

本地开发阶段增加 precommit 钩⼦,安装 huskynpm install husky --save-dev; 增加 npm script,通过 lint-staged 增量检查修改的⽂件:

// package.json 文件
"scripts": {
    "precommit": "lint-staged"
},
"lint-staged": {
    "linters": {
        "*.{js,scss}": ["eslint --fix", "git add"]
    }
},

然后,安装 ESLint 相关插件,并在项目根目录下创建 .eslintrc.js,代码规则可以根据自身项目特点和 ESLint 官网规则进行配置:

// .eslintrc.js 文件
module.exports = {
    "parser": "babel-eslint",
    "extends": "airbnb",
    "env": {
        "browser": true,
        "node": true
    },
    "rules": {
        "indent": ["error", 4]
    }
};

方案二:webpack 与 ESLint 集成

使⽤ eslint-loader 插件,构建时检查 JS 规范

image.png

3.12 webpack 打包组件和基础库

webpack 除了可以⽤来打包应⽤,也可以⽤来打包 js 库。\

例子:要求实现⼀个⼤整数加法库的打包:

  • 需要打包压缩版和⾮压缩版本
  • ⽀持 AMD/CJS/ESM 模块引⼊

打包输出的库名称:

  • 未压缩版 large-number.js
  • 压缩版 large-number.min.js

⽀持的使⽤⽅式:

    1. ⽀持 ES module
import * as largeNumber from 'large-number'; 
// ... 
largeNumber.add('999', '1');
    1. ⽀持 CJS
const largeNumbers = require('large-number'); 
// ... 
largeNumber.add('999', '1');
    1. ⽀持 AMD
require(['large-number'], function (large-number) { 
// ... 
largeNumber.add('999', '1'); });
    1. 可以直接通过 script 引⼊
<!doctype html>
<html>
...
<script src="https://unpkg.com/large-number"></script>
<script>
    // ...
    // Global variable
    largeNumber.add('999', '1');
    // Property in the window object
    window. largeNumber.add('999', '1');
    // ...
</script>
</html>

如何将库暴露出去?

  • library: 指定库的全局变量
  • libraryTarget: ⽀持库引⼊的⽅式
module.exports = {
    mode: "production",
    entry: {
        "large-number": "./src/index.js",
        "large-number.min": "./src/index.js"
    },
    output: {
        filename: "[name].js",
        library: "largeNumber",
        libraryExport: "default",
        libraryTarget: "umd"
    }
};

如何指对 .min 压缩:

  • 通过 include 设置只压缩 min.js 结尾的⽂件
// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
    mode: "none",
    entry: {
        "large-number": "./src/index.js",
        "large-number.min": "./src/index.js"
    },
    output: {
        filename: "[name].js",
        library: "largeNumber",
        libraryTarget: "umd",
        libraryExport: "default"
    },
    optimization: {
        minimize: true,
        minimizer: [
        // 压缩插件,支持ES6语法,在 webpack4的production环境,会自动开启压缩
            new TerserPlugin({ 
                include: /\.min\.js$/,
            }),
        ],
    }
};

注意:如果你使用的是 webpack v5 或以上版本,你不需要安装这个插件。webpack v5 自带最新的 terser-webpack-plugin。如果使用 webpack v4,则必须安装 terser-webpack-plugin v4 的版本。

设置⼊⼝⽂件:

  • package.jsonmain 字段为 index.jsprepublish 钩子:
// package.json
{
......
  "description": "大整数加法打包",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "prepublish": "webpack" // 发布到 npm 上时,执行打包
  },
  ......
}

在项目根目录中新建一个 index.js 文件:

// index.js代码
if (process.env.NODE_ENV === "production") {
    module.exports = require("./dist/large-number.min.js");
} else {
    module.exports = require("./dist/large-number.js");
}

然后,执行 npm prepublish 发布到 npm 上面。

3.13 webpack 实现 SSR 打包

页面打开过程

  • 开始加载
  • HTML加载成功,开始加载数据
  • 数据加载成功,渲染成功开始,加载图片资源
  • 图片加载成功,页面可交互

服务端渲染(SSR)是什么?

渲染:HTML + CSS + JS + Data -> 渲染后的HTML

服务端:

  • 所有的模板等资源都存储在服务端
  • 内网机器拉取数据更快
  • 一个 HTML 返回所有数据

浏览器和服务器交互流程

image.png

客户端渲染 vs 服务端渲染

image.png 总结:服务端渲染 (SSR) 的核⼼是减少请求。

SSR 的优势

  • 减少白屏时间
  • 对于SEO友好

SSR 代码实现思路

服务端:

  • 使用 react-dom/serverrenderToString 方法将 React 组件渲染成字符串
  • 服务端路由返回对应的模板

客户端:

  • 打包出针对服务端的组件
// server/index.js
const express = require("express");
const { renderTostring } = require("react-dom/server");
const SSR = require("../dist/search-server") ;

server(process.env.PORT || 3000);

function server(port){
    const app = epress();
    app.use(express.static("dist"));
    app.get("/search", (reg, res) =>{
        console log('Server response template' , renderTostring(SSR));
        res. status(200).send(renderMarkup(renderTosting(SSR)));
    }
app.listen(port, () => {
    console.log('server is running on port:' + port);
}
}
function renderMarkup(html) {
return  
    `<!DOCTYPE html>
<html>
<head>
    <title>服务端渲染</title>
    <meta charset="utf-8">
</head>
<body>
<div id="app">$(html)</div>
    </body>
</html>` 
}

webpack SSR 打包存在的问题:

问题1. 浏览器的全局变量(Node.js 中没有 docment,window)

  • 组件适配:将不兼容的组件根据打包环境进行适配
  • 请求适配:将 fetch 或者 ajax 发送请求的写法改成 isomorphic-fetch 或者 axios

问题2: 样式问题(Node.js 无法解析 css)

  • 方案一:服务端打包通过 ignore-loader 忽略掉 CSS 的解析
  • 方案二:将 style-loader 替换成 ismorphic-style-loader

如何解决样式不显示的问题

  • 使用打包出来的浏览器端html为模板
  • 设置占位符,动态插入组件

image.png

首屏数据如何处理

  • 服务端获取数据
  • 替换占位符

image.png

3.14 优化构建时命令行的显示日志

问题:当前构建时的日志显示会展示一大堆(npm run build 或者 npm run dev),其实很多并不需要开发者关注。

统计信息 stats

image.png

如何优化命令⾏的构建⽇志

  • 方式1:使⽤ friendly-errors-webpack-plugin
    · success: 构建成功的⽇志提示
    · warning: 构建警告的⽇志提示
    · error: 构建报错的⽇志提示 npm i friendly-errors-webpack-plugin -D
// webpack.config.js  
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');

module.exports = {
    entry: {
        app: './src/app.js',
        search: './src/search.js'
    },
    output: {
        filename: '[name][chunkhash:8].js',
        path: __dirname + '/dist'
    },
    plugins: [
        new FriendlyErrorsWebpackPlugin()
    ],
 };
  • 方式2: stats 设置成 errors-only
// webpack.config.js  
module.exports = {
 ...
 stats: 'errors-only',
 ...
}

或者

// webpack.config.js  
module.exports = {
 ...
 devServer: {
  contentBase:'./dist',
  hot: true,
  stats: 'errors-only',
 }
 ...
}

3.15 构建异常和中断配置

如何判断构建是否成功?

  • 在 CI/CD 的 pipline 或者发布系统需要知道当前构建状态
  • 每次构建完成后输⼊ echo $? 获取错误码,(0: 表示没有错误)

构建异常和中断处理

  • webpack4 之前的版本构建失败不会抛出错误码 (error code)
  • Node.js 中的 process.exit 规范
    · 0 表示成功完成,回调函数中,err 为 null
    · ⾮ 0 表示执⾏失败,回调函数中,err 不为 null,err.code 就是传给 exit 的数字

如何主动捕获并处理构建错误?

compiler 在每次构建结束后会触发 done 这 个 hookprocess.exit 主动处理构建报错。

// webpack.config.js
module.exports = {
    entry: entry,
    output: {
        path: path.join(__dirname, 'dist'),
        filename: '[name]-server.js',
        libraryTarget: 'umd'
    },
    plugins: [
    ......
        function() {
        // webpack4的写法如下,如果是webpack3,写成this.plugin('done', (stats)......
            this.hooks.done.tap('done', (stats) => {
                if (stats.compilation.errors && stats.compilation.errors.length 
                && process.argv.indexOf('--watch') == -1) {
                    console.log('build error');
                    process.exit(1);
                }
            })
        }    
    ]
};

4. 编写可维护的 webpack 构建配置

4.1 构建配置包设计

构建配置抽离成 npm 包的意义

  • 通用性
    ·业务开发者无需关注构建配置
    ·统一团队构建脚本
  • 可维护性
    ·构建配置合理的拆分
    ·README 文档、ChangeLog 文档等
  • 质量
    ·冒烟测试、单元测试、测试覆盖率
    ·持续集成

构建配置管理的可选方案

  • 方案1:通过多个配置文件管理不同环境的构建,webpack --config 参数进行控制;
  • 方案2:将构建配置设计成一个库,比如:hjs-webpack、Neutrino、webpack-blocks
  • 方案3:抽成一个工具进行管理,比如:create-react-app, kyt, nwb
  • 方案4:将所有的配置放在一个文件,通过 --env 参数控制分支选择。

构建配置包设计

  • 方式1:通过多个配置文件管理不同环境的 webpack 配置
    ·基础配置:webpack.base.js
    ·开发环境:webpack.dev.js
    ·生产环境:webpack.prod.js
    ·SSR环境:webpack.ssr.js
    ……
  • 方式2:抽离成一个 npm 包统一管理
    ·规范:Git commit日志、README、ESLint 规范、Semver 规范
    ·质量:冒烟测试、单元测试、测试覆盖率和 CI

通过 webpack-merge 组合配置

合并配置:module.exports = merge(baseConfig, devConfig);

 merge = require("webpack-merge") 
... 
 merge(
... { a: [1], b: 5, c: 20 }, 
... { a: [2], b: 10, d: 421 } 
... ) 

结果:{ a: [ 1, 2 ], b: 10, c: 20, d: 421 }

4.2 功能模块设计和目录结构

功能模块设计

image.png

目录结构设计

  • lib 放置源代码
  • test 放置测试代码

image.png

4.3 使用 ESLint 规范构建脚本

  • 使用 eslint-config-airbnb-base
  • eslint --fix 可以自动处理空格
npm i eslint babel-eslint eslint-config-airbnb-base -D

新建 .eslintrc.js 文件,代码:

// .eslintrc.js
module.exports = {
    "parser": "babel-eslint",
    "extends": "airbnb",
    "env": {
        "browser": true,
        "node": true
    },
    "rules": {
        "indent": ["error", 4]
    }
};

package.json 文件中,增加快捷命令用来处理空格:

"scripts": {
    "eslint": "eslint ./lib --fix",
    ......
  },

4.4 冒烟测试介绍和实际运用

冒烟测试是指对提交测试的软件在进行详细深入的测试之前而进行的预测试,这种预测试的主要目的是暴露导致软件需重新发布的基本功能失效等严重问题。

冒烟测试执行

  • 构建是否成功
  • 每次构建完成 build 目录是否有内容输出
    ·是否有 JS、CSS 等静态资源文件
    ·是否有 HTML 文件

判断构建是否成功 , 判断基本功能是否正常

  • 在示例项目里面运行构建,看看是否有报错
  • 编写 mocha 测试用例
    ·是否有 JS、CSS 等静态资源文件
    ·是否有 HTML 文件
// test/smoke/index.js
const path = require('path');
const webpack = require('webpack');
const rimraf = require('rimraf');
const Mocha = require('mocha');

const mocha = new Mocha({
    timeout: '10000ms'
});

process.chdir(path.join(__dirname, 'template'));

rimraf('./dist', () => {
    const prodConfig = require('../../lib/webpack.prod.js');

    webpack(prodConfig, (err, stats) => {
        if (err) {
            console.error(err);
            process.exit(2);
        }
        console.log(stats.toString({
            colors: true,
            modules: false,
            children: false
        }));

        console.log('Webpack build success, begin run test.');

        mocha.addFile(path.join(__dirname, 'html-test.js'));
        mocha.addFile(path.join(__dirname, 'css-js-test.js'));
        mocha.run();
    });
});
// test/smoke/html-test.js
const glob = require('glob-all');

describe('Checking generated html files', () => {
    it('should generate html files', (done) => {
        const files = glob.sync([
            './dist/index.html',
            './dist/search.html'
        ]);

        if (files.length > 0) {
            done();
        } else {
            throw new Error('no html files generated');
        }
    });
});
// test/smoke/css-js-test.js
const glob = require('glob-all');

describe('Checking generated css js files', () => {
    it('should generate css js files', (done) => {
        const files = glob.sync([
            './dist/index_*.js',
            './dist/index_*.css',
            './dist/search_*.js',
            './dist/search_*.css',
        ]);

        if (files.length > 0) {
            done();
        } else {
            throw new Error('no css js files generated');
        }
    });
});

4.5 单元测试和测试覆盖率

单纯的测试框架:mocha和ava; 需要配合断言库:.chai .should.js .expect .better-assert。 集成框架,开箱即用: Jasmine 和 Jest 。

编写单元测试用例

技术选型:Mocha + Chai
测试代码:describe, it, except
测试命令:mocha add.test.js

// add.test.js 
const expect = require('chai').expect;
const add = require('../src/add'); 

describe('use expect: src/add.js' , ()=>{
    it('add(1, 2) === 3' , () =>{ 
        expect(add(1, 2).to.equal(3));
    }); 
});

单元测试接入

  1. 安装 mocha + chai npm i mocha chai -D
  2. 新建 test 目录,并增加 xxx.test.js 测试文件
  3. 在 package.json 中的 scripts 字段增加 test 命令
"scripts": { "test": "node_modules/mocha/bin/_mocha ” },
  1. 执行测试命令 npm run test \

4.6 持续集成和 Travis CI

持续集成的作用

优点:

  • 快速发现错误
  • 防止分支大幅偏离主干
    核心措施是,代码集成到主干之前,必须通 过自动化测试。只要有一个测试用例失败, 就不能集成。

Github 最流行的CI

image.png

接入 Travis CI

travis.yml 文件内容

  • install 安装项目依赖
  • script 运行测试用例

image.png

4.7 发布构建包到 npm 社区

首先去 npm网站 查询包的名字有没有被人使用了。

先注册npm,再登录npm :npm login

  • 添加用户: npm adduser

  • 升级版本
    升级补丁版本号:npm version patch
    升级小版本号:npm version minor
    升级大版本号:npm version major

  • 发布版本:npm publish

4.8 Git Commit 规范和 changelog 生成

良好的 Git commit 规范优势:

  • 加快 Code Review 的流程
  • 根据 Git Commit 的元数据生成 Changelog
  • 后续维护者可以知道 Feature 被修改的原因

技术方案

image.png

提交格式要求

image.png

本地开发阶段增加precommit 钩子

  • 安装 husky
  • npm install husky --save-dev
  • 通过 commitmsg 钩子校验信息
"scripts": {
  "commitmsg": "validate-commit-msg",
  "changelog": "conventional-changelog-pangular -i CHANGELOG.md-s-r0"
},
"devDependencies": {
  "validate-commit-msg": "^2.11.1",
  "conventional-changelog-cli": "^1.2.0",
  "husky": "^0.13.1"
}

如果开发时按照以上格式提交代码的话,每次发布版本的时候,运行 npm run changelog,会很方便地生成changelog。

image.png

4.9 语义化版本

开源项目版本信息案例

  • 软件的版本通常由三位组成,形如: X.Y.Z
  • 版本是严格递增的,此处是:16.2.0 - > 16.3.0 -> 16.3.1
  • 在发布重要版本时,可以发布alpha, rc 等先行版本
  • alpha和rc等修饰版本的关键字后面可 以带上次数和meta信息

image.png

遵守 semver 规范的优势

image.png 优势:

  • 避免出现循环依赖
  • 依赖冲突减少

语义化版本(Semantic Versioning)规范格式

  • 主版本号:当你做了不兼容的 API 修改,
  • 次版本号:当你做了向下兼容的功能性新增,
  • 修订号:当你做了向下兼容的问题修正。

先行版本号

先行版本号可以作为发布正式版之前的版本,格式是在修订版本号后面加上一个连接号(-),再加上一连串以点(.)分割的标识符,标识符可以由英文、数字和连接号([0-9A-Za-z-])组成。

  • alpha:是内部测试版,一般不向外部发布,会有很多 Bug。一般只有测试人员使用。
  • beta:也是测试版,这个阶段的版本会一直加入新的功能。在 Alpha 版之后推出
  • rc:Release Candidate) 系统平台上就是发行候选版本。RC 版不会再加入新的功能了,主要着重于除错。

5. webpack 构建速度和体积优化策略

5.1 初级分析:使用webpack 内置的stats

stats: 构建的统计信息

  • package.json 中使用 stats

image.png

  • Node.js 中使用

image.png

5.2 速度分析:使用 speed-measure-webpack-plugin

image.png 图中红色时间所示代表耗时比较长,需要重点关注。

速度分析插件作用

  • 分析整个打包总耗时
  • 每个插件和loader的耗时情况

speed-measure-webpack-plugin插件

5.3 webpack-bundle-analyzer 分析体积

image.png

可以分析哪些问题?

  • 依赖的第三方模块文件大小
  • 业务里面的组件代码大小

5.4 使用高版本的webpack 和Node.js

使用高版本,构建时间降低了。

使用 webpack4:优化原因

  • V8 带来的优化(for of 替代 forEach、Map 和 Set 替代 Object、includes 替代indexOf)
  • 默认使用更快的 md4 hash 算法
  • webpacks AST 可以直接从 loader 传递给 AST,减少解析时间
  • 使用字符串方法替代正则表达式

5.5 多进程/多实例构建:资源并行解析可选方案

可选方案:thread-loader 或 parallel-webpack 或 HappyPack。

  • 使用 HappyPack 解析资源 原理:每次 webpack 解析一个模块,HappyPack 会将它及它的依赖分配给 worker 线程中。

image.png

HappyPack 作者已经不再维护这个插件了,建议大家使用 thread-loader。

  • 使用thread-loader 解析资源

原理:每次 webpack 解析一个模块,threadloader 会将它及它的依赖分配给 worker 线程中。

image.png

5.6 多进程/多实例:并行压缩

方法一:使用 parallel-uglify-plugin 插件

image.png

方法二:uglifyjs-webpack-plugin 开启 parallel 参数

image.png

方法三:terser-webpack-plugin 开启 parallel 参数 (推荐使用这个插件)

image.png

5.7 分包

分包:设置 Externals

image.png

进一步分包:预编译资源模块

思路:将 react、react-dom、redux、react-redux 基础包和业务基础包打包成一个文件

方法:使用 DLLPlugin 进行分包,DllReferencePlugin 对 manifest.json 引用

使用 DLLPlugin 进行分包

image.png

使用 DllReferencePlugin 引用manifest.json

image.png

5.8 缓存:提升二次构建速度

缓存思路:

  • babel-loader 开启缓存
  • terser-webpack-plugin 开启缓存
  • 使用 cache-loader 或者 hard-source-webpack-plugin

5.9 缩小构建目标

目的:尽可能的少构建模块。

比如 babel-loader 不解析 node_modules

image.png

减少文件搜索范围

  • 优化 resolve.modules 配置(减少模块搜索层级)
  • 优化 resolve.mainFields 配置
  • 优化 resolve.extensions 配置
  • 合理使用 alias

image.png

5.10 tree shaking(摇树优化) 擦除无用的js和css

概念:1 个模块可能有多个方法,只要其中的某个方法使用到了,则整个文件都会被打到bundle 里面去,tree shaking 就是只把用到的方法打入bundle ,没用到的方法会在uglify 阶段被擦除掉。

使用:

  • webpack 默认支持,在 .babelrc 里设置 modules: false 即可
  • production mode的情况下默认开启

要求:必须是 ES6 的语法,CJS 的方式不支持

无用的 CSS 如何删除掉?

  • PurifyCSS: 遍历代码,识别已经用到的 CSS class
  • uncss: HTML 需要通过 jsdom 加载,所有的样式通过PostCSS解析,通过document.querySelector 来识别在 html 文件里面不存在的选择器

在 webpack 中如何使用PurifyCSS?

image.png

5.11 使用webpack 进行图片压缩

要求:基于 Node 库的 imagemin 或者 tinypng API

使用:配置 image-webpack-loader

image.png

Imagemin的优点分析

  • 有很多定制选项
  • 可以引入更多第三方优化插件,例如pngquant
  • 可以处理多种图片格式

Imagemin的压缩原理

pngquant: 是一款PNG压缩器,通过将图像转换为具有alpha通道(通常比24/32位PNG文件小60-80%)的更高效的8位PNG格式,可显著减小文件大小。

pngcrush:其主要目的是通过尝试不同的压缩级别和PNG过滤方法来降低PNGIDAT数据流的大小。

optipng:其设计灵感来自于pngcrush。optipng可将图像文件重新压缩为更小尺寸,而不会丢失任何信息。

tinypng:也是将24位png文件转化为更小有索引的8位图片,同时所有非必要的metadata也会被剥离掉。

5.12 使用动态 Polyfill 服务

image.png

image.png

Polyfill Service原理

识别 User Agent,下发不同的 Polyfill

image.png

如何使用动态Polyfill service

polyfill.io 官方提供的服务

image.png

体积优化策略总结

  • Scope Hoisting
  • Tree-shaking
  • 公共资源分离
  • 图片压缩
  • 动态 Polyfill

参考资料:

  1. 极客时间《玩转webpack》
  2. 课件和例子