Skip to main content

初探前端工程化--讲课版

v8 的编译流程

001.png

  1. 将 Javascript 代码解析为 ATS(抽象语法树)。
  2. 基于 AST, 解释器(interpreter )将 AST 转化为字节码(bytecode),这一步 js 引擎实际上已经在执行 js 代码了。
  3. 为了进一步的优化,优化编译器(optimizing compiler)将热点函数优化编译为机器指令(machine code)执行。
  4. 如果优化假设失败,优化编译器会将机器码回退到字节码。

不过最早的 V8 是没有字节码的,就是直接解释执行 AST:
002.png 基于AST的灵活性诞生了很多工具,例如babelwebpack等等。

接下来就简单了解一下babel、webpack,并在最后搭建一个 react 脚手架。

Babel是什么?

Babel 是一个 JavaScript 编译器
Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。下面列出的是 Babel 能为你做的事情:

  • 语法转换
  • 通过 Polyfill 方式在目标环境中添加缺失的特性 (通过引入第三方 polyfill 模块,例如 core-js)(preset env 会根据 targets 来引入插件,实现转换和 polyfill)
  • 源码转换(codemods)

利用Babel对源码的操作,可以用来静态分析。实战中经常会用到的有:自动国际化、自动生成文档、自动埋点、js解释器等。

Babel的编译过程

babel 是 source to source 的转换,整体编译流程分为三步:

  • parse:通过 parser 把源码转成抽象语法树(AST)
  • transform:遍历 AST,调用各种 transform 插件对 AST 进行增删改
  • generate:把转换后的 AST 打印成目标代码,并生成 sourcemap

parse

parse 阶段的目的是把源码字符串转换成机器能够理解的 AST,这个过程分为词法分析、语法分析。

比如 let name = 'lin'; 这样一段源码,我们要先把它分成一个个不能细分的单词(token),也就是 let, name, =, 'lin',这个过程是词法分析,按照单词的构成规则来拆分字符串成单词。

之后要把 token 进行递归的组装,生成 AST,这个过程是语法分析,按照不同的语法结构,来把一组单词组合成对象。

transform

transform 阶段是对 parse 生成的 AST 的处理,会进行 AST 的遍历,遍历的过程中处理到不同的 AST 节点会调用注册的相应的 visitor 函数,visitor 函数里可以对 AST 节点进行增删改,返回新的 AST(可以指定是否继续遍历新生成的 AST)。这样遍历完一遍 AST 之后就完成了对代码的修改。

generate

generate 阶段会把 AST 打印成目标代码字符串,并且会生成 sourcemap。不同的 AST 对应的不同结构的字符串。比如 IfStatement 就可以打印成 if(test) {} 格式的代码。这样从 AST 根节点进行递归打印,就可以生成目标代码的字符串。


sourcemap 记录了源码到目标代码的转换关系,通过它我们可以找到目标代码中每一个节点对应的源码位置

AST

babel 编译的第一步是把源码 parse 成抽象语法树 AST (Abstract Syntax Tree),后续对这个 AST 进行转换。(之所以叫抽象语法树是因为省略掉了源码中的分隔符、注释等内容)

AST 也是有标准的,JS parser 的 AST 大多是 estree 标准,从 SpiderMonkey 的 AST 标准扩展而来。babel 的整个编译流程都是围绕 AST 来的,这一节我们来学一下 AST。

熟悉了 AST,也就是知道转译器和 JS 引擎是怎么理解代码的,对深入掌握 Javascript 也有很大的好处。

常见的AST

AST 是对源码的抽象,字面量、标识符、表达式、语句、模块语法、class 语法都有各自的 AST。我们分别来了解一下:

Literal

是字面量的意思,比如 let name = 'lin'中,'lin'就是一个字符串字面量 StringLiteral,相应的还有数字字面量 NumericLiteral,布尔字面量 BooleanLiteral,字符串字面量 StringLiteral,正则表达式字面量 RegExpLiteral 等。

Identifier

