用 CloudBase 静态网站托管部署 React SPA
一句话定义:用 Vite + React 出
dist,通过tcb hosting deploy dist -e <env-id>一键上传到 CloudBase 静态托管,自动拿到 CDN 加速 + HTTPS + 自定义域名能力;React Router 用BrowserRouter时,在控制台把错误页面配为index.html解决刷新 404。预计耗时:15 分钟 | 难度:入门
适用场景
这一篇覆盖的是最常见的 React 前端部署场景:纯 SPA、不要 SSR、不写后端逻辑,构建产物是一坨静态文件。CloudBase 静态托管底层是 COS + CDN,按存储和流量计费,没有容器实例费。
- 适用:React 18+ + Vite 或 Create React App 出的 SPA
- 适用:要部署在境内、域名要走 ICP 备案 + HTTPS
- 适用:成本敏感、不需要服务端进程
- 不适用:要 SSR / SSG / Next.js 服务端渲染——走云托管,参考 deploy-nextjs-to-cloudbase-run
- 不适用:要 BFF 或后端 API——也走云托管或云函数
静态托管 vs 云托管
一句话区分 :静态托管 = 纯前端文件 + CDN 分发;云托管 = 容器化的服务端进程。
| 维度 | 静态托管 | 云托管 |
|---|---|---|
| 部署对象 | 一坨静态文件(HTML/CSS/JS/图片) | 一个容器镜像(Dockerfile) |
| 运行时 | 无(CDN 直接回源 COS) | Node.js / Python / 自定义运行时进程 |
| 计费 | 存储 + 流量 | 实例时长 + 流量 |
| 适合 | SPA / 文档站 / 落地页 | SSR / API / WebSocket / 长连接 |
React SPA 就两种产物:HTML 一份 + JS bundle 一份 + 各种静态资源,全部走静态托管最便宜。
环境要求
| 依赖 | 版本 |
|---|---|
| Node.js | ≥ 18(Vite 5/6 的最低要求) |
| React | 18+ |
| Vite 或 CRA | Vite 5+ / Create React App 5+ |
react-router-dom | 6+(如用路由) |
@cloudbase/cli | latest(写文章时是 v3.x) |
| 一个已开通的 CloudBase 环境 | 含静态托管能力 |
CLI 全局安装并登录:
npm i -g @cloudbase/cli
tcb login
第一步:配置 React 项目
SPA 部署到静态托管,有两个配置必须先确认,否则到了第四步会出现刷新 404、资源 404 这类典型坑。
1.1 Vite 的 base 配置
vite.config.ts(或 vite.config.js):
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
base: '/', // 部署到根域名时填 '/'
// base: '/my-app/', // 如果要部署到子路径(例如默认域名/my-app/)就填 '/my-app/'
build: {
outDir: 'dist',
},
});
base 决定 build 出来的 HTML 里资源引用的前缀。如果你部署到默认域名根路径(https://<env-id>.tcloudbaseapp.com/),就用 /;如果通过 tcb hosting deploy dist /my-app -e <env-id> 部署到子路径,就要改成 /my-app/,前后斜杠都不能少。
Create React App 项目对应的字段是 package.json 里的 homepage,含义类似(CRA build 产物在 build/ 目录而不是 dist/,下文以 Vite 为例)。
1.2 React Router 的 basename
React Router 6+ 有两种写法,basename 都必须和 Vite 的 base 完全一致。
老写法(BrowserRouter):
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
function App() {
return (
<BrowserRouter basename="/">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</BrowserRouter>
);
}
export default App;
新写法(createBrowserRouter,推荐):
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
const router = createBrowserRouter(
[
{ path: '/', element: <Home /> },
{ path: '/about', element: <About /> },
],
{ basename: '/' },
);
function App() {
return <RouterProvider router={router} />;
}
export default App;
两种写法等价,但 createBrowserRouter 支持 data API(loader / action),React Router 官方推荐新项目用它。
basename 跟 Vite 的 base 不一致会出现「首页能打开但跳转 /about 之后所有资源 404」这种诡异现象,这是 hosting-vue 一样的陷阱。
如果你只想图省事不踩 history 模式的坑,可以改用 createHashRouter(或 HashRouter),URL 会变成 /#/about 这种带 # 的形式,不需要任何服务端 fallback 配置。但 SEO 不友好,生产项目一般不推荐。
第二步:构建 dist
npm install
npm run build
跑完之后项目根目录会出现 dist/ 文件夹:
dist/
├── index.html
├── assets/
│ ├── index-a1b2c3d4.js
│ ├── index-e5f6g7h8.css
│ └── ...(各种 chunk 和静态资源)
└── favicon.ico
打开 dist/index.html 检查一下:所有 script、link 的 src / href 应该都以你设的 base 开头。如果你的 base 是 /my-app/,HTML 里就应该是 /my-app/assets/index-xxxxx.js,而不是 /assets/index-xxxxx.js。base 没改对的话回去改 vite.config.ts 重新 build。
第三步:部署到 CloudBase 静态托管
CloudBase CLI 提供两条部署路径,取决于你想要的工作流。
路径 A:tcb app deploy(推荐,自动化构建 + 上传)
tcb app deploy 会一条命令跑完 安装依赖 → 构建 → 上传产物 → 绑定路由 全流程,云端帮你跑 build:
tcb app deploy --framework react -e <env-id>
CLI 会从 package.json 读 name 作为应用名,从 --framework react 推断构建命令是 npm run build、产物目录是 ./dist。第一次跑会交互式确认这些参数,之后会自动写回 cloudbaserc.json,下次只要 tcb app deploy 就够了。
参数清单(常用的):
| 参数 | 说明 |
|---|---|
--framework | 框架类型:vite / vue / react / next / nuxt / angular / static |
-e, --env-id <envId> | 目标环境 ID |
--build-command <cmd> | 自定义构建命令,覆盖框架默认 |
--output-dir <dir> | 构建产物目录,Vite 默认 ./dist,CRA 是 ./build |
--deploy-path <path> | 静态托管挂载路径,默认 /服务名 |
完整参数见 tcb app deploy 命令文档。
路径 B:tcb hosting deploy(已有 dist 直接上传)
如果你想把 build 留在本地控制(比如 CI 已经跑过 npm run build),可以只上传 dist/:
# 在项目根目录,先本地构建好
npm run build
# 部署 dist 整个目录到云端根目录
tcb hosting deploy dist -e <env-id>
# 或者部署到子路径
tcb hosting deploy dist /my-app -e <env-id>
tcb hosting deploy <localPath> [cloudPath] 是文件维度的工具,不会跑 install/build,只做上传。完整命令清单:
| 命令 | 用途 |
|---|---|
tcb hosting deploy <localPath> [cloudPath] -e <env-id> | 上传文件/文件夹到指定路径 |
tcb hosting list -e <env-id> | 查看云端文件列表 |
tcb hosting detail -e <env-id> | 查看服务信息(默认域名 / 状态) |
tcb hosting delete <cloudPath> -e <env-id> | 删除指定文件,加 --dir 删文件夹,加 --force 强删,加 --dry-run 预览 |
tcb hosting delete -e <env-id> | 清空整个静态托管 |
部署限制
- 单个文件最大 50TB,文件数无上限
- 上传遇到
socket hang up/ECONNRESET大概率是 SDK 长连接被网络中间设备断了,先关掉重试:export COS_SDK_KEEPALIVE=falsetcb hosting deploy dist -e <env-id>
完整命令文档见 CLI 静态托管命令。
第四步:配 SPA fallback
这一步是 React Router BrowserRouter / createBrowserRouter 部署到静态托管的必配项,跳过这一步的症状:首页能打开,但是任何子路由(/about / /users/123)刷新或直接访问都会 404。
原因:history 模式靠 pushState 改 URL,但物理上云端只有 index.html 一个文件,访问 /about 时 CDN 找不到 /about.html,按默认逻辑返回 404。
修法:让 4xx 错误页面也回 index.html,把路由判断扔回前端。
操作路径:
- 进入 云开发控制台 → 静态网站托管
- 切到「设置」标签页
- 找到「错误页面」配置
- 把 4xx 错误页面填为
index.html - 保存
配完之后访问 <默认域名>/about 会返回 index.html + 200 状态码(注意状态码,不是 404 改了响应体),React Router 拿到 URL 自己解析路由。
参考 静态托管控制台管理 文档的「重定向规则 → 错误码重定向」章节。
第五步:绑自定义域名 + HTTPS
默认域名 <env-id>.xxx.tcloudbaseapp.com 能用,但有访问频率限制,生产环境必须换自定义域名。
前置准备
配置步骤
- 进入 云开发平台 → HTTP 访问服务
- 点「添加域名」,填自定义域名(例如
app.example.com) - 上传 SSL 证书或选已有证书
- CDN 类型选「云开发 CDN」(适合静态托管,自动配 CDN 加速)
- 添加成功后控制台会给一段 CNAME 值,复制下来
- 去 DNS 服务商(腾讯云 DNS / 阿里云 / Cloudflare 等)添加 CNAME 记录指向那段值
- 等 3-5 分钟 DNS 全球生效,控制台状态变「已生效」即完成
完整流程包含三种 CDN 类型对比,见 自定义域名。
缓存配置(可选)
控制台「设置 → 缓存配置」,建议按资源类型分别配:
| 资源类型 | 建议缓存时间 |
|---|---|
| 图片、字体 | 30 天 |
| CSS / JS(带 hash 后缀) | 7 天 |
index.html | 1 小时 |
index.html 故意配短,否则前端发新版本之后用户浏览器缓存的 HTML 还指向旧版本的 JS bundle 文件名,新部署不生效。
运行验证
# 1. 默认域名拿到首页(应该返回 200 + HTML)
curl -I https://<env-id>.xxx.tcloudbaseapp.com/
# 2. 直接访问子路由(配了 fallback 应该返回 200 而不是 404)
curl -I https://<env-id>.xxx.tcloudbaseapp.com/about
# 3. 静态资源(确认 base 配对了)
curl -I https://<env-id>.xxx.tcloudbaseapp.com/assets/index-xxxxx.js
浏览器侧再做一遍:
- 打开默认域名首页,路由跳转一遍
/about→/users/123都正常 - 在
/about页面直接按 F5 刷新——必须不能 404,这是验证 fallback 配对了的关键测试 - 自定义域名 DNS 生效后用
https://app.example.com重测一遍
常见错误
| 现象 | 原因 | 修法 |
|---|---|---|
tcb hosting deploy ./dist -e xxx 报 ENOENT: no such file or directory | dist 路径写错或当前目录没在项目根目录;CRA 项目产物在 build/ 不是 dist/ | cd 到项目根目录,确认 ls dist(CRA 是 ls build)能看到 index.html 再部署 |
首页能开,但刷新 /about 返回 404 | 没配 SPA fallback(第四步) | 控制台「设置 → 错误页面」把 4xx 配为 index.html |
| 首页打开后 CSS / JS 全 404,控制台 Network 看到资源路径不对 | vite.config.ts 的 base 跟实际部署的 cloudPath 不一致 | base 部署到根目录用 /,部署到子路径 /my-app 就用 /my-app/,改完 重新 build 重新部署 |
| 子路由刷新返回 200 但页面空白,Network 显示 JS 404 | BrowserRouter basename / createBrowserRouter 的 basename 跟 vite.config.ts 的 base 不一致 | 两边改成完全一样的字符串(/ 或 /my-app/),重新 build 部署 |
| CRA 项目部署后资源 404 | CRA 的 homepage 字段没改,build 出来的 HTML 资源路径不对 | package.json 加 "homepage": "." 或 "homepage": "/my-app",重新 npm run build |
| 自定义域名访问 ERR_CERT_COMMON_NAME_INVALID | SSL 证书绑的不是当前域名,或上传的证书过期 | 去 SSL 证书控制台 重申一份对的证书,重新绑定 |
| 自定义域名 DNS 解析返回还是旧 IP | DNS 没切到 CloudBase 的 CNAME,或本地 DNS 缓存 | nslookup app.example.com 确认 CNAME 已指向腾讯云的 CDN 域名,本地 dscacheutil -flushcache 清缓存(macOS) |
| 自定义域名 403 | 域名没过 ICP 备案,或备案已过期 | 去 ICP 备案系统 查备案状态,没备案就先备案 |
大批量上传时 socket hang up / ECONNRESET | COS SDK 长连接被网络中间设备断了 | export COS_SDK_KEEPALIVE=false 关长连接重试 |
部署相关的报错码可在 错误码总览 查到完整列表。
相关文档
- Vue SPA + 静态托管 — 同模板的 Vue 端对照
- 静态托管快速开始 — 控制台和 CLI 两条上传路径
- 静态托管控制台管理 — 错误页面、缓存、防盗链等设置项
- CLI 静态托管命令 —
tcb hosting完整命令清单 - CLI 应用部署命令 —
tcb app deploy完整参数和cloudbaserc.json配置 - 自定义域名 — 三种 CDN 类型 + 域名所有权校验完整流程
- Next.js 部署到云托管 — 需要 SSR 时的对照方案
下一步
React SPA 部署上线之后,前端调后端是自然延伸——LLM 代理放云函数避免浏览器暴露 key,参考 connect-openai-api-cloud-function;给 React 站点加 CloudBase 登录走 add-auth-web-with-cloudbase-sdk,复用同一个环境;CI 跨环境(dev / staging / prod)的密钥分层方案见 secure-secrets-in-cloud-function,配合 tcb hosting deploy 自动部署。