Hono 前后端分离系列 04:CORS、环境变量和前端联调
wxk1991 Lv5

Hono 前后端分离系列 04:CORS、环境变量和前端联调

前后端分离项目里,第一个让新人怀疑人生的问题,通常不是数据库,也不是鉴权。

而是 CORS。

页面明明能打开,接口明明能单独访问,前端一请求就报错:

1
Access to fetch at ... has been blocked by CORS policy

这不是 Hono 的问题,也不是浏览器抽风。

这是浏览器在保护用户。

这篇文章把 Hono 项目里的 CORS、环境变量和前端联调讲清楚。这个基础打不好,后面 JWT、cookie、文件上传、部署都会继续出问题。


一、为什么本地会跨域

本地开发常见结构:

1
2
前端:http://localhost:5173
后端:http://localhost:8787

协议一样,域名一样,但端口不同。

浏览器判断 origin 时看三件事:

1
2
3
协议
域名
端口

只要有一个不同,就是不同源。

所以 localhost:5173 请求 localhost:8787,就是跨域。

服务器如果没有明确允许,浏览器就会拦截响应。

注意,是浏览器拦截。

你用 curl 或 Postman 能访问,不代表前端页面也能访问。


二、Hono 里启用 CORS

Hono 内置了 CORS middleware。

最简单的写法:

1
2
3
4
5
6
7
8
9
10
11
12
import { Hono } from 'hono'
import { cors } from 'hono/cors'

const app = new Hono()

app.use('/api/*', cors())

app.get('/api/health', (c) => {
return c.json({ ok: true })
})

export default app

Hono 官方文档提醒,CORS middleware 应该在 route 之前调用。

也就是说:

1
2
app.use('/api/*', cors())
app.get('/api/health', handler)

不要反过来。


三、开发环境不要直接放开生产环境

很多教程会这样写:

1
app.use('*', cors())

这在本地测试很方便,但生产环境不能无脑照搬。

更合理的方式是按环境配置 origin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const app = new Hono<{
Bindings: {
CORS_ORIGIN: string
}
}>()

app.use('/api/*', async (c, next) => {
const corsHandler = cors({
origin: c.env.CORS_ORIGIN,
allowMethods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization'],
})

return corsHandler(c, next)
})

如果你跑在 Node.js,也可以从环境变量读取:

1
2
3
4
5
const corsOrigin = process.env.CORS_ORIGIN ?? 'http://localhost:5173'

app.use('/api/*', cors({
origin: corsOrigin,
}))

生产环境应该明确允许你的前端域名:

1
https://app.example.com

而不是随手 *


四、credentials 是另一条规则

如果你用 cookie 保存登录态,前端请求通常要带:

1
2
3
fetch('/api/me', {
credentials: 'include',
})

这时后端 CORS 也要允许 credentials:

1
2
3
4
app.use('/api/*', cors({
origin: 'https://app.example.com',
credentials: true,
}))

这里有个关键点:

1
credentials: true 时,不要使用 origin: '*'

浏览器不会接受这种组合。

如果你用的是 Bearer Token 放在 Authorization header 里,不一定需要 credentials。但如果要发送 cookie,就必须同时处理:

  • CORS credentials
  • Cookie SameSite
  • Cookie Secure
  • HTTPS

前后端分离里,cookie 登录比 token header 更容易踩坑。


五、前端环境变量怎么配

前端不要把 API 地址写死在代码里。

Vite 项目可以这样配:

.env.development

1
VITE_API_BASE_URL=http://localhost:8787

.env.production

1
VITE_API_BASE_URL=https://api.example.com

请求封装:

1
2
3
4
5
6
7
8
9
10
11
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL

export async function apiFetch(path: string, init?: RequestInit) {
return fetch(`${API_BASE_URL}${path}`, {
...init,
headers: {
'Content-Type': 'application/json',
...init?.headers,
},
})
}

这样本地、测试、生产可以走不同 API 地址。

不要在代码里到处写:

1
fetch('http://localhost:8787/api/users')

后面改环境会很痛。


六、联调时怎么排查

遇到 CORS 问题,按这个顺序查。

第一,看请求是不是 OPTIONS 预检失败。

浏览器对复杂请求会先发 OPTIONS。比如带 Authorization header,或者 Content-Type: application/json 的某些请求,都可能触发预检。

第二,看响应头有没有:

1
2
3
Access-Control-Allow-Origin
Access-Control-Allow-Methods
Access-Control-Allow-Headers

第三,如果带 cookie,看有没有:

1
Access-Control-Allow-Credentials: true

第四,看 origin 是否精确匹配。

不要只看 URL 像不像,端口也算。

第五,确认 CORS middleware 放在 route 前面。

这几个点查完,80% 的 CORS 问题都能定位。


七、不要用代理掩盖问题

Vite proxy 很有用:

1
2
3
4
5
server: {
proxy: {
'/api': 'http://localhost:8787',
},
}

但它只是开发体验工具。

它会让浏览器以为请求仍然发给 localhost:5173,从而绕过跨域。

这不是坏事,但你必须清楚:

1
2
proxy 能让本地舒服
不能证明生产跨域配置正确

上线前,一定要用真实前端域名请求真实后端域名测试一次。


八、最后的建议

CORS 不应该靠“试到不报错”为止。

我的建议是:

1
2
3
4
5
本地明确允许 localhost 前端端口
测试环境明确允许测试前端域名
生产环境明确允许生产前端域名
需要 cookie 才开启 credentials
不要把所有 origin 都放开

Hono 的 CORS middleware 已经足够好用。真正的问题通常是我们没把前端域名、部署域名、登录方式和环境变量想清楚。

前后端分离的联调质量,往往从 CORS 配置就能看出来。


参考资料