2024-12-15 08:00:00
每次学习中碰到 k8s,都会被它的复杂性难住。现在网上的教程,要么直接指定 image,省略了 yaml 文件的编写,要么是只有应用节点,没有数据库节点,这就和实际生产环境脱离。所以,我决定自己从头搭建一个集群,整合 Nodejs 和 mysql,打造一个最小示例,作为备忘记录。
环境为 win11 + docker desktop + minikube。minikube start
。
服务只能手动打造了。本地建立一个 nodejs-app 文件夹,然后添加 app.js:
const express = require("express");
const mysql = require("mysql2");
const app = express();
const port = 3000;
const db = mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
});
db.connect((err) => {
if (err) {
console.error("Error connecting to MySQL:", err.stack);
return;
}
console.log("Connected to MySQL");
});
app.get("/", (req, res) => {
db.query("SELECT NOW()", (err, results) => {
if (err) {
res.status(500).send("Error querying database");
} else {
res.send(`Current time from MySQL: ${results[0]["NOW()"]}`);
}
});
});
app.listen(port, () => {
console.log(`Node.js app listening at http://localhost:${port}`);
});
然后在根目录下,添加 Dockerfile:
FROM node:14-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
ENV DB_HOST=mysql
ENV DB_USER=testuser
ENV DB_PASSWORD=testpassword
ENV DB_NAME=testdb
EXPOSE 3000
CMD ["node", "app.js"]
接下来是把镜像推送到 dockerhub 上。docker login 后,运行docker build -t nodejs-app:latest .
打造镜像,docker push nodejs-app xxx/nodejs-app:latest
推送到 dockerhub。
其实还有一种办法可以直接把镜像加载到 minikube 中,但是这次我没有成功。步骤如下:
docker build -t nodejs-app:latest .
docker save -o nodejs-app.tar nodejs-app:latest
minikube image load nodejs-app:latest
两份 yaml 文件分别对应 nodejs 服务和 mysql。两份都包含 deployment 和 service。
// nodejs-depl.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nodejs-app
labels:
app: nodejs-app
spec:
replicas: 3
selector:
matchLabels:
app: nodejs-app
template:
metadata:
labels:
app: nodejs-app
spec:
containers:
- name: nodejs-app
image: lfy123/nodejs-app:latest
env:
- name: DB_HOST
value: "mysql"
- name: DB_USER
value: "testuser"
- name: DB_PASSWORD
value: "testpassword"
- name: DB_NAME
value: "testdb"
ports:
- containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
name: nodejs-service
spec:
selector:
app: nodejs-app
ports:
- protocol: TCP
port: 3000
targetPort: 3000
nodePort: 30080 # 指定一个 NodePort
type: NodePort
// mysql-depl.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql
labels:
app: mysql
spec:
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
env:
- name: MYSQL_ROOT_PASSWORD
value: "rootpassword"
- name: MYSQL_DATABASE
value: "testdb"
- name: MYSQL_USER
value: "testuser"
- name: MYSQL_PASSWORD
value: "testpassword"
ports:
- containerPort: 3306
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
volumes:
- name: mysql-data
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
selector:
app: mysql
ports:
- protocol: TCP
port: 3306
targetPort: 3306
kubectl apply -f mysql-depl.yaml
kubectl apply -f nodejs-depl.yaml
验证:
kubectl get pods -o wide
因为我们是 minikube,要想在浏览器访问服务,就需要借助 minikube 的 tunnel 功能。
minikube service nodejs-service
2024-12-14 08:00:00
这阵子学习《Nodejs 实战》的时候,有看到压力测试的话题,恰好最近工作中也遇到一些高负载场景,所以把这些知识和工具整理出来。
首先明确我们做压力测试的目的:获得系统的承受能力上限,找到 bottleneck,然后针对 bottleneck 做优化。一般程序能在两个方面做出优化:1. 吞吐量,即每秒处理多少请求;2. 响应时间,即请求处理完到返回给客户端的时间。系统资源指标是 cpu 占用率和内存占用率。
所有的压测工具都会给出都会返回一个 benchmark 结果,包括:
一般来说,我们需要关注 TP99 的 QPS 和延迟,因为这个分位能够反应出系统在压力下的吞吐能力,也是很多 SLA(服务级别协议)所定的性能指标。
autocannon 是 Nodejs 社区最流行的压测工具,安装和运行有两种方式:
autocannon -c 100 -d 10 http://localhost:3000/
这种需要全局安装:npm install -g autocannon
. 这种方式能在控制台打印出 benchmark 的结果,比较美观。
const autocannon = require('autocannon')
const url = 'http://localhost:3000/'
const instance = autocannon({
url,
connections: 100,
duration: 10,
requests: [
{
method: 'GET',
path: '/'
headers: {
'User-Agent': 'autocannon'
'Content-Type': 'application/json'
},
},
]
})
instance.on('done', (results) => {
console.log(results)
// console.log('qps tp99: ', results.requests.p1)
// console.log('latency tp99: ', results.latency.p99)
});
当你的请求需要 token 等复杂的请求头时,这种脚本形式显然更方便。
Nodejs 依靠非阻塞 IO 和事件循环机制能在单线程下实现高并发。它能取得高并发的前提是让事件循环尽可能活跃,因此要减少 cpu 阻塞任务的执行时间。这方面最常用的做法是空间换时间(如把接口中的 cpu 阻塞任务的结果缓存起来),以及把任务 offload 到 worker threads 中。
另外,利用 pm2 工具起一个多核多进程的集群,让每个进程都跑在单独的线程上,这样能充分利用多核。最佳实践是把 pm2 的进程数设置为 CPU 核数的一半。
需要借助工具配合压测来观察 cpu 和内存占用率。Nodejs 是基于 V8 引擎的,chrome 也同样内置了 V8,所以可以借助 chrome dev tool 来对 Nodejs 进行调试。对入口文件运行node --inspect-brk index.js
,返回一个 ws 链接,去到 chrome 浏览器的地址栏,输入chrome://inspect
, 就能看到 Nodejs 进程了。具体参照下面的链接:
2024-11-23 08:00:00
掌握任何一个真实项目,必然要以熟悉编程语言的模块系统为前提。Go 的模块系统有一些独特之处,在这里做一下完整记录。
windows 安装 Go 时,有一个步骤是设置 GOPATH 环境变量,值是工作空间路径,默认可以设置为 C:\Users\<用户名>\go
。早期版本,Go 项目只能放在 GOPATH 目录下。从 1.13 版本开始,Go Mudules 成为默认的依赖管理方式。Modules 规定了内部模块的命名和依赖、项目版本管理和外部依赖。如果项目使用 Go Modules,GOPATH 主要用于存放下载的依赖包(缓存)等内容。
在项目的根目录下运行以下命令:
go mod init <module-name>
<module-name>
通常是项目的模块路径(比如 Git 仓库地址),例如:go mod init github.com/username/projectname
go mod init myproject
运行完成后,会生成一个 go.mod
文件,内容如下:
module myproject
go 1.20 // 你的 Go 版本
你可以在当前目录下自由组织代码,比如:
myproject/
├── go.mod
├── main.go
└── pkg/
└── mylib/
└── mylib.go
在 main.go
中可以正常导入包:
package main
import "myproject/pkg/mylib" // 路径以项目名开头
func main() {
mylib.Hello()
}
如果项目需要引入外部依赖库,直接在代码中使用 import
,然后运行以下命令安装依赖:
go mod tidy
这会更新 go.mod
并生成一个 go.sum
文件,用于下载的校验。
运行项目时,Go 会自动处理依赖:
go run main.go
或者生成二进制文件:
go build
项目源码和依赖会被分开存储:
项目源码保存在你定义的目录中(不受 GOPATH
限制)。
依赖库缓存保存在 GOPATH/pkg/mod
目录中。
go get
命令也是go modules系统中的一个命令,用来管理依赖包。运行它可以下载和更新依赖。例如,要更新依赖库,你可以运行:
go get -u <dependency-name>@version
go get
也会自动更新go.mod
和go.sum
。
本节内容引用自:
golang 里面有两个保留的函数:init 函数(能够应用于所有的 package)和 main 函数(只能应用于 package main)。这两个函数在定义时不能有任何的参数和返回值。
虽然一个 package 里面可以写任意多个 init 函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个 package 中每个文件只写一个 init 函数。
go 程序会自动调用 init()和 main(),所以你不需要在任何地方调用这两个函数。每个 package 中的 init 函数都是可选的,但 package main 就必须包含一个 main 函数。
程序的初始化和执行都起始于 main 包。如果 main 包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到 fmt 包,但它只会被导入一次,因为没有必要导入多次)。
当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行 init 函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对 main 包中的包级常量和变量进行初始化,然后执行 main 包中的 init 函数(如果存在的话),最后执行 main 函数。下图详细地解释了整个执行过程:
在 Go 语言中,我们可以用 _
作为匿名导入,这样可以避免导入一些不需要的包。例如:
import (
"fmt"
_ "moduleTest/lib1"
)
这样,我们只导入了 fmt
包,而 lib1
包中的代码就不会被执行。但是lib1中的init函数仍会被执行。
如果我们想导入一个包,但又想给它起一个别名,可以在同样位置加上别名。例如:
import (
"fmt"
we2 "moduleTest/lib2"
)
2024-11-21 08:00:00
这篇文章,我打算从 js 有哪些实现计算密集型任务的手段出发,延伸到进程和线程,再到 golang, 从而对并发有一个深刻的认知。
需求是这样的,一个 Nodejs web 后端,需要执行一个数据处理步骤,在内存中进行,非常耗时。项目用 pm2 起了四个服务,用 pm2 的观测命令可以看到,每次场景会把一个服务的 cpu 占满,运行四次就会把四个服务都占满,后面整个服务就瘫痪了,无法处理新的请求。
我们都知道,js 是基于事件循环机制来实现非堵塞 IO 效果的,且 js 是单线程运行。这意味着,JS 一个时刻只能处理一件事。如果一个任务占据了 js 线程过长时间,js 就不能处理后续的请求,表现就是服务挂掉了。因此,cpu 密集任务在 js 中是一个致命问题。
worker threads
和 child process
既然这一切是单线程的锅,那么如果有新的线程或者进程来处理这个 cpu 密集任务,把主 js 线程解放出来,这样服务就能正常处理请求了。所以,引入 js 中两个处理并发计算的工具:worker threads
和child process
。顾名思义,前者是新线程,后者是新进程。
这里顺便复习一下线程和进程的关系。就记住一句话,进程大于线程,进程之间互相隔离,线程之间共享内存。一个 Nodejs 进程,默认只有一个主线程,不过也有别的线程,比如 I/O 线程,还有我们即将谈到的工作线程(worker threads
)。
废话少说,直接上代码。这里我们定义一个朴实无华的 cpu 密集任务,即 1e10 级别的累加任务:
// CPU 密集型任务:计算 1 到 N 的总和
function heavyComputation(n) {
let sum = 0;
for (let i = 1; i <= n; i++) {
sum += 1;
}
return sum;
}
worker
的代码如下:
// worker.js
const { parentPort, workerData } = require("worker_threads");
// CPU 密集型任务:计算 1 到 N 的总和
function heavyComputation(n) {
let sum = 0;
for (let i = 1; i <= n; i++) {
sum += 1;
}
return sum;
}
// 计算并发送结果回主线程
const result = heavyComputation(workerData);
parentPort.postMessage(result);
// -----------------
// main.js
const { Worker } = require("worker_threads");
function runWorker(workerData) {
return new Promise((resolve, reject) => {
const worker = new Worker("./worker.js", { workerData });
worker.on("message", resolve); // 接收子线程的结果
worker.on("error", reject); // 子线程发生错误
worker.on("exit", (code) => {
if (code !== 0) {
reject(new Error(`Worker stopped with exit code ${code}`));
}
});
});
}
// 调用 worker 执行 CPU 密集型任务
(async () => {
try {
console.time("1");
console.log("Main thread: Starting CPU-intensive task...");
const result = await runWorker(1e10); // 计算 1 到 100000000 的总和
console.log(`Main thread: Task result is ${result}`);
console.timeEnd("1");
} catch (error) {
console.error("Error:", error);
}
})();
console.log("other task");
执行结果:
Main thread: Starting CPU-intensive task...
other task
Main thread: Task result is 10000000000
1: 11.216s
注意上面这种写法是把 worker 包进一个 promise 里面,这样 js 就把 runWorker 作为一个异步事件处理,并放入事件队列。当 resolve 被调用时,说明 worker 执行完毕,js 就会把回调事件 resolve 从任务队列中取出执行。在上面的代码中,worker.on("message", resolve)
等同于worker.on("message", (result)=>{resolve(result)})
。
回到 worker,它就是启动一个线程用于执行 cpu 密集任务,从而将主线程解放出来。
// compute.js
process.on("message", (n) => {
// CPU 密集型任务:计算 1 到 N 的总和
function heavyComputation(n) {
let sum = 0;
for (let i = 1; i <= n; i++) {
sum += 1;
}
return sum;
}
const result = heavyComputation(n);
process.send(result); // 将结果发送回主进程
});
// ------------------------
// main.js
const { fork } = require("child_process");
// 创建子进程
const computeProcess = fork("./compute.js");
// 监听子进程消息
computeProcess.on("message", (result) => {
console.log(`Main process: Task result is ${result}`);
console.timeEnd("1");
computeProcess.kill(); // 任务完成后杀死子进程
});
// 发送任务数据到子进程
console.time("1");
console.log("Main process: Starting CPU-intensive task...");
computeProcess.send(1e10);
console.log("other task");
打印结果:
Main process: Starting CPU-intensive task...
other task
Main process: Task result is 10000000000
1: 11.116s
可以看出,两者执行事件大致相同。
进程由于其相互间的隔离性较好,所以适合在执行外部程序或脚本时使用,或者需要 standard input/output 作为消息传递的场景。
线程则因其上下文切换开销小,故而是 cpu 密集任务的首选。
首先对并发和并行的概念做一下区分。
并发是cpu的一个核上进行多个任务执行,而并行则是多个核同时执行多个任务。有赖于现代cpu的核心数越来越多,并行是提高程序执行效率的手段之一。
在上面的例子中,因为程序恰好可以分割成各个互不相关的子任务,所以可以利用多核执行,从而提高效率。所以,worker_threads
的代码可以进一步改造为:
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
const os = require('os');
// 定义一个计算函数(在 Worker 中运行)
function calculateRange(start, end) {
let sum = 0;
for (let i = start; i <= end; i++) {
sum += 1;
}
return sum;
}
// Worker 逻辑
if (!isMainThread) {
const { start, end } = workerData;
const result = calculateRange(start, end);
parentPort.postMessage(result); // 将结果返回给主线程
}
// 主线程逻辑
if (isMainThread) {
const total = 1e10;
console.log('core nums:', os.cpus().length)
const threads = os.cpus().length; // 设置 Worker 数量
const range = Math.ceil(total / threads);
let completed = 0;
let finalSum = 0;
console.time('1')
for (let i = 0; i < threads; i++) {
const start = i * range + 1;
const end = i === threads - 1 ? total : (i + 1) * range;
const worker = new Worker(__filename, { workerData: { start, end } });
worker.on('message', (result) => {
finalSum += result;
completed++;
if (completed === threads) {
console.log(`Final Sum: ${finalSum}`);
console.timeEnd('1')
}
});
worker.on('error', (err) => console.error(err));
worker.on('exit', (code) => {
if (code !== 0) console.error(`Worker stopped with exit code ${code}`);
});
}
}
打印结果:
core nums: 16
Final Sum: 10000000000
1: 1.295s
在网络服务场景中,如果一个接口里面包含cpu密集任务,每次请求都要启动和销毁这么多的worker,会耗费额外资源。因此,可以借助线程池的概念进行优化,比如引入外部库
workerpool
。
Go 是一门编译型语言,且拥有非凡的性能。把上面的累加任务用 go 的写法:
package main
import (
"fmt"
"time"
)
func main() {
s := 0
start := time.Now()
for i := 0; i < 1e10; i++ {
s += 1
}
end := time.Since(start)
fmt.Println("1", end)
}
结果只用了 6 秒钟!只有 node 的一半!那么,如果通过 child_process 来调用 go 编译的二进制文件,速度如何呢?
把上面的 child process 的 main.js 改造成:
const { spawn } = require("child_process");
// 定义计算任务参数
const target = 1e10;
// 创建子进程调用 Go 编译的二进制文件
console.time("1");
console.log("Main process: Starting CPU-intensive task...");
const computeProcess = spawn("./compute.exe", [target.toString()]); // 假设编译后的文件名为 `compute`
computeProcess.stdout.on("data", (data) => {
console.log(`Main process: Task result is ${data}`);
});
computeProcess.stderr.on("data", (err) => {
console.error(`Main process: Error occurred: ${err}`);
});
computeProcess.on("close", (code) => {
console.timeEnd("1");
console.log(`Main process: Subprocess exited with code ${code}`);
});
console.log("other task");
同时编写 go 程序:
package main
import (
"fmt"
"os"
"strconv"
)
func main() {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "Error: Missing argument")
os.Exit(1)
}
// 将输入参数转换为整数
n, err := strconv.ParseInt(os.Args[1], 10, 64)
if err != nil {
fmt.Fprintln(os.Stderr, "Error: Invalid number")
os.Exit(1)
}
// 计算从 1 到 n 的总和
var sum int64
for i := int64(1); i <= n; i++ {
sum += 1
}
// 输出结果
fmt.Println(sum)
}
在终端中运行以下命令,将 compute.go 编译为二进制文件:
go build -o compute.exe compute.go
node main.js
运行主程序,结果:
Main process: Starting CPU-intensive task...
other task
Main process: Task result is 10000000000
1: 3.379s
Main process: Subprocess exited with code 0
时间缩短到约 1/3。
上面提到的worker_threads可以利用并行计算的优势,而Go更是这方面的专家。借助于goroutine,Go能创建出多个并发执行。
package main
import (
"fmt"
"sync"
)
func main() {
// 定义计数范围和批次
total := 10000000000
batchCount := 100
batchSize := total / batchCount
// 创建 WaitGroup 和结果 channel
var wg sync.WaitGroup
results := make(chan int, batchCount)
// 启动 goroutine 分批计算
for i := 0; i < batchCount; i++ {
start := i*batchSize + 1
end := (i + 1) * batchSize
wg.Add(1)
go func(start, end int) {
defer wg.Done()
sum := 0
for j := start; j <= end; j++ {
sum += 1
}
results <- sum
// fmt.Printf("Batch %d-%d calculated sum: %d\n", start, end, sum)
}(start, end)
}
// 等待所有 goroutines 完成并关闭结果 channel
go func() {
wg.Wait()
close(results)
}()
// 汇总所有批次的结果
totalSum := 0
for sum := range results {
totalSum += sum
}
// fmt.Printf("Total sum from 1 to %d is: %d\n", total, totalSum)
fmt.Println(totalSum)
}
执行时间仅需 538.356ms.
Nodejs 一直有一种说法,也就是擅于 IO 密集型任务,而不是 cpu 密集任务。这篇文章用详细的例子印证了这个说法,但是不仅于此,而是用worker_threads核child_process探索更多nodejs执行此类任务的 潜力。经实验表明,借助于工作线程核并行运算,速度能提升10倍。另外,与 Go 语言做了比较,发现 Go 的执行效率比 Nodejs 高了很多。
参考文章:
2024-11-13 08:00:00
url base64 编码有特殊逻辑。
需求:url query 的值用 base64 加密。
背景知识:
浏览器会把任何非 ascii 字符转化为 utf-8 字符,包括中文、特殊符号等。转换后的形式为以百分号后跟两位十六进制为单位的序列。
base64 编码/解码:编码过程,输入是二进制数,输出是A-Z, a-z, 0-9, +, /
的组合。
一开始,用 js 中的 btoa/atob 和 encodeURIComponent 就能将 base64 编码和解码 url param 参数的过程实现出来。如下:
function encodeBase64(input) {
return btoa(encodeURIComponent(input)); // 标准Base64编码
}
function decodeBase64(urlSafeBase64) {
return decodeURIComponent(atob(base64)); // 解码为原始字符串
}
这里有一个小细节。也许你会认为 url param 传到后端,后端拿到的是 utf-8 格式的 value。实际上,后端会自动将 utf-8 转回到原始字符。比如:
const { ask } = query;
console.log("ask", ask);
ask 是 query 中的一个参数。假如是中文“你好”,那么这里打印的就是“你好”。因为 btoa 只接受 ascii 字符串,所以,encodeURIComponent 是必须的。
但是,上线后很快就报错了。报错信息为:InvalidCharacterError: Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.
. 也就是解码时没有收到正确编码的字符串。原因是传统的 Base64 编码中使用的字符(例如 +
、/
和 =
)在 URL 中具有特殊含义或保留用途,可能会导致传输或解析上的问题。具体来说:
+
和 /
字符 :在 URL 中有特殊含义,+
通常代表空格,而 /
则是路径分隔符。使用这些字符时,可能会被误解或需要额外的转义。
=
字符 :传统 Base64 编码的结果会在末尾填充 =
,用于对齐编码块的长度。这些 =
在 URL 传递中通常不需要,而且在 URL 中也会增加不便。
URL 安全的 Base64 编码将传统 Base64 编码中的字符替换为 URL 兼容的字符:
把 +
替换为 -
把 /
替换为 _
去掉结尾的 =
,或者保持填充但进行编码处理。
举个例子,编码后的字符串有加号,那么经由浏览器传到后端,加号就会变成空格,不满足 base64 字符串的格式,所以 atob 函数就会报错了。正确做法是,编码的时候把这些特殊字符替换成别的,然后解码的时候先把这些特殊字符换回来再进行解码。
完整示例:
function toUrlSafeBase64(input) {
const base64 = btoa(unescape(encodeURIComponent(input))); // 标准Base64编码
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); // 替换字符并去掉尾部的=
}
function fromUrlSafeBase64(urlSafeBase64) {
let base64 = urlSafeBase64.replace(/-/g, "+").replace(/_/g, "/"); // 还原为标准Base64
while (base64.length % 4) {
base64 += "="; // 补足Base64长度
}
return decodeURIComponent(escape(atob(base64))); // 解码为原始字符串
}
const input = "我想查询南京市2024年所有订单总量";
const encoded = toUrlSafeBase64(input);
console.log("Encoded (URL Safe Base64):", encoded);
const decoded = fromUrlSafeBase64(encoded);
console.log("Decoded:", decoded);
2024-11-11 08:00:00
读懂配置文件,有助于分析 bug,并且更深一步了解项目。
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug",
"trace": true,
"skipFiles": [
"<node_internals>/**"
],
"preLaunchTask": "tsc: build - tsconfig.json",
"program": "${workspaceFolder}/debug/debug.js",
"sourceMaps": true,
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart",
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
],
"runtimeArgs": ["-r", "esm"]
}
]
}
"preLaunchTask": "tsc: build - tsconfig.json",
表示调试前都会将 ts 编译成 js 的过程执行一次。
program
是调试的入口文件。这个可以根据需要换成想要的文件。
outFiles
是 ts 编译结果输出的文件夹。
runtimeArgs
: 为 Node.js 运行时提供额外参数,这里使用 -r esm 参数,提前加载 ESM 模块,使项目可以使用 ESM 的语法特性。这个项目中,必须加入这两个参数。
{
"exclude": [
"node_modules"
],
"include": [
"src/**/*.ts",
"test/**/*.ts"
],
"compilerOptions": {
"outDir": "dist",
"allowJs": true,
"module": "Node16",
"moduleResolution": "Node16",
"sourceMap": true,
"allowSyntheticDefaultImports": true,
"target": "ES2018",
"lib": [
"ES2018"
]
}
}
重点部分是compilerOptions
:
outDir
: 指定编译后的文件输出目录,这里是 dist
,所以编译后的 .js
文件会存放在 dist
文件夹中。
allowJs
: 允许编译 .js
文件,默认情况下 TypeScript 只编译 .ts
文件,但设置为 true
后可以包含 .js
文件。
module
: 指定模块系统,这里设置为 Node16
,对应 Node.js 16 及其之后的模块系统,支持原生 ESM 模块和 CommonJS。
moduleResolution
: 设置模块解析策略,这里选择 Node16
,这意味着编译器将使用 Node.js 16 及之后的解析规则来查找模块。
sourceMap
: 启用 Source Map 生成,以便在调试时能够映射回原始的 TypeScript 代码。
allowSyntheticDefaultImports
: 允许从没有默认导出的模块中导入默认值,用于兼容 CommonJS 模块。
target
: 指定编译的目标 ECMAScript 版本,这里是 ES2018
,表示编译后的代码将兼容 ES2018 的特性。
lib
: 指定包含的库文件,"ES2018"
表示编译器将使用 ES2018 标准库类型定义。