hi,你好!欢迎访问本站!登录
本站由网站地图腾讯云宝塔系统阿里云强势驱动
当前位置:首页 - 教程 - 前端开发 - 正文 君子好学,自强不息!

详解Nodejs Express.js项目架构_WEB前端开发

2020-09-15前端开发搜奇网3°c
A+ A-

视频教程引荐: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 路由中星散出去。

因为未来的某天,你大概会在 CLI 东西或是其他地方处置惩罚你的营业。固然,也有大概不会,但在项目中运用API挪用的体式格局来处置惩罚本身的营业究竟不是一个好主意...

不要在掌握器中直接处置惩罚营业逻辑!!

在你的运用中,你大概经为了图方便而直接的在掌握器处置惩罚营业。不幸的是,这么做的话很快你将面临相面条一样庞杂的掌握器代码,“恶果”也会随之而来,比如在处置惩罚单元测试的时刻不能不运用庞杂的 requestresponse 模仿。

同时,在决议什么时候向客户端返回相应,或愿望在发送相应以后再举行一些处置惩罚的时刻,将会变得很庞杂。

请不要像下面例子如许做.

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

在 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项目架构的细致内容,更多请关注ki4网别的相干文章!

  选择打赏方式
微信赞助

打赏

QQ钱包

打赏

支付宝赞助

打赏

  移步手机端
详解Nodejs Express.js项目架构_WEB前端开发

1、打开你手机的二维码扫描APP
2、扫描左则的二维码
3、点击扫描获得的网址
4、可以在手机端阅读此文章

本文来源:搜奇网

本文地址:https://www.sou7.cn/300736.html

关注我们:微信搜索“搜奇网”添加我为好友

版权声明: 本文仅代表作者个人观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。请记住本站网址https://www.sou7.cn/搜奇网。