Node.js学习资料

309 阅读26分钟

什么是Node.js

Node.js是一个基于ChromeV8引擎的JavaScript运行环境,使用了一个事件驱动、非阻塞式I/O模型,JavaScript运行在浏览器之外的开发平台。

Node.js的使用场景

高并发、实时聊天、实时消息推送、客户端逻辑强大的SPA

Node.js 的特性

Node.js 保留了 JavaScript 在 Web 浏览器端中所使用的大部分 API,Node.js 的作者 Ryan Dahl 并没有改变这门语言本身的任何执行特性,它的编程模型依旧将基于作用域和原型链这些概念,这让 Node.js 这个 JavaScript 的运行环境具备了以下这些与众不同的特性:

  • 单一线程: Node.js 沿用了 JavaScript 单一线程的执行特性。即在 Node.js 中,JavaScript 的执行线程与其他线程之间同样也是无法共享状态的。单一线程的最大好处是不用像多线程编程那样处理很容易产生 bug 的同步问题,它从根本上杜绝了死锁问题,也避免了线程上下文交换所带来的性能上的开销。当然了,单一线程的执行方式也有它自身的弱点,譬如,它无法充分发挥多核处理器的性能、一个错误就会导致整个程序崩溃,以及当某一任务执行大量计算时会因长期占用处理器而影响其他异步 I/O 的执行。
  • 事件驱动: 在 Web 开发领域,JavaScript 如今在浏览器端正承担了越来越重要的角色,其事件驱动的编程模型也逐渐深入人心。当然了,这种编程模型虽然具有轻量级、松耦合等优势,但在多个异步任务的场景下,由于程序中的各个事件是彼此独立的,它们之间的协作就成为了一个需要我们费心解决的问题。
  • 异步编程: 在 Node.js 中,大部分 I/O 操作都是以异步调用的方式来进行的。Node.js 的开发者们在其底层构建了许多异步 I/O 的 API,包括文件读取、网络请求等。这样一来,我们就可以很自然地在语言层面上实现并行的 I/O 操作,使得程序中的每个调用都无须等待之前的 I/O 调用结束,从而提高程序的执行效率。例如,如果我们想要读取两个相互不依赖的文件,如果采用的是异步 I/O,其耗费的时间只取决于读取较慢的那个文件,而如果采用同步 I/O 的话,其耗时就是两个文件的读取时间之和了,异步操作模型带来的优势是显而易见的。

Node.js架构

image.png

Node三层架构

第一层:Node.js 标准库

这部分是由 Javascript编写的,即我们使用过程中直接能调用的 API,在源码中的 lib 目录下可以看到。

第二层:Node bindings

这一层是 Javascript 与底层 C/C++ 能够沟通的关键,前者通过 bindings 调用后者,相互交换数据,是第一层和第三层的桥梁。

第三层

是支撑 Node.js 运行的关键,由 C/C++ 实现,是node实现的一些底层逻辑。主要的有:

  • V8 是 Google 开发的 JavaScript 引擎,提供 JavaScript 运行环境,可以说它就是 Node.js 的发动机。
  • Libuv 是专门为 Node.js 开发的一个封装库,提供跨平台的异步 I/O 能力.
  • C-ares:提供了异步处理 DNS 相关的能力。
  • http_parser、OpenSSL、zlib 等:提供包括 http 解析、SSL、数据压缩等其他的能力。

Node.js事件循环(event loop)机制

概念

Node.js 在主线程里维护了一个事件队列, 当接到请求后,就将该请求作为一个事件放入这个队列中,然后继续接收其他请求。当主线程空闲时(没有请求接入时),就开始循环事件队列,检查队列中是否有要处理的事件,这时要分两种情况:如果是非 I/O 任务,就亲自处理,并通过回调函数返回到上层调用;如果是 I/O 任务或者,就从 **线程池 **中拿出一个线程来处理这个事件,并指定回调函数,然后继续循环队列中的其他事件。

