搜尋
首頁web前端js教程詳解Nodejs Express.js專案架構

詳解Nodejs Express.js專案架構

影片教學推薦:nodejs 教學 

引言

在node.js 領域中,Express.js 是一個為人所知的REST APIs 開發框架。雖然它非常的出色,但是該如何組織的專案程式碼,卻沒人告訴你。

通常這沒什麼,不過對開發者而言這又是我們必須面對的問題。

一個好的專案結構,不僅能消除重複程式碼,提升系統穩定性,改善系統的設計,還能在未來更容易的擴充。

多年來,我一直在處理重構和遷移專案結構很糟糕、設計不合理的 node.js 專案。而這篇文章正是我先前累積經驗的總結。

目錄結構

以下是我所推薦的專案程式碼組織方式。

它來自於我參與的專案的實踐,每個目錄模組的功能與作用如下:

  src
  │   app.js          # App 统一入口
  └───api             # Express route controllers for all the endpoints of the app
  └───config          # 环境变量和配置信息
  └───jobs            # 队列任务(agenda.js)
  └───loaders         # 将启动过程模块化
  └───models          # 数据库模型
  └───services        # 存放所有商业逻辑
  └───subscribers     # 异步事件处理器
  └───types           # Typescript 的类型声明文件 (d.ts)

而且,這不僅僅只是程式碼的組織方式...

#3 層結構

這個想法源自 關注點分離原則,把業務邏輯從node.js API 路由中分離出去。

3 层示意图

因為將來的某天,你可能會在 CLI 工具或其他地方處理你的業務。當然,也有可能不會,但在專案中使用API​​呼叫的方式來處理自身的業務終究不是一個好主意...

3 node.js REST API 的三层示意图

#不要在控制器中直接處理業務邏輯!! 

在你的應用程式中,你可能經過為了圖便利而直接的在控制器處理業務。不幸的是,這麼做的話很快你將面對相麵條一樣複雜的控制器程式碼,「惡果」也會隨之而來,例如在處理單元測試的時候不得不使用複雜的request # 或response  模擬。

同時,在決定何時向客戶端回傳回應,或希望在發送回應之後再進行一些處理的時候,將會變得很複雜。

請不要像下面範例這樣做.

  route.post('/', async (req, res, next) => {

    // 这里推荐使用中间件或Joi 验证器
    const userDTO = req.body;
    const isUserValid = validators.user(userDTO)
    if(!isUserValid) {
      return res.status(400).end();
    }

    // 一堆义务逻辑代码
    const userRecord = await UserModel.create(userDTO);
    delete userRecord.password;
    delete userRecord.salt;
    const companyRecord = await CompanyModel.create(userRecord);
    const companyDashboard = await CompanyDashboard.create(userRecord, companyRecord);

    ...whatever...

    // 这里是“优化”,但却搞乱了所有的事情
    // 向客户端发送响应...
    res.json({ user: userRecord, company: companyRecord });

    // 但这里的代码仍会执行 :(
    const salaryRecord = await SalaryModel.create(userRecord, companyRecord);
    eventTracker.track('user_signup',userRecord,companyRecord,salaryRecord);
    intercom.createUser(userRecord);
    gaAnalytics.event('user_signup',userRecord);
    await EmailService.startSignupSequence(userRecord)
  });

使用服務層(service)來處理業務 

在單獨的服務層處理業務邏輯是建議的做法。

這一層是遵循適用於node.js 的SOLID 原則的「類別」的集合

在這一層中,不應該有任何形式的資料查詢操作。正確的做法是使用資料存取層.

  • 從 express.js 路由清理業務程式碼。

  • 服務層不應包含 request 和 response。

  • 服務層不應傳回任何與傳輸層關聯的數據,例如狀態碼和回應頭。

範例

  route.post('/', 
    validators.userSignup, // 中间件处理验证
    async (req, res, next) => {
      // 路由的实际责任
      const userDTO = req.body;

      // 调用服务层
      // 这里演示如何访问服务层
      const { user, company } = await UserService.Signup(userDTO);

      // 返回响应
      return res.json({ user, company });
    });

下面是服務層範例程式碼。

  import UserModel from '../models/user';
  import CompanyModel from '../models/company';

  export default class UserService {

    async Signup(user) {
      const userRecord = await UserModel.create(user);
      const companyRecord = await CompanyModel.create(userRecord); // 依赖用户的数据记录 
      const salaryRecord = await SalaryModel.create(userRecord, companyRecord); // 依赖用户与公司数据

      ...whatever

      await EmailService.startSignupSequence(userRecord)

      ...do more stuff

      return { user: userRecord, company: companyRecord };
    }
  }

