动态

详情 返回 返回

NodeJS項目架構設計,看這一篇就足夠了! - 动态 详情

NodeJS項目架構設計,看這一篇就足夠了!

前言

大家好,我是倔強青銅三。我是一名熱情的軟件工程師,我熱衷於分享和傳播IT技術,致力於通過我的知識和技能推動技術交流與創新,歡迎關注我,微信公眾號:倔強青銅三


1. 🧩 整潔架構簡介

Clean Architecture(整潔架構)由Robert C. Martin(Uncle Bob)提出,它強調應用程序內部關注點的分離。該架構提倡業務邏輯應與任何框架、數據庫或外部系統無關,從而使應用程序更加模塊化、易於測試且能夠適應變化。

整潔架構的關鍵原則:
  • 獨立性:核心業務邏輯不應依賴於外部庫、UI、數據庫或框架。
  • 可測試性:應用程序應易於測試,且不依賴於外部系統。
  • 靈活性:應易於更改或替換應用程序的部分,而不影響其他部分。

2. 💡 為什麼選擇Node.js、Express和TypeScript?

Node.js

Node.js是一個強大的JavaScript運行時,允許你構建可擴展的網絡應用程序。它是非阻塞和事件驅動的,非常適合構建需要處理大量請求的API。

Express

Express是Node.js的一個極簡主義Web框架。它提供了一套強大的功能來構建Web和移動應用程序及API。其簡潔性使得入門容易,且高度可擴展。

TypeScript

TypeScript是JavaScript的一個超集,添加了靜態類型。在Node.js應用中使用TypeScript可以在開發早期捕獲錯誤,提高代碼可讀性,並增強整體開發體驗。

3. 🚧 設置項目

首先,創建一個新的Node.js項目並設置TypeScript:

mkdir clean-architecture-api
cd clean-architecture-api
npm init -y
npm install express
npm install typescript @types/node @types/express ts-node-dev --save-dev
npx tsc --init

接下來,配置你的tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules"]
}

4. 🏗️ 使用Clean Architecture構建項目結構

典型的Clean Architecture項目分為以下幾層:

  1. Domain Layer:包含業務邏輯、實體和接口。這一層獨立於其他層。
  2. Use Cases Layer:包含應用程序的用例或業務規則。
  3. Infrastructure Layer:包含在Domain Layer中定義的接口的實現,如數據庫連接。
  4. Interface Layer:包含控制器、路由和任何其他與Web框架相關的代碼。

項目目錄結構可能如下所示:

src/
├── domain/
│   ├── entities/
│   └── interfaces/
├── use-cases/
├── infrastructure/
│   ├── database/
│   └── repositories/
└── interface/
    ├── controllers/
    └── routes/

5. 📂 實現Domain Layer

在Domain Layer中定義你的實體和接口。假設我們正在構建一個管理書籍的簡單API。

實體(Book)

// src/domain/entities/Book.ts
export class Book {
  constructor(
    public readonly id: string,
    public title: string,
    public author: string,
    public publishedDate: Date
  ) {}
}

倉庫接口

// src/domain/interfaces/BookRepository.ts
import { Book } from "../entities/Book";

export interface BookRepository {
  findAll(): Promise<Book[]>;
  findById(id: string): Promise<Book | null>;
  create(book: Book): Promise<Book>;
  update(book: Book): Promise<void>;
  delete(id: string): Promise<void>;
}

6. 🔧 實現Use Cases

Use Cases定義了系統中可以執行的操作。它們與Domain Layer交互,並且與框架或數據庫無關。

Use Case(GetAllBooks)

// src/use-cases/GetAllBooks.ts
import { BookRepository } from "../domain/interfaces/BookRepository";

export class GetAllBooks {
  constructor(private bookRepository: BookRepository) {}

  async execute() {
    return await this.bookRepository.findAll();
  }
}

7. 🗂️ 實現Infrastructure Layer

在Infrastructure Layer中實現Domain Layer中定義的接口。這是與數據庫或外部服務交互的地方。

內存倉庫(為了簡化)

// src/infrastructure/repositories/InMemoryBookRepository.ts
import { Book } from "../../domain/entities/Book";
import { BookRepository } from "../../domain/interfaces/BookRepository";

