# 模块化

# 模块化的目的

高内聚,低耦合

# 常见的模块化规范

  1. CommonJS

  2. AMD / CMD

  3. ES6 Module

  4. UMD

// CommonJS
var math = require('math'); // 引入一个模块
math.add(2, 3);
module.exports = {}; // 导出一个模块

// ES6 Module
import math from 'math'; // 引入一个模块
math.add(2, 3);
export default {}; // 导出一个模块
export {};

// AMD
// 定义一个模块
define(['./a', './b'], function(a, b) {  // 依赖必须一开始就写好
  a.doSomething()
  // 此处略去 100 行
  b.doSomething()
  ...
})

// CMD
// 定义一个模块
define(function(require, exports, module) {
  var a = require('./a')
  a.doSomething()
  // 此处略去 100 行
  var b = require('./b') // 依赖可以就近书写
  b.doSomething()
  // ...
})

// UMD
// 兼容 cjs、AMD、CMD
(function(root, factory) {
  if (typeof module === 'object' && typeof module.exports === 'object') {
      console.log('是 commonjs 模块规范,nodejs 环境')
      module.exports = factory();
  } else if (typeof define === 'function' && define.amd) {
      console.log('是 AMD 模块规范,如 require.js')
      define(factory)
  } else if (typeof define === 'function' && define.cmd) {
      console.log('是 CMD 模块规范,如 sea.js')
      define(function(require, exports, module) {
          module.exports = factory()
      })
  } else {
      console.log('没有模块环境,直接挂载在全局对象上')
      root.umdModule = factory();
  }
}(this, function() {
  return {
      name: '我是一个 umd 模块'
  }
}))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

# ES6 Module 和 CommonJS 的区别

1. ES6 Module(import/export)

  • 静态解析:ES6 Module 采用静态解析,这意味着模块的依赖关系在代码静态分析阶段就能够确定。这使得工具能够更好地进行优化和静态分析。

  • 顶层执行:ES6 Module 在每个模块中都有独立的顶层作用域,模块内的变量不会自动成为全局变量。导入的值是只读的,不可修改。

  • 异步加载:ES6 Module 支持异步加载,可以使用 import() 语法进行动态加载模块。

  • 导出方式:ES6 Module 允许同一个 JS 文件(模块)同时有默认导出和普通导出。

// 普通导出
export const foo = 'foo';

// 默认导出
const bar = 'bar';
export default bar;
1
2
3
4
5
6

2. CommonJS(require/module.exports)

  • 动态运行时解析:CommonJS 采用动态运行时解析,模块的依赖关系在运行时确定。这导致在循环依赖的情况下可能会出现问题。

  • 共享顶层作用域:CommonJS 模块在同一应用中共享顶层作用域。导入的值是可变的,因此导入的变量可以在导出模块中被修改。

  • 同步加载:在 Node.js 中,CommonJS 是同步加载的,require 函数会阻塞执行,直到模块加载完成。但在浏览器环境,通常使用模块打包工具(如 Webpack)将 CommonJS 转换成异步加载的形式。

  • 导出方式:在 CommonJS 中一个 JS 文件无法同时有普通导出和默认导出。

// 普通导出
exports.foo = 'foo';

// 默认导出
module.exports = {};
1
2
3
4
5

ES6 Module 更适合前端开发,因为它具有更好的静态分析能力和异步加载特性。CommonJS 更适用于服务器端开发,尤其是在 Node.js 中,因为它在 Node.js 中有着较广泛的应用。

# AMD 和 CMD 的区别

说明

虽然 AMD 也支持 CMD 的写法,同时还支持将 require 作为依赖项传递,但 RequireJS 的作者默认是最喜欢上面的写法,也是官方文档里默认的模块定义写法。

  • AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。

说明

比如 AMD 里,require 分全局 require 和局部 require,都叫 require。CMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每个 API 都简单纯粹。

AMD 和 CMD 的区别有哪些? (opens new window)

# RequireJS 和 SeaJS

  • RequireJS 和 SeaJS 都是模块加载器,倡导模块化开发理念,核心价值是让 JavaScript 的模块化开发变得简单自然。

  • RequireJS 遵循 AMD(异步模块定义)规范,SeaJS 遵循 CMD(通用模块定义)规范。规范的不同,导致了两者 API 不同。SeaJS 更贴近 CommonJS Modules/1.1 和 Node Modules 规范。

# webpack 简介

# 为什么选择 webpack

  1. 社区生态丰富

  2. 配置灵活和插件化扩展

  3. 官方更新迭代速度快

# webpack 官网

  1. 英文官网 (opens new window)

  2. 中文官网 (opens new window)

# webpack 安装

// 全局安装
npm install --save-dev webpack webpack-cli -g

// 项目安装
npm install --save-dev webpack webpack-cli

// 全局安装指定版本
npm install --save-dev webpack@1 webpack-cli@1 -g

// 项目安装指定版本
npm install --save-dev webpack@1 webpack-cli@1
1
2
3
4
5
6
7
8
9
10
11

# webpack 的特点

  1. 拆分模块到代码块,按需加载。

  2. 快速初始化加载。

  3. 所有的静态资源都可以当作模块。

  4. 第三方库模块化。

  5. 自定义模块化打包。

  6. 适合大型项目。

# webpack 的配置文件

# 配置文件名称

  1. webpack 默认配置文件是:webpack.config.js

  2. 可以通过 webpack --config 指定配置文件

# webpack 配置组成

