express系列(5):路由

什么是路由?

假设,现在你尝试通过 example.com/someone 访问某人的推特或者微博主页,你会发现该请求的 HTTP 内容大致如下:

GET /someone http/1.1

其中包含了 HTTP 请求使用的方法(GET),URI 信息(/someone) 以及 HTTP 协议版本 (1.1)。

Express 中的路由就是负责将其中的 HTTP 方法和 URI 这对组合映射到对应的中间件。

简单说就是, /about_me 的GET 请求会执行某个中间件而对于 /new_user 的 POST 请求则执行另一个中间件。

下面我们通过一个简单示例来看看到底路由时如何工作的。

路由的一个简单示例

下面我们就对 example.com/someone 请求进行一个简单的实现,代码如下:

var express = require("express");
var app = express();

app.get('/someone', function(request, response) {
    response.send(" Welcome to someone's homepage! ");
});

app.use(function(request, response) {
    response.status(404).send("Page not found!");
});

app.listen(3000);

上面代码中有价值的是第三行:当你通过 HTTP 的 GET 方法对 /someone 发起请求时,程序会执行该中间件中的代码,其他请求则会被忽略并跳转到下一个中间件。

路由的特性

从工作原理来说:路由就是通过对 HTTP 方法和的 URI 的组合进行映射来实现对不同请求的分别处理。

当然,除了上面那种最简单的使用方式之外,Express 的路由还有更多实用的使用技巧和方法。

含参的通配路由

在上面的使用方式中使用的是全等判断来进行路由匹配的。虽然对于 /someone 这类非常管用。

但是对于形如 /users/1/users/2 这类 RESTful 路由就明显不那么友好了。

因为如果将后者路由一一列出的话,不管是从工作量还是后期维护来说都是非常差开发体验。针对这种情况,我们可以使用 Express 中含参的通配路由来解决。

该方法的工作原理就是,在路由中使用参数进行通配表示。而该参数所表示的具体数值会在变量 params 中获取到,下面是简单的代码示例:

app.get("/users/:userid", function(req, res) {
    // 将userId转换为整型
    var userId = parseInt(req.params.userid, 10);
    // ...
});
这样 RESTful 风格的动态路由就完全可以通过这种含参的通配路由进行处理。那么无论是 /users/123 还是 /users/8 都会被映射到同一中间件。
虽然 /users/ 或者 /users/123/posts 不会被匹配,但是 /users/cake/users/horse_ebooks 确会被匹配到。所以,如果实现更精准的路由匹配的话就需要使用其他方式了。

使用正则表达式匹配路由

针对上面的问题,我们可以使用正则来对路由进行更精准的匹配。

假设现在我们只需要匹配 /users/123/users/456 这种通配参数为数字的动态路由的同时忽略其他路由格式,那么可以将代码改为:

app.get(/^\/users\/(\d+)$/, function(req, res) {
    var userId = parseInt(req.params[0], 10);
    // ...
});

通过正则表达式代码对通配参数作为了严格限定:该参数必须是数字类型。

正则表达式可能阅读起来并不是很友好,但是它却可以实现对复杂路由匹配规则的准确定义。

例如,你想匹配路由 /users/100-500 这类表示某个用户范围的列表页面,那么该正则如下:

app.get(/^\/users\/(\d+)-(\d+)$/, function(req, res) {

    var startId = parseInt(req.params[0], 10);

    var endId = parseInt(req.params[1], 10);
    //
});

正则表达式可以让你的路由匹配定义更上一层楼。

捕获查询参数

另一种常用的动态传入 URL 参数的方法就是通过查询字符串。

例如,当你使用谷歌搜索 javascript-themed burrito 时,你可以会发现对应的 URL 可能是 www.google.com/search?q=ja…

如果 Google 是用 Express 进行实现的话,那么可以这样来获取用户传入的信息:

app.get("/search", function(req, res) {
    // req.query.q == "javasript-themed burrito"
    // ...
});
需要注意的是:查询参数中存在其实存在着类型安全问题。
例如:如果你访问 ?arg=something 那么 req.query.arg 就是一个字符串类型,但是如果访问的是 ?arg=something&arg=somethingelse 的话 req.query.arg 就变为了一个数组类型。
简单来说:不要轻易的断定查询参数的类型。

使用 Router 划分你的 app

伴随着应用的扩张,程序中产生的路由也会越来越多。而对这些庞大的路由进行管理并不是一件轻松的事,不过好在 Express 4 新增了 Router (可以理解为路由器)特性。

Router 的官方描述是:

