# ob-client **Repository Path**: ruidong/ob-client ## Basic Information - **Project Name**: ob-client - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-02-02 - **Last Updated**: 2026-02-09 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # OB-Client 项目文档 ## 项目概述 基于 SolidStart 框架构建的企业级前端应用,采用服务端渲染(SSR)架构,实现完整的用户认证、权限管理和业务功能。 ### 技术栈 - **框架**: SolidStart (Solid.js 服务端渲染框架) - **语言**: TypeScript - **样式**: TailwindCSS + DaisyUI - **图标**: Lucide-Solid - **HTTP客户端**: 自定义封装的 fetch 工具类 --- ## 项目结构 ``` src/ ├── actions/ # RPC 服务端 Action │ ├── (auth)/ # 认证相关 Action │ │ ├── login/ │ │ │ ├── login.action.ts # 登录 Action │ │ │ └── mail.action.ts # 发送邮件 Action │ │ └── register/ │ │ └── register.action.ts # 注册 Action │ └── (domain)/ # 业务域 Action │ └── profile/ │ └── change-avatar/ │ └── change-avatar.action.ts # 修改头像 Action ├── components/ # 公共组件 │ ├── (domain)/ # 业务域组件 │ │ └── profile/ │ │ ├── change-avatar/ │ │ │ └── index.tsx # 修改头像组件 │ │ └── change-pass/ │ │ └── index.tsx # 修改密码组件 │ ├── Link.tsx # 自定义链接组件(带认证拦截) │ ├── dynamic-icon.tsx # 动态图标组件 │ ├── left-menus.tsx # 左侧菜单组件 │ ├── loading.tsx # 加载状态组件 │ └── navbar.tsx # 顶部导航栏组件 ├── contexts/ # 全局上下文 │ └── user-context.tsx # 用户上下文 ├── lib/ # 公共扩展功能 │ └── session.ts # Session 管理工具 ├── routes/ # 路由层 │ ├── (auth)/ # 认证相关路由组 │ │ ├── forgot/ │ │ │ └── index.tsx # 忘记密码页面 │ │ ├── login/ │ │ │ └── index.tsx # 登录页面 │ │ ├── register/ │ │ │ └── index.tsx # 注册页面 │ │ └── reset/ │ │ └── index.tsx # 重置密码页面 │ ├── (domain)/ # 管理后台路由组 │ │ ├── access/ │ │ │ └── index.tsx # 访问控制页面 │ │ ├── buckets/ │ │ │ └── index.tsx # 存储桶管理页面 │ │ ├── files/ │ │ │ └── index.tsx # 文件管理页面 │ │ ├── monitor/ │ │ │ └── index.tsx # 监控统计页面 │ │ ├── profile/ │ │ │ └── index.tsx # 个人资料页面 │ │ ├── settings/ │ │ │ └── index.tsx # 设置页面 │ │ └── index.tsx # 首页 │ ├── api/ # API 路由 │ │ └── auth/ │ │ └── sync.ts # 用户信息同步 API │ ├── (domain).tsx # 管理后台布局 │ └── [...404].tsx # 404 页面 ├── types/ # 类型定义 │ └── tomal.d.ts # TOML 模块类型声明 ├── utils/ # 公共工具 │ ├── $.ts # HTTP 请求工具类 │ ├── config.ts # 配置文件 │ ├── logger.ts # 日志工具 │ ├── style.ts # 样式工具 │ └── sync-info.ts # 用户信息同步工具 ├── app.tsx # 应用根组件 ├── app.css # 全局样式 ├── entry-client.tsx # 客户端入口 ├── entry-server.tsx # 服务端入口 ├── global.d.ts # 全局类型定义 └── middleware.ts # 中间件(请求拦截器) ``` --- ## 核心运行逻辑 ### 一、应用启动流程 #### 1. 服务端启动(entry-server.tsx) ``` NodeJS 环境启动 ↓ createHandler() 创建服务器处理器 ↓ StartServer 组件渲染 HTML 文档结构 ↓ 包含 assets(CSS等资源)、children(页面内容)、scripts(JS脚本) ↓ 返回完整的 HTML 文档给浏览器 ``` #### 2. 客户端启动(entry-client.tsx) ``` 浏览器接收 HTML 文档 ↓ mount() 函数挂载应用 ↓ StartClient 组件接管客户端渲染 ↓ 应用进入交互状态 ``` #### 3. 路由初始化(app.tsx) ``` Router 组件初始化 ↓ FileRoutes 自动扫描 routes/ 目录 ↓ 根据 URL 匹配对应路由文件 ↓ Suspense 处理异步组件加载 ↓ 渲染匹配的页面组件 ``` --- ### 二、认证流程 #### 2.0 三层拦截机制 本项目采用三层拦截机制实现完整的用户认证和会话管理: **第一层:服务端中间件拦截 (middleware.ts)** - **执行时机**: 路由执行之前,项目打开和页面刷新时触发 - **功能**: 免密登录验证 **运行流程**: 1. **白名单过滤**: 对 `/login`, `/register`, `/forgot` 等公开路由和静态资源不进行拦截 2. **Cookie 解析**: 从请求头中解析 Session Cookie 3. **Token 验证**: - 检查 Cookie 是否存在 - 验证 Cookie 格式是否正确 - 使用 Access Token 验证用户身份 4. **自动续签**: - 当 Access Token 过期时,自动使用 Refresh Token 续签 - 续签成功后更新客户端 Cookie 5. **安全重定向**: - 验证失败时重定向到登录页 - 只有首页 (`/`) 会触发重定向,其他页面直接返回 undefined **关键代码位置**: `src/middleware.ts` **第二层:前端路由守卫拦截 ((domain).tsx)** - **执行时机**: 前端路由切换时 - **功能**: 获取用户信息,前端拦截用户操作 **运行流程**: 1. **状态初始化**: 创建用户状态 Store 2. **用户信息同步**: 调用 `syncInfo()` 获取当前用户信息 3. **认证状态检查**: - 根据 API 响应判断用户是否已认证 - 未认证用户重定向到登录页 4. **全局布局渲染**: - 提供左侧菜单和顶部导航栏 - 使用 Suspense 处理异步加载状态 **关键代码位置**: `src/routes/(domain).tsx` **第三层:操作拦截 (Link 组件)** - **执行时机**: 用户点击链接时 - **功能**: 登录过期拦截 **运行流程**: 1. **路由检查**: 检查目标路由是否与当前路由相同 2. **用户信息同步**: 在路由跳转前同步用户信息 3. **登录状态验证**: - 检测到 401 状态码时阻止跳转 - 自动重定向到登录页 4. **自定义事件处理**: 支持组件自定义的 onClick 事件 **关键代码位置**: `src/components/Link.tsx` #### 2.1 用户登录流程 ``` 用户在登录页面填写表单 ↓ 点击登录按钮,触发 login.action ↓ RPC 请求发送到服务端 ↓ 服务端调用后端登录接口 ↓ 后端验证邮箱、密码、验证码 ↓ 验证成功,返回双 token(access_token + refresh_token) ↓ 服务端将 token 存储到 session ↓ 通过 Set-Cookie 响应头将 session 写入浏览器 ↓ 浏览器自动保存 cookie ↓ 跳转到管理后台页面 ``` #### 2.2 Token 管理机制 **Token 结构**: ```typescript interface Tokens { access_token: string; // 访问令牌(短期有效,如 15 分钟) refresh_token: string; // 刷新令牌(长期有效,如 7 天) } ``` **Token 存储位置**: - 服务端:Session(通过 Cookie 传递) - 客户端:Cookie(httpOnly、secure、sameSite) --- ### 三、会话管理流程 #### 3.1 登录流程 ``` 用户提交登录表单 ↓ 触发 login.action.ts ↓ 调用后端登录 API(验证邮箱、密码、验证码) ↓ 后端返回双 token(access_token + refresh_token) ↓ 服务端设置会话 Cookie(setSession) ↓ 通过 Set-Cookie 响应头写入浏览器 ↓ 重定向到首页 ``` #### 3.2 会话验证流程 ``` 用户访问受保护页面 ↓ 从请求头解析 Cookie ↓ Base64 解码获取 Token ↓ 调用 /user/userinfo API 验证 Access Token ↓ ┌─ Token 有效 ───────────────┐ │ 继续处理请求 │ └─────────────────────────────┘ ↓ ┌─ Token 过期 ───────────────┐ │ 使用 Refresh Token 续签 │ │ ↓ │ │ 调用后端刷新接口 │ │ ↓ │ │ 获取新的双 token │ │ ↓ │ │ 更新客户端 Cookie │ │ ↓ │ │ 继续处理请求 │ └─────────────────────────────┘ ↓ ┌─ 续签失败 ─────────────────┐ │ 清除 Session │ │ ↓ │ │ 重定向到登录页 │ └─────────────────────────────┘ ``` #### 3.3 Token 刷新详细逻辑 ```typescript // 使用 syncInfo() 函数同步用户信息 async function syncInfo(): Promise { try { const response = await fetch('/api/auth/sync'); const data = await response.json(); if (data.code === 401) { // Token 过期或无效 return null; } return data; } catch (error) { console.error('同步用户信息失败:', error); return null; } } ``` **关键文件位置**: - `src/utils/sync-info.ts` - 用户信息同步工具 - `src/routes/api/auth/sync.ts` - 用户信息同步 API --- ### 四、请求拦截与 Token 刷新流程 #### 4.1 中间件拦截流程(middleware.ts) ``` 浏览器发起请求 ↓ 中间件 onRequest 拦截 ↓ 检查是否为静态资源或内部路由 ↓ 从请求头获取 Cookie ↓ 解析 Cookie 获取 session ↓ 从 session 中提取 token ↓ 验证 token 是否存在 ↓ 检查 access_token 是否过期 ↓ ┌─ 过期 ──────────────────────────────────────┐ │ 使用 refresh_token 刷新 token │ │ ↓ │ │ 调用后端刷新接口 │ │ ↓ │ │ 后端验证 refresh_token │ │ ↓ │ │ ┌─ 有效 ─────────────┐ ┌─ 无效 ────────┐ │ │ │ 返回新 token │ │ 返回 401 错误 │ │ │ │ ↓ │ │ ↓ │ │ │ │ 更新 session │ │ 清除 session │ │ │ │ ↓ │ │ 返回登录页 │ │ │ │ 设置新的 Cookie │ └───────────────┘ │ │ └────────────────────┘ │ └───────────────────────────────────────────────┘ ↓ 继续处理请求 ``` #### 3.2 Token 刷新详细逻辑 ```typescript // tokenManager.ts 中的刷新逻辑 async refreshAccessToken(refreshToken: string): Promise { // 1. 调用后端刷新接口 const response = await this.http.post('/auth/refresh', { refresh_token: refreshToken }); // 2. 检查响应状态 if (response.status === 401) { // refresh_token 无效,返回 null return null; } // 3. 解析新 token const newTokens = this.parseTokens(response.data); // 4. 验证新 token if (!this.validateTokens(newTokens)) { return null; } // 5. 返回新 token return newTokens; } ``` --- ### 四、HTTP 请求流程 #### 4.1 HTTP 客户端架构(utils/$.ts) ``` 创建 Http 实例 ↓ 配置 baseUrl、timeout 等参数 ↓ 注册请求拦截器(支持多个,按顺序执行) ↓ 注册响应拦截器(支持多个,按顺序执行) ↓ 提供请求方法(get、post、put、delete、patch 等) ``` **核心特性**: - **拦截器机制**:支持多个请求/响应拦截器,按注册顺序执行 - **超时控制**:支持自定义超时时间,尊重用户自定义的 AbortSignal - **自动 JSON 处理**:智能识别 Content-Type,自动序列化和解析 JSON - **健壮的 URL 构建**:支持多种 URL 格式,自动处理斜杠和查询参数 - **增强错误处理**:详细的错误分类(超时、网络、请求、未知) #### 4.2 请求拦截器流程 ```typescript // 请求拦截器 - 支持 Headers 实例和普通对象 async requestInterceptor(options: FetchOptions): Promise { // 1. 获取当前 token const token = getToken(); // 2. 确保 headers 存在 if (!options.headers) { options.headers = {}; } // 3. 添加 Authorization 请求头 if (token) { if (options.headers instanceof Headers) { options.headers.set('Authorization', `Bearer ${token.access_token}`); } else { options.headers['Authorization'] = `Bearer ${token.access_token}`; } } // 4. 智能 Content-Type 处理 // 如果未设置 Content-Type 且 body 是普通对象,自动设置为 application/json if (options.body && isPlainObject(options.body)) { const hasContentType = checkContentType(options.headers); if (!hasContentType) { if (options.headers instanceof Headers) { options.headers.set('Content-Type', 'application/json'); } else { options.headers['Content-Type'] = 'application/json'; } // 自动序列化 JSON if (typeof options.body !== 'string') { options.body = JSON.stringify(options.body); } } } return options; } ``` **改进点**: - 支持 Headers 实例和普通对象 - 不区分大小写的 Content-Type 检查 - 智能 JSON 序列化(避免重复序列化) #### 4.3 响应拦截器流程 ```typescript // 响应拦截器 - 支持多个拦截器链式处理 async responseInterceptor(response: Response): Promise { // 1. 检查响应状态 if (response.status === 401) { // Token 无效,触发刷新 const newToken = await refreshAccessToken(); if (newToken) { // 刷新成功,重试请求 return retryRequest(response, newToken); } else { // 刷新失败,跳转登录 redirectToLogin(); } } // 2. 处理其他错误状态 if (!response.ok) { throw new Error(`Request failed: ${response.status}`); } return response; } ``` **响应处理流程**: ``` 执行 fetch 请求 ↓ 获取 Response 对象 ↓ 依次执行响应拦截器(支持修改响应) ↓ 如果拦截器未处理且返回 Response 对象 ↓ 自动识别 Content-Type ↓ 如果是 JSON 类型,自动解析并返回 ↓ 如果是其他类型,返回原始 Response ↓ 如果拦截器已处理,直接返回处理结果 ``` **改进点**: - 支持多个响应拦截器链式处理 - 智能 JSON 解析(带错误处理) - 拦截器可以返回任意类型,不限于 Response --- ### 五、核心组件说明 #### 5.1 应用入口 (app.tsx) - 配置路由和全局布局 - 提供 UserProvider 上下文 - 使用 Suspense 处理异步加载 - 设置全局导航栏和左侧菜单 #### 5.2 导航组件 (navbar.tsx) - 显示用户信息和头像 - 提供侧边栏切换功能 - 用户下拉菜单(个人资料、设置、退出登录) - 响应式设计支持移动端 #### 5.3 左侧菜单 (left-menus.tsx) - 应用主要导航菜单 - 使用自定义 Link 组件(带认证拦截) - 支持菜单分组和图标显示 - 响应式设计支持折叠 #### 5.4 动态图标 (dynamic-icon.tsx) - 基于 Lucide 图标库 - 支持图标名称自动前缀处理 - 提供备用图标机制 - 动态加载图标组件 #### 5.5 自定义链接 (Link.tsx) - 封装 SolidJS 的 A 组件 - 集成认证拦截逻辑 - 支持自定义点击事件 - 路由切换前同步用户信息 --- ### 六、页面渲染流程 #### 6.1 服务端渲染(SSR) ``` 用户访问页面 ↓ NodeJS 接收请求 ↓ 中间件处理(验证 token、刷新 token) ↓ 路由匹配,加载页面组件 ↓ 执行页面组件的 load 函数(如果有) ↓ 获取页面所需数据 ↓ 渲染组件为 HTML ↓ 将数据注入到 HTML 中(注水) ↓ 返回完整的 HTML 文档 ``` #### 5.2 客户端水合(Hydration) ``` 浏览器接收 HTML 文档 ↓ 执行 JavaScript ↓ StartClient 组件接管 ↓ 恢复组件状态 ↓ 绑定事件监听器 ↓ 应用进入交互状态 ``` --- ### 六、状态管理流程 #### 6.1 用户状态管理(Context + createAsync) ``` app.tsx 提供默认的 UserProvider ↓ (domain).tsx 布局组件使用 createAsync 获取用户信息 ↓ 通过 UserProvider 提供给所有子组件 ↓ 子组件通过 useUserContext() 获取用户信息 ↓ 状态变更自动触发 UI 更新 ``` #### 6.2 架构说明 **为什么在布局组件中获取用户信息?** 由于 SolidStart 的限制,`createAsync` 只能在路由组件中使用,因此: 1. **app.tsx**:只提供默认的空 UserProvider 2. **(domain).tsx**:作为路由组件(布局),使用 `createAsync` 获取用户信息 3. **子组件**:通过 `useUserContext()` 获取用户信息 #### 6.3 使用示例 **在布局组件中((domain).tsx)**: ```typescript // 使用 createAsync 获取用户信息(只能在路由组件中使用) const userinfo = createAsync(() => getUser()); // 创建认证检查状态 const [authChecked, setAuthChecked] = createSignal(false); // 处理认证检查 createEffect(() => { const info = userinfo(); if (info === undefined) return; setAuthChecked(true); if (info && info.code === 401) { navigate('/login', { replace: true }); } }); // 通过 UserProvider 提供给子组件 userinfo() === undefined, isAuthenticated: () => userinfo() && userinfo().code !== 401 }}> {props.children} ``` **在子组件中(index.tsx、navbar.tsx)**: ```typescript // 使用 Hook 获取用户信息 const { user, loading, isAuthenticated } = useUserContext(); // 使用用户信息 {(info) => (
{info()?.data?.username}
)}
``` #### 6.4 认证保护机制 为了避免页面闪烁,采用双重加载保护: ```typescript // 1. Suspense 处理异步数据加载 }> // 2. Show 组件在认证检查完成前显示 Loading }> {/* 页面内容 */} ``` **工作流程**: 1. 用户访问页面 → 显示 Loading(Suspense fallback) 2. 用户信息加载完成 → 检查认证状态 3. 未登录 → 跳转到登录页(replace: true) 4. 已登录 → 显示页面内容 --- ### 七、路由系统 #### 7.1 路由文件命名规则 ``` routes/ ├── (auth)/ # 路由组:认证相关 │ ├── login/ │ │ └── index.tsx # /login │ └── register/ │ └── index.tsx # /register ├── (domain)/ # 路由组:管理后台 │ └── index.tsx # / (首页) ├── (domain).tsx # 管理后台布局 └── [...404].tsx # 404 页面 ``` #### 7.2 路由匹配规则 - `(name)` - 路由组,不影响 URL 路径 - `[id]` - 动态参数,可通过 `useParam()` 获取 - `[...404]` - 通配符路由,匹配所有未匹配的路由 --- ### 八、组件通信 #### 8.1 Props 传递 ```typescript // 父组件 // 子组件 interface Props { data: DataType; onAction: () => void; } ``` #### 8.2 Context 传递(推荐用于全局状态) **创建 Context**: ```typescript // contexts/user-context.tsx import { createContext, useContext, Accessor } from "solid-js"; // 定义接口 export interface UserContextType { user: Accessor; loading: Accessor; isAuthenticated: Accessor; } // 创建 Context const UserContext = createContext(); // 导出 Hook export function useUserContext(): UserContextType { const context = useContext(UserContext); if (!context) { throw new Error("useUserContext must be used within a UserProvider"); } return context; } // 导出 Provider export function UserProvider(props: { children: JSX.Element; value: UserContextType }) { return {props.children}; } ``` **使用 Context**: ```typescript // 在布局组件中提供 Context {props.children} // 在子组件中使用 Context const { user, loading, isAuthenticated } = useUserContext(); ``` **优势**: - 任何层级的子组件都可以直接获取 - 不需要层层传递 props - 代码简洁,易于维护 --- ### 九、错误处理 #### 9.1 HTTP 错误处理(增强版) ```typescript try { const data = await http.get('/api/data'); } catch (error) { // 增强的错误分类 if (error.name === 'TimeoutError') { console.error('请求超时:', error.message); // 处理超时错误(如显示重试按钮) } else if (error.name === 'NetworkError') { console.error('网络连接错误:', error.message); // 处理网络错误(如显示网络诊断提示) } else if (error.name === 'RequestError') { console.error('请求失败:', error.message); // 处理一般请求错误 } else if (error.name === 'UnknownError') { console.error('未知错误:', error.message); // 处理未知错误 } else if (error instanceof HttpError) { // 处理 HTTP 状态码错误 switch (error.status) { case 401: // 未授权,Token 过期 break; case 403: // 禁止访问 break; case 404: // 资源不存在 break; case 500: // 服务器错误 break; } } } ``` **错误类型说明**: | 错误类型 | 触发条件 | 处理建议 | |---------|---------|---------| | `TimeoutError` | 请求超过设定的超时时间 | 显示重试按钮,提示用户检查网络 | | `NetworkError` | 网络连接失败(fetch 抛出 TypeError) | 显示网络诊断提示,检查网络连接 | | `RequestError` | 请求执行过程中的其他错误 | 记录错误日志,显示通用错误提示 | | `UnknownError` | 非 Error 类型的异常 | 记录详细日志,联系技术支持 | **错误对象特性**: - 保留原始错误堆栈信息 - 提供详细的错误消息(包含 URL 和超时时间) - 支持错误链追踪 #### 9.2 表单验证错误 ```typescript // 在 Action 中验证 export const loginAction = action(async (formData: FormData) => { const email = formData.get('email') as string; const password = formData.get('password') as string; // 验证 if (!email || !password) { return { error: '请填写完整信息' }; } // 执行登录 // ... }); ``` --- ### 十、Context 上下文系统 #### 10.1 用户上下文架构 (user-context.tsx) **设计目标**: - 提供全局用户状态管理 - 支持跨组件状态共享 - 统一认证状态管理 **核心接口定义**: ```typescript /** * 用户信息数据结构 */ interface UserInfo { code?: number; // 响应状态码 msg?: string; // 响应消息 data?: { id?: string | number; // 用户ID nickname?: string; // 用户昵称 avatar?: string; // 用户头像URL email?: string; // 用户邮箱 [key: string]: any; // 其他扩展属性 }; statusCode?: number; // HTTP状态码 } /** * Store 状态接口 */ interface UserState { info: UserInfo | null; // 用户信息 loading: boolean; // 加载状态 isAuthenticated: boolean; // 认证状态 } /** * 操作接口 - 集中管理修改状态的方法 */ interface UserActions { sync: () => Promise; // 同步用户信息 updateUser?: (data: Partial) => void; // 更新用户信息 logout: () => void; // 用户登出 } /** * Context 类型定义 */ interface UserContextType { state: UserState; // 用户状态 actions: UserActions; // 用户操作 } ``` **使用方式**: ```typescript // 1. 在布局组件中提供 Context userinfo() === undefined, isAuthenticated: () => userinfo() && userinfo().code !== 401 }, actions: { sync: syncInfo, updateUser: (data) => { /* 更新逻辑 */ }, logout: () => { /* 登出逻辑 */ } } }}> {props.children} // 2. 在子组件中使用 Context const { state, actions } = useUserContext(); // 访问用户信息 state.info?.data?.nickname state.info?.data?.avatar // 调用操作 actions.sync(); // 同步用户信息 actions.logout(); // 用户登出 ``` **优势**: - 任何层级的子组件都可以直接获取 - 不需要层层传递 props - 状态变更自动触发 UI 更新 - 代码简洁,易于维护 #### 10.2 Context 与 Store 的关系 ``` (domain).tsx (布局组件) ↓ 创建 UserProvider,初始化 state 和 actions ↓ ┌─────────────────────────────────────────┐ │ State (响应式状态) │ │ - info: 用户信息 │ │ - loading: 加载状态 │ │ - isAuthenticated: 认证状态 │ └─────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────┐ │ Actions (操作方法) │ │ - sync(): 同步用户信息 │ │ - updateUser(): 更新用户信息 │ │ - logout(): 用户登出 │ └─────────────────────────────────────────┘ ↓ 所有子组件通过 useUserContext() 访问 ``` --- ### 十一、修改头像功能 #### 11.1 功能架构 修改头像功能采用 **Server Action + Context 更新** 的架构: ``` 用户选择头像文件 ↓ 前端预览和验证 ↓ 提交到 Server Action ↓ 后端 API 处理上传 ↓ 返回新头像 URL ↓ 更新 Context 状态 ↓ UI 自动同步更新 ``` #### 11.2 组件结构 (change-avatar/index.tsx) **组件职责**: - 头像文件选择和预览 - 文件格式和大小验证 - 调用 Server Action 上传 - 更新全局状态实现同步 **核心代码逻辑**: ```typescript export default function ChangeAvatar() { // 1. 获取全局 Store 状态和操作方法 const { state, actions } = useUserContext(); // 2. 本地 UI 状态 const [avatarPreview, setAvatarPreview] = createSignal(""); const [isPending, setIsPending] = createSignal(false); // 3. 绑定 Server Action const runUpload = useAction(changeAvatarAction); // 4. 文件验证 const validateFile = (file: File) => { const limitSize = 5 * 1024 * 1024; // 5MB const allows = ["image/png", "image/jpeg", "image/jpg", "image/webp"]; if (file.size > limitSize) { alert("头像不能超过5MB!"); return false; } if (!allows.includes(file.type)) { alert("只支持 png, jpg, webp 格式"); return false; } return true; }; // 5. 处理上传 async function handleUpload(e: Event) { e.preventDefault(); const formData = new FormData(form); // 从全局 Store 获取 UID formData.append("uid", state.info?.data?.uid || ""); try { const result = await runUpload(formData); if (result && result.success) { // ✅ 关键:更新全局 Store,让 Navbar 和 Profile 瞬间同步 if (result.data?.url) { actions.updateUserLocal({ avatar: result.data.url }); } else { await actions.sync(); } handleClose(); } } catch (err) { console.error("Upload Error:", err); } } } ``` #### 11.3 Server Action (change-avatar.action.ts) **职责**: - 服务端处理头像上传 - 从 Cookie 获取认证信息 - 调用后端 API 更新头像 ```typescript export const changeAvatarAction = action(async (formData: FormData) => { "use server" // 1. 获取用户ID const uid = formData.get("uid"); // 2. 从 Cookie 获取 Token const event = getRequestEvent(); const cookieString = event.request.headers.get("Cookie"); const cookies = parse(cookieString || ""); const session_id = cookies[Config.session.session_key]; const { access_token, refresh_token } = JSON.parse(atob(session_id || "")); // 3. 调用后端 API try { const response = await fetch( `http://localhost:3000/user/update-avatar/${uid}`, { method: "PATCH", headers: { "Authorization": `Bearer ${access_token}` }, body: formData } ); const data = await response.json(); return data; // 返回 { code, msg, data: { url } } } catch (e: any) { return { code: 500, msg: e.message }; } }); ``` #### 11.4 状态同步机制 **关键设计**:上传成功后立即更新全局状态 ```typescript // 方式一:直接更新本地状态(推荐) if (result.data?.url) { actions.updateUserLocal({ avatar: result.data.url }); } // 方式二:全量同步用户信息 await actions.sync(); ``` **同步效果**: - Navbar 中的用户头像立即更新 - Profile 页面的头像立即更新 - 无需刷新页面 - 所有使用 `useUserContext()` 的组件自动同步 #### 11.5 文件处理流程 ``` 用户选择文件 ↓ FileReader 读取文件 ↓ 生成 DataURL 预览 ↓ 显示预览图 ↓ 用户确认上传 ↓ FormData 封装文件 ↓ 添加 UID 到 FormData ↓ 调用 Server Action ↓ 后端处理并返回 URL ↓ 更新 Context 状态 ↓ 关闭弹窗,显示成功提示 ``` **支持的格式**: - PNG - JPEG/JPG - WebP **大小限制**:5MB --- ### 十二、性能优化 #### 12.1 代码分割 ``` routes/ 目录自动分割 ↓ 每个路由文件独立打包 ↓ 按需加载路由组件 ↓ 减少初始加载体积 ``` #### 12.2 组件懒加载 ```typescript const LazyComponent = lazy(() => import('./LazyComponent')); }> ``` #### 12.3 信号优化 ```typescript // 使用 createMemo 缓存计算结果 const computedValue = createMemo(() => { return expensiveCalculation(signal()); }); // 使用 createEffect 副作用 createEffect(() => { console.logger('Value changed:', signal()); }); ``` --- ## 开发指南 ### 环境配置 ```bash # 安装依赖 npm install # 启动开发服务器 npm run dev # 构建生产版本 npm run build ``` ### 代码规范 - 使用 TypeScript 编写代码 - 遵循 SolidJS 最佳实践 - 组件文件使用 `.tsx` 扩展名 - 工具文件使用 `.ts` 扩展名 - 每个函数添加详细的注释说明 ### Git 提交规范 ``` feat: 新功能 fix: 修复 bug docs: 文档更新 style: 代码格式调整 refactor: 重构代码 test: 测试相关 chore: 构建/工具链相关 ``` --- ## 常见问题 ### Q1: 为什么使用 Cookie 而不是 localStorage? **A**: 在服务端渲染(SSR)架构中,Cookie 是服务端和客户端之间传递认证信息的标准方式: - Cookie 会自动在每个请求中发送 - 服务端可以直接读取 Cookie 中的 session - 可以设置 httpOnly 属性防止 XSS 攻击 - 可以设置 secure 属性确保只在 HTTPS 下传输 ### Q2: 如何处理 Token 过期? **A**: 系统自动处理 Token 过期: 1. 中间件拦截请求,检查 Token 是否过期 2. 如果过期,自动使用 refresh_token 刷新 3. 如果 refresh_token 也过期,自动跳转登录页 4. 刷新成功后,自动重试原请求 ### Q3: 如何添加新的路由? **A**: 在 `routes/` 目录下创建对应的文件: - 创建文件夹作为路由组(如 `(new-group)`) - 在文件夹内创建 `index.tsx` 作为页面 - 创建 `(domain).tsx` 作为布局文件 ### Q4: 如何调用后端 API? **A**: 使用 `utils/$.ts` 中的 HTTP 客户端: ```typescript import { http } from '@/utils/$.ts'; // GET 请求 const data = await http.get('/api/data'); // POST 请求 const result = await http.post('/api/create', { name: 'test' }); ``` --- # 所有的原语+hooks ## 一、SolidJS 核心原语 ### 1.1 信号 (Signals) 信号是 SolidJS 的响应式基础,用于创建可观察的状态。 #### createSignal 创建响应式状态,返回 getter 和 setter。 ```typescript import { createSignal } from "solid-js"; // 基本用法 const [count, setCount] = createSignal(0); // 访问值 console.log(count()); // 0 // 更新值 setCount(1); setCount(prev => prev + 1); // 函数式更新 // 在 JSX 中使用
{count()}
``` **特点**: - 细粒度响应式,只有使用的地方会重新计算 - 支持函数式更新 - 自动追踪依赖 #### createMemo 创建记忆化的计算值,只在依赖变化时重新计算。 ```typescript import { createMemo } from "solid-js"; const [firstName, setFirstName] = createSignal("John"); const [lastName, setLastName] = createSignal("Doe"); // 记忆化计算全名 const fullName = createMemo(() => { console.log("Computing full name..."); return `${firstName()} ${lastName()}`; }); // 多次访问不会重复计算 console.log(fullName()); // John Doe console.log(fullName()); // John Doe(不会触发 console.log) // 只有依赖变化时才重新计算 setFirstName("Jane"); // 触发重新计算 ``` **使用场景**: - 昂贵的计算 - 派生状态 - 缓存计算结果 #### createEffect 创建副作用,在依赖变化时执行。 ```typescript import { createEffect } from "solid-js"; const [count, setCount] = createSignal(0); // 创建副作用 createEffect(() => { console.log("Count changed:", count()); document.title = `Count: ${count()}`; }); // 更新时会触发副作用 setCount(1); // 输出: Count changed: 1 ``` **注意**: - 自动追踪依赖 - 首次立即执行 - 清理函数支持 ### 1.2 资源管理 #### createResource 创建异步资源,处理数据获取和加载状态。 ```typescript import { createResource } from "solid-js"; // 定义获取函数 const fetchUser = async (id: number) => { const response = await fetch(`/api/users/${id}`); return response.json(); }; // 创建资源 const [user, { refetch, mutate }] = createResource(userId, fetchUser); // 在组件中使用
}>

