跳到主要内容

适配器模式

问题

什么是适配器模式?如何在前端处理不同数据格式、不同 API 接口的兼容问题?

答案

适配器模式(Adapter Pattern)将一个类的接口转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以协同工作。在前端开发中,适配器模式常用于处理数据格式转换、第三方 API 封装等场景。


核心概念

适配器类型

类型说明实现方式
类适配器通过继承实现继承目标类和被适配类
对象适配器通过组合实现持有被适配对象引用
接口适配器提供默认实现抽象类实现空方法

基础实现

对象适配器

// 目标接口 - 我们期望的格式
interface MediaPlayer {
play(fileName: string): void;
}

// 被适配者 - 旧的播放器
class LegacyAudioPlayer {
playAudio(file: string, format: 'mp3' | 'wav') {
console.log(`Playing ${format} file: ${file}`);
}
}

class LegacyVideoPlayer {
playVideo(file: string, resolution: string) {
console.log(`Playing video: ${file} at ${resolution}`);
}
}

// 适配器
class AudioPlayerAdapter implements MediaPlayer {
private legacyPlayer: LegacyAudioPlayer;

constructor() {
this.legacyPlayer = new LegacyAudioPlayer();
}

play(fileName: string) {
const format = fileName.endsWith('.mp3') ? 'mp3' : 'wav';
this.legacyPlayer.playAudio(fileName, format);
}
}

class VideoPlayerAdapter implements MediaPlayer {
private legacyPlayer: LegacyVideoPlayer;
private defaultResolution: string;

constructor(resolution = '1080p') {
this.legacyPlayer = new LegacyVideoPlayer();
this.defaultResolution = resolution;
}

play(fileName: string) {
this.legacyPlayer.playVideo(fileName, this.defaultResolution);
}
}

// 统一使用
function playMedia(player: MediaPlayer, file: string) {
player.play(file);
}

const audioAdapter = new AudioPlayerAdapter();
const videoAdapter = new VideoPlayerAdapter();

playMedia(audioAdapter, 'song.mp3'); // Playing mp3 file: song.mp3
playMedia(videoAdapter, 'movie.mp4'); // Playing video: movie.mp4 at 1080p

函数式适配器

// 源数据格式
interface LegacyUser {
user_id: string;
user_name: string;
user_email: string;
create_time: number;
}

// 目标数据格式
interface User {
id: string;
name: string;
email: string;
createdAt: Date;
}

// 适配器函数
function adaptUser(legacyUser: LegacyUser): User {
return {
id: legacyUser.user_id,
name: legacyUser.user_name,
email: legacyUser.user_email,
createdAt: new Date(legacyUser.create_time),
};
}

// 批量适配
function adaptUsers(legacyUsers: LegacyUser[]): User[] {
return legacyUsers.map(adaptUser);
}

// 双向适配器
function reverseAdaptUser(user: User): LegacyUser {
return {
user_id: user.id,
user_name: user.name,
user_email: user.email,
create_time: user.createdAt.getTime(),
};
}

// 使用
const legacyData: LegacyUser = {
user_id: '123',
user_name: 'John',
user_email: 'john@example.com',
create_time: Date.now(),
};

const user = adaptUser(legacyData);
console.log(user);
// { id: '123', name: 'John', email: 'john@example.com', createdAt: Date }

前端实际应用

1. API 响应适配器

// 不同 API 的响应格式
interface GitHubUser {
login: string;
avatar_url: string;
html_url: string;
public_repos: number;
}

interface GitLabUser {
username: string;
avatar_url: string;
web_url: string;
projects_count: number;
}

interface BitbucketUser {
nickname: string;
links: {
avatar: { href: string };
html: { href: string };
};
}

// 统一的用户格式
interface UnifiedUser {
username: string;
avatarUrl: string;
profileUrl: string;
repoCount?: number;
}

// 适配器接口
interface UserAdapter {
adapt(data: unknown): UnifiedUser;
}

