Express 实战(五):路由

Cover
Cover

做为 Express 中的最大特色之一,路由让你能够将不一样的请求映射到不一样的中间件中。这一章咱们将会深刻学习这部分的内容,另外还包括如何在 Express 使用 HTTPS 以及部分 Express 4 中的新特性等等。固然,学习过程仍是经过示例应用和代码的形式进行展示的。javascript

什么是路由?

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

GET /someone http/1.1html

其中包含了 HTTP 请求使用的方法(GET),URI 信息(/someone) 以及 HTTP 协议版本 (1.1)。Express 中的路由就是负责将其中的 HTTP 方法和 URI 这对组合映射到对应的中间件。简单说就是, /about_me 的GET 请求会执行某个中间件而对于 /new_user 的 POST 请求则执行另外一个中间件。java

下面咱们经过一个简单示例来看看到底路由时如何工做的。node

路由的一个简单示例

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

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 发起请求时,程序会执行该中间件中的代码,其余请求则会被忽略并跳转到下一个中间件。git

路由的特性

从工做原理来讲:路由就是经过对 HTTP 方法和的 URI 的组合进行映射来实现对不一样请求的分别处理。固然,除了上面那种最简单的使用方式以外,Express 的路由还有更多实用的使用技巧和方法。web

注意:在其它一些框架中(例如,Ruby on Rails )会有一个专门的文件进行路由管理,可是 Express 中并无这样的规定,你能够将路由按模块分开管理。ajax

含参的通配路由

在上面的使用方式中使用的是全等判断来进行路由匹配的。虽然对于 /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);
    // …
});复制代码

甚至你还能够做出更复杂的正则匹配路由定义,例如:匹配某个包含特定 UUID 的路由。UUID 是一长串 16 进制的字符串,大体以下:

xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx

若是,其中的 x 表示任何 16 进制数字,而 y 只能是 8,9,A 或者 B 。那么该路由的正则匹配就是:

var horribleRegexp = /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/i;
app.get(horribleRegexp, function(req, res) {
    var uuid = req.params[0];
    // ...
});复制代码

还有更多的使用示例就不一一列举了。这里只需记住一点:正则表达式可让你的路由匹配定义更上一层楼。

捕获查询参数

另外一种经常使用的动态传入 URL 参数的方法就是经过查询字符串(query string)。例如,当你使用谷歌搜索 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 协议 同时开启时须要使用不一样的端口号。

路由的应用示例

接下来,咱们搭建一个简单的 web 程序巩固一下这章所学的路由内容。该应用的主要功能是经过美国的 ZIP 邮政编码返回该地区的温度。

示例使用的是美式邮政编码,因此该示例只能在做者所在的美国正常使用。固然,你彻底可使用 H5 的 Geolocation API 对其进行改造。

示例主要包含两个部分:

  1. 一个静态页,用于询问用户的 ZPI 编码。用户输入编码后会经过 AJAX 发送异步请求获取天气。
  2. 解析得到 JSON 格式数据,并将结果映射 ZIP 编码对应的动态路由上。

准备工做

在示例中须要使用的 Node 类库有:Express、ForecastIO (用于获取天气数据)、Zippity-do-dah ( 将ZIP编码转为纬度/经度 )、EJS 模版引擎。

新建应用文件夹,并复制下面内容到 package.json 文件中:

{
    "name": "temperature-by-zip",
    "private": true,
    "scripts": {
        "start": "node app.js"
    },
    "dependencies": {
        "ejs": "^2.3.1",
        "express": "^5.0.0",
        "forecastio": "^0.2.0",
        "zippity-do-dah": "0.0.x"
    }
}复制代码

使用 npm install 命令完成依赖项的安装,并新建两个文件夹:public 和 views。另外,示例程序还会用到 jQuery 和名为 Pure 的 CSS 框架。最后,你须要去 Forecast.io 官网 注册开发帐号获取 API 接口密钥。

主入口代码

准备工做完成后,接下来就是编写代码了。这里咱们从程序的主入口开始编写 JavaScript 代码,新建 app.js 文件并拷贝代码:

