原生 Node 的单一请求处理函数,随着功能的扩张势必会变的愈来愈难以维护。而 Express 框架则能够经过中间件的方式按照模块和功能对处理函数进行切割处理。这样拆分后的模块不只逻辑清晰,更重要的是对后期维护和开发很是有利。html
本文将会详细介绍 Express 的使用,其中主要内容包括:node
但愿在读完本文后,你能对这个 Express 最主要的构成有更加清晰的认知。express
<!--more-->npm
对全部的 Web 应用来讲它的处理流程能够简单描述为:监听请求、解析请求、作出响应。固然,Node 也遵循这一套流程,只不过将那些请求都转化为了 JavaScript 对象。json
与原生 Node 代码不一样的是,Express 会将上图中的最后一部分拆分为一组中间件函数(中间件栈)。因此Express 的工做流大体以下:浏览器
与纯 Node 不一样的是,Express 中的中间件栈函数中除了表示请求和响应的参数外,还添加了第三个参数。该参数是一个函数对象,按照惯例咱们称之为 next 。它用于传递中间件栈对某个请求的处理流。缓存
在整个中间件栈的处理流中,最少有一个函数须要调用 res.end 方法结束响应处理。下面咱们就经过搭建静态文件服务来加深对中间件栈的理解。安全
建立一个文件夹并为此提供静态文件服务。你能够在文件夹中存听任何文件,例如:HTML 文件、图片。最终全部的这些文件都能经过示例程序进行网络访问。服务器
该示例程序的功能大体包括:可以正确返回存在的文件;文件不存在时返回 404 错误;打印全部的访问请求。因此,该示例的中间件栈以下:网络
流程图以下:
明确示例的目标和需求后,下面咱们就进行代码实现。
与以前同样,新建工程目录并将下面内容复制到 package.json 中:
{ "name": "static-file-fun", "private": true, "scripts": { "start": "node app.js" } }
接下来,咱们执行 npm install express --save 安装最新版 Express 。确保安装完成后,咱们在工程目录里新建文件夹 static 并在其中存放一些文件。最后,咱们新建工程主入口文件 app.js 。一切就绪后,工程的大体目录以下:
另外,值的一提的是之因此配置 npm start 命令,既是由于开发约定更重要的是让其余人开箱即用无需本身手动查找程序入口。
按照前面制订的处理流程,首先须要实现的就是日志中间件。复制下面代码到入口文件 app.js 中:
var express = require("express"); var path = require("path"); var fs = require("fs"); var app = express(); app.use(function(req, res, next) { console.log("Request IP: " + req.url); console.log("Request date: " + new Date()); }); app.listen(3000, function() { console.log("App started on port 3000"); });
经过上面的 app.use 函数,咱们成功实现了应用中的第一个功能,即记录每次网络请求。固然这里还有一个问题,当前应用并不会对请求作出响应。这意味这:若是你用 npm start 拉起服务并访问 loaclhost:3000 浏览器会一直挂起等待响应直到出现超时错误。不过不要担忧,等补全后面功能后咱们就能够在该中间件调用 next() 将响应的任务交给后续中间件。
这里咱们只须要明白:理论上一个中间件函数处理结束后,它必须执行如下两个步骤中的一个。
因此这里咱们先把 next() 调用补全将日志中间件的逻辑理顺:
// ... app.use(function(req, res, next) { console.log("Request IP: " + req.url); console.log("Request date: " + new Date()); next(); // 新的这行很重要 }); // ...
此时重启服务并访问 http://localhost:3000 的话访问请求会被完整记录下来。可是由于程序没有作出响应 ,Express 任会给客户端发送一个错误信息。因此,接下来咱们就补全后续流程。
静态文件服务中间件应该有如下几个功能:
其中咱们须要使用内置的 path 模块指定路径,而后使用内置的 fs 模块判断文件释放存在。将下面代码添加到日志中间件后面:
// 日志中间件 app.use(function(req, res, next) { // … }); app.use(function(req, res, next) { var filePath = path.join(__dirname, "static", req.url); fs.exists(filePath, function(exists) { if (exists) { res.sendFile(filePath); } else { next(); } }); }); app.listen(3000, function() { ... }
在中间件中咱们首先使用 path.join 拼接文件完整路径。例如,若是用户访问 http://localhost:3000/celine.... 文件的话 req.url 的值就是 /celine.mp3 拼接后的完整路径就是 "/path/to/your/project/static/celine.mp3" 了。
而后,该中间件调用 fs.exists 函数检查文件是否存在。若是文件存在则发生文件,不然调用 next() 继续执行下一个中间件。而若是访问的 URL 没有对应的文件的话就会出现以前同样的错误。因此下面须要实现最后一个中间件:404 处理中间件。
404 中间件的任务就是发送 404 错误信息,复制下面的实现代码并添加到静态服务中间件后面:
app.use(function(req, res) { // 设置状态码为404 res.status(404); // 发送错误提示 res.send("File not found!"); }); // ...
这样整个工程就算完成了。若是你再次启动服务的话,以前的错误就会被一个 404 错误取代。另外,若是你将该中间件函数移动到中间件栈的第一个,那么你会发现全部的请求都会获得 404 的错误信息。这意味着中间件栈中的函数顺序是很是重要的。
到这里,app.js 中的完整代码以下:
var express = require("express"); var path = require("path"); var fs = require("fs"); var app = express(); app.use(function(req, res, next) { console.log("Request IP: " + req.url); console.log("Request date: " + new Date()); next(); }); app.use(function(req, res, next) { var filePath = path.join(__dirname, "static", req.url); fs.stat(filePath, function(err, fileInfo) { if (err) { next(); return; } if (fileInfo.isFile()) { res.sendFile(filePath); } else { next(); } }); }); app.use(function(req, res) { res.status(404); res.send("File not found!"); }); app.listen(3000, function() { console.log("App started on port 3000"); });
固然,这只是初步的代码,还有不少地方能够进行优化。
在软件开发中若是你的问题已经存在比较好的解决方案,那么理想的作法是直接使用该方案而不该该“重复造轮子”。因此,下面咱们使用功能强大的 Morgan 替换掉上面本身实现的日志中间件。虽然,该中间件不是 Express 内置模块,可是它倒是由 Express 团队维护并久经考验。
运行 npm install morgan --save 安装最新版本的 Morgan 模块。而后使用 Morgan 替换掉以前的日志中间件:
var express = require("express"); var morgan = require("morgan"); ... var app = express(); app.use(morgan("short")); ...
当你再次启动服务并访问资源时,终端将会打印包括 IP 地址在内的有用信息:
代码中 morgan 其是一个函数而且它的返回值是一个中间件函数。当你调用它的时候,它会返回一个相似之间实现的日志中间件。为了代码更加清晰,你也能够将代码改写为:
var morganMiddleware = morgan("short"); app.use(morganMiddleware);
另外,这里在调用函数是使用的是 short 做为输出选项。其实该模块还提供另两个输出选项:combined 打印最多信息;tiny 打印最少的信息。
除了使用 Morgan 替换原有日志中间件以外,咱们还可使用内置的静态中间件替换以前的代码实现。
接下来,咱们使用 Express 内置的 express.static 模块来替换以前的静态文件中间件。它的工做原理与以前的中间件代码相似,可是它具备更好的安全性和性能。例如,它在内部实现了资源的缓存功能。
与 Morgan 同样,express.static 函数的返回值也是一个中间件函数。咱们只需为 express.static 函数指定路径参数便可。代码以下:
var staticPath = path.join(__dirname, "static"); // 设置静态文件的路径 app.use(express.static(staticPath)); // 使用express.static从静态路径提供服务 // ...
完成替换后你会发现代码相较以前明显变的简练了,与此同时功能反而比以前更强。另外,这些久经考验的中间件模块远比本身的代码实现功能更多也更可靠。此时 app.js 中的完整代码:
var express = require("express"); var morgan = require("morgan"); var path = require("path"); var app = express(); app.use(morgan("short")); var staticPath = path.join(__dirname, "static"); app.use(express.static(staticPath)); app.use(function(req, res) { res.status(404); res.send("File not found!"); }); app.listen(3000, function() { console.log("App started on port 3000"); });
以前我说过调用 next() 会按序执行下一个中间件。其实,真实状况并非这么简单。事实上,Express 中间件有两种类型。
到目前为止,你已经接触了第一种类型:包含三个参数的常规中间件函数(有时 next 会被忽略而只保留两个参数),而绝大多数时候程序中都是使用这种常规模式。
第二种类型很是少见:错误处理中间件。当你的 app 处于错误模式时,全部的常规中间件都会被跳过而直接执行 Express 错误处理中间件。想要进入错误模式,只需在调用 next 时附带一个参数。这是调用错误对象的一种惯例,例如:next(new Error("Something bad happened!")) 。
错误处理中间件中须要四个参数,其中后面三个和常规形式的一致而第一个参数则是 next(new Error("Something bad happened!")) 中传递过来的 Error 对象。你能够像使用常规中间件同样来使用错误处理中间件,例如:调用 res.end 或者 next 。若是调用含参数的 next 中间件会继续下一个错误处理中间件不然将会退出错误模式并调用下一个常规中间件。
假设,如今有四个中间件依次排开,其中第三个为错误处理中间件而其余的都是常规中间件。若是没有出现错误的话,流程应该是:
如上所示,当没有错误发生时错误处理中间件就像不存在同样。可是,一旦出现错误全部的常规中间件都被跳过,那么处理流程就会是这样:
虽然 Express 没有作出强制规定,可是通常错误处理中间件都会放在中间件栈的最下面。这样全部以前的常规中间件发生错误时都会被该错误处理中间件所捕获。
Express 的错误处理中间件只会捕获由 next 触发的错误,对于 throw 关键字触发的异常则不在处理范围内。对于这些异常 Express 有本身的保护机制,当请求失败时 app 会返回一个 500 错误而且整个服务依旧在持续运行。然而,对于语法错误这类异常将会直接致使服务奔溃。
如今经过一个简单示例来看看 Express 中的错误处理中间件。假设该应用对于用户的任何请求都是经过 res.sendFile 发生图片给用户。代码以下:
var express = require("express"); var path = require("path"); var app = express(); var filePath = path.join(__dirname, "celine.jpg"); app.use(function(req, res) { res.sendFile(filePath); }); app.listen(3000, function() { console.log("App started on port 3000"); });
能够看到这是以前静态文件服务的简化版,对于任意请求都会发生 celine.jpg 图片。可是若是该文件不存在,或者是文件读取过程发生了错误该怎么办呢?这就须要一些机制来处理这种异常错误了,而这正是错误处理中间件存在的理由。
为了触发异常处理,咱们在 res.sendFile 将异常回调函数补充完整。这个回调函数将会在文件发送以后获得执行而且该回调函数中有一个参数标记文件发送成功与否。代码示例以下:
res.sendFile(filePath, function(err) { if (err) { console.error("File failed to send."); } else { console.log("File sent!"); } });
固然,除了打印错误信息以外,咱们还能够经过触发异常进入错误处理中间件函数,而该部分代码实现以下:
// ... app.use(function(req, res, next) { res.sendFile(filePath, function(err) { if (err) { next(new Error("Error sending file!")); } }); }); // ...
异常触发后接下来就是错误处理中间件的实现了。
一般状况下咱们都会首先将错误信息记录下来,而这些信息通常也不会展现给用户。毕竟将一长段的 JavaScript 栈调用信息展现给不懂技术的用户会给他们形成没必要要的困惑。尤为是这些信息一旦暴露给了黑客,他们有可能就能逆向分析出网站是如何工做的从而形成信息风险。
下面,咱们仅仅在处理处理中间件中打印错误信息而不作任何进一步的处理。它与以前的中间件相似只不过这里打印错误信息而不是请求信息。将下面代码复制到全部常规中间件的后面:
// ... app.use(function(err, req, res, next) { // 记录错误 console.error(err); // 继续到下一个错误处理中间件 next(err); }); // ...
如今,当程序出现异常以后这些错误信息都将会被记录在控制台以便后面的进一步分析。固然,这里还有一些事情须要处理,例如:对请求做出响应。将下面代码放在上一个中间件以后:
// ... app.use(function(err, req, res, next) { // 设置状态码为500 res.status(500); // 发送错误信息 res.send("Internal server error."); }); // ...
请记住,这些错误处理中间件无论所在位置如何它都只能经过带参 next 进行触发。对于这个简单应用来讲可能没有那么多异常和错误会触发错误处理中间件。可是随着应用的扩张,你就须要对错误行为进行仔细测试。若是发生了异常,那么你应该对妥善的处理好这些异常而不是让程序崩溃。
在本文中咱们仔细探讨了 Express 的核心模块:中间件。其中的内容包括:
原文地址