likes
comments
collection
share

6-2 工程化实战之模块化+Webpack

作者站长头像
站长
· 阅读数 17

模块化

定义:将代码按照功能划分为独立、可复用的单元,每个单元称为一个模块。

发展历程

无模块

将 JS 代码直接在 html 里面按顺序引入

// calc.js 计算方法
function add(x, y) {
  return x + y
}
function subtract(x, y) {
  return x - y
}
function multiply(x, y) {
  return x * y
} 
function divide(x, y) {
  return x / y
}

// log.js 方法
function info(msg) {
  console.info("[ info msg ] >", msg);
}



// index.html
<!DOCTYPE html>
<html>
  // ...

  <body>
    <script src="./calc.js"></script>
    <script src="./log.js"></script>
  </body>
</html>

缺点:

  • 变量名全局污染,如calc.js里面定义了function add(),则其他地方就不能再使用add了,否则就会被覆盖
  • 代码只能通过 html 里面关联,如想在log.js里面使用add函数,就只有在 html 将calc.js引入代码放在log.js前面,然后才能用add函数,JS 多了则难以维护

模块化雏形 - IIFE

基于立即执行函数,形成函数作用域,可解决变量名全局污染的问题,但还是只能放在 html 里面关联

// calc.js 计算方法
var calc = (function () {
  function add(x, y) {
    return x + y;
  }
  function subtract(x, y) {
    return x - y;
  }
  function multiply(x, y) {
    return x * y;
  }
  function divide(x, y) {
    return x / y;
  }

  return { add, subtract, multiply, divide };
})();

// xxx.js 方法
var log = (function () {
  function info(msg) {
    console.info("[ info msg ] >", msg);
  }

  function error(msg) {
    console.error("[ error msg ] >", msg);
  }

  function add(msg) {
    console.warn("[ add msg ] >", msg);
  }

  return { info, error, add };
})();



// index.html
<html>
  // ...

  <body>
    <script src="./calc.js" />
    <script src="./xxx.js" />
    <script>
      console.log("[ calc ] >", calc);
      console.log("[ log ] >", log);
    </script>
  </body>
</html>

// [ calc ] > {add: ƒ, subtract: ƒ, multiply: ƒ, divide: ƒ}
// [ log ] > {info: ƒ, error: ƒ, add: ƒ}

高速发展:CJS、AMD、UMD

CJS:node 端的模块加载规范,仅支持同步的,语法为module.exports = {}、reqiure('./xx/xx.js')

AMD:浏览器端的模块加载规范,可支持异步,语法为如下:

// 定义一个简单的模块,无依赖
define(function () {
  return {
    name: 'simpleModule',
    doSomething: function () {
      console.log('Doing something...');
    }
  };
});

// 定义一个有依赖的模块
define(['dependency1', 'dependency2'], function (dep1, dep2) {
  return {
    method: function () {
      // 使用依赖模块的功能
      dep1.someFunction();
      dep2.anotherFunction();
    }
  };
});

// 异步加载模块并使用
require(['myModule'], function (myModule) {
  // myModule已经被加载完成
  myModule.doSomething();
});

UMD:将内容输出支持:IIFE、CJS、AMD 三种格式的语法

(function (root, factory) {
  // 判断环境
  if (typeof define === 'function' && define.amd) {
    // AMD环境,使用define方法注册模块
    define(['dependency1', 'dependency2'], factory);
  } else if (typeof module === 'object' && module.exports) {
    // CommonJS环境(如Node.js),使用exports导出模块
    module.exports = factory(require('dependency1'), require('dependency2'));
  } else {
    // 浏览器全局环境(非模块化环境),挂载到全局变量(如window)
    root.MyModule = factory(root.dependency1, root.dependency2);
  }
}(this, function (dependency1, dependency2) {
  // 模块的具体实现
  function MyModule() {
    // ...
  }

  // 返回模块的公共API
  return MyModule;
}));

虽然是高速发展了,但编码复杂性、全局污染、浏览器支持性等上都存在问题

新时代:官方下场 - ESM

最终官方下场,从语法层给出模块加载方式规范,即 ECMAScript Modules,关键词为import、export,终结了混乱的模块加载规范

// 导出单个函数或变量
export const PI = 3.14;

// 导出默认值
export default function myDefaultExport() {
  // ...
}

// 导出多个命名出口
export function func1() {}
export function func2() {}

// 原生 html 里面这样引入
<script type="module">
  import log from "./js/log.js";

  console.log("[ log ] >", log);
</script>
// or
<script type="module" src="./js/log.js"></script>

解决了以下问题:

  • 每个模块有独立的作用域,不会再污染全局
  • 支持同步、异步
  • 解决模块循环引用问题

缺点:对低版本浏览器不支持

总结

所以什么是模块化呢?就是将代码分割成可复用的单元,并且通过某种规范实现互相引用

模块化是前端工程化的基石

一些考点

CJS

node 端提出的模块加载机制,不支持异步,不支持浏览器,每个文件都有自己的作用域。

导出语法:

// add.js
function add() {}

module.exports = { add }
// or
exports.add = add

导入语法,require永远引入module.exports的值,对应 JS 文件可省略文件后缀

const { add } = require('./add.js')

特点

动态(同步):当代码执行到require那行时才去加载对应的文件并执行文件内容,可以理解为是“同步”的

reqiure伪代码实现(node 端),所以它的是同步,并且是对值的 “拷贝”

function require(filePath) {
    const content = fs.readFileSync(filePath);
    return eval(content);
}

对值的 “拷贝” 代码展示:

// a.js
let a = 1

setTimeout(() => a++, 500)

exports.a = a 

// index.js
const { a } = require('./a.js')

console.log(a)

setTimeout(() => console.log(a), 1000)

// 打印结果为:
1
1

module.exports 与 exports

初始时module.exports === exports 为 true,等价于const exports = module.exports

function add() {}
function subtract() {}

exports.add = add
exports.subtract = subtract

console.log(exports) // { add: [Function: add], subtract: [Function: subtract] }
console.log(module.exports) // { add: [Function: add], subtract: [Function: subtract] }

但当它们同时存在时,最终require得到的是module.exports的值

function add() {}
function subtract() {}

exports.add = add
exports.subtract = subtract

module.exports = { add }

console.log(exports) // { add: [Function: add], subtract: [Function: subtract] }
console.log(module.exports) // { add: [Function: add] }

所以为了避免混淆,同一文件只使用一种导出方式

对循环依赖的处理

a.js 引入 b.js,b.js 引入 a.js,则形成了循环依赖

// a.js
const b = require("./b");

console.log("b", b);
exports.a = 1;

// b.js
const a = require("./a");

console.log("a", a);
exports.b = 2;