在Github 查看範例程式碼

https://github.com/santiq/bulletproof-nodejs

##使用發布/訂閱模式

嚴格來講發布/訂閱模式並不屬於3 層結構的範疇,但卻很實用。

這裡有一個簡單的 node.js API 用來建立用戶,所以同時你可能還需要呼叫外部服務、分析資料、或發送一連串的郵件。很快,這個簡單的原本用於創建用戶的函數,由於充斥各種功能,程式碼已經超過了 1000 行。

現在是時候把這些功能都拆分為獨立功能了,這樣才能讓你的程式碼繼續保持可維護性。

  import UserModel from '../models/user';
  import CompanyModel from '../models/company';
  import SalaryModel from '../models/salary';

  export default class UserService() {

    async Signup(user) {
      const userRecord = await UserModel.create(user);
      const companyRecord = await CompanyModel.create(user);
      const salaryRecord = await SalaryModel.create(user, salary);

      eventTracker.track(
        'user_signup',
        userRecord,
        companyRecord,
        salaryRecord
      );

      intercom.createUser(
        userRecord
      );

      gaAnalytics.event(
        'user_signup',
        userRecord
      );

      await EmailService.startSignupSequence(userRecord)

      ...more stuff

      return { user: userRecord, company: companyRecord };
    }

  }

同步的呼叫依賴服務仍不是最佳的解決方案.

#更好的做法是觸發事件,如:一個新用戶使用郵箱方式註冊了。

就這樣,建立使用者的Api 完成了它的功能,剩下的就交給訂閱事件的處理器來負責。

  import UserModel from '../models/user';
  import CompanyModel from '../models/company';
  import SalaryModel from '../models/salary';

  export default class UserService() {

    async Signup(user) {
      const userRecord = await this.userModel.create(user);
      const companyRecord = await this.companyModel.create(user);
      this.eventEmitter.emit('user_signup', { user: userRecord, company: companyRecord })
      return userRecord
    }

  }
並且你可以把事件處理器分割成多個獨立的檔案。

  eventEmitter.on('user_signup', ({ user, company }) => {

    eventTracker.track(
      'user_signup',
      user,
      company,
    );

    intercom.createUser(
      user
    );

    gaAnalytics.event(
      'user_signup',
      user
    );
  })
  eventEmitter.on('user_signup', async ({ user, company }) => {
    const salaryRecord = await SalaryModel.create(user, company);
  })
  eventEmitter.on('user_signup', async ({ user, company }) => {
    await EmailService.startSignupSequence(user)
  })
你可以使用try-catch 來包裹await 語句,或 

透過process.on('unhandledRejection',cb) 的形式註冊'unhandledPromise' 的事件處理器

依賴注入

依賴注入或說控制反轉(IoC) 是一個通用的模式,透過'注入' 或傳遞類別或函數中涉及的依賴項的建構器,幫助你組織程式碼。

例如,當你為服務編寫單元測試,或在另外的上下文中使用服務時,透過這種方式,注入

依賴項就會變得很靈活。

不使用依賴注入的程式碼

  import UserModel from '../models/user';
  import CompanyModel from '../models/company';
  import SalaryModel from '../models/salary';  
  class UserService {
    constructor(){}
    Sigup(){
       //调用 UserMode, CompanyModel,等
      ...
    }
  }

手動依賴注入的程式碼

  export default class UserService {
    constructor(userModel, companyModel, salaryModel){
      this.userModel = userModel;
      this.companyModel = companyModel;
      this.salaryModel = salaryModel;
    }
    getMyUser(userId){
      // 通过this获取模型
      const user = this.userModel.findById(userId);
      return user;
    }
  }
現在你可以注入自訂的依賴。

  import UserService from '../services/user';
  import UserModel from '../models/user';
  import CompanyModel from '../models/company';
  const salaryModelMock = {
    calculateNetSalary(){
      return 42;
    }
  }
  const userServiceInstance = new UserService(userModel, companyModel, salaryModelMock);
  const user = await userServiceInstance.getMyUser('12346');

一个服务可以拥有的依赖项数量是无限的,当您添加一个新的服务时重构它的每个实例是一个无聊且容易出错的任务。

这就是创建依赖注入框架的原因。

其思想是在类中声明依赖项,当需要该类的实例时,只需调用「服务定位器」。

我们可以参考一个在 nodejs 引入 D.I npm库typedi的例子。

