在这篇文章中,我们将详细介绍如何从零开始搭建一个 TypeScript 开发环境,并发布一个名为 ts-logger-tools 的 npm 包。我们将涵盖项目初始化、安装 TypeScript 及开发工具、配置 TypeScript 编译器、创建项目结构和示例代码、编写测试(使用 Jest)、构建项目以及发布 npm 包的步骤。
搭建ts开发环境
1. 初始化项目
首先,我们需要创建一个新的项目文件夹并初始化一个 npm 项目。
mkdir ts-logger-tools
cd ts-logger-tools
npm init -y
npm init -y 命令会使用默认配置生成一个 package.json 文件。
2. 安装 TypeScript 和其他开发依赖
接下来,我们需要安装 TypeScript 和一些常用的开发工具。
npm install typescript ts-node @types/node --save-dev
tsc --init
typescript: TypeScript 编译器。ts-node: 允许直接运行 TypeScript 文件,而无需先编译成 JavaScript。@types/node: Node.js 的类型定义文件。
3. 配置 TypeScript
创建一个 tsconfig.json 文件来配置 TypeScript 编译器。
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./lib",
"types": ["node", "jest"],
"rootDir": "./src",
"declaration": true
},
"include": ["src/**/*.ts", "test/**/*.test.ts"],
"exclude": ["node_modules", "test"]
}
target: 指定 ECMAScript 目标版本为 ES2020,以支持现代 JavaScript 特性。module: 将模块解析设置为 CommonJS,以便与 Node.js 兼容。strict: 启用所有严格类型检查选项,以增强代码的类型安全性。esModuleInterop: 启用对 ES 模块的互操作性,以便使用import语句导入 CommonJS 模块。skipLibCheck: 跳过对声明文件(.d.ts 文件)的类型检查,以加快编译速度。forceConsistentCasingInFileNames: 强制文件名一致大小写,以避免在不同操作系统上的文件名大小写问题。outDir: 指定编译输出目录为./lib。types: 添加 Node 和 Jest 的类型定义。rootDir: 指定输入文件的根目录为./src。declaration: 生成类型定义文件。include: 包含src和test文件夹中的 TypeScript 文件。exclude: 排除node_modules文件夹。
4.配置eslint
安装依赖
确保安装了所有必要的依赖:
npm install eslint-plugin-import eslint-config-airbnb-base eslint-plugin-node @typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev
生成配置
npx eslint --init
Need to install the following packages:
eslint@9.12.0
Ok to proceed? (y) yes
You can also run this command directly using 'npm init @eslint/config@latest'.
Need to install the following packages:
@eslint/create-config@1.3.1
Ok to proceed? (y) y
> ts-logger-tools@0.0.0 npx
> create-config
@eslint/create-config: v1.3.1
✔ How would you like to use ESLint? · problems
✔ What type of modules does your project use? · esm
✔ Which framework does your project use? · none
✔ Does your project use TypeScript? · typescript
✔ Where does your code run? · browser
The config that you've selected requires the following dependencies:
eslint, globals, @eslint/js, typescript-eslint
✔ Would you like to install them now? · No / Yes
✔ Which package manager do you want to use? · npm
示例配置文件
生成的配置文件eslint.config.mjs可能需要手动调整,以匹配配置:
import globals from "globals";
import tseslint from "@typescript-eslint/eslint-plugin";
import parser from "@typescript-eslint/parser";
export default [
{
files: ["**/*.{js,mjs,cjs,ts}"],
ignores: ["node_modules/", "dist/", "build/"],
languageOptions: {
globals: globals.browser,
parser: parser,
parserOptions: {
ecmaVersion: 2020,
sourceType: "module"
}
},
plugins: {
"@typescript-eslint": tseslint
},
rules: {
"@typescript-eslint/no-inferrable-types": "off",
"no-case-declarations": "off",
"@typescript-eslint/no-useless-constructor": "off",
"@typescript-eslint/no-explicit-any": "off"
}
}
];
注意,新版的eslint忽略目录已经不用
.eslintignore而是直接写到配置中了
运行 ESLint
在 package.json 中添加一个脚本,以便于运行 ESLint:
{
"scripts": {
"lint": "eslint"
}
}
然后运行 ESLint:
npm run lint
这样,就可以在 TypeScript 项目中使用 ESLint 进行代码检查和风格规范了。
5. 创建项目结构
接下来,创建项目的基本结构。
mkdir src
touch src/index.ts
在 src/index.ts 文件中编写一些示例代码:
export const log = (message: string): void => {
console.log(`[ts-logger-tools]: ${message}`);
};
6. 编写测试
为了确保我们的代码是正确的,我们需要编写测试。我们将使用 Jest 作为测试框架。
npm install jest ts-jest @types/jest --save-dev
创建一个 jest.config.js 文件来配置 Jest:
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
moduleFileExtensions: ["ts", "js"],
transform: {
"^.+\\.ts$": "ts-jest",
},
testMatch: ["**/test/**/*.test.ts"], // 匹配测试文件的模式
};
在根目录中创建一个 test 文件夹,并添加一个测试文件 index.test.ts:
import { log } from '../src/index';
test("log function", () => {
console.log = jest.fn();
log("Hello, World!");
expect(console.log).toHaveBeenCalledWith("[ts-logger-tools]: Hello, World!");
});
在 package.json 中添加一个测试脚本:
"scripts": {
"test": "jest"
}
现在,可以运行 npm test 来执行测试。
7. 构建项目
在 package.json 中添加一个构建脚本:
"scripts": {
"build": "tsc -p tsconfig.build.json"
}
运行 npm run build,TypeScript 编译器会将 src 目录中的文件编译到 lib 目录中。
8.发版脚本
这里不一定要用脚本,自己一步步操作也可以。下面是小编常用的发版脚本release.sh
#!/bin/bash
# 运行 ESLint 检查
echo "Running ESLint..."
npm run lint
# 检查 ESLint 是否通过
if [ $? -ne 0 ]; then
echo "ESLint failed. Aborting release."
exit 1
fi
# 运行测试
echo "Running tests..."
npm run test
# 检查测试是否通过
if [ $? -ne 0 ]; then
echo "Tests failed. Aborting release."
exit 1
fi
# 构建项目
echo "Building project..."
npm run build
# 检查构建是否成功
if [ $? -ne 0 ]; then
echo "Build failed. Aborting release."
exit 1
fi
# 生成依赖树
echo "Generating dependency tree..."
npm run tree
# 检查依赖树生成是否成功
if [ $? -ne 0 ]; then
echo "Generating dependency tree failed. Aborting release."
exit 1
fi
# 获取提交信息
read -p "Enter commit message: " commit_message
# 提交到 Git 仓库
echo "Committing to Git..."
git add .
git commit -m "$commit_message"
# 推送到远程仓库
echo "Pushing to remote repository..."
git push
# 升级版本
echo "Upgrading version..."
npm version patch
# 发布到 npm
echo "Publishing to npm..."
npm publish
echo "Release process completed successfully."
package.json中的script
{
"scripts": {
"test": "jest",
"coverage": "jest --coverage",
"build": "tsc -p tsconfig.build.json",
"tree": "tree -l 10 -o tree-out.txt --ignore node_modules/ --ignore build/ --ignore dist/ --ignore migrations/",
"lint": "eslint .",
"push": "git add . && read -p '请输入本次更新: \n' msg && git commit -m \"$msg\" && git push -u origin master",
"rele": "bash release.sh"
}
}
9. 发布 npm 包
在发布 npm 包之前,确保已经登录到 npm:
npm login
然后,在 package.json 中添加以下字段:
"main": "lib/index.js",
"types": "lib/index.d.ts"
这将指定包的入口文件和类型声明文件。
最后,运行以下命令来发布包:
# 一系列npm XXX命令,然后
npm publish
# 或者直接 npm run rele
# win同学脚本可以自己改一下
小节
通过以上步骤,已经成功地搭建了一个 TypeScript 开发环境,并发布了一个名为 ts-logger-tools 的 npm 包。这个过程包括了项目初始化、安装依赖、配置 TypeScript、编写代码和测试、构建项目以及发布 npm 包。
日志系统设计
相关概念
- Trace: 一个完整的请求流,从开始到结束可能跨越多个服务和组件。每个
trace由一个唯一的traceId标识。 - Span:
Trace中的一个片段,通常代表一次服务调用或操作。每个span由一个唯一的spanId标识。 - ParentSpanId: 如果一个
span是由另一个span触发的,那么parentSpanId就是触发它的那个span的spanId。这有助于构建调用链的层级关系。 - 进入日志(Entry Log): 记录请求到达服务时的日志。
- 退出日志(Exit Log): 记录服务完成处理并返回响应时的日志。
示例
假设有一个用户请求经过多个服务的处理流程:
- 用户请求到达网关服务(生成一个
traceId和spanId)。 - 网关服务调用身份验证服务(生成一个新的
spanId,并记录parentSpanId)。 - 身份验证服务调用数据库服务(生成另一个新的
spanId,并记录parentSpanId)。 - 网关服务调用订单服务(生成一个新的
spanId,并记录parentSpanId)。
在每个服务中,都会记录进入和退出日志:
- 进入日志:记录请求到达时的详细信息。
- 退出日志:记录服务处理完成后的详细信息,包括处理时间和结果。
日志结构设计
基类
// src/base/index.ts
export interface Log {
traceId: string; // 追踪 ID
timestamp: number; // 时间戳
level: string; // 日志级别
message: string; // 日志消息
logType: "entry" | "exit"; // 日志类型(进入或退出)
[key: string]: any; // 其他字段
}
访问日志表(Access Logs)
// src/base/index.ts
// 访问日志类型
export interface AccessLog extends Log {
requestTime: number; // 请求时间
ipAddress: string; // IP 地址
requestUrl: string; // 请求 URL
httpMethod: string; // HTTP 方法
statusCode: number; // HTTP 状态码
userAgent: string; // 用户代理
responseTime: number; // 响应时间
userId?: string; // 用户 ID(可选)
role?: Role; // 用户角色(可选)
}
应用日志表(Application Logs)
// src/base/index.ts
// 应用日志类型
export interface ApplicationLog extends Log {
action: string; // 具体操作
userId?: string; // 用户 ID(可选)
details: any; // 详细信息
eventType: "login" | "security" | "trace" | "operation"; // 事件类型
loginResult?: "success" | "failure"; // 登录结果(仅用于登录事件)
failureReason?: string; // 失败原因
spanId?: string; // Span ID(仅用于追踪)
parentSpanId?: string; // 父 Span ID(仅用于追踪)
service?: string; // 服务名称(仅用于追踪)
operation?: string; // 操作名称(仅用于追踪)
duration?: number; // 操作持续时间(仅用于追踪)
error?: boolean; // 是否有错误(仅用于追踪)
}
功能设计
日志同步
- 访问日志同步: 系统应支持将访问日志同步到指定的存储介质中。
- 登录日志同步: 系统应支持将登录相关的应用日志同步到指定的存储介质中。
- 应用日志同步: 系统应支持将应用操作相关的日志同步到指定的存储介质中。
- 安全日志同步: 系统应支持将安全相关的应用日志同步到指定的存储介质中。
- 追踪日志同步: 系统应支持将追踪相关的应用日志同步到指定的存储介质中。
日志查询
- 查询访问日志: 系统应支持根据过滤条件、分页参数和排序条件查询访问日志。
- 查询应用日志: 系统应支持根据过滤条件、分页参数和排序条件查询应用日志。
日志计数
- 计数访问日志: 系统应支持根据过滤条件计算访问日志的数量。
- 计数应用日志: 系统应支持根据过滤条件计算应用日志的数量。
用户统计
- 统计访问日志中的不同用户数量: 系统应支持根据过滤条件统计访问日志中不同用户的数量。
- 统计应用日志中的不同用户数量: 系统应支持根据过滤条件统计应用日志中不同用户的数量。
追踪日志查询
- 查询指定链路ID的所有日志: 系统应支持根据追踪ID查询所有相关的访问日志和应用日志。
抽象日志同步器基类
// src/base/LogSyncer.ts
import { AccessLog, ApplicationLog, Sort } from ".";
// 抽象日志同步器基类
export abstract class LogSyncer {
// 同步日志方法
abstract syncAccessLog(log: AccessLog): Promise<void>;
abstract syncLoginLog(log: ApplicationLog): Promise<void>;
abstract syncApplicationLog(log: ApplicationLog): Promise<void>;
abstract syncSecurityLog(log: ApplicationLog): Promise<void>;
abstract syncTraceLog(log: ApplicationLog): Promise<void>;
// 查询日志方法
abstract queryAccessLogs(
filter: any,
skip?: number,
limit?: number,
sort?: Sort
): Promise<AccessLog[]>;
abstract queryApplicationLogs(
filter: any,
skip?: number,
limit?: number,
sort?: Sort
): Promise<ApplicationLog[]>;
// 计数日志方法
abstract countAccessLogs(filter: any): Promise<number>;
abstract countApplicationLogs(filter: any): Promise<number>;
// 统计不同用户数量的方法
abstract countDistinctUsersAccessLogs(filter: any): Promise<number>;
abstract countDistinctUsersApplicationLogs(filter: any): Promise<number>;
// 查询指定链路ID的所有日志
abstract queryLogsByTraceId(
traceId: string
): Promise<{ accessLogs: AccessLog[]; applicationLogs: ApplicationLog[] }>;
}
具体实现
MongoDB
安装MongoDB:
npm i mongodb
具体实现:
// src/syncer/mongodb.ts
import { MongoClient, Db, Collection, Filter } from "mongodb";
import { AccessLog, ApplicationLog, Sort } from "../base";
import { LogSyncer } from "../base/LogSyncer";
export class MongoDBLogSyncer extends LogSyncer {
private client: MongoClient;
private db: Db;
private accessLogs: Collection<AccessLog>;
private applicationLogs: Collection<ApplicationLog>;
constructor(url: string, dbname: string) {
super();
this.client = new MongoClient(url);
this.db = this.client.db(dbname);
this.accessLogs = this.db.collection<AccessLog>("accessLogs");
this.applicationLogs =
this.db.collection<ApplicationLog>("applicationLogs");
}
async connect(): Promise<void> {
await this.client.connect();
console.info("日志MongoDB数据库初始化完毕");
// 创建索引
await this.createIndexes();
}
private async createIndexes(): Promise<void> {
if (this.accessLogs) {
await this.accessLogs.createIndex({ traceId: 1 });
await this.accessLogs.createIndex({ timestamp: 1 });
await this.accessLogs.createIndex({ ipAddress: 1 });
await this.accessLogs.createIndex({ userId: 1 });
await this.accessLogs.createIndex({ userId: 1, timestamp: -1 });
}
if (this.applicationLogs) {
await this.applicationLogs.createIndex({ traceId: 1 });
await this.applicationLogs.createIndex({ timestamp: 1 });
await this.applicationLogs.createIndex({ userId: 1 });
await this.applicationLogs.createIndex({ spanId: 1 });
await this.applicationLogs.createIndex({ parentSpanId: 1 });
await this.applicationLogs.createIndex({ eventType: 1 });
await this.applicationLogs.createIndex({ userId: 1, timestamp: -1 });
}
}
async syncAccessLog(log: AccessLog): Promise<void> {
await this.accessLogs.insertOne(log);
}
async syncLoginLog(log: ApplicationLog): Promise<void> {
await this.syncApplicationLog(log);
}
async syncApplicationLog(log: ApplicationLog): Promise<void> {
await this.applicationLogs.insertOne(log);
}
async syncSecurityLog(log: ApplicationLog): Promise<void> {
await this.syncApplicationLog(log);
}
async syncTraceLog(log: ApplicationLog): Promise<void> {
await this.syncApplicationLog(log);
}
async queryAccessLogs(
filter: Filter<AccessLog>,
skip: number = 0,
limit: number = 10,
sort?: Sort
): Promise<AccessLog[]> {
const mongoSort = this.convertSort(sort);
return this.accessLogs
.find(filter)
.skip(skip)
.limit(limit)
.sort(mongoSort)
.toArray();
}
async queryApplicationLogs(
filter: Filter<ApplicationLog>,
skip: number = 0,
limit: number = 10,
sort?: Sort
): Promise<ApplicationLog[]> {
const mongoSort = this.convertSort(sort);
return this.applicationLogs
.find(filter)
.skip(skip)
.limit(limit)
.sort(mongoSort)
.toArray();
}
async countAccessLogs(filter: Filter<AccessLog>): Promise<number> {
return this.accessLogs.countDocuments(filter);
}
async countApplicationLogs(filter: Filter<ApplicationLog>): Promise<number> {
return this.applicationLogs.countDocuments(filter);
}
async countDistinctUsersAccessLogs(
filter: Filter<AccessLog>
): Promise<number> {
return this.accessLogs
.distinct("userId", filter)
.then((users) => users.length);
}
async countDistinctUsersApplicationLogs(
filter: Filter<ApplicationLog>
): Promise<number> {
return this.applicationLogs
.distinct("userId", filter)
.then((users) => users.length);
}
async queryLogsByTraceId(
traceId: string
): Promise<{ accessLogs: AccessLog[]; applicationLogs: ApplicationLog[] }> {
const accessLogs = await this.accessLogs.find({ traceId }).toArray();
const applicationLogs = await this.applicationLogs
.find({ traceId })
.toArray();
return { accessLogs, applicationLogs };
}
async close(): Promise<void> {
await this.client.close();
}
private convertSort(sort?: Sort): any {
if (!sort) return {};
const mongoSort: any = {};
for (const [field, order] of Object.entries(sort)) {
mongoSort[field] = order === "asc" ? 1 : -1;
}
return mongoSort;
}
}
注意:这里的实现多了一层convertSort处理排序,因为MongoDB和正常的库排序入参不一致
单元测试
MongoDB同步器MongoDBLogSyncer
// test/syncer/mongodb.test.ts
import { MongoMemoryServer } from "mongodb-memory-server";
import { MongoDBLogSyncer } from "../../src/syncer/mongodb";
import { AccessLog, ApplicationLog } from "../../src/base";
let mongoServer: MongoMemoryServer;
let syncer: MongoDBLogSyncer;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const uri = mongoServer.getUri();
syncer = new MongoDBLogSyncer(uri, "testdb");
await syncer.connect();
});
afterAll(async () => {
await syncer.close();
await mongoServer.stop();
});
describe("MongoDBLogSyncer", () => {
it("should insert an access log", async () => {
const log: AccessLog = {
traceId: "trace123",
timestamp: Date.now(),
level: "info",
message: "Access log message",
logType: "entry",
requestTime: Date.now(),
ipAddress: "127.0.0.1",
requestUrl: "/api/test",
httpMethod: "GET",
statusCode: 200,
userAgent: "Mozilla/5.0",
responseTime: Date.now(),
userId: "user1", // 添加 userId
};
await syncer.syncAccessLog(log);
const insertedLog = await (syncer as any).accessLogs.findOne({
traceId: "trace123",
});
expect(insertedLog).toMatchObject(log);
});
it("should insert an application log", async () => {
const log: ApplicationLog = {
traceId: "trace456",
timestamp: Date.now(),
level: "info",
message: "Application log message",
logType: "entry",
action: "testAction",
details: { key: "value" },
eventType: "operation",
userId: "user2", // 添加 userId
};
await syncer.syncApplicationLog(log);
const insertedLog = await (syncer as any).applicationLogs.findOne({
traceId: "trace456",
});
expect(insertedLog).toMatchObject(log);
});
it("should query access logs", async () => {
const logs = await syncer.queryAccessLogs({ traceId: "trace123" });
expect(logs.length).toBeGreaterThan(0);
expect(logs[0].traceId).toBe("trace123");
});
it("should query application logs", async () => {
const logs = await syncer.queryApplicationLogs({ traceId: "trace456" });
expect(logs.length).toBeGreaterThan(0);
expect(logs[0].traceId).toBe("trace456");
});
it("should count access logs", async () => {
const count = await syncer.countAccessLogs({ traceId: "trace123" });
expect(count).toBeGreaterThan(0);
});
it("should count application logs", async () => {
const count = await syncer.countApplicationLogs({ traceId: "trace456" });
expect(count).toBeGreaterThan(0);
});
it("should count distinct users in access logs", async () => {
// 插入多个日志以确保有不同的 userId
await syncer.syncAccessLog({
traceId: "trace789",
timestamp: Date.now(),
level: "info",
message: "Another access log",
logType: "entry",
requestTime: Date.now(),
ipAddress: "127.0.0.1",
requestUrl: "/api/another",
httpMethod: "POST",
statusCode: 201,
userAgent: "Mozilla/5.0",
responseTime: Date.now(),
userId: "user3",
});
const count = await syncer.countDistinctUsersAccessLogs({});
expect(count).toBeGreaterThan(0);
});
it("should count distinct users in application logs", async () => {
// 插入多个日志以确保有不同的 userId
await syncer.syncApplicationLog({
traceId: "trace789",
timestamp: Date.now(),
level: "info",
message: "Another application log",
logType: "entry",
action: "anotherAction",
details: { key: "anotherValue" },
eventType: "operation",
userId: "user4",
});
const count = await syncer.countDistinctUsersApplicationLogs({});
expect(count).toBeGreaterThan(0);
});
it("should query logs by trace ID", async () => {
const result = await syncer.queryLogsByTraceId("trace123");
expect(result.accessLogs.length).toBeGreaterThan(0);
expect(result.accessLogs[0].traceId).toBe("trace123");
const resultAppLogs = await syncer.queryLogsByTraceId("trace456");
expect(resultAppLogs.applicationLogs.length).toBeGreaterThan(0);
expect(resultAppLogs.applicationLogs[0].traceId).toBe("trace456");
});
});
发布版本
npm run rele
总结
在这篇文章中,我们详细介绍了如何从零开始搭建一个 TypeScript 开发环境,并发布一个名为 ts-logger-tools 的 npm 包。首先,我们初始化了项目并安装了 TypeScript 和相关开发工具,然后配置了 TypeScript 编译器和 ESLint。接着,我们创建了项目结构并编写了示例代码和测试,最后通过构建脚本将项目编译并发布到 npm。
在日志系统设计部分,我们介绍了日志系统的相关概念,如 Trace、Span、进入日志和退出日志。设计了日志结构,包括访问日志和应用日志,并详细描述了日志同步和查询功能。我们还提供了 MongoDB 的具体实现,包括日志同步器的基类和具体实现类,以及相关的单元测试,确保日志功能的正确性。
通过这些步骤,可以轻松搭建一个 TypeScript 项目,设计并实现一个功能齐全的日志系统,并将其发布为 npm 包。