xrain e0754ad635
Publish to npm / publish (push) Failing after 8s
query 增加了自处理异常
2026-05-04 02:27:43 +08:00
2026-04-23 21:39:57 +08:00
2026-05-04 02:27:43 +08:00
2026-04-23 21:39:38 +08:00
2026-04-23 21:39:38 +08:00
2026-04-26 10:44:40 +08:00
2026-04-08 02:44:04 +08:00
2026-04-22 22:08:35 +08:00
2026-04-23 21:39:38 +08:00

@simcu/simapi — SimApi Vue 前端库(AI 编码参考)

包名: @simcu/simapi | 技术栈: TypeScript + Vue3 + Pinia + 原生 fetch 后端对应: simapi-netSimcu.SimApi NuGet 包) 性质: simapi-net 的官方前端 HTTP 客户端,专为其统一响应格式设计

使用方式: 将本文档作为上下文提供给 AI,或粘贴到对话开头。AI 阅读本文档后应能正确编写调用 simapi-net 接口的前端代码。


0. 一句话定位

simapi-vue 是 simapi-net 的前端搭档。后端用 Simcu.SimApi 写接口,前端用 @simcu/simapi 调接口。两者共享同一套响应格式、认证方式和错误处理约定。


1. 核心概念(必读)

1.1 统一响应格式

simapi-net 所有接口的响应格式固定如下(HTTP 状态码始终 200):

{ "code": 200, "message": "成功", "data": { ... } }
code 含义
200 成功
401 需要登录

1.2 Token 认证

  • 前端通过请求头 Token: <value> 传递认证令牌
  • Token 存储在 localStoragekey 默认为 simapi-auth-token
  • 不使用 Cookie / 不使用 Authorization: Bearer

1.3 请求方式

  • 默认全部 POSTbody 为 JSON
  • 使用原生 fetch,不依赖 axios

2. 项目结构

simapi-vue/
├── src/
│   ├── types.ts            # 所有类型定义
│   ├── simapi.core.ts      # 纯 TS 核心(SimApiCore 类)
│   └── simapi.pinia.ts     # Vue3 Pinia Store 封装(useSimApi
├── dist/                   # 构建产物(ESM
├── package.json            # 包名 @simcu/simapi
└── vite.config.ts          # Vite 构建配置

