概述

Node.js已成为最流行的 Web 开发框架之一。这是因为它具有快速、可扩展和强大的架构。许多大公司,如Netflix、Uber、Trello、PayPal等,都从Node.js中获益。应用程序的扩展也变得容易。这都是因为 Node js 遵循的架构模式。

介绍

Node.js是完全免费和开源的。它还被世界各地的大量开发人员使用和支持。Node.js可以称为 Chrome 的 V8 Js 引擎、事件循环和低级 I/O API 的组合。下图显示了Node.js的体系结构:

Node.js的核心部分是用 C 和 C++ 编写的。Node js 基于单线程事件循环架构,允许 Node 处理多个客户端请求。Node js 使用异步模型和非阻塞 I/O 的概念。我们将详细研究这些术语。

Node.js 是其他服务器端平台的最佳选择之一,因为它遵循的架构模式。

Node.js中的单线程事件循环体系结构

Node.js基于单线程事件驱动架构。它具有非阻塞 I/O 模型。让我们详细看看这些概念:

Node.js是单线程事件循环

Node.js 中的事件循环是单线程的。所有请求可以包含两部分 - 同步和异步。事件循环或主线程占用同步部分,并将异步部分分配给后台线程执行。然后,主线程占用其他请求的同步部分。

例:

 
public class EventLoop {
    while(true) {
        if(Event Queue receives a JavaScript Function Call) {
       		ClientRequest request = EventQueue.getClientRequest();
                If(request requires BlokingIO or takes more computation time)
                    Assign request to Thread T1
                Else
                    Process and Prepare a response
        }
    }
}

节点具有非阻塞 I/O 模型

Node.js 中的主线程将耗时的异步操作分配给后台线程。它不会等待后台线程完成该操作;它占用事件队列中的下一个操作。

主线程仅处理每个请求的同步部分。后台线程完成异步任务后,会通知主线程继续执行该操作,以便进一步执行回调代码。因此,在异步操作执行 I/O 时,主线程不会被阻塞。

文件系统模块的示例。以下是阻止操作的示例:

 
const fs = require('fs');
const data = fs.readFileSync('/file.txt'); // blocks here until file is read

console.log('Node.js Architecture'); // Will execute after the file is read.

显示非阻塞的上述代码的异步版本:

 
const fs = require('fs');

fs.readFile('/file.txt', (err, data) => {
  if (err) throw err;
  console.log(data);
});

console.log('Node.js Architecture'); // Will execute before the file is read.

基于事件,而不是等待 I/O 操作

在后台运行的线程执行一些异步操作,使用事件通知主线程。异步任务完成后,需要调用关联的回调函数。如果主线程正忙于执行其他请求,那么每当它变得空闲时,主线程就会对后台线程的事件通知做出反应。它运行回调代码并将响应发送回客户端。

 
const fs = require('fs');

fs.readFile('/file.txt', (err, data) => {
  if (err) throw err;
  console.log(data);
});

在上面的代码中,读取文件后,会执行关联的回调函数。

Node.js体系结构的组件

让我们一一看所有组件:

  • 请求:请求由客户端发送以获取某些服务器资源,或由服务器本身发送到其他服务器以从中获取资源。因此,资源可以有两种类型:传入和传出。
  • 节点 js 服务器:它是服务器端或后端平台,用于在定义的各种端点上接收来自用户的请求。所有请求都由服务器处理,生成的响应将发送回客户端。服务器不断侦听系统中的指定端口。客户端通过指定服务器的目标 IP 和端口来发送请求。
  • 事件循环:它是一个有六个阶段的无限循环。重复所有这六个阶段,直到没有代码可以执行。在这六个阶段中,事件循环接收请求,处理请求,并将这些请求的响应返回给客户端。事件循环的六个阶段是:
    1. 定时器
    2. I/O 回调
    3. 等待/准备
    4. I/O 轮询
    5. setImmediate() 回调
    6. 关闭事件

  • 事件队列:客户端发送的所有请求都存储在事件队列中。然后,它们将逐个传递到事件循环。在其他线程上运行的操作的回调也会添加到其中,以便主线程占用它们。
  • 线程池:除了主线程之外,线程池中还管理着其他线程。所有异步进程和非阻塞 I/O 都附加到其中一个线程,因为它们继续在后台执行。
  • 外部资源:这些是服务器为满足客户端请求而必须获取的资源。需要它们来处理阻止请求。例如,计算、数据存储等。