当线程中的 I/O 任务完成以后,就执行指定的回调函数,并把这个完成的事件放到事件队列的尾部,等待事件循环,当主线程再次循环到该事件时,就直接处理并返回给上层调用。 这个过程就叫 事件循环(Event Loop)

事件循环模型

Node.js三层架构中,第三层的Libuv,为 Node.js 提供了跨平台,线程池,事件池,异步 I/O 等能力,是 Node.js 如此强大的关键,Node.js事件循环机制就是基于Libuv构建的。

libuv内部维护着一个默认4个线程的线程池,这些线程负责执行文件I/O操作、DNS操作、用户异步代码。

当 js 层传递给 libuv 一个操作任务时,libuv 会把这个任务加到队列中。之后分两种情况:

  1. 线程池中的线程都被占用的时候,队列中任务就要进行排队等待空闲线程(这也是setTimeout和setInterval计时并不准确的原因)。
  2. 线程池中有可用线程时,从队列中取出这个任务执行,执行完毕后,线程归还到线程池,等待下个任务。同时以事件的方式通知event-loop,event-loop接收到事件执行该事件注册的回调函数。

事件循环的模型如下: image.png 上图标明了单次循环中的操作阶段:

  1. timers:此阶段执行由 setTimeout 和 setInterval 设置的回调。
  2. pending callbacks:执行推迟到下一个循环迭代的 I/O 回调。
  3. idle, prepare, :仅在内部使用。
  4. poll:取出新完成的 I/O 事件;执行与 I/O 相关的回调(除了关闭回调,计时器调度的回调和setImmediate 之外,几乎所有这些回调)适当时,node 将在此处阻塞。
  5. check:在这里调用setImmediate 回调。
  6. close callbacks:一些关闭回调,例如 socket.on(‘close’, …)。

Node.js和浏览器的事件循环的差异

浏览器环境下,微任务的任务队列是每个宏任务执行完之后执行。而在Node.js中,微任务会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行微任务队列的任务

Node.js异步编程

概念

Node.js异步编程实际是依赖上文中所说的事件循环机制实现的异步效果。需要异步处理的任务都是事件循环中的事件(如上文事件循环部分所述)。

异步任务又分为宏任务和微任务。

宏任务

常见有:普通代码 / 外层同步代码、setTimeout、setInterval、i/o、ui render、异步ajax、文件操作

微任务

常见有:promise

Node.js 回调函数

概念

Node.js 异步编程的直接体现就是回调。

异步编程依托于回调来实现,但不能说使用了回调后程序就异步化了。

回调函数在完成任务后就会被调用,Node 使用了大量的回调函数,Node 所有 API 都支持回调函数。

例如,我们可以一边读取文件,一边执行其他命令,在文件读取完成后,我们将文件内容作为回调函数的参数返回。这样在执行代码时就没有阻塞或等待文件 I/O 操作。这就大大提高了 Node.js 的性能,可以处理大量的并发请求。

Node.js多进程

概念

Node.js 是以单线程的模式运行的,但它使用的是事件驱动来处理并发,这样有助于我们在多核 cpu 的系统上创建多个子进程,从而提高性能。

每个子进程总是带有三个流对象:child.stdin, child.stdout 和child.stderr。他们可能会共享父进程的 stdio 流,或者也可以是独立的被导流的流对象。

常用API

Node 提供了 child_process 模块来创建子进程,方法有:

  • exec - child_process.exec 使用子进程执行命令,缓存子进程的输出,并将子进程的输出以回调函数参数的形式返回。
  • spawn - child_process.spawn 使用指定的命令行参数创建新进程。
  • fork - child_process.fork 是 spawn()的特殊形式,用于在子进程中运行的模块,如 fork('./son.js') 相当于 spawn('node', ['./son.js']) 。与spawn方法不同的是,fork会在父进程与子进程之间,建立一个通信管道,用于进程之间的通信。

进程间通信