Identifer 是标识符的意思,变量名、属性名、参数名等各种声明和引用的名字,都是Identifer。
比如 let name = 'lin' 里面的name

Statement

statement 是语句,它是可以独立执行的单位,比如 break、continue、debugger、return 或者 if 语句、while 语句、for 语句,还有声明语句,表达式语句等。我们写的每一条可以独立执行的代码都是语句。

Declaration

声明语句是一种特殊的语句,它执行的逻辑是在作用域内声明一个变量、函数、class、import、export 等。

Expression

expression 是表达式,特点是执行完以后有返回值,这是和语句 (statement) 的区别。
有的节点可能会是多种类型,identifier、super 有返回值,符合表达式的特点,所以也是 expression。

Program & Directive

program 是代表整个程序的节点,它有 body 属性代表程序体,存放 statement 数组,就是具体执行的语句的集合。还有 directives 属性,存放Directive 节点,比如"use strict" 这种指令会使用 Directive 节点表示。

File & Comment

babel 的 AST 最外层节点是 File,它有 program、comments、tokens 等属性,分别存放 Program 程序体、注释、token 等,是最外层节点。
注释分为块注释和行内注释,对应 CommentBlock 和 CommentLine 节点。

AST 可视化查看工具

当然,我们并不需要记什么内容对应什么 AST 节点,可以通过 axtexplorer.net 这个网站来直观的查看。

AST 的公共属性

每种 AST 都有自己的属性,但是它们也有一些公共属性:

  • type: AST 节点的类型
  • start、end、loc:start 和 end 代表该节点对应的源码字符串的开始和结束下标,不区分行列。而 loc 属性是一个对象,有 line 和 column 属性分别记录开始和结束行列号。
  • leadingComments、innerComments、trailingComments: 表示开始的注释、中间的注释、结尾的注释,因为每个 AST 节点中都可能存在注释,而且可能在开始、中间、结束这三种位置,通过这三个属性来记录和 Comment 的关联。
  • extra:记录一些额外的信息,用于处理一些特殊情况。比如 StringLiteral 修改 value 只是值的修改,而修改 extra.raw 则可以连同单双引号一起修改。

Babel API

@babel/parser

Babel parser(以前的 Babylon)是Babel 中使用的 JavaScript 解析器。

输出

Babel 解析器根据Babel AST 格式生成 AST 。

例子

require("@babel/parser").parse("code", {
// parse in strict mode and allow module declarations
sourceType: "module",

plugins: [
// enable jsx and flow syntax
"jsx",
"flow",
],
});

@babel/traverse

当您想要转换一个AST时,您必须递归地遍历树。
我们可以将它与 babel 解析器一起使用来遍历和更新节点:

import * as parser from "@babel/parser";
import traverse from "@babel/traverse";

const code = `function square(n) {
return n * n;
}`;

const ast = parser.parse(code);

traverse(ast, {
enter(path) {
if (path.isIdentifier({ name: "n" })) {
path.node.name = "x";
}
},
});

Visitors

当我们谈论“去”一个节点,我们实际上意味着我们正在访问它们。我们之所以使用visitor这个术语,是因为有访问者这个概念。
访问者是跨语言的 AST 遍历中使用的模式。简单地说,它们是一个对象,其中定义了用于接受树中特定节点类型的方法。这有点抽象,让我们来看一个例子。

const MyVisitor = {
Identifier() {
console.log("Called!");
}
};

// You can also create a visitor and add methods on it later
let visitor = {};
visitor.MemberExpression = function() {};
visitor.FunctionDeclaration = function() {}

注意:Identifier(){…}是Identifier:{enter(){…}}的缩写。

这是一个基本的访问者,在遍历过程中使用时,它将为树中的每个 Identifier 调用 Identifier ()方法。
因此,使用这段代码,Identifier ()方法将被每个 Identifier (包括平方)调用四次。

function square(n) {
return n * n;
}
path.traverse(MyVisitor);
Called!
Called!
Called!
Called!

