Node.js 已经成为 Web 后台开发圈一股不容忽视的力量,凭借其良好的异步性能、丰富的 npm 库以及 JavaScript 语言方面的优点,已经成为了不少大公司开发其后台架构的重要技术之一,而 Express 框架则是其中知名度最高、也是最受欢迎的后端开发框架。在这篇教程中,你将了解 Express 在 Node 内置 http 模块的基础上作了怎样的封装,并掌握路由和中间件这两个关键概念,学习和使用模板引擎、静态文件服务、错误处理和 JSON API,最终开发出一个简单的我的简历网站。javascript
此教程属于 Node.js 后端工程师学习路线的一部分,欢迎来 Star 一波,鼓励咱们继续创做出更好的教程,持续更新中~。
自从 Ryan Dahl 在 2009 年的 JSConf 正式推出 Node.js 平台后,这门技术的使用率就如同坐了火箭通常迅速上升,成为了最受喜好的后端开发平台之一,而 Express 则是其中最为耀眼的 Web 框架。在正式开始这篇教程以前,咱们将列举一下这篇教程所须要的预备知识、所用技术和学习目标。css
本教程假定你已经知道了:html
读完这篇教程后,你将学会前端
注意虽然数据库是后端开发中很是重要的环节,但 Express 并不内置处理数据库的模块,须要额外的第三方库提供支持。这篇教程将重点放在了 Express 相关的概念讲解上,所以不会涉及数据库的开发。在学完这篇教程后,你能够浏览 Express 相关的进阶教程。java
在讲解 Express 以前,咱们先了解一下怎么用 Node.js 内置的 http 模块来实现一个服务器,从而可以更好地了解 Express 对底层的 Node 代码作了哪些抽象和封装。若是你尚未安装 Node.js,能够去官方网站下载并安装。node
咱们将实现一个我的简历网站。建立一个文件夹 express_resume,并进入其中:linux
mkdir express_resume && cd express_resume
建立 server.js 文件,代码以下:git
const http = require('http'); const hostname = 'localhost'; const port = 3000; const server = http.createServer((req, res) => { res.statusCode = 200; res.setHeader('Content-Type', 'text/html'); res.end('Hello World\n'); }); server.listen(port, () => { console.log(`Server running at http://${hostname}:${port}/`); });
若是你熟悉 Node.js,上面的代码含义很清晰:github
hostname
和端口号 port
http.createServer
建立 HTTP 服务器,参数为一个回调函数,接受一个请求对象 req
和响应对象 res
,并在回调函数中写入响应内容(状态码 200,类型为 HTML 文档,内容为 Hello World
)最后运行 server.js:web
node server.js
用浏览器打开 localhost:3000,能够看到 Hello World 的提示:
能够发现,直接用内置的 http 模块去开发服务器有如下明显的弊端:
http.createServer
的回调函数中经过判断请求 req
的内容才能实现路由功能,搭建大型应用时力不从心由此就引出了 Express 对内置 http 的两大封装和改进:
接下来,咱们将开始用 Express 来开发 Web 服务器!
在第一步中,咱们把服务器放在了一个 JS 文件中,也就是一个 Node 模块。从如今开始,咱们将把这个项目变成一个 npm 项目。输入如下命令建立 npm 项目:
npm init
接着你能够一路回车下去(固然也能够仔细填),就会发现 package.json 文件已经建立好了。而后添加 Express 项目依赖:
npm install express
在开始用 Express 改写上面的服务器以前,咱们先介绍一下上面提到的两大封装与改进。
首先是 Request 请求对象,一般咱们习惯用 req
变量来表示。下面列举一些 req
上比较重要的成员(若是不知道是什么也不要紧哦):
req.body
:客户端请求体的数据,多是表单或 JSON 数据req.params
:请求 URI 中的路径参数req.query
:请求 URI 中的查询参数req.cookies
:客户端的 cookies而后是 Response 响应对象,一般用 res
变量来表示,能够执行一系列响应操做,例如:
// 发送一串 HTML 代码 res.send('HTML String'); // 发送一个文件 res.sendFile('file.zip'); // 渲染一个模板引擎并发送 res.render('index');
Response 对象上的操做很是丰富,而且还能够链式调用:
// 设置状态码为 404,并返回 Page Not Found 字符串 res.status(404).send('Page Not Found');
提示在这里咱们并无简单地列举 Request 和 Response 的所有 API ,由于图雀社区的理念是——从实战中学习和深化理解,拒绝枯燥的 API 记忆!
客户端(包括 Web 前端、移动端等等)向服务器发起请求时包括两个元素:路径(URI)以及 HTTP 请求方法(包括 GET、POST 等等)。路径和请求方法合起来通常被称为 API 端点(Endpoint)。而服务器根据客户端访问的端点选择相应处理逻辑的机制就叫作路由。
在 Express 中,定义路由只需按下面这样的形式:
app.METHOD(PATH, HANDLER)
其中:
app
就是一个 express
服务器对象METHOD
能够是任何小写的 HTTP 请求方法,包括 get
、post
、put
、delete
等等PATH
是客户端访问的 URI,例如 /
或 /about
HANDLER
是路由被触发时的回调函数,在函数中能够执行相应的业务逻辑Nodemon 是一款颇受欢迎的开发服务器,可以检测工做区代码的变化,并自动重启。经过如下命令安装 nodemon:
npm install nodemon --save-dev
这里咱们将 nodemon 安装为开发依赖 devDependencies
,由于仅仅只有在开发时才须要用到。同时咱们在 package.json 中加入 start
命令,代码以下:
{ "name": "express_resume", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "nodemon server.js", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "express": "^4.17.1" }, "devDependencies": { "nodemon": "^2.0.2" } }
到了动手的时候了,咱们用 Express 改写上面的服务器,代码以下:
const express = require('express'); const hostname = 'localhost'; const port = 3000; const app = express(); app.get('/', (req, res) => { res.send('Hello World'); }); app.listen(port, () => { console.log(`Server running at http://${hostname}:${port}/`); });
在上面的代码中,咱们首先用 express()
函数建立一个 Express 服务器对象,而后用上面提到的路由定义方法 app.get
定义了主页 /
的路由,最后一样调用 listen
方法开启服务器。
从这一步开始,咱们运行 npm start
命令便可开启服务器,而且一样能够看到 Hello World 的内容,可是代码却简单明了了很多。
提示在运行
npm start
以后,可让服务器一直打开着,编辑代码并保存后,Nodemon 就会自动重启服务器,运行最新的代码。
接下来咱们开始讲解 Express 第二个重要的概念:中间件(Middleware)。
中间件并非 Express 独有的概念。相反,它是一种广为使用的软件工程概念(甚至已经延伸到了其余行业),是指将具体的业务逻辑和底层逻辑解耦的组件(可查看这个讨论)。换句话说,中间件就是可以适用多个应用场景、可复用性良好的代码。
Express 的简化版中间件流程以下图所示:
首先客户端向服务器发起请求,而后服务器依次执行每一个中间件,最后到达路由,选择相应的逻辑来执行。
提示这个是一个简化版的流程描述,目的是便于你对中间件有个初步的认识,在后面的章节中咱们将进一步完善这一流程。
有两点须要特别注意:
在 Express 中,中间件就是一个函数:
function someMiddleware(req, res, next) { // 自定义逻辑 next(); }
三个参数中,req
和 res
就是前面提到的 Request 请求对象和 Response 响应对象;而 next
函数则用来触发下一个中间件的执行。
注意若是忘记在中间件中调用
next
函数,而且又不直接返回响应时,服务器会直接卡在这个中间件不会继续执行下去哦!
在 Express 使用中间件有两种方式:全局中间件和路由中间件。
经过 app.use
函数就能够注册中间件,而且此中间件会在用户发起任何请求均可能会执行,例如:
app.use(someMiddleware);
经过在路由定义时注册中间件,此中间件只会在用户访问该路由对应的 URI 时执行,例如:
app.get('/middleware', someMiddleware, (req, res) => { res.send('Hello World'); });
那么用户只有在访问 /middleware
时,定义的 someMiddleware
中间件才会被触发,访问其余路径时不会触发。
接下来咱们就开始实现第一个 Express 中间件。功能很简单,就是在终端打印客户端的访问时间、 HTTP 请求方法和 URI,名为 loggingMiddleware
。代码以下:
// ... const app = express(); function loggingMiddleware(req, res, next) { const time = new Date(); console.log(`[${time.toLocaleString()}] ${req.method} ${req.url}`); next(); } app.use(loggingMiddleware); app.get('/', (req, res) => { res.send('Hello World'); }); // ...
注意在中间件中写
console.log
语句是比较糟糕的作法,由于console.log
(包括其余同步的代码)都会阻塞 Node.js 的异步事件循环,下降服务器的吞吐率。在实际生产中,推荐使用第三方优秀的日志中间件,例如 morgan、winston 等等。
运行服务器,而后用浏览器尝试访问各个路径。这里我访问了首页(localhost:3000)和 /hello
(localhost:3000/hello,浏览器应该看到的是 404),能够看到控制台相应的输出:
[11/28/2019, 3:54:05 PM] GET / [11/28/2019, 3:54:11 PM] GET /hello
这里为了让你初步理解中间件的概念,咱们只实现了一个功能很简单的中间件。实际上,中间件不只能够读取 req
对象上的各个属性,还能够添加新的属性或修改已有的属性(后面的中间件和路由函数均可以获取),可以很方便地实现一些复杂的业务逻辑(例如用户鉴权)。
最后,咱们的网站要开始展现一些实际内容了。Express 对当今主流的模板引擎(例如 Pug、Handlebars、EJS 等等)提供了很好的支持,能够作到两行代码接入。
提示若是你不了解模板引擎,不用担忧,这篇教程几乎不须要用到它的高级功能,你只需理解成一个“升级版的 HTML 文档”便可。
这篇教程将使用 Handlebars 做为模板引擎。首先添加 npm 包:
npm install hbs
建立 views 文件夹,用于放置全部的模板。而后在其中建立首页模板 index.hbs,代码以下:
<h1>我的简历</h1> <p>我是一只小小的图雀,渴望学习技术,磨炼实战本领。</p> <a href="/contact">联系方式</a>
建立联系页面模板 contact.hbs,代码以下:
<h1>联系方式</h1> <p>QQ:1234567</p> <p>微信:一只图雀</p> <p>邮箱:mrc@tuture.co</p>
最后即是在 server.js 中配置和使用模板。配置模板的代码很是简单:
// 指定模板存放目录 app.set('views', '/path/to/templates'); // 指定模板引擎为 Handlebars app.set('view engine', 'hbs');
在使用模板时,只需在路由函数中调用 res.render
方法便可:
// 渲染名称为 hello.hbs 的模板 res.render('hello');
修改后的 server.js 代码以下:
// ... const app = express(); app.set('views', 'views'); app.set('view engine', 'hbs'); // 定义和使用 loggingMiddleware 中间件 ... app.get('/', (req, res) => { res.render('index'); }); app.get('/contact', (req, res) => { res.render('contact'); }) // ...
注意在上面的代码中,咱们添加了 GET /contact
的路由定义。
最后,咱们再次运行服务器,访问咱们的主页,能够看到:
点击”联系方式“,跳转到相应页面:
一般网站须要提供静态文件服务,例如图片、CSS 文件、JS 文件等等,而 Express 已经自带了静态文件服务中间件 express.static
,使用起来很是方便。
例如,咱们添加静态文件中间件以下,并指定静态资源根目录为 public
:
// ... app.use(express.static('public')); app.get('/', (req, res) => { res.render('index'); }); // ...
假设项目的 public 目录里面有这些静态文件:
public ├── css │ └── style.css └── img └── tuture-logo.png
就能够分别经过如下路径访问:
http://localhost:3000/css/style.css http://localhost:3000/img/tuture-logo.png
样式文件 public/css/style.css 的代码以下(直接复制粘贴便可):
body { text-align: center; } h1 { color: blue; } img { border: 1px dashed grey; } a { color: blueviolet; }
图片文件可经过这个 GitHub 上的连接下载,而后下载到 public/img 目录中。固然,你也可使用本身的图片,记得在模板中替换相应的连接就能够了。
在首页模板 views/index.hbs 中加入 CSS 样式表和图片:
<link rel="stylesheet" href="/css/style.css" /> <h1>我的简历</h1> <img src="/img/tuture-logo.png" alt="Logo" /> <p>我是一只小小的图雀,渴望学习技术,磨炼实战本领。</p> <a href="/contact">联系方式</a>
在联系模板 views/contact.hbs 中加入样式表:
<link rel="stylesheet" href="/css/style.css" /> <h1>联系方式</h1> <p>QQ:1234567</p> <p>微信:一只图雀</p> <p>邮箱:mrc@tuture.co</p>
再次运行服务器,并访问咱们的网站。首页以下:
联系咱们页面以下:
能够看到样式表和图片都成功加载出来了!
人有悲欢离合,月有阴晴圆缺,服务器也有出错的时候。HTTP 错误通常分为两大类:
若是你打开服务器,访问一个不存在的路径,例如 localhost:3000/what
,就会出现这样的页面:
很显然,这样的用户体验是很糟糕的。
在这一节中,咱们将讲解如何在 Express 框架中处理 404(页面不存在)及 500(服务器内部错误)。在此以前,咱们要完善一下 Express 中间件的运做流程,以下图所示:
这张示意图和以前的图有两点重大区别:
next
函数向下传递、直接返回响应,还能够抛出异常 从这张图就能够很清晰地看出怎么实现 404 和服务器错误的处理了:
在 Express 中,能够经过中间件的方式处理访问不存在的路径:
app.use('*', (req, res) => { // ... });
*
表示匹配任何路径。将此中间件放在全部路由后面,便可捕获全部访问路径均匹配失败的请求。
Express 已经自带了错误处理机制,咱们先来体验一下。在 server.js 中添加下面这条”坏掉“的路由(模拟现实中出错的情形):
app.get('/broken', (req, res) => { throw new Error('Broken!'); });
而后开启服务器,访问 localhost:3000/broken
:
危险!服务器直接返回了出错的调用栈!很明显,向用户返回这样的调用栈不只体验糟糕,并且大大增长了被攻击的风险。
实际上,Express 的默认错误处理机制能够经过设置 NODE_ENV
来进行切换。咱们将其设置为生产环境 production
,再开启服务器。若是你在 Linux、macOS 或 Windows 下的 Git Bash 环境中,能够运行如下命令:
NODE_ENV=production node server.js
若是你在 Windows 下的命令行,运行如下命令:
set NODE_ENV=production node server.js
这时候访问 localhost:3000/broken
就会直接返回 Internal Server Error(服务器内部错误),不会显示任何错误信息:
体验仍是很很差,更理想的状况是可以返回一个友好的自定义页面。这能够经过 Express 的自定义错误处理函数来解决,错误处理函数的形式以下:
function (err, req, res, next) { // 处理错误逻辑 }
和普通的中间件函数相比,多了第一个参数,也就是 err
异常对象。
经过上面的讲解,实现自定义的 404 和错误处理逻辑也就很是简单了。在 server.js 全部路由的后面添加以下代码:
// 中间件和其余路由 ... app.use('*', (req, res) => { res.status(404).render('404', { url: req.originalUrl }); }); app.use((err, req, res, next) => { console.error(err.stack); res.status(500).render('500'); }); app.listen(port, () => { console.log(`Server running at http://${hostname}:${port}/`); });
提示在编写处理 404 的逻辑时,咱们用到了模板引擎中的变量插值功能。具体而言,在
res.render
方法中将须要传给模板的数据做为第二个参数(例如这里的{ url: req.originalUrl }
传入了用户访问的路径),在模板中就能够经过{{ url }}
获取数据了。
404 和 500 的模板代码分别以下:
<link rel="stylesheet" href="/css/style.css" /> <h1>找不到你要的页面了!</h1> <p>你所访问的路径 {{ url }} 不存在</p>
<link rel="stylesheet" href="/css/style.css" /> <h1>服务器好像开小差了</h1> <p>过一下子再试试看吧!See your later~</p>
再次运行服务器,访问一个不存在的路径:
访问 localhost:3000/broken
:
体验很不错!
在这篇教程的最后,咱们将实现一个很是简单的 JSON API。若是你有过其余后端 API 开发(特别是 Java)的经验,那么你必定会以为用 Express 实现一个 JSON API 端口简单得难以想象。在以前提到的 Response 对象中,Express 为咱们封装了一个 json
方法,直接就能够将一个 JavaScript 对象做为 JSON 数据返回,例如:
res.json({ name: '百万年薪', price: 996 });
会返回 JSON 数据 { "name": "百万年薪", "price": 996 }
,状态码默认为 200。咱们还能够指定状态码,例如:
res.status(502).json({ error: '公司关门了' });
会返回 JSON 数据 { "error": "公司关门了"}
,状态码为 502。
到了动手环节,让咱们在 server.js 中添加一个简单的 JSON API 端口 /api
,返回关于图雀社区的一些数据:
// ... app.get('/api', (req, res) => { res.json({ name: '图雀社区', website: 'https://tuture.co' }); }); app.get('/broken', (req, res) => { throw new Error('Broken!'); }); // ...
咱们能够用浏览器访问 localhost:3000/api 端口,看到返回了想要的数据:
或者你能够用 Postman 或 Curl 访问,也能看到想要的数据哦。
当咱们的网站规模愈来愈大时,把全部代码都放在 server.js 中可不是一个好主意。“拆分逻辑”(或者说“模块化”)是最多见的作法,而在 Express 中,咱们能够经过子路由 Router
来实现。
const express = require('express'); const router = express.Router();
express.Router
能够理解为一个迷你版的 app
对象,可是它功能完备,一样支持注册中间件和路由:
// 注册一个中间件 router.use(someMiddleware); // 添加路由 router.get('/hello', helloHandler); router.post('/world', worldHandler);
最后,因为 Express 中“万物皆中间件”的思想,一个 Router
也做为中间件加入到 app
中:
app.use('/say', router);
这样 router
下的所有路由都会加到 /say
之下,即至关于:
app.get('/say/hello', helloHandler); app.post('/say/world', worldHandler);
到了动手环节,首先建立 routes 目录,用于存放全部的子路由。建立 routes/index.js 文件,代码以下:
const express = require('express'); const router = express.Router(); router.get('/', (req, res) => { res.render('index'); }); router.get('/contact', (req, res) => { res.render('contact'); }); module.exports = router;
建立 routes/api.js,代码以下:
const express = require('express'); const router = express.Router(); router.get('/', (req, res) => { res.json({ name: '图雀社区', website: 'https://tuture.co' }); }); router.post('/new', (req, res) => { res.status(201).json({ msg: '新的篇章,即将开始' }); }); module.exports = router;
最后咱们把 server.js 中老的路由定义所有删掉,替换成刚刚实现的两个 Router
,代码以下:
const express = require('express'); const path = require('path'); const indexRouter = require('./routes/index'); const apiRouter = require('./routes/api'); const hostname = 'localhost'; const port = 3000; const app = express(); // ... app.use(express.static('public')); app.use('/', indexRouter); app.use('/api', apiRouter); app.use('*', (req, res) => { res.status(404).render('404', { url: req.originalUrl }); }); // ...
是否是瞬间清爽了不少呢!若是你服务器还开着,能够测试一下以前的路由是否还能成功运行哦。这里我贴一下用 Curl 测试 /api
路由的结果:
$ curl localhost:3000/api {"name":"图雀社区","website":"https://tuture.co"} $ curl -X POST localhost:3000/api/new {"msg":"新的篇章,即将开始"}
至此,这篇教程也就结束了。所完成的网站的确很简单,可是但愿你能从中学到 Express 的两大精髓:路由和中间件。掌握了这两大概念以后,后续进阶教程的学习也会轻松不少哦!
想要学习更多精彩的实战技术教程?来 图雀社区逛逛吧。