父进程向子进程发送消息

父进程中可以调用 child_process的fork()方法后会得到一个子进程的实列,通过该实列我们可以监听到来自子进程的消息或向子进程发送消息。

const childProcess = require('child_process');
const worker = childProcess.fork('./worker.js');

// 主进程向子进程发送消息
worker.send('Hello World');

// 监听子进程发送过来的消息
worker.on('message', (msg) => {
  console.log('Received message from worker:' + msg);
});

子进程向父进程发送消息

子进程则通过process对象接口来监听父进程的消息或向父进程发送消息

// 接收主进程发来的消息
process.on('message', (msg) => {
  console.log('Received message from master:' + msg);
  // 子进程向主进程发送消息
  process.send('Hi master.');
});

Node.js的多线程

概念

Node.js 通过事件循环机制(初始化和回调)的方式运行 JavaScript 代码,并且提供了一个线程池处理诸如文件 I/O 等高成本的任务。 Node 的伸缩性非常好,某些场景下它甚至比类似 Apache 等更重量级的解决方案表现更优异。 Node 可伸缩性的秘诀在于它仅使用了极少数的线程就可以处理大量客户端连接。 如果 Node.js 只需占用很少的线程,那么它就可以将更多的系统 CPU 时间和内存花费在客户端任务而不是线程的空间和时间消耗上(内存,上下文切换)。 但是同样由于 Node.js 只有少量线程,你必须非常小心的组织你的应用程序以便合理的使用它们。

多线程?单线程?

Node.js 是用很少量的线程来处理大量客户端请求的。 在 Node.js 中,有两种类型的线程:一个事件循环线程(也被称为主循环,主线程,事件线程等)。另外一个是在工作线程池里的 k 个工作线程(也被称为线程池)。

一般说Nodejs是单线程的,是指Nodejs运行JS代码时是单线程的,即上文中的事件循环线程(也被称为主循环,主线程,事件线程等),但处理其他高成本任务时使用的是线程池提供的工作线程。

事件轮询线程

当 Node.js 程序运行时,程序首先完成初始化部分,即处理 require 加载的模块和注册事件回调。 然后,Node.js 应用程序进入事件循环阶段,通过执行对应回调函数来对客户端请求做出回应。 此回调将同步执行,并且可能在完成之后继续注册新的异步请求。 这些异步请求的回调也会在事件轮询线程中被处理。

事件循环中同样也包含很多非阻塞异步请求的回调,如网络 I/O。

总体来说,事件轮询线程执行事件的回调函数,并且负责对处理类似网络 I/O 的非阻塞异步请求。

工作线程池

Node.js 的工作线程池是通过 libuv(相关文档)来实现的,它对外提供了一个通用的任务处理 API。

Node.js 使用工作线程池来处理“高成本”的任务。 这包括一些操作系统并没有提供非阻塞版本的 I/O 操作,以及一些 CPU 密集型的任务。

Node.js 模块中有如下这些 API 用到了工作线程池:

  1. I/O 密集型任务:

    1. DNSdns.lookup()dns.lookupService()
    2. 文件系统:所有的文件系统 API。除 fs.FSWatcher() 和那些显式同步调用的 API 之外,都使用 libuv 的线程池。
  2. CPU 密集型任务:

    1. Cryptocrypto.pbkdf2()crypto.scrypt()crypto.randomBytes()crypto.randomFill()crypto.generateKeyPair()
    2. Zlib:所有 Zlib 相关函数,除那些显式同步调用的 API 之外,都适用 libuv 的线程池。

在许多 Node.js 应用程序中,这些 API 是工作线程池任务的唯一来源。此外应用程序和模块可以使用 C++ 插件 向工作线程池提交其它任务。

需要注意,要避免在事件轮询线程的回调函数中调用I/O密集型任务和CPU密集型任务的API

Node.js流(stream)

概念

流(Stream)是为 Node.js 应用提供动力的基本概念之一。它们是数据处理方法,用于将输入的数据顺序读取或把数据写入输出。