让我们看看上面那个树怎么运行的

  • Enter FunctionDeclaration
    • Enter Identifier (id)
      • Hit dead end
    • Exit Identifier (id)
    • Enter Identifier (params[0])
      • Hit dead end
    • Exit Identifier (params[0])
    • Enter BlockStatement (body)
      • Enter ReturnStatement (body)
        • Enter BinaryExpression (argument)
          • Enter Identifier (left)
            • Hit dead end
          • Exit Identifier (left)
          • Enter Identifier (right)
            • Hit dead end
          • Exit Identifier (right)
        • Exit BinaryExpression (argument)
      • Exit ReturnStatement (body)
    • Exit BlockStatement (body)
  • Exit FunctionDeclaration

如果需要,还可以为多个访问者节点应用相同的函数,方法是将它们与方法名中的 | 作为一个字符串(比如 Identifier | MemberExpression)分开。
flow-comments插件中的用法示例

const MyVisitor = {
"ExportNamedDeclaration|Flow"(path) {}
};

还可以使用别名作为访问者节点(如 babel-types 中定义的那样)。
Function 是 FunctionDeclaration, FunctionExpression, ArrowFunctionExpression, ObjectMethod and ClassMethod的别名

const MyVisitor = {
Function(path) {}
};

@babel/generator

将 AST 转换为代码。

例子

import { parse } from "@babel/parser";
import generate from "@babel/generator";

const code = "class Example {}";
const ast = parse(code);

const output = generate(
ast,
{
/* options */
sourceMaps: true
},
code
);

@babel/code-frame

一般用于标出代码位置

例子

import { codeFrameColumns } from "@babel/code-frame";

const rawLines = `class Foo {
constructor()
}`;
const location = { start: { line: 2, column: 16 } };

const result = codeFrameColumns(rawLines, location, {
/* options */
});

console.log(result);
  1 | class Foo {
> 2 | constructor()
| ^
3 | }

更多库 可以到Babel官网进行查看。

用途

老生常谈的 ES6 -> ES5 、动态导入、特殊语法转换、taro编译 ... 就不说了

自动埋点

babel-auto-tracker

自动国际化

react-i18n-auto babel插件

自动生成API文档

other

linter 压缩混淆``类型检查``模块遍历器

webpack

webpack历史

构建工具的作用

主流浏览器 ES6 module 支持情况 image.png

  • 转换 ES6 语法
  • 转换 JSX 、VUE
  • CSS 预处理器 / 前缀补全
  • 压缩混淆
  • 图片压缩

webpack 配置文件

webpack 默认配置文件 webpack.config.js
通过webpack --config 指定配置文件 image.png

环境搭建

mkdir my-webpack-project
cd my-webpack-project
npm -y

npm i webpack webpack-cli --save-dev

运行

image.png

webpack基础

entry

entry 用来指定webpack打包入口

用法

单入口:entry 是一个字符串

moule.exports = {
entry:'./src/index.js'
}

多入口:entry 是个对象

moule.exports = {
entry:{
app:'./src/index.js',
admin:'./src/admin.js'
}
}

output

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

单入口配置

module.exports = {
entry: './src/index.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js',
}
}

多入口配置

通过占位符确保文件名的唯一

module.exports = {
entry: './src/index.js',
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js',
}
}

loader

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

常见的loader

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

loader的用法

module.exports = {
entry: './src/index.tsx',
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader'],
},
{ test: /\.(js|jsx)$/, loader: 'babel-loader', exclude: /node_modules/ },
{ test: /\.(ts|tsx)$/, loader: 'ts-loader', exclude: /node_modules/ },
],
}
}

exclude: /node_modules/ 排除 node_modules 的文件

webpack asset module

资源模块(asset module)是一种模块类型,它允许使用资源文件(字体,图标等)而无需配置额外 loader。
在 webpack 5 之前,通常使用:

资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:

  • asset/resource 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。
  • asset/inline 导出一个资源的 data URI。之前通过使用 url-loader 实现。
  • asset/source 导出资源的源代码。之前通过使用 raw-loader 实现。
  • asset 在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader,并且配置资源体积限制实现。

