# 使用 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 的数据,并支持筛选和分页功能。
- 支持的筛选条件包括
name
和type
- 如果用户提供了
name
参数,SQL 查询将筛选 Biomarker 名称中包含该名称的记录(使用LIKE
模糊匹配)。 - 如果用户提供了
type
参数,SQL 查询将只返回指定类型的 Biomarker。
- 如果用户提供了
limit
和offset
用于控制分页,默认limit
为 10。
实现细节:
- 构造 SQL 查询,并根据用户提供的参数动态添加条件。
params
数组用于存储每个查询参数,以避免 SQL 注入。- 使用
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 的
Category
、Source
和Application
的去重列表,供前端构建筛选器界面。
实现细节:
- 构建
queries
数组,为每个筛选条件(如Category
、Location
等)定义一个 SQL 查询。 - 使用
Promise.all
同时执行多个数据库查询,以获取每个字段的去重数据。 - 将查询结果整合到一个对象
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,并返回相应的详细数据。
实现细节:
- 从
req.params
中获取id
,并构造 SQL 查询以匹配该 ID。 - 如果查询结果为空,则返回 404 错误,表示没有找到对应的 Biomarker。
- 否则返回该 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
运行。