流是一种以有效方式处理读写文件、网络通信或任何类型的端到端信息交换的方式。

流的处理方式非常独特,流不是像传统方式那样将文件一次全部读取到存储器中,而是逐段读取数据块并处理数据的内容,不将其全部保留在内存中。

这种方式使流在处理大量数据时非常强大,例如,文件的大小可能大于可用的内存空间,从而无法将整个文件读入内存进行处理。那是流的用武之地!

既能用流来处理较小的数据块,也可以读取较大的文件。

以 YouTube 或 Netflix 之类的“流媒体”服务为例:这些服务不会让你你立即下载视频和音频文件。取而代之的是,你的浏览器以连续的块流形式接收视频,从而使接收者几乎可以立即开始观看和收听。

但是,流不仅涉及处理媒体和大数据。它们还在代码中赋予了我们“可组合性”的力量。考虑可组合性的设计意味着能够以某种方式组合多个组件以产生相同类型的结果。在 Node.js 中,可以通过流在其他较小的代码段中传递数据,从而组成功能强大的代码段。

流的优势

两个主要优点:

  1. 内存效率: 你无需事先把大量数据加载到内存中即可进行处理
  2. 时间效率: 得到数据后立即开始处所需的时间大大减少,不必等到整个有效数据全部发送完毕才开始处理

流的分类

有 4 种流:

  1. 可写流: 可以向其中写入数据的流。例如,fs.createWriteStream() 使我们可以使用流将数据写入文件。
  2. 可读流: 可从中读取数据的流。例如:fs.createReadStream() 让我们读取文件的内容。
  3. 双工流(可读写的流): 可读和可写的流。例如,net.Socket
  4. Transform: 可在写入和读取时修改或转换数据。例如在文件压缩的情况下,你可以在文件中写入压缩数据,也可以从文件中读取解压缩的数据。

简单使用

创建可读流为例子:

// 导入流模块
const Stream = require('stream')
// 创建流并初始化
const readableStream = new Stream.Readable()
// 向流发送数据
readableStream.push('ping!')
readableStream.push('pong!')

Node.js文件系统(fs)

概念

fs全称为(file system)文件系统,提供一组类似 UNIX(POSIX)标准的文件操作 API。

常见API

操作异步方法同步方法
打开文件fs.open(path, flags[, mode], callback)fs.openSync(path, flags[, mode])
文件信息fs.stat(path[, options], callback)fs.statSync(path[, options])
新建文件fs.appendFile(path, data[, options], callback)fs.appendFileSync(path, data[, options])
写入文件fs.writeFile(file, data[, options], callback)fs.writeFileSync(file, data[, options])
读取文件fs.read()
读取文件fs.readFile(path[, options], callback)fs.readFileSync(path[, options])
重命名文件fs.rename(oldPath, newPath, callback)fs.renameSync(oldPath, newPath)
关闭文件fs.close(fd, callback)fs.closeSync(fd)
截取文件fs.ftruncate(fd[, len], callback)fs.ftruncateSync(fd[, len])
删除文件fs.unlink(path, callback)fs.unlinkSync(path)
文件存在fs.stat() / fs.access()fs.existsSync(path)
监听文件fs.watchFile(filename[, options], listener)
停止监听fs.unwatchFile(filename[, listener])
打开大文件fs.createReadStream(path[, options])
写入大文件fs.createWriteStream(path[, options])
创建目录fs.mkdir(path[, options], callback)fs.mkdirSync(path[, options])
读取目录fs.readdir(path[, options], callback)fs.readdirSync(path[, options])
删除目录fs.rmdir(path, callback)fs.rmdirSync(path)

Node.js的模块化

概念

Node应用是由模块组成的,遵循了CommonJS模块规范,来隔离每个模块的作用域,使每个模块在它自身的命名空间中执行。