当在 webpack 5 中使用旧的 assets loader(如 file-loader/url-loader/raw-loader 等)和 asset 模块时,你可能想停止当前 asset 模块的处理,并再次启动处理,这可能会导致 asset 重复,你可以通过将 asset 模块的类型设置为 'javascript/auto' 来解决。

{
test: /.(woff|woff2|eot|ttf|otf)$/,
type:'asset/resource'
},
{
test: /\.(png|svg|jpg|gif)$/,
type:'asset',
parser: {
dataUrlCondition: {
maxSize: 10 * 1024 // 100kb
}
}
},

plugins

插件用于 bundle 文件的优化,资源管理和环境变量注入
作用于整个构建过程

名称描述
HtmlWebpackPlugin创建 html 标签 去承载输出的bundle
CleanWebpackPlugin清理构建目录
CommonsChunkPlugin将chunks相同的模块代码提取成公共js将块相同的模块代码提取成公共js
ExtractTextWebpackPlugin将css从 bunlde文件里提取成一个独立的css文件
uglifyjsWebpackPlugin压缩JS

用法

module.exports={
entry:"./src/index.js",
mode:'production',
output:{
filename:'bundle.js'
},
plugins:[
new HtmlWebpackPlugin()
]
}

mode

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

mode 的内置函数功能

development会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 development. 为模块和 chunk 启用有效的名。
production会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 production。为模块和 chunk 启用确定性的混淆名称,FlagDependencyUsagePlugin,FlagIncludedChunksPlugin,ModuleConcatenationPlugin,NoEmitOnErrorsPlugin 和 TerserPlugin 。
none不使用任何默认优化选项

如果没有设置,webpack 会给 mode 的默认值设置为 production

解析ES6

使⽤ babel-loader babel的配置⽂件是:.babelrc

module.exports = {
entry: './src/index.tsx',
output: {
path: path.join(__dirname, 'dist'),
filename: '[name]__[chunkhash].js',
},
module: {
rules: [
{ test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ }
],
}
}

增加ES6的babel preset配置

@babel/preset-env是一个智能预设,允许您使用最新的 JavaScript,而无需微观管理目标环境需要哪些语法转换(以及可选的浏览器 polyfill)。这既让你的工作更轻松,也让 JavaScript 包更小!

@babel/preset-env 利用一些很棒的开源项目,比如browserslistcompat-tableelectron-to-chromium. 我们利用这些数据源来维护我们支持的目标环境的哪个版本获得了对 JavaScript 语法或浏览器功能的支持的映射,以及这些语法和功能到 Babel 转换插件和 core-js polyfills 的映射

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

解析React JSX

新建 .babelrc

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

此预设(preset)始终包含以下插件:

如果开启了 development 参数,还将包含以下插件: Classic runtime adds:

解析 CSS

css-loader ⽤于加载 .css ⽂件,并且转换成 commonjs 对象 style-loader 将样式通过 <style> 标签插入到 head 中, loader 是从后到前执行的。

module.exports = {
entry: './src/index.tsx',
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
}
]
}
}
module.exports = {
entry: './src/index.tsx',
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader'],
}
]
}
}

解析图片

file-loader ⽤于处理⽂件

module.exports = {
entry: './src/index.tsx',
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js',
},
module: {
rules: [
{
test:/\.(png|svg|jpg|gif)$/,
use:['file-loader']
}
]
}
}

解析字体

file-loader 也可以处理字体

module.exports = {
entry: './src/index.tsx',
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js',
},
module: {
rules: [
{
test:/\.(png|svg|jpg|gif)$/,
use:['file-loader']
}
]
}
}

处理小资源

url-loader 可以处理图片 设置指定文件的最大大小 (以字节为单位) 自动转base64,作为 data url 内嵌
目的:小文件使用Data URL,减少请求次数。

url-loader 不可和 file-loader同时使用 url-loader具有 file-loader 的功能



module.exports = {
entry: './src/index.tsx',
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.(png|svg|jpg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10240
},
},
],
}
]
}
}

asset module