export class InMemoryBookRepository implements BookRepository {
  private books: Book[] = [];

  async findAll(): Promise<Book[]> {
    return this.books;
  }

  async findById(id: string): Promise<Book | null> {
    return this.books.find(book => book.id === id) || null;
  }

  async create(book: Book): Promise<Book> {
    this.books.push(book);
    return book;
  }

  async update(book: Book): Promise<void> {
    const index = this.books.findIndex(b => b.id === book.id);
    if (index !== -1) {
      this.books[index] = book;
    }
  }

  async delete(id: string): Promise<void> {
    this.books = this.books.filter(book => book.id !== id);
  }
}

8. 🌐 實現Interface Layer

Interface Layer包含處理HTTP請求並將它們映射到Use Cases的控制器和路由。

Book Controller

// src/interface/controllers/BookController.ts
import { Request, Response } from "express";
import { GetAllBooks } from "../../use-cases/GetAllBooks";

export class BookController {
  constructor(private getAllBooks: GetAllBooks) {}

  async getAll(req: Request, res: Response) {
    const books = await this.getAllBooks.execute();
    res.json(books);
  }
}

路由

// src/interface/routes/bookRoutes.ts
import { Router } from "express";
import { InMemoryBookRepository } from "../../infrastructure/repositories/InMemoryBookRepository";
import { GetAllBooks } from "../../use-cases/GetAllBooks";
import { BookController } from "../controllers/BookController";

const router = Router();

const bookRepository = new InMemoryBookRepository();
const getAllBooks = new GetAllBooks(bookRepository);
const bookController = new BookController(getAllBooks);

router.get("/books", (req, res) => bookController.getAll(req, res));

export { router as bookRoutes };

主應用程序

// src/index.ts
import express from "express";
import { bookRoutes } from "./interface/routes/bookRoutes";

const app = express();

app.use(express.json());
app.use("/api", bookRoutes);

const PORT = 3000;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

9. 🔌 依賴注入

依賴注入(DI)是一種技術,其中對象的依賴項由外部提供,而不是硬編碼在對象內部。這促進了鬆散耦合,並使你的應用程序更易於測試。

示例

// src/infrastructure/DIContainer.ts
import { InMemoryBookRepository } from "./repositories/InMemoryBookRepository";
import { GetAllBooks } from "../use-cases/GetAllBooks";

class DIContainer {
  private static _bookRepository = new InMemoryBookRepository();

  static getBookRepository() {
    return this._bookRepository;
  }

  static getGetAllBooksUseCase() {
    return new GetAllBooks(this.getBookRepository());
  }
}

export { DIContainer };

在控制器中使用DIContainer:

// src/interface/controllers/BookController.ts
import { Request, Response } from "express";
import { DIContainer } from "../../infrastructure/DIContainer";

export class BookController {
  private getAllBooks = DIContainer.getGetAllBooksUseCase();

  async getAll(req: Request, res: Response) {
    const books = await this.getAllBooks.execute();
    res.json(books);
  }
}

10. 🚨 錯誤處理

適當的錯誤處理可以確保你的API能夠優雅地處理意外情況,並向客户端提供有意義的錯誤消息。

示例

// src/interface/middleware/errorHandler.ts
import { Request, Response, NextFunction } from "express";

export function errorHandler(err: any, req: Request, res: Response, next: NextFunction) {
  console.error(err.stack);
  res.status(500).json({ message: "Internal Server Error" });
}

在主應用程序中使用錯誤處理中間件:

// src/index.ts
import express from "express";
import { bookRoutes } from "./interface/routes/bookRoutes";
import { errorHandler } from "./interface/middleware/errorHandler";

const app = express();

app.use(express.json());
app.use("/api", bookRoutes);
app.use(errorHandler);

const PORT = 3000;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

11. ✔️ 驗證

驗證對於確保進入應用程序的數據正確且安全至關重要。

示例

npm install class-validator class-transformer

創建用於書籍創建的DTO:

// src/interface/dto/CreateBookDto.ts
import { IsString, IsDate } from "class-validator";

export class CreateBookDto {
  @IsString()
  title!: string;

  @IsString()
  author!: string;

  @IsDate()
  publishedDate!: Date;
}

