@giime/crx
v1.0.1
Published
面向基于 giime 的 Chrome 扩展(MV3)插件:axios 与登录协调器。popup / options / contentScript / background 共用同一套代码;contentScript 经 background 自动桥接。
Downloads
1,671
Maintainers
Readme
@giime/crx
Chrome 扩展(MV3)专用的 axios + 登录协调封装。让 popup / options / contentScript / background 同一份业务代码调 API,自动桥接 contentScript 经 background 发请求;统一处理 401 / 登录失效 / 自动重发;登录窗口去重 + 冷却期保护。
跟 giime 主库的 createAxios 是平级封装——giime 那套面向 web 项目(document.cookie、iframe LoginDialog),这套面向 CRX(chrome.cookies、chrome.windows.create)。
安装
pnpm add @giime/crx依赖 vue@^3 / axios@^1 / element-plus / giime(都是 peerDependencies)。
接入清单(8 步)
1. 配 .env
# 必须:业务方在自己的 .env 配
VITE_CRX_PLUGIN_ID=your-plugin-id
# 可选:90% 情况下不需要配;不配时自动 fallback 到 Vite 自带 import.meta.env.MODE。
# 只有跑非默认 mode(比如 `vite --mode release` / `--mode test`)才需要显式配
# VITE_GIIME_MODE=develop四套 mode 对应的 host(包内硬编码,跟着包走,业务不用关心):
| mode | host | | --- | --- | | production | api.giikin.com | | release | api-pre.giikin.com | | test | api-test.giikin.cn | | develop | api-dev.giikin.cn |
2. 配 src/env.d.ts
/// <reference types="@giime/crx/client" />让 import.meta.env.VITE_CRX_PLUGIN_ID 有类型提示。
3. 配 Vite 插件
// vite.config.ts
import { defineConfig } from 'vite';
import { crxAxiosIdPlugin } from '@giime/crx/vite-plugin';
export default defineConfig({
plugins: [crxAxiosIdPlugin()],
});插件做的事:编译期给每个 createCrxAxios({...}) 调用注入 __id__: "<源文件相对路径>",让 contentScript 桥接到 background 时能找到对应的 axios 实例。
4. 写一个 service 文件
文件名必须是 service.ts / serviceNext.ts / request.ts 之一(background 通过 glob 加载,文件名定死才能匹配)。
// src/api/basic/request.ts
import { createCrxAxios, currentCrxEnv } from '@giime/crx';
const { service } = createCrxAxios({
// baseURL 推荐用 currentCrxEnv.apiHost 派生,自动跟随 mode 切换
baseURL: `https://${currentCrxEnv.apiHost}/guard`,
successCode: 200,
});
export default service;5. controller 用同一个 service
// src/api/basic/controller/postBasicV1OrgList.ts
import request from '@/api/basic/request';
export const postBasicV1OrgList = (config?: AxiosRequestConfig) =>
request.post(`/basic/v1/org/list`, config);6. background entry
// src/background/main.ts
import { registerAxiosBridge, registerLoginCoordinator } from '@giime/crx';
// 让所有 service 文件在 background bundle 里被加载,自动注册到桥接 registry
import.meta.glob('@/api/**/{service,serviceNext,request}.ts', { eager: true });
registerAxiosBridge(); // background 收 contentScript 桥接消息
registerLoginCoordinator(); // background 协调登录窗 / cookie 监听 / 广播7. contentScript entry
// src/contentScripts/index.ts
import { createRouter, createWebHashHistory } from 'vue-router';
import { configureCrx } from '@giime/crx';
import Giime from 'giime';
// 创建 shadow DOM root
const root = document.createElement('div');
const shadowDOM = container.attachShadow({ mode: 'open' });
shadowDOM.appendChild(root);
// 必传:让 LoginPrompt / 错误 toast 挂到 shadow root,避免被宿主页面样式干扰
configureCrx({ contentScriptRoot: root });
const app = createApp(App);
const router = createRouter({ history: createWebHashHistory(), routes: [] });
// router 既要传给 Giime(让 giime 内部 useGlobalConfig 拿到),
// 也要 app.use(router) 给 vue-router 自己——否则 GmButton 等组件
// 会报 injection "Symbol(route location)" not found。
//
// disabledVersionAttr: 关掉 giime 写 document.body[data-giime-version]——
// content script 里这个 body 是宿主页的,会跟宿主页自己的 giime 版本号来回打架。
app.use(Giime, {
env: import.meta.env,
router,
disabledLoginDialog: true,
disabledVersionAttr: true,
});
app.use(router);
app.mount(root);8. popup / options entry
业务直接 import { postBasicV1OrgList } from '@/api/basic/controller' 调即可。
挂 Vue 应用时遵循跟 contentScript 同样的规则:app.use(Giime, { router }) + app.use(router) 必须同时调,缺一个 <GmButton> / 任何用 vue-router 注入的 gm 组件就会运行时报错。
行为细节
401 / 登录失效
| 入口 | 行为 | | --- | --- | | popup | 跳过 LoginPrompt 直接让 background 开登录标签页(popup 视口太窄 + 失焦即关闭,dialog 体验差)| | options | LoginPrompt 弹窗 → 用户点立即登录 → 开登录标签页 → 等 cookie 变化广播 → 重发原请求 | | contentScript | 同 options | | background 自己调 | 直接 reject(无 UI)|
background 侧的双重保护
- 登录标签页去重(P1):已有登录标签页 + URL 仍在登录页 prefix → focus 它;否则关掉重开。不会出现 2 个登录标签页。
- 登录后冷却期(P2):cookie 变化后 12 秒内再来的 401 视作"慢响应",告诉客户端 token 已更新直接重发,不再开窗。
自动关登录窗
cookie 检测到 token 写入 → background 广播 __crxLoginCompleted__ + chrome.windows.remove(loginWindowId)。loginWindowId 用 chrome.storage.session 持久化跨 SW 重启。
dk-plugin-id 请求头
所有 axios 请求自动带 dk-plugin-id: ${VITE_CRX_PLUGIN_ID},让后端识别请求来源。
contentScript 里的 ElDialog / GmDialog 必须显式 append-to
ElementPlus 的 ElDialog / ElOverlay / ElMessage 默认 <teleport to="body">——这里的 body 是宿主页的 document.body,shadow DOM 隔离不住 teleport,DOM 会跨出 shadow root 渲染到宿主页里,被宿主页样式污染、定位错乱。
修法:每个 dialog 显式传 append-to,指向 shadow root 内的容器元素(也就是你 configureCrx({ contentScriptRoot }) 传进来的那个):
<template>
<gm-dialog v-model="visible" :append-to="appendTo">...</gm-dialog>
</template>
<script setup lang="ts">
import { getCrxConfig } from '@giime/crx';
const appendTo = getCrxConfig().contentScriptRoot as HTMLElement;
</script>外层 dialog 加上 append-to 后,里面套的 GmTableCtx 自带 search dialog 等子级 portal 会继承同一个 teleport context,跟着一起进 shadow root,不用每个都加。
append-to-body是 ElDialog 的旧 prop(已废弃且默认 true),效果跟 teleport 到宿主 body 一样,shadow DOM 场景别用。
contentScript 桥接 cfg 会被 sanitize
signal / cancelToken / adapter / transformRequest / transformResponse / paramsSerializer / validateStatus / onUploadProgress / onDownloadProgress / httpAgent / httpsAgent + 任何函数字段,在 chrome.runtime.sendMessage 前会被剥掉——它们要么过不了 structured clone,要么跨进程没意义(如 AbortSignal)。详见 DESIGN.md 三。
401 不进 chrome://extensions 错误面板
桥接层 / 真实 axios 拦截器抛 401 时打 __crxHandled__ 标记,包内 unhandledrejection 全局过滤器 preventDefault() 掉。非 401 异常(5xx / 网络错 / 业务错)仍正常报,真 bug 不被埋。详见 DESIGN.md 七。
公开 API 速查
// 工厂
createCrxAxios(config): { service: AxiosInstance, __id__: string }
// 注册(background entry 用)
registerAxiosBridge(): void
registerLoginCoordinator(): void
// 配置
configureCrx(cfg: Partial<CrxConfig>): void
getCrxConfig(): Readonly<CrxConfig>
// 环境
currentCrxEnv: { apiHost: string, cookieDomain: string }
crxPluginId: string
isBackground() / isContentScript() / isExtensionPage() / isPopup(): boolean
// 登录
crxLoginUrl: string
isOnLoginPage(): boolean
showLoginPrompt(opts?): Promise<boolean> // imperative 弹窗,业务一般用不上设计文档
详细架构、__id__ 注入原理、跨 entry 桥接协议、为什么不用 iframe 等讨论见同目录的 DESIGN.md。
