>  기사  >  웹 프론트엔드  >  Nodejs Express.js 프로젝트 아키텍처에 대한 자세한 설명

Nodejs Express.js 프로젝트 아키텍처에 대한 자세한 설명

青灯夜游
青灯夜游앞으로
2020-09-15 11:09:424549검색

Nodejs Express.js 프로젝트 아키텍처에 대한 자세한 설명

비디오 튜토리얼 추천: nodejs tutorial

소개

node.js 분야에서 Express.js는 잘 알려진 REST API 개발 프레임워크입니다. 아주 좋은데도 프로젝트 코드를 어떻게 구성해야 하는지 아무도 알려주지 않습니다.

보통 이것은 아무 것도 아니지만 개발자에게는 이것이 우리가 직면해야 하는 또 다른 문제입니다.

좋은 프로젝트 구조는 중복 코드를 제거하고, 시스템 안정성을 개선하고, 시스템 설계를 개선할 뿐만 아니라 향후 확장도 더 쉽게 만들어줍니다.

수년 동안 저는 잘못 구조화되고 잘못 설계된 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 的三层示意图

비즈니스 로직을 컨트롤러에서 직접 처리하지 마세요

애플리케이션에서는 편의를 위해 컨트롤러에서 직접 비즈니스를 처리할 수도 있습니다. 불행하게도 이렇게 하면 곧 복잡한 컨트롤러 코드에 직면하게 될 것이며, 단위 테스트 시뮬레이션을 처리할 때 복잡한 요청 또는 응답을 사용해야 하는 등의 "부정적인 결과"가 따르게 됩니다.

동시에 클라이언트에 응답을 언제 반환할지 또는 응답을 보낸 후 일부 처리를 수행할지 결정할 때 복잡해집니다.

아래 예시와 같이 하지 마세요.

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

비즈니스 처리를 위해 서비스 레이어를 사용하세요

비즈니스 로직을 별도의 서비스 레이어에서 처리하는 것을 권장합니다.

이 레이어는 node.js에 적용되는 SOLID 원칙을 따르는 "클래스"의 모음입니다.

이 레이어에는 어떤 형태의 데이터 쿼리 작업도 있어서는 안 됩니다. 올바른 접근 방식은 데이터 액세스 계층을 사용하는 것입니다.

  • express.js 경로에서 비즈니스 코드를 정리하세요.

  • 서비스 계층에는 요청과 응답이 포함되어서는 안 됩니다.

  • 서비스 계층은 상태 코드 및 응답 헤더와 같이 전송 계층과 관련된 데이터를 반환해서는 안 됩니다.

Example

  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

Publish/Subscribe 모델을 사용하세요

엄밀히 말하면 발행/구독 모델은 3에 속하지 않습니다. - 계층 구조이지만 매우 실용적입니다.

다음은 사용자 생성을 위한 간단한 node.js API입니다. 동시에 외부 서비스를 호출하거나 데이터를 분석하거나 일련의 이메일을 보내야 할 수도 있습니다. 원래 사용자 생성을 위해 사용했던 이 간단한 함수는 어느새 다양한 함수로 채워져 코드가 1,000줄을 넘었습니다.

이제 코드를 계속 유지 관리할 수 있도록 이러한 함수를 독립적인 함수로 분할할 차례입니다.

  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를 사용하여 wait 문을 래핑하거나 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;
  }

在 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.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제