// 命令行运行:node a.js
// 打印结果如下:
// a > {}
// b > { 2 }
// (node:95411) Warning: Accessing non-existent property 'Symbol(nodejs.util.inspect.custom)' of module exports inside circular dependency
// (Use `node --trace-warnings ...` to show where the warning was created)
// (node:95411) Warning: Accessing non-existent property 'constructor' of module exports inside circular dependency
// (node:95411) Warning: Accessing non-existent property 'Symbol(Symbol.toStringTag)' of module exports inside circular dependency
// (node:95411) Warning: Accessing non-existent property 'Symbol(Symbol.iterator)' of module exports inside circular dependency

// 命令行运行:node b.js
// 打印结果如下:
// b > {}
// a > { 1 }
// (node:95411) Warning: Accessing non-existent property 'Symbol(nodejs.util.inspect.custom)' of module exports inside circular dependency
// (Use `node --trace-warnings ...` to show where the warning was created)
// (node:95411) Warning: Accessing non-existent property 'constructor' of module exports inside circular dependency
// (node:95411) Warning: Accessing non-existent property 'Symbol(Symbol.toStringTag)' of module exports inside circular dependency
// (node:95411) Warning: Accessing non-existent property 'Symbol(Symbol.iterator)' of module exports inside circular dependency

结论:谁先执行,则它里面的能拿到值,并且伴随循环引用的报错

ESM

ECMAScript 提供的模块加载规范,支持浏览器、node,支持异步、同步

导出语法

// add.js
export function add() {}
export function subtract() {}

导入语法,对应 JS 文件默认不可省略文件后缀,除非有配置

import { add, subtract } from './add.js'

特点

静态(异步):是因为 ESM 的核心流程是分成三步(构建、实例化、求值),并且可以分别完成,所以称为异步

为什么要分成三个,不能直接一起吗?因为浏览器加载执行 JS 时会阻塞主线程,造成的后果很大。

构建:根据import创建模块之间的依赖关系图(编译时输出),然后下载模块文件生成模块记录(记录importName、importUrl)

实例化:基于生成的模块记录,找到模块的代码与导出的变量名,然后将相同导入、导出指向同一个地址

求值:运行模块的代码,将值赋到实例化后的地址内

对值的 “引用” 代码展示:

// a.js
export let a = 1

setTimeout(() => a++, 500)

// index.js
import { a } from './a.js'

console.log(a)

setTimeout(() => console.log(a), 1000)

// 打印结果为:
1
2

export 与 export default

这是两种导出方式,可并存,只是import的逻辑不同

export default之后的值将被导出,可以是任意类型,但import时只能命名为一个变量,就算export default了一个对象,也不支持解构,该变量的l值为export default之后的值,一个文件只能有一个export default

function error(msg) {
  console.error("[ error msg ] >", msg);
}

function add(msg) {
  console.warn("[ add msg ] >", msg);
}
export default { error, add };

// 引入时
import log from './log.js' //  只能当做变量
import { error } from './log.js' // 不能解构,会报错的

export之后的值将被导出,可以是任意类型,但import时只能当做对象来解构其值,就算export了一个基础类型,也不支持作为一个变量使用(除非使用* as语法)

function error(msg) {
  console.error("[ error msg ] >", msg);
}

function add(msg) {
  console.warn("[ add msg ] >", msg);
}
export { error, add };

// 引入时
import { error } from './log.js' // 只能解构
import log from './log.js' //  不能当做变量,会报错的
import * as log from './log.js' //  导出所有的(包括 export default 的)

总结:export default导出的只能作为变量使用,export导出的只能解构使用

对循环依赖的处理

// a.js
import { b } from "./b"

console.log("b", b);
export const a = 1;

// b.js
import { a } from "./a"

console.log("a", a);
export const b = 2;

// node --experimental-modules a.js
// a.js:3 Uncaught ReferenceError: Cannot access 'b' before initialization

结论: 直接报错

Webpack

官方文档

核心概念(了解下即可,后面会讲原理的)

Sourcemap

文件指纹技术

Babel 与 AST

TreeShaking

优化:构建速度、提高页面性能

原理:Webpack、Plugin、Loader

手写实现 Webpack 打包基本原理

初始化项目

  1. 随便创建个项目文件,然后创建src文件夹与空文件
mkdir src && touch src/add.js && touch src/minus.js && touch src/index.js && touch index.html
  1. 初始化项目pnpm init,然后安装依赖pnpm add fs-extra
  2. src/add.js写入相关代码
export default (a, b) => a + b;
  1. src/minus.js写入相关代码
export const minus = (a, b) => a - b;
  1. src/index.js写入相关代码
import add from "./add.js";
import { minus } from "./minus.js";

const sum = add(1, 2);
const division = minus(2, 1);
console.log("[ add(1, 2) ] >", sum);
console.log("[ minus(2, 1) ] >", division);
  1. index.html写入相关代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>手写实现 Webpack</title>
  </head>
  <body>
    <div>我在手写实现 Webpack</div>
    <script src="./src/index.js"></script>
  </body>
</html>
  1. 然后使用 VScode 的 Live Server 启动index.html看效果

6-2 工程化实战之模块化+Webpack6-2 工程化实战之模块化+Webpack

肯定是报错的,因为我们的<script没加type="module"

我们期望正确的结果是:

6-2 工程化实战之模块化+Webpack

原理实现

Webpack 的主要作用是从入口开始就一系列的依赖文件,最终打包成一个文件,这也是我们要实现的功能。

我们常用的打包命令是:npm run build

  1. 新建一个webpack.js作为手写 webpack的入口(这个会基于 node 环境去运行的哦)
touch webpack.js

然后我们期望运行这个node webpack.js后,生成一个dist文件夹,其中有

  • 一个bundle.js,包含了我们src源码下面的所有文件的代码
  • 一个index.html,将之前的<script src="./src/index.js"></script>改为了<script src="./bundle.js"></script>后的 html

然后 Live Server 运行dist/index.html后,最终浏览器能正确运行并打印:

6-2 工程化实战之模块化+Webpack

1、基于主入口,读取文件

  1. webpack.js编码:基于主入口,读取文件
/**
 * 功能设计:
 * 1. 找到主入口,即 src/index.js 文件,然后加载进来(getFileInfo(path) -> fileContent)
 */

// fs 模块,读取文件内容
const fs = require("fs");

// 主入口路径变量,目前写死
const entry = "./src/index.js";

/**
 * 获取文件信息
 *
 * @param path 文件路径
 * @returns 返回文件内容
 */
function getFileInfo(path) {
  return fs.readFileSync(path, "utf-8");
}

const entryFileContent = getFileInfo(entry);

console.log("[ entryFileContent ] >", entryFileContent);
  1. 运行node webpack.js调试,发现已拿到主入口对应的代码了

