Skyroc Admin Docs
架构设计

适配器模式

让核心包跨 Web / RN / 小程序的关键设计:抽象接口 + 端实现

问题

仓库的核心能力(请求、日志、存储)希望:

  • 写一份代码,跨 Web / RN / 小程序运行;
  • 各端的平台 API(fetch vs XMLHttpRequest vs wx.requestlocalStorage vs AsyncStorage vs wx.setStorage)不污染核心;
  • 类型安全。

直接 if (Platform.OS === 'web') 会让核心包:

  • 绑定到平台 SDK(依赖膨胀);
  • 难以 tree-shake;
  • 单元测试需要模拟平台环境;
  • 新增平台时核心要改。

解决:适配器模式

+--------------------+         +-----------------------+
|  Core (业务核心)    | ─uses→  |  Adapter Interface    |
|  无平台 API         |         |  (TS interface / 抽象)|
+--------------------+         +-----------------------+

                       ┌──────────────────┼──────────────────┐
                       ▼                  ▼                  ▼
              +--------------+   +--------------+   +--------------+
              |  Web Adapter |   |  RN Adapter  |   | MiniApp Ad.  |
              | localStorage |   | AsyncStorage |   | wx.setStorage|
              +--------------+   +--------------+   +--------------+
  • Core 只面向 interface
  • 端项目在启动时注入具体实现;
  • 核心不再认识具体平台,平台不再侵入核心。

仓库内的真实例子

1. @skyroc/serviceRequestAdapter

@skyroc/service 提供请求 + 错误处理 + token 刷新流水线,但自己不发请求

// @skyroc/service 内部
export interface RequestAdapter {
  request<T>(config: RequestConfig): Promise<T>;
  // ... cancel, etc.
}

Web 端注入 @skyroc/axios 实现:

import { createAppRequest } from '@skyroc/service';
import { createAxiosAdapter } from '@skyroc/axios/service';   // 仅 Web 用

const request = createAppRequest({
  adapter: createAxiosAdapter({ baseURL, timeout: 10000 }),
  // ...
});

RN 端可以注入自己的 fetch / okhttp 适配器,service 流水线完全复用。

详见 @skyroc/service

2. @skyroc/loggerStorageAdapter

LogLayer 透出本地日志能力,但本地存储跨端不同:

// @skyroc/logger 内部
export interface StorageAdapter {
  set(key: string, value: string): Promise<void>;
  get(key: string): Promise<string | null>;
  remove(key: string): Promise<void>;
  clear(): Promise<void>;
}

包内提供两套实现:

子入口实现平台
@skyroc/logger/webWebStorageAdapter(IndexedDB / localStorage)Web
@skyroc/logger/nativeRNStorageAdapter(AsyncStorage)RN

业务方按需 import 对应实现并注入。详见 @skyroc/logger

3. @skyroc/utils/storageIStorage

export interface IStorage {
  getItem<T>(key: string): T | null;
  setItem<T>(key: string, value: T, expireSeconds?: number): void;
  removeItem(key: string): void;
  clear(): void;
}

Web 端实现:基于 localStorage / sessionStorage 的包装,附带过期时间、JSON 序列化。 RN 端实现:基于 AsyncStorage,相同接口语义。

@skyroc/core-stateatomWithStorage 接受 IStorage,因此跨平台 Jotai 状态持久化只需注入正确的实现。

适配器在仓库的标准结构

@core/<pkg>/
├── src/
│   ├── adapter.ts            # 抽象接口(核心定义)
│   ├── core.ts               # 核心流水线(消费 adapter)
│   ├── web/                  # 平台子入口
│   │   └── index.ts          # 暴露 createWebAdapter
│   ├── native/
│   │   └── index.ts
│   └── index.ts              # 主入口(仅 adapter + core,不带任何平台实现)
└── package.json
    "exports": {
      ".":         { "default": "./dist/index.mjs" },
      "./web":     { "default": "./dist/web.mjs" },
      "./native":  { "default": "./dist/native.mjs" }
    }

关键约定:主入口绝不能 import 任何平台 API。这是 RN 项目能正常打包的前提(任何 import 走到 window.localStorage 都会爆炸)。

何时该引入适配器

场景是否引入
模块只在 Web 用❌(直接放 web/
模块只在 RN 用❌(直接放 native/
同一业务逻辑,平台 API 不同✅ 必须
同一业务逻辑,平台 API 相同❌(放 shared/ / @core/
跨平台但实现完全相同❌(不需要 adapter,写一次即可)

注入时机

在应用启动序列中,端项目把适配器注入到核心包:

// apps/admin/src/setup/service.ts(示意)
import { createAppRequest } from '@skyroc/service';
import { createAxiosAdapter } from '@skyroc/axios/service';

export const request = createAppRequest({
  adapter: createAxiosAdapter({ baseURL: import.meta.env.VITE_BASE_URL })
});
// apps/admin/src/setup/logger.ts(示意)
import { createLogger } from '@skyroc/logger';
import { createWebStorageAdapter } from '@skyroc/logger/web';

export const logger = createLogger({
  storage: createWebStorageAdapter({ name: 'admin-logs' })
});

各端的 setup 通常集中在 src/setup/*src/bootstrap.tsx

收益对比

维度直接耦合平台 API适配器模式
跨端复用需要分支判断或维护多份一份核心,多套适配器
测试必须模拟平台环境注入内存版 adapter 即可
维护平台 API 变化影响核心仅改对应适配器
扩展新增平台要改核心新增平台仅写适配器

包分层平台优先 的关系

  • 包分层定义:抽象 / 实现 / 装配 的纵向层;
  • 平台优先定义:横向上每个平台一个目录;
  • 适配器模式连接两者:核心包定义抽象(在纵向中靠上层),各平台目录提供实现(在横向中分散)。

如果一个包想跨平台,它的「平台 API」必须用适配器隔离——这是仓库最重要的硬约束之一。

On this page