// GitHub 适配器
class GitHubUserAdapter implements UserAdapter {
adapt(data: GitHubUser): UnifiedUser {
return {
username: data.login,
avatarUrl: data.avatar_url,
profileUrl: data.html_url,
repoCount: data.public_repos,
};
}
}

// GitLab 适配器
class GitLabUserAdapter implements UserAdapter {
adapt(data: GitLabUser): UnifiedUser {
return {
username: data.username,
avatarUrl: data.avatar_url,
profileUrl: data.web_url,
repoCount: data.projects_count,
};
}
}

// Bitbucket 适配器
class BitbucketUserAdapter implements UserAdapter {
adapt(data: BitbucketUser): UnifiedUser {
return {
username: data.nickname,
avatarUrl: data.links.avatar.href,
profileUrl: data.links.html.href,
};
}
}

// 适配器工厂
class UserAdapterFactory {
private static adapters: Record<string, UserAdapter> = {
github: new GitHubUserAdapter(),
gitlab: new GitLabUserAdapter(),
bitbucket: new BitbucketUserAdapter(),
};

static getAdapter(platform: string): UserAdapter {
const adapter = this.adapters[platform];
if (!adapter) {
throw new Error(`Unknown platform: ${platform}`);
}
return adapter;
}
}

// 使用
async function fetchUser(platform: string, username: string): Promise<UnifiedUser> {
const apiUrls: Record<string, string> = {
github: `https://api.github.com/users/${username}`,
gitlab: `https://gitlab.com/api/v4/users/${username}`,
bitbucket: `https://api.bitbucket.org/2.0/users/${username}`,
};

const response = await fetch(apiUrls[platform]);
const data = await response.json();

const adapter = UserAdapterFactory.getAdapter(platform);
return adapter.adapt(data);
}

2. 存储适配器

// 统一的存储接口
interface Storage {
get<T>(key: string): T | null;
set<T>(key: string, value: T): void;
remove(key: string): void;
clear(): void;
}

// LocalStorage 适配器
class LocalStorageAdapter implements Storage {
get<T>(key: string): T | null {
const item = localStorage.getItem(key);
if (!item) return null;
try {
return JSON.parse(item) as T;
} catch {
return item as unknown as T;
}
}

set<T>(key: string, value: T): void {
localStorage.setItem(key, JSON.stringify(value));
}

remove(key: string): void {
localStorage.removeItem(key);
}

clear(): void {
localStorage.clear();
}
}

// SessionStorage 适配器
class SessionStorageAdapter implements Storage {
get<T>(key: string): T | null {
const item = sessionStorage.getItem(key);
if (!item) return null;
try {
return JSON.parse(item) as T;
} catch {
return item as unknown as T;
}
}

set<T>(key: string, value: T): void {
sessionStorage.setItem(key, JSON.stringify(value));
}

remove(key: string): void {
sessionStorage.removeItem(key);
}

clear(): void {
sessionStorage.clear();
}
}

// Cookie 适配器
class CookieStorageAdapter implements Storage {
private days: number;

constructor(days = 7) {
this.days = days;
}

get<T>(key: string): T | null {
const cookies = document.cookie.split(';');
for (const cookie of cookies) {
const [name, value] = cookie.split('=').map((s) => s.trim());
if (name === key) {
try {
return JSON.parse(decodeURIComponent(value)) as T;
} catch {
return decodeURIComponent(value) as unknown as T;
}
}
}
return null;
}

set<T>(key: string, value: T): void {
const expires = new Date(Date.now() + this.days * 24 * 60 * 60 * 1000);
document.cookie = `${key}=${encodeURIComponent(JSON.stringify(value))};expires=${expires.toUTCString()};path=/`;
}

remove(key: string): void {
document.cookie = `${key}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`;
}

clear(): void {
const cookies = document.cookie.split(';');
for (const cookie of cookies) {
const [name] = cookie.split('=');
this.remove(name.trim());
}
}
}