6-2 工程化实战之模块化+Webpack

  1. node webpack.js放到package.json中,之后就可以pnpm build执行了

6-2 工程化实战之模块化+Webpack

2、基于入口文件内容,去分析依赖关系

(一)解析为 AST

第一步拿到了入口文件的内容,我们就需要去分析其中的依赖关系,即import关键词。

如何分析呢?要么原始的通过字符串匹配import然后分析;要么借用其他工具帮我们解析与分析

这里采用Babel工具来帮我们分析

什么是 Babel?JS 编译器,可将高版本转为低版本的 JS

流程为:解析(将源代码转为 AST)、转换(对 AST 进行增删改查)、生成(将 AST 转为 JS 代码)

所以我们可以利用它来帮我们解析 JS 代码

  1. 安装@babel/parser依赖,官方使用文档:@babel/parser · Babel
pnpm add @babel/parser
  1. 引入并使用
/**
 * 功能设计:
 * 1. 找到主入口,即 src/index.js 文件,然后加载进来(getFileInfo(path) -> fileContent)
 * 2. 解析主入口的内容(parseFile(fileContent) -> AST)
 */

// fs 模块,读取文件内容
const fs = require("fs");

// 主入口路径变量,目前写死
const entry = "./src/index.js";

/**
 * 获取文件信息
 *
 * @param path 文件路径
 * @returns 返回文件内容
 */
function getFileInfo(path) {
  // 使用 fs.readFileSync 方法同步读取文件内容
  return fs.readFileSync(path, "utf-8");
}

const entryFileContent = getFileInfo(entry);

console.log("[ entryFileContent ] >", entryFileContent);

// ++++ 以下为新增 ++++
// @babel/parser 解析文件内容
const parser = require("@babel/parser");

/**
 * 解析文件内容并返回抽象语法树(AST)
 *
 * @param fileContent 文件内容
 * @returns 抽象语法树(AST)
 */
function parseFile(fileContent) {
  // 解析文件内容,生成抽象语法树(AST)
  const ast = parser.parse(fileContent, {
    sourceType: "module", // 要解析的模块是 ESM
  });
  // 返回抽象语法树(AST)
  return ast;
}

const entryFileContentAST = parseFile(entryFileContent);

console.log("[ entryFileContentAST ] >", entryFileContentAST);
  1. 然后运行pnpm build,看下打印 AST 的结构

6-2 工程化实战之模块化+Webpack

可以发现是正确打印了,但是一些关键信息被隐藏了,比如 body 里面的

这时候就可以借助 AST 在线工具,将我们的代码拷贝进去,看完整的结构:

6-2 工程化实战之模块化+Webpack

(二)分析 AST,形成依赖图

还是使用工具,帮我直接分析依赖:@babel/traverse · Babel

  1. 安装@babel/traverse依赖
pnpm add @babel/traverse
  1. webpack.js编码:分析 AST,形成依赖图
/**
 * 功能设计:
 * 1. 找到主入口,即 src/index.js 文件,然后加载进来(getFileInfo(path) -> fileContent)
 * 2. 解析主入口的内容(parseFile(fileContent)),找到所有依赖,形成依赖关系(createDependencyMap(AST) -> dependencyMap)
 */

// 主入口路径变量,目前写死
const entry = "./src/index.js";

// path 模块,获取文件路径
const path = require("path");

// fs 模块,读取文件内容
const fs = require("fs");

/**
 * 获取文件信息
 *
 * @param path 文件路径
 * @returns 返回文件内容
 */
function getFileInfo(path) {
  // 使用 fs.readFileSync 方法同步读取文件内容
  return fs.readFileSync(path, "utf-8");
}

const entryFileContent = getFileInfo(entry);

// @babel/parser 解析文件内容
const parser = require("@babel/parser");

/**
 * 解析文件内容并返回抽象语法树(AST)
 *
 * @param fileContent 文件内容
 * @returns 抽象语法树(AST)
 */
function parseFile(fileContent) {
  // 解析文件内容,生成抽象语法树(AST)
  const ast = parser.parse(fileContent, {
    sourceType: "module", // 要解析的模块是 ESM
  });
  // 返回抽象语法树(AST)
  return ast;
}

const entryFileContentAST = parseFile(entryFileContent);

// ++++ 以下为新增 ++++
const traverse = require("@babel/traverse").default;
/**
 * 创建依赖关系图
 *
 * @param ast 抽象语法树
 * @returns 依赖关系图
 */
function createDependencyMap(ast) {
  // 创建依赖关系图
  const dependencyMap = {};

  // 遍历抽象语法树(AST)
  traverse(ast, {
    ImportDeclaration({ node }) {
      const { value } = node.source; // 从 AST 中获取到导入的相对文件路径

      const dirname = path.dirname(entry); // 获取存放主入口文件的文件名

      const abspath = "./" + path.join(dirname, value); // 拼接出每个导入文件的绝对路径

      dependencyMap[value] = abspath; // 添加到依赖关系图
    },
  });

  console.log("[ dependencyMap ] >", dependencyMap);

  return dependencyMap;
}

createDependencyMap(entryFileContentAST);
  1. 运行pnpm build,可以看到打印的依赖图

6-2 工程化实战之模块化+Webpack

3、再将 AST 转换为低版本的 JS 代码

因为我们之前写的代码都是高版本的,所以有些浏览器不一定能识别,因此需要将其转为低版本代码,并且该低代码最终会在浏览器中运行哦

  1. 安装bable相关依赖
pnpm add babel @babel/preset-env @babel/core
(三)将 AST 转换为低版本的 JS 代码
  1. webpack.js编码:将 AST 转换为低版本的 JS 代码
/**
 * 功能设计:
 * 1. 找到主入口,即 src/index.js 文件,然后加载进来(getFileInfo(path) -> fileContent)
 * 2. 解析主入口的内容(parseFile(fileContent)),找到所有依赖,形成依赖关系(createDependencyMap(AST) -> dependencyMap)
 * 3. 在将 AST 转换成低版本的 JS 代码,(generateCode(AST))
 */

// 主入口路径变量,目前写死
const entry = "./src/index.js";

// path 模块,获取文件路径
const path = require("path");

// fs 模块,读取文件内容
const fs = require("fs");

/**
 * 获取文件信息
 *
 * @param path 文件路径
 * @returns 返回文件内容
 */
function getFileInfo(path) {
  // 使用 fs.readFileSync 方法同步读取文件内容
  return fs.readFileSync(path, "utf-8");
}

const entryFileContent = getFileInfo(entry);

// @babel/parser 解析文件内容
const parser = require("@babel/parser");

/**
 * 解析文件内容并返回抽象语法树(AST)
 *
 * @param fileContent 文件内容
 * @returns 抽象语法树(AST)
 */