在控制器中驗證DTO:

// src/interface/controllers/BookController.ts
import { Request, Response } from "express";
import { validate } from "class-validator";
import { CreateBookDto } from "../dto/CreateBookDto";
import { DIContainer } from "../../infrastructure/DIContainer";

export class BookController {
  private getAllBooks = DIContainer.getGetAllBooksUseCase();

  async create(req: Request, res: Response) {
    const dto = Object.assign(new CreateBookDto(), req.body);
    const errors = await validate(dto);

    if (errors.length > 0) {
      return res.status(400).json({ errors });
    }

    // 繼續創建邏輯...
  }
}

12. 💾 真實數據庫集成

將內存數據庫切換到如MongoDB或PostgreSQL等真實數據庫,可以使你的應用程序準備好投入生產。

示例

npm install mongoose @types/mongoose

Book創建Mongoose模型:

// src/infrastructure/models/BookModel.ts
import mongoose, { Schema, Document } from "mongoose";

interface IBook extends Document {
  title: string;
  author: string;
  publishedDate: Date;
}

const BookSchema: Schema = new Schema({
  title: { type: String, required: true },
  author: { type: String, required: true },
  publishedDate: { type: Date, required: true },
});

const BookModel = mongoose.model<IBook>("Book", BookSchema);
export { BookModel, IBook };

實現倉庫:

// src/infrastructure/repositories/MongoBookRepository.ts
import { Book } from "../../domain/entities/Book";
import { BookRepository } from "../../domain/interfaces/BookRepository";
import { BookModel } from "../models/BookModel";

export class MongoBookRepository implements BookRepository {
  async findAll(): Promise<Book[]> {
    return await BookModel.find();
  }

  async findById(id: string): Promise<Book | null> {
    return await BookModel.findById(id);
  }

  async create(book: Book): Promise<Book> {
    const newBook = new BookModel(book);
    await newBook.save();
    return newBook;
  }

  async update(book: Book): Promise<void> {
    await BookModel.findByIdAndUpdate(book.id, book);
  }
  async delete(id: string): Promise<void> {
    await BookModel.findByIdAndDelete(id);
  }
}

更新DIContainer以使用MongoBookRepository:

// src/infrastructure/DIContainer.ts
import { MongoBookRepository } from "./repositories/MongoBookRepository";
import { GetAllBooks } from "../use-cases/GetAllBooks";

class DIContainer {
  private static _bookRepository = new MongoBookRepository();

  static getBookRepository() {
    return this._bookRepository;
  }

  static getGetAllBooksUseCase() {
    return new GetAllBooks(this.getBookRepository());
  }
}

export { DIContainer };

13. 🔒 身份驗證和授權

保護你的API至關重要。JWT(JSON Web Tokens)是一種常用的無狀態身份驗證方法。

示例

npm install jsonwebtoken @types/jsonwebtoken

創建一個身份驗證中間件:

// src/interface/middleware/auth.ts
import jwt from "jsonwebtoken";
import { Request, Response, NextFunction } from "express";

export function authenticateToken(req: Request, res: Response, next: NextFunction) {
  const token = req.header("Authorization")?.split(" ")[1];
  if (!token) return res.sendStatus(401);

  jwt.verify(token, process.env.JWT_SECRET as string, (err, user) => {
    if (err) return res.sendStatus(403);
    req.user = user;
    next();
  });
}

在路由中使用此中間件來保護路由:

// src/interface/routes/bookRoutes.ts
import { Router } from "express";
import { BookController } from "../controllers/BookController";
import { authenticateToken } from "../middleware/auth";

const router = Router();

const bookController = new BookController();

router.get("/books", authenticateToken, (req, res) => bookController.getAll(req, res));

export { router as bookRoutes };

14. 📝 日誌記錄和監控

日誌記錄在調試和生產環境中監控應用程序時至關重要。

示例

npm install winston

創建一個記錄器:

// src/infrastructure/logger.ts
import { createLogger, transports, format } from "winston";

const logger = createLogger({
  level: "info",
  format: format.combine(format.timestamp(), format.json()),
  transports: [new transports.Console()],
});

export { logger };

在應用程序中使用記錄器:

// src/index.ts
import express from "express";
import { bookRoutes } from "./interface/routes/bookRoutes";
import { errorHandler } from "./interface/middleware/errorHandler";
import { logger } from "./infrastructure/logger";

const app = express();

app.use(express.json());
app.use("/api", bookRoutes);
app.use(errorHandler);

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  logger.info(`Server is running on port ${PORT}`);
});

15. ⚙️ 環境配置

管理不同的環境對於確保你的應用程序在開發、測試和生產環境中正確運行至關重要。

示例

npm install dotenv

創建一個.env文件:

PORT=3000
JWT_SECRET=your_jwt_secret

在應用程序中加載環境變量:

// src/index.ts
import express from "express";
import dotenv from "dotenv";

import { bookRoutes } from "./interface/routes/bookRoutes";
import { errorHandler } from "./interface/middleware/errorHandler";
import { logger } from "./infrastructure/logger";

dotenv.config();

const app = express();

app.use(express.json());
app.use("/api", bookRoutes);
app.use(errorHandler);

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  logger.info(`Server is running on port ${PORT}`);
});

16. 🚀 CI/CD和部署

自動化API的測試、構建和部署可以確保一致性和可靠性。

示例

創建一個.github/workflows/ci.yml文件:

name: Node.js CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:

    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [14.x, 16.x]

    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v2
      with:
        node-version: ${{ matrix.node-version }}
    - run: npm install
    - run: npm test

17. 🧹 代碼質量和Linting

在協作環境中保持一致的代碼質量至關重要。

示例

npm install eslint prettier eslint-config-prettier eslint-plugin-prettier --save-dev

創建ESLint配置:

// .eslintrc.json
{
  "env": {
    "node": true,
    "es6": true
  },
  "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
  "plugins": ["@typescript-eslint", "prettier"],
  "parser": "@typescript-eslint/parser",
  "rules": {
    "prettier/prettier": "error"
  }
}

添加Prettier配置:

// .prettierrc
{
  "singleQuote": true,
  "trailingComma": "all",
  "printWidth": 80
}

18. 🛠️ 項目文檔

為你的API編寫文檔對於開發人員和最終用户都至關重要。

示例

npm install swagger-jsdoc swagger-ui-express

創建Swagger文檔:

// src/interface/swagger.ts
import swaggerJSDoc from "swagger-jsdoc";
import swaggerUi from "swagger-ui-express";
import { Express } from "express";

const options = {
  definition: {
    openapi: "3.0.0",
    info: {
      title: "Clean Architecture API",
      version: "1.0.0",
    },
  },
  apis: ["./src/interface/routes/*.ts"],
};

const swaggerSpec = swaggerJSDoc(options);

function setupSwagger(app: Express) {
  app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));
}

export { setupSwagger };

在主應用程序中設置Swagger:

// src/index.ts
import express from "express";
import dotenv from "dotenv";

import { bookRoutes } from "./interface/routes/bookRoutes";
import { errorHandler } from "./interface/middleware/errorHandler";
import { logger } from "./infrastructure/logger";
import { setupSwagger } from "./interface/swagger";

dotenv.config();

const app = express();

app.use(express.json());
app.use("/api", bookRoutes);
app.use(errorHandler);
setupSwagger(app);

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  logger.info(`Server is running on port ${PORT}`);
});

19. 🏁 結論

在本博客中,我們探討了如何使用Node.js、Express和TypeScript構建現代API,同時遵循整潔架構原則。我們擴展了初始實現,添加了關鍵功能,如依賴注入、錯誤處理、驗證、真實數據庫集成、身份驗證和授權、日誌記錄和監控、環境配置、CI/CD、代碼質量和Linting以及項目文檔。

通過遵循這些實踐,你將確保你的API不僅功能齊全,而且易於維護、可擴展且準備好投入生產。隨着你繼續開發,請隨時探索其他模式和工具,以進一步增強你的應用程序。

user avatar toopoo 头像 cyzf 头像 pengxiaohei 头像 zs_staria 头像 littlelyon 头像 6fafa 头像 razyliang 头像 leexiaohui1997 头像 banana_god 头像 hard_heart_603dd717240e2 头像 u_16307147 头像 xiaoxxuejishu 头像
点赞 156 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.