// IndexedDB 适配器(简化版)
class IndexedDBAdapter implements Storage {
private dbName: string;
private storeName: string;
private db: IDBDatabase | null = null;

constructor(dbName = 'app-storage', storeName = 'keyvalue') {
this.dbName = dbName;
this.storeName = storeName;
}

private async getDB(): Promise<IDBDatabase> {
if (this.db) return this.db;

return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);

request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(this.db);
};

request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName);
}
};
});
}

// 注意:IndexedDB 是异步的,这里简化为同步 API
get<T>(key: string): T | null {
// 实际应用中应该返回 Promise
console.warn('IndexedDB get is async, use getAsync instead');
return null;
}

async getAsync<T>(key: string): Promise<T | null> {
const db = await this.getDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction(this.storeName, 'readonly');
const store = transaction.objectStore(this.storeName);
const request = store.get(key);

request.onsuccess = () => resolve(request.result ?? null);
request.onerror = () => reject(request.error);
});
}

set<T>(key: string, value: T): void {
this.setAsync(key, value);
}

async setAsync<T>(key: string, value: T): Promise<void> {
const db = await this.getDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction(this.storeName, 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.put(value, key);

request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}

remove(key: string): void {
this.removeAsync(key);
}

async removeAsync(key: string): Promise<void> {
const db = await this.getDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction(this.storeName, 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.delete(key);

request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}

clear(): void {
this.clearAsync();
}

async clearAsync(): Promise<void> {
const db = await this.getDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction(this.storeName, 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.clear();

request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
}

// 使用 - 可随时切换存储方式
const storage: Storage = new LocalStorageAdapter();
storage.set('user', { name: 'John' });
const user = storage.get('user');

3. HTTP 请求适配器

// 统一的请求接口
interface HttpClient {
get<T>(url: string, config?: RequestConfig): Promise<T>;
post<T>(url: string, data?: unknown, config?: RequestConfig): Promise<T>;
put<T>(url: string, data?: unknown, config?: RequestConfig): Promise<T>;
delete<T>(url: string, config?: RequestConfig): Promise<T>;
}

interface RequestConfig {
headers?: Record<string, string>;
timeout?: number;
}

// Fetch 适配器
class FetchAdapter implements HttpClient {
private baseURL: string;

constructor(baseURL = '') {
this.baseURL = baseURL;
}

private async request<T>(
url: string,
options: RequestInit
): Promise<T> {
const response = await fetch(this.baseURL + url, options);
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
return response.json();
}

async get<T>(url: string, config?: RequestConfig): Promise<T> {
return this.request<T>(url, {
method: 'GET',
headers: config?.headers,
});
}

async post<T>(url: string, data?: unknown, config?: RequestConfig): Promise<T> {
return this.request<T>(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...config?.headers,
},
body: JSON.stringify(data),
});
}

async put<T>(url: string, data?: unknown, config?: RequestConfig): Promise<T> {
return this.request<T>(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...config?.headers,
},
body: JSON.stringify(data),
});
}

async delete<T>(url: string, config?: RequestConfig): Promise<T> {
return this.request<T>(url, {
method: 'DELETE',
headers: config?.headers,
});
}
}

// XMLHttpRequest 适配器(兼容旧浏览器)
class XHRAdapter implements HttpClient {
private baseURL: string;

constructor(baseURL = '') {
this.baseURL = baseURL;
}

private request<T>(
method: string,
url: string,
data?: unknown,
config?: RequestConfig
): Promise<T> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, this.baseURL + url);

if (config?.timeout) {
xhr.timeout = config.timeout;
}

if (config?.headers) {
Object.entries(config.headers).forEach(([key, value]) => {
xhr.setRequestHeader(key, value);
});
}

if (data) {
xhr.setRequestHeader('Content-Type', 'application/json');
}

xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(`HTTP Error: ${xhr.status}`));
}
};

xhr.onerror = () => reject(new Error('Network Error'));
xhr.ontimeout = () => reject(new Error('Request Timeout'));