module.exports = {
  entry: "./src/index.js", // 打包的入口文件
  output: "./dist/main.js", // 打包的输出
  mode: "production", // 环境
  module: {
    rules: [
      // loader 配置
      { test: /\.txt$/, use: "raw-loader" }
    ]
  },
  plugins: [
    // 插件配置
    new HtmlWebpackPlugin({
      template: "./src/index.html"
    })
  ]
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 零配置 webpack

module.exports = {
  entry: "./src/index.js", // 指定默认的 entry 为 ./src/index.js
  output: "./dist/main.js", // 打包的输出 output 为 ./dist/main.js
  mode: "production", // 环境
  module: {
    rules: [
      // loader 配置
      { test: /\.txt$/, use: "raw-loader" }
    ]
  },
  plugins: [
    // 插件配置
    new HtmlWebpackPlugin({
      template: "./src/index.html"
    })
  ]
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# webpack 的核心配置选项

1. entry:配置入口资源。

  • 单入口:entry 是一个字符串
module.exports = {
  entry: "./src/index.js"
};
1
2
3
  • 多入口:entry 是一个对象
module.exports = {
  entry: {
    app: "./src/app.js",
    admin: "./src/admin.js"
  }
};
1
2
3
4
5
6

2. output:配置编译后的资源,用来告诉 webpack 如何将编译后的文件输出到磁盘。

  • 单入口配置
module.exports = {
  output: {
    filename: "bundle.js",
    path: path.join(__dirname, "dist"),
    publicPath: "/"
  }
};
1
2
3
4
5
6
7
  • 多入口配置
module.exports = {
  output: {
    filename: "[name].[hash:5].js", // 通过占位符来确保文件名称的唯一
    path: path.join(__dirname, "dist"),
    publicPath: "/"
  }
};
1
2
3
4
5
6
7

注意

  • publicPath 主要用于配置页面资源的基本路径,一般打包后如果资源找不到,需要先看下有没有指定 publicPath 为 /

  • 如果项目的静态资源接入了 CDN,还可以通过 publicPath 统一指定 CDN 路径。比如:

module.exports = {
  output: {
    publicPath: "https://cdn.example.com/assets/"
  }
};
1
2
3
4
5

3. module:资源处理,loader 在这里配置。

webpack 开箱即用支持 JS 和 JSON 两种文件类型,通过 loaders 去支持其他文件类型并且把它们转化成有效的模块,并且可以添加到依赖图中。

loader 本身是一个函数,接收源文件作为参数,返回转换后的结果。

常用的 loader 有:

  • babel-loader:转换 ES6、ES7 等新特性语法。

  • css-loader:支持 .css 文件的加载和解析。

  • style-loader:将样式通过 <style> 标签插入到 head 中.

  • less-loader:将 less 文件转换成 css。

  • ts-loader:将 ts 转换成 js。

  • file-loader:进行图片、字体等的打包。

  • raw-loader:将文件以字符串的形式导入。

  • thread-loader:多进程打包 js 和 css。

4. resolve:配置资源别名/扩展名等。

5. plugins:插件,比 loader 更强大。

插件用于 bundle 文件(打包后的文件)的优化,资源管理和环境变量注入。可以理解为 loader 不能做的工作可以由 plugins 来完成。

作用于整个构建过程。

常见的 plugins 有:

  • CommonsChunkPlugin:将 chunks 相同的模块代码提取成公共 js。

  • CleanWebpackPlugin:清理构建目录。

  • ExtractTextWebpackPlugin:将 css 从 bundle 文件里提取成一个单独的 css 文件。

  • CopyWebpackPlugin:将文件或者文件夹拷贝到构建的输出目录。

  • HtmlWebpackPlugin:创建 html 文件去承载输出的 bundle。

  • UglifyjsWebpackPlugin:压缩 js。

  • ZipWebpackPlugin:将打包出的资源生成一个 zip 包。

6. mode:webpack4 新增的功能。

mode 用来指定当前的构建环境是:production、development 还是 none。

设置 mode 可以使用 webpack 内置的函数,默认值为 production。

mode 的内置函数功能

选项 描述
development 设置 process.env.NODE_ENV 的值为 development,开启 NamedChunksPlugin 和 NamedModulesPlugin
production 设置 process.env.NODE_ENV 的值为 production,开启 FlagDependencyUsagePlugin,FlagIncludedChunksPlugin,ModuleContenationPlugin,NoEmitOnErrorsPlugin,OncurrenceOrderPlugin,SideEffectsFlagPlugin 和 TerserPlugin。
none 不开启任何优化选项

# webpack 使用

# 解析 CSS

  1. css-loader 用于加载 .css 文件,并且转换成 commonjs 对象。

  2. style-loader 将样式通过 <style> 标签插入到 head 中。

  3. 使用时有一点需要注意的是,loader 的调用是链式调用的,调用顺序是从右向左,因此要先写 style-loader,再写 css-loader

module: {
  rules: [
    {
      test: /\.css$/i,
      use: ["style-loader", "css-loader"]
    }
  ];
}
1
2
3
4
5
6
7
8

# 解析 Less 和 Sass

  1. less-loader 用于将 less 转换成 css。需要安装 less 和 less-loader

  2. sass-loader 用于将 sass 转换成 css。需要安装 sass 和 sass-loader

  3. 只需要在解析 css 的基础上再加上对应的 loader 就行了。

module: {
  rules: [
    {
      test: /\.less$/i,
      use: ["style-loader", "css-loader", "less-loader"]
    }
  ];
}
1
2
3
4
5
6
7
8

# 转换 ES6+ 代码

  1. 涉及 js 的一些转换操作,一般都要用到 babel (opens new window)。babel 是一个 JavaScript 编译器,它的配置文件是:.babelrc

  2. 想要将 ES6+ 语法转换成 ES5 语法,就需要安装 @babel/core (opens new window)@babel/preset-env (opens new window)babel-loader (opens new window)

  3. 然后在 webpack.config.js 文件中加入配置:

module: {
  rules: [
    {
      test: /\.js$/,
      use: {
        loader: "babel-loader",
        options: {
          presets: ["@babel/preset-env"]
        }
      },
      exclude: /node_modules/
    }
  ];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

不过,一般我们会将跟 babel 相关的配置放在 .babelrc 文件中:

{
  "presets": [
    "@babel/preset-env"
  ]
}
1
2
3
4
5
  1. @babel/preset-env 并不能转换所有的 ES6+ 代码,它只能转换标准引入的语法,比如:箭头函数、let、const、class 等等。像标准引入的全局变量、部分原生对象新增的原型链上的方法(比如:Array.map()、Object.assign() 等)、Promise 等等就转换不了了,此时就需要使用 polyfill 来处理这些内容。
import "@babel/polyfill";
1

但是这种用法有个问题,就是整个引入 @babel/polyfill 会导致打包后的文件体积很大。因此我们需要按需引入,按需引入的方法就是在 webpack.config.js 中配置 useBuiltIns: 'usage',加了这个配置之后,js 文件中也不需要手动导入 @babel/polyfill 了。

module: {
  rules: [
    {
      test: /\.js$/,
      use: {
        loader: "babel-loader",
        options: {
          presets: [
            "@babel/preset-env",
            {
              // 必须同时设置 corejs,否则会抛出警告
              // 因为 @babel/preset-env 默认使用的是 corejs2,但是新特性都添加在 corejs3 中
              corejs: 3,
              useBuiltIns: "usage"
            }
          ]
        }
      },
      exclude: /node_modules/
    }
  ];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

或者在 .babelrc 中进行配置:

{
  "presets": [
    "@babel/preset-env",
    {
      "corejs": 3,
      "useBuiltIns": "usage"
    }
  ]
}
1
2
3
4
5
6
7
8
9
  1. 使用 @babel/polyfill 还有一个问题就是,它是以全局变量的形式注入的,会污染到全局环境。如果是在开发业务中使用可能没什么问题,但是如果是在开发类库、组件库时很可能会因为全局变量被污染而出现问题。

因此,如果不想污染全局环境,推荐使用这个插件:@babel/plugin-transform-runtime (opens new window),它是以闭包的形式注入的,不会污染全局环境。

跟 @babel/polyfill 需要配置 corejs3 类似,@babel/plugin-transform-runtime 使用时也需要配置 corejs3,使用的插件是:@babel/runtime-corejs3

module: {
  rules: [
    {
      test: /\.js$/,
      use: {
        loader: "babel-loader",
        options: {
          plugins: [
            [
              "@babel/plugin-transform-runtime",
              {
                corejs: 3
              }
            ]
          ]
        }
      },
      exclude: /node_modules/
    }
  ];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

或者在 .babelrc 中配置:

{
  "plugin": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": 3
      }
    ]
  ]
}
1
2
3
4
5
6
7
8
9
10

# 解析 React JSX

  1. 安装 react、react-dom、@babel/preset-react。

  2. 在 .babelrc 文件增加 react 的 babel preset 配置:

{
  "presets": [
    "@babel/preset-react"
  ]
}
1
2
3
4
5

# 解析图片和字体

  1. file-loader 用于处理文件。
module: {
  rules: [
    {
      test: /.(png|jpg|gif|jpeg)$/,
      use: {
        loader: "file-loader",
        options: {
          name: "[name].[hash:5].[ext]", // 指定生成的文件名
          outputPath: "assets" // 生成的文件将会放在 dist/assets 文件夹中
        }
      }
    }
  ];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  1. file-loader 也可以用于处理字体。
module: {
  rules: [
    {
      test: /.(woff|woff2|eot|ttf|otf)$/,
      use: {
        loader: "file-loader",
        options: {
          name: "[name].[hash:5].[ext]", // 指定生成的文件名
          outputPath: "font" // 生成的文件将会放在 dist/font 文件夹中
        }
      }
    }
  ];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  1. url-loader 也可以用来处理图片和字体,和 file-loader 差不多,只不过它可以设置较小的资源自动转换成 base64(通过 options 里的 limit 字段来指定,单位是字节),其内部也是使用了 file-loader。
module: {
  rules: [
    {
      test: /.(png|jpg|gif|jpeg)$/,
      use: [
        {
          loader: "url-loader",
          options: {
            limit: 10240
          }
        }
      ]
    }
  ];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 文件监听

1. 文件监听是在发现源码发生变化时,自动重新构建出新的输出文件。

2. webpack 开启监听模式,有两种方式:

  • 启动 webpack 命令时,带上 --watch 参数。

  • 在 webpack.config.js 中设置 watch: true

    这两种方式唯一的缺点就是需要手动刷新浏览器。

3. 文件监听的原理分析

  • 轮询判断文件的最后编辑时间是否变化。

  • 某个文件发生了变化,并不会立刻告诉监听者,而是先缓存起来,等 aggregateTimeout。

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

# 热更新

1. webpack-dev-server

(1)使用 webpack-dev-server 作为服务器启动。

(2)devServer 中配置 hot:true

(3)plugins 中配置 new webpack.HotModuleReplacementPlugin().

(4)在 js 文件中使用 module.hot.accept() 指定对哪个文件开启热替换功能。

"scripts": {
  "dev:server": "webpack-dev-server --open"
}
1
2
3
module.exports = {
  plugins: [new webpack.HotModuleReplacementPlugin()],
  devServer: {
    contentBase: "./dist",
    hot: true
  }
};
1
2
3
4
5
6
7
if (module.hot) {
  module.hot.accept("./print.js", function() {
    // 开启热替换功能
    console.log("Accepting the updated printMe module!");
    printMe();
  });
  module.hot.decline("./print.js"); // 关闭热替换功能
}
1
2
3
4
5
6
7
8

这样,当改变 print.js 文件的内容时,只会更新对应模块,而不会影响其他模块,导致整个页面刷新。

注意

WDS 不刷新浏览器,不输出文件,而是放在内存中。上面两种文件监听的方式是输出文件到本地磁盘中,所以热更新要更快。

2. webpack-dev-middleware

  • WDM 将 webpack 输出的文件传输给服务器。

  • 适用于灵活的定制场景。

const express = require("express");
const webpack = require("webpack");
const webpackDevMiddleware = require("webpack-dev-middleware");

const app = express();
const config = require("./webpack.config.js");
const compiler = webpack(config);

app.use(
  webpackDevMiddleware(compiler, {
    publicPath: config.output.publicPath
  })
);

app.listen(3000, function() {
  console.log("Example app listening on port 3000\n");
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 文件指纹策略

1. 文件指纹就是打包后输出的文件名的后缀。

文件指纹不能跟热更新一起使用,因此在生产模式下进行配置。

2. 文件指纹如何生成?

  • hash:和整个项目的构建相关,只要项目文件有修改,整个项目构建的 hash 值就会更改。

  • chunkhash:和 webpack 打包的 chunk 有关,不同的 entry 会生成不同的 chunkhash 值。

  • contenthash:根据文件内容来定义 hash,文件内容不变,则 contenthash 不变。

3. JS 的文件指纹设置

设置 output 的 filename,使用 [chunkhash]。

module.exports = {
  entry: {
    app: "./src/app.js",
    search: "./src/search.js"
  },
  output: {
    filename: "[name][chunkhash:8].js", // :8 表示取前八位
    path: path.resolve(__dirname, "dist")
  }
};
1
2
3
4
5
6
7
8
9
10

4. CSS 的文件指纹设置

设置 MiniCssExtractPlugin 的 filename,使用 [contenthash]。

module.exports = {
  entry: {
    app: './src/app.js',
    search: './src/search.js'
  },
  output: {
    filename: '[name][chunkhash:8].js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name][contenthash:8].css'
    });
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

5. 图片的文件指纹设置

设置 file-loader 的 name,使用 [hash]。

占位符名称 含义
[ext] 资源后缀名
[name] 文件名称
[path] 文件的相对路径
[folder] 文件所在的文件夹
[contenthash] 文件的内容 hash,默认是 md5 生成
[hash] 文件内容的 hash,默认是 md5 生成
[emoji] 一个随机的指代文件内容的 emoji
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]"
            }
          }
        ]
      }
    ]
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 代码压缩

1. JS 文件的压缩

  • webpack4 生产模式下默认会开启 js 代码压缩,但是当我们进行 css 压缩后会影响到 js 代码压缩,此时就需要自己手动使用插件进行 js 压缩。

  • 之前可以使用 uglifyjs-webpack-plugin (opens new window) ,对这个插件进行一定的配置还可以达到不同的压缩效果,比如并行压缩。不过现在这个插件已经不维护了。

  • 现在压缩 js 代码推荐使用 terser-webpack-plugin (opens new window)

2. CSS 文件的压缩

使用 optimize-css-assets-webpack-plugin (opens new window),它有以下几个配置选项:

  • assetNameRegExp:一个正则表达式,指定应该被优化的静态资源名称。 提供的正则表达式针对配置中的 ExtractTextPlugin 实例导出的文件的文件名而不是源 CSS 文件的文件名运行。 默认为 /\.css$/g

  • cssProcessor:用于优化 CSS 的 CSS 处理器,默认为 cssnano (opens new window)。 这应该是 cssnano.process 接口之后的函数(接收 CSS 和 options 参数并返回 Promise)。

  • cssProcessorOptions:传递给 cssProcessor 的选项,默认为 {}

  • cssProcessorPluginOptions:传递给 cssProcessor 的插件选项,默认为 {}

  • canPrint:一个布尔值,表示插件是否可以将消息打印到控制台,默认为 true

module.exports = {
  entry: {
    app: "./src/app.js",
    search: "./src/search.js"
  },
  output: {
    filename: "[name][chunkhash:8].js",
    path: path.resolve(__dirname, "dist")
  },
  plugins: [
    new OptimizeCSSAssetsPlugin({
      assetNameRegExp: /\.css$/g,
      cssProcessor: require("cssnano")
    })
  ]
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

3. HTML 文件的压缩

使用 html-webpack-plugin (opens new window),通过设置压缩属性 minify

module.exports = {
  entry: {
    app: "./src/app.js",
    search: "./src/search.js"
  },
  output: {
    filename: "[name][chunkhash:8].js",
    path: path.resolve(__dirname, "dist")
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.join(__dirname, "src/search.html"),
      filename: "search.html",
      chunks: ["search"],
      inject: true,
      minify: {
        html5: true,
        collapseWhitespace: true,
        preserveLineBreaks: false,
        minifyCSS: true,
        minifyJS: true,
        removeComments: false
      }
    })
  ]
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 自动清理构建目录产物

每次构建的时候不清理目录的话,会造成构建的输出目录 output 里的文件越来越多。

1. 通过 npm scripts 清理构建目录

  • Mac 系统下配置
rm -rf ./dist && webpack
1
  • Windows 系统下配置
rimraf ./dist && webpack
1

2. 自动清理构建目录

使用 clean-webpack-plugin (opens new window),默认会删除 output 指定的输出目录。

const { CleanWebpackPlugin } = require("clean-webpack-plugin");

module.exports = {
  entry: {
    app: "./src/app.js",
    search: "./src/search.js"
  },
  output: {
    filename: "[name][chunkhash:8].js",
    path: path.resolve(__dirname, "dist")
  },
  plugins: [new CleanWebpackPlugin()]
};
1
2
3
4
5
6
7
8
9
10
11
12
13

# 自动补全 CSS 前缀

安装 postcss (opens new window)postcss-loader (opens new window)Autoprefixer (opens new window),并在 webpack.config.js 中加入以下配置。

关于 Autoprefixer 插件可以使用的 Browserslist 可以看这里:Browserslist docs (opens new window)

module.exports = {
  module: {
    rules: [
      {
        test: /\.less$/,
        use: [
          "style-loader",
          "css-loader",
          "less-loader",
          {
            loader: "postcss-loader",
            options: {
              postcssOptions: {
                plugins: [
                  [
                    "autoprefixer",
                    {
                      overrideBrowserslist: ["last 2 version", ">1%", "iOS 7"]
                    }
                  ]
                ]
              }
            }
          }
        ]
      }
    ]
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

不过,一般情况下我们会把 postcss 的相关配置放到 postcss.config.js 文件中:

module.exports = {
  plugins: [
    [
      "autoprefixer",
      {
        overrideBrowserslist: ["last 2 version", ">1%", "iOS 7"]
      }
    ]
  ]
};
1
2
3
4
5
6
7
8
9
10

# 移动端 CSS px 自动转换成 rem

使用 px2rem-loader (opens new window)。页面渲染时计算根元素的 font-size 值。

可以使用手淘的 lib-flexible (opens new window) 库。

需要安装 px2rem-loader 和 lib-flexible。

npm install px2rem-loader -D

npm install lib-flexible
1
2
3
module.exports = {
  module: {
    rules: [
      {
        test: /\.less$/,
        use: [
          "style-loader",
          "css-loader",
          "less-loader",
          {
            loader: "px2rem-loader",
            options: {
              remUnit: 75, // 设置 rem 相对 px 的转换单位,一个 rem 等于 75px
              remPrecision: 8 // px 转换为 rem 时小数点后的位数
            }
          }
        ]
      }
    ]
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 资源内联

1. 资源内联的意义

代码层面

  • 页面框架的初始化脚本

  • 上报相关打点

  • css 内联避免页面闪动

请求层面

  • 减少 http 网络请求数

  • 小图片或者字体内联直接使用 url-loader 就可以了,通过 limit 参数

2. HTML 和 JS 内联

需要使用 raw-loader 和 babel-loader,需要注意的是,raw-loader 要使用 0.5.1 的版本,最新的版本会有点问题。

  • raw-loader 内联 html
<%=require("raw-loader!./meta.html")%>
1
  • raw-loader 内联 js
<script>
  <%=require('raw-loader!babel-loader!../node_modules/lib-flexible/flexible.js')%>
</script>
1
2
3

3. CSS 内联

  • 方案一:借助 style-loader
module.exports = {
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          {
            loader: "style-loader",
            options: {
              insertAt: "top", // 样式插入到 <head>
              singleton: true // 将所有的 style 标签合并成一个
            }
          },
          "css-loader",
          "sass-loader"
        ]
      }
    ]
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  • 方案二:借助 html-inline-css-webpack-plugin

# 多页面应用打包通用方案

1. MPA 概念

每一次页面跳转的时候,后台服务器都会返回一个新的 html 文档,这种类型的网站就是多页网站,也叫多页应用。

2. 多页面打包基本思路

  • 每个页面对应一个 entry,一个 html-webpack-plugin。

  • 缺点:每次新增或删除页面需要改 webpack 配置。

3. 多页面打包通用方案

  • 动态获取 entry 和设置 html-webpack-plugin 数量。

  • 利用 glob.sync,glob 模块无需安装,直接引用就行了。

{
  entry: glob.sync(path.join(__dirname, "./src/*/index.js"));
}
1
2
3
  • 实际配置如下:
const path = require("path");
const glob = require("glob");
const HtmlWebpackPlugin = require("html-webpack-plugin");

const setMPA = () => {
  const entry = {};
  const htmlWebpackPlugins = [];
  const entryFiles = glob.sync(path.join(__dirname, "./src/*/index.js")); // 获取入口文件路径
  Object.values(entryFiles).forEach((file) => {
    const pageName = file.match(/src\/(.*)\/index\.js/)[1]; //  获取页面所在的目录
    entry[pageName] = file;
    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,
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "[name]_[chunkhash:8].js"
  },
  mode: "production",
  module: {
    // ...
  },
  plugins: [
    // ...
  ].concat(htmlWebpackPlugins)
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

# 使用 source map

1. 什么是 source map

Source map 就是一个信息文件,里面储存着位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置。

有了它,出错的时候,除错工具将直接显示原始代码,而不是转换后的代码。这无疑给开发者带来了很大方便。

可以参考这篇文章:JavaScript Source Map 详解 (opens new window)

2. source map 作用

通过 source map 定位到源代码。

开发环境开启,线上环境关闭。因为如果线上环境也开启的话会将源代码的业务逻辑暴露出去,线上排查问题的时候可以将 source map 上传到错误监控系统。

3. source map 关键字

在 webpack 中和 source map 相关主要有以下几个关键字:

  • eval:使用 eval 包裹模块代码。

  • source map:产生 .map 文件。

  • cheap:不包含列信息。

  • inline:将 .map 作为 DataURI 嵌入,不单独生成 .map 文件。

  • module:包含 loader 的 source map。

4. source map 类型

devtool 首次构建 二次构建 是否适合生产环境 可以定位的代码
(none) +++ +++ yes 最终输出的代码
eval +++ +++ no webpack 生成的代码(一个个的模块)
cheap-eval-source-map + ++ no 经过 loader 转换后的代码(只能看到行)
cheap-module-eval-source-map o ++ no 源代码(只能看到行)
eval-source-map -- + no 源代码
cheap-source-map + o yes 经过 loader 转换后的代码(只能看到行)
cheap-module-source-map o - yes 源代码(只能看到行)
inline-cheap-source-map + o no 经过 loader 转换后的代码(只能看到行)
inline-cheap-module-source-map o - no 源代码(只能看到行)
source-map -- -- yes 源代码
inline-source-map -- -- no 源代码
hidden-source-map -- -- yes 源代码
nosources-source-map -- -- yes 无源代码内容
module.exports = {
  devtool: "eval"
};
1
2
3

提示

  • +++ 非常快速,++ 快速,+ 比较快,o 中等,- 比较慢,-- 慢

  • 开发环境一般推荐使用 cheap-module-eval-source-map,能看到源代码,也能定位到具体哪一行。生产环境如果需要调试的话,一般推荐使用 cheap-module-source-map,不会暴露源代码,并且错误定位信息比较具体;如果不需要调试,直接置为 none 就行了。

# 提取公共资源

1. 基础库分离

const HtmlWebpackExternalsPlugin = require("html-webpack-externals-plugin");

module.exports = {
  plugins: [
    new HtmlWebpackExternalsPlugin({
      externals: [
        {
          module: "react",
          entry:
            "https://cdn.staticfile.org/react/16.9.0/umd/react.production.min.js",
          global: "React"
        },
        {
          module: "react-dom",
          entry:
            "https://cdn.staticfile.org/react-dom/16.9.0/umd/react-dom.production.min.js",
          global: "ReactDOM"
        }
      ]
    })
  ]
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

2. 利用 SplitChunksPlugin 进行公共脚本分离

选项 描述
async 异步引入的库进行分离(默认)
initial 同步引入的库进行分离
all 所有引入的库进行分离(推荐)
module.exports = {
  optimization: {
    splitChunks: {
      chunks: "all", // 需要抽取的包的引入方式
      minSize: 30000, // 只要包最小的大小满足,就进行抽取,单位是 kb
      maxSize: 0, // 只要包最大的大小满足,就进行抽取
      minChunks: 1, // 某个方法/模块最少使用的次数,这里表示使用次数大于等于 1 的时候就会被抽离成公共文件
      maxAsyncRequests: 5, // 最大异步加载次数
      maxInitialRequests: 3, // 最大同步加载次数
      automaticNameDelimiter: "~", // 输出文件名之间的分隔符
      name: false, // name 默认为 true,表示使用默认生成的文件名,如果为 false,可以自定义文件名
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/, // 匹配 node_modules 目录下的文件
          priority: -10, // 优先级比较高,优先使用优先级高的选项
          reuseExistingChunk: true, // 设为 true 表示已经打包过的模块就不会再打包
          filename: "jquery.js" // 自定义抽取出来的包的文件名为 jquery.js
        },
        default: {
          minChunks: 2,
          priority: -20, // 优先级比较低
          reuseExistingChunk: true,
          filename: "common.js" // 自定义抽取出来的包的文件名为 common.js
        }
      }
    }
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  • 分离基础包
module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        commons: {
          test: /(react|react-dom)/, // 匹配出需要分离的包
          name: "vendors",
          chunks: "all"
        }
      }
    }
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 分离页面公共文件
module.exports = {
  optimization: {
    splitChunks: {
      minSize: 0, // 分离的包体积的最小大小
      cacheGroups: {
        commons: {
          name: "commons",
          chunks: "all",
          minChunks: 2 // 设置最小引用次数为 2 次
        }
      }
    }
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# ScopeHoisting 使用和原理分析

1. 当前打包现象

webpack 构建后的代码存在大量闭包代码,这会导致以下两个问题:

  • 大量闭包函数包裹代码,导致体积增大(模块越多越明显)。

  • 运行代码时创建的函数作用域变多,内存开销变大。

2. 模块转换分析

webpack

可以看到:

  • 被 webpack 转换后的模块会带上一层包裹。

  • import 会被转换成 __webpack_require__

webpack

可以看到:

  • 打包出来是一个 IIFE(匿名闭包)。

  • modules 是一个数组,每一项是一个模块初始化函数。

  • __webpack_require__ 用来加载模块,返回 module.exports。

  • 通过 WEBPACK_REQUIRE_METHOD(0) 启动程序。

3. scope hoisting 原理

  • 原理:将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突。

  • 对比:通过 scope hoisting 可以减少函数声明代码和内存开销。

webpack

4. scope hoisting 使用

mode 为 production 时会默认开启 ModuleConcatenationPlugin 插件,必须是 ES6 语法,不能是 CJS。

# 代码分割和动态 import

1. 代码分割的意义

  • 对于大的 web 应用来讲,将所有的代码都放在一个文件里显然是不合理的,特别是当某些代码块是在某些特殊的时候才会被使用到。webpack 有一个功能就是能够将代码分割成 chunks(块),当代码运行到需要它们的时候再加载。

  • 适用场景

    抽离相同代码到一个共享块。

    脚本懒加载,使得初始下载的代码更小。

2. 懒加载 JS 脚本的方式

  • CommonJS:require.ensure

  • ES6:动态 import

目前还没有原生支持动态 import,需要 babel 转换。方法如下:

  • 安装 babel 插件
npm install @babel/plugin-syntax-dynamic-import -- save-dev
1
  • 在 .babelrc 文件中添加以下配置
{
  "plugins": ["@babel/plugin-syntax-dynamic-import"]
}
1
2
3
  • 引入模块时可以这么写:
// 魔法注释
import(/*webpackChunkName: 'jquery'*/ "jquery").then(({ default: $ }) => {
  console.log($);
});
1
2
3
4

# 与 ESLint 结合

1. 行业内优秀的 ESLint 规范实践

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

  • 腾讯

    alloyteam 团队:eslint-config-alloy

    ivweb 团队:eslint-config-ivweb

2. ESLint 如何执行落地?

  • 和 CI/CD 系统集成

  • 和 webpack 集成

3. 方案一:webpack 与 CI/CD 集成

在 build 阶段增加 lint 检查

webpack

本地阶段增加 precommit 钩子

  • 安装 husky
npm install husky --save-dev
1
  • 增加 npm script,通过 lint-staged 增量检查修改的文件。
"scripts": {
  "precommit": "lint-staged"
},
"lint-staged": {
  "linters": {
    "*.{js, scss}": ["eslint-fix", "git add"]
  }
}
1
2
3
4
5
6
7
8

由于本地的 precommit 钩子还是有些方法是可以绕过的,所以在 CI/CD 上增加 lint 检查是很有必要的。

4. 方案二:webpack 与 ESLint 集成

使用 eslint-loader,构建时检查 JS 规范

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: ["babel-loader", "eslint-loader"]
      }
    ]
  }
};
1
2
3
4
5
6
7
8
9
10
11

5. ESLint 配置文件

# 打包组件和基础库

1. 实现一个大整数加法库的打包

  • 需要打包压缩版本和非压缩版本

  • 支持 AMD/CJS/ESM 模块引入

2. 库的目录结构

webpack

3. 支持的使用方式

  • ES Module
import * as largeNumberTnd from "large-number-tnd";

largeNumberTnd("999", "1");
1
2
3
  • CJS
const largeNumberTnd = require("large-number-tnd");

largeNumberTnd("999", "1");
1
2
3
  • AMD
require(["large-number-tnd"], function(largeNumberTnd) {
  largeNumberTnd("999", "1");
});
1
2
3

4. 如何将库暴露出去?

module.exports = {
  entry: {
    "large-number-tnd": "./src/index.js",
    "large-number-tnd.min": "./src/index.js"
  },
  output: {
    filename: "[name].js",
    library: "large-number-tnd",
    libraryTarget: "umd",
    libraryExport: "default"
  }
};
1
2
3
4
5
6
7
8
9
10
11
12

5. 如何打包压缩版本和非压缩版本

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

module.exports = {
  entry: {
    "large-number-tnd": "./src/index.js",
    "large-number-tnd.min": "./src/index.js"
  },
  output: {
    filename: "[name].js",
    library: "large-number-tnd",
    libraryTarget: "umd",
    libraryExport: "default"
  },
  mode: "none",
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        include: /\.min\.js$/
      })
    ]
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

