news 2026/10/23 15:52:09

从 PHP 到 AI + Golang,程序员自救转型手记(十三):用 TaoToken 统一 Key 打通前端状态商店与多语言初始化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从 PHP 到 AI + Golang,程序员自救转型手记(十三):用 TaoToken 统一 Key 打通前端状态商店与多语言初始化

1. 从 PHP 单体到前后端分离,前端状态与多语言为什么最容易翻车

如果你和我一样是从 PHP 全栈一路写过来的,迁到 AI + Golang 的前后端分离架构时,最先崩的往往不是 Go 的语法,而是前端工程化里那两块“看起来简单、写起来全是坑”的东西:pinia 状态商店的模块拆分与持久化,以及 vue-i18n 多语言初始化的按需加载与语言切换。

PHP 时代我们习惯把 session、cookie、全局配置一股脑塞在服务端,页面刷新数据还在,语言切换靠后端渲染模板。到了 Vue3 + Vite 这套体系里,状态是内存里的响应式对象,刷新就没了;语言包如果全量 import,首屏体积直接起飞。这两个问题不解决,你后面接 AI 接口、写 Golang 后端联调时,会一直被“登录态丢了”“切了语言刷新又变回去”这种低级问题打断节奏。

这篇是转型手记的第十三篇,聚焦的就是这两块硬骨头。我会给你可复制的 pinia store 目录结构与持久化插件配置、vue-i18n 初始化与懒加载语言包代码,并给出本地启动后切换语言、刷新保持状态的验证动作。中间还会穿插一个统一 Key 的思路——用 TaoToken 把模型调用的凭证收敛到一处,避免你前端、后端、脚本里到处散落 API Key。适合正在从 PHP 往 AI + Golang 转型、前端工程化刚起步的同学跟着做。

先说清楚目标:pinia 要做到“模块拆分清晰 + 刷新不丢关键状态”,vue-i18n 要做到“语言包按需加载 + 支持子目录 + 切换即时生效”。这两件事做完,你的前端骨架才算立住。

2. TaoToken 前置:把模型调用的 Key 统一收口,别散落在前端

在讲 pinia 和 vue-i18n 之前,先花点篇幅说清楚 TaoToken 在这里扮演什么角色,因为很多转型的同学会把“前端状态管理”和“模型调用凭证管理”混在一起,最后把 API Key 硬编码进前端 store,这是大忌。

TaoToken 是一个统一的大模型 API 接入层,官网在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 入口是 https://taotoken.net/api 。它的价值在于:你不需要为每个模型厂商单独维护一套 Key 和 Base URL,而是用一套凭证去调用不同模型。对于从 PHP 转过来的同学,这相当于把你以前在 config.php 里维护的一堆第三方密钥,收敛成一个统一的配置中心。

为什么这件事和前端状态商店有关?因为你的 adminInfo store 里会存 token,但这个 token 是“用户登录态”,不是“模型调用凭证”。这两者必须分开。用户登录态放 pinia 并持久化到 localStorage,模型调用凭证应该放在后端 Golang 服务里,前端通过自己的后端接口去间接调用模型。如果你图省事把模型 Key 塞进 pinia,一旦持久化到浏览器,等于把钥匙挂在大门上。

我试过的做法是:前端只负责 UI 状态和用户态,所有涉及模型调用的请求,统一打到 Golang 后端的/api/ai/chat这类接口,后端再用 TaoToken 的 Key 去请求 https://taotoken.net/api 。这样前端 store 里永远不出现模型凭证,安全边界清晰。

具体到操作,你需要先在 TaoToken 控制台创建一个 API Key。入口在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite ,登录后新建一个 Key,复制出来。这个 Key 只放在后端环境变量里,比如 Golang 的.env或系统环境变量TAOTOKEN_API_KEY。前端一行都不要写。

如果你还在用 Claude Code 这类工具辅助写代码,它的配置也可以指向 TaoToken 的统一入口,文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。这样你写前端代码时,AI 辅助工具和你的项目运行时用的是同一套凭证体系,排查问题时不会因为“工具用了一个 Key、项目用了另一个 Key”而互相干扰。

一句话总结这一节:pinia 管用户态,TaoToken 管模型调用凭证,两者物理隔离。记住这条,后面配置才不会乱。

3. 可复制配置:pinia 模块拆分 + 持久化插件 + vue-i18n 懒加载

这一节是全文的核心,给你可以直接抄的配置。先建目录,再写代码,最后说清楚每个文件为什么这么放。

