模块化
定义:将代码按照功能划分为独立、可复用的单元,每个单元称为一个模块。
发展历程
无模块
将 JS 代码直接在 html 里面按顺序引入
javascript
// 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 里面关联
javascript
// 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:浏览器端的模块加载规范,可支持异步,语法为如下:
javascript
// 定义一个简单的模块,无依赖
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 三种格式的语法
javascript
(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
,终结了混乱的模块加载规范
javascript
// 导出单个函数或变量
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 端提出的模块加载机制,不支持异步,不支持浏览器,每个文件都有自己的作用域。
导出语法:
csharp
// add.js
function add() {}
module.exports = { add }
// or
exports.add = add
导入语法,require
永远引入module.exports
的值,对应 JS 文件可省略文件后缀
csharp
const { add } = require('./add.js')
特点
动态(同步):当代码执行到require
那行时才去加载对应的文件并执行文件内容,可以理解为是"同步"的
reqiure
伪代码实现(node 端),所以它的是同步,并且是对值的 "拷贝"
scss
function require(filePath) {
const content = fs.readFileSync(filePath);
return eval(content);
}
对值的 "拷贝" 代码展示:
javascript
// 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
sql
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
的值
sql
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,则形成了循环依赖
javascript
// 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,支持异步、同步
导出语法
javascript
// add.js
export function add() {}
export function subtract() {}
导入语法,对应 JS 文件默认不可省略文件后缀,除非有配置
csharp
import { add, subtract } from './add.js'
特点
静态(异步):是因为 ESM 的核心流程是分成三步(构建、实例化、求值),并且可以分别完成,所以称为异步
为什么要分成三个,不能直接一起吗?因为浏览器加载执行 JS 时会阻塞主线程,造成的后果很大。
构建:根据import
创建模块之间的依赖关系图(编译时输出),然后下载模块文件生成模块记录(记录importName、importUrl)
实例化:基于生成的模块记录,找到模块的代码与导出的变量名,然后将相同导入、导出指向同一个地址
求值:运行模块的代码,将值赋到实例化
后的地址内
对值的 "引用" 代码展示:
javascript
// 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
javascript
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
语法)
javascript
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
导出的只能解构使用
对循环依赖的处理
css
// 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 打包基本原理
初始化项目
- 随便创建个项目文件,然后创建
src
文件夹与空文件
bash
mkdir src && touch src/add.js && touch src/minus.js && touch src/index.js && touch index.html
- 初始化项目
pnpm init
,然后安装依赖pnpm add fs-extra
src/add.js
写入相关代码
css
export default (a, b) => a + b;
src/minus.js
写入相关代码
css
export const minus = (a, b) => a - b;
src/index.js
写入相关代码
ini
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);
index.html
写入相关代码
xml
<!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>
- 然后使用 VScode 的 Live Server 启动
index.html
看效果
肯定是报错的,因为我们的<script
没加type="module"
我们期望正确的结果是:
原理实现
Webpack 的主要作用是从入口开始就一系列的依赖文件,最终打包成一个文件,这也是我们要实现的功能。
我们常用的打包命令是:npm run build
- 新建一个
webpack.js
作为手写 webpack
的入口(这个会基于 node 环境去运行的哦)
bash
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
后,最终浏览器能正确运行并打印:
1、基于主入口,读取文件
webpack.js
编码:基于主入口,读取文件
php
/**
* 功能设计:
* 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);
- 运行
node webpack.js
调试,发现已拿到主入口对应的代码了
- 将
node webpack.js
放到package.json
中,之后就可以pnpm build
执行了
2、基于入口文件内容,去分析依赖关系
(一)解析为 AST
第一步拿到了入口文件的内容,我们就需要去分析其中的依赖关系,即import
关键词。
如何分析呢?要么原始的通过字符串匹配import
然后分析;要么借用其他工具帮我们解析与分析
这里采用Babel
工具来帮我们分析
什么是 Babel?JS 编译器,可将高版本转为低版本的 JS
流程为:解析(将源代码转为 AST)、转换(对 AST 进行增删改查)、生成(将 AST 转为 JS 代码)
所以我们可以利用它来帮我们解析 JS 代码
- 安装
@babel/parser
依赖,官方使用文档:@babel/parser · Babel
sql
pnpm add @babel/parser
- 引入并使用
php
/**
* 功能设计:
* 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);
- 然后运行
pnpm build
,看下打印 AST 的结构
可以发现是正确打印了,但是一些关键信息被隐藏了,比如 body 里面的
这时候就可以借助 AST 在线工具,将我们的代码拷贝进去,看完整的结构:
(二)分析 AST,形成依赖图
还是使用工具,帮我直接分析依赖:@babel/traverse · Babel
- 安装
@babel/traverse
依赖
sql
pnpm add @babel/traverse
webpack.js
编码:分析 AST,形成依赖图
php
/**
* 功能设计:
* 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);
- 运行
pnpm build
,可以看到打印的依赖图
3、再将 AST 转换为低版本的 JS 代码
因为我们之前写的代码都是高版本的,所以有些浏览器不一定能识别,因此需要将其转为低版本代码,并且该低代码最终会在浏览器中运行哦
- 安装
bable
相关依赖
sql
pnpm add babel @babel/preset-env @babel/core
(三)将 AST 转换为低版本的 JS 代码
webpack.js
编码:将 AST 转换为低版本的 JS 代码
php
/**
* 功能设计:
* 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);
- 运行
pnpm build
,可以看到打印的 code
考点:"use strict"是什么?
"use strict"
是 ES5 的严格模式,JS 解释器将采用更严格的规则来解析和执行代码,目的是消除常见错误与禁用不安全的操作(因为 JS 太灵活了)
- 变量名不能重复使用 var 声明
- eval 不能使用
- 变量必须先声明再使用
- 函数内部的 this 不会默认绑到全局对象上
- 对象属性名不能重复
- 函数参数名不能重复
- 等等
(四)将上述代码聚合到一个方法内 - getModuleInfo
webpack.js
编码:将上述代码聚合到一个方法内 -getModuleInfo
php
/**
* 功能设计:
* 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);
- 运行 build,得到如下结果
4、基于依赖关系图,去加载对应的所有文件
webpack.js
编码:基于依赖关系图,去加载对应的所有文件
scss
// 前面的不变....
/**
* 加载模块
*
* @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);
- 运行 build,得到如下结果
- 然后将数组结构转为对象结构,便于通过
path
取值
javascript
// 前面的不变....
// 加载入口模块,并递归加载依赖模块
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);
- 运行 build,得到如下结果
(五)将本阶段的代码聚合到一个方法内 - parseModules
webpack.js
编码:将本阶段的代码聚合到一个方法内 -parseModules
php
/**
* 功能设计:
* 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
在浏览器上的上下文
webpack.js
编码:注入上下文
php
// 前面的不变......
/**
* 处理上下文,生成一个函数,该函数接受一个模块映射对象作为参数,
* 并返回一个立即执行函数表达式,该函数内部定义了一个 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 与相关文件
javascript
// 前面的不变......
/**
* 创建输出文件
*
* @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
完整代码如下:
xml
<!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
完整代码如下:
php
/**
* 功能设计:
* 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);
- 运行
pnpm build
,生成如下代码:
_dist/index.html
:
xml
<!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
:
javascript
(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};',
},
});
- 然后 Live Server 启动
_dist/index.html
至此最简单的实现了手写 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 进行转换处理。
css
module.exports = {
module: {
rules:[ { test:/.less$/, use: 'less-loader'} ]
}
}
Loader 是链式传递的,Webpack 会按顺序链式调用每个 Loader,Loader 的输入与输出都是字符串,并且每个 Loader 只应该做一件事并且无状态
less-loader: 将 less 文件处理后通过 style 标签渲染到页面上
Plugin 简介
在 Webpack 构建工程中,特定时间注入的扩展逻辑,用来改变或优化构建结果。
arduino
const HTMLWebpackPlugin = require('html-webpack-plugin')
module.exports = {
plugin: [ new HTMLWebpackPlugin({ template: './public/index.html'}) ]
}
自定义插件开发文档:自定义插件 | webpack 中文文档
核心就是采用固定格式:写一个类,再写一个apply
方法,通过compiler.hooks[钩子名].tap(插件名称, 插件功能)
,然后重点写我们的插件功能
即可。
插件功能
就可以随意发挥了,它是运行在node
环境下的,所以可以使用fs
来创建你想要的文件,也可以使用jszip
将dist
压缩为.zip
等等,甚至可以使用axios
调用接口干事情
比如写一个打包时,创建一个version.json
文件的插件,用于表示本次的版本
emit
AsyncSeriesHook
输出 asset 到 output 目录之前执行。这个钩子 不会 被复制到子编译器。
- 回调参数:compilation
javascript
// 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()],
}
输入结果如下:
Chunk 简介
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
插件进行分析
思路3 :优化构建速度,利用speed-measure-webpack-plugin
插件进行分析
一些优化操作
1、配置cache
属性,会缓存生成的 webpack 模块和 chunk,来改善构建速度 。Webpack5 之前使用专门的cache-loader
来缓存
2、配置externals
属性,防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。可减少产物体积
3、配置resolve.alias
属性,这样Utilities
是一个绝对路径的别名,有助于降低解析文件的成本
javascript
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
ruby
module.exports = {
//...
resolve: {
mainFields: ['main'], // 默认为 ['browser', 'module', 'main']
},
};
5、配置resolve.extensions
属性,影响 Webpack 解析文件的顺序,将高频文件类型放在前面。
java
module.exports = {
//...
resolve: {
extensions: ['.js', '.json', '.wasm'],
},
};
以上优化代码:
lua
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