6. 设置引用库时的入口文件

在库的根目录下新建 index.js,并加入以下配置。同时确保 package.json 中的 main 字段值是 index.js。

if (process.env.NODE_ENV === "production") {
  module.exports = require("./dist/large-number-tnd.min.js");
} else {
  module.exports = require("./dist/large-number-tnd.js");
}
1
2
3
4
5

7. 将包发到 npm 上

如何发布一个 npm 包

# 打包 SSR

1. 服务端渲染是什么?

  • 客户端渲染

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

  • 服务端渲染

    所有模板等资源都存储在服务端

    内网机器拉取数据更快

    一个 HTML 返回所有数据

2. 客户端渲染 VS 服务端渲染

对比项 客户端渲染 服务端渲染
请求 多个请求(HTML,数据等) 1 个请求
加载过程 HTML&数据串行加载 1 个请求返回 HTML&数据
渲染 前端渲染 服务端渲染

总结:服务端渲染(SSR)的核心是减少请求。

SSR 的优势

  • 减少白屏时间

  • 对 SEO 友好

3. SSR 实现思路

  • 服务端

(1)使用 react-dom/server (opens new window)renderToString (opens new window) 方法将 React 组件渲染成字符串。

(2)服务端路由返回对应的模板。

  • 客户端

(1)打包出针对服务端的组件。

4. SSR 实际操作

  • 新建 ssr 的 webpack 配置文件:webpack.ssr.config.js。内容基本跟生产环境配置一样,不过要注意修改下 entry 的文件匹配,匹配 .*-server.js 结尾的文件;以及 output 的输出文件名,去掉哈希值,改成 [name]-server.js

  • 新建 ssr 的入口打包文件,格式为 *-server.js,注意不能像平时那样直接使用 ReactDOM.render() 这种写法了,而是应该将组件导出出去,比如:module.exports = <Search />

  • 新建 server 文件夹,存放服务端文件 index.js,编写以下内容。

if (typeof window === "undefined") {
  // hack,解决 window is not defined 的问题
  global.window = {};
}

const express = require("express");
const { renderToString } = require("react-dom/server");
const SSR = require("../dist/search-server");

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); // 这里返回的是字符串,但是实际上是需要返回一个 html 文件的,所以需要用一个模板包装起来
  });

  app.listen(port, () => {
    console.log(`Server is listening on port ${port}...`);
  });
};

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

const renderMarkup = (html) => {
  return `
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    <body>
      <div id="root">${html}</div>
    </body>
    </html>
  `;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
  • 在 package.json 中新增打包命令。
{
  "scripts": {
    "build:ssr": "webpack --config webpack.ssr.config.js"
  }
}
1
2
3
4
5

完成以上步骤后,执行打包命令,同时执行 node server/index.js 启动服务,在浏览器中访问 http://localhost:3000/search 就可以看到 SSR 已经生效了,返回的不再是一个 div,而是一个完整的 html。

webpack

5. webpack SSR 打包存在的问题

  • 浏览器的全局变量(Node.js 中没有 document,window)。需要做 hack:
if (typeof window === "undefined") {
  global.window = {};
}
1
2
3
  • 组件适配:将不兼容的组件根据打包环境进行适配。

  • 请求适配:将 fetch 或者 ajax 发送请求的写法改成 isomorphic-fetch 或者 axios。

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

    方案一:服务端打包通过 ignore-loader 忽略掉 css 的解析。

    方案二:将 style-loader 替换成 isomorphic-style-loader。不过这种方案会改变我们常规的 css 写法,需要使用 css in js 的方式编写 css 才会生效。

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

  • 使用打包出来的浏览器端 html 为模板。
const fs = require("fs");
const path = require("path");

const template = fs.readFileSync(
  path.join(__dirname, "../dist/search.html"),
  "utf-8"
);

const renderMarkup = (html) => {
  return template.replace("<!--HTML_PLACEHOLDER-->", html);
};
1
2
3
4
5
6
7
8
9
10
11
  • 设置占位符,动态插入组件。占位符一般使用 html 注释就行了,中间的文字可以随便写。
<div id="root"><!--HTML_PLACEHOLDER--></div>
1

重新打包访问之后就可以看到样式出来了。

webpack

7. 首屏数据如何处理?

  • 服务端获取数据。

server 文件夹下新建一个 data.json 文件,存放数据。数据可以随便从网上复制一些,然后在 bejson (opens new window) 这个网站上格式化一下。

const data = require("./data.json");
1
  • 替换占位符。在 html 文件中新增另一个专门放置数据的占位符,服务端再对它进行替换。
<div id="root"><!--HTML_PLACEHOLDER--></div>
<!--INITIAL_DATA_PLACEHOLDER-->
1
2
const renderMarkup = (html) => {
  const dataStr = JSON.stringify(data);
  return template
    .replace("<!--HTML_PLACEHOLDER-->", html)
    .replace(
      "<!--INITIAL_DATA_PLACEHOLDER-->",
      `<script>window.__initial_data=${dataStr}</script>`
    );
};
1
2
3
4
5
6
7
8
9

重新打包访问,查看源代码就可以看到首屏的数据获取到了。有了数据之后,我们就可以对这些数据进行处理然后展现在页面上。

webpack

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

1. 统计信息 stats

Preset Alternative Description
"errors-only" none 只在发生错误时输出
"minimal" none 只在发生错误或有新的编译时输出
"none" false 没有输出
"normal" true 标准输出
"verbose" none 全部输出

2. 如何优化命令行的构建日志

  • 使用 friendly-errors-webpack-plugin。

    success:构建成功的日志提示

    warning:构建警告的日志提示

    error:构建报错的日志提示

  • stats 设置成 errors-only。

const friendlyErrorsWebpackPlugin = require("friendly-errors-webpack-plugin");

module.exports = {
  mode: "production",
  plugins: [new friendlyErrorsWebpackPlugin()],
  stats: "errors-only"
};

module.exports = {
  mode: "development",
  plugins: [new friendlyErrorsWebpackPlugin()],
  devServer: {
    contentBase: "./dist",
    hot: true,
    stats: "errors-only"
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 构建异常和中断处理

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

  • compiler 在每次构建结束后会触发 done 这个 hook。

  • process.exit 主动处理构建报错。

module.exports = {
  plugins: [
    function() {
      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); // 修改错误代码,原来是 2
        }
      });
    }
  ]
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 获取环境参数动态加载配置文件

  1. 使用 yargs-parser (opens new window) 获取当前环境变量。
const argv = require("yargs-parser")(process.argv.slice(2));
const _mode = argv.mode || "development";
1
2
  1. 根据拿到的环境变量值获取不同环境的配置文件。
const _mergeConfig = require(`./config/webpack.${_mode}.config.js`);
1
  1. 使用 webpack-merge (opens new window) 合并所有配置选项并导出。
const { merge } = require("webpack-merge");
webpackConfig = {
  // ...
};
module.exports = merge(_mergeConfig, webpackConfig);
1
2
3
4
5

此外,拿到当前环境变量后一般还可以这么使用:

const argv = require("yargs-parser")(process.argv.slice(2));
const _mode = argv.mode || "development";
const _modeFlag = _mode === "production";
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

webpackConfig = {
  plugins: [
    new MiniCssExtractPlugin({
      filename: _modeFlag ? "styles/[name].[hash:5].css" : "styles/[name].css",
      chunkFilename: _modeFlag ? "styles/[id].[hash:5].css" : "styles/[id].css"
    })
  ]
};
1
2
3
4
5
6
7
8
9
10
11
12
13

# 编写 webpack 构建配置包

# 构建配置包设计

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

(1)通用性

  • 业务开发者无需关注构建配置

  • 统一团队构建脚本

(2)可维护性

  • 构建配置合理拆分

  • README 文档、ChangeLog 文档等

(3)质量

  • 冒烟测试、单元测试、测试覆盖率

  • 持续集成

2. 构建配置管理的可选方案

  • 通过多个配置文件管理不同环境的构建,webpack --config 参数进行控制。

  • 将构建配置设计成一个库,比如:hjs-webpack、Neutrino、webpack-blocks。

  • 抽成一个工具进行管理,比如:create-react-app、kyt、nwb。

  • 将所有的配置放在一个文件,通过 --env 参数控制分支选择。

3. 构建配置包设计

(1)通过多个配置文件管理不同环境的 webpack 配置

  • 基础配置:webpack.base.config.js

  • 开发环境:webpack.development.config.js

  • 生产环境:webpack.production.config.js

  • SSR 环境:webpack.ssr.config.js

......

(2)抽离成一个 npm 包统一管理

  • 规范:Git commit 日志、README、ESLint 规范、Semver 规范

  • 质量:冒烟测试、单元测试、测试覆盖率和 CI

4. 通过 webpack-merge 组合配置

