一杯茶的时间,上手 Express 框架开发

Node.js 已经成为 Web 后台开发圈一股不容忽视的力量,凭借其良好的异步性能、丰富的 npm 库以及 JavaScript 语言方面的优点,已经成为了不少大公司开发其后台架构的重要技术之一,而 Express 框架则是其中知名度最高、也是最受欢迎的后端开发框架。在这篇教程中,你将了解 Express 在 Node 内置 http 模块的基础上作了怎样的封装,并掌握路由和中间件这两个关键概念,学习和使用模板引擎、静态文件服务、错误处理和 JSON API,最终开发出一个简单的我的简历网站。javascript

此教程属于Node.js 后端工程师学习路线的一部分,欢迎来 Star 一波,鼓励咱们继续创做出更好的教程,持续更新中~。css

旧时代:用内置 http 模块实现一个服务器

自从 Ryan Dahl 在 2009 年的 JSConf 正式推出 Node.js 平台后,这门技术的使用率就如同坐了火箭通常迅速上升,成为了最受喜好的后端开发平台之一,而 Express 则是其中最为耀眼的 Web 框架。在正式开始这篇教程以前,咱们将列举一下这篇教程所须要的预备知识、所用技术和学习目标。html

预备知识

本教程假定你已经知道了:前端

  • JavaScript 语言基础知识(包括一些经常使用的 ES6+ 语法)
  • Node.js 基础知识,特别是异步编程(这篇教程主要用到的是回调函数)和 Node 模块机制,还有 npm 的基本使用,能够参考这篇教程进行学习
  • HTTP 协议基础知识,浏览器和服务器之间是如何互动的

所用技术

  • Node.js:8.x 及以上
  • npm:6.x 及以上
  • Express.js:4.x

学习目标

读完这篇教程后,你将学会java

  • Express 框架的两大核心概念:路由和中间件
  • 使用模板引擎渲染页面,并接入 Express 框架中
  • 使用 Express 的静态文件服务
  • 编写自定义的错误处理函数
  • 实现一个简单的 JSON API 端口

提示node

虽然数据库是后端开发中很是重要的环节,但 Express 并不内置处理数据库的模块,须要额外的第三方库提供支持。这篇教程将重点放在了 Express 相关的概念讲解上,所以不会涉及数据库的开发。在学完这篇教程后,你能够浏览 Express 相关的进阶教程linux

用内置 http 模块建立服务器

在讲解 Express 以前,咱们先了解一下怎么用 Node.js 内置的 http 模块来实现一个服务器,从而可以更好地了解 Express 对底层的 Node 代码作了哪些抽象和封装。若是你尚未安装 Node.js,能够去官方网站下载并安装。git

咱们将实现一个我的简历网站。建立一个文件夹 express_resume,并进入其中:github

mkdir express_resume && cd express_resume
复制代码

