# 使用 Vue 3 + Element Plus 从头开始写一个数据库网站 - 05-API 的设计与实现

由于 PrismJS 尚不支持 Vue 的语法高亮,因此 Vue 代码块均先使用 HTML 的高亮

2024-09-23

# 设计 API 功能

由于这是一个 Biomarker 数据库网站,所以应该具有的基础功能有:

  • 获取 Biomarker 列表,并支持根据条件(如名称和类型)进行筛选;
  • 获取筛选条件,以便前端为用户提供筛选选项;
  • 获取单个 Biomarker 的详细信息;

# 实现 API

# 创建 biomarkers.js 路由文件

首先,我们在 backend/routes/ 目录下创建一个新的路由文件 biomarkers.js

该文件定义了与 Biomarker 相关的 API 路由,并使用 Express 框架来处理 HTTP 请求。

# 导入必要模块

// routes/biomarkers.js
import express from 'express';
import db from '../db.js'; // 引入数据库连接
const router = express.Router();

此部分代码中,导入了 express 和一个数据库连接模块 db ,然后创建了一个路由实例 router ,用于定义各个路由。

# 路由定义与实现

# 路由 1:获取所有 Biomarker 列表(带筛选和分页)

// GET /biomarkers - 获取所有 biomarker,支持筛选和分页
router.get('/', async (req, res) => {
    const { name, type, limit = 10, offset = 0 } = req.query;
    let query = 'SELECT * FROM biomarkers WHERE 1=1';
    const params = [];
    if (name) {
        query += ' AND `Biomarker(abbr.)` LIKE ?';
        params.push(`%${name}%`);
    }
    if (type) {
        query += ' AND type = ?';
        params.push(type);
    }
    query += ' LIMIT ? OFFSET ?';
    params.push(Number(limit), Number(offset));
    try {
        const [biomarkers] = await db.query(query, params);
        res.status(200).json(biomarkers);
    } catch (error) {
        res.status(500).json({ message: error.message });
    }
});

功能说明

  • 该路由用于获取所有 Biomarker 的数据,并支持筛选和分页功能。
  • 支持的筛选条件包括 nametype
    • 如果用户提供了 name 参数,SQL 查询将筛选 Biomarker 名称中包含该名称的记录(使用 LIKE 模糊匹配)。
    • 如果用户提供了 type 参数,SQL 查询将只返回指定类型的 Biomarker。
  • limitoffset 用于控制分页,默认 limit 为 10。

实现细节

  1. 构造 SQL 查询,并根据用户提供的参数动态添加条件。
  2. params 数组用于存储每个查询参数,以避免 SQL 注入。
  3. 使用 db.query 执行 SQL 查询,并将结果返回给客户端。

# 路由 2:获取筛选条件

这里,我们选择先编写获取筛选条件的路由是有原因的

在 Express 中,路由匹配机制是基于路径的顺序进行的,如果某个路由包含动态参数(例如 /:id ),并且它的位置在更具体的路径(如 /filters )之前,那么 Express 会优先匹配动态路由,导致路径 /filters 被错误地解释为 /:id

// GET /biomarkers/filters - 获取筛选条件
router.get('/filters', async (req, res) => {
    const queries = [
        { field: 'Category', query: 'SELECT DISTINCT Category FROM biomarkers WHERE Category IS NOT NULL' },
        { field: 'Source', query: 'SELECT DISTINCT Source FROM biomarkers WHERE Source IS NOT NULL' },
        { field: 'Application', query: 'SELECT DISTINCT Application FROM biomarkers WHERE Application IS NOT NULL' }
    ];
    try {
        const filterResults = await Promise.all(
            queries.map(async ({ field, query }) => {
                const [results] = await db.query(query); // 使用 Promise 版本的 query
                //console.log (`Results for ${field}:`, results); // 打印查询结果
                const data = results.map(row => row[field]);
                return { field, data };
            })
        );
        // 将查询结果整合到一个对象中
        const filters = {};
        filterResults.forEach(({ field, data }) => {
            filters[field] = data;
        });
        //console.log ("Combined Filters Object:", filters);  // 打印最终的 filters 对象
        res.json(filters);
    } catch (error) {
        console.error('Error in /filters route:', error);
        res.status(500).send({ message: error.message });
    }
});