const { merge } = require("webpack-merge");
module.exports = merge(baseConfig, devConfig);
1
2

# 使用 ESLint 规范构建脚本

  • 使用 eslint-config-airbnb-base

  • eslint --fix 可以自动处理空格

npm install eslint babel-eslint eslint-config-airbnb-base eslint-plugin-import -D
1
module.exports = {
  parser: "babel-eslint",
  extends: "airbnb-base",
  env: {
    browser: true,
    node: true
  }
};
1
2
3
4
5
6
7
8

# 冒烟测试

1. 什么是冒烟测试?

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

2. 冒烟测试点

针对这个基础包,需要做的冒烟测试有以下两点:

  • 构建是否成功。

  • 每次构建完成 dist 目录是否有内容输出,包括 JS、CSS、HTML。

3. 冒烟测试实操

  • 新建一个 smoke 文件夹,存放冒烟入口文件 index.js,模板文件夹 template 和测试用例文件 *.spec.js

webpack

  • 安装 rimraf、glob-all、mocha。
npm i rimraf glob-all mocha -D
1
  • 编写冒烟入口文件。
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")); // 构建前进入到 template 文件夹下

rimraf("./dist", () => {
  const prodConfig = require("../../lib/webpack.prod.config"); // 加载生产环境的配置文件

  webpack(prodConfig, (error, stats) => {
    // 判断是否构建成功
    if (error) {
      console.log(error);
      process.exit(2);
    }
    console.log(
      stats.toString({
        colors: true,
        modules: false,
        children: false,
        chunks: false,
        chunkModules: false
      })
    );

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

    mocha.addFile(path.join(__dirname, "html.spec.js")); // 添加测试用例
    mocha.addFile(path.join(__dirname, "cssjs.spec.js"));

    mocha.run(); // 运行测试用例
  });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  • 编写测试用例文件,判断是否有生成指定文件。
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) {
      done();
    } else {
      throw new Error("no html files found");
    }
  });
});
1
2
3
4
5
6
7
8
9
10
11
12
const glob = require("glob-all");

