diff --git a/README.md b/README.md index 441afe9..199218e 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,9 @@ simapi-net 所有接口的响应格式固定如下(HTTP 状态码始终 200) ### 1.2 Token 认证 - 前端通过请求头 `Token: ` 传递认证令牌 -- Token 存储在 localStorage,key 默认为 `simapi-auth-token` -- **不使用 Cookie / 不使用 Authorization: Bearer** +- Token 存储在 Cookie 中,key 默认为 `simapi-auth-token` +- Cookie 属性: `path=/; secure; samesite=none` +- **不使用 Authorization: Bearer** ### 1.3 请求方式 @@ -74,57 +75,33 @@ npm install @simcu/simapi pinia > `pinia` 是 peerDependency,Vue3 项目必须安装。 -### 3.2 第一步:public/config.js — 外部配置文件 +### 3.2 第一步:public/config.json — 外部配置文件 -在项目的 `public/` 目录下创建 `config.js`,定义 API 地址: +在项目的 `public/` 目录下创建 `config.json`,定义 API 地址: -```javascript -// public/config.js -window.simapi = { - debug: true, - endpoints: { - default: "http://127.0.0.1:5210" // 后端 simapi-net 服务地址 - // default: "https://api.example.com" // 生产环境 +```json +{ + "debug": true, + "endpoints": { + "default": "http://127.0.0.1:5210" } } ``` -**为什么用 config.js 而不是硬编码?** +**为什么用 config.json 而不是硬编码?** - 前后端分离部署时,API 地址可能变化 - `public/` 下的文件 Vite 会直接复制到输出目录,不经过构建 -- 打包后运维人员可以直接修改 `config.js` 切换环境,无需重新构建 +- 打包后运维人员可以直接修改 `config.json` 切换环境,无需重新构建 -**config.js 支持的字段:** +**config.json 支持的字段:** | 字段 | 类型 | 说明 | 默认值 | |------|------|------|--------| | `debug` | boolean | 是否打印请求/响应日志 | `false` | -| `endpoints` | object | 多端点地址映射 | `{ default: '' }` | -| `defaultEndpoint` | string | 默认使用的端点名称 | `'default'` | +| `endpoints` | object | 多端点地址映射 | `{ "default": "" }` | +| `defaultEndpoint` | string | 默认使用的端点名称 | `"default"` | -### 3.3 第二步:index.html — 引入 config.js - -```html - - - - - - - My App - - - - -
- - - -``` - -> ` @@ -184,12 +157,12 @@ onMounted(() => { | 要点 | 说明 | |------|------| | `import from '@simcu/simapi/pinia'` | Vue3 项目**必须**用 `/pinia` 子路径导入 | -| `onMounted` 中初始化 | 确保 DOM 已加载、config.js 已执行、Pinia 已就绪 | -| `autoInit()` | 读取 `window.simapi` 的 `endpoints`、`defaultEndpoint`、`debug` | +| `onMounted` 中初始化 | 确保 DOM 已加载、Pinia 已就绪 | +| `await api.loadFromFile()` | 从 `config.json` 加载 endpoints、defaultEndpoint、debug | | `setBusinessCallback(401, fn)` | 当后端返回 code=401 时自动执行 | | `setBusinessCallback('common', fn)` | 兜底回调,任何非 200 且未匹配其他回调时触发 | -### 3.6 第五步:在组件中使用 +### 3.5 第四步:在组件中使用 ```vue @@ -213,7 +186,6 @@ const api = useSimApi() const users = ref([]) async function loadUsers() { - // query 返回 Promise>,code !== 200 时会抛出异常 const res = await api.query('/user/list', { page: 1 }) users.value = res.data ?? [] } @@ -224,55 +196,17 @@ async function loadUsers() { ## 4. 完整项目模板(可直接复制使用) -以下是一个完整的 Vue3 + simapi-vue 项目初始化清单: +### public/config.json -### 文件清单 - -``` -my-project/ -├── public/ -│ └── config.js # ← API 配置(第 1 步) -├── index.html # ← 引入 config.js(第 2 步) -├── src/ -│ ├── main.ts # ← 注册 Pinia(第 3 步) -│ ├── App.vue # ← 初始化 SimApi(第 4 步) -│ ├── router/ -│ │ └── index.ts -│ └── views/ -│ └── Login.vue # ← 使用示例 -├── package.json # ← 依赖 -└── vite.config.ts -``` - -### public/config.js - -```javascript -window.simapi = { - debug: true, - endpoints: { - default: "http://localhost:5000" +```json +{ + "debug": true, + "endpoints": { + "default": "http://localhost:5000" } } ``` -### index.html - -```html - - - - - - My App - - - -
- - - -``` - ### src/main.ts ```typescript @@ -300,8 +234,8 @@ import { useRouter } from 'vue-router' const api = useSimApi() const router = useRouter() -onMounted(() => { - api.autoInit() +onMounted(async () => { + await api.loadFromFile() api.setBusinessCallback(401, () => { api.logout() router.replace('/login') @@ -333,7 +267,7 @@ const code = ref('') async function handleLogin() { await api.login({ phone: phone.value, code: code.value }) - router.push('/') // 登录成功自动存 Token,然后跳转首页 + router.push('/') } ``` @@ -374,7 +308,7 @@ const api = useSimApi() // 单例模式,全局状态共享 | 方法 | 参数 | 返回值 | 说明 | |------|------|--------|------| -| `autoInit()` | 无 | `void` | 从 `window.simapi` 读取 endpoints/debug 配置 | +| `loadFromFile(file?)` | string (默认 `'config.json'`) | `Promise` | 从 JSON 文件加载 endpoints/debug 配置 | | `configure(options)` | `SimApiOptions` | `void` | 手动配置(深合并) | | `setDebug(bool)` | boolean | `void` | 设置调试模式 | | `setEndpoints(map)` | `{[name]: url}` | `void` | 设置多端点映射 | @@ -409,15 +343,6 @@ async function query( const res = await api.query('/user/list', { page: 1, count: 20 }) console.log(res.data) // User[] 数组 -// 带参数 -const res = await api.query('/user/detail', { id: 'xxx' }) - -// 指定端点 -await api.query('/admin/stats', {}, 'admin') - -// 自定义请求头 -await api.query('/file/upload', formData, undefined, { 'Content-Type': 'multipart/form-data' }) - // 错误处理 try { const res = await api.query('/user/list') @@ -425,7 +350,6 @@ try { // err 是 SimApiBaseResponse 类型 console.log(err.code) // 业务错误码,如 400/401/403/500 console.log(err.message) // 错误消息 - console.log(err.data) // 可能携带的错误详情 } ``` @@ -442,8 +366,6 @@ try { ```typescript // login:POST 到 auth.login_url(默认 /auth/login),自动保存返回的 Token await api.login({ phone: '13800138000', code: '123456' }) -// 后端返回 { code: 200, data: "token-string" } -// 前端自动将 data 存入 localStorage // logout:清除本地 Token,可选调后端登出接口 await api.logout() // 调 /auth/logout @@ -453,9 +375,7 @@ await api.logout(null) // 只清本地 Token,不调后端 ### 5.5 setBusinessCallback — 业务错误处理 ```typescript -// 针对特定错误码 api.setBusinessCallback(401, (data) => { - console.log('未授权', data.message) router.replace('/login') }) @@ -478,36 +398,36 @@ api.setBusinessCallback('common', (data) => { ```typescript /** 标准响应 */ interface SimApiBaseResponse { - code: number // 200=成功,其他=业务错误码 - message: string // 提示信息 - data?: T // 业务数据 + code: number + message: string + data?: T } /** 版本信息 */ interface SimApiVersions { - uiApp: string // 前端应用版本 - uiSimApi: string // 前端 SimApi 版本 - apiApp: string // 后端应用版本(简化版) - apiSimApi: string // 后端 SimApi 版本(简化版) - apiAppFull: string // 后端应用版本(完整版) - apiSimApiFull: string // 后端 SimApi 版本(完整版) + uiApp: string + uiSimApi: string + apiApp: string + apiSimApi: string + apiAppFull: string + apiSimApiFull: string } /** 认证配置 */ interface SimApiAuthConfig { - token_name: string // localStorage key,默认 'simapi-auth-token' - check_url: string // 登录检查接口,默认 '/auth/check' - logout_url: string // 登出接口,默认 '/auth/logout' - login_url: string // 登录接口,默认 '/auth/login' + token_name: string + check_url: string + logout_url: string + login_url: string } /** API 配置 */ interface SimApiApiConfig { - endpoints: { [name]: string } // 多端点映射 - defaultEndpoint: string // 默认端点 - businessCallback: SimApiBusinessCallback // 错误码回调 - responseCallback: SimApiResponseCallback // 响应拦截器 - timeout?: number // 超时毫秒数,默认 10000 + endpoints: { [name]: string } + defaultEndpoint: string + businessCallback: SimApiBusinessCallback + responseCallback: SimApiResponseCallback + timeout?: number } /** 完整选项 */ @@ -522,27 +442,24 @@ interface SimApiOptions { ## 7. 多端点支持 -适用于需要连接多个后端服务的场景: - -```javascript -// config.js -window.simapi = { - debug: true, - endpoints: { - default: 'https://api.example.com', // 主服务 - admin: 'https://admin.example.com', // 管理后台服务 - cdn: 'https://cdn.example.com', // CDN/文件服务 +```json +{ + "debug": true, + "endpoints": { + "default": "https://api.example.com", + "admin": "https://admin.example.com", + "cdn": "https://cdn.example.com" }, - defaultEndpoint: 'default' + "defaultEndpoint": "default" } ``` ```typescript // 使用默认端点 -await api.query('/user/list') // → https://api.example.com/user/list +await api.query('/user/list') // 指定端点 -await api.query('/system/stats', {}, 'admin') // → https://admin.example.com/system/stats +await api.query('/system/stats', {}, 'admin') ``` 也可以运行时动态添加: @@ -555,45 +472,29 @@ api.setEndpoints({ backup: 'https://backup-api.example.com' }) ## 8. configure — 手动完整配置 -除了 autoInit 从 `window.simapi` 读取外,也可以手动配置一切: +除了 `loadFromFile` 从 `config.json` 读取外,也可以手动配置一切: ```typescript api.configure({ debug: false, - - auth: { - token_name: 'my-app-token', - check_url: '/auth/check', - logout_url: '/auth/logout', - login_url: '/auth/login', - }, - + auth: { token_name: 'my-app-token' }, api: { - endpoints: { - default: 'https://api.example.com', - }, + endpoints: { default: 'https://api.example.com' }, defaultEndpoint: 'default', timeout: 15000, - businessCallback: { 401: () => router.replace('/login'), 403: (data) => alert('无权限'), - 500: (data) => console.error('服务器错误', data), 'common': (data) => MessagePlugin.error(data.message), }, - - responseCallback: { - success: (res) => res, // 成功响应拦截(可做数据转换) - error: (err) => console.error('网络错误', err), - }, }, }) ``` -**configure vs autoInit 的关系:** -- `autoInit()` 只读 `window.simapi` 的 `endpoints`、`defaultEndpoint`、`debug` +**loadFromFile vs configure 的关系:** +- `loadFromFile()` 只读 `config.json` 的 `endpoints`、`defaultEndpoint`、`debug` - `configure()` 可以覆盖所有字段,包括 auth 和 callbacks -- 通常做法是:`autoInit()` 读基础配置 + `setBusinessCallback()` 补充回调 +- 通常做法是:`loadFromFile()` 读基础配置 + `setBusinessCallback()` 补充回调 --- @@ -603,13 +504,11 @@ api.configure({ |---------|---------| | `import { useSimApi } from '@simcu/simapi'` (Vue3) | `from '@simcu/simapi/pinia'`(必须带 `/pinia`) | | 忘记 `app.use(createPinia())` | **必须在 `useSimApi()` 之前**注册 Pinia | -| `config.js` 放在 `src/` 下 | 必须放在 `public/` 下,Vite 才能原样复制 | -| `index.html` 中不引 config.js 或放在 main.ts 之后 | config.js 必须**同步加载**且在模块代码之前执行 | | 用 `Authorization: Bearer xxx` | 用 `Token` 请求头(这是 simapi-net 约定) | | 期望 HTTP 4xx/5xx 表示错误 | 所有错误都是 **HTTP 200 + JSON `code` 字段** | -| `res.data` 直接用而不判空 | `res.data` 可能是 `undefined`,用 `res.data ?? []` 或 `res.data!` | -| 在 setup 外部调用 `useSimApi()` | `useSimApi()` 只能在 **setup 上下文**(或 pinia active)中调用 | -| 用 Cookie 传 Token | Token 通过 **请求头 `Token`** + **localStorage** 存储 | +| `res.data` 直接用而不判空 | `res.data` 可能是 `undefined`,用 `res.data ?? []` | +| 在 setup 外部调用 `useSimApi()` | `useSimApi()` 只能在 **setup 上下文**中调用 | +| 用 Authorization Bearer 传 Token | Token 通过 **请求头 `Token`** + **Cookie** 存储 | | `new SimApiCore()` 在 Vue 项目里用 | Vue 项目统一用 `useSimApi()` Pinia Store | --- @@ -629,8 +528,6 @@ api.configure({ ### 10.2 内置路由对照表 -simapi-net 启用认证后,自动生成以下路由,simapi-vue 已内置对应方法: - | simapi-net 路由 | simapi-vue 方法 | 触发条件 | |-----------------|-----------------|----------| | `POST /auth/login` | `api.login(request)` | `EnableSimApiAuth = true` | @@ -639,38 +536,13 @@ simapi-net 启用认证后,自动生成以下路由,simapi-vue 已内置对 | `POST /user/info` | `api.query('/user/info')` | `EnableSimApiAuth = true`(需登录) | | `GET /versions` | `api.getVersion()` | `EnableVersionUrl`(默认开启) | -### 10.3 登录流程示例 - -``` -用户输入手机号+验证码 - ↓ -前端: api.login({ phone: '138xx', code: '123456' }) - ↓ POST /auth/login { phone, code } -后端: 验证通过 → 返回 { code: 200, data: "token-string" } - ↓ -前端: 自动将 token 存入 localStorage['simapi-auth-token'] - ↓ -后续请求: api.query('/user/info') - ↓ 自动带上 Header: Token: token-string -后端: SimApiAuthMiddleware 解析 Token → HttpContext.Items["LoginInfo"] - ↓ Controller 可通过 LoginInfo 获取当前用户 -``` - --- ## 11. 构建 ```bash -# 开发模式(监听文件变化) -npm run dev - -# 生产构建 -npm run build - -# 本地 link 调试 -npm link -cd ../your-project -npm link @simcu/simapi +npm run dev # 开发模式 +npm run build # 生产构建 ``` 构建产物位于 `dist/` 目录: @@ -682,67 +554,23 @@ npm link @simcu/simapi 库使用 `declare const` 声明版本常量,构建时通过 Vite 的 `define` 注入。 -**注意:** -- **SimApiVersion**:由 simapi 库自身构建时注入 -- **AppVersion**:由**调用方项目**在自己的 `vite.config.ts` 中注入 +**SimApiVersion** 由 simapi 库自身构建时从 `package.json` 注入。未配置时默认为 `0.0.0-develop`。 -#### simapi 库自身构建 — 注入 SimApiVersion - -在 simapi-vue 的 `vite.config.ts` 中,从自身 `package.json` 读取版本号: - -```typescript -import { readFileSync } from 'node:fs' - -const pkg = JSON.parse(readFileSync('./package.json', 'utf-8')) - -export default defineConfig({ - // ... 其他配置 - define: { - SimApiVersion: JSON.stringify(pkg.version), // ← 取 simapi-vue 自身的 version - }, -}) -``` - -未配置时默认为 `0.0.0-develop`。 - -#### 调用方项目 — 注入 AppVersion(推荐方式:从 package.json 自动读取) - -在调用方项目的 `vite.config.ts` 中添加: - -```typescript -import { readFileSync } from 'node:fs' - -const pkg = JSON.parse(readFileSync('./package.json', 'utf-8')) - -export default defineConfig({ - // ... 其他配置 - define: { - AppVersion: JSON.stringify(pkg.version), // ← 自动取 package.json 的 version 字段 - }, -}) -``` - -这样每次发版只需改 `package.json` 的 `version`,无需同步修改其他地方。 +**AppVersion** 由调用方项目在自己的 `vite.config.ts` 中注入。 --- ## 12. 从 axios 迁移 -如果你之前用的是 axios: - ```diff - import axios from 'axios' - const res = await axios.post('/user/list', { page: 1 }) -- console.log(res.data) + import { useSimApi } from '@simcu/simapi/pinia' + const api = useSimApi() + const res = await api.query('/user/list', { page: 1 }) -+ console.log(res.data) // res.data 就是业务数据 ``` -主要区别: - | axios | simapi-vue | |-------|-----------| | `axios.post()` | `api.query()` | diff --git a/src/simapi.core.ts b/src/simapi.core.ts index bc66c90..5572fa0 100644 --- a/src/simapi.core.ts +++ b/src/simapi.core.ts @@ -108,23 +108,34 @@ export class SimApiCore { } /** - * 从 window.simapi 读取配置并初始化 + * 从 JSON 文件加载配置 * - * 支持字段:endpoints, defaultEndpoint, debug - * 业务回调(businessCallback / responseCallback)需在代码中处理 + * 部署时替换 config.json 即可切换环境,无需重新构建。 + * + * @param file - 配置文件路径,默认 'config.json' + * @example + * await api.loadFromFile() + * await api.loadFromFile('/env/prod.json') */ - autoInit(): void { - const config = (window as any).simapi - if (!config) return - - if (config.debug !== undefined) { - this.debug = config.debug - } - if (config.endpoints) { - this.api.endpoints = {...this.api.endpoints, ...config.endpoints} - } - if (config.defaultEndpoint) { - this.api.defaultEndpoint = config.defaultEndpoint + async loadFromFile(file: string = 'config.json'): Promise { + try { + const resp = await fetch(file) + if (!resp.ok) { + this.logDebug(`配置文件 ${file} 加载失败: HTTP ${resp.status}`) + return + } + const config = await resp.json() + if (config.debug !== undefined) { + this.debug = config.debug + } + if (config.endpoints) { + this.api.endpoints = { ...this.api.endpoints, ...config.endpoints } + } + if (config.defaultEndpoint) { + this.api.defaultEndpoint = config.defaultEndpoint + } + } catch (err: any) { + this.logDebug(`配置文件 ${file} 加载失败: ${err.message}`) } } diff --git a/src/simapi.pinia.ts b/src/simapi.pinia.ts index c9a1cc2..d179257 100644 --- a/src/simapi.pinia.ts +++ b/src/simapi.pinia.ts @@ -15,8 +15,8 @@ export const useSimApi = defineStore('simapi', { }, actions: { // 所有方法直接代理到 core - autoInit(): void { - this._core.autoInit() + async loadFromFile(file: string = 'config.json'): Promise { + return this._core.loadFromFile(file) }, configure(options: SimApiOptions): void { @@ -81,5 +81,3 @@ export const useSimApi = defineStore('simapi', { }, }, }) - -