Architecture
This document is currently available in Chinese only.
本文档面向平台内部与深度接入方,内容可能随实现演进。
本文说明 SDK 的定位、分层、运行模式、数据流、存储模型与演进接缝,结论以 SDK 1.2.0 为准。快速接入见「快速开始」,方法签名见「API 速查」。
定位与设计目标
SDK 让 H5 游戏在不关心运行环境差异的前提下,用同一套 API 获得平台能力(账号、钱包、存档、榜单、埋点、广告、游戏级存储)。核心设计目标:
- StorageDriver:键值持久化,现为 localStorage / 内存; - HostAdapter:宿主能力查询与广告播放,现为 android / web / browser; - Transport:HTTP 请求,现为基于 fetch 的实现。
- 本地优先(local-first),按模块区分。本地模式(含
auto回落)下读写全部以本地存储为准(localStorage,不可用时降级内存),断网、无后端时游戏仍可完整运行;远端模式下存档仍先落本地、再同步服务端,而钱包与广告奖励直接读写服务端,读取以服务端为准,仅NETWORK时回退本地(目前为排行榜与存档缓存)。 - 后端形状 API(backend-shaped API)。模块按未来后端的资源边界划分:account / wallet / game / leaderboard / analytics / ads / storage。本地实现先以相同形状落地,接入远端时游戏代码零改动。
- 两个接缝加传输层。把设备与浏览器差异收敛到三个可替换抽象:
- 未来换后端不改游戏代码。游戏只依赖
types契约,运行时由 SDK 装配层决定注入本地实现还是远端实现。
非目标(1.x 不做):实时多人、支付、好友关系、真实广告 SSV 与反作弊。
分层架构
| 层 | 职责 |
|---|---|
| 契约层(types) | 纯类型:配置、各模块接口与数据结构,可被零成本引用 |
| 门面层(Instfun 单例) | 模式解析、依赖装配、本地/远端工厂与单例管理 |
| 本地模块层(modules) | account / wallet / game(编排会话、记录、统计)/ progress(存档)/ leaderboard / analytics / ads / game-storage |
| 远端模块层(remote) | identity、wallet、game(包装本地 game + 同步)、outbox、cache、leaderboard(fallback 本机榜)、analytics、ads、migrate |
| 接缝层 | StorageDriver(LocalStorage / Memory)、HostAdapter(Android / Web / Browser)、Transport(Fetch) |
| 底座(core) | config 常量、errors 错误模型、ids 标识符、meta 平台元信息、watermark 页面水印、url-locale |
依赖方向单向:core ← storage ← modules ← sdk,以及 core / storage / host ← remote ← sdk。modules 不感知 remote;remote 也不直接引用 modules,本地实例由装配层在运行期作为依赖注入(如远端 game 包装本地 game、远端 analytics 包装本地队列)。装配层是唯一同时引用两层的代码。
运行模式
mode 支持 local / remote / auto;不传时按 auto 处理,但网络错误回落只认显式 mode: "auto"。初始化后 sdk.mode 只会是 local 或 remote。
基础 URL 解析优先级:
| 顺序 | 条件 | 结果 |
|---|---|---|
| 1 | mode: "local" | null(纯本地,忽略 apiBaseUrl) |
| 2 | config.apiBaseUrl 非空 | 使用它 |
| 3 | window.INSTFUN_CONFIG.apiBaseUrl 非空(门户注入) | 使用它 |
| 4 | mode: "remote" | 默认 "/api/v1"(同源部署) |
| 5 | 其余(auto 且未配置) | null,本地模式 |
auto 的回落规则:初始化先尝试远端;只有 mode: "auto"、错误是 InstfunError 且错误码为 NETWORK 时才回落本地。配置错误、鉴权失败等会原样抛给调用方,显式 remote 不做任何回落,避免把真实问题静默成单机模式。
初始化成功后统一记录 sdk_init 埋点,属性为 { mode, host, persistent }。
模块关系与依赖方向
本地层:
- wallet、analytics、leaderboard 只依赖存储与底座;
- game 是编排层:持有存档存储、analytics、leaderboard 引用,负责会话/记录/统计,并在
finish时把存档与榜单作为可选副作用调用; - ads 依赖宿主适配器、钱包模块与埋点模块;
- account 的身份结果由装配层以
ensureUserId()注入其它模块,保证同一进程内 userId 稳定。
远端层(包装而非重写):
- account、wallet、leaderboard、analytics、ads 都实现与本地同名模块一致的接口;
- 远端 game 组合本地 game:先本地落盘,再远端同步;本地模块同时充当离线缓存与统计来源;
- 远端 analytics 的 track / getQueue / clear 直接转发本地实现,只重写
flush()(分片上报); - 远端 ads 在宿主为浏览器时直接返回本地广告模块(模拟广告没有服务端票据可核销),其余宿主走「服务端领 token → 播放 → 核销」;
- 远端 leaderboard 的 fallback 指向本地排行榜,仅
NETWORK时回退; - outbox 与远端 game 共享同一套存档载荷 helper;outbox 与远端 game 之间存在模块级循环引用,依赖函数声明提升与延迟调用化解;
- 存档缓存直接复用本地存档键,使模式切换(如
auto回落)时不会出现两份存档。
依赖方向:core / storage / host 是底座,modules 与 remote 同层,装配层是唯一同时引用两层的文件;types 可被任意层 import type。
数据流
初始化与身份引导:
- 内存资料命中则直接返回; - 有 token 时调用 GET /me 校验并获取最新资料,401 则删除 token 后用 deviceSeed 重新游客登录; - 无 token 时用 POST /auth/guest { deviceSeed } 换回同一游客账号。
- 解析 baseUrl;无 baseUrl 时直接构建纯本地 SDK。
- 有 baseUrl 时先装配远端模块(不发请求),再执行 bootstrap:
- 首次远端启动执行本地存档一次性迁移:本地有存档且服务端无存档时上传一次(带迁移标记,避免重复)。
- 记录
sdk_init埋点并返回 SDK 实例。
deviceSeed 是 32 字节随机十六进制串,一旦生成不再变化,是「同一设备同一账号」的锚点;token 失效只会重新游客登录,不会换号。
游玩主链路:
loadProgress():请求服务端存档,成功则写回本地缓存;仅NETWORK时读本地缓存并返回pendingSync: true。start():本地创建会话、更新平台元信息并埋点;发现上次遗留的未结束会话时按quit补记一条。saveProgress():本地先落盘(revision + 1、256KB 校验),再按节流窗口决定直推还是合并进 outbox;直推冲突时拉取服务端存档、以服务端为底做浅合并后重试一次,二次冲突进 outbox;NETWORK/RATE_LIMITED同样进 outbox 并返回pendingSync: true,其它错误上抛。finish():先本地落盘记录与统计,再提交记录(NETWORK/RATE_LIMITED进 outbox),随后在存在progress时推送存档,最后在submitScore且分数合法时提交榜单并拉取快照。
sync 与离线补偿:
- 先 flush 埋点队列:按 100 条分批上传,全部成功才清空;中途失败回写未发送尾部。
- 再按入队顺序重放 outbox:遇到第一个可重试失败(
NETWORK/SERVER_ERROR/RATE_LIMITED)即停止并保留该操作及其后所有操作;不可重试错误移除该操作并上报outbox_op_dropped埋点;存档二次冲突保留在队列中但不阻塞后续操作。 - 返回
{ flushed, pending },本地模式下不发网络请求,只返回埋点积压数。
存储模型
所有键都以 instfun:v1 开头;远端专用键在 v1 前缀下嵌入 :v2: 段,与本地键空间隔离。
| 键 | 结构 | 读写方 |
|---|---|---|
instfun:v1:identity | 身份记录 | account |
instfun:v1:meta | PlatformMeta { games, recentGameIds } | 平台元信息 |
instfun:v1:wallet:balance | 余额快照 | wallet |
instfun:v1:wallet:transactions | 流水数组(≤ 200 条) | wallet |
instfun:v1:analytics:queue | 事件队列(≤ 500 条) | analytics |
instfun:v1:game:{gameId}:progress | 云存档(含 revision) | progress / 存档缓存 |
instfun:v1:game:{gameId}:records | 记录数组(≤ 50 条) | game |
instfun:v1:game:{gameId}:stats | 游戏统计 | game |
instfun:v1:game:{gameId}:session | 未结束会话 | game |
instfun:v1:game:{gameId}:boards | 按 boardId 的本机榜记录 | leaderboard |
instfun:v1:game:{gameId}:kv:{key} | 任意可 JSON 化值 | game-storage |
instfun:v1:v2:device-seed | 32 字节 hex 字符串(裸键) | identity |
instfun:v1:v2:token | token 字符串(裸键) | identity |
instfun:v1:v2:session | { userId, profile }(信封) | identity |
instfun:v1:v2:outbox | 离线操作队列(≤ 200 条) | outbox |
instfun:v1:v2:migrated:{gameId} | 迁移标记(裸键) | migrate |
「信封」是本地持久化的统一结构:
{ "schemaVersion": 1, "updatedAt": 1700000000000, "value": "..." }读取时若 JSON 损坏、缺字段或 schemaVersion 不匹配,一律按「无数据」返回 null。device-seed、token、recovery-code 与迁移标记为裸字符串,不经信封,因此 schema 升级不会使它们失效。
游戏级 KV 的前缀为 instfun:v1:game:{gameId}:kv:,keys() 按前缀过滤并裁掉前缀,游戏只看到自己的 key;不同游戏即使 key 同名也互不影响。
安全与配额
SDK 侧本地校验:
| 项 | 阈值 | 行为 |
|---|---|---|
| 存档 JSON 序列化长度 | 256KB | 写盘前抛 PAYLOAD_TOO_LARGE |
| 单条事件 properties 长度 | 8KB | 丢弃该事件(不入队、不抛错) |
| 单次进程埋点入队上限 | 1000 条(可配置) | 超出丢弃 |
| 埋点队列长度 | 500 条 | 保留最新 |
| 钱包流水条数 | 200 条 | 保留最新 |
| 游玩记录条数 | 50 条 | 保留最新 |
| 单次会话时长 | 6 小时 | 封顶 |
| 单笔金额 | [1, 10000] 整数 | 非法抛 INVALID_AMOUNT |
| 远端存档推送节流 | 2000ms(可配置) | 窗口内走 outbox 合并 |
远端错误处理:
- 429
RATE_LIMITED:details可含retryAfterSeconds/limit/window。存档、记录、榜单在 outbox 中视为可重试,延迟到下次sync();钱包与广告奖励原样抛给游戏,不应自动重试。 - 413
PAYLOAD_TOO_LARGE:即使响应无 JSON 信封也按 HTTP 状态兜底映射;本地已在 256KB 处拦截存档。 Retry-After:服务端补充该响应头,SDK 不解析,时间语义以details.retryAfterSeconds为准。
水印:在官网 play/{gameId}/ 内运行时,SDK 读取页面水印 <meta name="instfun-wm">(trim 后 1~64 字符有效,进程内缓存一次),埋点未自带 wm 时自动附加 properties.wm;游戏显式传入的优先。水印仅用于内容溯源,不参与鉴权。
版本与兼容
SDK_VERSION = "1.2.0",随包导出,同时写入埋点事件、云存档与游戏元信息;- 云存档另带
gameVersion,埋点事件带gameId/gameVersion,便于按版本切分数据; - 存档信封的
schemaVersion当前为1:读取时版本不一致即返回null。升级该值等于让旧本地数据失效,必须配套迁移方案; - 兼容原则:只做加法变更(新增可选字段、模块、宿主能力),不修改既有字段语义与错误码含义;
- 迁移只针对旧版存档:远端首次启动时把本地存档上传一次(仅服务端无存档时),并打标避免重复。
演进接缝
- 传输层已就绪:
Transport只要求request<T>({ method, path, body });默认实现已处理信封解码、token 注入与错误归一。自定义实现或代理可通过初始化依赖注入。 - 本地到远端的模块替换点:集中在装配层。替换某个能力时只需实现对应的远端模块,在装配时替换工厂调用;本地模块保持原样供离线兜底。
- 新的存储后端:
StorageDriver的方法全部返回 Promise,天然适配异步后端。新增 IndexedDB 实现时只需实现该接口并在存储探测中增加分支,业务代码无需改动。 - 新的宿主:实现
HostAdapter后增加探测分支与HostKind成员;广告是否走服务端票据由远端广告模块按宿主类型判断。 - 服务端协议演进:远端调用只依赖统一信封
{ ok, data }/{ ok: false, error: { code, message, details } };新增端点只需替换远端模块中的 path,错误码经映射表收敛到InstfunErrorCode。
术语表
| 术语 | 说明 |
|---|---|
| 门面(facade) | 负责模式解析、依赖装配、工厂与单例的入口层,游戏侧唯一接触点 |
| 接缝(seam) | 把设备与浏览器差异收敛成的三个可替换抽象:StorageDriver、HostAdapter、Transport |
| local-first | 本地模式(含 auto 回落)下读写以本地存储为准,断网时游戏仍可完整运行 |
| backend-shaped API | 模块按后端资源边界划分,本地与远端实现同形,接入远端时游戏代码零改动 |
| 信封(envelope) | 本地持久化的统一结构 { schemaVersion, updatedAt, value } |
| outbox | 远端模式下失败写操作(存档/记录/榜单)的持久化补偿队列,sync() 时按序重放 |
| fallback | 读取路径仅在 NETWORK 错误时回退本地数据,其它错误上抛 |
| bootstrap | 初始化时执行一次的流程:deviceSeed 游客登录与本地存档一次性迁移 |
| 水印 wm | 官网游戏页通过 meta 注入的溯源标记,埋点未自带 wm 时自动附加 |
| 宿主(host) | 承载 H5 游戏的环境抽象(android / web / browser) |
| Transport | HTTP 请求抽象,负责信封解码与 token 注入 |
| revision | 云存档的单调版本号,推送时以 revision - 1 做乐观并发控制 |