xhr.send(data ? JSON.stringify(data) : null);
});
}

get<T>(url: string, config?: RequestConfig) {
return this.request<T>('GET', url, undefined, config);
}

post<T>(url: string, data?: unknown, config?: RequestConfig) {
return this.request<T>('POST', url, data, config);
}

put<T>(url: string, data?: unknown, config?: RequestConfig) {
return this.request<T>('PUT', url, data, config);
}

delete<T>(url: string, config?: RequestConfig) {
return this.request<T>('DELETE', url, undefined, config);
}
}

// 自动选择适配器
function createHttpClient(baseURL?: string): HttpClient {
if (typeof fetch !== 'undefined') {
return new FetchAdapter(baseURL);
}
return new XHRAdapter(baseURL);
}

// 使用
const http = createHttpClient('https://api.example.com');
const users = await http.get<User[]>('/users');

4. 事件系统适配器

// 统一的事件接口
interface EventEmitter {
on(event: string, handler: (...args: unknown[]) => void): void;
off(event: string, handler: (...args: unknown[]) => void): void;
emit(event: string, ...args: unknown[]): void;
}

// DOM 事件适配器
class DOMEventAdapter implements EventEmitter {
private element: HTMLElement;
private handlers: Map<string, Map<Function, EventListener>> = new Map();

constructor(element: HTMLElement) {
this.element = element;
}

on(event: string, handler: (...args: unknown[]) => void) {
const listener: EventListener = (e) => handler(e);

if (!this.handlers.has(event)) {
this.handlers.set(event, new Map());
}
this.handlers.get(event)!.set(handler, listener);

this.element.addEventListener(event, listener);
}

off(event: string, handler: (...args: unknown[]) => void) {
const listener = this.handlers.get(event)?.get(handler);
if (listener) {
this.element.removeEventListener(event, listener);
this.handlers.get(event)!.delete(handler);
}
}

emit(event: string, ...args: unknown[]) {
const customEvent = new CustomEvent(event, { detail: args });
this.element.dispatchEvent(customEvent);
}
}

// WebSocket 适配器
class WebSocketEventAdapter implements EventEmitter {
private ws: WebSocket;
private handlers: Map<string, Set<(...args: unknown[]) => void>> = new Map();

constructor(url: string) {
this.ws = new WebSocket(url);

this.ws.onmessage = (event) => {
try {
const { type, data } = JSON.parse(event.data);
this.trigger(type, data);
} catch {
this.trigger('message', event.data);
}
};

this.ws.onopen = () => this.trigger('open');
this.ws.onclose = () => this.trigger('close');
this.ws.onerror = (e) => this.trigger('error', e);
}

private trigger(event: string, ...args: unknown[]) {
this.handlers.get(event)?.forEach((handler) => handler(...args));
}

on(event: string, handler: (...args: unknown[]) => void) {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event)!.add(handler);
}

off(event: string, handler: (...args: unknown[]) => void) {
this.handlers.get(event)?.delete(handler);
}

emit(event: string, ...args: unknown[]) {
this.ws.send(JSON.stringify({ type: event, data: args }));
}
}

// 使用 - 统一的事件处理方式
const domEvents = new DOMEventAdapter(document.body);
domEvents.on('click', (e) => console.log('Clicked', e));

const wsEvents = new WebSocketEventAdapter('wss://example.com');
wsEvents.on('message', (data) => console.log('Received', data));
wsEvents.emit('chat', 'Hello');

5. 地图 API 适配器

// 统一的地图接口
interface MapService {
createMap(container: string, options: MapOptions): void;
setCenter(lat: number, lng: number): void;
addMarker(lat: number, lng: number, options?: MarkerOptions): void;
setZoom(level: number): void;
}

interface MapOptions {
center: { lat: number; lng: number };
zoom: number;
}

interface MarkerOptions {
title?: string;
icon?: string;
}

