Express 框架的初衷是为了拓展 Node 内置模块的功能提升开发效率。当你深刻研究后就会发现,Express 实际上是在 Node 内置的 HTTP 模块上构建了一层抽象。理论上全部 Express 实现的功能,一样可使用纯 Node 实现。css
在本文中,咱们将基于前面的 Node 内容去探究 Express 和 Node 之间的关系,其中包括:中间件和路由等概念。固然,这里只会进行一些综述具体的细节会在后面带来。html
总的来讲,Express 提供了 4 个主要特性:node
<!--more-->git
中间件是 Express 中最大的特性之一。中间件与原生的 Node 处理函数很是相似(接受一个请求并作出响应),可是与原生不一样的是,中间件将处理过程进行划分,而且使用多个函数构成一个完整的处理流程。github
咱们将会看到中间件在代码中的各类应用。例如,首先使用一个中间件记录全部的请求,接着在其余的中间件中设置 HTTP 头部信息,而后继续处理流程。虽然在一个“大函数”中也能够完成请求处理,可是将任务进行拆分为多个功能明确独立的中间件明显更符合软件开发中的 SRP 规则。web
中间件并非 Express 特有,Python 的 Django 或者 PHP 的 Laravel 也有一样的概念存在。一样的 Ruby 的 Web 框架中也有被称为 Rack 中间件概念。正则表达式
如今咱们就用 Express 中间件来从新实现 Hello World 应用。你将会发现只需几行代码就能完成开发,在提升效率的同时还消除了一些隐藏 bug。express
首先新建一个Express工程:新建一个文件夹并在其中新建 package.json 文件。回想一下 package.json 的工做原理,其中完整的列出了该工程的依赖、项目名称、做者等信息。咱们新工程中的 package.json 大体以下:npm
{ "name": "hello-world", "author": "Your Name Here!", "private": true, "dependencies": {} }
接下来执行命令,安装最新的 Express 而且将其保存到 package.json 中:json
npm install express -sava
命令执行完成后,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 进行处理,尤为是后者。
下面咱们来实现前面提到的验证中间件函数。为了简单起见,这里只容许当前分钟数为偶数的状况经过验证。那么,该中间件函数代码大体以下:
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 也是 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 记录的日志了。
经过网络发送静态文件对 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 静态中间以外,还有不少其余功能强大的中间件,例如:
路由是一种将 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 等全部网络请求。函数中第一个参数是一个路径,例如 /about 或者 /weather 或者简单的根目录 / ,第二个参数是一个请求处理函数。该处理函数与以前的中间件工做方式同样,惟一的区别就是调用时机。
除了固定路由形式外,它还能够匹配更复杂的路由(使用正则等方式):
// 指定“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 。例如,有一个用户为 ExpressSuperHero ,那么他的我的信息页面 URL 多是:
在 Express 中你能够经过这种通配方式简化路由定义,而没必要将全部用户的特定路由都一一列举出来。
官方文档中还展现了一个使用正则表达式来进行复杂匹配的例子,而且你能够经过路由作更多其它的事情。不过这章中只须要知道路由概念就好了,更多的内容将会在第五章中深刻讲解。
Express 在原来基础上对 request 和 response 对象进行了功能扩展。你能够在官方文档中找到全部细节内容,不过咱们能够先来领略其中的一部分:
Express 提供的功能中 redirect 算一个很是棒的功能,使用方法以下:
response.redirect("/hello/world"); response.redirect("http://expressjs.com");
原生 Node 中并无重定向 redirect 方法。虽然咱们也可以使用原生代码实现重定向功能,但明显它的代码量会更多。
另外,在 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 全部拓展中的冰山一角,你能够在文档中看到更多的示例。
几乎全部的网站内容都是基于 HTML 进行展现的,而且大多时候这些 HTML 内容都是动态生成的。你可能须要为当前登陆用户提供特定欢迎页或者须要在页面中动态生成数据表。为了应对动态内容的渲染,社区中出现了大量的 Express 模版引擎,例如: EJS、Handlebars、Pug。
下面是 EJS 模版引擎使用示例:
var express = require("express"); var path = require("path"); var app = express(); // 告诉 Express 你的视图存在于一个名为 views 的文件夹中 app.set("views", path.resolve(__dirname, "views")); // 告诉 Express 你将使用EJS模板引擎 app.set("view engine", "ejs");
在代码中,首先咱们导入了必要的模块。而后设置了视图文件所在的路径。紧接着,咱们将模版引擎设置为 EJS (文档)。固然在使用 EJS 执行,咱们还须要经过 npm install ejs --save 命令进行安装。
安装并设置好 EJS 引擎以后,接下里就是如何使用的问题了。
首先,咱们在 views 文件夹下面建立一个 index.ejs 文件,并拷贝下面的内容:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello, world!</title> </head> <body> <%= message %> </body> </html>
EJS 实质上是 HTML 的一个超集,全部 HTML 的语法均可以直接使用而且彻底兼容。可是 EJS 对语法进行了部分拓展。 例如,你能够经过 <%= message %> 语法将传递过来的参数 message 插入到标签中。
app.get("/", function(request, response) { response.render("index", { message: "Hey everyone! This is my webpage." }); });
Express 给 response 对象添加了一个名为 render 的方法。该方法在视图目录下查找第一个参数对应的模版视图文件并将第二个参数传递给该模版文件。
下面是通过引擎渲染动态生成后的 HTML 文件内容:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello, world!</title> </head> <body> Hey everyone! This is my webpage. </body> </html>
最后这部分,咱们将会使用到前面的技术来构建一个完整的留言板 web 程序。经过这个示例来加深对上面内容的掌握,该应用主要包含两个页面:
首先,咱们新建一个文件夹并新建项目,并复制下面内容到新建的 package.json 文件中:
{ "name": "express-guestbook", "private": true, "scripts": { "start": "node app" } }
你能够在文件中添加其余字段信息(例如做者或者版本),可是在本例中这并非必要信息。接下来,咱们安装依赖文件,输入命令:
npm install express morgan body-parser ejs --save
由于须要实现留言新建动做,因此这里须要使用 body-parser 对 POST 请求进行解析。
准备工做完成后,接下来就建立 app.js 文件并复制下面的代码:
var http = require("http"); var path = require("path"); var express = require("express"); var logger = require('morgan'); var bodyParser = require("body-parser"); var app = express(); // 设置引擎 app.set("views", path.resolve(__dirname, "views")); app.set("view engine", "ejs"); // 设置留言的全局变量 var entries = []; app.locals.entries = entries; // 使用 Morgan 进行日志记录 app.use(logger("dev")); // 设置用户表单提交动做信息的中间件,全部信息会保存在 req.body 里 app.use(bodyParser.urlencoded({ extended: false })); // 当访问了网站根目录,就渲染主页(位于views/index.ejs) app.get("/", function(request, response) { response.render("index"); }); // 渲染“新留言”页面(位于views/index.ejs)当get访问这个URL的时候 app.get("/new-entry", function(request, response) { response.render("new-entry"); }); // POST 动做进行留言新建的路由处理 app.post("/new-entry", function(request, response) { // 若是用户提交的表单没有标题或者内容,则返回一个 400 的错误 if (!request.body.title || !request.body.body) { response.status(400).send("Entries must have a title and a body."); return; } // 添加新留言到 entries 中 entries.push({ title: request.body.title, content: request.body.body, published: new Date() }); // 重定向到主页来查看你的新条目 response.redirect("/"); }); // 渲染404页面,由于你请求了未知资源 app.use(function(request, response) { response.status(404).render("404"); }); // 在3000端口启动服务器 http.createServer(app).listen(3000, function() { console.log("Guestbook app started on port 3000."); });
最后咱们须要将页面的视图文件补全,新建 views 文件夹,而后复制下面内容到新建 header.ejs 文件中:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Express Guestbook</title> <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"> </head> <body class="container"> <h1> Express Guestbook <a href="/new-entry" class="btn btn-primary pull-right"> Write in the guestbook </a> </h1>
这里使用了 Twitter 的 Bootstrap 框架,固然你也能够进行任意替换。最重要的一点是,该文件会作为全部页面的通用头部。
接下来,在相同目录下新建 footer.ejs 做为通用的 footer:
</body> </html>
通用部分完成后,接下来就是 index、new-entry、404 页面文件了。复制下面代码到文件 views/index.ejs 中:
<% include header %> <% if (entries.length) { %> <% entries.forEach(function(entry) { %> <div class="panel panel-default"> <div class="panel-heading"> <div class="text-muted pull-right"> <%= entry.published %> </div> <%= entry.title %> </div> <div class="panel-body"> <%= entry.body %> </div> </div> <% }) %> <% } else { %> No entries! <a href="/new-entry">Add one!</a> <% } %> <% include footer %>
同时将下面的代码复制到 views/new-entry.ejs 中
<% include header %> <h2>Write a new entry</h2> <form method="post" role="form"> <div class="form-group"> <label for="title">Title</label> <input type="text" class="form-control" id="title" name="title" placeholder="Entry title" required> </div> <div class="form-group"> <label for="content">Entry text</label> <textarea class="form-control" id="body" name="body" placeholder="Love Express! It's a great tool for building websites." rows="3" required></textarea> </div> <div class="form-group"> <input type="submit" value="Post entry" class="btn btn-primary"> </div> </form> <% include footer %>
最后就是 views/404.ejs 文件了:
<% include header %> <h2>404! Page not found.</h2> <% include footer %>
全部的视图文件都建立完成了,接下来就是运行服务了。
若是你如今就使用 npm start 拉起服务,而后访问对应的 URL ,你就能见到下图所示的场景了。
最后,咱们回顾一下这个小项目的几个关键点:
原文地址