你可以在官方文档上查看更多关于使用 typedi的方法

https://www.github.com/typestack/typedi

警告 typescript 例子

  import { Service } from 'typedi';
  @Service()
  export default class UserService {
    constructor(
      private userModel,
      private companyModel, 
      private salaryModel
    ){}

    getMyUser(userId){
      const user = this.userModel.findById(userId);
      return user;
    }
  }

services/user.ts

现在,typedi 将负责解析 UserService 所需的任何依赖项。

  import { Container } from 'typedi';
  import UserService from '../services/user';
  const userServiceInstance = Container.get(UserService);
  const user = await userServiceInstance.getMyUser('12346');

滥用服务定位器是一种反面模式

在 Express.js 中使用依赖注入

在express.js中使用 D.I. 是 node.js 项目架构的最后一个难题。

路由层

  route.post('/', 
    async (req, res, next) => {
      const userDTO = req.body;

      const userServiceInstance = Container.get(UserService) // Service locator

      const { user, company } = userServiceInstance.Signup(userDTO);

      return res.json({ user, company });
    });

太棒了,项目看起来很棒!
它是如此有组织,以至于我现在就想写代码了。

在github上查看源码

https://github.com/santiq/bulletproof-nodejs

一个单元测试的例子

通过使用依赖注入和这些组织模式,单元测试变得非常简单。

您不必模拟 req/res 对象或要求(…)调用。

例子:注册用户方法的单元测试

tests/unit/services/user.js

  import UserService from '../../../src/services/user';

  describe('User service unit tests', () => {
    describe('Signup', () => {
      test('Should create user record and emit user_signup event', async () => {
        const eventEmitterService = {
          emit: jest.fn(),
        };

        const userModel = {
          create: (user) => {
            return {
              ...user,
              _id: 'mock-user-id'
            }
          },
        };

        const companyModel = {
          create: (user) => {
            return {
              owner: user._id,
              companyTaxId: '12345',
            }
          },
        };

        const userInput= {
          fullname: 'User Unit Test',
          email: 'test@example.com',
        };

        const userService = new UserService(userModel, companyModel, eventEmitterService);
        const userRecord = await userService.SignUp(teamId.toHexString(), userInput);

        expect(userRecord).toBeDefined();
        expect(userRecord._id).toBeDefined();
        expect(eventEmitterService.emit).toBeCalled();
      });
    })
  })

定时任务

因此,既然业务逻辑封装到了服务层中,那么在定时任务中使用它就更容易了。

您永远不应该依赖 node.js的 setTimeout 或其他延迟执行代码的原生方法,而应该依赖一个框架把你的定时任务和执行持久化到数据库。

这样你就可以控制失败的任务和成功的反馈信息。

我已经写了一个关于这个的好的练习,

 查看我关于使用 node.js 最好的任务管理器 agenda.js 的指南.

https://softwareontheroad.com/nodejs-scalability-issues

配置项和私密信息

根据 应用程序的12个因素 的最佳概念,我们存储 API 密钥和数据库连接配置的最佳方式是使用 .env文件。

创建一个 .env 文件,一定不要提交 (在你的仓库里要有一个包含默认值的.env 文件)dotenv 这个npm 包会加载 .env 文件,并把变量添加到 node.js 的 process.env 对象中。

这些本来已经足够了,但是我喜欢添加一个额外的步骤。
拥有一个 config/index.ts 文件,在这个文件中,dotenv 这个npm 包会加载 .env 文件,然后我使用一个对象存储这些变量,至此我们有了一个结构和代码自动加载。

config/index.js

  const dotenv = require('dotenv');
  //config()方法会读取你的 .env 文件,解析内容,添加到 process.env。
  dotenv.config();

  export default {
    port: process.env.PORT,
    databaseURL: process.env.DATABASE_URI,
    paypal: {
      publicKey: process.env.PAYPAL_PUBLIC_KEY,
      secretKey: process.env.PAYPAL_SECRET_KEY,
    },
    paypal: {
      publicKey: process.env.PAYPAL_PUBLIC_KEY,
      secretKey: process.env.PAYPAL_SECRET_KEY,
    },
    mailchimp: {
      apiKey: process.env.MAILCHIMP_API_KEY,
      sender: process.env.MAILCHIMP_SENDER,
    }
  }

这样可以避免代码中充斥着 process.env.MY_RANDOM_VAR 指令,并且通过自动完成,您不必知道 .env 文件中是如何命名的。

在github 上查看源码