function parseFile(fileContent) {
  // 解析文件内容,生成抽象语法树(AST)
  const ast = parser.parse(fileContent, {
    sourceType: "module", // 要解析的模块是 ESM
  });
  // 返回抽象语法树(AST)
  return ast;
}

const entryFileContentAST = parseFile(entryFileContent);

const traverse = require("@babel/traverse").default;
/**
 * 创建依赖关系图
 *
 * @param ast 抽象语法树
 * @returns 依赖关系图
 */
function createDependencyMap(ast) {
  // 创建依赖关系图
  const dependencyMap = {};

  // 遍历抽象语法树(AST)
  traverse(ast, {
    ImportDeclaration({ node }) {
      const { value } = node.source; // 从 AST 中获取到导入的相对文件路径

      const dirname = path.dirname(entry); // 获取存放主入口文件的文件名

      const abspath = "./" + path.join(dirname, value); // 拼接出每个导入文件的绝对路径

      dependencyMap[value] = abspath; // 添加到依赖关系图
    },
  });

  console.log("[ dependencyMap ] >", dependencyMap);

  return dependencyMap;
}

createDependencyMap(entryFileContentAST);

// ++++以下为新增的代码++++
/**
 * 生成代码
 *
 * @param ast AST 对象
 * @returns 返回生成的代码字符串
 */
function generateCode(ast) {
  // 使用 Babel 将抽象语法树(AST)转换为可执行的 JavaScript 代码
  const { code } = require("@babel/core").transformFromAst(ast, null, {
    presets: ["@babel/preset-env"], // 指定转译的语法
  });

  // 返回生成的代码
  return code;
}

generateCode(entryFileContentAST);
  1. 运行pnpm build,可以看到打印的 code

6-2 工程化实战之模块化+Webpack

考点:"use strict"是什么?

"use strict"是 ES5 的严格模式,JS 解释器将采用更严格的规则来解析和执行代码,目的是消除常见错误与禁用不安全的操作(因为 JS 太灵活了)

  • 变量名不能重复使用 var 声明
  • eval 不能使用
  • 变量必须先声明再使用
  • 函数内部的 this 不会默认绑到全局对象上
  • 对象属性名不能重复
  • 函数参数名不能重复
  • 等等
(四)将上述代码聚合到一个方法内 - getModuleInfo
  1. webpack.js编码:将上述代码聚合到一个方法内 - getModuleInfo
/**
 * 功能设计:
 * 1. 找到主入口,即 src/index.js 文件,然后加载进来(getFileInfo(path) -> fileContent)
 * 2. 解析主入口的内容(parseFile(fileContent)),找到所有依赖,形成依赖关系(createDependencyMap(AST) -> dependencyMap)
 * 3. 在将 AST 转换成低版本的 JS 代码,(generateCode(AST))
 */

// 主入口路径变量,目前写死
const entry = "./src/index.js";

// path 模块,获取文件路径
const path = require("path");

// fs 模块,读取文件内容
const fs = require("fs");

// @babel/parser 解析文件内容
const parser = require("@babel/parser");

// @babel/traverse 遍历抽象语法树(AST)
const traverse = require("@babel/traverse").default;

// @babel/generator 将 AST 转换成代码字符串
const babelCore = require("@babel/core");

/**
 * 获取模块信息
 *
 * @param _path 文件路径
 * @returns 包含文件路径、依赖关系图和生成代码的对象
 */
function getModuleInfo(_path) {
  /**
   * 获取文件信息
   *
   * @param path 文件路径
   * @returns 返回文件内容
   */
  function getFileInfo(path) {
    // 使用 fs.readFileSync 方法同步读取文件内容
    return fs.readFileSync(path, "utf-8");
  }

  /**
   * 解析文件内容并返回抽象语法树(AST)
   *
   * @param fileContent 文件内容
   * @returns 抽象语法树(AST)
   */
  function parseFile(fileContent) {
    // 解析文件内容,生成抽象语法树(AST)
    const ast = parser.parse(fileContent, {
      sourceType: "module", // 要解析的模块是 ESM
    });
    // 返回抽象语法树(AST)
    return ast;
  }

  /**
   * 创建依赖关系图
   *
   * @param ast 抽象语法树
   * @returns 依赖关系图
   */
  function createDependencyMap(ast) {
    // 创建依赖关系图
    const dependencyMap = {};

    // 遍历抽象语法树(AST)
    traverse(ast, {
      ImportDeclaration({ node }) {
        const { value } = node.source; // 从 AST 中获取到导入的相对文件路径

        const dirname = path.dirname(entry); // 获取存放主入口文件的文件名

        const abspath = "./" + path.join(dirname, value); // 拼接出每个导入文件的绝对路径

        dependencyMap[value] = abspath; // 添加到依赖关系图
      },
    });

    return dependencyMap;
  }

  /**
   * 生成代码
   *
   * @param ast AST 对象
   * @returns 返回生成的代码字符串
   */
  function generateCode(ast) {
    // 使用 Babel 将抽象语法树(AST)转换为可执行的 JavaScript 代码
    const { code } = babelCore.transformFromAst(ast, null, {
      presets: ["@babel/preset-env"], // 指定转译的语法
    });

    // 返回生成的代码
    return code;
  }

  const _pathFileContent = getFileInfo(_path);
  const _pathFileContentAST = parseFile(_pathFileContent);
  const _pathFileDepsMap = createDependencyMap(_pathFileContentAST);
  const _pathFileCode = generateCode(_pathFileContentAST);

  return { path: _path, deps: _pathFileDepsMap, code: _pathFileCode };
}

const entryModuleInfo = getModuleInfo(entry);

console.log("[ entryModuleInfo ] >", entryModuleInfo);
  1. 运行 build,得到如下结果

6-2 工程化实战之模块化+Webpack

4、基于依赖关系图,去加载对应的所有文件

  1. webpack.js编码:基于依赖关系图,去加载对应的所有文件
// 前面的不变....

/**
 * 加载模块
 *
 * @param dependencyMap 模块依赖映射表
 * @returns 返回加载的模块数组
 */
function loadModules(dependencyMap) {
  const modules = [];

  // 如果dependencyMap为空,则返回一个空数组
  if (!dependencyMap) return [];

  // 遍历dependencyMap的每一个key
  for (let key in dependencyMap) {
    // 获取模块信息
    const moduleInfo = getModuleInfo(dependencyMap[key]);
    // 将模块信息添加到modules数组中
    modules.push(moduleInfo);
    // 如果模块信息中存在依赖,则递归加载依赖模块,并将加载的依赖模块添加到modules数组中
    if (moduleInfo.deps) modules.push(...loadModules(moduleInfo.deps));
  }

  // 返回加载的模块数组
  return modules;
}