3.1 pinia 目录结构与持久化插件注册

推荐的目录结构如下,和 BuildAdmin/web 的思路一致,但做了精简,适合个人项目:

src/ stores/ index.ts # pinia 实例 + 持久化插件注册 constant/ cacheKey.ts # 持久化 key 常量,避免拼错 interface/ index.ts # 所有 store 的 interface 集中管理 adminInfo.ts # 管理员状态商店 config.ts # 全局配置状态商店(语言等)

先写src/stores/index.ts,初始化 pinia 并挂载持久化插件:

// src/stores/index.ts import { createPinia } from 'pinia' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' const pinia = createPinia() pinia.use(piniaPluginPersistedstate) export default pinia

然后在main.ts里注册为 Vue 插件:

// src/main.ts import { createApp } from 'vue' import App from './App.vue' import pinia from '/@/stores/index' const app = createApp(App) app.use(pinia) app.mount('#app')

持久化的 key 统一放常量文件,避免手写字符串:

// src/stores/constant/cacheKey.ts export const ADMIN_INFO = 'adminInfo' export const CONFIG = 'config'

interface 集中管理,小项目不用一个类型一个文件:

// src/stores/interface/index.ts export interface AdminInfo { id: number username: string nickname: string avatar: string last_login_at: string last_login_ip: string token: string super: boolean }

3.2 adminInfo store 与 dataFill 批量填充

// src/stores/adminInfo.ts import { defineStore } from 'pinia' import { ADMIN_INFO } from '/@/stores/constant/cacheKey' import type { AdminInfo } from '/@/stores/interface' export const useAdminInfo = defineStore('adminInfo', { state: (): AdminInfo => ({ id: 0, username: '', nickname: '', avatar: '', last_login_at: '', last_login_ip: '', token: '', super: false, }), actions: { dataFill(state: Partial<AdminInfo>, exclude: boolean | string[] = true) { if (exclude === true) { exclude = ['token'] } else if (exclude === false) { exclude = [] } if (Array.isArray(exclude)) { exclude.forEach((item) => { delete state[item as keyof AdminInfo] }) } this.$patch(state) }, setToken(token: string) { this.token = token }, removeToken() { this.token = '' }, }, persist: { key: ADMIN_INFO, }, })

这里dataFill默认排除 token,是个很实用的设计:从后端拉管理员信息时,token 单独用setToken设置,避免批量填充时把 token 覆盖成空。

3.3 config store 与 vue-i18n 懒加载

先建 config store,存当前语言:

// src/stores/config.ts import { defineStore } from 'pinia' import { CONFIG } from '/@/stores/constant/cacheKey' export type LangKey = 'zh-cn' | 'en' export const useConfig = defineStore('config', { state: () => ({ lang: { active: 'zh-cn' as LangKey, fallback: 'zh-cn' as LangKey, }, }), actions: { setLang(lang: LangKey) { this.lang.active = lang }, }, persist: { key: CONFIG, }, })

然后是 vue-i18n 初始化,核心是用import.meta.glob支持子目录懒加载:

// src/lang/index.ts import { merge, set } from 'lodash-es' import type { App } from 'vue' import { createI18n } from 'vue-i18n' import { useConfig, type LangKey } from '/@/stores/config' export const langs: LangKey[] = ['zh-cn', 'en'] export const langNames: Record<LangKey, string> = { en: 'English', 'zh-cn': '简体中文', } const i18n = createI18n({ legacy: false, locale: 'zh-cn', fallbackLocale: 'zh-cn', messages: {}, }) const langGlobs: Record<LangKey, Record<string, () => Promise<{ default: any }>>> = { en: import.meta.glob('./en/**/*.ts') as Record<string, () => Promise<{ default: any }>>, 'zh-cn': import.meta.glob('./zh-cn/**/*.ts') as Record<string, () => Promise<{ default: any }>>, } export async function setupI18n(app: App): Promise<void> { const config = useConfig() i18n.global.fallbackLocale.value = config.lang.fallback await setLang(config.lang.active) app.use(i18n) } export async function setLang(lang: LangKey): Promise<void> { await loadMessages(lang) const config = useConfig() i18n.global.locale.value = lang config.setLang(lang) } export async function loadMessages(lang: LangKey): Promise<void> { if (i18n.global.availableLocales.includes(lang)) { return } try { const glob = langGlobs[lang] const promises = Object.entries(glob).map(async ([path, loader]) => { const module = await loader() return { path, default: module.default } }) const modules = await Promise.all(promises) const mergedMessages: Record<string, any> = {} for (const { path, default: moduleData } of modules) { if (typeof moduleData !== 'object' || moduleData === null) { continue } const keys = filePathToKeys(lang, path) if (keys.length === 0) { merge(mergedMessages, moduleData) } else { merge(mergedMessages, set({}, keys, moduleData)) } } i18n.global.setLocaleMessage(lang, mergedMessages) } catch (error) { console.error(`Failed to load lang: ${lang}`, error) } } const filePathToKeys = (lang: LangKey, path: string) => { const langPathPrefix = `/${lang}` const pathName = path.slice( path.lastIndexOf(langPathPrefix) + (langPathPrefix.length + 1), path.lastIndexOf('.') ) const keys = pathName.split('/') if (keys.length === 1 && keys[0] === 'index') { return [] } return keys } export default i18n

