环境

  • Ubuntu 22.04
  • Node.js 16.15.1
  • VSCode 1.69.2
  • Firefox 103.0

概述

Express官网对Express的定义是:基于Node.js平台,快速、开放、极简的Web开发框架。

Express是npm上的第三方包,作用和Node.js内置的 http 模块类似,用来创建Web服务器。

安装方法为: npm i express

简单示例

创建文件 0727_9.js 如下:

const express = require('express');

const app = express();

app.get('/index.html', (req, res) => {
    res.send('Success!');
});

app.listen(8080, () => {
    console.log('server started');
});

运行程序,访问服务器:

nodejs express响应Json数据格式 nodejs的express_中间件

可以用 req.query 获取查询参数(比如 ?name=Tom&age=20 ):

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

效果如下:

nodejs express响应Json数据格式 nodejs的express_javascript_02

可以用 req.params 获取请求的路径参数:

app.get('/:xxx/b.html', (req, res) => {
    res.send(req.params);
});

效果如下:

nodejs express响应Json数据格式 nodejs的express_javascript_03

托管静态资源

使用 express.static() 方法,可以很方便的创建一个静态资源服务器。

我们之前通过把MarkDown文档转换为HTML文档,生成了 preview 目录,其中包含了 test.html 文件,和一些css以及js文件。如下:

nodejs express响应Json数据格式 nodejs的express_express_04

创建文件 0727_10.js 如下:

const express = require('express');

const app = express();

app.use(express.static('preview'));

app.listen(8080, () => {
    console.log('server started');
});

效果如下:

nodejs express响应Json数据格式 nodejs的express_node.js_05

也可以定制化访问的URL路径,例如:

app.use('/bbb', express.static('preview'));

则访问时,也需要加上 /bbb 前缀,即: http://localhost:8080/bbb/test.html

Express路由

前面的例子里,我们使用了多个 app.get() 方法,对应不同的请求处理。事实上Express不建议将路由直接挂载到app上,而是推荐将路由抽离为单独的模块。使用 express.Router() 方法创建路由对象,通过 module.exports() 导出路由对象,在外面使用 app.use() 方法注册路由模块。

创建文件 0727_11.js

const express = require('express');

const router = express.Router();

router.get('/users', (req, res) => {
    res.send('get users');
});

router.get('/books', (req, res) => {
    res.send('get books');
});

module.exports = router;

创建文件 0727_12.js

const express = require('express');

const myRouter = require('./0727_11.js');

const app = express();

app.use(myRouter);

app.listen(8080, () => {
    console.log('server started');
});

效果如下:

nodejs express响应Json数据格式 nodejs的express_html_06

nodejs express响应Json数据格式 nodejs的express_node.js_07

同样,也可以定制化访问的URL路径,例如:

app.use('/bbb', myRouter);

则访问时,也需要加上 /bbb 前缀,即: http://localhost:8080/bbb/users

Express中间件

可以在处理请求之前,先做一个或者多个预处理。

注意:一定要在路由之前注册中间件。

全局中间件

全局中间件适用于所有请求。

用法为:

app.use((req, res, next) => {
    ......
    next();
});

创建文件 0727_13.js

const express = require('express');

const app = express();

app.use((req, res, next) => {
    console.log(req.url + ': 1st middleware');
    next();
});

app.use((req, res, next) => {
    console.log(req.url + ': 2nd middleware');
    next();
});

app.use((req, res, next) => {
    console.log(req.url + ': 3rd middleware');
    next();
});

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

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

app.listen(8080, () => {
    console.log('server started');
});

当访问 http://localhost:8080/users 时,输出如下:

/users: 1st middleware
/users: 2nd middleware
/users: 3rd middleware

当访问 http://localhost:8080/books 时,输出如下:

/books: 1st middleware
/books: 2nd middleware
/books: 3rd middleware

可见:

  • 全局中间件适用于所有请求;
  • 可定义多个全局中间件,按其定义顺序执行;

局部中间件

全局中间件针对所有的请求,如果想要只针对指定的请求,可以使用局部中间件。

创建文件 0727_13.js

const express = require('express');

const app = express();

const mw1 = (req, res, next) => {
    console.log(req.url + ': 1st middleware');
    next();
};

const mw2 = (req, res, next) => {
    console.log(req.url + ': 2nd middleware');
    next();
};

const mw3 = (req, res, next) => {
    console.log(req.url + ': 3rd middleware');
    next();
};

app.get('/users', [mw1, mw2], (req, res) => {
    res.send('get users');
});

app.get('/books', [mw2, mw3], (req, res) => {
    res.send('get books');
});

app.listen(8080, () => {
    console.log('server started');
})

本例中,给 /users 请求绑定了 mw1mw2 这两个中间件,给 /books 请求绑定了 mw2mw3 这两个中间件。

当访问 http://localhost:8080/users 时,输出如下:

/users: 1st middleware
/users: 2nd middleware

当访问 http://localhost:8080/books 时,输出如下:

/books: 2nd middleware
/books: 3rd middleware

可见,只有绑定的中间件才会生效。

中间件的分类

  • 应用级别的中间件
  • 路由级别的中间件
  • 错误级别的中间件
  • Express内置的中间件
  • 第三方中间件

应用级别的中间件

通过 app.use()app.get()app.post() 绑定到 app 实例上的中间件。

前面的例子,都是应用级别的中间件。

路由级别的中间件

绑定到 express.Router() 上的中间件,其用法和应用级别中间件类似。

创建文件 0727_15.js 如下:

const express = require('express')

const app = express();

const router = express.Router();

router.use((req, res, next) => {
    console.log(req.url + ': 1st router');
    next();
});

router.use((req, res, next) => {
    console.log(req.url + ': 2nd router');
    next();
});

router.get('/users', (req, res) => {
    res.send('get users');
});

router.get('/books', (req, res) => {
    res.send('get books');
});

app.use(router);

app.listen(8080, () => {
    console.log('server started');
});

当访问 http://localhost:8080/users 时,输出如下:

/users: 1st router
/users: 2nd router

当访问 http://localhost:8080/books 时,输出如下:

/books: 1st router
/books: 2nd router

错误级别的中间件

注意:需要注册在所有路由之后,这是和其它中间件不同的。

创建文件 0727_16.js 如下:

const express = require('express');

const app = express();

app.get('/', (req, res) => {
    console.log('get it');
    throw new Error('Something is wrong!');
    res.send('Can you get it?');
});

app.use((err, req, res, next) => {
    console.log('Error occurred');
    console.log(err.message);

    res.send('Sorry but an error occurred');
});

app.listen(8080, () => {
    console.log('server started');
});

运行程序,访问 http://localhost:8080 ,如下:

nodejs express响应Json数据格式 nodejs的express_node.js_08

输出如下:

get it
Error occurred
Something is wrong!

Express 内置的中间件

比如 express.static() ,在前面提到过。

第三方的中间件

需要显式安装的三方包。

跨域请求

创建文件 0727_17.js 如下:

const express = require('express');
//const cors = require('cors');

const app = express();

//app.use(cors());

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

app.listen(8080, () => {
    console.log('server started');
});

效果如下:

nodejs express响应Json数据格式 nodejs的express_express_09

创建文件 0727_17.html 如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="lib/jquery-3.6.0.min.js"></script>
</head>
<body>
    <button id="btn1">OK</button>
    <script>
        $('#btn1').on('click', function () {
            $.get('http://localhost:8080/users', function (res) {
                console.log(res);
            });
        });
    </script>
</body>
</html>

点击 OK 按钮会报错:

nodejs express响应Json数据格式 nodejs的express_html_10

报错原因:这是一个跨域请求,因为二者的协议不同( file VS. http )。

解决办法有 CORSJSONP

CORS

首先要安装 cors

➜  temp0727 npm i cors

added 2 packages, and audited 60 packages in 2s

7 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

0727_17.js 文件中添加两行代码(参考注释的代码):

const cors = require('cors');

app.use(cors());

注意要放在 app.get() 前面。

重启服务,现在点击 OK 按钮就不报错了:

nodejs express响应Json数据格式 nodejs的express_express_11

查看Response Headers,如下:

nodejs express响应Json数据格式 nodejs的express_express_12

可见,多了 Access-Control-Allow-Origin: *

注意:浏览器貌似有缓存,如果此时删掉那两行代码,然后重启服务,刷新浏览器,点击 OK 按钮,仍然能成功。只有关闭并重新打开浏览器,点击 OK 按钮才会失败。

客户端在请求cors接口时,可以将cors的请求分为两大类:

  • 简单请求
  • 预检请求

同时满足以下两大条件的请求,就属于简单请求:

  • 请求方式: GET 或者 POST 或者 HEAD
  • 头部信息:
  • 无自定义头部字段
  • Accept
  • Accept-Language
  • Content-Language
  • DPR
  • Downlink
  • Save-Data
  • Viewport-Width
  • Width
  • Content-Type(只有三个值application/x-www-form-urlencoded、multipart/form-data、text/plain)

其它请求则属于预检请求。

对于预检请求,浏览器会先发送一个 OPTIONS 请求进行预检,以获知服务器是否允许该实际请求,服务器成功响应预检请求后,才会发送真正的请求。

创建文件0727_18.js 如下:

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

const app = express();

app.use(cors());

app.delete('/users', (req, res) => {
    res.send('delete users');
});

app.listen(8080, () => {
    console.log('server started');
});

创建文件 0727_18.html 如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="lib/jquery-3.6.0.min.js"></script>
</head>
<body>
    <button id="btn1">OK</button>
    <script>
        $('#btn1').on('click', function () {
            $.ajax({
                type: 'DELETE',
                url: 'http://localhost:8080/users',
                success: function (res) {
                    console.log(res);
                }
            });
        });
    </script>
</body>
</html>

效果如下:

nodejs express响应Json数据格式 nodejs的express_node.js_13

可见,对于 DELETE 请求,浏览器先发送了一个 OPTIONS 请求,然后才发送真正的 DELETE 请求。

JSONP

创建文件 0727_19.js 如下:

const express = require('express');

const app = express();

app.get('/jsonp', (req, res) => {
    const funcName = req.query.callback;
    const data = {name: 'Tom', age: 20};
    const str = `${funcName}(${JSON.stringify(data)})`;
    res.send(str);
})

app.listen(8080, () => {
    console.log('server started');
});

创建文件 0727_19.html 如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="lib/jquery-3.6.0.min.js"></script>
</head>
<body>
    <button id="btn1">OK</button>
    <script>
        $('#btn1').on('click', function () {
            $.ajax({
                type: 'GET',
                url: 'http://localhost:8080/jsonp',
                dataType: 'jsonp',
                success: function (res) {
                    console.log(res);
                }
            });
        });
    </script>
</body>
</html>

效果如下:

nodejs express响应Json数据格式 nodejs的express_javascript_14

nodejs express响应Json数据格式 nodejs的express_html_15