资源模块(asset module)是一种模块类型,它允许使用资源文件(字体,图标等)而无需配置额外 loader。
在 webpack 5 之前,通常使用:

资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:

  • asset/resource 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。
  • asset/inline 导出一个资源的 data URI。之前通过使用 url-loader 实现。
  • asset/source 导出资源的源代码。之前通过使用 raw-loader 实现。
  • asset 在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader,并且配置资源体积限制实现。

当在 webpack 5 中使用旧的 assets loader(如 file-loader/url-loader/raw-loader 等)和 asset 模块时,你可能想停止当前 asset 模块的处理,并再次启动处理,这可能会导致 asset 重复,你可以通过将 asset 模块的类型设置为 'javascript/auto' 来解决。

{
test: /.(woff|woff2|eot|ttf|otf)$/,
type: 'asset/resource',
},
{
test: /\.(png|svg|jpg|gif)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10 * 1024, // 100kb
},
},
},

文件监听

⽂件监听是在发现源码发⽣变化时,⾃动重新构建出新的输出⽂件。
webpack 开启监听模式,有两种⽅式:

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

缺点:每次需要手动刷新浏览器

原理分析

轮询判断⽂件的最后编辑时间是否变化 某个⽂件发⽣了变化,并不会⽴刻告诉监听者,⽽是先缓存起来,等 aggregateTimeout

 module.export = {
//默认 false,也就是不开启
watch: true,
//只有开启监听模式时,watchOptions才有意义
wathcOptions: {
//默认为空,不监听的文件或者文件夹,支持正则匹配
ignored: /node_modules/,
//监听到变化发生后会等300ms再去执行,默认300ms aggregateTimeout: 300,
//判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次 poll: 1000
}
}

HMR热更新

webpack-dev-server 可用于快速开发应用程序 , wds 不用手动刷新浏览器,不输出文件,而是放在内存中

npm i webpack-dev-server

devServer.hot

'only' boolean = true
启用 webpack 的 热模块替换 特性:
webpack.config.js

module.exports = {
//...
devServer: {
hot: true,
},
};
"scripts": {
"dev": "webpack-dev-server --config webpack.dev.js",
}

也可以使用通过 CLI 调用 webpack-dev-server

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

从 webpack-dev-server v4 开始,HMR 是默认启用的。它会自动应用 webpack.HotModuleReplacementPlugin,这是启用 HMR 所必需的。因此当 hot 设置为 true 或者通过 CLI 设置 --hot,你不需要在你的 webpack.config.js 添加该插件。查看 HMR concepts page 以获取更多信息。

原理分析

image.png 初次构建 1 -> 2 -> A -> B
热更新 1 -> 2 -> 3 -> 4

HMR Server 是服务端,用来将变化的 js 模块通过 websocket 的消息通知给浏览器端。

HMR Runtime是浏览器端,用于接受 HMR Server 传递的模块数据,浏览器端可以看到 .hot-update.json 的文件过来。

Runtime 可以理解为 js 运行环境

Webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。

HMR的核心就是客户端从服务端拉去更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 WDS 与浏览器之间维护了一个 Websocket,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该chunk的增量更新。
后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理,像react-hot-loader 和 vue-loader 都是借助这些 API 实现 HMR。

Webpack HMR 原理解析

文件指纹

打包文件后输出的文件名的后缀 image.png

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

js的文件指纹设置

设置 output 的 filename,用 chunkhash。

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

css的文件指纹设置

设置 MiniCssExtractPlugin 的 filename,使用 contenthash。

module.exports = {
entry: {
app: './scr/app.js',
search: './src/search.js'
},
output: {
filename: '[name][chunkhash:8].js',
path:__dirname + '/dist'
},
plugins:[
new MiniCssExtractPlugin({
filename: `[name][contenthash:8].css`
})
]
}