导出路径(Subpath Exports

导入路径 内容 适用场景
@simcu/simapi SimApiCore + 类型 纯 TS / Node.js / 任意 JS 环境
@simcu/simapi/pinia useSimApi Pinia Store Vue3 项目(推荐)

3. 快速开始(Vue3 项目标准用法)

3.1 安装

npm install @simcu/simapi pinia

pinia 是 peerDependencyVue3 项目必须安装。

3.2 第一步:public/config.js — 外部配置文件

在项目的 public/ 目录下创建 config.js,定义 API 地址:

// public/config.js
window.simapi = {
  debug: true,
  endpoints: {
    default: "http://127.0.0.1:5210"   // 后端 simapi-net 服务地址
    // default: "https://api.example.com"  // 生产环境
  }
}

为什么用 config.js 而不是硬编码?

  • 前后端分离部署时,API 地址可能变化
  • public/ 下的文件 Vite 会直接复制到输出目录,不经过构建
  • 打包后运维人员可以直接修改 config.js 切换环境,无需重新构建

config.js 支持的字段:

字段 类型 说明 默认值
debug boolean 是否打印请求/响应日志 false
endpoints object 多端点地址映射 { default: '' }
defaultEndpoint string 默认使用的端点名称 'default'

3.3 第二步:index.html — 引入 config.js

<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My App</title>
    <!-- ⬇️ 在这里引入 config.js,必须在 main.ts 之前加载 -->
    <script src="/config.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

<script src="/config.js"> 不加 type="module",确保它在所有模块之前同步执行,设置好 window.simapi

3.4 第三步:main.ts — 创建 Pinia 实例

// src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'     // ← 必须安装并注册 Pinia
import App from './App.vue'
import router from './router'

const app = createApp(App)
app.use(createPinia())                   // ← useSimApi 依赖 Pinia,必须先注册
app.use(router)
// app.use(其他插件)
app.mount('#app')

顺序很重要: createPinia() 必须在 useSimApi() 调用之前完成注册。

3.5 第四步:App.vue — 初始化 SimApi 并设置回调

<!-- src/App.vue -->
<template>
  <router-view></router-view>
</template>

<script setup lang="ts">
import { useSimApi } from '@simcu/simapi/pinia'   // ← 注意 /pinia 子路径
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'

// 获取 api 实例(单例,全局共享同一状态)
const api = useSimApi()
const router = useRouter()

onMounted(() => {
  // ① 从 window.simapi 读取配置(endpoints、debug 等)
  api.autoInit()

  // ② 注册业务错误码回调 —— 401 时跳转登录页
  api.setBusinessCallback(401, () => {
    api.logout()
    router.replace({ path: '/login' })
  })

  // ③ 注册通用兜底回调 —— 其他所有非 200 错误统一提示
  api.setBusinessCallback('common', (data: any) => {
    console.error('[SimApi]', data.code, data.message)
    // 如果用了 UI 组件库可以在这里弹提示:
    // MessagePlugin.error({ content: data.message })
  })
})
</script>

关键点说明:

要点 说明
import from '@simcu/simapi/pinia' Vue3 项目必须/pinia 子路径导入
onMounted 中初始化 确保 DOM 已加载、config.js 已执行、Pinia 已就绪
autoInit() 读取 window.simapiendpointsdefaultEndpointdebug
setBusinessCallback(401, fn) 当后端返回 code=401 时自动执行
setBusinessCallback('common', fn) 兜底回调,任何非 200 且未匹配其他回调时触发

3.6 第五步:在组件中使用

<!-- src/views/UserList.vue -->
<template>
  <div>
    <button @click="loadUsers">加载用户</button>
    <ul v-for="user in users" :key="user.id">{{ user.name }}</ul>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useSimApi } from '@simcu/simapi/pinia'

interface User {
  id: string
  name: string
}

const api = useSimApi()
const users = ref<User[]>([])

async function loadUsers() {
  // query 返回 Promise<SimApiBaseResponse<T>>code !== 200 时会抛出异常
  const res = await api.query<User[]>('/user/list', { page: 1 })
  users.value = res.data ?? []
}
</script>

4. 完整项目模板(可直接复制使用)

以下是一个完整的 Vue3 + simapi-vue 项目初始化清单:

文件清单

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

window.simapi = {
  debug: true,
  endpoints: {
    default: "http://localhost:5000"
  }
}

index.html

<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My App</title>
    <script src="/config.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

src/main.ts

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'

createApp(App)
  .use(createPinia())
  .use(router)
  .mount('#app')

src/App.vue

<template><router-view /></template>

<script setup lang="ts">
import { useSimApi } from '@simcu/simapi/pinia'
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'

const api = useSimApi()
const router = useRouter()

onMounted(() => {
  api.autoInit()
  api.setBusinessCallback(401, () => {
    api.logout()
    router.replace('/login')
  })
})
</script>

src/views/Login.vue

<template>
  <form @submit.prevent="handleLogin">
    <input v-model="phone" placeholder="手机号" />
    <input v-model="code" placeholder="验证码" />
    <button type="submit">登录</button>
  </form>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useSimApi } from '@simcu/simapi/pinia'

const api = useSimApi()
const router = useRouter()
const phone = ref('')
const code = ref('')

async function handleLogin() {
  await api.login({ phone: phone.value, code: code.value })
  router.push('/')  // 登录成功自动存 Token,然后跳转首页
}
</script>

5. API 参考

5.1 import 方式

// ✅ Vue3 项目 — 用 Pinia Store(推荐)
import { useSimApi } from '@simcu/simapi/pinia'

// ✅ 非 Vue 项目 / 纯 TS — 用 Core 类
import { SimApiCore } from '@simcu/simapi'

5.2 useSimApi Store — 方法与属性一览