Nodejs Server 的工作流程

使用 Node.js 创建的 Web 服务器的工作流涉及上一节中讨论的所有组件。整个建筑工作如下图所示。

  • 客户端向 Web 服务器发送请求。这些请求可以是阻塞(复杂)或非阻塞(简单)。请求的目的可能是查询数据、删除数据或更新数据。
  • 检索请求并将其添加到事件队列中。
  • 然后,请求将逐个从事件队列传递到事件循环。
  • 简单请求由主线程处理,并将响应发送回客户端。
  • 将复杂请求分配给线程池中的线程。

Node.js应用程序架构的最佳实践

合并发布者-订阅者模型

发布/订阅模型是一种消息传递模式,其中组件发布消息,其他组件订阅消息。这样做是为了通知其他人并将数据发送给他们。因此,这个通信系统涉及两个组成部分——发布者和订阅者。

消息由发布者通过指定的渠道发送,而订阅者或接收端对此一无所知。这些频道另一端的订阅者在发布者不知情的情况下收听这些消息。因此,可以使用此数据共享模型连接多个节点。所有订阅者都可以收听一个操作并做出响应。

现在让我们借助一些示例来理解这个概念。如果我们必须向大量用户发送有关某些事件的通知,那么可能需要快速向客户端发送消息。这是一个巨大的挑战。当用户数量扩大时,它会变得更大。当用户处于非活动状态时,用户可能会错过这些消息而不响应它们。因此,它落在系统上,以确保客户端在处于活动状态时接收消息。

pub/sub 模型允许组件的解耦,并且每个组件都绑定依赖于消息代理。发布者发送消息,客户端订阅它们以接收来自代理的消息。实现代理是为了保存消息并在客户端联机时传递消息。因此,开发人员无需担心其他组件的接口和结构。这也允许系统的可扩展性。

采用分层方法

express.js的使用允许将应用程序的逻辑轻松分发到各种类别中。通过划分关注点,代码质量得到提高,调试变得更加容易。该设置将应用程序的逻辑与 API 路由分开,以便后台进程不会变得复杂。 代码分为三类——业务逻辑、数据库和 API 路由。

控制器层:这是定义 API 路由的地方。请求在此处被解构,并根据请求中的信息完成一些处理。完成的处理将被收集并传递到服务层。

服务层:此处定义了与应用程序相关的业务逻辑。使用的类和函数包含在此层中。遵循面向对象编程的 SOLID 原则。各种路由的处理逻辑也在这个层中。

数据访问层:此层负责数据库处理。数据库的所有读取、写入或任何其他操作都由该层处理。此处定义了 SQL 查询、与数据库的连接、文档模型和其他相关代码。

使用依赖注入

依赖关系注入是软件的一种设计模式,其中软件的依赖关系作为参数传入,而不是包含它们或在软件内部创建依赖关系。这种技术提高了模块的灵活性、独立性、可伸缩性和可重用性。测试也变得容易。

让我们举个例子来理解这个概念:

 
class PostManager {
  constructor(postStore) {
    this.posts = [];
    this.postStore = postStore;
  }
 
  getPosts = () => {
    return this.posts;
  };
 
  loadPosts = async () => {
    let res = await this.postStore.getList();
    this.posts = res;
  };
}

在上面的代码示例中,我们只关注管理帖子,而不是如何存储帖子。获取帖子的依赖项在 PostManager 中传递,并且在使用时无需担心依赖项的内部工作。PostManager 类只需要知道如何使用 postStore。

依赖注入提高了代码的可伸缩性和可理解性。该代码也易于测试。

利用第三方解决方案

Node.js有幸拥有支持它的大型开发人员社区。NPM,即 Node 包管理器,是一个包管理器,可用于轻松地在 Node.js 中安装所有第三方模块。NPM 有很多有据可查和维护的框架。利用所有这些使开发变得非常容易,开发人员可以更专注于逻辑部分。