图片的文件指纹设置
设置file-loader的name,使用hash。
占位符名称及含义

  • ext 资源后缀名
  • name 文件名称
  • path 文件的相对路径
  • folder 文件所在的文件夹
  • contenthash 文件的内容hash,默认是md5生成
  • hash 文件内容的hash,默认是md5生成
  • emoji 一个随机的指代文件内容的emoj
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename:'bundle.js',
path:path.resolve(__dirname, 'dist')
},
module:{
rules:[{
test:/\.(png|svg|jpg|gif)$/,
use:[{
loader:'file-loader',
options:{
name:'img/[name][hash:8].[ext]'
}
}]
}]
}
}

代码压缩

  • HTML 压缩
  • CSS 压缩
  • JS 压缩

HTML 压缩

使用 HtmlMinimizerWebpackPlugin

Name类型默认描述
testString|RegExp|Array<String|RegExp>/\.html(\?.*)?$/i测试以匹配文件。
includeString|RegExp|Array<String|RegExp>undefined要包括的文件。
excludeString|RegExp|Array<String|RegExp>undefined要排除的文件。
parallelBoolean|Numbertrue使用多进程并行运行来提高构建速度。
minifyFunction|Array<Function>HtmlMinimizerPlugin.htmlMinifierTerser允许您覆盖默认的缩小功能。
minimizerOptionsObject|Array<Object>{ caseSensitive: true, collapseWhitespace: true, conservativeCollapse: true, keepClosingSlash: true, minifyCSS: true, minifyJS: true, removeComments: true, removeScriptTypeAttributes: true, removeStyleLinkTypeAttributes: true, }Html-minifier-terser选项
优化。
const HtmlWebpackPlugin = require("html-webpack-plugin");
const HtmlMinimizerPlugin = require("html-minimizer-webpack-plugin");

module.exports={
entry:"./src/index.js",
mode:'production',
output:{
filename:'bundle.js'
},
optimization: {
minimizer: [
new HtmlMinimizerPlugin(),
],
},
}

这将仅在生产环境开启 CSS 优化。
如果还想在开发环境下启用 CSS 优化,请将 optimization.minimize 设置为 true:
webpack.config.js

// [...] 
module.exports = {
optimization: {
// [...]
minimize: true,
},
};

CSS 压缩

使用 css-minimizer-webpack-plugin and mini-css-extract-plugin

这个插件使用 cssnano 优化和压缩 CSS。

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");

module.exports = {
module: {
rules: [
{
test: /.less$/,
use: [MiniCssExtractPlugin.loader, "css-loader", "less-loader"],
},
],
},
optimization: {
minimizer: [
// 在 webpack@5 中,你可以使用 `...` 语法来扩展现有的 minimizer(即 `terser-webpack-plugin`),将下一行取消注释
// `...`,
new CssMinimizerPlugin(),
],
},
plugins: [new MiniCssExtractPlugin()],
};

JS压缩

V5内置了
terser-webpack-plugin,如果需要特殊配置 仍然需要安装

const TerserPlugin = require("terser-webpack-plugin");

module.exports = {
optimization: {
minimize: true,
minimizer: [new TerserPlugin()],
},
};

webpack进阶

自动清理构建目录

image.png

自动补全前缀

image.png PostCSS 插件 autoprefixer 自动补全 css3 前缀

postcss-preset-env 包含 autoprefixer

npm install --save-dev postcss-loader postcss postcss-preset-env
  module: {
rules: [
{
test: /\.less$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
},
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [['postcss-preset-env']],
},
},
},
'less-loader',
],
},
],
},

多设备同步

px2rem-loader 和 lib-flexible 自动实现多设备同步

资源内联

代码层面:

  • 页面框架的初始化脚本
  • 上报相关打点
  • css内联避免页面闪动

请求方面:减少http网络请求数

  • 小图片或者字体内联

HTML 和 js 内联

<%= require('raw-loader!./meta.html').default %>

<script><%= require('raw-loader!babel-loader!../node_modules/amfe-flexible/index.min.js').default %></script>

CSS内联采用 style-loader

多页面打包

优化 SEO ,加快首屏渲染

'use strict';

const glob = require('glob');
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

....


