express系列(3)Express 基础
Express 框架的初衷是为了拓展 Node 内置模块的功能提高开发效率。
当你深入研究后就会发现,Express 其实是在 Node 内置的 HTTP 模块上构建了一层抽象。理论上所有 Express 实现的功能,同样可以使用纯 Node 实现。
在本文中,我们将基于前面的 Node 内容去探究 Express 和 Node 之间的关系,其中包括:中间件和路由等概念。当然,这里只会进行一些综述具体的细节会在后面带来。
总的来说,Express 提供了 4 个主要特性:
- Express 使用“中间件栈”处理流。
- 路由与中间件类似,只有当你通过特定 HTTP 方法访问特定 URL 时才会触发处理函数的调用。
- 对 request 和 response 对象方法进行了拓展。
- 视图模块允许你动态渲染和改变 HTML 内容,并且使用其他语言编写 HTML 。
中间件
中间件是 Express 中最大的特性之一。中间件将处理过程进行划分,并且使用多个函数构成一个完整的处理流程。
我们将会看到中间件在代码中的各种应用。
例如,首先使用一个中间件记录所有的请求,接着在其他的中间件中设置 HTTP 头部信息,然后继续处理流程。
虽然在一个“大函数”中也可以完成请求处理,但是将任务进行拆分为多个功能明确独立的中间件明显更符合软件开发中的 SRP 规则。
中间件并不是 Express 特有,Python 的 Django 或者 PHP 的 Laravel 也有同样的概念存在。同样的 Ruby 的 Web 框架中也有被称为 Rack 中间件概念。
现在我们就用 Express 中间件来重新实现 Hello World 应用。你将会发现只需几行代码就能完成开发,在提高效率的同时还消除了一些隐藏 bug。
Express 版 Hello World
首先新建一个Express工程:新建一个文件夹并在其中新建 package.json 文件。
回想一下 package.json 的工作原理,其中完整的列出了该工程的依赖、项目名称、作者等信息。我们新工程中的 package.json 大致如下:
{ "name": "hello-world", "author": "Your Name Here!", "private": true, "dependencies": {} }
接下来执行命令,安装最新的 Express 并且将其保存到 package.json 中:
npm install express -save
命令执行完成后,Express 会自动安装到 node_modules 的文件下,并且会在 package.json 明确列出改依赖。此时 package.json 中的内容如下:
{ "name": "hello-world", "author": "Your Name Here!", "private": true, "dependencies": { "express": "^5.0.0" } }
接下来将下列代码复制到 app.js 中:
var express = require("express"); var http = require("http"); var app = express(); app.use(function(request, response) { response.writeHead(200, { "Content-Type": "text/plain" }); response.end("Hello, World!"); }); http.createServer(app).listen(3000);
首先,我们依次引入了 Express 和 HTTP 模块。
然后,使用 express() 方法创建变量 app ,该方法会返回一个请求处理函数闭包。这一点非常重要,因为它意味着我可以像之前一样将其传递给 http.createServer 方法。
还记得前一章提到的原生 Node 请求处理吗?它大致如下:
var app = http.createServer(function(request, response) { response.writeHead(200, { "Content-Type": "text/plain" }); response.end("Hello, world!"); });
两段代码非常相似,回调闭包都包含两个参数并且响应也一样。
最后,我们创建了一个服务并且启动了它。http.createServer 接受的参数是一个函数,所以合理猜测 app 也只是一个函数,只不过该函数表示的是 Express 中一个完整的中间件处理流程。
中间件如何在高层工作
在原生的 Node 代码中,所有的 HTTP 请求处理都在一个函数中:
function requestHandler(request, response) { console.log("In comes a request to: " + request.url); response.end("Hello, world!"); }
抽象成流程图的话,它看起来就像:
这并不是说在处理过程中不能调用其它函数,而是所有的请求响应都由该函数发送。
而中间件则使用一组中间件栈函数来处理这些请求,处理过程如下图:
那么,接下来我们就有必要了解 Express 使用一组中间件函数的缘由,以及这些函数作用
回顾一下前面用户验证的例子:只有验证通过才会展示用户的私密信息,与此同时每次访问请求都要进行记录。
在这个应用中存在三个中间件函数:请求记录、用户验证、信息展示。
中间件工作流为:先记录每个请求,然后进行用户验证,验证通过进行信息展示,最后对请求做出响应。所以,整个工作流有两种可能情形(成功以及不成功):
另外,这些中间件函数中部分函数需要对响应做出响应。
这样做的好处就是,我们可以将应用进行拆分。而拆分后的组件不仅利于后期维护,并且组件之间还可以进行不同组合。
不做任何修改的中间件
中间件函数可以对 request、response 进行修改,但它并不是必要操作。
例如,前面的日志记录中间件代码:它只需要进行记录操作。而一个不做任何修改,纯功能性的中间函数代码大致如下:
function myFunMiddleware(request, response, next) { ... nest(); }
因为中间件函数的执行是从上到下的。所以,加入纯功能性的请求记录中间件后,代码如下:
var express = require("express"); var http = require("http"); var app = express(); // 日志记录中间件 app.use(function(request, response, next) { console.log("In comes a " + request.method + " to " + request.url); next(); }); // 发送实际响应 app.use(function(request, response) { response.writeHead(200, { "Content-Type": "text/plain" }); response.end("Hello, world!"); }); http.createServer(app).listen(3000);
修改 request、response 的中间件
在部分中间件函数需要对 request、response 进行处理,尤其是后者。
下面我们来实现前面提到的验证中间件函数。这里只允许当前分钟数为偶数的情况通过验证。那么,该中间件函数代码大致如下:
app.use(function(request, response, next) { console.log("In comes a " + request.method + " to " + request.url); next(); }); app.use(function(request, response, next) { var minute = (new Date()).getMinutes(); // 如果在这个小时的第一分钟访问,那么调用next()继续 if ((minute % 2) === 0) { next(); } else { // 如果没有通过验证,发送一个403的状态码并进行响应 response.statusCode = 403; response.end("Not authorized."); } }); app.use(function(request, response) { response.end('Secret info: the password is "swordfish"!'); // 发送密码信息 });
第三方中间件类库
在大多数情况下,你正在尝试的工作可能已经被人实现过了。也就是说,对于一些常用的功能社区中可能已经存在成熟的解决方案了。下面,我们就来介绍一些 Express 中常用的第三方模块。
MORGAN:日志记录中间件
Morgan 是一个功能非常强大的日志中间件。
它能对用户的行为和请求时间进行记录。而这对于分析异常行为和可能的站点崩溃来说非常有用。大多数时候 Morgan 也是 Express 中日志中间件的首选。
使用命令 npm install morgan --save 安装该中间件,并修改 app.js 中的代码:
var express = require("express"); var logger = require("morgan"); var http = require("http"); var app = express(); app.use(logger("short")); app.use(function(request, response){ response.writeHead(200, {"Content-Type": "text/plain"}); response.end("Hello, world!"); }); http.createServer(app).listen(3000);
再次访问 http://localhost:3000 你就会看到 Morgan 记录的日志了。
Express 的静态文件中间件
通过网络发送静态文件对 Web 应用来说是一个常见的需求场景。
这些资源通常包括图片资源、CSS 文件以及静态 HTML 文件。
但是一个简单的文件发送行为其实代码量很大,因为需要检查大量的边界情况以及性能问题的考量。而 Express 内置的 express.static 模块能最大程度简化工作。
假设现在需要对 public 文件夹提供文件服务,只需通过静态文件中间件我们就能极大压缩代码量:
var express = require("express"); var path = require("path"); var http = require("http"); var app = express(); var publicPath = path.resolve(__dirname, "public"); app.use(express.static(publicPath)); app.use(function(request, response) { response.writeHead(200, { "Content-Type": "text/plain" }); response.end("Looks like you didn't find a static file."); }); http.createServer(app).listen(3000);
在 public 目录下的静态文件能直接请求了,可以将所有需要的文件的放在该目录下。
如果 public 文件夹中没有任何匹配的文件存在,它将继续执行下一个中间件,响应信息为没有匹配的文件。
为什么使用 path.resolve ? 之所以不直接使用 /public 是因为 Mac 和 Linux 中目录为 /public 而 Windows 使用万恶的反斜杠 \public 。path.resolve 就是用来解决多平台目录路径问题。
更多中间件
除此上面介绍的 Morgan 中间件和 Express 静态中间之外,还有很多其他功能强大的中间件,例如:
- connect-ratelimit:可以让你控制每小时的连接数。如果某人向服务发起大量请求,那么可以直接返回错误停止处理这些请求。
- helmet:可以添加 HTTP 头部信息来应对一些网络攻击。具体内容会在后面关于安全的章节讲到。
- cookie-parses:用于解析浏览器中的 cookie 信息。
- response-time:通过发送 X-Response-Time 信息,让你能够更好的调试应用的性能。
路由
路由是一种将 URL 和 HTTP 方法映射到特定处理回调函数的技术。假设工程里有一个主页,一个关于页面以及一个 404 页面,接下来看看路由是如何进行映射的:
var express = require("express"); var path = require("path"); var http = require("http"); var app = express(); // 像之前一样设置静态文件中间件。 // 所有的请求通过这个中间件,如果没有文件被找到的话会继续前进 var publicPath = path.resolve(__dirname, "public"); app.use(express.static(publicPath)); // 当请求根目录的时候被调用 app.get("/", function(request, response) { response.end("Welcome to my homepage!"); }); // 当请求/about的时候被调用 app.get("/about", function(request, response) { response.end("Welcome to the about page!"); }); // 当请求/weather的时候被调用 app.get("/weather", function(request, response) { response.end("The current weather is NICE."); }); // 前面都不匹配,则路由错误。返回 404 页面 app.use(function(request, response) { response.statusCode = 404; response.end("404"); }); http.createServer(app).listen(3000);
上面代码中除了添加前面提到的中间件之外,后面三个 app.get 函数就是 Express 中强大的路由系统了。
它们使用 app.post 来响应一个 POST 或者 PUT 等所有网络请求。函数中第一个参数是一个路径,第二个参数是一个请求处理函数。
该处理函数与之前的中间件工作方式一样,唯一的区别就是调用时机。
除了固定路由形式外,它还可以匹配更复杂的路由(使用正则等方式):
// 指定“hello”为路由的固定部分 app.get("/hello/:who", function(request, response) { // :who 并不是固定住,它表示 URL 中传递过来的名字 response.end("Hello, " + request.params.who + "."); });
重启服务并访问 localhost:3000/hello/earth 等到的响应信息为:
Hello, earth
注意到如果你在 URL 后面插入多个 / 的话,例如:localhost:3000/hello/entire/earth 将会返回一个 404 错误。
你应该在日常生活中见过这种 URL 链接,特定的用户能够访问特定的 URL 。
扩展 request 和 response
Express 在原来基础上对 request 和 response 对象进行了功能扩展。
我们可以先来领略其中的一部分:
Express 提供的功能中 redirect 算一个非常棒的功能,使用方法如下:
response.redirect("/hello/world");
response.redirect("http://expressjs.com");
虽然我们也能够使用原生代码实现重定向功能,但明显它的代码量会更多。
另外,在 Express 中文件发送也变的更加简单,只需一行代码就能实现:
response.sendFile("path/to/cool_song.mp3")
该功能的原生实现代码也比较复杂。
除了对响应对象 response 进行了拓展之外,Express 也对请求对象 request 进行了拓展。
例如:你可以通过 request.ip 获取发送请求的机器 IP 地址或者通过 request.get 获取 HTTP 头部。
下面我们使用它实现 IP 黑名单功能,代码如下:
var express = require("express"); var app = express(); var EVIL_IP = "123.45.67.89"; app.use(function(request, response, next) { if (request.ip === EVIL_IP) { response.status(401).send("Not allowed!"); } else { next(); } }); ...
这里使用到了 req.ip 以及 res.status() 和 res.send() ,而这些方法全都来自于 Express 的拓展。
理论上来说,我们只需要知道 Express 拓展了 request 和 response 并知道如何使用就行了,至于细节可以不去做了解。
上面的例子,只是 Express 所有拓展中的冰山一角,你可以在文档中看到更多的示例。