CRA 已经废弃,Next.js 太重——纯 SPA 现在的标准答案是 Vite。
下面建立一个开箱即用的最小项目,包含路由、CSS 模块化、ESLint、
开发 + 生产构建。
1. 脚手架
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
--template react-ts 直接给 TypeScript。
2. 装常用依赖
npm i react-router-dom
npm i -D @types/node prettier eslint-config-prettier
3. 给 tsconfig.json 加 path alias
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
让 Vite 也认 alias:
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'node:path'
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: { port: 5173, host: true },
build: {
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
react: ['react', 'react-dom', 'react-router-dom'],
},
},
},
},
})
manualChunks 把 React 切成独立 chunk,业务代码变化不会重新拉这部分。
4. 主入口
// src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from '@/App'
import '@/index.css'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>,
)
5. App + 路由
// src/App.tsx
import { Routes, Route, Link } from 'react-router-dom'
import Home from '@/pages/Home'
import About from '@/pages/About'
export default function App() {
return (
<>
<nav>
<Link to="/">Home</Link> | <Link to="/about">About</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</>
)
}
// src/pages/Home.tsx
export default function Home() {
return <h1>Hello</h1>
}
6. CSS Modules
/* src/pages/Home.module.css */
.title { font-size: 2rem; color: #2563eb; }
import s from './Home.module.css'
export default function Home() {
return <h1 className={s.title}>Hello</h1>
}
TypeScript 默认认得 .module.css —— vite/client 类型定义里包含了。
7. ESLint + Prettier
npm i -D eslint @eslint/js typescript-eslint eslint-plugin-react-hooks \
eslint-plugin-react-refresh prettier
eslint.config.js:
import js from '@eslint/js'
import ts from 'typescript-eslint'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
export default ts.config(
{ ignores: ['dist'] },
js.configs.recommended,
...ts.configs.recommended,
{
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': 'warn',
},
},
)
package.json 加 script:
{
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"lint": "eslint .",
"format": "prettier -w ."
}
}
8. 跑
npm run dev # 开发,HMR
# http://localhost:5173
npm run build # tsc 类型检查 + Vite 打包,输出到 dist/
npm run preview # 本地预览生产构建
9. 部署(静态托管)
npm run build
# dist/ 直接 rsync 到任何静态服务器:
rsync -avz --delete ./dist/ user@server:/var/www/myapp/
服务端 nginx:
server {
listen 80;
server_name myapp.example.com;
root /var/www/myapp;
index index.html;
# SPA 路由:所有未命中文件的请求都 fallback 到 index.html
location / { try_files $uri $uri/ /index.html; }
# 资源缓存 1 年(文件名带 hash 所以安全)
location ~* \.(css|js|png|jpg|svg|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
踩过的坑
- 用 alias
@/后 IDE 不识别 → 重启 TypeScript Server(VSCode:Cmd+Shift+P
→ "TypeScript: Restart TS Server")。 - HMR 突然不工作:检查文件名首字母是否大写。
Home.tsx默认 export
必须是Home(首字母大写),否则 react-refresh 不接管。 - 生产构建 hash 文件名后部署时新文件先到,旧 index.html 还在引用,
瞬间 404。解决:先部署assets/再部署index.html,并保留旧 hash
文件 2-3 个版本。 vite preview不能完全替代真实生产 nginx,特别是 try_files fallback ——
preview 自带 SPA fallback,nginx 没配就 404。
登录后参与评论。