获取实例:

const api = useSimApi()  // 单例模式,全局状态共享

属性(Getters

属性 类型 说明
api.token string 当前存储的 Token(只读)
api.isLoggedIn boolean 是否已登录(Token 是否存在)
api.debug boolean 调试开关
api.api SimApiApiConfig API 配置对象(一般不直接操作)
api.auth SimApiAuthConfig 认证配置对象(一般不直接操作)

方法(Actions

方法 参数 返回值 说明
autoInit() void window.simapi 读取 endpoints/debug 配置
configure(options) SimApiOptions void 手动配置(深合并)
setDebug(bool) boolean void 设置调试模式
setEndpoints(map) {[name]: url} void 设置多端点映射
query(uri, params?, endpoint?, headers?) 见下方详解 Promise<SimApiBaseResponse<T>> 核心方法:发送 POST 请求
login(request) {[key]: any} Promise<SimApiBaseResponse<string>> 登录,成功后自动存 Token
logout(url?) string? Promise<any> 登出,清除 Token 并调后端接口
checkLogin(url?) string? Promise<void> 检查登录态,过期则触发 401 回调
setBusinessCallback(code, fn) number|string, callback void 注册业务错误码回调
getToken() string 获取当前 Token
setToken(token) string void 手动设置 Token
removeToken() void 清除 Token
getVersion(endpoint?) string? Promise<SimApiVersions> 获取前后端版本信息
getEndpoint(name?) string? string 获取某端点的 baseURL

5.3 query 方法详解

这是最核心的方法——几乎所有数据交互都通过它:

async function query<T = any>(
  uri: string,                    // 接口路径,如 '/user/list'
  params?: any = {},              // 请求体(POST body),JSON 对象
  endpointKey?: string,           // 可选:指定端点名(默认用 default)
  extraHeaders?: Record<string, string>  // 可选:额外请求头
): Promise<SimApiBaseResponse<T>>

使用示例:

// 基本查询
const res = await api.query<User[]>('/user/list', { page: 1, count: 20 })
console.log(res.data)   // User[] 数组

// 带参数
const res = await api.query<User>('/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')
} catch (err: any) {
  // err 是 SimApiBaseResponse 类型
  console.log(err.code)     // 业务错误码,如 400/401/403/500
  console.log(err.message)  // 错误消息
  console.log(err.data)     // 可能携带的错误详情
}

⚠️ query 的行为要点:

  1. 自动在请求头添加 Token(如果存在)
  2. code === 200 → 正常返回 SimApiBaseResponse<T>
  3. code !== 200 → 先执行对应的 businessCallback,然后 throw 异常
  4. 网络错误 → 执行 responseCallback.errorthrow 包装后的 {code: -1} 异常
  5. 所以调用方只需 try/catch 处理异常即可

5.4 login / logout 方法

// loginPOST 到 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
await api.logout(null)         // 只清本地 Token,不调后端

5.5 setBusinessCallback — 业务错误处理

// 针对特定错误码
api.setBusinessCallback(401, (data) => {
  console.log('未授权', data.message)
  router.replace('/login')
})

api.setBusinessCallback(403, (data) => {
  alert('无权限:' + data.message)
})

// 兜底:任何未单独处理的非 200 错误都会走 common
api.setBusinessCallback('common', (data) => {
  console.error('请求失败:', data.code, data.message)
})

回调执行顺序: 匹配具体错误码 → 未匹配则走 'common' → 然后 throw


6. 类型定义速查

/** 标准响应 */
interface SimApiBaseResponse<T = any> {
  code: number        // 200=成功,其他=业务错误码
  message: string     // 提示信息
  data?: T            // 业务数据
}

/** 版本信息 */
interface SimApiVersions {
  uiApp: string         // 前端应用版本
  uiSimApi: string      // 前端 SimApi 版本
  apiApp: string        // 后端应用版本(简化版)
  apiSimApi: string     // 后端 SimApi 版本(简化版)
  apiAppFull: string    // 后端应用版本(完整版)
  apiSimApiFull: string // 后端 SimApi 版本(完整版)
}

/** 认证配置 */
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'
}