CommonJS 是一套代码规范,目的是为了构建 JavaScript 在浏览器之外的生态系统(服务器端,桌面端)

Node.js模块化的特点

  1. 所有代码运行在当前模块作用域中,不会污染全局作用域
  2. 模块同步加载,根据代码中出现的顺序依次加载
  3. 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存

module对象

Node内部提供一个Module构建函数。所有模块都是Module的实例。每个模块内部,都有一个module对象,代表当前模块。

每个导出的模块都有一个moudle对象,该对象包含的属性有:

  1. module.exports属性:表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。
  2. exports变量:指向moudle.exports,为了操作的方便,它不能直接指向一个值,因为这样等于切断了exports与module.exports的联系。

模块加载

require命令

Node使用CommonJS模块规范,内置的require命令用于加载模块文件。

require命令的基本功能是,读入并执行一个JavaScript文件然后返回该模块的exports对象。如果没有发现指定模块,会报错

加载规则

require命令用于加载文件,后缀名默认为.js。
根据参数的不同格式,require命令去不同路径寻找模块文件:

  1. 以“/”开头,表示加路径是绝对路径;
  2. 以“./”开头,表示加载路径是相对路径;
  3. 不以“./“或”/“开头,表示加载的模式是node的核心模块,在node安装路径的node_modules中;
  4. 不以“./“或”/“开头,而且是一个路径,则将先找到example-module的位置,然后再以它为参数,找到后续路径。
  5. 如果指定的模块文件没有发现,Node会尝试为文件名添加.js、.json、.node后,再去搜索。.js件会以文本格式的JavaScript脚本文件解析,.json文件会以JSON格式的文本文件解析,.node文件会以编译后的二进制文件解析。
  6. 如果想得到require命令加载的确切文件名,使用require.resolve()方法。

基于目录的加载

通常,我们会把相关的文件会放在一个目录里面,便于组织。这时,最好为该目录设置一个入口文件,让require方法可以通过这个入口文件,加载整个目录。在目录中放置一个package.json文件,并且将入口文件写入main字段。

require发现参数字符串指向一个目录以后,会自动查看该目录的package.json文件,然后加载main字段指定的入口文件。如果package.json文件没有main字段,或者根本就没有package.json文件,则会加载该目录下的index.js文件或index.node文件。

模块的缓存

第一次加载某个模块时,Node会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的module.exports属性。 如果想要多次执行某个模块,可以让该模块输出一个函数,然后每次require这个模块的时候,重新执行一下输出的函数。所有缓存的模块保存在require.cache之中,如果想删除模块的缓存,可以使用遍历require.cache,使用delete require.cache[key];来删除模块缓存

模块分类

Node.js 的模块分为两类,

  1. 原生(核心)模块
    原生模块在 Node.js 源代码编译的时候编译进了二进制执行文件,加载的速度最快
  2. 文件模块
    是动态加载的,加载速度比原生模块慢。

Node.js 对原生模块和文件模块都进行了缓存,于是在第二次 require 时,是不会有重复开销的。其中原生模块都被定义在 lib 这个目录下面,文件模块则不定性

Node.js的包管理(npm)

概念

NPM 的全称是 Node Package Manager,是随同 NodeJS 一起安装的包管理和分发工具,它很方便让 JavaScript 开发者下载、安装、上传以及管理已经安装的包。

使用场景

  • 允许用户从NPM服务器下载别人编写的第三方包到本地使用。
  • 允许用户从NPM服务器下载并安装别人编写的命令行程序到本地使用。
  • 允许用户将自己编写的包或命令行程序上传到NPM服务器供别人使用

使用

安装和升级

npm 不需要单独安装。在安装 Node 的时候,会连带一起安装 npm 。 单独升级npm版本的命令:npm install npm@latest -g

常见指令