var path = require("path");
var express = require("express");
var zipdb = require("zippity-do-dah");
var ForecastIo = require("forecastio");
var app = express();
var weather = new ForecastIo("你的FORECAST.IO的API密钥");

app.use(express.static(path.resolve(__dirname, "public")));
app.set("views", path.resolve(__dirname, "views"));
app.set("view engine", "ejs");

app.get("/", function(req, res) {
    res.render("index");
});

app.get(/^\/(\d{5})$/, function(req, res, next) {
    var zipcode = req.params[0];
    var location = zipdb.zipcode(zipcode);
    if (!location.zipcode) {
        next();
        return;
    }

    var latitude = location.latitude;
    var longitude = location.longitude;
    weather.forecast(latitude, longitude, function(err, data) {
        if (err) {
            next();
            return;
        }

        res.json({
            zipcode: zipcode,
            temperature: data.currently.temperature
        });
    });
});

app.use(function(req, res) {
    res.status(404).render("404");
});

app.listen(3000);复制代码

接下来就是使用 EJS 引擎编写视图文件了。

两个视图

示例应用中会有两个视图:404 页面和主页。为了尽量保持页面风格的统一,这里将会使用到模版技术。首先动手实现通用的 headerfooter 模版。

其中 views/header.ejs 文件中的代码以下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Temperature by ZIP code</title>
    <link rel="stylesheet" href="http://yui.yahooapis.com/pure/0.6.0/pure-min.css">
    <link rel="stylesheet" href="/main.css">
</head>
<body>复制代码

紧接着就是 views/footer.ejs

</body>
</html>复制代码

完成上面通用模版以后,下面就能够实现 404 页面 views/404.ejs 了:

<% include header %>
    <h1>404 error! File not found.</h1>
<% include footer %>复制代码

一样的,主页 views/index.ejs 代码以下:

<% include header %>
<h1>What's your ZIP code?</h1>
<form class="pure-form">
    <fieldset>
        <input type="number" name="zip" placeholder="12345" autofocus required>
        <input type="submit" class="pure-button pure-button-primary" value="Go">
    </fieldset>
</form>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="/main.js"></script>
<% include footer %>复制代码

上面页面代码中使用了一些 Pure 框架里的样式来优化界面 UI 。

除此以外,咱们还须要在 public/main.css 指定页面布局:

html {
    display: table;
    width: 100%;
    height: 100%;
}
body {
    display: table-cell;
    vertical-align: middle;
    text-align: center;
}复制代码

在该样式文件中,咱们将页面内容同时设置为了水平和垂直居中。

最后拷贝下面的代码,把缺失的 public/main.js 补充完整。

$(function() {
    var $h1 = $("h1");
    var $zip = $("input[name='zip']");
    $("form").on("submit", function(event) {
        // 禁止表单的默认提交
        event.preventDefault();
        var zipCode = $.trim($zip.val());
        $h1.text("Loading...");

        var request = $.ajax({
            url: "/" + zipCode,
            dataType: "json"
        });
        request.done(function(data) {
            var temperature = data.temperature;
            $h1.html("It is " + temperature + "° in " + zipCode + ".");
        });
        request.fail(function() {
            $h1.text("Error!");
        });
    });
});复制代码

运行示例程序

结束全部编码任务后,下面咱们经过 npm start 运行示例程序。当你访问 http://localhost:3000 并输入 ZIP 编码后界面以下:

05_01
05_01

在这个简单的示例中,咱们使用了 Express 中的路由特性,另外还使用了 EJS 模版引擎来编写视图文件。你能够在此基础上继续发挥想象力完善该示例。

总结

在本章中,咱们学到了:

  • 从概念上知道了什么是路由:进行 URL 和代码的映射的工具。
  • 简单的路由以及经常使用映射处理。
  • 获取路由中的参数。
  • Express 4 路由模块的新特性。
  • 将路由应用到中间件处理。
  • 如何在 Express 中使用 HTTPS。

原文地址

相关文章
相关标签/搜索