describe("Checking generated css and js files", () => {
  it("should generate css and js files", (done) => {
    const files = glob.sync([
      "./dist/index_*.css",
      "./dist/search_*.css",
      "./dist/index_*.js",
      "./dist/search_*.js"
    ]);
    if (files.length) {
      done();
    } else {
      throw new Error("no css and js files found");
    }
  });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 单元测试和测试覆盖率

1. 目前流行的测试框架

2. 编写单元测试用例

  • 技术选型:mocha + chai

  • 测试代码:describe,it,expect

  • 测试命令:mocha test.js

在 test 文件夹下新建单元测试入口文件 index.js 和 unit 文件夹,存放单元测试用例文件,如:webpack.base.config.spec.js。分别编写入口文件和单元测试用例文件:

const path = require("path");

process.chdir(path.join(__dirname, "smoke/template")); // 构建前进入到 template 文件夹下

describe("webpack-tnd test case", () => {
  require("./unit/webpack.base.config.spec");
});
1
2
3
4
5
6
7
const assert = require("assert");

describe("webpack.base.config.js test case", () => {
  const baseConfig = require("../../lib/webpack.base.config");

  it("test entry", () => {
    assert.equal(
      baseConfig.entry.index,
      "D:/project/custom/webpack-learn/webpack-tnd/test/smoke/template/src/index/index.js"
    );
    assert.equal(
      baseConfig.entry.search,
      "D:/project/custom/webpack-learn/webpack-tnd/test/smoke/template/src/search/index.js"
    );
  });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

然后在 package.json 中添加命令:

"scripts": {
  "test": "./node_modules/.bin/_mocha"
}
1
2
3

3. 单元测试覆盖率

使用 istanbul (opens new window) 这个库,注意要安装 v1.0.0-alpha.2 这个版本,否则不会收集覆盖率。安装完成后,将 package.json 里的命令改成:

// Windows 系统或 MacOS 系统
"scripts": {
  "test": "istanbul cover node_modules/mocha/bin/_mocha"
}

// MacOS 系统
"scripts": {
  "test": "istanbul cover ./node_modules/.bin/_mocha"
}
1
2
3
4
5
6
7
8
9

# 持续集成和 Travis CI

1. 持续集成的作用

  • 快速发现错误。

  • 防止分支大幅偏离主干。

核心措施是,代码集成到主干之前,必须通过自动化测试。只要有一个测试用例失败,就不能集成。

2. 目前 github 最流行的 CI

webpack

3. 接入 Travis CI

注意

Travis CI 只支持 Github 项目。

  • 访问 Travis CI (opens new window) 网站,并使用 GitHub 账号登录。

  • 为自己的 GitHub 仓库里的某个项目开启 travis ci。

  • 项目根目录下新增 .travis.yml 文件。

language: node_js

sudo: false

cache:
  apt: true
  directories:
    - node_modules

node_js: stable

install:
  - npm install -D
  - cd ./test/smoke/template
  - npm install -D
  - cd ../../../

scripts:
  - npm test
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

做好以上工作后,每次往 github 提交代码,都会触发 Travis CI 自动构建了。

webpack

# 发布到 npm

如何发布一个 npm 包

# Git 规范和 Changelog 生成

1. 良好的 Git commit 规范的优势

  • 加快 Code Review 的流程。

  • 根据 Git Commit 的元数据生成 Changelog。

  • 后续维护者可以知道 Feature 被修改的原因。

2. 实现技术方案

commitizen 和 validate-commit-msg 的具体使用方法

  1. 首先需要全局安装 commitizen
npm install -g commitizen
1
  1. 然后再初始化适配器 cz-conventional-changelog
// 使用 npm
commitizen init cz-conventional-changelog --save-dev --save-exact

// 使用 yarn
commitizen init cz-conventional-changelog --yarn --dev --exact
1
2
3
4
5
  1. 执行完以上命令之后,就会在 package.json 中看到生成了以下代码:
"config": {
  "commitizen": {
    "path": "./node_modules/cz-conventional-changelog"
  }
}
1
2
3
4
5

此时,就可以在提交代码的时候,使用 git cz 命令进行提交了,会有相关的提交规范提示。

  1. 接着再安装 validate-commit-msg 和 ghooks,并在 package.json 中加上以下代码:
"config": {
  "commitizen": {
    "path": "./node_modules/cz-conventional-changelog"
  },
  "ghooks": {
    "commit-msg": "validate-commit-msg"
  }
}
1
2
3
4
5
6
7
8
  1. 创建 .vcmrc 文件,用于配置 validate-commit-msg 的配置信息,如下:
{
  "types": [
    "feat",
    "fix",
    "docs",
    "style",
    "refactor",
    "perf",
    "test",
    "build",
    "ci",
    "chore",
    "revert"
  ],
  "scope": {
    "required": false,
    "allowed": ["*"],
    "validate": false,
    "multiple": false
  },
  "warnOnFail": false,
  "maxSubjectLength": 100,
  "subjectPattern": ".+",
  "subjectPatternErrorMsg": "subject does not match subject pattern!",
  "helpMessage": "提交信息不规范!请参考 README.md 中的代码提交信息规范或者使用 git cz 命令进行提交",
  "autoFix": false
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

这样配置好之后,validate-commit-msg 也就生效了,提交代码时,如果使用 git commit -m "xxx" 命令提交的信息不规范,是无法成功提交的。

  1. 如何在 commit 里使用 emoji

git commit 规范和如何在 commit 里使用 emoji (opens new window)

3. 本地开发阶段增加 pre-commit 钩子

  • 安装 husky

Husky (opens new window) 可以帮助我们简单直接地实现 git hooks。你们团队正在协作开发,并希望在整个团队中推行一套编码标准?没问题!有了 Husky,你就可以要求所有人在提交或推送到存储库之前自动完成 lint 并测试其代码。

npm i husky -D
1
  • 一个实现 husky hooks 的示例
{
  "husky": {
    "hooks": {
      "pre-commit": "npm lint",
      "pre-push": "npm test"
    }
  }
}
1
2
3
4
5
6
7
8

这里 pre-commit 的 hooks 会在你提交到存储库之前运行。在将代码推送到存储库之前,将运行 pre-push hook。

# 语义化版本规范格式

Semver(语义化版本号)扫盲 (opens new window)

# webpack 优化

# 速度分析

  1. 使用 speed-measure-webpack-plugin (opens new window),可以看到整个打包总耗时以及每个 loader 和插件的执行耗时。
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();

module.exports = smp.wrap({
  // ...
});
1
2
3
4
5
6
  1. 使用更高版本的 webpack 和 node.js 版本。webpack4 做了以下的优化:
  • V8 带来的优化(for of 替代 forEach、Map 和 Set 替代 Object、includes 替代 indexOf)。

  • 默认使用更快的 md4 hash 算法。

  • webpack AST 可以直接从 loader 传递给 AST,减少解析时间。

  • 使用字符串方法替代正则表达式。

# 体积分析

  1. 使用 webpack-bundle-analyzer (opens new window) 分析体积,构建完成后会在 8888 端口展示大小。

  2. 使用 webpack-chart (opens new window),需要先通过以下命令生成 stats.json 文件,可以在 package.json 中配一个执行脚本。生成之后再把这个 stats.json 文件上传到此处 (opens new window)进行分析。

{
  "scripts": {
    "chart": "webpack --mode development --profile --json > stats.json"
  }
}
1
2
3
4
5

注意

生成 stats.json 文件的过程要保证配置文件是 “干净” 的,生成完成后可以看下 stats.json 中有没有错误,如果有错误的话,是分析不出来的。

  1. 使用 webpack 官方推荐的 analyse (opens new window),这个工具也比较简单直观。还是需要先生成 stats.json 文件,再上传到网站上进行分析。这个工具有一个很大的好处就是,它可以分析展示出 node_modules 中所有包之间的依赖关系。

# 开发体验优化

1. 打包进度监控

使用 progress-bar-webpack-plugin (opens new window) 插件可以在打包构建过程中显示进度条。

2. 打包完成通知

使用 webpack-build-notifier (opens new window) 插件可以在打包完成后弹出消息提示框,提示框的样式根据系统的不同而有所不同。

3. 优化日志信息

使用 friendly-errors-webpack-plugin (opens new window) 插件可以优化 webpack 打包后输出的日志信息,显得不那么杂乱。

# 缩小构建目标

  1. 目的是尽可能的少构建模块,比如 babel-loader 不解析 node_modules。
{
  test: /\.js$/i,
  use: [
    'babel-loader?cacheDirectory=true',
    'thread-loader'
  ],
  exclude: /node_modules/
}
1
2
3
4
5
6
7
8
  1. 减小文件搜索范围
  • 优化 resolve.modules 配置(减少模块搜索层级)

  • 优化 resolve.mainFields 配置

  • 优化 resolve.extensions 配置

  • 合理使用 alias

{
  test: /\.js$/i,
  include: path.resolve('src'),
  use: [
    'babel-loader?cacheDirectory=true',
    'thread-loader'
  ],
  exclude: /node_modules/
}
1
2
3
4
5
6
7
8
9
resolve: {
  alias: {
    'react': path.resolve(__dirname, './node_modules/react/umd/react.production.min.js'),
    'react-dom': path.resolve(__dirname, './node_modules/react-dom/umd/react-dom.production.min.js')
  },
  modules: [path.resolve(__dirname, 'node_modules')],
  extensions: ['.js'],
  mainFields: ['main']
}
1
2
3
4
5
6
7
8
9

# JS tree shaking

1. 什么是 tree shaking?

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

2. JS 的 tree shaking

  • webpack3 中使用 uglifyjsWebpackPlugin (opens new window) 插件来实现。

  • webpack4 中 production 模式下默认就会开启 tree shaking。不过它只会对 ES6 模块语法做处理,不支持 CommonJS 语法。

  • 如果想更进一步的进行 tree shaking,可以使用 webpack-deep-scope-analysis-plugin (opens new window) 插件,这个插件包括了一个作用域分析器,可以分析一个模块里面的作用域,得到不同作用域之间变量的引用关系。当我们知道一个作用域是否会被使用,就可以因此而推断出这个作用域做引用的其他作用域是否也会被使用。这就是作用域分析器帮助消除无用代码的原理。

3. tree shaking 的原理

  • 要理解 tree shaking 的原理,必须先知道 DCE(Dead Code Elimination),DCE 有以下三种情况:

    • 代码不会被执行,不可达到

    • 代码执行的结果不会被用到

    • 代码只会影响死变量(只写不读)

  • 原理

    利用 ES6 模块的特点:

    (1)只能作为模块顶层的语句出现

    (2)import 的模块名只能是字符串常量

    (3)import binding 是 immutable

    代码擦除:

    (1)uglify 阶段删除无用代码

补充

lodash 提供了多种构建方式和模块方式,它也有专门以 ES6 语法导出函数的库:lodash-es (opens new window)

# CSS tree shaking

1. purifycss

const PurgecssPlugin = require("purgecss-webpack-plugin");

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

module.exports = {
  plugins: [
    new PurgecssPlugin({
      paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true })
    })
  ]
};
1
2
3
4
5
6
7
8
9
10
11
12
13

2. uncss

  • uncss (opens new window) 是一种可从样式表中删除未使用的 CSS 的工具。它可以跨多个文件工作,并支持注入 Javascript 的 CSS。

  • html 需要通过 jsdom 加载,所有的样式通过 postcss 解析,通过 document.querySelector 来识别在 html 文件里不存在的选择器。

# 多进程/多实例构建

资源并行解析的可选方案:

1. thread-loader

2. parallel-webpack

3. HappyPack

  • HappyPack (opens new window) 在 webpack4 之前用的比较多,现在这个库作者也不维护了。

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

  • 默认是开启 3 个线程。

const HappyPack = require("happypack");
const os = require("os");
//开辟一个线程池
const happyThreadPoll = HappyPack.ThreadPool({ size: os.cpus().length });
module.exports.plugins = [
  new HappyPack({
    id: "babel",
    threadPool: happyThreadPoll,
    loaders: [
      {
        loader: "babel-loader"
      }
    ]
  })
];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 多进程/多实例并行压缩

  1. 使用 webpack-parallel-uglify-plugin (opens new window) 插件。
plugins: [
  new ParallelUglifyPlugin({
    exclude: /\.min.js$/,
    workerCount: os.cpus().length,
    // uglifyJS: {
    // },
    uglifyES: {
      output: {
        beautify: false,
        comments: false
      },
      compress: {
        warnings: false,
        drop_console: true,
        collapse_vars: true
      }
    }
  })
];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  1. uglify-webpack-plugin (opens new window) 开启 parallel 参数。

  2. terser-webpack-plugin (opens new window) 开启 parallel 参数,推荐使用。

# 代码拆分和懒加载

使⽤ SplitChunksPlugin动态导⼊(import())来实现代码拆分,减少单个代码块的⼤⼩,提⾼构建效率。

# 利⽤外部模块(Externals)

将不经常更改的第三⽅库标记为外部(externals (opens new window)),这样可以避免它们在每次构建时被重新打包。

# 打包第三方库为单独文件

如果想要将第三方库打包成单独的文件,有以下三种方式:

  • 入口配置:entry 配置多入口
module.exports = {
  entry: {
    index: "./src/index.js",
    demo: "./src/demo.js",
    jquery: "jquery"
  }
};
1
2
3
4
5
6
7

全局使用第三方库

如果想全局使用 jquery,可以直接使用 webpack 自带的 ProvidePlugin

module.exports = {
  plugins: [
    new webpack.ProvidePlugin({
      $: "jquery",
      jquery: "jquery"
    })
  ]
};
1
2
3
4
5
6
7
8

这样配置好之后就可以全局使用了,不需要手动导入。

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

分包的方式除了上面的提取公共资源提到的方式之外,还有其它方式。

使用 DLLPlugin (opens new window) 进行分包,DllReferencePlugin 对 mainfest.json 进行引用。

  1. 使用 DLLPlugin 进行分包
  • 新建 webpack.dll.js 文件。
const path = require("path");
const webpack = require("webpack");

module.exports = {
  entry: {
    library: ["react", "react-dom"]
  },
  output: {
    filename: "[name]_[hash].dll.js",
    path: path.join(__dirname, "build/library"),
    library: "[name]"
  },
  plugins: [
    new webpack.DllPlugin({
      name: "[name]_[hash]",
      path: path.join(__dirname, "build/library/[name].json")
    })
  ]
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  1. 使用 DllReferencePlugin 引用 library.json。
new webpack.DllReferencePlugin({
  minifest: require("./build/library/library.json")
});
1
2
3

# 充分利用缓存提升二次构建速度

  1. 缓存方案
{
  test: /\.js$/i,
  use: [
    'babel-loader?cacheDirectory=true',
    'thread-loader'
    // 'eslint-loader'
  ],
  exclude: /node_modules/ // 不加这句的话生产模式下打包会报错
}
1
2
3
4
5
6
7
8
9
optimization: {
  minimize: true,
  minimizer: [
    new TerserPlugin({
      parallel: true,
      cache: true
    }),
  ],
}
1
2
3
4
5
6
7
8
9
const HardSourceWebpackPlugin = require("hard-source-webpack-plugin");

module.exports = {
  plugins: [new HardSourceWebpackPlugin()]
};
1
2
3
4
5

开启了缓存之后,在 node_modules 文件夹下会生成一个 .cache 文件夹,里面存放的就是缓存文件。

webpack

# 图片压缩

  1. 基于 Node 库的 imagemin (opens new window)

  2. 使用 image-webpack-loader (opens new window)

  3. imagemin 的优点

  • 有很多定制选项

  • 可以引入更多第三方优化插件,如 pngquant

  • 可以处理多种图片格式

注意

要先处理图片,然后再进行压缩。因此 image-webpack-loader 的配置要放在 file-loader 的配置前面,因为 loader 的执行是从后往前的。

{
  test: /.(png|jpg|gif|jpeg)$/,
  use: [
    {
      loader: 'image-webpack-loader',
      options: {
        mozjpeg: {
          progressive: true,
        },
        // optipng.enabled: false will disable optipng
        optipng: {
          enabled: false,
        },
        pngquant: {
          quality: [0.65, 0.90],
          speed: 4
        },
        gifsicle: {
          interlaced: false,
        },
        // the webp option will enable WEBP
        webp: {
          quality: 75
        }
      }
    }, {
      loader: 'file-loader',
      options: {
        name: '[name]_[hash:8].[ext]'
      }
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 构建体积优化:动态 Polyfill

注意

永远不要使用 babel-polyfill,这个包太大太占资源了。

  1. 使用 polyfill service,只给用户返回需要的 polyfill。缺点是部分国内奇葩浏览器 UA 可能无法识别,但是可以降级返回所需全部 polyfill。

  2. polyfill service 原理:识别 User Agent,下发不同的 polyfill。

  3. 如何使用动态的 polyfill service?

  1. @babel/preset-env (opens new window) 中通过配置 useBuiltIns: 'usage' 参数来动态加载 polyfill。

# webpack 启动过程分析

# webpack 入口文件

  • 当运行 npm run dev 或者 npm run build 命令后,npm 会让命令行工具进入 node_modules/.bin 目录查找是否存在 webpack.sh 或者 webpack.cmd 文件,如果存在,就执行,不存在,就报错。

  • 实际的入口文件是:node_modules/webpack/bin/webpack.js

# 入口文件分析

process.exitCode = 0;                                     // 1、正常执行返回

const runCommand = (command, args) => {...};              // 2、运行某个命令

const isInstalled = packageName => {...};                 // 3、判断某个包是否安装

const CLIs = [...];                                       // 4、webpack 可用的 cli 有两个:webpack-cli 和 webpack-command

const installedClis = CLIs.filter(cli => cli.installed);  // 5、判断安装了几个 cli

if (installedClis.length === 0) {                         // 6、根据安装的 cli 的数量做不同的处理
} else if (installedClis.length === 1) {
} else {}
1
2
3
4
5
6
7
8
9
10
11
12
13

# webpack-cli 源码分析

# webpack-cli 做了哪些事情?

  • 引入 yargs (opens new window),对命令行进行定制。

  • 分析命令行参数,对各个参数进行转换,组成编译配置项。

  • 引用 webpack,根据配置项进行编译和构建。

# NON_COMPILATION_CMD

NON_COMPILATION_CMD 分析出不需要编译的命令。

# NON_COMPILATION_ARGS

webpack-cli 的 constants.js 文件中的 NON_COMPILATION_ARGS 提供了不需要编译的命令。

// v3.3.12
const NON_COMPILATION_ARGS = [
  "init", // 创建一份 webpack 配置文件
  "migrate", // 进行 webpack 版本迁移
  "serve", // 运行 webpack-serve
  "generate-loader", // 生成 webpack loader 代码
  "generate-plugin", // 生成 webpack plugin 代码
  "info" // 返回与本地环境相关的一些信息
];
1
2
3
4
5
6
7
8
9

# yargs

命令行工具包 yargs (opens new window)

  • 提供命令和分组参数

  • 动态生成 help 帮助信息

  • 参数分组(config/config-args.js),将命令划分为 9 类:

Config options:配置相关参数(文件名称、运行环境等)

Basic options:基础参数(entry 设置、debug 模式设置、watch 监听设置、devtool 设置)

Module options:模块参数,给 loader 设置扩展

Output options:输出参数(输出路径、输出文件名称)

Advanced options:高级用法(记录设置、缓存设置、监听频率、bail 等)

Resolving options:解析参数(alias 和解析后的文件后缀设置)

Optimizing options:优化参数

Stats options:统计参数

options:通用参数(帮助命令、版本信息等)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# options 和 compiler

  • 最重要的两个对象是 optionscompiler

  • convert-argv.js 文件就是处理返回 options 对象的。

# webpack-cli 的执行结果

  • 对配置文件和命令行参数进行转换最终生成配置选项参数 options。

  • 最终会根据配置参数实例化 webpack 对象,然后执行构建流程。

# webpack 构建流程分析

webpack

# webpack 的整体运行流程

webpack 本质上就是一个 JS Module Bundler,用于将多个代码模块进行打包。bundler 从一个构建入口出发,解析代码,分析出代码模块依赖关系,然后将依赖的代码模块组合在一起,在 JavaScript bundler 中,还需要提供一些胶水代码让多个代码模块可以协同工作,相互引用。

// entry.js
import { bar } from "./bar.js"; // 依赖 ./bar.js 模块

// bar.js
const foo = require("./foo.js"); // 依赖 ./foo.js 模块

// 递归下去,直至没有更多的依赖模块,最终形成一颗模块依赖树。
1
2
3
4
5
6
7

分析出依赖关系后,webpack 会利用 JavaScript Function 的特性提供一些代码来将各个模块整合到一起,即是将每一个模块包装成一个 JS Function,提供一个引用依赖模块的方法,如下面例子中的 __webpack__require__,这样做,既可以避免变量相互干扰,又能够有效控制执行顺序。

// 分别将各个依赖模块的代码用 modules 的方式组织起来打包成一个文件
================================entry======================================
================================moudles======================================
// entry.js
modules['./entry.js'] = function() {
  const { bar } = __webpack__require__('./bar.js')
}

// bar.js
modules['./bar.js'] = function() {
  const foo = __webpack__require__('./foo.js')
};

// foo.js
modules['./foo.js'] = function() {
  // ...
}

================================output===========================
// 已经执行的代码模块结果会保存在这里
const installedModules = {}
function __webpack__require__(id) {
  // 如果 installedModules 中有就直接获取
  // 没有的话从 modules 中获取 function 然后执行,
  //将结果缓存在 installedModules 中然后返回结果
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

webpack

Compiler(Tapable) -> Compilation(Tapable) -> Chunk -> Module -> runLoaders -> Dependency(AST) -> Template
1

1. Compiler

webpack 的运行入口,compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用,可以使用它来访问 webpack 的主环境。

2. Compilation

Compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键步骤的回调,以供插件做自定义处理时选择使用。

3. Chunk

即用于表示 chunk 的类,对于构建时需要的 chunk 对象由 Compilation 创建后保存管理(webpack 中最核心的负责编译的 Compiler 和负责创建 bundles 的 Compilation 都是 Tapable 的实例)。一般一个入口对应一个 chunk。

4. Module

用于表示代码模块的基础类,衍生出很多子类用于处理不同的情况。关于代码模块的所有信息都会存在 Module 实例中,例如 dependencies 记录代码模块的依赖等。

当一个 Module 实例被创建后,比较重要的一步是执行 compilation.buildModule 这个方法,它会调用 Module 实例的 build 方法来创建 Module 实例需要的一些东西,然后调用自身的 runLoaders 方法。runLoaders:loader-runner,执行对应的 loaders,将代码源码内容一一交由配置中指定的 loader 处理后,再把处理的结果保存起来。

5. Parser

其中相对复杂的一个部分,基于 acorn (opens new window) 来分析 AST 语法树,解析出代码模块的依赖

6. Dependency

解析时用于保存代码模块对应的依赖使用的对象。Module 实例的 build 方法在执行完对应的 loader 时,处理完模块代码自身的转换后,继续调用 Parser 的实例来解析自身依赖的模块,解析后的结果存放在 module.dependencies 中,首先保存的是依赖的路径,后续会经由 compilation.processModuleDependencies 方法,再来处理各个依赖模块,递归地去建立整个依赖。

7. Template

生成最终代码要使用到的代码模板,像上述提到的胶水代码就是用对应的 Template 来生成。

Template 基础类:lib/Template.js (opens new window)

常用的主要 Template 类:lib/MainTemplate.js (opens new window)

# webpack 的编译流程

webpack 的编译都是按照下面的钩子调用顺序执行的

webpack

# WebpackOptionsApply

webpack.js 中的 WebpackOptionsApply 将所有的配置 options 参数转换成 webpack 内部插件,使用默认插件列表,比如:

  • output.library -> LibraryTemplatePlugin

  • externals -> ExternalsPlugin

  • devtool -> EvalDevtoolModulePlugin, SourceMapDevToolPlugin

  • AMDPlugin, CommonJsPlugin

  • RemoveEmptyChunksPlugin

# Compiler Hooks

  • 流程相关

    run、beforeRun、beforeCompile、afterCompile、make、emit、afterEmit、done

  • 监听相关

    watchRun、watchClose

# ModuleFactory

  • Compilation.js 文件中有两类工厂函数,分别是:NormalModuleFactoryContextModuleFactory

# Module

在 webpack 中有以下几种模块类型,每种模块类型对应一个 js 文件。

  • NormalModule(普通模块)

  • ContextModule

./src/a
./src/b
1
2
  • ExternalModule
module.exports = {};
1
  • DelegatedModule
manifest
1
  • MultiModule
entry: ['a', 'b']
1

# Compilation Hooks

  • 模块相关

    buildModule、failedModule、succeedModule

  • 资源生成相关

    moduleAsset、chunkAsset

  • 优化和 seal 相关

    seal、afterSeal、optimize、optimizeModules、optimizeModulesBasic、optimizeModulesAdvanced、afterOptimizeModules、afterOptimizeChunks、afterOptimizeTree、optimizeChunkModules、optimizeChunkModulesBasic、optimizeChunkModulesAdvanced、afterOptimizeChunkModules、optimizeModuleOrder、optimizeChunkOrder、beforeChunkIds、beforeModuleId、afterOptimizeModuleIds、afterOptimizeChunkIds、optimizeModuleIds、optimizeChunkIds、beforeHash、afterHash

# 文件生成

在 Compiler.js 文件中的 emit 阶段完成。

this.hooks.emit.callAsync(compilation, (err) => {
  if (err) return callback(err);
  outputPath = compilation.getPath(this.outputPath);
  this.outputFileSystem.mkdirp(outputPath, emitFiles);
});
1
2
3
4
5

# webpack 打包后的代码解读

  • webpack 编译打包后的代码其实就是一个闭包,参数就是键值对形式的模块,使用的是 CommonJS 规范。

  • 每个模块参数里边都是用 eval 去包裹源代码。

  • 每个 require 都会被转换成 __webpack_require__

  • 闭包最后得到的就是一个函数体,并且会执行该函数体得到最终的结果。

(function(modules) {
  // webpack 启动文件
  // 模块缓存
  var installedModules = {};

  // 注册 __webpack_require__ 函数
  function __webpack_require__(moduleId) {
    // 如果模块已经安装过,有缓存,就直接取
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // 如果模块没有缓存过,就把模块导出的资源注入到缓存中
    var module = (installedModules[moduleId] = {
      i: moduleId, // 模块名字,'./src/index.js'、'./src/test.js'
      l: false, // 标记模块是否加载过
      exports: {} // 模块的值
    });

    // 核心代码
    // 执行每个模块的函数,也就是如下所示的函数
    // function(module, __webpack_exports__, __webpack_require__) {}
    // 大部分类库都喜欢用 call 或 apply 来执行函数,一是方便修正 this 指向,二是方便传参
    modules[moduleId].call(
      module.exports,
      module,
      module.exports,
      __webpack_require__
    );

    // 如果模块已经加载过,标记为 true
    module.l = true;

    // Return the exports of the module
    return module.exports;
  }

  // Load entry module and return exports
  // 加载主入口模块
  return __webpack_require__((__webpack_require__.s = "./src/index.js"));
})(
  /************************************************************************/
  {
    "./src/index.js": function(
      module,
      __webpack_exports__,
      __webpack_require__
    ) {
      "use strict";
      eval(`
      __webpack_require__.r(__webpack_exports__);
      var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/test.js");
      Object(_test__WEBPACK_IMPORTED_MODULE_0__[default])();
      //# sourceURL=webpack:///./src/index.js?
    `);
    },

    "./src/test.js": function(
      module,
      __webpack_exports__,
      __webpack_require__
    ) {
      "use strict";
      eval(`
      __webpack_require__.r(__webpack_exports__);
      const result = () => {
        console.log('bbb');
      }
      __webpack_exports__[default] = (result);
      //# sourceURL=webpack:///./src/test.js?
    `);
    }
  }
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

# 基于 AST 实现简单的 webpack

1. 可以将 ES6 语法转换成 ES5 语法

2. 可以分析模块之间的依赖关系

3. 生成的 JS 文件可以在浏览器中运行

// src/index.js
import test from "./test.js";
console.log(test);
console.log("基于 ast 实现简单的 webpack");
1
2
3
4
// src/test.js
const data = "哈哈哈😄";
export default data;
1
2
3
// 基于 ATS 实现的简单 webpack
const fs = require("fs");
const parser = require("babylon"); // 用于生成 ast 树
const traverse = require("@babel/traverse").default; // 用于遍历 ast 树
const MagicString = require("magic-string"); // 魔法字符串,用于方便快捷地操作字符串
const { join } = require("path");
const ejs = require("ejs");

const entry = "./src/index.js";
let dependencies = []; // 依赖

function parse(filename) {
  let dependArray = [];
  const content = fs.readFileSync(filename, "utf-8");
  const code = new MagicString(content);
  // 将文件内容转成 ast 树
  const ast = parser.parse(content, {
    sourceType: "module"
  });
  // console.log(ast);
  traverse(ast, {
    ExportDeclaration({ node }) {
      // 带有 export 声明的节点,进行替换
      const { start, end, declaration } = node;
      code.overwrite(
        start,
        end,
        `__webpack_exports__["default"] = ${declaration.name};`
      );
    },
    ImportDeclaration({ node }) {
      // 带有 import 声明的节点,进行替换
      const { start, end, specifiers, source } = node;
      const fullFilePath = "./src/" + join(source.value); // 完整文件路径
      // console.log(node);
      code.overwrite(
        start,
        end,
        `var ${specifiers[0].local.name} = __webpack_require__("${fullFilePath}").default;`
      );
      dependArray.push(fullFilePath);
    }
  });
  // console.log(code.toString());
  const _code = code.toString();
  dependencies.push({
    filename,
    _code
  });
  return dependArray;
}

const arr = parse(entry);
for (const item of arr) {
  parse(item);
}
// 模版
let template = `
  (function(modules) {
    var installedModules = {};

    function __webpack_require__(moduleId) {
      if(installedModules[moduleId]) {
        return installedModules[moduleId].exports;
      }

      var module = installedModules[moduleId] = {
        i: moduleId,
        l: false,
        exports: {}
      }

      modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
      module.l = true;

      return module.exports;
    }

    return __webpack_require__(__webpack_require__.s = "./src/index.js");
  })({
    <% for (var i = 0; i < dependencies.length; i++) { %>
      "<%- dependencies[i]["filename"] %>":(function(module, __webpack_exports__, __webpack_require__) {
        <%- dependencies[i]["_code"] %>
      }),
    <% } %>
  })
`;

const result = ejs.render(template, {
  dependencies
});
fs.writeFileSync("./dist/main.js", result); // 将最终生成的代码输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

最终生成的代码在浏览器中运行的效果如下:

webpack

# loader 解读

# 什么是 loader

  • loader 是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中。

  • loader 本身仅仅只是一个函数,接收模块代码的内容,然后返回代码内容转化后的结果。

module.exports = function(source) {
  return source;
};
1
2
3

# loader 的执行顺序

  • 多个 loader 串行执行,顺序从后到前。

  • 最后的 loader 最早调用,传入原始的资源内容(可能是代码,也可能是二进制文件,用 buffer 处理)第一个 loader 最后调用,期望返回是 JS 代码和 sourcemap 对象(可选)。中间 的 loader 执行时,传入的是上一个 loader 执行的结果。

为什么 loader 是从后往前执行的?

函数组合有两种方式,loader 执行顺序之所以是从后往前,是因为 webpack 采用的是 compose 的组合方式。

  • Unix 中的 pipline。

  • compose(webpack 采用的)。

compose = (f, g) => (...args) => f(g(...args))
1

# loader-runner

  • loader-runner (opens new window) 允许在不安装 webpack 的情况下运行 loader。

  • 作用

    作为 webpack 的依赖,webpack 中使用它执行 loader。

    进行 loader 的开发和调试。

  • 使用

    import { runLoaders } from "loader-runner";
    
    runLoaders(
      {
        resource: "/abs/path/to/file.txt?query", // 资源的绝对路径,可以增加查询字符串
        loaders: ["/abs/path/to/loader.js?query"], // loader 的绝对路径,可以增加查询字符串
        context: { minimize: true }, // 基础上下文之外的额外 loader 上下文
        readResource: fs.readFile.bind(fs) // 读取资源的函数
      },
      function(err, result) {}
    );
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

# loader 的参数获取

  • loader 传参可以通过 options 传递。
runLoaders({
  loaders: [
    {
      loader: path.join(__dirname, "./src/raw-loader.js"),
      options: {
        name: "test loader"
      }
    }
  ]
});
1
2
3
4
5
6
7
8
9
10
  • 通过 loader-utils (opens new window) 的 getOptions 方法获取。编写自己的 loader 时就需要用到 loader-utils,调用 loaderUtils.getOptions(this) 拿到 webpack 的配置参数,然后进行处理。
const loaderUtils = require("loader-utils");

module.exports = function(content) {
  const { name } = loaderUtils.getOptions(this);
};
1
2
3
4
5

# loader 异常处理

开发一个同步的 loader 可以通过以下两种方式抛出异常。

  • loader 内直接通过 throw 抛出。
throw new Error("loader error");
1
  • 通过 this.callback 传递错误。
this.callback(
  err: Error | null,
  content: string | Buffer,
  sourceMap?: SourceMap,
  meta?: any
)
1
2
3
4
5
6

还可以通过 this.callback 传递多个值。

this.callback(null, str, 2, 3, 4);
1

# loader 的异步处理

  • 有一些 loader 在执行过程中可能依赖于外部 I/O 的结果,导致它必须使用异步的方式来处理,这时需要在 loader 执行时使用 this.async 来标识该 loader 是异步处理的,然后使用 this.callback 来返回 loader 处理结果。

  • 通过 this.async 来返回一个异步函数。第一个参数是 Error,第二个参数是处理的结果。

const fs = require("fs");
const path = require("path");
module.exports = function(source) {
  const callback = this.async();
  fs.readFile(path.join(__dirname, "./async.txt"), "utf8", (err, data) => {
    if (err) {
      callback(err, "");
    }
    callback(null, data);
  });
};
1
2
3
4
5
6
7
8
9
10
11

# 在 loader 中使用缓存

  • webpack 中默认开启 loader 缓存,可以通过 this.cacheloader(false) 关掉缓存。

  • 缓存条件:loader 的结果在相同的输入下有确定的输出。有依赖的 loader 无法使用缓存。

# loader 如何进行文件输出

// loader 上下文,文件占位符,文件内容
const interpolatedName = loaderUtils.interpolateName(
  loaderContext,
  name,
  options
);
1
2
3
4
5
6
const loaderUtils = require("loader-utils");

module.exports = function(source) {
  console.log("Loader a is excuted!");
  const filename = loaderUtils.interpolateName(this, "[name].[ext]", source);
  console.log(filename);
  this.emitFile(filename, source);
  return source;
};
1
2
3
4
5
6
7
8
9

# style-loader 的实现原理

style-loader 的实现原理主要涉及两个方面:样式加载和样式注入

  • 样式加载

当 Webpack 解析到 import 或 require 语句引入 CSS 文件时,style-loader 开始发挥作用。

style-loader 将 CSS 文件中的样式代码提取出来,并创建一个 style 元素,将样式代码放入该元素的 style 属性中。

  • 样式注入

生成的 style 元素会被插入到 HTML 文档的 head 元素中,或者在指定的容器中,具体取决于配置。

这样,样式就会被应用到页面上。

// 简单的 style-loader 实现

// 将 CSS 样式插入到页面中的 head 元素
function insertStyle(style) {
  const styleElement = document.createElement('style');
  styleElement.type = 'text/css';
  styleElement.appendChild(document.createTextNode(style));
  document.head.appendChild(styleElement);
}

// 将样式字符串转为 JavaScript 模块
function transform(style) {
  return `
    const style = ${JSON.stringify(style)};
    insertStyle(style);
  `;
}

// 注册为 Webpack loader
module.exports = function(style) {
  const transformedStyle = transform(style);
  return transformedStyle;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# plugin 解读

plugin 的功能更强大,它的功能更加丰富,loader 不能做的事它都能做。

# plugin 的运行环境

  • 插件没有像 loader 那样的独立运行环境。

  • 只能在 webpack 里面运行。

# plugin 的基本结构和使用

  • 基本结构

plugin 的实现是一个类,类中最重要的方法就是 apply,该方法在 webpack compiler 安装插件时会被调用一次,apply 接收 webpack compiler 对象实例的引用,我们可以在 compiler 对象实例上注册各种事件钩子函数,来影响 webpack 的所有构建流程,以便完成更多其它的构建任务。

class MyPlugin {
  // 插件名称
  apply(compiler) {
    // 插件上的 apply 方法
    compiler.hooks.done.tap("My Plugin", () => {
      // 插件的 hooks
      console.log("Hello World"); // 插件处理逻辑
    });
  }
}
module.exports = MyPlugin;
1
2
3
4
5
6
7
8
9
10
11
  • 使用
plugins: [new MyPlugin()];
1

# plugin 的实现机制

webpack 实现插件机制的大体方式是:

1. 创建 —— webpack 在其内部对象上创建各种钩子;

2. 注册 —— 插件将自己的方法注册到对应钩子上,交给 webpack;

3. 调用 —— webpack 编译过程中,会适时地触发相应钩子,因此也就触发了插件的方法。

const pluginName = "ConsoleLogOnBuildWebpackPlugin";

class ConsoleLogOnBuildWebpackPlugin {
  apply(compiler) {
    // 在 compiler 的 hooks 中注册一个方法,当执行到该阶段时会调用
    compiler.hooks.run.tap(pluginName, (compilation) => {
      console.log("The webpack build process is starting!!!");
    });
  }
}
1
2
3
4
5
6
7
8
9
10
plugins: [new ConsoleLogOnBuildWebpackPlugin()];
1

# Tapable 插件架构和 Hooks 设计

  • webpack 的本质上可以理解成是一种基于事件流的编程范例,一系列的插件运行。

  • webpack 利用了 tapable (opens new window) 这个库来协助实现对于整个构建流程各个步骤的控制。 tapable 定义了主要构建流程后,使用 tapable 这个库添加了各种各样的钩子方法来将 webpack 扩展至功能十分丰富,这就是 plugin 的机制。

  • CompilerCompilation 这两个对象都是继承自 Tapable。在 lib 文件夹里分别有 Compiler.js 和 Compilation.js 文件。它们向外暴露了很多个 hook 可供调用。

1. Tapable 是什么?

  • Tapable (opens new window) 是一个类似于 Node.js 的 EventEmitter 的库,主要是控制钩子函数的发布与订阅,控制着 webpack 的插件系统。

  • webpack 核心使用 Tapable 来实现插件(plugins)的 binding 和 applying。Tapable 是一个用于事件发 布订阅执行的插件架构。Tapable 就是 webpack 用来创建钩子的库。

2. Hooks 设计

  • Tapable 暴露了很多 Hook(钩子)类,为插件提供挂载的钩子。

webpack

const {
  SyncHook, // 同步钩子
  SyncBailHook, // 同步熔断钩子
  SyncWaterfallHook, // 同步流水钩子
  SyncLoopHook, // 同步循环钩子
  AsyncParallelHook, // 异步并发钩子
  AsyncParallelBailHook, // 异步并发熔断钩子
  AsyncSeriesHook, // 异步串行钩子
  AsyncSeriesBailHook, // 异步串行熔断钩子
  AsyncSeriesWaterfallHook // 异步串行流水钩子
} = require("tapable");
1
2
3
4
5
6
7
8
9
10
11

事件钩子会有不同的类型 SyncBailHook,AsyncSeriesHook,SyncHook 等。

如果是异步的事件钩子,那么可以使用 tapPromise 或者 tapAsync 来注册事件函数,tapPromise 要求方法返回 Promise 以便处理异步,而 tapAsync 则是需要用 callback 来返回结果。

compiler.hooks.done.tapPromise('PluginName', (stats) => {
  return new Promise((resolve, reject) => {
    // 处理 promise 的返回结果 reject(err) : resolve()
  })
})

compiler.hooks.done.tapAsync('PluginName', (stats, callback) => {
  callback(err))
})
1
2
3
4
5
6
7
8
9

除了同步和异步的,名称带有 parallel 的,注册的事件函数会并行调用;名称带有 bail 的,注册的事件函数会被顺序调用,直至一个处理方法有返回值名称带有 waterfall 的;每个注册的事件函数,会将上一个方法的返回结果作为输入参数。有一些类型是可以结合到一起的,如 AsyncParallelBailHook,这样它就具备了更加多样化的特性。

  • Tapable Hooks 类型
type description
Hook 所有钩子的后缀
Waterfall 同步方法,但是它会传值给下一个函数
Bail 熔断:当函数有任何返回值,就会在当前执行函数停止
Loop 监听函数返回 true 表示继续循环,返回 undefined 表示结束循环
Sync 同步方法
AsyncSeries 异步串行钩子
AsyncParallel 异步并行执行钩子
  • 钩子的绑定与执行
Async* Sync*
绑定:tapAsync/tapPromise/tap 绑定:tap
执行:callAsync/promise 执行:call
  • Tapable 的使用
const { SyncHook } = require("tapable");
const hook = new SyncHook(["arg1", "arg2", "arg3"]);

hook.tap("hook1", (arg1, arg2, arg3) => {
  console.log(arg1, arg2, arg3);
});
hook.call(1, 2, 3);
1
2
3
4
5
6
7

3. Tapable 是如何与 webpack 结合起来的?

// webpack.js
let compiler;
if (Array.isArray(options)) {
  compiler = new MultiCompiler(
    Array.from(options).map((options) => webpack(options))
  );
} else if (typeof options === "object") {
  options = new WebpackOptionsDefaulter().process(options);

  compiler = new Compiler(options.context);
  compiler.options = options;
  new NodeEnvironmentPlugin({
    infrastructureLogging: options.infrastructureLogging
  }).apply(compiler);
  if (options.plugins && Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
      if (typeof plugin === "function") {
        plugin.call(compiler, compiler);
      } else {
        plugin.apply(compiler);
      }
    }
  }
  compiler.hooks.environment.call();
  compiler.hooks.afterEnvironment.call();
  compiler.options = new WebpackOptionsApply().process(options, compiler);
} else {
  throw new Error("Invalid argument: options");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# plugin 的参数获取

  • 插件传参
plugins: [
  new MyPlugin({
    name: "my plugin"
  })
];
1
2
3
4
5
  • 通过构造函数的 options 来获取参数
class MyPlugin {
  constructor(options) {
    this.options = options;
  }
  apply(compiler) {
    console.log("My plugin options", this.options);
  }
}

module.exports = MyPlugin;
1
2
3
4
5
6
7
8
9
10

# plugin 的错误处理

  • 在参数校验阶段可以直接通过 throw 的方式抛出。
throw new Error("Error message");
1
  • 通过 compilation 对象的 warnings 和 errors 接收。
compilation.warnings.push("warning");
compilation.errors.push("error");
1
2

# 处理插件的文件写入

  • Compilation 上的 assets 可以用于文件写入,可以将 zip 资源包设置到 compilation.assets 对象上。

  • 文件写入需要使用 webpack-sources (opens new window)

# 插件扩展:编写插件的插件

# 实现一个压缩资源为 zip 包的插件

// zip-plugin.js
const path = require("path");
const RawSource = require("webpack-sources").RawSource;
const JSZip = require("jszip");
const zip = new JSZip();

class ZipPlugin {
  constructor(options) {
    this.options = options;
  }
  apply(compiler) {
    // 异步钩子
    compiler.hooks.emit.tapAsync("ZipPlugin", (compilation, callback) => {
      const folder = zip.folder(this.options.filename);

      for (let filename in compilation.assets) {
        const source = compilation.assets[filename].source();
        folder.file(filename, source);
      }

      zip
        .generateAsync({
          type: "nodebuffer"
        })
        .then((content) => {
          // 获取的是绝对路径
          const outputPath = path.join(
            compilation.options.output.path,
            this.options.filename + ".zip"
          );
          // 获取相对路径
          const relativeOutputPath = path.relative(
            compilation.options.output.path,
            outputPath
          );

          compilation.assets[relativeOutputPath] = new RawSource(content);

          callback();
        });
    });
  }
}

module.exports = ZipPlugin;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// webpack.config.js
const path = require("path");
const ZipPlugin = require("./plugins/zip-plugin");

module.exports = {
  mode: "development",
  entry: "./src/index.js",
  output: {
    filename: "main.js",
    path: path.join(__dirname, "dist")
  },
  plugins: [
    new ZipPlugin({
      filename: "source"
    })
  ]
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# Tree-shaking 原理

Tree-shaking 是⼀种通过删除未引⽤代码(即 “死代码”)来减⼩最终打包⽂件体积的技术。它的工作原理分为以下几个关键步骤:

# 1. ES6 模块语法

  • 静态结构

Tree-shaking 的有效性很⼤程度上依赖于 ES6 模块的静态结构特性。这意味着模块的导⼊和导出在⽂件的编译阶段就已经确定,与运⾏时⽆关。

# 2. 识别 “死代码”

  • 分析依赖树

在构建过程中,打包⼯具分析应⽤的依赖树,识别出所有导⼊的模块。

  • 标记未使⽤的导出

打包⼯具进⼀步检查每个模块的导出,确定哪些导出没有在其他地⽅被引⽤。这些未被引⽤的导出就是候选的 “死代码”。

# 3. 删除未使⽤代码

  • 删除过程

在最终的打包⽂件中,这些未被引⽤的导出(即 “死代码”)会被删除。这个过程就是所谓的 Tree-shaking。

# 4. 压缩和优化

  • 进⼀步压缩

通常在 Tree-shaking 之后,还会进⾏代码压缩和优化,⽐如通过 UglifyJS 或 Terser 来去除未使⽤的函数和变量。

# 5. 侧⾯效应(Side Effects)

  • 副作⽤注意

在 JavaScript 中,某些操作可能有副作⽤(side effects),即即使没有明显引⽤,也可能影响应⽤的状态。因此,打包⼯具在进⾏ Tree-shaking 时需要考虑代码的副作⽤。

  • sideEffects 属性

在 package.json 中,可以通过 sideEffects 属性指明模块⽂件是否包含副作⽤,以帮助打包⼯具更准确地进⾏ Tree-shaking。

# 6. 注意事项

  • 模块格式重要

仅 ES6 模块格式(即 import / export )⽀持 Tree-shaking。CommonJS 模块由于其动态性质,通常不适⽤于 Tree-shaking。

  • 配置正确性

为了使 Tree-shaking ⽣效,需要确保打包⼯具和 Babel(如果使⽤)的配置正确。

Tree-shaking 是⼀种有效的优化⼿段,通过去除未使⽤的代码来减少打包⽂件的体积,提⾼应⽤的加载和执⾏效率。正确地使⽤和配置 ES6 模块以及理解代码的副作⽤对实现有效的 Tree-shaking ⾄关重要。

# webpack 热更新原理

Webpack 的热更新(Hot Module Replacement,简称 HMR)是⼀个⾮常强⼤的功能,允许在运⾏时更新、添加或删除模块,⽽⽆需进⾏完整的⻚⾯刷新。

# HMR 运⾏机制

  • Webpack Dev Server

这是⼀个⼩型的 Node.js Express 服务器,它使⽤ Webpack 进⾏构建,并提供了⼀个 WebSocket 服务⽤于实现 HMR。

  • WebSocket 通信

当源代码发⽣变化时,Webpack Dev Server 会通过 WebSocket 向客户端发送更新消息。

  • Manifest ⽂件

Webpack 编译过程中会⽣成⼀个 manifest ⽂件,它描述了所有模块之间的依赖关系。当进⾏模块更新时,Webpack 使⽤这个 manifest 来找出哪些模块需要被更新。

# HMR 流程

1. ⽂件更改监测

当源代码⽂件发⽣更改时,Webpack Dev Server 会重新编译那些更改了的模块。

2. ⽣成更新⽂件

Webpack 为更改的模块⽣成新的模块⽂件,并且⽣成⼀个 “热更新 chunk”(hot update chunk),这是⼀个包含了新模块版本的 JSON ⽂件。

3. 通知客户端

通过建⽴的 WebSocket 连接,Webpack Dev Server 通知客户端有模块发⽣了更改。

4. 下载更新

客户端(浏览器)收到更新通知后,会请求并下载更新的模块⽂件和热更新 chunk。

5. 更新模块

⼀旦新的模块被下载,HMR 运⾏时会根据 manifest ⽂件的信息进⾏模块替换。旧的模块会被新的模块替换掉,⽽⻚⾯不会进⾏完整的刷新。

# 模块热替换处理

  • 模块接⼝更新

为了使模块能够被热替换,开发者需要在模块代码中实现 HMR 接⼝。通常这意味着开发者需要指定当模块被更新时如何处理这种变化。

  • 状态保持

理想情况下,模块的状态在热更新过程中可以被保留,这通常需要开发者编写⼀些额外的代码来管理状态。

# 容错和降级

  • 失败处理

如果某个模块⽆法被热更新(⽐如缺少必要的 HMR 接⼝实现),Webpack HMR 会进⾏容错处理。这可能意味着进⾏⻚⾯全刷新或者在控制台输出错误。

# webpack5 新特性

# 内置清除输出目录

之前的版本经常需要一个叫 clean-webpack-plugin 的插件来帮助我们清除上次构建的 dist 产物,现在我们只要一个参数配置就可以搞定:

module.exports = {
  output: {
    clean: true
  }
};
1
2
3
4
5

# 持久性缓存

webpack5 会缓存生成的模块和 chunk,来改善构建速度。它追踪了每个模块的依赖,并创建了文件快照,与真实的文件系统进行对比,当发生差异时,触发对应的模块重新构建。

默认开启缓存,有两种类型:

module.exports = {
  cache: {
    type: "memory | filesystem"
  }
};
1
2
3
4
5

默认配置是 memory,filesystem 是缓存到本地文件系统,默认的缓存目录是 node_modules/.cache/webpack,当然也可以自己通过 cacheDirectory 属性配置。

# 资源模块

webpack5 内置了处理资源的模块 Asset Modules (opens new window),有 4 种类型。

const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  mode: "development",
  output: {
    filename: "[name].[hash:5].js",
    clean: true,
    assetModuleFilename: "assets/[hash:6][ext]" // 用来配置资源模块输出的位置以及文件名
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./index.html"
    })
  ],
  module: {
    rules: [
      {
        test: /\.png/,
        type: "asset/resource" //将 png 图片使用文件的方式打包,相当于 file-loader
      },
      {
        test: /\.txt/,
        type: "asset/source" //将文件内容原封不动的放到 asset 中,相当于 raw-loader
      },
      {
        test: /\.jpg/,
        type: "asset/inline" // 将 jpg 图片都处理成 base64 方式存储,相当于 url-loader
      },
      {
        test: /\.gif/,
        type: "asset", // 根据文件大小自动选择导出结果的类型,以前通过 url-loader 配置资源文件大小限制来实现
        generator: {
          filename: "gif/[hash:6][ext]" // 如果处理出来的结果是文件,存放位置命名规则是什么,会覆盖上面 assetModuleFilename 配置项
        },
        parser: {
          dataUrlCondition: {
            maxSize: 4 * 1024 // 如果文件尺寸小于 4kb 的话使用 base64 的方式,大于 4kb 的话使用文件的方式
          }
        }
      }
    ]
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

假如在 webpack5 中使用以前的 loader 来处理资源,你可能希望停止资源模块再次处理资源,因为这会导致资源重复。此时可以将资源的模块类型设置为:

module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/i,
        use: [
          {
            loader: "url-loader",
            options: {
              limit: 8192
            }
          }
        ],
        type: "javascript/auto"
      }
    ]
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# moduleIds & chunkIds 的优化

在 webpack5 之前,没有从 entry 打包的 chunk 文件,都会默认以 1、2、3... 的文件命名方式输出,这样删除某些文件由于顺序变了可能会导致缓存失效。

在 webpack5 中,生产环境下默认使用了 deterministic (opens new window) 的方式生成短 hash 值来分配给 modules 和 chunks 来解决上述问题。

# Tree Shaking 优化

1. 对 ES6 模块的优化

Webpack5 更精确地处理 ES6 模块,通过静态分析和剔除未使用的代码,以减小输出包的大小。

2. SideEffects 标志的使用

Webpack5 引入了 sideEffects 配置选项,使得开发者能够标记哪些文件是 “有副作用” 的,以帮助 Webpack 更好地判断哪些代码可以被 Tree Shaking。

3. ModuleConcatenationPlugin 的默认启用

Webpack5 默认启用了 ModuleConcatenationPlugin 插件,用于模块连接和作用域提升。这有助于减小生成的 bundle 大小,同时提高 Tree Shaking 效果。

4. JSON 模块 Tree Shaking 支持

Webpack5 提供了对 JSON 模块的 Tree Shaking 支持,可以正确地剔除未使用的 JSON 模块。

5. Dynamic Import 的 Tree Shaking 优化

Webpack5 进一步优化了对动态导入(Dynamic Import)的支持,通过动态导入引入的模块可以更好地被 Tree Shaking。

# 模块联邦

假设有这样一个场景:有两个独立的项目 A、B,如果在这两个项目间有公共依赖,我们通常的做法是什么?貌似能想到的最优解就是将公共依赖做成 npm 包,然后两个项目分别安装,但是在每次对这个 npm 包升级的时候,两个项目都需要重新更新版本号。项目一旦多了,这种方式也很繁琐。

因此,webpack5 推出了模块联邦(Module Federation) (opens new window)这一新特性。即 webpack 提供了一种解决方案,将公共依赖打包放在远程地址,各项目间通过 CDN 的方式引用,以达到一种在线 runtime 的效果

关于模块联邦有两个概念:remotes 和 host,即依赖的提供方和依赖的消费方,为了便于理解可以叫它们为 provider 和 consumer。实际配置过程中可以使用 ModuleFederationPlugin (opens new window) 插件。

// provider/src/webpack.config.js
const { ModuleFederationPlugin } = require("webpack").container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "provider", // 必须唯一,模块的名称
      filename: "remoteEntry.js", // 必须,生成的模块名称
      exposes: {
        // 很明显,需要对外暴露的模块 注意该对象的 key 必须这么写
        "./Search": "./src/Search",
        "./utils": "./src/utils"
      }
    })
  ]
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// consumer/src/webpack.config.js
const { ModuleFederationPlugin } = require("webpack").container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "consumer", // 必须唯一,模块的名称
      // 很明显,需要映射的远程 provider
      remotes: {
        /**
         * 这个地方来拆解下这个对象的参数
         * key: 无所谓随意取,但在后续消费的时候有用
         * value: "provider@http://localhost:9000/remoteEntry.js"
         * 这里的provider:依然是上面project-a配置的name
         * http://localhost:9000/: 这个表示 provider 项目部署后的远程地址
         * remoteEntry.js:指的是上面 provider 项目中定义的 filename
         */
        module1: "provider@http://localhost:9000/remoteEntry.js"
      }
    })
  ]
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

webpack 配置定义好之后,可以在 consumer 中这么用:

import React, { lazy, Suspense, useEffect } from "react";

/**
 * import("module1/Search")
 * 这里的 module1 指的是上面 consumer 配置中定义 remotes 时设置的 key
 * Search 指的是上面 provider 配置中定义 exposes 时设置的 key
 */
const ProviderSearch = lazy(() => import("module1/Search"));

const App = () => {
  return (
    <div>
      <h1>这是comsumer项目</h1>
      <Suspense>
        <ProviderSearch />
      </Suspense>
    </div>
  );
};

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

此外,还有一种全局调用的方法:

首先在 provider 的 webpack 配置中加上:

const { ModuleFederationPlugin } = require("webpack").container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      // ...
      library: { type: "var", name: "provider" }
    })
  ]
};
1
2
3
4
5
6
7
8
9
10