// 1.装包
npm install xxx
// 2.装包并记录在package.json文件中
npm install xxx --save
// 3.安装指定版本(安装3.0.0版本包)
npm install xxx@3.0.0
// 4.更新指定包
npm update xxx
// 5.更新指定包
npm update xxx
// 6.卸载指定包
npm unistall xxx
// 7.查看镜像源
npm config get registry
// 8.设置镜像源(比如设置淘宝镜像)
npm set registry https://registry.npm.taobao.org/
// 9.查看npm的全局安装路径(默认全局地址:C:\Users\Administrator\AppData\Roaming\npm)
npm config get prefix
// 10.查看npm的缓存路径(默认缓存地址:C:\Users\Administrator\AppData\Roaming\npm-cache)
npm config get cache
// 11.npm配置信息
npm config ls
// 12.修改npm的全局安装路径(注意:修改后需要将路径添加到环境变量)
npm config set prefix "complete_path"
// 13.修改npm的缓存路径
npm config set cache "complete_path"
// 14.清除项目中没有被使用的包
npm prune

package.json和package-lock.json

package.json

package.json 是在运行 “ npm init ”时生成的,主要记录项目依赖,主要有以下结构:

  • name:项目名,也就是在使用npm init 初始化时取的名字,但是如果使用的是npm init -y 快速初始化的话,那这里的名字就是默认存放这个文件的文件名;
  • version:版本号;
  • private:希不希望授权别人以任何形式使用私有包或未发布的;
  • scripts-serve:是vue的项目启动简写配置;
  • scripts-build:是vue的打包操作简写配置;
  • dependencies:指定了项目运行时所依赖的模块;
  • devDependencies:指定项目开发时所需要的模块,也就是在项目开发时才用得上,一旦项目打包上线了,就将移除这里的第三方模块;

package-lock.json

package-lock.json 是在运行 “npm install” 时生成的一个文件,用于记录当前状态下项目中实际安装的各个 package 的版本号、模块下载地址、及这个模块又依赖了哪些依赖

package.json和package-lock.json的差异 / 为什么需要package-lock.json

当项目中已有 package-lock.json 文件,在安装项目依赖时,将以该文件为主进行解析安装指定版本依赖包,而不是使用 package.json 来解析和安装模块,因为以下两点原因:

  1. 同一份package.json安装的依赖版本可能不同,如果依赖包有小版本更新并且引入了bug会导致重新装包的项目报错。
  2. package.json中声明的只是直接依赖,依赖的依赖无法通过package.json控制。例如:项目依赖包A,包A依赖包B,包A的版本可以通过package.json中固定版本号的形式固定下来(A: 1.0.0),但是A的依赖B的版本号可能是^2.0.0,这样包B的版本还是无法固定。

也就是说 package.json 只是指定的版本不够具体。

package-lock.json 为每个模块及其每个依赖项指定了版本,位置和完整性哈希,所以它每次创建的安装都是相同的。无论你使用什么设备,或者将来安装它都无关紧要,每次都应该给你相同的结果

koa框架

是什么?

Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件,通过洋葱模型来使开发更加便捷,。

特性(基于Koa2)

1.丢弃了回调函数,并有效地增强了异常处理

Koa使用Promise配合Async函数实现异步,解决了Node回调地狱的问题。

Koa是通过全局错误事件监听实现对错误的监听,这样把错误处理写在最外层即可。

2.不内置任何中间件,而且中间件模型采用了洋葱模型

不内置任何中间件,好处是使得框架轻量化,让开发者按需安装中间件,更灵活自由。

koa使用洋葱模型作为其中间件模型,可以将中间件级联执行,由用户决定是否向下级中间件执行;而且多个中间件之间通信等变得更加可行和简单。

3.koa提供了执行上下文(Context)

  • Koa的Context 将 node 的 request 和 response 对象封装在一起.
  • 每接收一个HTTP请求都将创建一个 Context,并在中间件中作为接收器引用,或者 ctx 标识符。

总结来讲,执行上下文可以理解为HTTP请求周期内的作用域环境,来托管请求,响应和中间件,方便它们之间相互访问.

核心机制