// 加载入口模块,并递归加载依赖模块
const allModules = [entryModuleInfo].concat(loadModules(entryModuleInfo.deps));

console.log("[ allModules ] >", allModules);
  1. 运行 build,得到如下结果

6-2 工程化实战之模块化+Webpack

  1. 然后将数组结构转为对象结构,便于通过path取值
// 前面的不变....

// 加载入口模块,并递归加载依赖模块
const allModulesArray = [entryModuleInfo].concat(
  loadModules(entryModuleInfo.deps)
);

/**
 * 创建模块映射表
 *
 * @param modules 模块数组
 * @returns 返回模块路径为键,模块对象为值的映射表
 */
function createModuleMap(modules) {
  // 使用reduce方法遍历modules数组,并返回一个对象
  return modules.reduce((modulesMap, module) => {
    // 将module对象按照path属性作为键,module对象作为值存储到modulesMap对象中
    modulesMap[module.path] = module;
    // 返回更新后的modulesMap对象
    return modulesMap;
    // 初始值为一个空对象
  }, {});
}
const allModulesMap = createModuleMap(allModulesArray);
console.log("[ allModulesMap ] >", allModulesMap);
  1. 运行 build,得到如下结果

6-2 工程化实战之模块化+Webpack

(五)将本阶段的代码聚合到一个方法内 - parseModules
  1. webpack.js编码:将本阶段的代码聚合到一个方法内 - parseModules
/**
 * 功能设计:
 * 1. 找到主入口,即 src/index.js 文件,然后加载进来(getFileInfo(path) -> fileContent)
 * 2. 解析主入口的内容(parseFile(fileContent)),找到所有依赖,形成依赖关系(createDependencyMap(AST) -> dependencyMap)
 * 3. 在将 AST 转换成低版本的 JS 代码,(generateCode(AST))
 * 4. 基于依赖关系图,去加载对应的所有文件(loadModules(dependencyMap)),然后转为对象结构(createModuleMap(dependencyMap))
 */

// 主入口路径变量,目前写死
const entry = "./src/index.js";

// path 模块,获取文件路径
const path = require("path");

// fs 模块,读取文件内容
const fs = require("fs");

// @babel/parser 解析文件内容
const parser = require("@babel/parser");

// @babel/traverse 遍历抽象语法树(AST)
const traverse = require("@babel/traverse").default;

// @babel/generator 将 AST 转换成代码字符串
const babelCore = require("@babel/core");

/**
 * 获取模块信息
 *
 * @param _path 文件路径
 * @returns 包含文件路径、依赖关系图和生成代码的对象
 */
function getModuleInfo(_path) {
  /**
   * 获取文件信息
   *
   * @param path 文件路径
   * @returns 返回文件内容
   */
  function getFileInfo(path) {
    // 使用 fs.readFileSync 方法同步读取文件内容
    return fs.readFileSync(path, "utf-8");
  }

  /**
   * 解析文件内容并返回抽象语法树(AST)
   *
   * @param fileContent 文件内容
   * @returns 抽象语法树(AST)
   */
  function parseFile(fileContent) {
    // 解析文件内容,生成抽象语法树(AST)
    const ast = parser.parse(fileContent, {
      sourceType: "module", // 要解析的模块是 ESM
    });
    // 返回抽象语法树(AST)
    return ast;
  }

  /**
   * 创建依赖关系图
   *
   * @param ast 抽象语法树
   * @returns 依赖关系图
   */
  function createDependencyMap(ast) {
    // 创建依赖关系图
    let dependencyMap = null;

    // 遍历抽象语法树(AST)
    traverse(ast, {
      ImportDeclaration({ node }) {
        const { value } = node.source; // 从 AST 中获取到导入的相对文件路径

        const dirname = path.dirname(entry); // 获取存放主入口文件的文件名

        const abspath = "./" + path.join(dirname, value); // 拼接出每个导入文件的绝对路径

        if (!dependencyMap) dependencyMap = {};

        dependencyMap[value] = abspath; // 添加到依赖关系图
      },
    });

    return dependencyMap;
  }

  /**
   * 生成代码
   *
   * @param ast AST 对象
   * @returns 返回生成的代码字符串
   */
  function generateCode(ast) {
    // 使用 Babel 将抽象语法树(AST)转换为可执行的 JavaScript 代码
    const { code } = babelCore.transformFromAst(ast, null, {
      presets: ["@babel/preset-env"], // 指定转译的语法
    });

    // 返回生成的代码
    return code;
  }

  const _pathFileContent = getFileInfo(_path);
  const _pathFileContentAST = parseFile(_pathFileContent);
  const _pathFileDepsMap = createDependencyMap(_pathFileContentAST);
  const _pathFileCode = generateCode(_pathFileContentAST);

  return { path: _path, deps: _pathFileDepsMap, code: _pathFileCode };
}

function parseModules(moduleInfo) {
  /**
   * 加载模块
   *
   * @param dependencyMap 模块依赖映射表
   * @returns 返回加载的模块数组
   */
  function loadModules(dependencyMap) {
    const modules = [];

    // 如果dependencyMap为空,则返回一个空数组
    if (!dependencyMap) return [];

    // 遍历dependencyMap的每一个key
    for (let key in dependencyMap) {
      // 获取模块信息
      const _moduleInfo = getModuleInfo(dependencyMap[key]);
      // 将模块信息添加到modules数组中
      modules.push(_moduleInfo);
      // 如果模块信息中存在依赖,则递归加载依赖模块,并将加载的依赖模块添加到modules数组中
      if (_moduleInfo.deps) modules.push(...loadModules(_moduleInfo.deps));
    }

    // 返回加载的模块数组
    return modules;
  }

  /**
   * 创建模块映射表
   *
   * @param modules 模块数组
   * @returns 返回模块路径为键,模块对象为值的映射表
   */
  function createModuleMap(modules) {
    // 使用reduce方法遍历modules数组,并返回一个对象
    return modules.reduce((modulesMap, module) => {
      // 将module对象按照path属性作为键,module对象作为值存储到modulesMap对象中
      modulesMap[module.path] = module;
      // 返回更新后的modulesMap对象
      return modulesMap;
      // 初始值为一个空对象
    }, {});
  }

  const modulesArray = [moduleInfo].concat(loadModules(moduleInfo.deps));
  return createModuleMap(modulesArray);
}

const entryModuleInfo = getModuleInfo(entry);

const allModulesMap = parseModules(entryModuleInfo);

console.log("[ allModulesMap ] >", allModulesMap);

5、处理上下文

我们分析下打印出来的code,我们要求它能直接在浏览器中运行,但它里面有两个关键点reqiure(函数)、exports(对象),咋一看这是 CJS 的语法,肯定是不能在浏览器中运行的,所以我们需要分别给定义出reqiure、exports在浏览器上的上下文

6-2 工程化实战之模块化+Webpack

  1. webpack.js编码:注入上下文
// 前面的不变......


/**
 * 处理上下文,生成一个函数,该函数接受一个模块映射对象作为参数,
 * 并返回一个立即执行函数表达式,该函数内部定义了一个 require 函数,
 * 用于根据模块路径加载模块并执行模块代码,最后返回模块的导出对象。
 *
 * @param modulesMap 模块映射对象,键为模块路径,值为模块对象,
 * 模块对象包含两个属性:deps(依赖数组)和 code(模块代码字符串)。
 * @returns 返回一个立即执行函数表达式的字符串形式。
 */
function handleContext(modulesMap) {
  const modulesMapString = JSON.stringify(modulesMap);
  return `(function (modulesMap) {
    function require(path) {
      function absRequire(absPath) {
        return require(modulesMap[path].deps[absPath]);
      }

      var exports = {};

      (function (require, exports, code) {
        eval(code);
      })(absRequire, exports, modulesMap[path].code);

      return exports;
    }
    require('${entry}');
  })(${modulesMapString});`;
}

// 最终生成的 bundle.js 的代码字符串
const bundle_js_code_string = handleContext(allModulesMap);

6、生成 dist 与相关文件

// 前面的不变......


/**
 * 创建输出文件
 *
 * @param _output 输出文件路径和文件名
 * @param codeString 要写入的代码字符串
 */
function createOutPutFiles(_output, codeString) {
  function createFolder(path) {
    // 判断目录是否存在,如果存在则删除
    const isExist = fs.existsSync(path);
    if (isExist) fs.removeSync(path);

    // 创建目录
    fs.mkdirSync(path);
  }

  /**
   * 创建HTML文件
   *
   * @param path 文件路径
   * @param scriptSrc 脚本源路径
   */
  function createHTML(path, scriptSrc) {
    const htmlName = "index.html";
    // HTML 内容的字符串
    const htmlContent = fs.readFileSync(htmlName, "utf-8");

    // 找到合适的插入点,这里假设在 body 结束前插入
    const insertPointPattern = /</body>/i;
    const insertionPoint = htmlContent.search(insertPointPattern);

    if (insertionPoint !== -1) {
      // 创建 script 标签列表
      const scriptTags = `<script src="./${scriptSrc}"></script>`;

      // 插入 script 标签到 HTML 内容中
      const newHtmlContent = `${htmlContent.slice(0, insertionPoint)}
  ${scriptTags}
${htmlContent.slice(insertionPoint)}`;

      // 创建 html 文件
      const htmlPath = path + "/" + htmlName;
      fs.writeFileSync(htmlPath, newHtmlContent);
    }
  }

  const { path, filename } = _output;
  // 创建 输出目录
  createFolder(path);
  // 创建 bundle.js 文件
  fs.writeFileSync(path + "/" + filename, codeString);
  // 创建 index.html 文件
  createHTML(path, filename);
}

// 最终生成的 bundle.js 的代码字符串
const bundle_js_code_string = handleContext(allModulesMap);
createOutPutFiles(output, bundle_js_code_string);

7、代码完成,运行看效果

index.html完整代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>手写实现 Webpack</title>
  </head>
  <body>
    <div>我在手写实现 Webpack</div>
  </body>
</html>

webpack.js完整代码如下:

/**
 * 功能设计:
 * 1. 找到主入口,即 src/index.js 文件,然后加载进来(getFileInfo(path) -> fileContent)
 * 2. 解析主入口的内容(parseFile(fileContent)),找到所有依赖,形成依赖关系(createDependencyMap(AST) -> dependencyMap)
 * 3. 在将 AST 转换成低版本的 JS 代码,(generateCode(AST))
 * 4. 基于依赖关系图,去加载对应的所有文件(loadModules(dependencyMap)),然后转为对象结构(createModuleMap(dependencyMap))
 * 5. 处理上下文,注入 reqiure、exports 这两个变量的具体功能(handleContext(moduleMap))
 */

// 主入口路径变量,目前写死
const entry = "./src/index.js";
const output = { path: "_dist", filename: "bundle.js" };

// path 模块,获取文件路径
const path = require("path");

// fs 模块,读取文件内容
const fs = require("fs-extra");

// @babel/parser 解析文件内容
const parser = require("@babel/parser");

// @babel/traverse 遍历抽象语法树(AST)
const traverse = require("@babel/traverse").default;

// @babel/generator 将 AST 转换成代码字符串
const babelCore = require("@babel/core");

/**
 * 获取模块信息
 *
 * @param _path 文件路径
 * @returns 包含文件路径、依赖关系图和生成代码的对象
 */
function getModuleInfo(_path) {
  /**
   * 获取文件信息
   *
   * @param path 文件路径
   * @returns 返回文件内容
   */
  function getFileInfo(path) {
    // 使用 fs.readFileSync 方法同步读取文件内容
    return fs.readFileSync(path, "utf-8");
  }

  /**
   * 解析文件内容并返回抽象语法树(AST)
   *
   * @param fileContent 文件内容
   * @returns 抽象语法树(AST)
   */
  function parseFile(fileContent) {
    // 解析文件内容,生成抽象语法树(AST)
    const ast = parser.parse(fileContent, {
      sourceType: "module", // 要解析的模块是 ESM
    });
    // 返回抽象语法树(AST)
    return ast;
  }

  /**
   * 创建依赖关系图
   *
   * @param ast 抽象语法树
   * @returns 依赖关系图
   */
  function createDependencyMap(ast) {
    // 创建依赖关系图
    let dependencyMap = null;

    // 遍历抽象语法树(AST)
    traverse(ast, {
      ImportDeclaration({ node }) {
        const { value } = node.source; // 从 AST 中获取到导入的相对文件路径

        const dirname = path.dirname(entry); // 获取存放主入口文件的文件名

        const abspath = "./" + path.join(dirname, value); // 拼接出每个导入文件的绝对路径

        if (!dependencyMap) dependencyMap = {};

        dependencyMap[value] = abspath; // 添加到依赖关系图
      },
    });

    return dependencyMap;
  }

  /**
   * 生成代码
   *
   * @param ast AST 对象
   * @returns 返回生成的代码字符串
   */
  function generateCode(ast) {
    // 使用 Babel 将抽象语法树(AST)转换为可执行的 JavaScript 代码
    const { code } = babelCore.transformFromAst(ast, null, {
      presets: ["@babel/preset-env"], // 指定转译的语法
    });

    // 返回生成的代码
    return code;
  }

  const _pathFileContent = getFileInfo(_path);
  const _pathFileContentAST = parseFile(_pathFileContent);
  const _pathFileDepsMap = createDependencyMap(_pathFileContentAST);
  const _pathFileCode = generateCode(_pathFileContentAST);

  return { path: _path, deps: _pathFileDepsMap, code: _pathFileCode };
}

