使用 Node.js 和 Express 构建 Web API

Web API

使用 Web 作为运行应用程序的平台:任何人都可以使用遵守 HTTP浏览器、客户端、软件访问你的应用程序

  • Node.js 有一个名为 HTTP 的核心模块,可帮助构造 Web 应用程序
  • HTTP 模块的速度不及使用框架那么快,Node.js 有许多 Web 框架,例如 Hapi、Fastify、Koa、Express

对 Web 中数据的基本概念

  • 数据存储:一般在文件系统数据库
  • 数据访问:使用 HTTP 的 Web 应用和 API 来提供该数据

在生成 Web 应用程序和 API 时需要考虑:

  • 路由:应用程序根据 URL 地址的不同部分划分为不同的部分,例如 https://baike.baidu.com/item/ 中路径部分 /item
  • 支持不同内容类型:要提供的数据可能以不同的格式存在
  • 身份验证/授权:某些数据可能是敏感数据
  • 读取/写入数据:用户通常需要同时查看和向系统添加数据
  • 成本:若要高效地创建 Web 应用程序和 API,请选择为常见问题提供解决方案的工具和库

Node.js 中的 http 模块

  • 帮助管理请求的对象

    • http.Server: 表示 HTTP Server 的实例,需要指示此对象侦听特定端口地址上的不同事件
    • http.IncomingMessage: 此对象是由 http.Server 或 http.ClientRequest 创建的可读流,使用它访问状态、标头和数据
    • http.ServerResponse: 此对象是 HTTP 服务器在内部创建的流,此类定义响应应有的外观
  • 示例

    const http = require('http');
    const PORT = 3000;
    
    // createServer() 方法创建 http.Server 类的实例,其中箭头函数是 createServer() 需要实现的回调函数
    //   1. 箭头函数参数列表:req,res 分别表示请求和响应
    //   2. 箭头函数的函数体:需要对请求和响应进行处理,处理根据项目需求而定
    //   3. 不了解请求或响应,建议温习或学习计算机网络相关知识,也可单独学习 HTTP 协议的相关内容
    const server = http.createServer((req, res) => {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('hello world');
    });
    
    // 开始侦听请求,此时服务器即可接受客户端请求
    server.listen(PORT, () => {
    console.log(`listening on port ${PORT}`)
    })
    
  • Node.js 流

    流 (stream) 不是 Node.js 概念,而是操作系统概念,它定义数据来回传输的方式

    流是 Node.js 中的基本数据结构

    • 可以读取和写入数据
    • 可以发送和接收消息或事件

    之前的 reqres 参数都是流

    • 使用 on() 方法侦听来自客户端请求的传入数据
    • 使用 end() 方法发送回客户端的数据

创建 Express 框架 Web 应用程序

Express 框架

为什么要将 Express 作为构建下一个应用的框架?

  • 过多便捷高效的功能
  • 使复杂性抽象化,使整个开发体验变得更加容易
  • 解决常见的 Web 问题,有助于解决路由管理、缓存、重定向等
  • 由数百万开发者信任,意味着维护性和保障

Express 中的路由管理

  • Express 框架使用 URL、路由、HTTP 谓词进行路由管理

  • Express 可帮助注册路由,并将它们与适当的 HTTP 谓词配对以组织 Web 应用程序

  • 示例

    // 该请求具有与 HTTP 谓词 get 关联的地址 /products
    app.get('/products', (req, res) => {
        // handle the request
    })
    
    // 看待 get 对 /products 和 post 对 /products 不同
    app.post('/products', (req, res) => {
        // handle the request
    })
    

Express 支持许多不同的内容格式,这些可以返回给调用客户端

  • 若要返回纯文本,可使用 send() 方法

  • 对于 JSON 等其他类型的数据,可通过 json() 方法确保内容类型和数据转换正确

  • 示例

    // Express 框架
    res.send('plain text')
    res.json({ id: 1, name: "Catcher in the Rye" })
    
    // 原生
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ id: 1, name: "Catcher in the Rye" }));
    

若要开始使用 Express 框架开发 Node.js 应用程序,建议首先初始化 Node.js 项目以便下载的任何依赖项最后都在 package.json 文件中(这是针对为 Node.js 运行时开发的应用的一般建议),Express 框架创建 Web 应用程序的基本步骤:

  • 实例化应用
  • 定义路由和路由处理程序
  • 配置中间件
  • 启动应用

Express 示例

一个基本的 Express Web 程序

  • 初始化项目:在命令行随便创建一个目录 express-web,进入目录执行 npm init -y && npm install express,其中 && 表示前一个命令顺利执行后一个才能执行

  • 在 package.json 文件中确认 express 安装

    "dependencies": {
        "express": "^4.18.2"
    }
    
  • 创建服务端,先创建名为 app.js 的文件

    /**
     * app.js
    */
    const express = require('express');
    
    const port = 3000;
    // 通过调用 express() 方法创建 Express 应用程序的实例
    const app = express();
    
    // 设置 Web 应用的根路由 '/',访问根路由会响应文本 'Hello World!'
    app.get('/', (req, res) => res.send('Hello World!'));
    
    // 通过调用 listen() 方法启动 Web 应用
    app.listen(port, () => console.log(`Example app listening on port ${port}!`));
    
  • 在终端中,运行 node app.js 命令以启动 Express Web 应用

    • 应会看到以下输出 Example app listening on port 3000!,此时程序是处于监听状态
    • 在浏览器中访问 http://localhost:3000,会看到以下输出 Hello World!
  • 在终端中,按 Ctrl+C 即可停止 Express Web 程序