https://github.com/santiq/bulletproof-nodejs

加载器

加载器源于 W3Tech microframework 但不依赖于他们扩展。

这个想法是指,你可以拆分启动加载过程到可测试的独立模块中。

先来看一个传统的 express.js 应用的初始化示例:

  const mongoose = require('mongoose');
  const express = require('express');
  const bodyParser = require('body-parser');
  const session = require('express-session');
  const cors = require('cors');
  const errorhandler = require('errorhandler');
  const app = express();

  app.get('/status', (req, res) => { res.status(200).end(); });
  app.head('/status', (req, res) => { res.status(200).end(); });
  app.use(cors());
  app.use(require('morgan')('dev'));
  app.use(bodyParser.urlencoded({ extended: false }));
  app.use(bodyParser.json(setupForStripeWebhooks));
  app.use(require('method-override')());
  app.use(express.static(__dirname + '/public'));
  app.use(session({ secret: process.env.SECRET, cookie: { maxAge: 60000 }, resave: false, saveUninitialized: false }));
  mongoose.connect(process.env.DATABASE_URL, { useNewUrlParser: true });

  require('./config/passport');
  require('./models/user');
  require('./models/company');
  app.use(require('./routes'));
  app.use((req, res, next) => {
    var err = new Error('Not Found');
    err.status = 404;
    next(err);
  });
  app.use((err, req, res) => {
    res.status(err.status || 500);
    res.json({'errors': {
      message: err.message,
      error: {}
    }});
  });

  ... more stuff 

  ... maybe start up Redis

  ... maybe add more middlewares

  async function startServer() {    
    app.listen(process.env.PORT, err => {
      if (err) {
        console.log(err);
        return;
      }
      console.log(`Your server is ready !`);
    });
  }

  // 启动服务器
  startServer();

天呐,上面的面条代码,不应该出现在你的项目中对吧。

再来看看,下面是一种有效处理初始化过程的示例:

  const loaders = require('./loaders');
  const express = require('express');

  async function startServer() {

    const app = express();

    await loaders.init({ expressApp: app });

    app.listen(process.env.PORT, err => {
      if (err) {
        console.log(err);
        return;
      }
      console.log(`Your server is ready !`);
    });
  }

  startServer();

现在,各加载过程都是功能专一的小文件了。

loaders/index.js

  import expressLoader from './express';
  import mongooseLoader from './mongoose';

  export default async ({ expressApp }) => {
    const mongoConnection = await mongooseLoader();
    console.log('MongoDB Intialized');
    await expressLoader({ app: expressApp });
    console.log('Express Intialized');

    // ... 更多加载器

    // ... 初始化 agenda
    // ... or Redis, or whatever you want
  }

express 加载器。

loaders/express.js

  import * as express from 'express';
  import * as bodyParser from 'body-parser';
  import * as cors from 'cors';

  export default async ({ app }: { app: express.Application }) => {

    app.get('/status', (req, res) => { res.status(200).end(); });
    app.head('/status', (req, res) => { res.status(200).end(); });
    app.enable('trust proxy');

    app.use(cors());
    app.use(require('morgan')('dev'));
    app.use(bodyParser.urlencoded({ extended: false }));

    // ...More middlewares

    // Return the express app
    return app;
  })

mongo 加载器

loaders/mongoose.js

  import * as mongoose from 'mongoose'
  export default async (): Promise<any> => {
    const connection = await mongoose.connect(process.env.DATABASE_URL, { useNewUrlParser: true });
    return connection.connection.db;
  }</any>

在 Github 查看更多加载器的代码示例

https://github.com/santiq/bulletproof-nodejs

结语

我们深入的分析了 node.js 项目的结构,下面是一些可以分享给你的总结:

  • 使用3层结构。

  • 控制器中不要有任何业务逻辑代码。

  • 采用发布/订阅模型来处理后台异步任务。

  • 使用依赖注入。

  • 使用配置管理,避免泄漏密码、密钥等机密信息。

  • 将 node.js 服务器的配置拆分为可独立加载的小文件。

前往 Github 查看完整示例

https://github.com/santiq/bulletproof-nodejs

原文網址:https://dev.to/santypk4/bulletproof-node-js-project-architecture-4epf

翻譯網址:https://learnku.com/ nodejs/t/38129

更多程式相關知識,請造訪:程式設計入門! !

以上是詳解Nodejs Express.js專案架構的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文轉載於:learnku。如有侵權,請聯絡admin@php.cn刪除
JavaScript的角色:使網絡交互和動態JavaScript的角色:使網絡交互和動態Apr 24, 2025 am 12:12 AM

