Heim  >  Artikel  >  Web-Frontend  >  Detaillierte Erläuterung der Projektarchitektur von Nodejs Express.js

Detaillierte Erläuterung der Projektarchitektur von Nodejs Express.js

青灯夜游
青灯夜游nach vorne
2020-09-15 11:09:424477Durchsuche

Detaillierte Erläuterung der Projektarchitektur von Nodejs Express.js

Video-Tutorial-Empfehlung: nodejs-Tutorial

Einführung

Im Bereich node.js ist Express.js ein bekanntes REST-API-Entwicklungsframework. Obwohl es sehr gut ist, sagt Ihnen niemand, wie Sie den Projektcode organisieren sollen.

Normalerweise ist das nichts, aber für Entwickler ist dies ein weiteres Problem, dem wir uns stellen müssen.

Eine gute Projektstruktur kann nicht nur doppelten Code beseitigen, die Systemstabilität verbessern, das Systemdesign verbessern, sondern auch die zukünftige Erweiterung erleichtern.

Seit Jahren beschäftige ich mich mit der Umgestaltung und Migration schlecht strukturierter und schlecht gestalteter node.js-Projekte. Dieser Artikel ist eine Zusammenfassung meiner bisher gesammelten Erfahrungen.

Verzeichnisstruktur

Das Folgende ist meine empfohlene Art, Projektcode zu organisieren.

Es stammt aus der Praxis der Projekte, an denen ich teilgenommen habe. Die Funktionen und Funktionen jedes Verzeichnismoduls sind wie folgt:

  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)

Und das ist nicht nur die Art und Weise, wie der Code organisiert ist ...

3-Schichten-Struktur

Diese Idee stammt aus Dem Prinzip der Interessenstrennung , das die Geschäftslogik vom API-Routing von node.js trennt.

3 层示意图

Denn eines Tages in der Zukunft können Sie Ihr Geschäft in einem CLI-Tool oder anderswo abwickeln. Natürlich ist das vielleicht nicht der Fall, aber die Verwendung von API-Aufrufen zur Abwicklung Ihres eigenen Geschäfts im Projekt ist doch keine gute Idee ...

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

Handeln Sie die Geschäftslogik nicht direkt im Controller!! In Ihrer Bewerbung können Sie Geschäfte bequem direkt im Controller abwickeln. Wenn Sie dies tun, werden Sie leider bald mit einem komplexen Controller-Code konfrontiert, und die „negativen Konsequenzen“ werden folgen, wie z. B. die Verwendung komplexer

request

oder responses beim Umgang mit Unit-Tests Simulation. Gleichzeitig wird es kompliziert, wenn Sie entscheiden, wann Sie eine Antwort an den Kunden zurücksenden oder ob Sie nach dem Senden der Antwort noch etwas bearbeiten möchten.

Bitte machen Sie dies nicht wie im folgenden Beispiel.

  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)
  });

Verwenden Sie eine Serviceschicht, um Geschäfte abzuwickeln.

Es wird empfohlen, die Geschäftslogik in einer separaten Serviceschicht abzuwickeln.

Diese Ebene ist eine Sammlung von „Klassen“, die den für node.js geltenden

SOLID

-Prinzipien folgen.

In dieser Ebene sollte es keinerlei Datenabfrageoperationen geben. Der richtige Ansatz besteht darin, eine Datenzugriffsschicht zu verwenden.

    Bereinigen Sie den Geschäftscode aus dem express.js-Routing.
  • Die Serviceschicht sollte keine Anfrage und Antwort enthalten.
  • Die Serviceschicht sollte keine mit der Transportschicht verbundenen Daten zurückgeben, wie z. B. Statuscodes und Antwortheader.
Beispiel

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

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

      // 返回响应
      return res.json({ user, company });
    });
Das Folgende ist der Beispielcode der Serviceschicht.

  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 };
    }
  }

Sehen Sie sich den Beispielcode auf Github an

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

Verwenden Sie das Publish/Subscribe-Modell

Genau genommen gehört das Publish/Subscribe-Modell nicht zu den 3 -stufige Struktur. Aber es ist sehr praktisch.

Hier ist eine einfache node.js-API zum Erstellen von Benutzern. Gleichzeitig müssen Sie möglicherweise auch externe Dienste aufrufen, Daten analysieren oder eine Reihe von E-Mails senden. Bald wurde diese einfache Funktion, die ursprünglich zum Erstellen von Benutzern verwendet wurde, mit verschiedenen Funktionen gefüllt und der Code umfasste mehr als 1.000 Zeilen.

Jetzt ist es an der Zeit, diese Funktionen in unabhängige Funktionen aufzuteilen, damit Ihr Code weiterhin wartbar bleibt.

  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 };
    }

  }

Der synchrone Aufruf abhängiger Dienste ist immer noch nicht die beste Lösung.

Ein besserer Ansatz besteht darin, Ereignisse auszulösen, wie zum Beispiel: Ein neuer Benutzer registriert sich per E-Mail.

Auf diese Weise hat die API zum Erstellen von Benutzern ihre Funktion erfüllt und der Rest bleibt dem Prozessor überlassen, der das Ereignis abonniert.

  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
    }

  }

Und Sie können den Event-Handler in mehrere unabhängige Dateien aufteilen.

  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)
  })

Sie können try-catch verwenden, um die Wait-Anweisung zu verpacken, oder

Registrieren Sie den Ereignishandler „unhandledPromise“ in Form von

process.on('unhandledRejection',cb)Abhängigkeitsinjektion

oder Abhängigkeitsinjektion Inversion of Control (IoC) ist ein allgemeines Muster, das Ihnen hilft, Ihren Code zu organisieren, indem es Konstruktoren von Abhängigkeiten, die an einer Klasse oder Funktion beteiligt sind, „injiziert“ oder übergibt.

Auf diese Weise wird das Einfügen von

Abhängigkeiten

sehr flexibel, wenn Sie beispielsweise Unit-Tests für Ihren Dienst schreiben oder wenn Sie den Dienst in einem anderen Kontext verwenden.

Code ohne Abhängigkeitsinjektion

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

Code mit manueller Abhängigkeitsinjektion

  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;
    }
  }
Jetzt können Sie benutzerdefinierte Abhängigkeiten injizieren.

  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;
  }

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

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

结语

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

  • 使用3层结构。

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

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

  • 使用依赖注入。

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

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

前往 Github 查看完整示例

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

Originaladresse: https://dev.to/santypk4/bulletproof-node-js-project-architecture-4epf

Übersetzungsadresse: https://learnku.com/nodejs/t/38129

Mehr für Programmierkenntnisse finden Sie unter: Einführung in die Programmierung! !

Das obige ist der detaillierte Inhalt vonDetaillierte Erläuterung der Projektarchitektur von Nodejs Express.js. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:learnku.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen