Express 是 Node.js 中用的比较多的一个 Web 应用框架。使用 Express 你可以很方便的创建 HTTP 服务器和处理各种 HTTP 请求,无论是编写网站还是 API 服务,Express 都是一个不错的选择。

Express 是一个灵活可扩展的框架,Express 并不会集成所有的 HTTP 服务器功能,一些功能需要通过安装第三方中间件的方式实现。操作 Cookie 和 Session 之类的就需要通过安装中间件来实现。

创建一个简单的 HTTP 服务

初始化项目:

npm init -y

使用 npm 安装 Express:

npm install express --save

在跟目录创建一个 index.js 作为入口文件,启动一个 HTTP 服务:

const express = require('express');
const app = express();

// 配置请求响应
app.get('/', (req, res) => {
  res.send('Hello Express');
});

// 启动服务
app.listen(80, () => {
  console.log('server start http:127.0.0.1');
});

在命令行中输入 node index.js 运行,成功运行后命令行会输出 server start http:127.0.0.1

上面启动了一个 HTTP 服务,端口使用的是 80,在浏览器中访问 http://localhosthttp://127.0.0.1 就可以看到 res.send 输出的 Hello Express

路由

路由主要包括客户端请求的路径和方式,你可以根据不同的请求路径或请求方式返回不同的内容。

我上面的 app.get 就是处理 GET 请求,第一个参数是路径,/ 就是根路径,直接访问域名或 IP ,默认就是根路径,第二个参数就是处理请求的回调函数。

回调函数可以接收两个参数,req (Request)和 res (Response)。Request 就是客户端信息,客户端传过来的数据和 Cookie 之类的就需要通过 Request 获取。Response 就是服务端回复,你需要返回数据的时候,包括 body 数据、header 、状态码之类的都可以通过 Response 来设置。

Express 也可以使用 postputdelete 方法来处理对应的请求,使用方式和 get 是一样的:

const express = require('express');
const app = express();

// 配置请求响应
app.get('/', (req, res) => {
  res.send('get');
});

// 处理 post 请求
app.post('/', (req, res) => {
  res.send('post');
});

// 处理 put 请求
app.put('/', (req, res) => {
  res.send('put');
});

// 处理 delete 请求
app.delete('/', (req, res) => {
  res.send('delete');
});

一般 GET 请求用来获取数据,POST 请求用来新增数据,PUT 请求用来修改数据,DELETE 请求用来删除数据。

不过很多 API 为了方便,也不会区分的那么细,大多数都是使用 GET 和 POST 请求。

Express 还提供了一个 all 来处理所有 HTTP 请求,使用方法和 getpost 是差不多的。

路由路径匹配

路由路径可以是各种符合 URL 地址标准的字符,结尾也可以是文件名后缀,如果你愿意的话,路径还可以是 .php 结尾:

const express = require('express');
const app = express();

app.get('/about', (req, res) => {
  res.send('关于网站');
});

app.get('/archives/1.html', (req, res) => {
  res.send('1');
});

app.get('/index.php', (req, res) => {
  res.send('这是一个 PHP 网页');
});

路由路径也可以使用正则表达式来匹配处理:

const express = require('express');
const app = express();

// 匹配路径中包含 q  的请求
app.get(/q/, (req, res) => {
  res.send('路径中包含q');
});

// 匹配 .db 结尾的路径
app.get(/\.db$/, (req, res) => {
  res.send('你想干嘛?');
});

// 匹配 archives/数字/ 的路径
app.get(/\/archives\/\d+\//, (req, res) => {
  // 输出路由路径中的数字
  res.send(/\d+/.exec(req.path)[0]);
});

也可以使用 路径:id 的方式来匹配数字结尾的请求:

const express = require('express');
const app = express();

// 匹配 archives/数字ID
app.get('/archives/:id', (req, res) => {
  // 输出路由路径中的数字
  res.send(/\d+/.exec(req.path)[0]);
});

上面的路径匹配访问 地址/archives/12地址/archives/12/ 都可以匹配到,结尾有没有 / 都能匹配到。

响应请求

在收到 HTTP 请求后就需要响应请求,否则客户端请求会被挂起。

下面是 res (Response)用于响应请求的一些方法:

res.send

send 可用于响应各种 HTTP 请求,响应 body 可以是 String 字符串、Buffer、对象:

const express = require('express');
const app = express();
const fs = require('fs');

app.get('/img', (req, res) => {
  // 读取文件
  const file = fs.readFileSync('logo.png');
  // 传入文件 Buffer
  res.send(file);
});

app.get('/json', (req, res) => {
  res.send({name: 'Mr. Ma', age: 17});
});

app.get('/', (req, res) => {
  res.send('My blog www.misterma.com');
});

如果传入的是字符串,headerContent-type 就是 text/html ,传入 Buffer,Content-type 就是 application/octet-stream ,传入对象,Content-type 就是 application/json

我上面的读取文件为 Buffer 只是简单演示,用的是同步的读取文件方式,一般服务器中不会使用同步的方式读取文件,下载文件也不会读取整个文件。

res.end

end 用于在不返回数据的情况下结束响应:

app.get('/', (req, res) => {
  res.end('not found');
  res.status(404).end();
});

end 也可以返回 String 字符串,但需要返回数据的话,建议用 sendjson

res.json

json 可以发送 JSON 响应,可以传入对象或数组:

res.json({name: 'Mr. Ma', age: 17});

Express 会自动进行 JSON.stringify 和设置正确的 Content-type

res.jsonp

因为同源策略的限制,浏览器端是不能跨域请求的,JSONP 就是解决跨域请求的一种方式。

jsonp 和 json 的使用是差不多的,传入对象,返回转换后的 JSON:

res.jsonp({name: 'Mr. Ma', age: 17});

res.download

download 可以用于响应下载文件请求,第一个参数是文件路劲,第二个参数是下载时使用的文件名,下载时使用的文件名可以和真实的文件名不一样:

const path = require('path');

app.get('/download', (req, res) => {
  res.download(path.join(__dirname, 'logo.png'), '图片.png');
});

download 也可以接收第三个参数,第三个参数是发生错误时的回调函数:

app.get('/download', (req, res) => {
  res.download(path.join(__dirname, 'logo.png'), '图片.png', error => {
    // 返回状态码
    res.end(`${error.statusCode} error`);
  });
});

在文件不存在或没有访问权限的时候就会触发回调函数。

res.redirect

redirect 可以用于重定向,第一个参数是状态码或 URL,如果第一个参数传入了 Numbar 类型的状态码的话,第二个参数就是 URL:

app.get('/', (req, res) => {
  res.redirect(301, 'https://www.misterma.com/');
});

URL 可以是同站的其它路径,也可以是其他网站。

状态码如果省略的话,默认是 302。

res.render

render 可以输出模板渲染,第一个参数是模板的变量,第二个参数是出错的回调函数。

模板引擎可以让你在单独的文件中按照正常的格式来编写 HTML,在模板文件中也可以使用变量或判断语法,在运行的时候,模板引擎会把变量替换为实际的数据并生成 HTML 代码。

Express 的模板引擎需要单独安装,并且有很多模板引擎可以选择。

关于模板引擎的使用,我后面会单独写一篇。

res.sendFile

sendFile 可以返回一个文件,第一个参数是文件路径,第二个参数是可选的出错的回调函数:

const path = require('path');

app.get('/', (req, res) => {
  res.sendFile(path.join(__dirname, 'public/index.html'));
});

sendFile 会根据文件类型来设置 Content-type ,如果是常见浏览器能打开的文件就可以直接在浏览器中显示,如果是浏览器不能打开的文件就会显示文件下载。

sendFile 也可以返回静态的 HTML 文件,如果你的网页是完全的前后端分离的话,也可以使用 sendFile 来输出 HTML 文件。

res.sendStatus

sendStatus 可以直接返回一个状态码,参数就是一个 Numbar 的状态码:

res.sendStatus(404);

处理静态文件

前端网页会用到很多 CSS、JS、图片文件,这些文件没有必要单独编写路由处理,Express 提供了静态文件处理的中间件,可以直接映射到静态资源目录。

下面把项目目录下的 public 设置为静态文件目录:

const express = require('express');
const app = express();
const path = require('path');

// 注册用于静态文件处理的 static 中间件
app.use(express.static(path.join(__dirname, 'public')));

设置完成后 public 目录里的文件都可以通过 地址/文件名 访问到,比如我的 public 目录里有一个 logo.png ,我在本地服务器通过 http://localhost/logo.png 就可以访问到,不需要加目录名。

静态文件目录也可以设置多个目录:

app.use(express.static(path.join(__dirname, 'public')));
app.use(express.static(path.join(__dirname, 'static')));

我上面同时设置了 publicstatic 目录,我要访问这两个目录里的文件也是使用 地址/文件名 ,不需要加目录名。

如果你需要让地址路径和你实际的路径不一样的话,Express 的 static 也可以设置虚拟路径,下面还是把 public 设置为静态文件目录,地址路径设置为 static

app.use('/static', express.static(path.join(__dirname, 'public')));

现在如果我需要访问 public 目录里的 logo.png 我就需要使用 地址/static/logo.png ,比如 http://localhost/static/logo.png ,不能再使用 地址/文件名。我设置的 static 只存在于地址路径中,实际映射到的还是 public 目录。

中间件

Express 的中间件就是一组处理 HTTP 请求的函数。中间件可分为 应用层中间件、路由中间件、错误处理中间件、内置中间件、第三方中间件。上面处理静态文件使用的就是内置中间件,处理 HTTP 请求的 getpost 之类的也是中间件。

Express 使用 use 注册中间件,每一个应用中间件函数都可以接收 req (Request)、res (Response)、next 下一个中间件。

下面是一个简单的应用中间件:

const express = require('express');
const app = express();

app.use((req, res) => {
  res.send('Hello');
});

上面只要是服务器收到请求,不管是 GET 还是 POST,无论路径是什么,都会返回 hello

中间件除了接收 reqres 外,还能接收第三个参数 nextnext 可以把请求传递给下一个中间件:

app.use((req, res, next) => {
  if (req.path === '/') {
    res.send('这是首页');
  }
  // 如果路径不是 / 就传递给下一个中间件执行
  next();
});

app.use((req, res, next) => {
  if (req.path === '/about') {
    res.send('关于');
  }
  next();
});

app.use((req, res) => {
  res.send('不知道你访问的是哪个页面');
});

上面的第一个中间件会判断请求路径,如果路径是 / 就返回 这是首页 ,否则就传给下一个中间件执行,第二个中间件也是一样的,如果不符合条件就再传给下一个中间件执行。

中间件也可以设置路径,只有指定的路由才会触发中间件函数:

app.use('/about', (req, res) => {
  res.send('关于');
});

app.use('/user', (req, res) => {
  res.send('用户信息');
});

路由中间件

Express 还提供了一个单独的路由系统 Router ,通过 Express 的 Router 也可以注册中间件。

下面把 API 请求处理放到单独的 api-router.js 文件中,使用 Express 的 Router 来注册中间件:

const router = require('express').Router();

// 用户相关
router.use('/user', (req, res) => {
  res.send('用户相关');
});

// 文章相关
router.use('/post', (req, res) => {
  res.send('文章相关');
});

module.exports = router;

下面引入 api-router.js 注册使用:

const express = require('express');
const app = express();
const apiRouter = require('./api-router');

app.use('/api', apiRouter);

我的中间件注册到了 /api ,我访问 地址/api/user 就会触发用户相关的中间件,访问 地址/api/post 就会触发文章相关的中间件。