/** API 配置 */
interface SimApiApiConfig {
  endpoints: { [name]: string }       // 多端点映射
  defaultEndpoint: string              // 默认端点
  businessCallback: SimApiBusinessCallback  // 错误码回调
  responseCallback: SimApiResponseCallback   // 响应拦截器
  timeout?: number                     // 超时毫秒数,默认 10000
}

/** 完整选项 */
interface SimApiOptions {
  debug?: boolean
  auth?: Partial<SimApiAuthConfig>
  api?: Partial<SimApiApiConfig>
}

7. 多端点支持

适用于需要连接多个后端服务的场景:

// config.js
window.simapi = {
  debug: true,
  endpoints: {
    default: 'https://api.example.com',     // 主服务
    admin: 'https://admin.example.com',      // 管理后台服务
    cdn: 'https://cdn.example.com',          // CDN/文件服务
  },
  defaultEndpoint: 'default'
}
// 使用默认端点
await api.query('/user/list')  // → https://api.example.com/user/list

// 指定端点
await api.query('/system/stats', {}, 'admin')  // → https://admin.example.com/system/stats

也可以运行时动态添加:

api.setEndpoints({ backup: 'https://backup-api.example.com' })

8. configure — 手动完整配置

除了 autoInit 从 window.simapi 读取外,也可以手动配置一切:

api.configure({
  debug: false,

  auth: {
    token_name: 'my-app-token',
    check_url: '/auth/check',
    logout_url: '/auth/logout',
    login_url: '/auth/login',
  },

  api: {
    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.simapiendpointsdefaultEndpointdebug
  • configure() 可以覆盖所有字段,包括 auth 和 callbacks
  • 通常做法是:autoInit() 读基础配置 + setBusinessCallback() 补充回调

9. GOTCHASAI 最容易犯的错)

错误 正确
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 存储
new SimApiCore() 在 Vue 项目里用 Vue 项目统一用 useSimApi() Pinia Store

10. 与 simapi-net 后端的对接约定

10.1 通信协议

[Vue 前端] -- POST(JSON) --> [simapi-net 后端]
              Header: Token: <value>
              Body:   { key: value }

[Vue 前端] <-- JSON {code, message, data} -- [simapi-net 后端]
              (HTTP Status 始终 200)

10.2 内置路由对照表

simapi-net 启用认证后,自动生成以下路由,simapi-vue 已内置对应方法:

simapi-net 路由 simapi-vue 方法 触发条件
POST /auth/login api.login(request) EnableSimApiAuth = true
POST /auth/check api.checkLogin() EnableSimApiAuth = true
POST /auth/logout api.logout() EnableSimApiAuth = true
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. 构建

# 开发模式(监听文件变化)
npm run dev

# 生产构建
npm run build

# 本地 link 调试
npm link
cd ../your-project
npm link @simcu/simapi

构建产物位于 dist/ 目录:

  • dist/index.mjs — 核心(SimApiCoreESM 入口
  • dist/pinia.mjs — Pinia Store ESM 入口
  • dist/*.d.ts — TypeScript 类型声明

版本号注入

库使用 declare const 声明版本常量,构建时通过 Vite 的 define 注入。

注意:

  • SimApiVersion:由 simapi 库自身构建时注入
  • AppVersion:由调用方项目在自己的 vite.config.ts 中注入

simapi 库自身构建 — 注入 SimApiVersion

在 simapi-vue 的 vite.config.ts 中,从自身 package.json 读取版本号:

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 中添加:

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.jsonversion,无需同步修改其他地方。


12. 从 axios 迁移

如果你之前用的是 axios

- 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()
response.data 直接是业务数据 SimApiBaseResponse<T>.data 是业务数据
HTTP 4xx/5xx 表示错误 HTTP 200 + code 字段表示错误
interceptors.response businessCallback + responseCallback
Authorization Bearer Token header
S
Description
No description provided
Readme
102 KiB
Languages
TypeScript 100%