Web 应用返回 JSON 数据

  • 打开 app.js 文件,在原有的基础上添加以下代码

    app.get("/products", (req,res) => {
        const products = [
            {
                id: 1,
                name: "hammer",
            },
            {
                id: 2,
                name: "screwdriver",
            },
            {
                id: 3,
                name: "wrench",
            },
        ];
    
        res.json(products);
    });
    
  • 再次运行 app.js,浏览器访问 http://localhost:3000/products,可以看到

    [{"id":1,"name":"hammer"},{"id":2,"name":"screwdriver"},{"id":3,"name":"wrench"}]
    

使用中间件管理请求生命周期

当请求到达 Web 应用程序时,你可能需要验证用户是否已登录或是否允许他们查看特定资源

考虑将请求作为一系列步骤来处理

  • pre 请求:调查用户是否通过请求标头发送了正确的凭据
  • 构造响应:与某种数据源(如数据库或终结点)通信
  • post 请求: 一个可选步骤,用于在请求处理后运行一段代码

Express 框架对以此方式处理请求提供内置支持

  • Express 中的 pre 或 post 请求称为“中间件”

  • 若要运行 pre 或 post 请求,需要对 Express 实例化对象实现 use() 方法

  • 示例

    app.use((req, res, next) => {});
    
    • req: 包含请求标头和调用 URL 的传入请求
    • res: 用于写入要发送回调用客户端的标头和数据等信息的响应流
    • next: 指示请求正常并已准备好处理的参数,如果未调用 next() 则请求的处理将停止

如果你的路由从中间件运行 pre 或 post 请求中生效,请设置它:

  • 需要在请求之前运行的中间件(pre 请求)定义在实际请求前
  • 需要在请求之后运行的中间件(post 请求)定义在实际请求后
// 有效设置
app.use((req, res, next) => {
    // Pre request,进行验证
})
app.get('/protected-resource', () => {
    // Handle the actual request,Pre 中验证通过方可执行
})
app.use((req, res, next) => {
    // Post request,可选
})

// 没有使用
app.get('/login', () => {})

// 将 pre 请求中间件代码作为处理请求的参数来运行
app.get(
    '/route',
    () => {
        // Pre request middleware
    },
    () => {
        // Handle the actual request
    }
);

实际中使用的身份验证/授权功能需要比我们的示例更可靠一些,有必要了解 OAuth、JSON Web 令牌、JWT 等概念和库 bcrypt,以确保为应用提供适当的保护级别

在 Node.js 中使用 JavaScript 路由管理

URL 是用户在浏览器等客户端中输入的用于查找特定服务器和特定资源的地址

scheme:[//authority]path[?query][#fragment]
  • Scheme: 协议
  • authority: 颁发机构,域名、主机、IP
  • path: 路径部分包含零至多个段,是服务器中以站点为根路径
  • query: 可以通过请求特定页面中的多条记录来进一步筛选数据
  • fragment: 请求内容更加具体

路由是 URL 的一个分段,通常指向特定资源

  • Express 定义路由,并将不同处理程序与它们关联

    // 路由参数写入请求对象 req 上的 params 属性中
    app.get('/products/:id', (req, res) => {
        // 如果请求 /products/144,那么 req.params.id=144 
    })
    
  • Express 具有一种处理查询参数的简单方法

// 查询参数会写入到 res 请求对象上的 query 查询对象
app.get('/products', (req, res) => {
    // 如果请求 /products?page=1&pageSize=20,那么 res.query 等价 {page: 1, pageSize: 20}
})

实现对读取和写入数据的支持

在 products 资源上实现简单的 CRUD API

const express = require('express')
const app = express()
const port = 3000

// 导入正文分析器,需要将传入数据转换为可读的格式
let bodyParser = require('body-parser');
// 将传入的正文数据分析为预期格式
app.use(bodyParser.json());

// 存储数据,但一般此处是由数据库
let products = [];

// C 创建
app.post('/products', function (req, res) {
    const newProduct = { ...req.body, id: products.length + 1 }
    products = [ ...products, newProduct]
    res.json(newProduct);
});

// R 读取
app.get('/products', (req, res) => {
    res.json(products);
})

// U 更新
app.put('/products', function (req, res) {
    let updatedProduct;
    products = products.map(p => {
        if (p.id === req.body.id) {
            updatedProduct = { ...p, ...req.body };
            return updatedProduct;
        }
        return p;
    })
    res.json(updatedProduct);
});

// D 删除
app.delete('/products/:id', function (req, res) {
    const deletedProduct = products.find(p => p.id === +req.params.id);
    products = products.filter(p => p.id !== +req.params.id);
    res.json(deletedProduct);
});

// 启动,进行监听
app.listen(port, () => console.log(`Example app listening on port ${port}!`))

Express 有一种 route() 方法,可以对代码进行分组使其更易于阅读

const express = require('express')

const app = express()
const port = 3000

let bodyParser = require('body-parser');
app.use(bodyParser.json());

let products = [];

// 下面不同处
app.route('/products')
    .get((req, res) => {
        res.json(products);
    })
    .post((req, res) => {
        const newProduct = { ...req.body, id: products.length + 1 }
        products = [...products, newProduct]
        res.json(newProduct);
    })
    .put((req, res) => {
        let updatedProduct;
        products = products.map(p => {
            if (p.id === req.body.id) {
                updatedProduct = { ...p, ...req.body };
                return updatedProduct;
            }
            return p;
        })
        res.json(updatedProduct);
    })
    .delete((req, res) => {
        const deletedProduct = products.find(p => p.id === +req.body.id);
        products = products.filter(p => p.id !== +req.body.id);
        res.json(deletedProduct);
    });

app.listen(port, () => console.log(`Example app listening on port ${port}!`))