建立 server.js 文件,代码以下:web

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,上面的代码含义很清晰:

  1. 导入 http 模块
  2. 指定服务器的主机名 hostname 和端口号 port
  3. http.createServer 建立 HTTP 服务器,参数为一个回调函数,接受一个请求对象 req 和响应对象 res,并在回调函数中写入响应内容(状态码 200,类型为 HTML 文档,内容为 Hello World
  4. 在指定的端口开启服务器

最后运行 server.js:

node server.js
复制代码

用浏览器打开 localhost:3000,能够看到 Hello World 的提示:

能够发现,直接用内置的 http 模块去开发服务器有如下明显的弊端:

  • 须要写不少底层代码——例如手动指定 HTTP 状态码和头部字段,最终返回内容。若是咱们须要开发更复杂的功能,涉及到多种状态码和头部信息(例如用户鉴权),这样的手动管理模式很是不方便
  • 没有专门的路由机制——路由是服务器最重要的功能之一,经过路由才能根据客户端的不一样请求 URL 及 HTTP 方法来返回相应内容。可是上面这段代码只能在 http.createServer 的回调函数中经过判断请求 req 的内容才能实现路由功能,搭建大型应用时力不从心

由此就引出了 Express 对内置 http 的两大封装和改进:

  • 更强大的请求(Request)和响应(Response)对象,添加了不少实用方法
  • 灵活方便的路由的定义与解析,可以很方便地进行代码拆分

接下来,咱们将开始用 Express 来开发 Web 服务器!

新时代:用 Express 搭建服务器

在第一步中,咱们把服务器放在了一个 JS 文件中,也就是一个 Node 模块。从如今开始,咱们将把这个项目变成一个 npm 项目。输入如下命令建立 npm 项目:

npm init
复制代码

接着你能够一路回车下去(固然也能够仔细填),就会发现 package.json 文件已经建立好了。而后添加 Express 项目依赖:

npm install express
复制代码

在开始用 Express 改写上面的服务器以前,咱们先介绍一下上面提到的两大封装与改进

更强大的 Request 和 Response 对象

首先是 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 请求方法,包括 getpostputdelete 等等
  • PATH 是客户端访问的 URI,例如 //about
  • HANDLER 是路由被触发时的回调函数,在函数中能够执行相应的业务逻辑

正式实现

到了动手的时候了,咱们用 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 方法开启服务器。

像第一步那样再次运行 server.js,一样能够看到 Hello World 的内容,可是代码却简单明了了很多。

提示

若是以前的服务器还开着,记得按 Ctrl+C 关掉哦。每次修改代码以后,都要手动关掉服务器而后再次运行才能生效。在后续的进阶教程中,咱们会教你用更先进的工具边修改代码边检查效果!

编写第一个中间件

接下来咱们开始讲解 Express 第二个重要的概念:中间件(Middleware)。

理解中间件

中间件并非 Express 独有的概念。相反,它是一种广为使用的软件工程概念(甚至已经延伸到了其余行业),是指将具体的业务逻辑和底层逻辑解耦的组件(可查看这个讨论)。换句话说,中间件就是可以适用多个应用场景、可复用性良好的代码。

Express 的简化版中间件流程以下图所示:

首先客户端向服务器发起请求,而后服务器依次执行每一个中间件,最后到达路由,选择相应的逻辑来执行。

提示

这个是一个简化版的流程描述,目的是便于你对中间件有个初步的认识,在后面的章节中咱们将进一步完善这一流程。

有两点须要特别注意:

  • 中间件是按顺序执行的,所以在配置中间件时顺序很是重要,不能弄错
  • 中间件在执行内部逻辑的时候能够选择将请求传递给下一个中间件,也能够直接返回用户响应

Express 中间件的定义

在 Express 中,中间件就是一个函数:

function someMiddleware(req, res, next) {
  // 自定义逻辑
  next();
}
复制代码

三个参数中,reqres 就是前面提到的 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 的异步事件循环,下降服务器的吞吐率。在实际生产中,推荐使用第三方优秀的日志中间件,例如 morganwinston 等等。

运行服务器,而后用浏览器尝试访问各个路径。这里我访问了首页(localhost:3000)和 /hellolocalhost: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> 复制代码

再次运行服务器,并访问咱们的网站。首页以下:

联系咱们页面以下:

能够看到样式表和图片都成功加载出来了!

处理 404 和服务器错误

人有悲欢离合,月有阴晴圆缺,服务器也有出错的时候。HTTP 错误通常分为两大类:

  • 客户端方面的错误(状态码 4xx),例如访问了不存在的页面(404)、权限不够(403)等等
  • 服务器方面的错误(状态码 5xx),例如服务器内部出现错误(500)或网关错误(503)等等

若是你打开服务器,访问一个不存在的路径,例如 localhost:3000/what,就会出现这样的页面:

很显然,这样的用户体验是很糟糕的。

在这一节中,咱们将讲解如何在 Express 框架中处理 404(页面不存在)及 500(服务器内部错误)。在此以前,咱们要完善一下 Express 中间件的运做流程,以下图所示:

这张示意图和以前的图有两点重大区别:

  • 每一个路由定义本质上是一个中间件(更准确地说是一个中间件容器,可包含多个中间件),当 URI 匹配成功时直接返回响应,匹配失败时继续执行下一个路由
  • 每一个中间件(包括路由)不只能够调用 next 函数向下传递、直接返回响应,还能够抛出异常

从这张图就能够很清晰地看出怎么实现 404 和服务器错误的处理了:

  • 对于 404,只需在全部路由以后再加一个中间件,用来接收全部路由均匹配失败的请求
  • 对于错误处理,前面全部中间件抛出异常时都会进入错误处理函数,可使用 Express 自带的,也能够自定义。

处理 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

在这篇教程的最后,咱们将实现一个很是简单的 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 端口,看到返回了想要的数据:

或者你能够用 PostmanCurl 访问,也能看到想要的数据哦。

至此,这篇教程也就结束了。所完成的网站的确很简单,可是但愿你能从中学到 Express 的两大精髓:路由和中间件。掌握了这两大概念以后,后续进阶教程的学习也会轻松不少哦!

想要学习更多精彩的实战技术教程?来图雀社区逛逛吧。

相关文章
相关标签/搜索