适配器模式和外观模式是前端开发中最常用的两种"包装型"设计模式。很多开发者能写出转换函数和封装工具,却讲不清它们什么时候用、区别在哪。本文用六个真实场景把这两种模式拆开,对比它们的本质差异。

一、适配器模式:让不兼容的接口一起工作
适配器模式的核心是:将一个接口转换成客户期望的另一个接口,使原本不兼容的类可以一起工作。 它解决的是"已经有了一堆东西,但它们对接不上"的问题。
1. 统一后端接口数据格式
后端接口升级,返回结构全变了,而页面组件期望的仍是旧格式。在数据进入组件之前加一层适配器,组件零改动。
javascript
// 适配器:把新接口数据转成组件期望的旧格式
function userAdapter(apiData) {
const user = apiData.data;
return {
id: user.user_id,
name: user.user_name,
email: user.contact?.email_addr ?? '',
phone: user.contact?.phone_num ?? '',
};
}
// 使用
const adaptedUser = userAdapter(newApiResponse);
renderUserCard(adaptedUser);
价值:后端怎么改都行,只要更新适配器函数,所有组件无需变动。Axios 的请求/响应拦截器本质上也是适配器------在请求发出前和响应回来后做数据转换。
2. 多端 API 统一
同一个 H5 需要运行在浏览器、微信小程序、企业微信中,它们的存储 API 各不相同。用一个适配器对象统一对外接口:
javascript
const storageEngines = {
browser: {
get(key) { return localStorage.getItem(key); },
set(key, val) { localStorage.setItem(key, val); },
},
wechat: {
get(key) { return wx.getStorageSync(key); },
set(key, val) { wx.setStorageSync(key, val); },
},
wecom: {
get(key) { return wx.qy.getStorageSync(key); },
set(key, val) { wx.qy.setStorageSync(key, val); },
},
};
function createStorage() {
const engine = typeof wx?.qy !== 'undefined' ? 'wecom'
: typeof wx !== 'undefined' ? 'wechat'
: 'browser';
return storageEngines[engine];
}
价值 :新增支付宝小程序,只需在 storageEngines 加一个 key,所有业务代码不动。
3. 封装第三方图表库
ECharts 和 Highcharts 初始化方式完全不同,通过适配器提供统一的 setOptions 和 destroy 接口,业务组件不感知底层引擎。
javascript
class EChartsAdapter {
constructor(container) { this.chart = echarts.init(container); }
setOptions(options) { this.chart.setOption(options); }
destroy() { this.chart.dispose(); }
}
class HighchartsAdapter {
constructor(container) { this.chart = new Highcharts.Chart(container, {}); }
setOptions(options) { this.chart.update(options); }
destroy() { this.chart.destroy(); }
}
价值:切换图表库只需改一个字符串,调用方代码完全不变。适配器让更换第三方库的成本降到最低。
二、外观模式:把复杂的变简单
外观模式的核心是:为复杂的子系统提供一个简化的统一接口,隐藏内部实现细节,让调用方更容易使用。 它解决的是"东西本身很好用,但用起来太麻烦"的问题。
1. 封装地图 SDK
高德地图初始化需要设置容器、中心点、添加控件,内部步骤繁琐。外观模式将其封装成几个简单方法,团队其他人无需学习 SDK 细节。
javascript
class MapFacade {
#map = null;
init(containerId, { zoom = 12, center }) {
this.#map = new AMap.Map(containerId, { zoom, center });
this.#map.addControl(new AMap.Scale());
this.#map.addControl(new AMap.ToolBar());
return this;
}
addMarker(position, content, onClick) {
const marker = new AMap.Marker({ position, content });
if (onClick) marker.on('click', onClick);
this.#map.add(marker);
return marker;
}
destroy() { this.#map?.destroy(); }
}
价值:将来换成百度地图,只需改外观类内部,所有调用方不动。外观让复杂 SDK 变得"开箱即用"。
2. 统一浏览器存储 API
localStorage、sessionStorage、Cookie 原生 API 各不相同,每次存取还需手动序列化。外观模式对外提供一致的 get、set 接口:
javascript
export const storage = {
local: {
get(key) {
const val = localStorage.getItem(key);
return val ? JSON.parse(val) : null;
},
set(key, value) { localStorage.setItem(key, JSON.stringify(value)); },
remove(key) { localStorage.removeItem(key); },
},
session: {
get(key) { /* 同理 */ },
set(key, value) { /* 同理 */ },
},
cookie: {
get(key) { /* 同上 */ },
set(key, value, days = 7) { /* 同上 */ },
},
};
价值:调用方不需要知道底层用哪种存储,也不用每次手动序列化。统一的 API 让代码更简洁。
3. 简化复杂操作流程
导出 PDF 需要调用 html2canvas 和 jsPDF 两个库,涉及截图、生成、下载等多个步骤。外观函数将这些步骤打包成一个接口。
javascript
export async function exportPDF(title, elementId) {
const element = document.getElementById(elementId);
const loading = showLoading('正在生成 PDF...');
try {
const canvas = await html2canvas(element, { scale: 2 });
const pdf = new jsPDF('p', 'mm', 'a4');
pdf.addImage(canvas.toDataURL('image/png'), 'PNG', 0, 0, 210, 297);
pdf.save(`${title}.pdf`);
} finally {
hideLoading(loading);
}
}
价值 :调用方从需要了解两个库的复杂用法,变成一行 await exportPDF('报表', 'container')。外观让复杂操作像点外卖一样简单。
三、适配器 vs 外观:一张表讲清楚
| 对比维度 | 适配器模式 | 外观模式 |
|---|---|---|
| 核心目的 | 解决不兼容------让原本对接不上的接口能协作 | 解决太复杂------隐藏内部细节,提供简单入口 |
| 典型场景 | 新旧 API 格式统一、多端适配、第三方库接口转接 | 封装地图/图表 SDK、统一存储、简化复杂操作流程 |
| 接口变化 | 改变接口形状(A→B) | 定义新的简化接口 |
| 类比 | "翻译官"------把一种语言转成另一种 | "遥控器"------按一个键就能完成复杂操作 |
| 前端内置 | Axios 拦截器、Babel 转译、Vue 的 computed | Vue 的 createApp、React 的 createRoot、Axios 本身 |
一个最简区分口诀:适配器是"换皮"------改变接口,解决兼容问题;外观是"减负"------降低使用门槛,让复杂变简单。如果你在两个已有接口之间做转换,那是适配器;如果你把一堆复杂的调用封装成一个简单方法,那是外观。
它们也可以组合使用:外观类的内部,可能使用适配器来对接多个不兼容的子系统;反过来,适配器也能作为一个更大外观的一部分。
五、总结
适配器和外观都做包装,但目的不同:
- 适配器:已有两个东西对接不上 → 加一层转换,让它们能一起工作。
- 外观:一个东西本身太复杂 → 加一层简化,让调用者更好用。
在日常开发中,当你写了一个函数去转换数据格式,那是适配器;当你封装了一个工具类让团队更容易上手,那是外观。把这两个模式分清楚,能让你的代码更优雅。