其他一些也可以提供帮助的Node.js库包括:

  • 时刻(日期和时间)
  • Nodemon(代码更新时自动重启应用
  • 议程(作业安排)
  • Winston(日志记录)
  • 咕噜声(自动任务运行程序)

有几个第三方模块可用。但应该巧妙地使用它们。过度使用或依赖这些模块是不好的,因为它可能会影响应用程序的安全性。此外,它们可能会在项目中引入大量依赖项。

应用统一的文件夹结构

我们已经讨论了分层结构。将代码划分为各种模块有助于使调试和测试变得容易。它还促进了可重用性。

以下是在 Node.js 中设置新应用程序时应遵循的基本文件夹结构:

控制器层是 API 目录,服务层是 Services 目录,数据访问层是 Models 目录。/config 存储环境变量,而 /scripts 存储工作流自动化脚本。/test 目录包含测试用例,/或订阅者将事件处理程序存储在 pub/sub 模式中。

使用线框、格式化程序、样式指南和注释进行干净的编码

Linting 和格式化:检查代码是否存在 bug、错误和其他错误构造的静态代码分析器称为 linter。它们有助于识别我们代码中的错误和其他有害模式。一些例子是 Jslint 和 Eslint。另一方面,格式化程序确保在项目中遵循一致的样式。更漂亮的代码格式化程序是格式化格式化程序的一个例子。Linters 和格式化程序现在在许多 IDE 中都作为插件提供。

风格指南:风格指南有助于遵循顶级开发人员使用的命名约定和其他编码标准。其中一些例子是谷歌和Airbnb的风格指南。

添加注释:为了使代码更易于其他人阅读和理解,我们必须在项目的每个部分使用注释。它们告诉其他人您在代码的当前部分使用了什么逻辑。注释也是记录作者、功能和其他目的等详细信息的好方法。

通过单元测试、日志记录和错误处理来纠正错误

单元测试:这样做是为了检查一个单元或部分代码的准确性和正确性。这有助于减少调试时间和成本。它有助于验证各个单元是否正确执行其任务。 Jest、Mocha 和 Jasmine 是用于此目的的一些框架。

日志记录和错误处理:错误是程序中发生的问题或故障。它显示有错误信息和可能的修复。许多编程语言都有内置的日志生成器。这些记录系统在开发过程的每个阶段都是必不可少的。

在Node.js中,最常见的日志记录方式是使用 console.log() 函数在控制台中打印信息。

在日志记录过程中检查了三个重要的流:

  • Stdin – 处理进程的输入(例如键盘或鼠标)
  • Stdout – 处理控制台或单独文件的输出
  • Stderr – 专门处理错误消息和警告。

处理错误也是Node.js应用程序开发的重要组成部分。必须妥善解决回调地狱等问题。 应使用集中式错误处理组件,以避免重复错误。该组件应向管理员发送消息,处理监视事件并记录所有内容。必要时应使用 try-catch。

使用配置文件和环境变量

当应用程序被扩展时,就需要每个模块都可以访问的全局变量。我们可以将所有全局变量分隔在 config 文件夹中的一个文件中。所有环境变量都可以保存在 .env 文件中。

API 密钥、数据库密码和其他此类信息可以通过这种方式存储。它被保存为具有所有环境变量的 .env 文件。

 
DB_HOST=localhost
DB_USER=root
DB_PASS=abc@123

dotenv 包可用于导入所有这些环境变量。如果要进行任何更改,可以在一个地方完成,并将反映在应用程序中。

采用 Gzip 压缩

每当我们必须传输文件时,大文件可能会产生问题。因此,使用文件压缩技术会有所帮助。Gzip 是一种无损压缩机制,可以压缩文件以便快速传输。您可以压缩网页上提供的多媒体文件,以减少负载并加快处理速度。

Express.js有助于轻松实现压缩技术。在Express.js的文档中,建议使用 Gzip 压缩。

结论

  • Node.js基于单线程事件循环体系结构。
  • 主线程只处理每个请求的同步部分,并将异步部分分配给后台线程。
  • 发布/订阅模型是一种消息传递模式,其中组件发布消息,其他组件订阅消息。
  • 分层体系结构为应用程序提供了可靠性。
  • 依赖关系注入是一种软件设计模式,其中软件的依赖关系作为参数传入
  • 将代码划分为各种模块有助于使调试和测试变得容易。
  • 所有环境变量都可以保存在 .env 文件中。
  • Gzip 是一种用于压缩文件的无损压缩机制。
Logo

一起探索未来云端世界的核心,云原生技术专区带您领略创新、高效和可扩展的云计算解决方案,引领您在数字化时代的成功之路。

更多推荐