然后在需要使用的地方这么用:

function loadComponent(scope, module) {
  return async () => {
    await __webpack_init_sharing__("default");
    const container = window[scope];
    await container.init(__webpack_share_scopes__.default);
    const factory = await window[scope].get(module);
    const Module = factory();
    return Module;
  };
}
// provider 指的是上面在 library 配置中定义的 name 属性,utils 指的是 provider 向外暴露的公共依赖
loadComponent("provider", "utils");
1
2
3
4
5
6
7
8
9
10
11
12

如果 provider 项目和 consumer 项目都使用了第三方模块 react,那么两个项目都会引入一次,如何通过模块联邦处理共享模块呢?

只需要在原有的配置加上 shared 属性:

//给两个项目都配置上shared
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      shared: {
        react: {
          singleton: true // 整个微前端项目全局唯一
        }
      }
    })
  ]
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

需要注意的是,shared 默认选择的是高版本的共享模块,如果需要指定版本可以添加 requiredVersion (opens new window) 属性。

# Vite 的实现原理

Vite 的源码实现原理是基于现代浏览器的原⽣ ESM ⽀持,结合⾼效的服务器端处理和优化。它在开发环境中尽量减少打包步骤,以实现快速重载和模块更新,同时在⽣产构建中利⽤ Rollup 的⾼效和优化特性。通过其插件系统,Vite 可以灵活地扩展和定制以满⾜各种开发需求。