语言包目录示例:

src/lang/ zh-cn/ index.ts # 顶层合并 common.ts # 通用文案 menu/ index.ts # 嵌套到 menu 下 en/ index.ts common.ts menu/ index.ts

import.meta.glob('./zh-cn/**/*.ts')里的**是关键,它能匹配子目录,比() => import('./en')那种写法强得多,后者加载不到en/test/test.ts这种深层文件。

3.4 App.vue 里接 element-plus 多语言

<template> <el-config-provider :value-on-clear="() => null" :locale="elLocale"> <router-view></router-view> </el-config-provider> </template> <script setup lang="ts"> import elEn from 'element-plus/es/locale/lang/en' import elZhCn from 'element-plus/es/locale/lang/zh-cn' import { computed } from 'vue' import { useConfig } from '/@/stores/config' const config = useConfig() const elLocales: Record<string, typeof elEn> = { en: elEn, 'zh-cn': elZhCn, } const elLocale = computed(() => elLocales[config.lang.active] || elLocales[config.lang.fallback]) </script>

到这里,pinia 和 vue-i18n 的配置就齐了。注意setupI18n要在app.mount之前 await 调用,否则首屏可能拿不到语言包。

4. 验证请求与成功结果:切换语言、刷新保持状态

配置写完不算完,得验证。这一节给你具体的验证动作和预期结果。

4.1 验证 pinia 持久化

在任意组件里写个测试按钮,调用useAdminInfo().setToken('test-token-123'),然后打开浏览器 DevTools 的 Application 面板,看 Local Storage。你应该能看到一个 key 为adminInfo的条目,值是 JSON,里面包含token: "test-token-123"。

然后刷新页面,在控制台执行:

JSON.parse(localStorage.getItem('adminInfo'))

预期能看到 token 还在。再在组件里读useAdminInfo().token,应该也是test-token-123。这就说明持久化插件生效了。

4.2 验证 vue-i18n 懒加载与切换

在页面上放一个语言切换按钮:

<template> <el-button @click="switchLang">切换语言</el-button> <p>{{ t('common.hello') }}</p> </template> <script setup lang="ts"> import { useI18n } from 'vue-i18n' import { setLang } from '/@/lang' import { useConfig } from '/@/stores/config' const { t } = useI18n() const config = useConfig() const switchLang = async () => { const next = config.lang.active === 'zh-cn' ? 'en' : 'zh-cn' await setLang(next) } </script>

点击按钮,页面文案应该立即从中文变英文。打开 DevTools 的 Network 面板,切到英文时应该能看到en目录下的 ts 文件被动态加载(Vite 开发模式下是模块请求)。再刷新页面,语言应该保持在你切换后的那个,因为configstore 持久化了lang.active。

4.3 验证模型调用链路(可选)

如果你想顺便验证 TaoToken 的接入,可以在 Golang 后端写一个测试接口,用环境变量里的 Key 请求 https://taotoken.net/api ,前端通过自己的接口调用。前端这边只关心请求成功返回,不碰 Key。模型对话的调试入口在 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite ,你可以先在网页上确认 Key 能用,再写进后端。

预期结果:前端切换语言即时生效、刷新保持;pinia 里的用户态刷新不丢;模型调用走后端,前端无凭证泄露。

5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth

这一节对照真实报错,给你排查路径。这些错我在转型过程中基本都踩过。

401 Unauthorized:如果你在调 TaoToken 接口时遇到 401,先检查后端环境变量里的 Key 是否正确读取。常见坑是.env文件没被加载,或者变量名拼错。Golang 里用os.Getenv("TAOTOKEN_API_KEY")读,打印一下确认非空。另外确认请求头是Authorization: Bearer <key>,少个 Bearer 也会 401。前端如果直接调模型接口出现 401,说明你把 Key 放前端了,赶紧挪回后端。