/**
 * 解析模块信息
 *
 * @param moduleInfo 模块信息
 * @returns 返回模块路径为键,模块对象为值的映射表
 */
function parseModules(moduleInfo) {
  /**
   * 加载模块
   *
   * @param dependencyMap 模块依赖映射表
   * @returns 返回加载的模块数组
   */
  function loadModules(dependencyMap) {
    const modules = [];

    // 如果dependencyMap为空,则返回一个空数组
    if (!dependencyMap) return [];

    // 遍历dependencyMap的每一个key
    for (let key in dependencyMap) {
      // 获取模块信息
      const _moduleInfo = getModuleInfo(dependencyMap[key]);
      // 将模块信息添加到modules数组中
      modules.push(_moduleInfo);
      // 如果模块信息中存在依赖,则递归加载依赖模块,并将加载的依赖模块添加到modules数组中
      if (_moduleInfo.deps) modules.push(...loadModules(_moduleInfo.deps));
    }

    // 返回加载的模块数组
    return modules;
  }

  /**
   * 创建模块映射表
   *
   * @param modules 模块数组
   * @returns 返回模块路径为键,模块对象为值的映射表
   */
  function createModuleMap(modules) {
    // 使用reduce方法遍历modules数组,并返回一个对象
    return modules.reduce((modulesMap, module) => {
      // 将module对象按照path属性作为键,module对象作为值存储到modulesMap对象中
      modulesMap[module.path] = module;
      // 返回更新后的modulesMap对象
      return modulesMap;
      // 初始值为一个空对象
    }, {});
  }

  // 加载入口模块,并递归加载依赖模块
  const modulesArray = [moduleInfo].concat(loadModules(moduleInfo.deps));
  return createModuleMap(modulesArray);
}

const entryModuleInfo = getModuleInfo(entry);

const allModulesMap = parseModules(entryModuleInfo);

/**
 * 处理上下文,生成一个函数,该函数接受一个模块映射对象作为参数,
 * 并返回一个立即执行函数表达式,该函数内部定义了一个 require 函数,
 * 用于根据模块路径加载模块并执行模块代码,最后返回模块的导出对象。
 *
 * @param modulesMap 模块映射对象,键为模块路径,值为模块对象,
 * 模块对象包含两个属性:deps(依赖数组)和 code(模块代码字符串)。
 * @returns 返回一个立即执行函数表达式的字符串形式。
 */
function handleContext(modulesMap) {
  const modulesMapString = JSON.stringify(modulesMap);
  return `(function (modulesMap) {
    function require(path) {
      function absRequire(absPath) {
        return require(modulesMap[path].deps[absPath]);
      }

      var exports = {};

      (function (require, exports, code) {
        eval(code);
      })(absRequire, exports, modulesMap[path].code);

      return exports;
    }
    require('${entry}');
  })(${modulesMapString});`;
}

/**
 * 创建输出文件
 *
 * @param _output 输出文件路径和文件名
 * @param codeString 要写入的代码字符串
 */
function createOutPutFiles(_output, codeString) {
  function createFolder(path) {
    // 判断目录是否存在,如果存在则删除
    const isExist = fs.existsSync(path);
    if (isExist) fs.removeSync(path);

    // 创建目录
    fs.mkdirSync(path);
  }

  /**
   * 创建HTML文件
   *
   * @param path 文件路径
   * @param scriptSrc 脚本源路径
   */
  function createHTML(path, scriptSrc) {
    const htmlName = "index.html";
    // HTML 内容的字符串
    const htmlContent = fs.readFileSync(htmlName, "utf-8");

    // 找到合适的插入点,这里假设在 body 结束前插入
    const insertPointPattern = /</body>/i;
    const insertionPoint = htmlContent.search(insertPointPattern);

    if (insertionPoint !== -1) {
      // 创建 script 标签列表
      const scriptTags = `<script src="./${scriptSrc}"></script>`;

      // 插入 script 标签到 HTML 内容中
      const newHtmlContent = `${htmlContent.slice(0, insertionPoint)}
  ${scriptTags}
${htmlContent.slice(insertionPoint)}`;

      // 创建 html 文件
      const htmlPath = path + "/" + htmlName;
      fs.writeFileSync(htmlPath, newHtmlContent);
    }
  }

  const { path, filename } = _output;
  // 创建 输出目录
  createFolder(path);
  // 创建 bundle.js 文件
  fs.writeFileSync(path + "/" + filename, codeString);
  // 创建 index.html 文件
  createHTML(path, filename);
}

// 最终生成的 bundle.js 的代码字符串
const bundle_js_code_string = handleContext(allModulesMap);
createOutPutFiles(output, bundle_js_code_string);
  1. 运行pnpm build,生成如下代码:

_dist/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>手写实现 Webpack</title>
  </head>
  <body>
    <div>我在手写实现 Webpack</div>

    <script src="./bundle.js"></script>
  </body>
</html>

_dist/bundle.js

(function (modulesMap) {
  function require(path) {
    function absRequire(absPath) {
      return require(modulesMap[path].deps[absPath]);
    }

    var exports = {};

    (function (require, exports, code) {
      eval(code);
    })(absRequire, exports, modulesMap[path].code);

    return exports;
  }
  require("./src/index.js");
})({
  "./src/index.js": {
    path: "./src/index.js",
    deps: { "./add.js": "./src/add.js", "./minus.js": "./src/minus.js" },
    code: '"use strict";\n\nvar _add = _interopRequireDefault(require("./add.js"));\nvar _minus = require("./minus.js");\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\nvar sum = (0, _add["default"])(1, 2);\nvar division = (0, _minus.minus)(2, 1);\nconsole.log("[ add(1, 2) ] >", sum);\nconsole.log("[ minus(2, 1) ] >", division);',
  },
  "./src/add.js": {
    path: "./src/add.js",
    deps: null,
    code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports["default"] = void 0;\nvar _default = exports["default"] = function _default(a, b) {\n  return a + b;\n};',
  },
  "./src/minus.js": {
    path: "./src/minus.js",
    deps: null,
    code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.minus = void 0;\nvar minus = exports.minus = function minus(a, b) {\n  return a - b;\n};',
  },
});
  1. 然后 Live Server 启动_dist/index.html6-2 工程化实战之模块化+Webpack

至此最简单的实现了手写 Webpack 功能

但真正的 Webpack 远远不止这么简单哈

总结

上面手动实现了一个最简单的 Webpack 打包功能,可以发现我们以前配的 Webpack 选项影子