# 模块解析和加载

  • 使⽤原⽣ ES 模块

Vite 利⽤浏览器原⽣⽀持的 ES 模块(ESM)加载。这意味着在开发环境中,Vite 服务端对模块请求做最⼩处理,直接交给浏览器处理。

  • 2. 服务器处理请求

当浏览器请求模块时,Vite 开发服务器拦截这些请求。它判断请求的是源代码模块还是第三⽅依赖,并相应地处理。

# 依赖预构建

  • 优化 node_modules

Vite 会在第⼀次启动时检测 node_modules 中的依赖。对于那些不是原⽣ ESM 的依赖,Vite 使⽤ esbuild 将它们预构建成 ESM。

  • 缓存处理

预构建的结果会被缓存,以便在后续启动时直接使⽤,加快启动速度。

# 热模块替换(HMR)

  • WebSocket 通信

Vite 使⽤ WebSocket 与浏览器建⽴通信,⽤于实现 HMR。当本地⽂件发⽣变化时,Vite 服务器通知浏览器进⾏相应的模块更新。

  • 模块级更新

HMR 的实现是模块级别的。Vite 可以精确地知道哪些模块发⽣了变化,并仅更新这些模块。

# 插件系统

  • 基于 Rollup 插件 API

Vite 的插件系统是基于 Rollup 的。这使得许多 Rollup 插件可以在 Vite 中⽆缝使⽤。

  • 钩⼦和中间件