Router 是一个独立于中间件和路由的实例,你可以将 Router 看作是只能执行执行中间件和路由的应用。而 Express 程序本身就内置了一个 Router 实例。

Router 的行为与中间件类型,它可以通过 .use() 来调用其他的 Router 实例。

换句话就是,可以使用 Router 将应用划分为几个小的模块。虽然对于一些小型应用来说这样做可能是过度设计,但是一旦 app.js 中的路由扩张太快的话你就可以考虑使用 Router 进行模块拆分了。

注意:程序越大 Router 发挥的作用就越明显。虽然这里我不会编写一个大型应用程序,但是你可以在你的脑海中对下面的示例功能进行无限扩张。

var express = require("express");
var path = require("path");

// 引入 API  Router
var apiRouter = require("./routes/api_router");

var app = express();
var staticPath = path.resolve(__dirname, "static");
app.use(express.static(staticPath));
// API  Router 文件的调用
app.use("/api", apiRouter);
app.listen(3000);

如上所示,Router 的使用方式和之前的中间件非常类似。

其实 Router 本质上就是中间件。在代码中我们将所有 /api 开头的 URL 全部转发到了 apiRouter 中了, 这意味着 /api/users/api/message 的处理都会在 apiRouter 中进行。

下面就是 api_router.js 文件的一个简单代码示例:

var express = require("express");
var ALLOWED_IPS = [
    "127.0.0.1",
    "123.456.7.89"
];
var api  = express.Router();
api.use(function(req, res, next) {
    var userIsAllowed = ALLOWED_IPS.indexOf(req.ip) !== -1;
    if(!userIsAllowed) {
        res.status(401).send("Not authorized!");
    } else {
        next();
    }
});
api.get("/users", function(req, res) { /* ... */ });
api.post("/users", function(req, res) { /* ... */ });
api.get("/messages", function(req, res) { /* ... */ });
api.post("/messages", function(req, res) { /* ... */ });
module.exports = api;

其实 Router 与 app.js 在功能上没有任何区别,都是处理中间件和路由。

最大的不同在于:Router 只能已模块形式存在并不能独立运行。

参照示例,你可以在自己的应用中按模块划分出更多的 Router 。

静态文件

除非应用是纯 API 服务,否则总可能需要发送静态文件。这些文件可能是静态图片 CSS 样式文件或者是静态 HTML 文件。

静态文件中间件

直接查看代码:

var express = require("express");
var path = require("path");
var http = require("http");
var app = express():
// 设置你的静态文件路径
var publicPath = pathresolve(dirname, "public");
// 从静态文件夹中发送静态文件
app.use(express.static(publicPath));
app.use(function(request, response) {
    response.writeHead(200, { "Content-Type": "text/plain"});
    reponse.end("Looks like you didn't find a static file.");
});
http.createServer(app).listen(3000);

修改静态文件的 URL

通常情况下,我们会把站点的静态文件 URL 路径直接挂在域名后面,例如:jokes.edu 站点中的 jokes.txt 文件 URL 样式应该是 jokes.edu/jokes.txt

你可可以按照自己的习惯给这些静态文件提供 URL 。

例如,将一些无序但有趣的图片存放在文件夹 offensive 中并将其中图片的 URL 设置为 jokes.edu/offensive/p… 这种形式。那么该样式 URL 如何实现呢?

在 Express 中,我们可以使用指定前缀的中间件来对静态文件 URL 进行自定义。所以上面问题的代码实现如下:

// ... 
var photoPath = path.resolve(__dirname, "offensive-photos-folder");
app.use("/offensive", express.static(photoPath));
// ...

这样你所有静态文件的 URL 都可以实现自定义了,而不是粗暴的直接挂在域名后面了。其实除了静态中间件和前面 Router 外,其它中间件同样可以指定 URL 前缀。

多个静态文件夹的路由

真实项目中可能户存在多个静态文件夹,例如:一个存放 CSS 等公用文件的 public 文件夹,一个存放用户上传文件的 user_uploads 文件夹。

epxress.static 本身作为中间件是可以在代码中多次调用的:

// ...
var publiscPath = path.resolve(__dirname, "public");
var userUploadPath = path.resove(__dirname, "user_uploads");
app.use(express.static(publicPath));
app.use(express.static(userUploadsPath));
// ...

接下来,我们通过四个模拟场景看看上面代码是如何工作的:

  1. 用户请求的资源两个文件夹里都没有,则上面两个中间件都会被跳过执行。
  2. 用户请求的资源只在 public 里面则第一个中间件响应执行并返回。
  3. 用户请求的资源只在 user_uploads 里面则第一个中间件被跳过而第二个得道执行。
  4. 用户请求的资源在两个文件夹中都存在,则第一个中间件响应执行并返回,第二个不会得到执行。