功能说明

  • 该路由用于获取前端筛选 Biomarker 所需的可选条件。
  • 返回数据包括 Biomarker 的 CategorySourceApplication 的去重列表,供前端构建筛选器界面。

实现细节

  1. 构建 queries 数组,为每个筛选条件(如 CategoryLocation 等)定义一个 SQL 查询。
  2. 使用 Promise.all 同时执行多个数据库查询,以获取每个字段的去重数据。
  3. 将查询结果整合到一个对象 filters 中,最终返回给客户端。

# 路由 3:根据 ID 获取 Biomarker 详情

// GET /biomarkers/:id - 根据 ID 获取 biomarker 详情
router.get('/:id', async (req, res) => {
    const { id } = req.params;
    try {
        const [biomarker] = await db.query('SELECT * FROM biomarkers WHERE id = ?', [id]);
        if (biomarker.length === 0) {
            return res.status(404).json({ message: 'Biomarker not found' });
        }
        res.status(200).json(biomarker[0]);
    } catch (error) {
        res.status(500).json({ message: error.message });
    }
});

功能说明

  • 此路由用于获取特定 ID 对应的 Biomarker 详细信息。
  • 通过 id 路径参数识别要查询的 Biomarker,并返回相应的详细数据。

实现细节

  1. req.params 中获取 id ,并构造 SQL 查询以匹配该 ID。
  2. 如果查询结果为空,则返回 404 错误,表示没有找到对应的 Biomarker。
  3. 否则返回该 Biomarker 的详细信息。

#app.js 中注册路由

// app.js
import express from 'express';
import cors from 'cors'; // 引入 CORS 中间件
import biomarkersRouter from './routes/biomarkers.js';
const app = express();
// 使用 CORS 中间件,允许所有来源
app.use(cors());
app.use(express.json()); // 解析 JSON 请求体
// 注册 biomarkers 路由
app.use('/api/biomarkers', biomarkersRouter);
export default app;

# 编辑 bin/www 文件

由于我们使用 ES6 语法, bin/www 文件也需要做一些修改:

// www
import app from '../app.js';
import debugModule from 'debug';
import http from 'http';
const debug = debugModule('backend:server');
// 设置端口
const port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
// 创建 HTTP 服务器
const server = http.createServer(app);
// 监听指定的端口
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
 * Normalize a port into a number, string, or false.
 */
function normalizePort(val) {
    const port = parseInt(val, 10);
    if (isNaN(port)) {
        return val;
    }
    if (port >= 0) {
        return port;
    }
    return false;
}
/**
 * Event listener for HTTP server "error" event.
 */
function onError(error) {
    if (error.syscall !== 'listen') {
        throw error;
    }
    const bind = typeof port === 'string'
        ? 'Pipe ' + port
        : 'Port ' + port;
    switch (error.code) {
        case 'EACCES':
            console.error(bind + ' requires elevated privileges');
            process.exit(1);
            break;
        case 'EADDRINUSE':
            console.error(bind + ' is already in use');
            process.exit(1);
            break;
        default:
            throw error;
    }
}
/**
 * Event listener for HTTP server "listening" event.
 */
function onListening() {
    const addr = server.address();
    const bind = typeof addr === 'string'
        ? 'pipe ' + addr
        : 'port ' + addr.port;
    debug('Listening on ' + bind);
    console.log(`服务器正在运行在 http://localhost:${addr.port}`);
}

# 运行 Express 服务器

进入 backend 目录使用以下命令可以运行服务器:

pnpm start

默认情况下,服务器会在 http://localhost:3000 运行。