local proxy failed:这个错通常出现在你本地配了某种转发但目标不可达时。排查顺序是:先确认 https://taotoken.net/api 能通,再确认你的后端服务本身在监听。如果你在 Claude Code 或类似工具里看到这个错,检查工具的 Base URL 配置是否指向了正确入口,文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。不要在本机配任何来路不明的转发规则,直接用官方入口最稳。

reading choices 相关报错:这类错一般出现在解析模型返回结构时。不同模型返回的 JSON 结构不完全一样,choices字段可能不存在或层级不同。你的后端解析时要先判断字段存在,再做类型断言。前端这边如果直接把模型原始返回渲染,遇到结构不符就会报 reading 某个字段失败。建议后端统一封装成固定结构再返回给前端。

OAuth 相关报错:如果你用 Claude Code 的 OAuth 登录方式,遇到 token 过期或回调失败,先确认系统时间准确,OAuth 对时间敏感。然后确认回调地址和配置一致。如果只是想快速跑通,用 API Key 方式更直接,Key 在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 创建。

pinia 持久化不生效:检查pinia.use(piniaPluginPersistedstate)是否在createPinia()之后调用,顺序反了插件不生效。还要确认 store 里写了persist: { key: xxx },没写这个配置不会持久化。

vue-i18n 切换后刷新变回默认:说明configstore 没持久化,或者setupI18n里读的config.lang.active在持久化恢复之前执行了。确保 pinia 插件注册在 i18n 初始化之前。

语言包子目录加载不到:确认用的是import.meta.glob('./zh-cn/**/*.ts'),**不能少。用() => import('./en')这种写法加载不到深层文件,这是 Vite 的机制决定的。

6. 语义一致 CTA:把 Key 收口,把状态管好,继续往下走

走到这里,你的前端骨架已经有了:pinia 负责用户态和配置态的持久化,vue-i18n 负责多语言的按需加载与切换,模型调用的凭证通过 TaoToken 统一收口在后端。这三件事分开管,后面接 Golang 接口、写 AI 功能时就不会互相打架。

如果你在接入过程中卡在 Key 管理或接口调试上,可以直接去 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 创建 Key,配合 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 的接入文档对照排查。想先验证模型能不能通,用 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 快速试一下。如果你打算长期用 AI 辅助编码、跑 Agent 类任务,Coding Plan 在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite ,控制台在 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 。

下一篇我会继续记录 Golang 后端的接口分层和 AI 调用封装,把前端这套状态管理和后端串起来。你现在要做的,就是把这一篇的配置跑通,确认切换语言、刷新保持状态这两个动作都符合预期,再往下走。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 15:27:09

DeepSeek Harness (dsh) 插件开发实战:从零写一个自动调用 Codex 的插件

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/22 3:49:02

当光伏遇上大型工商业:解析主从机防逆流保护技术如何保障“自发自用”|TaoToken 统一 Key 通道下的 GOOSE 联调实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/23 0:21:30

驱动升级指南:从16.1656到16.1692的稳定性优化与问题修复

1. 从16.1656到16.1692&#xff1a;一次驱动版本升级背后的真实动机驱动版本号这种东西&#xff0c;平时没人会盯着看&#xff0c;只有当设备开始闹脾气——花屏、掉帧、外设时断时续、系统日志里反复刷错误码——才会有人翻出设备管理器&#xff0c;盯着那一串数字发呆。16.16…

作者头像 李华
网站建设 2026/10/16 17:30:31

生物医学基础模型强在哪里?我们真的知道它学到了什么吗

生物医学基础模型越来越强&#xff0c;刷榜速度一个季度比一个季度快。组会里最常听到的一句话是"这个模型又SOTA了"&#xff0c;但紧接着的问题往往是&#xff1a;它到底抓住了什么生物学信号&#xff1f;是真实学到了调控机制&#xff0c;还是只记住了数据里的批次…

作者头像 李华
网站建设 2026/10/16 17:30:33

AI Skills:从提示词到可复用技能包,一文学会开发与落地

你有没有过这种体验&#xff1a;在 Claude 或 Codex 里折腾一下午&#xff0c;反复调整提示词&#xff0c;结果它写出来的代码还是不像你团队的风格&#xff1b;让它处理批量文件&#xff0c;每次都得重新交代一遍规则&#xff1b;换一台电脑&#xff0c;之前调教好的工作流全部…

作者头像 李华