{user().name}

``` **返回对象**: - `user()` - 数据 - `user.loading` - 加载状态 - `user.error` - 错误信息 - `refetch()` - 重新获取 - `mutate()` - 直接修改数据 ### 1.3 条件渲染 #### Show 条件渲染组件。 ```typescript import { Show } from "solid-js"; const [isLoggedIn, setIsLoggedIn] = createSignal(false); } > // 使用 keyed 模式(每次条件变化都重新创建) {(userData) =>
{userData.name}
}
``` #### For 循环渲染组件。 ```typescript import { For } from "solid-js"; const [items, setItems] = createSignal([ { id: 1, name: "Item 1" }, { id: 2, name: "Item 2" }, ]);
    {(item, index) => (
  • {index()}: {item.name}
  • )}
``` **特点**: - 高效的 diff 算法 - 保持 DOM 状态 - 支持索引访问 #### Switch / Match 多条件匹配渲染。 ```typescript import { Switch, Match } from "solid-js"; Not found}> ``` #### Portal 将子元素渲染到 DOM 的其他位置。 ```typescript import { Portal } from "solid-js/web"; // 渲染到 body 末尾 // 渲染到指定容器 ``` **使用场景**: - Modal 对话框 - Toast 提示 - Tooltip #### Suspense 处理异步加载状态。 ```typescript import { Suspense } from "solid-js"; }> ``` ### 1.4 生命周期 #### onMount 组件挂载时执行。 ```typescript import { onMount } from "solid-js"; onMount(() => { console.log("Component mounted"); // 初始化逻辑 // 返回清理函数(可选) return () => { console.log("Cleanup"); }; }); ``` #### onCleanup 组件卸载或重新执行时清理。 ```typescript import { onCleanup } from "solid-js"; const [count, setCount] = createSignal(0); createEffect(() => { const timer = setInterval(() => { setCount(c => c + 1); }, 1000); // 清理函数 onCleanup(() => { clearInterval(timer); }); }); ``` ### 1.5 其他原语 #### batch 批量更新,减少重新渲染次数。 ```typescript import { batch } from "solid-js"; const [first, setFirst] = createSignal(""); const [last, setLast] = createSignal(""); // 批量更新 batch(() => { setFirst("John"); setLast("Doe"); // 只会触发一次重新渲染 }); ``` #### untrack 在响应式上下文中读取值但不追踪依赖。 ```typescript import { untrack } from "solid-js"; createEffect(() => { console.log(count()); // 追踪依赖 console.log(untrack(count)); // 不追踪依赖 }); ``` #### mergeProps 合并多个 props 对象。 ```typescript import { mergeProps } from "solid-js"; const merged = mergeProps( { color: "blue", size: "medium" }, props ); ``` #### splitProps 将 props 拆分为多个对象。 ```typescript import { splitProps } from "solid-js"; const [local, others] = splitProps(props, ["class", "style"]); // local.class, local.style // others 包含剩余的 props ``` #### children 解析 children 为响应式信号。 ```typescript import { children } from "solid-js"; const resolved = children(() => props.children); // 在 effect 中监听 children 变化 createEffect(() => { console.log("Children:", resolved()); }); ``` --- ## 二、SolidStart 专用 Hooks ### 2.1 路由相关 #### useParams 获取 URL 参数。 ```typescript import { useParams } from "@solidjs/router"; // URL: /users/:id const params = useParams(); // 访问参数 console.log(params.id); ``` #### useSearchParams 获取查询参数。 ```typescript import { useSearchParams } from "@solidjs/router"; // URL: /search?q=keyword&page=1 const [searchParams, setSearchParams] = useSearchParams(); // 访问查询参数 console.log(searchParams.q); // "keyword" console.log(searchParams.page); // "1" // 更新查询参数 setSearchParams({ page: 2 }); ``` #### useLocation 获取当前位置信息。 ```typescript import { useLocation } from "@solidjs/router"; const location = useLocation(); console.log(location.pathname); // 当前路径 console.log(location.search); // 查询字符串 console.log(location.hash); // 哈希值 console.log(location.state); // 状态数据 ``` #### useNavigate 编程式导航。 ```typescript import { useNavigate } from "@solidjs/router"; const navigate = useNavigate(); // 基本导航 navigate("/home"); // 带选项 navigate("/login", { replace: true, // 替换当前历史记录 state: { from: "/dashboard" }, // 传递状态 }); // 返回上一页 navigate(-1); ``` #### useIsRouting 检查是否正在路由切换。 ```typescript import { useIsRouting } from "@solidjs/router"; const isRouting = useIsRouting(); // 在 JSX 中使用 ``` ### 2.2 数据获取 #### createAsync 在路由组件中异步获取数据。 ```typescript import { createAsync } from "@solidjs/router"; // 定义获取函数 const getUser = async () => { const response = await fetch("/api/user"); if (!response.ok) throw new Error("Failed to fetch"); return response.json(); }; // 在路由组件中使用 export default function Profile() { const user = createAsync(getUser); return ( }>
{user()?.name}
); } ``` **特点**: - 服务端渲染时自动获取数据 - 客户端水合时复用服务端数据 - 支持缓存和重新验证 #### useRouteData 访问路由数据(已废弃,推荐使用 createAsync)。 ### 2.3 Server Actions #### action 定义服务端 action。 ```typescript import { action } from "@solidjs/router"; export const myAction = action(async (data: FormData) => { "use server"; // 服务端逻辑 const result = await processData(data); return result; }, "action_name"); // 可选:action 名称 ``` #### useAction 在组件中使用 action。 ```typescript import { useAction } from "@solidjs/router"; import { myAction } from "~/actions/my-action"; export default function MyComponent() { const runAction = useAction(myAction); const handleSubmit = async (e: Event) => { e.preventDefault(); const formData = new FormData(e.target as HTMLFormElement); const result = await runAction(formData); console.log(result); }; return
...
; } ``` #### useSubmission 获取 action 提交状态。 ```typescript import { useSubmission } from "@solidjs/router"; export default function MyComponent() { const submission = useSubmission(myAction); return (
{(result) =>
Result: {result}
}
{(error) =>
Error: {error.message}
}
); } ``` **状态属性**: - `pending` - 是否正在提交 - `result` - 提交结果 - `error` - 错误信息 - `input` - 提交的输入数据 #### useSubmissions 获取多个 action 提交状态(用于乐观更新)。 ```typescript import { useSubmissions } from "@solidjs/router"; const submissions = useSubmissions(myAction); // 遍历所有提交 {(submission) => (
{submission.pending ? "Saving..." : "Saved"}
)}
``` --- ## 三、RPC (Remote Procedure Call) 使用方式 ### 3.1 基本概念 SolidStart 的 RPC 系统允许你在客户端直接调用服务端函数,就像调用本地函数一样。 **核心特点**: - 类型安全 - 自动序列化/反序列化 - 支持 FormData 和 JSON - 服务端代码自动隔离 ### 3.2 Action 定义 #### 基本 Action ```typescript // src/actions/my-action.ts import { action } from "@solidjs/router"; export const myAction = action(async (formData: FormData) => { "use server"; // 标记为服务端代码 // 服务端逻辑 const name = formData.get("name"); const result = await db.create({ name }); return { success: true, data: result }; }, "my_action"); // 可选:action 名称用于调试 ``` #### 带验证的 Action ```typescript // src/actions/login/login.action.ts import { action, redirect } from "@solidjs/router"; import { setSession } from "~/lib/session"; import { $ } from "~/utils/$"; export const loginAction = action(async (formData: FormData) => { "use server"; const email = formData.get('email'); const password = formData.get('password'); try { // 调用后端 API const data: any = await $.post("/user/login", { email, password }); if (data.code === 0) { // 登录成功,设置 session await setSession({ access_token: data.data.access_token, refresh_token: data.data.refresh_token, }); // 重定向到首页 throw redirect("/"); } else { return { code: data.code, msg: data.msg }; } } catch (error) { if (error instanceof Response) throw error; return { code: 500, msg: "登录服务异常" }; } }, "auth_login"); ``` #### 文件上传 Action ```typescript // src/actions/upload-file.action.ts import { action } from "@solidjs/router"; import { getRequestEvent } from "solid-js/web"; import { parse } from "cookie"; import Config from "~/utils/config"; export const changeAvatarAction = action(async (formData: FormData) => { "use server"; const uid = formData.get("uid"); const event = getRequestEvent(); // 从 Cookie 获取 Token const cookieString = event.request.headers.get("Cookie"); const cookies = parse(cookieString || ""); const session_id = cookies[Config.session.session_key]; const { access_token } = JSON.parse(atob(session_id || "")); // 调用后端 API const response = await fetch( `http://localhost:3000/user/update-avatar/${uid}`, { method: "PATCH", headers: { "Authorization": `Bearer ${access_token}` }, body: formData // 直接传递 FormData } ); return await response.json(); }); ``` ### 3.3 Action 调用 #### 基本调用 ```typescript // src/routes/login/index.tsx import { useAction, useSubmission } from "@solidjs/router"; import { loginAction } from "~/actions/login/login.action"; export default function LoginPage() { const login = useAction(loginAction); const loginState = useSubmission(loginAction); const handleSubmit = async (e: SubmitEvent) => { e.preventDefault(); const formData = new FormData(e.currentTarget as HTMLFormElement); try { const result = await login(formData); if (result && result.code !== 0) { // 显示错误 alert(result.msg); } // 成功时会自动重定向 } catch (err) { console.error(err); } }; return (
); } ``` #### 处理提交状态 ```typescript export default function MyComponent() { const runAction = useAction(myAction); const submission = useSubmission(myAction); return (
{/* 加载状态 */} {/* 成功结果 */} {(result) => (
操作成功: {JSON.stringify(result())}
)}
{/* 错误信息 */} {(error) => (
错误: {error().message}
)}
); } ``` ### 3.4 RPC 工作流程 ``` 客户端调用 useAction(action) ↓ 生成唯一的 action ID ↓ 序列化参数(FormData/JSON) ↓ 发送 HTTP POST 请求到 /_server ↓ 服务端接收请求 ↓ 根据 action ID 找到对应的 action 函数 ↓ 执行 "use server" 标记的代码 ↓ 返回结果 ↓ 客户端反序列化结果 ↓ 更新 submission 状态 ↓ 触发 UI 更新 ``` ### 3.5 最佳实践 #### 1. 错误处理 ```typescript export const myAction = action(async (data: FormData) => { "use server"; try { const result = await processData(data); return { success: true, data: result }; } catch (error) { // 返回统一的错误格式 return { success: false, error: error instanceof Error ? error.message : "未知错误" }; } }); // 客户端处理 const result = await runAction(data); if (!result.success) { // 处理错误 console.error(result.error); } else { // 处理成功 console.log(result.data); } ``` #### 2. 重定向 ```typescript import { redirect } from "@solidjs/router"; export const loginAction = action(async (formData: FormData) => { "use server"; const success = await authenticate(formData); if (success) { throw redirect("/dashboard"); // 使用 throw 触发重定向 } return { error: "认证失败" }; }); ``` #### 3. 表单验证 ```typescript export const registerAction = action(async (formData: FormData) => { "use server"; const email = formData.get("email") as string; const password = formData.get("password") as string; // 服务端验证 if (!email || !email.includes("@")) { return { field: "email", error: "邮箱格式不正确" }; } if (!password || password.length < 6) { return { field: "password", error: "密码至少6位" }; } // 继续处理... }); ``` #### 4. 乐观更新 ```typescript import { useSubmissions } from "@solidjs/router"; export default function TodoList() { const submissions = useSubmissions(addTodoAction); const [todos, setTodos] = createSignal([]); return (
    {/* 显示已保存的 todos */} {(todo) =>
  • {todo.text}
  • }
    {/* 显示正在提交的 todos(乐观更新) */} {(submission) => (
  • {submission.input.get("text")}
  • )}
); } ``` ### 3.6 与传统 API 对比 | 特性 | RPC (Action) | 传统 REST API | |------|-------------|--------------| | 类型安全 | ✅ 自动生成 | ❌ 手动维护 | | 代码组织 | 服务端/客户端同文件 | 分离的 API 文件 | | 调用方式 | 像调用函数 | HTTP 请求 | | 参数序列化 | 自动 | 手动 | | 错误处理 | 统一的异常处理 | 手动处理 HTTP 错误 | | 渐进增强 | 原生支持 | 需要额外配置 | --- ## 总结 本项目采用 SolidStart 框架构建,实现了完整的服务端渲染架构,具有以下特点: 1. **服务端渲染(SSR)**: 提升首屏加载速度和 SEO 2. **自动 Token 刷新**: 无缝处理 Token 过期问题 3. **中间件拦截**: 统一处理认证和权限 4. **组件化架构**: 高度模块化,易于维护 5. **Context 状态管理**: 使用 Context API 实现全局状态共享 6. **createAsync 数据获取**: 在路由组件中高效获取服务端数据 7. **双重加载保护**: 避免页面闪烁,提升用户体验 8. **类型安全**: 全面使用 TypeScript 9. **响应式设计**: 基于 TailwindCSS 和 DaisyUI 通过合理的设计和架构,项目具有良好的可维护性、可扩展性和用户体验。 # 大文件上传的流程 - 1GB文件上传的速度不会很快 - 上传的时候 需要 进度条 - 文件总量 : files[0].size - 文件切片 : files[0].slice(0,1024*1024*5) - 每一个 chunk , 都需要独立上传 - 1.初始化 , 告诉一个 后端 我有多少个 文件块 , 文件总体积有多大 - 2.按顺序 传递 chunk ..... - 3.发出 chunk 的合并请求 , 后端把刚刚的小块 合并为一个 完整的文件 - 回复成功 - 返回失败 , 清理所有的 垃圾块