koa的洋葱模型

什么是洋葱模型?

洋葱模型是由多个同心层构成,它们相互连接,并朝向代表领域的核心。它是基于控制反转(Inversion of Control,IoC)的原则。该架构并不关注底层技术或框架,而是关注实际的领域模型。洋葱模型各层是通过接口连接的。在领域实体和业务规则构成架构的核心部分时,尽可能将外部依赖性保持在外。

洋葱模型的特点

  • 它提供了灵活、可持续和可移植的架构。
  • 各层之间没有紧密的耦合,并且有关注点的分离。
  • 由于所有的代码都依赖于更深的层或者中心,所以提供了更好的可维护性。
  • 提高了整体代码的可测试性,因为单元测试可以为单独的层创建,而不会影响到其他的模块。
  • 框架/技术可以很容易地改变而不影响核心领域

Koa中间件的洋葱模型

如何更好地理解中间件和洋葱模型

Express框架

概念

Express是目前流行的基于Node.js运行环境的Web应用程序开发框架,它简洁且灵活,为Web应用程序提供了强大的功能。

Express提供了一个轻量级模块,类似于jQuery(封装的工具库),它把Node.js的HTTP模块的功能封装在一个简单易用的接口中,用于扩展HTTP模块的功能,能够轻松地处理服务器的路由、响应、Cookie和HTTP请求的状态。

特性

  1. 简洁的路由定义方式。
  2. 简化HTTP请求参数的处理。
  3. 提供中间件机制控制HTTP请求。
  4. 拥有大量第三方中间件。
  5. 支持多种模版引擎。

核心机制

Express中间件

Express通过中间件接收客户端发来的请求,并对请求做出响应,也可以将请求交给下一个中间件继续处理。

Express中间件指业务流程中的中间处理环节,可以把中间件理解为客户端请求的一系列方法。如果把请求比作水流,那么中间件就是阀门,阀门可以控制水流是否继续向下流动,也可以在当前阀门处对水流进行排污处理,处理完成后再继续向下流动。

使用场景

  1. 路由保护:当客户端访问登录页面时,可以先使用中间件判断用户的登录状态,如果用户未登录,则拦截请求,直2. 接响应提示信息,并禁止用户跳转到登录页面。
  2. 网站维护公告:在所有路由的最上面定义接收所有请求的中间件,直接为客户端做出响应,并提示网站正在维护中。
  3. 自定义404页面:在所有路由的最上面定义接收所有请求的中间件,直接为客户端做出响应,并提示404页面错误信息。

等等

中间件组成

中间件主要由中间件方法请求处理函数这两个部分构成。中间件方法由Express 提供,负责拦截请求。请求处理函数由开发人员编写,负责处理请求。

常用的中间件方法有app.get()、app.post()、app.use()

模块化路由

虽然可以使用app.get()方法和app.post()方法来实现简单的路由功能,但没有对路由进行模块化管理。在实际的项目开发中,不推荐将不同功能的路由都混在一起存放在一个文件中,因为随着路由的种类越来越多,管理起来会非常麻烦。为了方便路由的管理,通过express.Router()实现模块化路由管理。

express.Router()方法用于创建路由对象route,然后使用route.get()和route.post()来注册当前模块路由对象下的二级路由,这就是一个简单的模块化路由。

koa和express对比

  • koa不在内核中绑定任何中间件,express则内置了一些功能如express.Router()

  • koa原生支持async/await 相比于promise处理异步回调更加优雅。

  • 两者对异步的中间件调用顺序是不同的

    • express不会等待中间件的异步处理执行完毕,主要是由于内部是通过回调函数的组合,其next的机制导致不会等待异步的完成而继续执行同步操作,当然对于一个中间件的内部使用了async await,执行顺序还是正确的;
    • koa的next机制是利用闭包和递归的性质,一个个执行中间件,并且每次执行都是返回promise的封装,再结合generator状态机,实现同步异步的按顺序执行.