const setMPA = () => {
const entry = {};
const htmlWebpackPlugins = [];
const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js'));

Object.keys(entryFiles)
.map((index) => {
const entryFile = entryFiles[index];
// '/Users/alan/my-project/src/index/index.js'
const match = entryFile.match(/src\/(.*)\/index\.js/);
const pageName = match && match[1];

entry[pageName] = entryFile;
htmlWebpackPlugins.push(
new HtmlWebpackPlugin({
template: path.join(__dirname, `src/${pageName}/index.html`),
filename: `${pageName}.html`,
chunks: [pageName],
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false
}
})
);
});

return {
entry,
htmlWebpackPlugins
}
}

const { entry, htmlWebpackPlugins } = setMPA();

module.exports = {
entry: entry,
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
mode: 'development',
module: ....
plugins: [
....
].concat(htmlWebpackPlugins),
....
};

source map

作用:通过 source map 定位到源代码
sourmap 科普文

devtool: 'source-map'
devtoolperformanceproductionqualitycomment
(none)build: fastest

rebuild: fastest | yes | bundle | Recommended choice for production builds with maximum performance. | | eval | build: fast

rebuild: fastest | no | generated | Recommended choice for development builds with maximum performance. | | eval-cheap-source-map | build: ok

rebuild: fast | no | transformed | Tradeoff choice for development builds. | | eval-source-map | build: slowest

rebuild: ok | no | original | Recommended choice for development builds with high quality SourceMaps. | | cheap-source-map | build: ok

rebuild: slow | no | transformed | | | source-map | build: slowest

rebuild: slowest | yes | original | Recommended choice for production builds with high quality SourceMaps. |

more : Devtool

基础库分离

讲 react 、react-dom 基础包分出不打入bundle,可以单出打出文件,也可引入cdn,
对于 echars 等大型包也可 通过 test 检测单出打出,做更细致的优化

splitChunks: {
chunks: 'all', // 匹配的块的类型:initial(初始块),async(按需加载的异步块),all(所有块)
automaticNameDelimiter: '-',
cacheGroups: {
// 项目第三方组件
vendor: {
name: 'vendors',
enforce: true, // ignore splitChunks.minSize, splitChunks.minChunks, splitChunks.maxAsyncRequests and splitChunks.maxInitialRequests
test: /[\\/]node_modules[\\/]/,
priority: 10,
},
// 项目公共组件
default: {
minSize: 0, // 分离后的最小块文件大小默认3000
name: 'common', // 用以控制分离后代码块的命名
minChunks: 3, // 最小共用次数
priority: 10, // 优先级,多个分组冲突时决定把代码放在哪块
reuseExistingChunk: true,
},
},
},

tree shaking

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

scope hoisting

webpack3以后production时默认开启
原理:将所有模块的代码按照引⽤顺序放在⼀个函数作⽤域⾥,然后适当的重命名⼀ 些变量以防⽌变量名冲突 对⽐: 通过 scope hoisting 可以减少函数声明代码和内存开销

动态import

需要安装babel插件@babel/plugin-syntax-dynamic-import

"plugins": ["@babel/plugin-syntax-dynamic-import"]
import React, { useState } from 'react'
export function Hello() {
const [text, useText] = useState('before')

const loadimport = () => {
import('./web.js').then((data) => {
useText(data.default)
})
}
return (
<div className='contain'>
<button onClick={loadimport}>hello react</button>
{text}
</div>
)
}

SSR

优化SEO


if (typeof window === 'undefined') {
global.window = {};
}

const fs = require('fs');
const path = require('path');
const express = require('express');
const { renderToString } = require('react-dom/server');
const SSR = require('../dist/search-server');
const template = fs.readFileSync(path.join(__dirname, '../dist/search.html'), 'utf-8');
const data = require('./data.json');

const server = (port) => {
const app = express();

app.use(express.static('dist'));
app.get('/search', (req, res) => {
const html = renderMarkup(renderToString(SSR));
res.status(200).send(html);
});

app.listen(port, () => {
console.log('Server is running on port:' + port);
});
};

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

