前端 SDK 通用架构设计
一、需求分析
1.1 什么是前端 SDK
前端 SDK(Software Development Kit)是为开发者提供的一套封装好的工具包,用于快速集成某项功能到其应用中。常见的前端 SDK 包括:
| SDK 类型 | 典型产品 | 核心功能 |
|---|---|---|
| 监控 SDK | Sentry、Fundebug、ARMS | 错误捕获、性能监控、用户行为追踪 |
| 分析 SDK | Google Analytics、Mixpanel | 埋点、用户画像、转化分析 |
| 播放器 SDK | video.js、DPlayer、hls.js | 视频播放、弹幕、自适应码率 |
| IM SDK | 环信、融云、腾讯云 IM | 即时通讯、消息收发、会话管理 |
| 地图 SDK | 高德 JS API、Mapbox GL | 地图渲染、路径规划、地理编码 |
| 编辑器 SDK | ProseMirror、Slate、Tiptap | 富文本编辑、协同、插件扩展 |
| 支付 SDK | Stripe.js、微信 JS-SDK | 支付流程、安全验签 |
1.2 核心设计目标
面试中回答「如何设计一个 SDK」时,先阐明设计目标,再展开架构设计,能体现系统性思维。
| 设计目标 | 说明 | 实现手段 |
|---|---|---|
| 最小化 API | 对外暴露尽可能少的接口,降低学习成本 | 门面模式、链式调用 |
| 渐进式增强 | 核心功能开箱即用,高级功能按需引入 | 插件系统、可选扩展 |
| 零依赖 / 轻依赖 | 不引入大型第三方库,减少与宿主冲突 | 自研核心模块、外部依赖可选注入 |
| Tree-shakable | 未使用的模块不打入最终产物 | ESM 导出、sideEffects: false |
| 类型安全 | 完善的 TypeScript 类型支持 | 泛型配置、声明合并、类型推导 |
| 环境兼容 | 浏览器、Node.js、小程序等多端运行 | 适配层、Platform 抽象 |
| 稳定可靠 | 插件崩溃不影响核心,全局错误兜底 | 错误隔离、降级策略 |
| 可观测 | 日志、调试工具完善 | 日志等级、DevTools 插件 |
1.3 非功能需求
| 需求 | 指标 | 实现方式 |
|---|---|---|
| 体积 | Core < 5KB gzipped | Tree Shaking、代码分割 |
| 性能 | 初始化 < 5ms,不阻塞主线程 | 异步初始化、延迟加载 |
| 兼容性 | 支持 IE11+(可选)或现代浏览器 | Polyfill 按需注入、构建降级 |
| 安全性 | 防止 XSS、CSRF,数据脱敏 | 输入校验、CSP 兼容 |
二、整体架构
2.1 SDK 架构全景
2.2 分层架构说明
SDK 通常采用分层架构,从上到下分为四层:
- 用户层(User Layer):对外暴露的 API,简洁易用,通常只有
init、use、on、destroy等少量方法 - 核心层(Core Layer):SDK 的心脏,包含事件总线、配置管理、生命周期、日志、错误边界等基础设施
- 插件层(Plugin Layer):所有业务功能以插件形式实现,通过插件管理器统一注册、调度、卸载
- 平台适配层(Platform Layer):抽象底层 API 差异(DOM、网络请求、存储等),实现多端运行
内核尽可能小,功能尽可能由插件提供。这是现代 SDK 架构的核心哲学,参考 Vite 的插件化设计、Sentry 的 Integration 机制。内核只负责调度,不负责具体业务。
三、核心模块设计
3.1 Core 内核
内核是 SDK 的中央调度器,串联所有模块。
interface SDKOptions<TConfig extends BaseConfig = BaseConfig> {
/** SDK 名称 */
name: string;
/** SDK 版本 */
version: string;
/** 用户配置 */
config: TConfig;
/** 初始插件列表 */
plugins?: Plugin<TConfig>[];
}
class SDK<TConfig extends BaseConfig = BaseConfig> {
readonly name: string;
readonly version: string;
// 核心模块:通过组合而非继承的方式组织
private configManager: ConfigManager<TConfig>;
private eventEmitter: EventEmitter;
private pluginManager: PluginManager<TConfig>;
private lifecycleManager: LifecycleManager;
private logger: Logger;
private errorBoundary: ErrorBoundary;
private initialized = false;
constructor(options: SDKOptions<TConfig>) {
this.name = options.name;
this.version = options.version;
// 按依赖顺序初始化核心模块
this.logger = new Logger(options.config.logLevel ?? 'warn');
this.errorBoundary = new ErrorBoundary(this.logger);
this.eventEmitter = new EventEmitter();
this.configManager = new ConfigManager(options.config);
this.lifecycleManager = new LifecycleManager(this.eventEmitter);
this.pluginManager = new PluginManager(this);
}
/** 初始化 SDK */
async init(): Promise<void> {
if (this.initialized) {
this.logger.warn('SDK already initialized');
return;
}
this.lifecycleManager.emit('beforeInit');
try {
// 按依赖关系排序后,依次初始化插件
await this.pluginManager.initAll();
this.initialized = true;
this.lifecycleManager.emit('initialized');
this.logger.info(`${this.name}@${this.version} initialized`);
} catch (error) {
this.errorBoundary.capture(error as Error, { phase: 'init' });
throw error;
}
}
/** 注册插件 */
use(plugin: Plugin<TConfig>): this {
this.pluginManager.register(plugin);
return this; // 链式调用
}
/** 监听事件 */
on(event: string, handler: EventHandler): this {
this.eventEmitter.on(event, handler);
return this;
}
/** 触发事件 */
emit(event: string, ...args: unknown[]): void {
this.eventEmitter.emit(event, ...args);
}
/** 获取配置 */
getConfig(): Readonly<TConfig> {
return this.configManager.getConfig();
}
/** 更新配置 */
setConfig(partial: Partial<TConfig>): void {
this.configManager.update(partial);
this.eventEmitter.emit('configChange', this.configManager.getConfig());
}
/** 获取日志实例 */
getLogger(): Logger {
return this.logger;
}
/** 销毁 SDK */
async destroy(): Promise<void> {
this.lifecycleManager.emit('beforeDestroy');
await this.pluginManager.destroyAll();
this.eventEmitter.removeAllListeners();
this.initialized = false;
this.lifecycleManager.emit('destroyed');
}
}
3.2 事件总线(EventEmitter)
事件总线是模块间通信的核心机制,解耦各模块间的直接依赖。
type EventHandler = (...args: unknown[]) => void | Promise<void>;
class EventEmitter {
private listeners = new Map<string, Set<EventHandler>>();
private maxListeners = 50;
/** 注册事件监听 */
on(event: string, handler: EventHandler): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
const handlers = this.listeners.get(event)!;
if (handlers.size >= this.maxListeners) {
console.warn(
`Event "${event}" has ${handlers.size} listeners. Possible memory leak.`
);
}
handlers.add(handler);
// 返回取消函数,方便解绑
return () => this.off(event, handler);
}
/** 一次性监听 */
once(event: string, handler: EventHandler): () => void {
const wrapper: EventHandler = (...args) => {
this.off(event, wrapper);
return handler(...args);
};
return this.on(event, wrapper);
}
/** 取消监听 */
off(event: string, handler: EventHandler): void {
this.listeners.get(event)?.delete(handler);
}
/** 触发事件 */
emit(event: string, ...args: unknown[]): void {
const handlers = this.listeners.get(event);
if (!handlers) return;
for (const handler of handlers) {
try {
handler(...args);
} catch (error) {
console.error(`Error in event handler for "${event}":`, error);
}
}
}
/** 异步触发(等待所有 handler 完成) */
async emitAsync(event: string, ...args: unknown[]): Promise<void> {
const handlers = this.listeners.get(event);
if (!handlers) return;
// 串行执行,保证执行顺序(类似 Koa 中间件)
for (const handler of handlers) {
await handler(...args);
}
}
/** 清除所有监听 */
removeAllListeners(event?: string): void {
if (event) {
this.listeners.delete(event);
} else {
this.listeners.clear();
}
}
/** 获取事件监听数量 */
listenerCount(event: string): number {
return this.listeners.get(event)?.size ?? 0;
}
}
- 内存泄漏:忘记
off导致监听器不断累积。SDK 应提供maxListeners限制并输出警告 - 异常隔离:某个 handler 抛错不应影响其他 handler 的执行,需 try-catch 包裹
- 执行顺序:同步
emit保证注册顺序,异步emitAsync串行执行防止竞态
3.3 生命周期管理
SDK 和插件都有自己的生命周期,统一管理可以确保初始化和销毁的顺序正确。
/** SDK 生命周期状态 */
enum LifecycleState {
Created = 'created',
Initializing = 'initializing',
Initialized = 'initialized',
Running = 'running',
Destroying = 'destroying',
Destroyed = 'destroyed',
}
/** 生命周期事件类型 */
type LifecycleEvent =
| 'beforeInit'
| 'initialized'
| 'beforeDestroy'
| 'destroyed'
| 'configChange'
| 'error';
class LifecycleManager {
private state: LifecycleState = LifecycleState.Created;
private eventEmitter: EventEmitter;
constructor(eventEmitter: EventEmitter) {
this.eventEmitter = eventEmitter;
}
getState(): LifecycleState {
return this.state;
}
emit(event: LifecycleEvent, ...args: unknown[]): void {
// 根据事件自动切换状态
const stateMap: Partial<Record<LifecycleEvent, LifecycleState>> = {
beforeInit: LifecycleState.Initializing,
initialized: LifecycleState.Running,
beforeDestroy: LifecycleState.Destroying,
destroyed: LifecycleState.Destroyed,
};
if (stateMap[event]) {
this.state = stateMap[event]!;
}
this.eventEmitter.emit(`lifecycle:${event}`, ...args);
}
/** 监听生命周期事件 */
on(event: LifecycleEvent, handler: EventHandler): () => void {
return this.eventEmitter.on(`lifecycle:${event}`, handler);
}
}
四、插件系统设计
插件系统是 SDK 架构中最核心的扩展机制。一个优秀的插件系统应该做到:插件间解耦、依赖可排序、错误可隔离、通信有规范。
4.1 插件接口定义
/** 插件元信息 */
interface PluginMeta {
/** 插件唯一标识 */
name: string;
/** 插件版本 */
version: string;
/** 依赖的其他插件名称 */
dependencies?: string[];
/** 插件描述 */
description?: string;
}
/** 插件上下文 —— 插件可访问的 SDK 能力 */
interface PluginContext<TConfig extends BaseConfig = BaseConfig> {
/** SDK 配置 */
getConfig(): Readonly<TConfig>;
/** 事件总线 */
on(event: string, handler: EventHandler): () => void;
emit(event: string, ...args: unknown[]): void;
/** 日志 */
logger: Logger;
/** 获取其他插件实例(用于插件间通信) */
getPlugin<T extends Plugin>(name: string): T | undefined;
}
/** 插件接口 —— 所有插件必须实现 */
interface Plugin<TConfig extends BaseConfig = BaseConfig> {
/** 插件元信息 */
meta: PluginMeta;
/** 安装钩子:插件初始化逻辑 */
install(ctx: PluginContext<TConfig>): void | Promise<void>;
/** 卸载钩子:清理资源 */
uninstall?(): void | Promise<void>;
}
4.2 插件管理器
class PluginManager<TConfig extends BaseConfig = BaseConfig> {
private plugins = new Map<string, Plugin<TConfig>>();
private installedPlugins = new Set<string>();
private sdk: SDK<TConfig>;
constructor(sdk: SDK<TConfig>) {
this.sdk = sdk;
}
/** 注册插件 */
register(plugin: Plugin<TConfig>): void {
const { name } = plugin.meta;
if (this.plugins.has(name)) {
this.sdk.getLogger().warn(`Plugin "${name}" already registered, skipping`);
return;
}
this.plugins.set(name, plugin);
this.sdk.getLogger().debug(`Plugin "${name}" registered`);
}
/** 按依赖拓扑排序后初始化所有插件 */
async initAll(): Promise<void> {
const sorted = this.topologicalSort();
const ctx = this.createContext();
for (const plugin of sorted) {
await this.installPlugin(plugin, ctx);
}
}
/** 安装单个插件(带错误隔离) */
private async installPlugin(
plugin: Plugin<TConfig>,
ctx: PluginContext<TConfig>
): Promise<void> {
const { name } = plugin.meta;
try {
await plugin.install(ctx);
this.installedPlugins.add(name);
this.sdk.getLogger().debug(`Plugin "${name}" installed`);
} catch (error) {
// 关键:插件安装失败不应导致整个 SDK 崩溃
this.sdk.getLogger().error(`Plugin "${name}" install failed:`, error);
this.sdk.emit('pluginError', { plugin: name, error, phase: 'install' });
}
}
/** 拓扑排序:解决插件依赖问题 */
private topologicalSort(): Plugin<TConfig>[] {
const result: Plugin<TConfig>[] = [];
const visited = new Set<string>();
const visiting = new Set<string>(); // 检测循环依赖
const visit = (name: string) => {
if (visited.has(name)) return;
if (visiting.has(name)) {
throw new Error(`Circular dependency detected: ${name}`);
}
visiting.add(name);
const plugin = this.plugins.get(name);
if (!plugin) {
throw new Error(`Missing dependency: "${name}"`);
}
// 先递归处理依赖
for (const dep of plugin.meta.dependencies ?? []) {
visit(dep);
}
visiting.delete(name);
visited.add(name);
result.push(plugin);
};
for (const name of this.plugins.keys()) {
visit(name);
}
return result;
}
/** 创建插件上下文 */
private createContext(): PluginContext<TConfig> {
return {
getConfig: () => this.sdk.getConfig(),
on: (event, handler) => {
this.sdk.on(event, handler);
return () => {}; // 简化,实际返回取消函数
},
emit: (event, ...args) => this.sdk.emit(event, ...args),
logger: this.sdk.getLogger(),
getPlugin: <T extends Plugin>(name: string) =>
this.plugins.get(name) as T | undefined,
};
}
/** 销毁所有插件(逆序销毁) */
async destroyAll(): Promise<void> {
const reversed = [...this.installedPlugins].reverse();
for (const name of reversed) {
const plugin = this.plugins.get(name);
try {
await plugin?.uninstall?.();
this.sdk.getLogger().debug(`Plugin "${name}" uninstalled`);
} catch (error) {
this.sdk.getLogger().error(`Plugin "${name}" uninstall failed:`, error);
}
}
this.installedPlugins.clear();
}
/** 获取已安装的插件 */
getPlugin<T extends Plugin>(name: string): T | undefined {
return this.plugins.get(name) as T | undefined;
}
}
4.3 插件生命周期
4.4 插件示例:性能监控插件
const PerformancePlugin: Plugin<MonitorConfig> = {
meta: {
name: 'performance',
version: '1.0.0',
description: '性能数据采集插件',
dependencies: [], // 无依赖
},
install(ctx) {
const config = ctx.getConfig();
const logger = ctx.logger;
// 采集 Web Vitals
if (typeof window !== 'undefined' && 'PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
ctx.emit('performance:metric', {
name: entry.name,
value: (entry as PerformanceEntry & { value?: number }).value
?? entry.duration,
timestamp: Date.now(),
});
}
});
try {
observer.observe({ type: 'largest-contentful-paint', buffered: true });
observer.observe({ type: 'first-input', buffered: true });
observer.observe({ type: 'layout-shift', buffered: true });
logger.info('PerformancePlugin: observing Web Vitals');
} catch (e) {
logger.warn('PerformancePlugin: some metrics not supported');
}
// 存储 observer 引用,方便卸载时断开
(this as Plugin & { _observer?: PerformanceObserver })._observer = observer;
}
// 监听配置变更
ctx.on('configChange', (newConfig: unknown) => {
logger.debug('PerformancePlugin: config updated', newConfig);
});
},
uninstall() {
// 清理 PerformanceObserver
const self = this as Plugin & { _observer?: PerformanceObserver };
self._observer?.disconnect();
self._observer = undefined;
},
};
4.5 插件间通信
插件间通信有两种模式:
| 模式 | 说明 | 适用场景 |
|---|---|---|
| 事件驱动 | 通过事件总线发布/订阅 | 松耦合、广播式通信 |
| 直接引用 | 通过 ctx.getPlugin() 获取实例 | 紧耦合、需要调用特定方法 |
const ReporterPlugin: Plugin<MonitorConfig> = {
meta: {
name: 'reporter',
version: '1.0.0',
dependencies: ['performance'], // 依赖 performance 插件
},
install(ctx) {
const buffer: unknown[] = [];
const config = ctx.getConfig();
// 方式一:事件驱动 —— 监听其他插件发出的事件
ctx.on('performance:metric', (data: unknown) => {
buffer.push(data);
if (buffer.length >= (config.batchSize ?? 10)) {
flush();
}
});
// 方式二:直接引用
const perfPlugin = ctx.getPlugin('performance');
if (perfPlugin) {
ctx.logger.debug('ReporterPlugin: performance plugin found');
}
function flush(): void {
if (buffer.length === 0) return;
const batch = buffer.splice(0);
// 使用 sendBeacon 保证页面关闭时数据不丢失
if (navigator.sendBeacon) {
navigator.sendBeacon(
config.reportUrl ?? '/api/report',
JSON.stringify(batch)
);
} else {
fetch(config.reportUrl ?? '/api/report', {
method: 'POST',
body: JSON.stringify(batch),
keepalive: true,
}).catch(() => {});
}
}
// 页面卸载前 flush 一次
window.addEventListener('beforeunload', flush);
},
};
五、配置管理
5.1 配置合并策略
SDK 配置通常采用默认配置 + 用户配置深度合并的方式:
interface BaseConfig {
/** 是否启用调试模式 */
debug?: boolean;
/** 日志等级 */
logLevel?: LogLevel;
/** 环境:自动检测或手动指定 */
environment?: 'development' | 'staging' | 'production';
/** 应用标识 */
appId?: string;
}
/** 默认配置 */
const DEFAULT_CONFIG: BaseConfig = {
debug: false,
logLevel: 'warn',
environment: detectEnvironment(),
};
class ConfigManager<TConfig extends BaseConfig = BaseConfig> {
private config: TConfig;
private initialConfig: TConfig;
constructor(userConfig: TConfig) {
// 深度合并:默认配置 ← 用户配置
this.config = this.deepMerge(
{ ...DEFAULT_CONFIG } as TConfig,
userConfig
);
// 保存初始配置,支持 reset
this.initialConfig = structuredClone(this.config);
}
/** 获取当前配置(只读) */
getConfig(): Readonly<TConfig> {
return Object.freeze({ ...this.config });
}
/** 运行时更新配置 */
update(partial: Partial<TConfig>): void {
this.config = this.deepMerge(this.config, partial as TConfig);
}
/** 重置为初始配置 */
reset(): void {
this.config = structuredClone(this.initialConfig);
}
/** 深度合并工具函数 */
private deepMerge<T extends Record<string, unknown>>(
target: T,
source: Partial<T>
): T {
const result = { ...target };
for (const key in source) {
const sourceVal = source[key];
const targetVal = result[key];
if (
sourceVal !== null &&
typeof sourceVal === 'object' &&
!Array.isArray(sourceVal) &&
targetVal !== null &&
typeof targetVal === 'object' &&
!Array.isArray(targetVal)
) {
(result as Record<string, unknown>)[key] = this.deepMerge(
targetVal as Record<string, unknown>,
sourceVal as Record<string, unknown>
);
} else if (sourceVal !== undefined) {
(result as Record<string, unknown>)[key] = sourceVal;
}
}
return result;
}
}
/** 自动检测环境 */
function detectEnvironment(): 'development' | 'staging' | 'production' {
if (typeof window === 'undefined') return 'production';
const hostname = window.location.hostname;
if (hostname === 'localhost' || hostname === '127.0.0.1') return 'development';
if (hostname.includes('staging') || hostname.includes('test')) return 'staging';
return 'production';
}
5.2 配置校验
用户传入的配置可能不合法,SDK 应在初始化阶段进行校验,给出明确的错误提示,而不是在运行时才报错。
type ValidationRule<T> = {
field: keyof T;
type?: string;
required?: boolean;
validator?: (value: unknown) => boolean;
message: string;
};
function validateConfig<T extends BaseConfig>(
config: T,
rules: ValidationRule<T>[]
): void {
const errors: string[] = [];
for (const rule of rules) {
const value = config[rule.field];
if (rule.required && (value === undefined || value === null)) {
errors.push(`[Config] "${String(rule.field)}" is required. ${rule.message}`);
continue;
}
if (value !== undefined && rule.type && typeof value !== rule.type) {
errors.push(
`[Config] "${String(rule.field)}" expected ${rule.type}, got ${typeof value}`
);
}
if (value !== undefined && rule.validator && !rule.validator(value)) {
errors.push(`[Config] "${String(rule.field)}" validation failed: ${rule.message}`);
}
}
if (errors.length > 0) {
throw new Error(`SDK Config Validation Failed:\n${errors.join('\n')}`);
}
}
六、日志与调试
6.1 日志系统
type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';
const LOG_LEVELS: Record<LogLevel, number> = {
debug: 0,
info: 1,
warn: 2,
error: 3,
silent: 4,
};
class Logger {
private level: LogLevel;
private prefix: string;
private remoteHandler?: (level: LogLevel, args: unknown[]) => void;
constructor(level: LogLevel = 'warn', prefix: string = '[SDK]') {
this.level = level;
this.prefix = prefix;
}
/** 设置日志等级 */
setLevel(level: LogLevel): void {
this.level = level;
}
/** 设置远程日志上报处理器 */
setRemoteHandler(handler: (level: LogLevel, args: unknown[]) => void): void {
this.remoteHandler = handler;
}
debug(...args: unknown[]): void {
this.log('debug', args);
}
info(...args: unknown[]): void {
this.log('info', args);
}
warn(...args: unknown[]): void {
this.log('warn', args);
}
error(...args: unknown[]): void {
this.log('error', args);
}
private log(level: LogLevel, args: unknown[]): void {
// 等级过滤
if (LOG_LEVELS[level] < LOG_LEVELS[this.level]) return;
const timestamp = new Date().toISOString();
const formattedArgs = [`${this.prefix} [${timestamp}]`, ...args];
// 本地输出
switch (level) {
case 'debug': console.debug(...formattedArgs); break;
case 'info': console.info(...formattedArgs); break;
case 'warn': console.warn(...formattedArgs); break;
case 'error': console.error(...formattedArgs); break;
}
// 远程上报(仅 warn 和 error)
if (this.remoteHandler && LOG_LEVELS[level] >= LOG_LEVELS['warn']) {
try {
this.remoteHandler(level, args);
} catch {
// 远程上报失败不应影响 SDK 运行
}
}
}
}
6.2 开发调试模式
/** 开发模式工具,生产环境自动 Tree Shake */
function enableDevTools<TConfig extends BaseConfig>(sdk: SDK<TConfig>): void {
if (typeof window === 'undefined') return;
// 在 window 上挂载调试对象
const devtools = {
/** 查看当前配置 */
getConfig: () => sdk.getConfig(),
/** 查看 SDK 版本 */
version: sdk.version,
/** 手动触发事件 */
emit: (event: string, ...args: unknown[]) => sdk.emit(event, ...args),
/** 修改日志等级 */
setLogLevel: (level: LogLevel) => sdk.getLogger().setLevel(level),
};
(window as Record<string, unknown>).__SDK_DEVTOOLS__ = devtools;
sdk.getLogger().info(
'DevTools enabled. Access via window.__SDK_DEVTOOLS__'
);
}
生产环境下可以通过 URL 参数临时开启调试模式,例如在 URL 中添加 ?__sdk_debug=true,SDK 检测到后自动将日志等级设为 debug,方便线上排查问题。
七、错误处理
7.1 全局错误边界
interface ErrorInfo {
/** 错误发生阶段 */
phase: 'init' | 'runtime' | 'destroy';
/** 相关插件名称 */
plugin?: string;
/** 额外上下文 */
context?: Record<string, unknown>;
}
class ErrorBoundary {
private logger: Logger;
private handlers: Array<(error: Error, info: ErrorInfo) => void> = [];
private errorCount = 0;
private readonly maxErrors = 100; // 防止错误风暴
constructor(logger: Logger) {
this.logger = logger;
}
/** 注册错误处理器 */
onError(handler: (error: Error, info: ErrorInfo) => void): void {
this.handlers.push(handler);
}
/** 捕获错误 */
capture(error: Error, info: ErrorInfo): void {
this.errorCount++;
// 防止错误风暴:超过阈值后只记录不处理
if (this.errorCount > this.maxErrors) {
if (this.errorCount === this.maxErrors + 1) {
this.logger.error(
'Too many errors captured, further errors will be suppressed'
);
}
return;
}
this.logger.error(`[${info.phase}] ${error.message}`, info);
for (const handler of this.handlers) {
try {
handler(error, info);
} catch (handlerError) {
// 错误处理器本身报错,只记录不再抛出
this.logger.error('Error handler threw:', handlerError);
}
}
}
/** 安全执行:包裹可能出错的函数 */
safeExecute<T>(
fn: () => T,
info: ErrorInfo,
fallback?: T
): T | undefined {
try {
return fn();
} catch (error) {
this.capture(error as Error, info);
return fallback;
}
}
/** 安全执行异步函数 */
async safeExecuteAsync<T>(
fn: () => Promise<T>,
info: ErrorInfo,
fallback?: T
): Promise<T | undefined> {
try {
return await fn();
} catch (error) {
this.capture(error as Error, info);
return fallback;
}
}
}
7.2 插件错误隔离策略
// 在 PluginManager 的 installPlugin 方法中
private async installPlugin(
plugin: Plugin<TConfig>,
ctx: PluginContext<TConfig>
): Promise<void> {
const { name } = plugin.meta;
// 策略一:错误隔离 —— 单个插件失败不影响其他插件
const result = await this.sdk.errorBoundary.safeExecuteAsync(
() => plugin.install(ctx),
{ phase: 'init', plugin: name }
);
if (result !== undefined) {
this.installedPlugins.add(name);
} else {
// 策略二:降级处理 —— 记录失败插件,后续提供 retry 机制
this.failedPlugins.add(name);
this.sdk.emit('plugin:installFailed', {
plugin: name,
retryable: true,
});
}
}
插件安装失败后,SDK 应该:
- 记录失败原因,方便开发者排查
- 不影响其他插件,保证核心功能可用
- 提供重试机制,网络类插件可能只是暂时失败
- 通知上层,通过事件或回调让业务方感知
八、版本管理与发布
8.1 Semver 语义化版本
| 版本段 | 含义 | 升级场景 |
|---|---|---|
| Major (X.0.0) | 不兼容的 API 变更 | 删除/重命名 API、改变参数类型 |
| Minor (0.X.0) | 向后兼容的新功能 | 新增插件、新增配置项 |
| Patch (0.0.X) | 向后兼容的问题修复 | Bug 修复、性能优化 |
- 在 SDK 初始化日志中打印版本号:
SDK v1.2.3 initialized - 在上报数据中附带 SDK 版本,方便后端区分数据格式
- 发布 Breaking Change 时提供迁移指南和 codemod 工具
- 大版本更新同时维护旧版 LTS(Long-Term Support)
8.2 多格式产物
SDK 需要输出多种模块格式,以适配不同的使用场景:
| 格式 | 文件 | 使用场景 | 特点 |
|---|---|---|---|
| ESM | dist/index.esm.js | 现代构建工具(Vite、Webpack 5) | 支持 Tree Shaking |
| CJS | dist/index.cjs.js | Node.js、旧版 Webpack | require() 引入 |
| UMD | dist/index.umd.js | CDN <script> 引入 | 挂载全局变量 |
| IIFE | dist/index.iife.js | 直接 <script> 引入 | 最简单的引入方式 |
import { defineConfig } from 'rollup';
import typescript from '@rollup/plugin-typescript';
import terser from '@rollup/plugin-terser';
import { dts } from 'rollup-plugin-dts';
export default defineConfig([
// 主构建
{
input: 'src/index.ts',
output: [
{ file: 'dist/index.esm.js', format: 'esm', sourcemap: true },
{ file: 'dist/index.cjs.js', format: 'cjs', sourcemap: true },
{
file: 'dist/index.umd.js',
format: 'umd',
name: 'MySDK',
sourcemap: true,
},
{
file: 'dist/index.iife.js',
format: 'iife',
name: 'MySDK',
plugins: [terser()], // IIFE 通常用于 CDN,需要压缩
},
],
plugins: [typescript()],
},
// 类型声明
{
input: 'src/index.ts',
output: { file: 'dist/index.d.ts', format: 'esm' },
plugins: [dts()],
},
]);
8.3 package.json 多入口配置
{
"name": "@my-org/sdk",
"version": "1.0.0",
"main": "./dist/index.cjs.js",
"module": "./dist/index.esm.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs.js",
"types": "./dist/index.d.ts"
},
"./plugins/*": {
"import": "./dist/plugins/*.esm.js",
"require": "./dist/plugins/*.cjs.js",
"types": "./dist/plugins/*.d.ts"
}
},
"sideEffects": false,
"files": ["dist"],
"unpkg": "./dist/index.umd.js",
"jsdelivr": "./dist/index.umd.js"
}
8.4 CDN + npm 双发布
九、体积优化
9.1 Tree Shaking
SDK 的 ESM 导出方式直接影响 Tree Shaking 效果:
// 具名导出:支持 Tree Shaking
export { SDK } from './core/sdk';
export { EventEmitter } from './core/event-emitter';
export { Logger } from './core/logger';
// 插件单独导出
export { PerformancePlugin } from './plugins/performance';
export { ReporterPlugin } from './plugins/reporter';
export { ErrorPlugin } from './plugins/error';
// 类型导出(零体积)
export type { Plugin, PluginMeta, PluginContext } from './core/plugin';
export type { BaseConfig } from './core/config-manager';
不要使用桶文件(barrel files)的 export * 重导出,这会导致 Tree Shaking 失效。确保 package.json 中设置 "sideEffects": false。
9.2 按需加载插件
// 方式一:静态导入(全量打包)
import { SDK, PerformancePlugin, ReporterPlugin } from '@my-org/sdk';
// 方式二:动态导入(按需加载,推荐)
const sdk = new SDK({ name: 'myApp', version: '1.0.0', config: {} });
// 只有需要时才加载性能监控插件
if (shouldEnablePerformance()) {
const { PerformancePlugin } = await import('@my-org/sdk/plugins/performance');
sdk.use(PerformancePlugin);
}
await sdk.init();
9.3 体积对比参考
| SDK | Core 体积 (gzipped) | 全量体积 (gzipped) | 插件化 |
|---|---|---|---|
| Sentry Browser | ~6 KB | ~28 KB | 是(Integrations) |
| Google Analytics (gtag) | ~30 KB | ~80 KB | 否 |
| Mixpanel | ~15 KB | ~15 KB | 否 |
| 理想 SDK 架构 | < 3 KB | < 15 KB | 是 |
十、TypeScript 类型设计
10.1 泛型配置
SDK 的配置类型应支持泛型扩展,让用户可以定义自己的配置字段:
/** 基础配置 */
interface BaseConfig {
debug?: boolean;
logLevel?: LogLevel;
environment?: string;
}
/** 监控 SDK 配置(扩展基础配置) */
interface MonitorConfig extends BaseConfig {
appId: string;
reportUrl: string;
sampleRate?: number;
batchSize?: number;
maxBreadcrumbs?: number;
}
/** 播放器 SDK 配置 */
interface PlayerConfig extends BaseConfig {
container: string | HTMLElement;
src: string;
autoplay?: boolean;
controls?: boolean;
}
// SDK 实例类型随配置类型变化
const monitorSDK = new SDK<MonitorConfig>({
name: 'monitor',
version: '1.0.0',
config: {
appId: 'xxx', // 类型安全:必填
reportUrl: '/api/report', // 类型安全:必填
sampleRate: 0.1, // 类型安全:可选
},
});
10.2 插件类型扩展(声明合并)
利用 TypeScript 的声明合并,让用户安装插件后获得增强的类型提示:
// 基础 SDK 事件类型
interface SDKEventMap {
'lifecycle:beforeInit': () => void;
'lifecycle:initialized': () => void;
'lifecycle:beforeDestroy': () => void;
'lifecycle:destroyed': () => void;
'configChange': (config: BaseConfig) => void;
}
// 性能插件通过声明合并扩展事件类型
// 安装 PerformancePlugin 后,用户 on('performance:metric', ...) 会有类型提示
declare module '@my-org/sdk' {
interface SDKEventMap {
'performance:metric': (data: {
name: string;
value: number;
timestamp: number;
}) => void;
'performance:longtask': (duration: number) => void;
}
}
// 强类型的事件监听
class TypedEventEmitter {
on<K extends keyof SDKEventMap>(
event: K,
handler: SDKEventMap[K]
): () => void {
// ...
return () => {};
}
emit<K extends keyof SDKEventMap>(
event: K,
...args: Parameters<SDKEventMap[K]>
): void {
// ...
}
}
10.3 条件类型与工具类型
/** 提取插件的 meta.name 字面量类型 */
type PluginName<P extends Plugin> = P['meta']['name'];
/** 深度只读 */
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
/** 深度可选 */
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
/** 排除 undefined 的配置 */
type RequiredConfig<T extends BaseConfig> = Required<{
[K in keyof T]: NonNullable<T[K]>;
}>;
/** 从配置类型推断 SDK 实例类型 */
type SDKInstance<TConfig extends BaseConfig> = SDK<TConfig> & {
getConfig(): DeepReadonly<TConfig>;
};
十一、实际案例分析
11.1 Sentry SDK 架构
Sentry 是前端监控领域最知名的 SDK,其架构设计极具参考价值:
Sentry 的核心设计思想:
| 概念 | 说明 | 对应本文架构 |
|---|---|---|
| Hub | 全局单例,管理 Client 和 Scope | Core 内核 |
| Scope | 上下文信息(用户、tags、breadcrumbs) | ConfigManager |
| Client | 负责事件处理和发送 | PluginManager + Reporter |
| Transport | 底层传输抽象(fetch/XHR) | Platform 适配层 |
| Integration | 插件系统 | Plugin 插件系统 |
Sentry SDK 使用示例
import * as Sentry from '@sentry/react';
Sentry.init({
dsn: 'https://xxx@sentry.io/123',
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration(),
],
tracesSampleRate: 0.1,
replaysSessionSampleRate: 0.1,
});
// 手动捕获错误
try {
dangerousOperation();
} catch (error) {
Sentry.captureException(error, {
tags: { module: 'payment' },
extra: { orderId: '12345' },
});
}
11.2 Analytics SDK 架构
Analytics SDK(如 Google Analytics、Mixpanel)的核心是事件采集 + 数据上报:
| 模块 | 职责 | 关键技术 |
|---|---|---|
| 采集器 | 自动采集 PV、点击、滚动等 | MutationObserver、IntersectionObserver |
| 会话管理 | 用户标识、会话超时 | Cookie、localStorage、指纹 |
| 数据管道 | 缓冲、批量、压缩、采样 | requestIdleCallback、sendBeacon |
| 身份解析 | 匿名 ID → 登录 ID 关联 | identify/alias |
11.3 播放器 SDK 架构
播放器 SDK(如 video.js、DPlayer)是插件化架构的经典案例:
播放器 SDK 的特殊设计点:
- 状态机:播放器有复杂的状态转换(idle → loading → playing → paused → ended)
- 中间件模式:请求拦截(token 注入、CDN 切换)、响应拦截(DRM 解密)
- 自适应码率:根据网速动态切换分辨率(ABR 算法)
- 多端适配:H5、iOS WebView、Android WebView 的差异处理
十二、扩展设计
12.1 中间件模式
除了插件系统,SDK 还可以引入中间件模式处理数据流:
type Middleware<T> = (data: T, next: () => Promise<T>) => Promise<T>;
class MiddlewareChain<T> {
private middlewares: Middleware<T>[] = [];
use(middleware: Middleware<T>): void {
this.middlewares.push(middleware);
}
async execute(data: T): Promise<T> {
let index = 0;
const next = async (): Promise<T> => {
if (index >= this.middlewares.length) {
return data;
}
const middleware = this.middlewares[index++];
return middleware(data, next); // 洋葱模型
};
return next();
}
}
// 使用示例:上报数据的中间件链
const reportChain = new MiddlewareChain<ReportData>();
// 中间件 1:添加公共字段
reportChain.use(async (data, next) => {
data.sdkVersion = '1.0.0';
data.timestamp = Date.now();
return next();
});
// 中间件 2:数据脱敏
reportChain.use(async (data, next) => {
if (data.userInfo?.phone) {
data.userInfo.phone = data.userInfo.phone.replace(
/(\d{3})\d{4}(\d{4})/,
'$1****$2'
);
}
return next();
});
// 中间件 3:采样控制
reportChain.use(async (data, next) => {
if (Math.random() > 0.1) return data; // 90% 丢弃
return next();
});
12.2 Hook 机制
提供生命周期 Hook,让用户在关键节点注入自定义逻辑:
interface SDKHooks<TConfig extends BaseConfig> {
/** 数据上报前,可修改或拦截 */
beforeSend?: (data: ReportData) => ReportData | null;
/** 错误捕获后 */
onError?: (error: Error, info: ErrorInfo) => void;
/** 配置变更后 */
onConfigChange?: (config: TConfig) => void;
/** SDK 初始化完成 */
onReady?: () => void;
}
// 使用示例
const sdk = new SDK<MonitorConfig>({
name: 'monitor',
version: '1.0.0',
config: {
appId: 'xxx',
reportUrl: '/api/report',
hooks: {
beforeSend(data) {
// 过滤掉 Script error
if (data.message === 'Script error.') return null;
// 添加自定义字段
data.extra = { page: location.pathname };
return data;
},
onError(error) {
console.log('Custom error handler:', error.message);
},
},
},
});
常见面试问题
Q1: 如何设计一个可扩展的前端 SDK?
答案:
设计可扩展 SDK 的核心是内核 + 插件架构,遵循「开放-封闭原则」:对扩展开放,对修改封闭。
设计要点:
- 分层架构:用户层(API)→ 核心层(调度)→ 插件层(功能)→ 适配层(平台)
- 最小化内核:内核只负责生命周期管理、事件调度、配置管理、错误处理
- 插件系统:所有业务功能以插件形式实现,支持注册、卸载、依赖排序
- 事件总线:模块间通过事件通信,避免直接依赖
- TypeScript 类型:泛型配置、声明合并支持插件类型扩展
// 内核只有几十行代码
const sdk = new SDK({ name: 'mySDK', version: '1.0.0', config: {} });
// 所有功能由插件提供
sdk.use(ErrorPlugin);
sdk.use(PerformancePlugin);
sdk.use(ReporterPlugin);
await sdk.init();
面试加分点:提到 Sentry 的 Integration、Vite 的 Plugin、Webpack 的 Tapable 等实际参考,并能对比它们的设计差异。
Q2: SDK 的插件系统怎么实现?
答案:
插件系统实现的核心包括四个部分:
| 部分 | 关键实现 |
|---|---|
| 插件接口 | 定义 meta(名称、版本、依赖)+ install + uninstall |
| 插件管理器 | 注册、去重、拓扑排序、依次安装、逆序卸载 |
| 插件上下文 | 暴露受限的 SDK 能力(配置、事件、日志)给插件 |
| 错误隔离 | 单个插件安装失败不影响其他插件和内核 |
依赖排序使用拓扑排序算法:
// 插件声明依赖
const ReporterPlugin = {
meta: { name: 'reporter', dependencies: ['performance'] },
install(ctx) { /* ... */ },
};
// PluginManager 使用拓扑排序确保 performance 在 reporter 之前安装
// 同时检测循环依赖:visiting Set 中出现重复即为循环
**插件上下文(PluginContext)**是安全性的关键 —— 不直接暴露 SDK 实例,而是提供一个受限的接口,防止插件做破坏性操作。
Q3: 如何保证 SDK 的稳定性?
答案:
SDK 的稳定性需要从设计、开发、测试、运行时四个层面保障:
| 层面 | 手段 |
|---|---|
| 设计 | 插件错误隔离、全局错误边界、降级策略 |
| 开发 | 严格的 TypeScript 类型、Code Review、100% 核心路径测试覆盖 |
| 测试 | 单元测试、集成测试、兼容性测试(BrowserStack)、压力测试 |
| 运行时 | 错误风暴保护(maxErrors)、异常不上抛到宿主、sendBeacon 保证数据 |
关键代码示例 —— 安全执行包装器:
// 所有可能出错的操作都用 safeExecute 包裹
errorBoundary.safeExecute(
() => plugin.install(ctx),
{ phase: 'init', plugin: plugin.meta.name },
undefined // fallback 值
);
面试加分点:
- 提到 灰度发布:新版本先推 1% 用户,观察监控数据再全量
- 提到 版本锁定:CDN 引入使用确定版本号
@1.2.3,而非@latest - 提到 兼容性矩阵:维护一张 浏览器 x 特性 的支持表
Q4: SDK 的体积如何优化到极致?
答案:
SDK 体积优化分为构建时和运行时两个阶段:
构建时优化:
| 策略 | 效果 | 实现方式 |
|---|---|---|
| Tree Shaking | 移除未使用代码 | ESM 导出 + sideEffects: false |
| 代码压缩 | 减小体积 30%+ | Terser / esbuild minify |
| 插件分包 | 按需加载 | 每个插件独立入口 |
| 零依赖 | 避免引入第三方库 | 自研轻量工具函数 |
运行时优化:
// 动态导入:只在需要时加载插件
const sdk = new SDK({ name: 'app', version: '1.0.0', config: {} });
// Core 只有 2KB,插件按需加载
if (needPerformanceMonitoring) {
const { PerformancePlugin } = await import('@my-org/sdk/plugins/performance');
sdk.use(PerformancePlugin);
}
体积审计工具:
bundlephobia.com:查看 npm 包体积webpack-bundle-analyzer:可视化分析打包产物size-limit:CI 中自动检查体积是否超标
{
"size-limit": [
{ "path": "dist/index.esm.js", "limit": "3 KB" },
{ "path": "dist/plugins/performance.esm.js", "limit": "2 KB" }
]
}
Q5: 如何实现 SDK 的多端适配(浏览器、Node.js、小程序)?
答案:
多端适配的核心是平台抽象层(Platform Adapter):
interface PlatformAdapter {
/** 网络请求 */
request(url: string, options: RequestOptions): Promise<Response>;
/** 存储 */
getStorage(key: string): string | null;
setStorage(key: string, value: string): void;
/** 定时器 */
setTimeout(fn: () => void, ms: number): number;
/** 页面可见性 */
onVisibilityChange?(callback: (visible: boolean) => void): void;
}
// 浏览器适配
class BrowserAdapter implements PlatformAdapter {
request(url: string, options: RequestOptions) {
return fetch(url, options);
}
getStorage(key: string) {
return localStorage.getItem(key);
}
// ...
}
// Node.js 适配
class NodeAdapter implements PlatformAdapter {
async request(url: string, options: RequestOptions) {
const { default: fetch } = await import('node-fetch');
return fetch(url, options);
}
getStorage() {
return null; // Node.js 无 localStorage
}
// ...
}
SDK 初始化时自动检测环境并选择适配器,也支持用户手动注入:
const sdk = new SDK({
name: 'mySDK',
version: '1.0.0',
config: {},
// 自动检测或手动指定
platform: isBrowser() ? new BrowserAdapter() : new NodeAdapter(),
});
相关链接
- Sentry JavaScript SDK 源码 - 前端监控 SDK 的参考实现
- video.js 插件开发指南 - 播放器 SDK 的插件系统设计
- Rollup 官方文档 - SDK 构建工具首选
- tsup 文档 - 零配置 TypeScript 库打包
- size-limit - 控制 JS 库体积的工具
- Semantic Versioning - 语义化版本规范
- MDN - PerformanceObserver - 性能观察器 API
- MDN - sendBeacon - 可靠的数据上报 API