对于第四章情况,如果该资源是相同的还好说,但是一旦只是资源同名就存在明显错误了。为此,我们依旧可以使用 URL 前缀来应对:

// ...
app.use("/public", express.static(publicPath));
app.use("/uploads", express.static(userUploadsPath));
// ...

这样对于同名文件 image.jpg Express 会将其分别映射到 /public/image.jpg 和 /uploads/image.jpg 。

路由到静态文件映射

在程序中有可能还存在对动态路由请求响应静态文件情形,例如,当用户访问 /users/123/profile_photo 路径时程序需要发送该用户的图片。

静态中间件本身时无法处理该需求,不过好在 Express 可以使用与静态中间件类似的机制来处理这种情况。

假设当有人发起 /users/:userid/profile_photo 请求时,我们都需要响应对应 userid 用户的图片。另外,假设程序中存在一个名为 getProfilePhotoPath 的函数,该函数可以根据 userid 获取图片的存储路径。那么该功能的实现代码如下:

app.get("/users/:userid/profile_photo", function(req, res) {
    res.sendFile(getProfilePhotoPath(req.params.userid));
});

仅仅只需指定路由然后通过 sendFile 函数,我们就可以完成该路由对应文件的发送任务。

 

在 Express 使用 HTTPS

HTTPS 是在 HTTP 基础上添加了一个安全层,通常情况下该安全层被称为 TLS 或者 SSL 。

虽然两个名字可以互换,但是 TSL 在技术上涵盖了 SSL。

这里并不会介绍 HTTPS 复杂的 RSA 加密数学原理(欧拉函数)。

简单来说 HTTPS 的加密过程就是:所有的客户端都使用服务端公开的公钥加密请求信息,然后服务端使用私钥对加密后内容进行解密。

这样就能在某种程度上防止信息被窃听。另外,加密的公钥也被称为证书。客户端在拿到公钥证书后会向 Google 这样的证书颁发机构进行验证。

注意:类似 Heroku 这样的虚拟主机商已经提供了 HTPPS 服务,所以这部分内容只在你需要自己实现 HTTPS 时才派得上用场。

首先,我们通过 OpenSSL 生成自签名的公钥和私钥。Windows 系统可以使用去官网获取 OpenSSL 安装文件,Linux 可以使用保管理器进行安装,而 macOS 系统已经预装过了。

通过 openssl version 验证系统是否成功安装了 OpenSSL, 确保安装后输入下面两个命令:

openssl genrsa -out privatekey.pem 1024
openssl req -new -key privatekey.pem -out request.pem

第一个命令会生成名为 privatekey.pem 的私钥。第二个命令会让你输入一些信息,然后使用 privatekey.pem 生成签名的证书请求文件 request.pem

然后你就可以去证书请求机构申请一个加密的公钥证书。虽然大部分证书都是收费的,但是你还是可以去 letsencrypt 申请免费版本证书。

一旦获取了 SSL 证书文件,你就可以使用 Node 内置的 HTTPS 模块了,代码如下:

var express = require("express");
var https = require("https");
var fs = require("fs");
var app = express();
// ... 定义你的app ...
// 定义一个对象来保存证书和私钥
var httpsOptions = {
    key: fs.fs.readFileSync("path/to/private/key.pem");
    cert: fs.fs.readFileSync("path/to/certificate.pem");
}

https.createServer(httpsOptions, app).listen(3000);

除了配置私钥和公钥证书参数之外,其他部分与之前 HTTP 模块的使用时一致的。当然,如果你想同时支持 HTTP 和 HTTPS 协议的话也是可以的:

var express = require("express");
var http = require("http");
var https = require("https");
var fs = require("fs");
var app = express();
// ... 定义你的app ...
var httpsOptions = {
    key: fs.readFileSync("path/to/private/key.pem"),
    cret: fs.readFileSync("path/to/certificate.pem")
};
http.createServer(app).listen(80);
https.createServer(httpsOptions, app).listen(443);

需要注意的是 HTTP 和 HTTPS 协议 同时开启时需要使用不同的端口号。

总结

在本章中,我们学到了:

  • 从概念上知道了什么是路由:进行 URL 和代码的映射的工具。
  • 简单的路由以及常用映射处理。
  • 获取路由中的参数。
  • Express 4 路由模块的新特性。
  • 将路由应用到中间件处理。
  • 如何在 Express 中使用 HTTPS。
posted @ 2020-05-03 14:26  Magi黄元  阅读(227)  评论(0编辑  收藏  举报