JavaScript是現代網站的核心,因為它增強了網頁的交互性和動態性。 1)它允許在不刷新頁面的情況下改變內容,2)通過DOMAPI操作網頁,3)支持複雜的交互效果如動畫和拖放,4)優化性能和最佳實踐提高用戶體驗。

C和JavaScript:連接解釋C和JavaScript:連接解釋Apr 23, 2025 am 12:07 AM

C 和JavaScript通過WebAssembly實現互操作性。 1)C 代碼編譯成WebAssembly模塊,引入到JavaScript環境中,增強計算能力。 2)在遊戲開發中,C 處理物理引擎和圖形渲染,JavaScript負責遊戲邏輯和用戶界面。

從網站到應用程序:JavaScript的不同應用從網站到應用程序:JavaScript的不同應用Apr 22, 2025 am 12:02 AM

JavaScript在網站、移動應用、桌面應用和服務器端編程中均有廣泛應用。 1)在網站開發中,JavaScript與HTML、CSS一起操作DOM,實現動態效果,並支持如jQuery、React等框架。 2)通過ReactNative和Ionic,JavaScript用於開發跨平台移動應用。 3)Electron框架使JavaScript能構建桌面應用。 4)Node.js讓JavaScript在服務器端運行,支持高並發請求。

Python vs. JavaScript:比較用例和應用程序Python vs. JavaScript:比較用例和應用程序Apr 21, 2025 am 12:01 AM

Python更適合數據科學和自動化,JavaScript更適合前端和全棧開發。 1.Python在數據科學和機器學習中表現出色,使用NumPy、Pandas等庫進行數據處理和建模。 2.Python在自動化和腳本編寫方面簡潔高效。 3.JavaScript在前端開發中不可或缺,用於構建動態網頁和單頁面應用。 4.JavaScript通過Node.js在後端開發中發揮作用,支持全棧開發。

C/C在JavaScript口譯員和編譯器中的作用C/C在JavaScript口譯員和編譯器中的作用Apr 20, 2025 am 12:01 AM

C和C 在JavaScript引擎中扮演了至关重要的角色,主要用于实现解释器和JIT编译器。1)C 用于解析JavaScript源码并生成抽象语法树。2)C 负责生成和执行字节码。3)C 实现JIT编译器,在运行时优化和编译热点代码,显著提高JavaScript的执行效率。

JavaScript在行動中:現實世界中的示例和項目JavaScript在行動中:現實世界中的示例和項目Apr 19, 2025 am 12:13 AM

JavaScript在現實世界中的應用包括前端和後端開發。 1)通過構建TODO列表應用展示前端應用,涉及DOM操作和事件處理。 2)通過Node.js和Express構建RESTfulAPI展示後端應用。

JavaScript和Web:核心功能和用例JavaScript和Web:核心功能和用例Apr 18, 2025 am 12:19 AM

JavaScript在Web開發中的主要用途包括客戶端交互、表單驗證和異步通信。 1)通過DOM操作實現動態內容更新和用戶交互;2)在用戶提交數據前進行客戶端驗證,提高用戶體驗;3)通過AJAX技術實現與服務器的無刷新通信。

了解JavaScript引擎:實施詳細信息了解JavaScript引擎:實施詳細信息Apr 17, 2025 am 12:05 AM

理解JavaScript引擎內部工作原理對開發者重要,因為它能幫助編寫更高效的代碼並理解性能瓶頸和優化策略。 1)引擎的工作流程包括解析、編譯和執行三個階段;2)執行過程中,引擎會進行動態優化,如內聯緩存和隱藏類;3)最佳實踐包括避免全局變量、優化循環、使用const和let,以及避免過度使用閉包。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

MantisBT

MantisBT

Mantis是一個易於部署的基於Web的缺陷追蹤工具,用於幫助產品缺陷追蹤。它需要PHP、MySQL和一個Web伺服器。請查看我們的演示和託管服務。

EditPlus 中文破解版

EditPlus 中文破解版

體積小,語法高亮,不支援程式碼提示功能

ZendStudio 13.5.1 Mac

ZendStudio 13.5.1 Mac

強大的PHP整合開發環境

Safe Exam Browser

Safe Exam Browser

Safe Exam Browser是一個安全的瀏覽器環境,安全地進行線上考試。該軟體將任何電腦變成一個安全的工作站。它控制對任何實用工具的訪問,並防止學生使用未經授權的資源。

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)