// 高德地图适配器
class AMapAdapter implements MapService {
private map: unknown;

createMap(container: string, options: MapOptions) {
// @ts-ignore - AMap 全局变量
this.map = new AMap.Map(container, {
center: [options.center.lng, options.center.lat],
zoom: options.zoom,
});
}

setCenter(lat: number, lng: number) {
// @ts-ignore
this.map.setCenter([lng, lat]);
}

addMarker(lat: number, lng: number, options?: MarkerOptions) {
// @ts-ignore
const marker = new AMap.Marker({
position: [lng, lat],
title: options?.title,
});
// @ts-ignore
this.map.add(marker);
}

setZoom(level: number) {
// @ts-ignore
this.map.setZoom(level);
}
}

// 百度地图适配器
class BMapAdapter implements MapService {
private map: unknown;

createMap(container: string, options: MapOptions) {
// @ts-ignore - BMap 全局变量
this.map = new BMap.Map(container);
// @ts-ignore
const point = new BMap.Point(options.center.lng, options.center.lat);
// @ts-ignore
this.map.centerAndZoom(point, options.zoom);
}

setCenter(lat: number, lng: number) {
// @ts-ignore
const point = new BMap.Point(lng, lat);
// @ts-ignore
this.map.setCenter(point);
}

addMarker(lat: number, lng: number, options?: MarkerOptions) {
// @ts-ignore
const point = new BMap.Point(lng, lat);
// @ts-ignore
const marker = new BMap.Marker(point);
if (options?.title) {
// @ts-ignore
marker.setTitle(options.title);
}
// @ts-ignore
this.map.addOverlay(marker);
}

setZoom(level: number) {
// @ts-ignore
this.map.setZoom(level);
}
}

// 使用 - 切换地图服务只需更换适配器
const mapService: MapService = new AMapAdapter();
// const mapService: MapService = new BMapAdapter();

mapService.createMap('map-container', {
center: { lat: 39.9, lng: 116.4 },
zoom: 12,
});
mapService.addMarker(39.9, 116.4, { title: '北京' });

常见面试问题

Q1: 适配器模式的优缺点?

答案

优点缺点
解耦客户端与接口增加系统复杂度
提高类的复用性过多适配器难以维护
灵活性好可能影响性能
符合开闭原则-

Q2: 适配器和装饰器的区别?

答案

对比项适配器模式装饰器模式
目的接口转换功能增强
接口转换为新接口保持原接口
时机设计后期设计时考虑
结构单层包装可多层包装
// 适配器 - 接口转换
class Adapter implements NewInterface {
private adaptee: OldInterface;

newMethod() {
return this.adaptee.oldMethod(); // 转换调用
}
}

// 装饰器 - 功能增强
class Decorator implements Interface {
private wrapped: Interface;

method() {
// 增强前
const result = this.wrapped.method();
// 增强后
return result;
}
}

Q3: 什么时候使用适配器模式?

答案

// 场景 1: 集成第三方库
class ThirdPartyLibAdapter {
adapt(data: OurFormat): TheirFormat {
// 转换数据格式
}
}

// 场景 2: 处理遗留系统
class LegacySystemAdapter {
private legacy: LegacySystem;

modernMethod() {
return this.legacy.oldMethod(); // 包装旧接口
}
}

// 场景 3: 统一多个 API
class UnifiedApiAdapter {
getUser(platform: string) {
switch(platform) {
case 'github': return adaptGitHubUser();
case 'gitlab': return adaptGitLabUser();
}
}
}

// 场景 4: 跨平台兼容
class CrossPlatformAdapter {
storage: Storage;

constructor() {
this.storage = isNode
? new FileStorageAdapter()
: new LocalStorageAdapter();
}
}

Q4: 前端常见的适配器应用?

答案

场景说明
API 数据转换后端数据格式 → 前端格式
组件库封装统一不同 UI 库接口
存储抽象LocalStorage/IndexedDB 统一
请求封装Fetch/Axios 统一接口
地图服务高德/百度/腾讯地图统一
支付集成微信/支付宝统一接口

相关链接