const renderMarkup = (str) => {
const dataStr = JSON.stringify(data);
return template.replace('<!--HTML_PLACEHOLDER-->', str)
.replace('<!--INITIAL_DATA_PLACEHOLDER-->', `<script>window.__initial_data=${dataStr}</script>`);
}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<div id="root"><!--HTML_PLACEHOLDER--></div>
<!--INITIAL_DATA_PLACEHOLDER-->
</body>
</html>

优化打包速度

webpack内置的stats

"build:stats": "webpack --config webpack.prod.js --json > stats.json"

loader plugin 速度分析

speed-measure-webpack-plugin

bundle 分析

webpack-bundle-analyzer:一个 plugin 和 CLI 工具,它将 bundle 内容展示为一个便捷的、交互式、可缩放的树状图形式。 image.png

多进程打包

thread-loader 、 HappyPack

{
test: /\.(tsx?|js|jsx)$/,
include: [SRC_PATH],
exclude: [/node_modules/, /public/, /(.|_)min\.js$/],
use: [
'cache-loader',
+ {
+ loader: 'thread-loader',
+ options: {
+ workers: 3,
+ },
+ },
'babel-loader?cacheDirectory=true',
],
},

并行压缩

webpack5默认开启

DLL

将基础库提前打包,加快build速度
webpack.dll.js

const path = require('path')
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
entry: {
vendor: ['react', 'react-dom'],
//other:['a','b','c']
},
mode: 'production',
output: {
path: path.join(__dirname, 'build'),
filename: '[name].dll.js',
library: '[name]_[hash]',
},
plugins: [
new CleanWebpackPlugin(),
new webpack.DllPlugin({
path: path.join(__dirname, 'build', '[name]_manifest.json'),
name: '[name]_[hash]',
}),
],
}



webpack.prod.js

new webpack.DllReferencePlugin({
context: path.join(__dirname),
manifest: require('./build/vendor_manifest.json'),
}),
new AddAssetHtmlPlugin({
filepath: path.resolve(__dirname, './build/*.dll.js'),
includeSourcemap: false,
//html导入目录
publicPath: './library/js',
//输出目录
outputPath: './library/js',
}),

缓存

目的:提升二次构建速度
思路:
babel-loader
hard-source-webpack-plugin 或者 cache-loader

缩小构建目标

比如 babel-loader 不解析 node_modules

{
test: /\.js$/,
+ exclude: /node_modules/,
use: [
'cache-loader',
{
loader: 'thread-loader',
options: {
workers: 3,
},
},
'babel-loader?cacheDirectory=true',
],
},

图片压缩

image-webpack-loader

PurifyCSS

purgecss-webpack-plugin remove unused css.

const path = require('path')
const glob = require('glob')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const PurgeCSSPlugin = require('purgecss-webpack-plugin')

const PATHS = {
src: path.join(__dirname, 'src')
}

module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
},
optimization: {
splitChunks: {
cacheGroups: {
styles: {
name: 'styles',
test: /\.css$/,
chunks: 'all',
enforce: true
}
}
}
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader"
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css",
}),
new PurgeCSSPlugin({
paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),
}),
]
}

动态polyfill

babel-plugin-transform-runtime
缺点:不能polyfill原型上的方法

{
"presets": ["@babel/preset-env"],
"plugins": [
[
'@babel/plugin-transform-runtime',
{
corejs: 3,
regenerator: true,
},
],
]
}

polyfill server
缺点:社区维护,部分国内浏览器魔改,导师UA无法识别(可以降级全部返回)
基于polyfill.io官网提供的服务
识别 User Agent,下发不同的 Polyfill

代码质量

  • 抽离成npm包
  • 测试
    • 冒烟测试
    • 单元测试
    • 测试覆盖率
  • eslint
  • ci
  • git 规范
  • changelog 文档
  • husky
  • 版本号

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

react脚手架

持续更新地址:react-webpack-template 本文持续更新栏目:前端工程化

other

  • 监控
  • 组件库
  • gitlab or 自研平台
  • 低代码平台
  • 微前端
  • 测试平台

参考

babel 小册--神说若有光
玩转 webpack--极客时间