
web源码中的路由和接口是如何协作的
打开一个网页时,地址栏里的路径、页面上点击的链接,以及浏览器发出的接口请求,看起来都像“访问一个 URL”。但它们可能由不同的程序处理:前端路由决定显示哪个页面,后端路由决定调用哪段服务器代码,接口则约定前后端如何交换数据。理解这几者的分工,才能读懂 Web 源码,也能避免页面刷新后 404、接口返回 HTML、权限只在前端判断等问题。
本文从一次请求的实际流程出发,说明前端路由、后端路由与接口如何配合,并用 Express 搭建一个可运行的小例子。示例包含列表查询、详情查询和新增操作,也会说明参数校验、权限判断和常见边界。代码面向 Node.js 18 或更高版本,使用 Express 4。
一、先区分三个容易混淆的概念
1. 前端路由:决定浏览器里显示什么
前端路由通常运行在浏览器中。比如访问 /products/42,前端应用根据路径显示商品详情页。现代单页应用(SPA)加载一次 HTML 和 JavaScript 后,页面切换可以由 JavaScript 完成,不必每次都请求一份新的 HTML。
前端路由的职责主要是页面呈现和导航。它可以根据路径选择组件、读取路径参数、执行页面级的登录提示,但它不是服务器的访问控制边界。用户可以绕过页面,直接构造 HTTP 请求访问后端接口,因此不能把前端隐藏按钮或跳转登录页当作权限保护。
2. 后端路由:决定服务器执行什么
后端路由运行在服务器上。服务器收到 HTTP 请求后,会根据请求方法、路径等信息,选择处理函数。例如 GET /api/products/42 可以查找商品,POST /api/products 可以创建商品。
后端路由处理函数一般会依次完成身份认证、权限检查、参数校验、业务逻辑调用和响应生成。实际项目还可能通过路由中间件、控制器、服务层和数据访问层拆分职责。
3. 接口:前后端遵守的请求与响应约定
接口不是某个单独的文件或路由,而是一份通信契约。它说明请求使用什么方法和路径、参数放在哪里、数据结构是什么、成功或失败时返回什么。只写了前端页面和后端处理代码,却没有统一这些约定,双方仍然可能无法协作。
可以用一句话记住三者关系:前端路由负责选择页面,后端路由负责选择服务器处理逻辑,接口负责约定两端交换的数据和行为。
二、一次请求是怎样走完的
用户在前端访问 /products/42 时,常见流程如下:
- 浏览器向服务器请求页面资源。首次访问 SPA 时,服务器通常返回应用入口 HTML,之后浏览器加载 JavaScript 和 CSS。
- 前端路由读取当前路径,匹配商品详情页,并从路径中取得商品编号
42。 - 页面代码向后端发送接口请求,例如
GET /api/products/42。 - 后端路由匹配方法和路径,把请求交给对应处理函数。
- 处理函数检查请求、执行查询,并以 JSON 等约定格式返回结果。
- 前端根据响应更新页面,展示商品、加载状态或错误信息。
因此,同一个浏览器页面可能涉及两种不同请求:获取页面的请求和获取业务数据的接口请求。区分它们,是排查“页面打开正常但数据为空”或“接口被返回一整页 HTML”时的重要起点。
三、路径、方法和参数分别表达什么
HTTP 请求通常由方法、路径、查询字符串、请求头和请求体等部分组成。它们不是可以随意互换的参数容器,应该按语义使用。
- 路径:通常标识资源或资源层级,例如
/api/products/42。 - HTTP 方法:表达对资源的操作意图。常见约定是 GET 查询、POST 创建、PUT 替换、PATCH 局部更新、DELETE 删除。具体语义仍须由接口契约定义。
- 路径参数:通常用于标识某个资源,例如
/products/:id中的id。 - 查询参数:通常用于筛选、排序和分页,例如
/products?page=2&limit=20。 - 请求体:通常承载创建或更新所需的数据,例如 JSON 对象。
- 请求头:用于传递内容类型、认证凭据等元信息;具体格式应由项目约定。
在 HTTP 语义上,GET 请求不应用来执行会改变服务器状态的操作。请求体的支持情况会受到客户端、服务器和中间代理影响,不适合作为 GET 查询参数的通用替代方式。分页参数中的 limit 也不是自动安全的:服务端应自行验证并设置合理上限,不能信任客户端传来的值。
四、搭建一个能运行的 Express 示例
示例提供三个接口:
GET /api/products:读取商品列表,支持搜索、分页。GET /api/products/:id:读取指定商品。POST /api/products:创建商品,演示请求体校验。
先新建目录,初始化项目并安装依赖:
npm init -y
npm install express
在项目根目录创建 server.js,内容如下。这个示例用内存数组保存数据,适合本地演示;服务重启后,新增数据会消失。真实应用通常应把持久化交给数据库,并在服务层处理业务规则。
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json({ limit: '32kb' }));
let products = [
{ id: 1, name: '键盘', price: 299 },
{ id: 2, name: '鼠标', price: 129 },
{ id: 3, name: '显示器', price: 1599 }
];
function parsePositiveInteger(value) {
if (!/^[1-9]\d*$/.test(String(value))) return null;
const number = Number(value);
return Number.isSafeInteger(number) ? number : null;
}
app.get('/api/products', (req, res) => {
const q = typeof req.query.q === 'string' ? req.query.q.trim() : '';
const page = parsePositiveInteger(req.query.page ?? '1');
const limit = parsePositiveInteger(req.query.limit ?? '10');
if (!page || !limit || limit > 50) {
return res.status(400).json({
error: 'INVALID_PAGINATION',
message: 'page 必须为正整数,limit 必须为 1 到 50'
});
}
const filtered = products.filter(product =>
product.name.toLowerCase().includes(q.toLowerCase())
);
const start = (page - 1) * limit;
const items = filtered.slice(start, start + limit);
res.json({
items,
page,
limit,
total: filtered.length
});
});
app.get('/api/products/:id', (req, res) => {
const id = parsePositiveInteger(req.params.id);
if (!id) {
return res.status(400).json({
error: 'INVALID_ID',
message: 'id 必须为正整数'
});
}
const product = products.find(item => item.id === id);
if (!product) {
return res.status(404).json({
error: 'NOT_FOUND',
message: '商品不存在'
});
}
res.json({ item: product });
});
app.post('/api/products', (req, res) => {
const body = req.body;
if (!body || typeof body !== 'object' || Array.isArray(body)) {
return res.status(400).json({
error: 'INVALID_BODY',
message: '请求体必须是 JSON 对象'
});
}
const name = typeof body.name === 'string' ? body.name.trim() : '';
const price = body.price;
if (name.length < 1 || name.length > 80) {
return res.status(400).json({
error: 'INVALID_NAME',
message: 'name 长度必须为 1 到 80 个字符'
});
}
if (typeof price !== 'number' || !Number.isFinite(price) || price < 0) {
return res.status(400).json({
error: 'INVALID_PRICE',
message: 'price 必须是非负有限数字'
});
}
const product = {
id: products.reduce((max, item) => Math.max(max, item.id), 0) + 1,
name,
price
};
products.push(product);
res.status(201).json({ item: product });
});
app.use('/api', (req, res) => {
res.status(404).json({
error: 'API_NOT_FOUND',
message: '接口不存在'
});
});
app.listen(PORT, () => {
console.log(`Server listening at http://localhost:${PORT}`);
});
运行服务:
node server.js
用浏览器打开 http://localhost:3000/api/products,或使用命令行验证:
curl "http://localhost:3000/api/products?q=鼠标&page=1&limit=10"
curl "http://localhost:3000/api/products/2"
curl -X POST "http://localhost:3000/api/products" \
-H "Content-Type: application/json" \
-d '{"name":"耳机","price":399}'
注意,以上路由的前缀是 /api。这个前缀是项目约定,方便区分数据接口和页面资源,不是 HTTP 强制规定。小项目可以使用其他命名,但应该保持一致,避免前端误把页面路径当成接口路径。
五、让前端路由调用接口
下面用原生 JavaScript 展示最小调用方式,不依赖具体前端框架。可以在同一个目录创建 index.html,然后在浏览器打开。由于示例服务没有配置静态文件托管,直接访问该文件时,页面和接口分别通过浏览器本地文件与 localhost:3000 通信;某些浏览器对本地文件的跨域行为有限制,更稳妥的方式是让开发服务器托管页面,或配置 CORS。为避免引入跨域配置,下面将静态文件也交给 Express 托管。
把 index.html 放进名为 public 的目录,然后在 server.js 的 API 路由之前增加这一行:
app.use(express.static('public'));
页面示例读取列表、提交新增请求,并在页面上显示结果:
<!doctype html>
<html lang="zh-CN">
<meta charset="utf-8">
<title>商品示例</title>
<h1>商品列表</h1>
<form id="searchForm">
<input id="query" placeholder="搜索商品">
<button>搜索</button>
</form>
<ul id="list"></ul>
<form id="createForm">
<input name="name" required maxlength="80" placeholder="商品名">
<input name="price" type="number" min="0" step="0.01" required placeholder="价格">
<button>新增</button>
</form>
<p id="message"></p>
<script>
const list = document.querySelector('#list');
const message = document.querySelector('#message');
async function loadProducts(q = '') {
const params = new URLSearchParams({ q, page: '1', limit: '10' });
const response = await fetch(`/api/products?${params}`);
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || '读取失败');
}
list.replaceChildren();
for (const product of data.items) {
const li = document.createElement('li');
li.textContent = `${product.name}:${product.price}`;
list.append(li);
}
}
document.querySelector('#searchForm').addEventListener('submit', async event => {
event.preventDefault();
try {
await loadProducts(document.querySelector('#query').value);
message.textContent = '';
} catch (error) {
message.textContent = error.message;
}
});
document.querySelector('#createForm').addEventListener('submit', async event => {
event.preventDefault();
const form = new FormData(event.currentTarget);
const response = await fetch('/api/products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: form.get('name'),
price: Number(form.get('price'))
})
});
const data = await response.json();
if (!response.ok) {
message.textContent = data.message || '新增失败';
return;
}
message.textContent = '新增成功';
event.currentTarget.reset();
await loadProducts();
});
loadProducts().catch(error => {
message.textContent = error.message;
});
</script>
</html>
重启服务后访问 http://localhost:3000。前端用 fetch 发请求,后端返回 JSON;前端并不是直接调用服务器里的 JavaScript 函数,而是通过 HTTP 网络协议通信。即便前后端由同一团队开发,也应把请求和响应当成明确的数据边界来处理。
六、接口的边界:校验、状态码与错误处理
服务端必须重新校验输入
示例中的 required、maxlength、min 是浏览器侧的交互校验,能减少误操作,但不能替代服务端校验。客户端可以被绕过,甚至可以完全不使用浏览器页面直接构造请求。因此,决定数据是否有效的规则必须在可信的服务端再次执行。
参数校验要与业务规则相符。示例限制商品名长度为 1 到 80 个字符,价格为非负有限数字,分页大小不超过 50;这些只是演示用的应用规则,不是 Express、HTTP 或 JavaScript 对参数的普遍限制。真实项目应根据业务、数据库字段、性能预算和产品需求明确规则。还要考虑空白字符、数字精度、枚举范围、日期格式以及字符长度按何种规则计算。
express.json({ limit: '32kb' }) 限制的是 JSON 请求体解析器接受的请求体大小,不是每个字段允许的长度,也不是路径参数长度限制。超过限制时,解析中间件会拒绝请求;项目可以统一捕获这类错误并返回合适的客户端错误响应。路径长度、请求头大小及并发连接限制还可能由 Node.js、反向代理、网关或部署平台共同影响,不能从这一个配置推导出整套服务的最大请求限制。
合理使用 HTTP 状态码
示例使用 200 表示成功查询、201 表示创建成功、400 表示请求参数不符合接口要求、404 表示资源或接口不存在。真实接口应由团队统一错误格式和状态码语义。前端不能只解析响应体就假设请求成功,还应检查 response.ok 或状态码。网络错误、超时和非 JSON 响应也应纳入错误处理。
API 的 404 与 SPA 页面路由的处理方式不同。访问 /api/unknown 应得到接口错误,而访问前端应用的 /products/42 时,服务器可能需要返回 SPA 的入口 HTML,再由前端路由显示页面。如果不区分两类路径,常见结果是接口拼错后服务器返回了 index.html:浏览器看到状态码可能是 200,但解析 JSON 却失败。
七、安全用途与不能混淆的边界
路由本身只是请求分发机制,不会自动提供认证、授权或输入安全。以下概念应分别处理:
- 认证:确认请求者是谁,例如验证会话或访问令牌。
- 授权:判断这个用户是否有权执行当前操作、访问当前资源。
- 校验:确认输入符合接口和业务规则,避免非法数据进入后续流程。
- 输出安全:根据输出上下文进行编码或使用安全的 DOM API,降低内容被当作可执行 HTML 的风险。
示例通过 textContent 将商品名写入页面,而不是拼接 innerHTML。这是适合普通文本内容的做法。若项目需要输出富文本,应采用经过审查的消毒方案,并明确允许的标签和属性。服务端的输入校验不能替代输出编码,反过来也一样。
生产环境中,身份验证应放在服务端可执行的中间件或业务流程里,权限判断还要结合具体资源。例如,用户通过身份验证,并不意味着他可以读取任意用户的订单;应在服务端确认请求用户与目标资源的关系。把“只有登录用户看得到按钮”当作权限控制是不成立的。
还应根据部署方式考虑其他安全问题:浏览器 Cookie 会话需要评估 CSRF 风险和相应防护;允许跨域访问时,应明确可信来源,而不是为了省事对所有来源开放;敏感操作应有速率限制、审计记录和适当的滥用防护。具体方案取决于认证方式、浏览器访问模式和系统架构,不存在仅靠某个路由写法就自动解决所有安全问题的通用配置。
八、前端路由与服务器部署的协作
前端使用 History API 实现的路由,可以在不重新加载页面的情况下改变地址栏路径。但用户直接刷新 /products/42,或把这个地址发给别人时,浏览器会重新向服务器请求这个路径。若服务器只认识 /api/products,就可能返回 404。因此,SPA 部署通常需要配置:已知的静态资源按文件提供,/api 路径交给后端处理,其余合法页面路径回退到前端入口 HTML。
这个回退规则必须谨慎设置。若把所有未知请求都无条件回退到 index.html,一个不存在的 API 也可能得到 HTML;若静态资源漏配,则 JavaScript 文件也可能被返回 HTML,浏览器会报 MIME 类型或语法错误。常见实践是将 API 与页面路由分开,例如使用 /api 前缀,优先匹配静态文件和 API,再配置前端路由回退,并为 API 未匹配路径保留 JSON 404。
前后端分开部署时,页面与 API 可能位于不同域名或端口,浏览器会执行同源策略检查。此时需要正确配置 API 地址、CORS 策略、认证凭据和环境变量。CORS 是浏览器对跨源读取响应的限制机制,不是身份验证,也不能阻止所有非浏览器客户端发送请求。具体跨域策略要按实际部署的来源和凭据模式设置。
九、读 Web 源码时可以按这条线索排查
- 从页面入口开始,找前端路由的配置,确认 URL 与页面组件的对应关系。
- 在页面或数据层代码中搜索
fetch、Axios 或项目封装的请求方法,确认接口地址、方法、请求头和请求体。 - 查看后端路由注册处,确认方法和路径是否与前端一致,再沿着控制器、服务层追踪业务处理。
- 检查响应结构与错误处理,确认前端读取的字段名和后端返回的字段名一致。
- 分别验证页面路径和 API 路径:直接刷新页面、调用接口、访问不存在的 API,观察状态码和响应内容。
如果项目使用反向代理或 API 网关,还要多看一层:浏览器请求的 URL 可能被代理改写后才到达应用。此时要核对外部路径、代理前缀、应用内部路由以及转发后的请求头,避免误以为前后端路由“名称相同就一定能匹配”。
十、常见错误及其原因
把页面路径当成接口地址
/products/42 可能是前端页面路由,返回 HTML;/api/products/42 才可能是数据接口。应通过 Network 面板检查实际请求的 URL、响应类型和状态码,不要只看地址栏。
只在前端校验参数
浏览器表单校验能改善体验,但请求可以被脚本、命令行或其他客户端直接构造。服务端仍要校验类型、长度、范围和业务约束。
没有区分路径参数与查询参数
路径参数适合定位资源,查询参数适合筛选和分页。接口可以根据业务采用不同设计,但双方必须按统一契约读写;不要前端发在查询字符串中、后端却从请求体读取。
忘记处理非成功响应
fetch 遇到 404 或 500 通常仍会返回一个 Response 对象,不会自动按业务失败抛出异常。需要检查 response.ok,并对错误格式、超时和网络中断做清晰处理。
对所有未知路径都返回前端首页
这可能让拼错的 API 获得 HTML 200 响应,造成 JSON 解析错误,也会掩盖服务器路由配置问题。应让 API 404 与页面路由回退分开处理。
用隐藏按钮实现权限
隐藏按钮只是界面行为,用户仍能直接发送请求。授权检查必须在服务端执行,并根据当前用户和目标资源判断。
结语
路由和接口协作的核心并不复杂:浏览器请求进入前端页面路由或后端 API 路由,后端按请求方法和路径分发处理,双方依照接口契约交换数据。真正容易出问题的,是把页面 URL 与 API URL 混为一谈、只在客户端验证、忽略状态码,或没有为 SPA 部署配置正确的服务器回退规则。
阅读源码时,从地址栏路径追到前端路由,再追到请求封装、后端路由和业务处理,最后核对响应格式与部署代理,通常就能还原完整链路。设计接口时,则要明确参数放置位置、输入边界、错误语义和服务端权限判断,让路由负责分发,让接口契约负责协作,让安全检查落在真正可信的服务端边界上。