Vite 插件可以利⽤各种⽣命周期钩⼦和中间件来修改服务端⾏为和构建过程。

# ⽣产环境构建

虽然 Vite 在开发环境中不使⽤打包,但在构建⽣产版本时,它使⽤ Rollup 作为打包⼯具,以获得最优化的打包结果。

# 处理 CSS 和其他资产

Vite 特别处理 CSS 和其他资产(如图⽚),包括模块热更新和构建优化。

# Babel 的实现原理

Babel 是⼀个⼴泛使⽤的 JavaScript 编译器,它主要⽤于将 ECMAScript 2015+ 代码转换成向后兼容的 JavaScript 版本,以便在当前和旧版本的浏览器或环境中运⾏。

Babel 的⼯作原理可以分为三个主要阶段:解析(Parsing)、转换(Transforming)和⽣成(Generating)

# 解析(Parsing)

解析阶段将源代码转换成更抽象的表示形式(通常是 AST,即抽象语法树):

1. 词法分析(Lexical Analysis)

这个步骤将原始代码字符串分解成称为 “令牌”(tokens)的东⻄。这些 tokens 是代码中的最⼩单位,例如:关键字、运算符、数字、标点符号等。

2. 语法分析(Syntactic Analysis)

在语法分析阶段,词法分析器输出的 tokens 被转换成 AST。AST 是⼀个深层嵌套的对象,以树的形式表示代码的结构,其中每个节点都代表代码中的⼀部分(如声明、表达式等)。

# 转换(Transforming)

在这个阶段,Babel 接收之前⽣成的 AST,并通过插件对其进⾏操作来执⾏代码转换:

1. 遍历(Traversal)

Babel 遍历 AST 的所有节点。在这个过程中,它可以操作树中的节点:添加、修改、删除节点。

2. 应⽤转换

Babel 使⽤⼀系列插件或预设(preset)来应⽤转换。这些插件可以是语法转换(例如将 ES6 转换为 ES5),也可以是其他转换(例如优化代码)。

# ⽣成(Generating)

最后⼀步是代码⽣成,这个阶段 Babel 将经过转换的 AST 转换回代码字符串,同时可以⽣成 source map。

1. 代码⽣成

Babel 使⽤转换后的 AST 来⽣成新的代码字符串。这个过程包括将 AST 节点重新转换成代码形式,并构建最终的代码字符串。

2. Source Map ⽣成

如果需要,Babel 还可以⽣成 source map,这些 map 可以将转换后的代码映射回原始源代码,这对于调试转换后的代码⾮常有⽤。

通过这三个阶段的⼯作,Babel 能够读取最新版本的 JavaScript 代码,对其进⾏转换和优化,最后⽣成浏览器兼容的 JavaScript 代码。Babel 的强⼤之处在于其插件系统,可以灵活配置各种转换和优化,这也是为什么它成为现代前端开发流程中不可或缺的⼀部分。

# SWC 的实现原理

SWC(Speedy Web Compiler)是⼀个⽤ Rust 编写的⾼性能 JavaScript/TypeScript 编译器。它与 Babel 类似,可以⽤于将 ECMAScript 2015+ 代码转换成向后兼容的 JavaScript 版本,以便在现代和旧版浏览器或环境中运⾏。

SWC 的主要卖点是它的性能,由于 Rust 的⾼效性,SWC ⽐⼤多数基于 JavaScript 的编译器(如 Babel)更快。

SWC 的⼯作原理⼤体上与 Babel 相似,也是遵循解析(Parsing)、转换(Transforming)和⽣成(Generating)的过程。

# 解析(Parsing)

解析阶段的⽬标是将源代码转换成 AST(抽象语法树):

1. 词法分析

SWC ⾸先执⾏词法分析,将源代码字符串分解成 tokens(令牌)。这些 tokens 是代码的最⼩单位,例如关键字、运算符、数字等。

2. 语法分析

在语法分析阶段,SWC 将这些 tokens 转换为 AST。AST 是⼀个树形结构,每个节点代表源代码的⼀部分(如表达式、语句等)。

由于 SWC 是⽤ Rust 编写的,它在这⼀阶段的执⾏速度相⽐基于 JavaScript 的编译器更快。

# 转换(Transforming)

这个阶段涉及对 AST 的操作以执⾏代码转换:

1. 遍历 AST

SWC 遍历 AST 的所有节点。在这个过程中,可以根据需要添加、修改或删除节点。

2. 应⽤转换

使⽤⼀系列插件来对 AST 进⾏操作。这些插件可能是为了兼容性的语法转换(例如,将 ES6 转换为 ES5),也可能是为了性能优化或其他⽬的。

与 Babel 类似,SWC 的强⼤之处在于它的灵活性和插件系统,允许开发者根据项⽬需求⾃定义编译流程。

# ⽣成(Generating)

在⽣成阶段,SWC 将经过转换的 AST 转换回 JavaScript 代码:

1. 代码⽣成

SWC 根据修改后的 AST ⽣成新的代码字符串。这个过程涉及将 AST 节点转换回代码。

2. Source Map ⽣成

如有需要,SWC 还能⽣成 source map,帮助开发者在调试过程中将编译后的代码映射回原始代码。

SWC 的主要优势在于其性能。由于 Rust 语⾔的⾼效性和并⾏处理能⼒,SWC 在解析、转换和⽣成代码的过程中通常⽐基于 JavaScript 的编译器(如 Babel)快得多。这对于⼤型项⽬和复杂的构建过程尤为重要,因为它可以显著减少构建和重新构建的时间。

SWC 为现代 JavaScript 和 TypeScript 项⽬提供了⼀个⾼效、快速且可配置的编译解决⽅案。它通过 Rust 的⾼效执⾏和强⼤的并⾏能⼒,在提供类似于 Babel 的功能的同时,⼤幅提⾼了编译速度。

# swc-loader 相比 babel-loader 有哪些优势

使⽤ swc-loader 相⽐于使⽤ babel-loader 主要有以下⼏个优势:

  • 性能提升

最显著的优势是性能提升。SWC 是⽤ Rust 编写的,因此在执⾏编译任务时⽐基于 JavaScript 的 Babel 更快。对于⼤型项⽬或需要频繁编译的开发环境,这可以显著减少构建时间。

  • 更低的资源消耗

与 Babel 相⽐,SWC 在编译过程中通常消耗更少的内存和 CPU 资源。这使得它在资源受限的环境(如 CI/CD 管道或低配硬件)中表现更好。

  • 更好的并⾏处理能⼒

由于 Rust 的并发性,SWC 可以更有效地进⾏并⾏处理,进⼀步提⾼性能。

  • 简化的配置

SWC 旨在提供更简洁直观的配置选项,尽管它的插件⽣态可能不如 Babel 成熟。

  • 兼容性

SWC ⼒求兼容 Babel,这意味着它能处理⼤多数 Babel 可以处理的代码。对于已经使⽤ Babel 的项⽬,迁移到 SWC 可能相对容易。

尽管 SWC 提供了这些优势,但也有⼀些考虑因素:

  • 插件⽣态

Babel 拥有⼀个庞⼤⽽成熟的插件⽣态系统。对于⼀些特定的、⾼度定制的⽤例,Babel 可能提供了 SWC 尚未⽀持的插件。

  • 迁移和兼容性

虽然 SWC ⼒求兼容 Babel,但在某些复杂的配置或插件使⽤上可能仍存在差异。在迁移到 SWC 时可能需要⼀些调整和测试。

  • 社区和⽀持

Babel 作为⼀个⻓期存在且⼴泛使⽤的⼯具,拥有⼀个庞⼤的社区和丰富的⽂档资源。对于 SWC 来说,虽然社区正在快速增⻓,但可能还没有达到 Babel 的⽔平。

综上所述,如果你的项⽬对构建性能有较⾼要求,或者你想从 Rust 提供的性能优势中受益,那么选择 swc-loader 是⼀个很好的选择。但同时,也需要考虑到项⽬的具体需求和现有的技术栈。

# webpack 和 gulp 的优缺点

两者都是构建工具,但是侧重点不同。

1. Gulp

gulp 侧重于对开发流程的管理

优点

  • 轻量,配置文件简单。

  • 基于 nodejs 强大的流(stream)能力,构建速度快。

  • 适合多页 web 应用和 node 服务端应用。

缺点

  • 不太适合单页应用或自定义模块的开发。

2. Webpack

webpack 侧重于模块的打包

优点

  • 任何资源都可以作为模块进行处理。

  • 社区资源丰富,有很多的 plugin 和 loader 可以用。

缺点

  • 配置复杂。

  • 不适合 node 服务端应用。

  • 构建速度较慢,需要做很多性能优化。

# 为什么要用 gulp 打包 nodejs

# nodejs 打包要求

首先对于 nodejs 程序的打包要求,有以下几点:

  • 打包后需要支持最新的 es 版本(node 作为在服务端运行的 js 代码,对于这些 es 版本支持的新特性是不会更新很快的);

  • 打包编译速度不能太慢,这样对开发进度可控;

  • 打包后的文件结构不能发生变化,如有需要可以发生一点点变化;

  • 配置文件友好。

# 高效的 gulp

  • gulp 主打简单、高效、生态。

  • gulp 内部使用的是 node 流机制,流是一种相当高效且不占内存的数据格式,它并不会过多的占用 node 的堆内内存,而且所占内存的最大值在 30m 以内,在流向最后一个消费者时才会写入磁盘中,在打包过程中,并不会占用磁盘空间或者内存。

  • 因此,选择 gulp 来打包 nodejs 的理由有以下几点:

    配置简单,编译速度快;

    开发时并不会占用很多的内存空间;

    打包后保持文件结构不变。

# 前端构建工具对比

选择适合的构建⼯具需要根据项⽬的具体需求、团队的技术栈以及对构建速度和配置复杂度的需求来决定。

# Webpack

特点:⾼度可定制,⽀持⼤量插件和加载器,适合复杂项⽬依赖。

适⽤场景:⼤型复杂的前端项⽬,需要细粒度控制的项⽬。

# Gulp

特点:轻量,配置文件简单,基于 nodejs 强大的流(stream)能力,构建速度快。

适⽤场景:适合多页 web 应用和 node 服务端应用。

# Rollup

特点:⽣成更简洁、效率更⾼的结果,专注于 ES6 模块。

适⽤场景:JavaScript 库和简单应⽤的打包。

# Parcel

特点:零配置,易于上⼿和使⽤,⾃动代码分割。

适⽤场景:中⼩型项⽬,追求快速启动和少量配置。

# Vite

特点:极快的服务器启动和模块热更新,使⽤ Rollup 进⾏⽣产打包。

适⽤场景:现代 Web 应⽤,强调启动速度和 HMR。

# esbuild

特点:极快的构建速度,由 Go 语⾔编写。

适⽤场景:需要快速构建的项⽬,尤其在开发环境中。

# SWC

特点:Rust 编写的快速 JavaScript/TypeScript 编译器。

适⽤场景:替代 Babel,适⽤于需要快速编译的项⽬。

# Rome

特点:全栈前端⼯具,集成多种开发⼯具功能。

适⽤场景:⼀站式前端开发⼯具解决⽅案。

# Bun

特点:⾼性能 JavaScript 运⾏时和包管理器,强调性能和简洁。

适⽤场景:性能敏感和依赖管理的项⽬。

# 如何理解前端工程化

前端工程化旨在通过系统化、标准化、⾃动化的⽅法来提升前端开发的效率、质量和可维护性。它不仅涉及技术和⼯具,还包括团队协作、开发流程和最佳实践的整合。

# 1. 代码模块化和组件化

  • 模块化:将复杂的代码分解为独⽴、可重⽤的模块,以提⾼代码的可维护性和复⽤性。

  • 组件化:特别针对 UI 开发,通过组件化的⽅式来构建⽤户界⾯,每个组件负责⼀块独⽴的 UI 功能。

# 2. ⾃动化⼯具

  • 构建⼯具:如 Webpack、Rollup 等,⽤于⾃动化处理模块化代码的打包、压缩、合并等。

  • 代码质量⼯具:如 ESLint、Prettier 等,确保代码⻛格⼀致性和遵循最佳实践。

  • 测试⼯具:如 Jest、Mocha、Cypress 等,⾃动化进⾏单元测试、集成测试和端到端测试。

# 3. 标准化

  • 代码规范:建⽴统⼀的编码标准和规范,如 Airbnb JavaScript Style Guide。

  • 组件规范:定义组件开发的标准,包括命名、接⼝、⽂档等。

  • 流程规范:确定代码审查、合并、发布等流程的标准。

# 4. 性能优化

  • 加载性能:优化资源加载,如代码拆分、懒加载、预加载等。

  • 运⾏性能:优化⻚⾯渲染过程,如减少重绘和回流、使⽤虚拟 DOM 等。

# 5. 开发体验和效率

  • 热模块替换(HMR):提供即时反馈,提⾼开发效率。

  • 组件库和⼯具集:建⽴可复⽤的组件库和⼯具集,加速开发流程。

# 6. 持续集成和持续部署(CI/CD)

  • ⾃动化流程:⾃动化测试、构建、部署等流程,确保代码质量和加速产品迭代。

  • 版本控制:使⽤ Git 等版本控制系统,管理代码变更和协作。

# 7. 跨平台开发

  • 响应式设计:确保应⽤在不同设备和分辨率下的兼容性和⽤户体验。

  • 框架和⼯具:利⽤如 React Native、Flutter 等技术进⾏跨平台应⽤开发。

上次更新时间: 2024年01月10日 17:20:13