比如:entry、output

然后 Webpack 强大的在于Loader、Plugin系统,你可以粗暴理解就是我们手写时引入的其他依赖(fs-extra、babel),帮我做更多的事情

只是 Webpack 的Loader、Plugin系统做的很完善和强大

Webpack 原生 Loader支持加载的文件有:JS 和 JSON,其他类型(css/svg 等)的就要安装对应的Loader来处理

Loader 简介

是对模块的源代码进行转换的,默认只能处理js、json,其他类型的css、txt、less 等需要专门的 Loader 进行转换处理。

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

Loader 是链式传递的,Webpack 会按顺序链式调用每个 Loader,Loader 的输入与输出都是字符串,并且每个 Loader 只应该做一件事并且无状态

less-loader: 将 less 文件处理后通过 style 标签渲染到页面上

Plugin 简介

在 Webpack 构建工程中,特定时间注入的扩展逻辑,用来改变或优化构建结果。

const HTMLWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  plugin: [ new HTMLWebpackPlugin({ template: './public/index.html'}) ]
}

自定义插件开发文档:自定义插件 | webpack 中文文档

核心就是采用固定格式:写一个类,再写一个apply方法,通过compiler.hooks[钩子名].tap(插件名称, 插件功能),然后重点写我们的插件功能即可。

插件功能就可以随意发挥了,它是运行在node环境下的,所以可以使用fs来创建你想要的文件,也可以使用jszipdist压缩为.zip等等,甚至可以使用axios调用接口干事情

比如写一个打包时,创建一个version.json文件的插件,用于表示本次的版本

emit

AsyncSeriesHook

输出 asset 到 output 目录之前执行。这个钩子 不会 被复制到子编译器。

  • 回调参数:compilation
// RawSource 是其中一种 “源码”("sources") 类型,
const { RawSource } = require("webpack-sources");

class VersionFilePlugin {
	apply(compiler) {
		compiler.hooks.emit.tap(VersionFilePlugin.name, (compilation) => {
			const version = `${Number(new Date())}_${Math.random().toString(36)}`;
			// 向 compilation 添加新的资源,这样 webpack 就会自动生成并输出到 outputFile 目录
			compilation.emitAsset(
				"version.json",
				new RawSource(JSON.stringify({ version })),
			);
		});
	}
}

module.exports = { VersionFilePlugin };


// 使用
const { VersionFilePlugin } = require("../webpack-plugin/versionFile");

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

输入结果如下:

6-2 工程化实战之模块化+Webpack6-2 工程化实战之模块化+Webpack

Chunk 简介

6-2 工程化实战之模块化+Webpack

Chunk:构建过程中产生的代码块,代表一组模块的集合。可通过分片技术生成不同的 chunks,最终生成不同的 bundle 文件

Tree Shaking 简介

官方文档:Tree Shaking | webpack 中文文档

Tree Shaking:“树摇”,将枯死的叶子摇掉。代码层面指:移除不使用的代码,可减少打包体积。

在 Webpack 中开启 Tree shaking 必须满足以下 3 个条件:

1、使用 ESM 写代码:import、export、export default

2、配置optimization.usedExports 为 true

3、启动优化功能,三选一

a、配置mode=production(常用的)

b、配置optimization.minimize = true

c、配置optimization.minimizer数组

原理

标记模块导出中未被使用的值,再使用terser来删除相关代码。

流程:分析 -> 标记 -> 清除

基于生成的依赖关系图,分析对应的关系;将未使用的导出变量,存储为标记依赖图;生成代码时进行清除。

Webpack 5 的新增功能:

1、新增了cache属性,可支持本地缓存编译结果,提供构建性能

2、内置了静态资源(如图片、字体等)的官方 Loader

3、提升了 Tree Shaking 能力

4、增加了模块联邦,支持共享代码模块

一些优化思路

思路1:先确定需要进行哪些优化,可基于 Webpack 的配置:resolve、module、externals、plugins 等

思路2:优化产物体积,利用webpack-bundle-analyzer插件进行分析

6-2 工程化实战之模块化+Webpack

思路3:优化构建速度,利用speed-measure-webpack-plugin插件进行分析

6-2 工程化实战之模块化+Webpack

一些优化操作

1、配置cache属性,会缓存生成的 webpack 模块和 chunk,来改善构建速度。Webpack5 之前使用专门的cache-loader来缓存

2、配置externals属性,防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。可减少产物体积

3、配置resolve.alias属性,这样Utilities是一个绝对路径的别名,有助于降低解析文件的成本

const path = require('path');

module.exports = {
  //...
  resolve: {
    alias: {
      Utilities: path.resolve(__dirname, 'src/utilities/'),
      Templates: path.resolve(__dirname, 'src/templates/'),
    },
  },
};

// import Utility from '../../utilities/utility';
import Utility from 'Utilities/utility';

4、配置resolve.mainFields属性,影响 Webpack 搜索第三方库的顺序。一般 npm 库使用的是main

module.exports = {
  //...
  resolve: {
    mainFields: ['main'], // 默认为 ['browser', 'module', 'main']
  },
};

5、配置resolve.extensions 属性,影响 Webpack 解析文件的顺序,将高频文件类型放在前面。

module.exports = {
  //...
  resolve: {
    extensions: ['.js', '.json', '.wasm'],
  },
};

以上优化代码:

const path = require('path')

module.exports = {
  // ...
  
  resolve: {
    alias: {
      "@": path.resolve(__dirname, './src'),
      "utils": path.resolve(__dirname, './src/utils')
    },

    externals: {
      react: 'React',
    },
    
    mainFields: ['main'],
    extensions: ['.js', '.jsx']
  }
}

Vite

新一代构建工具。

核心分为两个阶段:开发环境使用 Esbuild(干的事跟 Webpack 一样,速度却更快);生成环境使用 Rollup

开发环境时:类似于Webpack + Webpack Dev Server Plugin的集合,Vite它自带Dev Server,当你采用ESM导入模块时,自建的Dev Server就给你按需编译(Esbuild)然后返回,这样就跳过了整体的打包流程,所以本地开发很快;

生成环境时:使用 Rollup 将代码打包成 bundle

那为什么要使用两个构建工具呢?

1、因为 Esbuild 不支持

1、不支持降级到 es5 的代码,低版本浏览器跑不起来(es6/es7+)。

2、不支持 const、enum 等语法,会报错

3、打包不够灵活:无法配置打包流程、不支持代码分割

2、所以生成环境要使用其他工具,然后 Rollup 比Webpack 简单高效一些,所以用了 Rollup

包管理工具

Lerna:管理多版本的 npm,文档:lerna.js.org/docs/introd…

Verdaccio:私有的 npm 代理仓库,文档:What is Verdaccio? | Verdaccio