多端统一适配指南:告别 if else
引言:多端 H5 的「分裂」之痛
随着移动互联网的发展,H5 页面早已不只在手机浏览器中运行。它可能被嵌入多个不同的 App(如公司主 App、合作方 App)、运行在微信/支付宝/字节等小程序容器中,甚至还要兼容 PC 浏览器。每个端都有一套自己的 JS-SDK,用来调用原生能力(支付、分享、登录、地理位置等)。这些 SDK 的方法名、参数格式、返回值规范往往各不相同,甚至有些端根本不存在某些能力。
当业务方要求「一套 H5 代码,多端运行」时,开发者面临的第一道坎就是:如何优雅地处理这些差异?
直接 if else 判断环境可能是最先想到的方案,但这是最优解吗?本文将带你从原始 if else 出发,逐步演进到适配器模式,并最终封装成开箱即用的 SDK,彻底解决多端 API 调用的混乱局面。
一、问题复现:不同端的 API 差异有多大?
假设我们需要实现一个支付功能,在三端调用方式如下:
- App1 :通过
window.App1JSBridge.invoke('pay', params, callback)调用,参数是对象,回调返回结果。 - App2 :通过
window.Native.call('PayOrder', JSON.stringify(params))调用,参数是 JSON 字符串,返回值是 Promise。 - 小程序 :使用
wx.requestPayment(params),参数格式完全不同,且返回格式也与 App 不同。
如果业务代码直接写死某端的调用,那在其他端就会报错。于是,第一个朴素的想法诞生了:
二、方案一:业务代码中的「万能 if else」
实现方式
在每个需要调用 SDK 的地方,判断当前环境,然后执行对应的代码:
javascript
function pay(orderInfo) {
const env = getEnv(); // 返回 'app1' | 'app2' | 'mini' | 'web'
if (env === 'app1') {
window.App1JSBridge.invoke('pay', orderInfo, (res) => {
if (res.code === 0) console.log('支付成功');
});
} else if (env === 'app2') {
window.Native.call('PayOrder', JSON.stringify(orderInfo))
.then(res => console.log('支付成功'));
} else if (env === 'mini') {
wx.requestPayment({
timeStamp: orderInfo.timeStamp,
nonceStr: orderInfo.nonceStr,
package: orderInfo.package,
signType: 'MD5',
paySign: orderInfo.paySign,
success: () => console.log('支付成功')
});
} else {
// Web 端没有原生支付,可能跳转 H5 支付页面
window.location.href = `https://pay.example.com?order=${orderInfo.id}`;
}
}
优点
- 简单直观:新手也能立即上手,无需设计额外抽象。
- 快速实现:对于少量调用点,能迅速完成适配。
缺点
- 代码膨胀:每个需要适配的地方都要写一堆 if else,随着调用点增多,代码行数爆炸。
- 维护噩梦 :当新增一个端(比如 App3),你需要搜索整个项目,找到所有用到相关 API 的地方,逐个添加
else if。极易遗漏。 - 违反开闭原则:对修改是开放的,对扩展却是封闭的------每增加新端,必须修改已有业务代码。
- 可读性差:业务逻辑与适配逻辑高度耦合,阅读者需要同时理解业务和所有端的 API 细节。
- 测试困难:无法轻松模拟某个端的返回值,单元测试需要 mock 多个环境。
显然,if else 只适合极少数、极简单的场景。当项目发展到一定规模,必须寻找更优雅的方案。
三、方案二:适配器模式------将变化封装起来
适配器模式(Adapter Pattern)的核心思想是:定义一个统一接口,内部封装不同端的实现细节,对外提供一致的方法调用。 业务代码只需依赖这个接口,无需关心具体是哪个端。
3.1 定义统一接口
首先,根据业务需求定义一套"理想"的 API,例如支付功能统一为 pay(orderInfo) 方法,返回 Promise。
3.2 创建各端适配器
分别为 App1、App2、小程序等编写适配器,实现上述统一接口,内部调用各自的 SDK。
3.3 根据环境选择适配器
在应用启动时,通过环境识别函数,决定使用哪个适配器,并导出统一 API。
代码示例(简化版)
javascript
// adapters/index.js
import app1Adapter from './app1Adapter';
import app2Adapter from './app2Adapter';
import miniAdapter from './miniAdapter';
import webAdapter from './webAdapter';
const env = getEnv(); // 返回 'app1' | 'app2' | 'mini' | 'web'
let adapter;
switch (env) {
case 'app1':
adapter = app1Adapter;
break;
case 'app2':
adapter = app2Adapter;
break;
case 'mini':
adapter = miniAdapter;
break;
default:
adapter = webAdapter;
}
export const pay = adapter.pay;
export const share = adapter.share;
// ... 其他统一方法
各适配器实现:
javascript
// adapters/app1Adapter.js
export default {
pay(orderInfo) {
return new Promise((resolve, reject) => {
window.App1JSBridge.invoke('pay', orderInfo, (res) => {
res.code === 0 ? resolve(res) : reject(res);
});
});
},
share(shareData) {
// ... App1 分享实现
}
};
javascript
// adapters/miniAdapter.js
export default {
pay(orderInfo) {
return new Promise((resolve, reject) => {
wx.requestPayment({
...orderInfo,
success: resolve,
fail: reject
});
});
}
// ...
};
业务代码调用:
javascript
import { pay } from '@/adapters';
async function checkout() {
try {
await pay({ orderId: '123', amount: 100 });
showSuccess('支付成功');
} catch (err) {
showError('支付失败');
}
}
优点
- 业务代码统一 :所有调用处只有一行
pay(),无需任何 if else。 - 易于维护:新增端只需新建适配器,修改原有端的实现也只影响适配器文件,业务代码无感知。
- 可测试性:可以轻松 mock 适配器,进行单元测试。
- 符合开闭原则:对扩展开放(加新端只需加适配器),对修改封闭(业务代码不动)。
缺点
- 初期设计成本:需要抽象出统一接口,并考虑各端差异(比如参数格式转换、错误码归一化)。
- 可能引入间接层:如果适配器实现过于复杂,可能带来性能损耗(通常可忽略)。
3.4 适配器架构图

四、适配器模式就够了吗?还得考虑这些!
适配器模式解决了 API 调用的统一,但在实际项目中,我们还需要关注以下问题:
4.1 环境识别
getEnv() 如何实现?通常需要结合 UserAgent、全局变量、容器特性等综合判断。例如:
- 判断是否在微信小程序:
typeof wx !== 'undefined' && wx.requestPayment - 判断是否在 App1:
window.App1JSBridge是否存在 - 判断是否在 App2:
window.Native是否存在
注意识别顺序(有些容器可能同时满足多个条件),一般优先级高的先判断。
4.2 参数格式转换
不同端的参数格式差异可能很大,适配器内部需要做转换。例如 App2 需要 JSON 字符串,而统一接口接收的是对象,适配器里要 JSON.stringify。
4.3 返回值统一
各端成功/失败的回调形式不同,有的用回调,有的用 Promise,有的错误码不同。适配器应统一返回 Promise,并将错误标准化(如统一抛出特定错误码)。
4.4 能力降级
某些端可能不支持某个功能(比如 Web 端没有原生支付),适配器可以优雅降级:例如跳转 H5 支付页,或者抛出一个特殊错误,让业务方决定如何处理。
4.5 初始化与生命周期
有些 SDK 需要先初始化(如监听 ready 事件),适配器可能需要提供 init() 方法,并在内部管理状态。
4.6 按需加载
如果适配器体积较大,可以考虑使用动态 import(),只在特定环境加载对应适配器代码,减少主包体积。
五、更进一步:将适配器封装为 SDK
既然适配器层已经将多端差异隔离,为什么不把它打包成一个独立的 npm 包,让所有业务线直接安装使用呢?这样不仅避免了重复造轮子,还能统一维护和升级。
5.1 SDK 设计目标
- 零配置或极简配置:业务方安装后,直接引入方法即可使用。
- 自动环境识别:内部自动判断当前端,加载对应适配器。
- 统一 API :所有功能通过命名空间导出,如
sdk.pay、sdk.share。 - 类型支持:提供 TypeScript 定义,增强开发体验。
- 轻量:按功能拆分,支持 tree-shaking。
5.2 SDK 结构示例
text
markdown
multi-end-sdk/
├── src/
│ ├── adapters/ # 各端适配器实现
│ │ ├── app1.js
│ │ ├── app2.js
│ │ ├── mini.js
│ │ └── web.js
│ ├── env.js # 环境识别
│ ├── index.js # 统一导出
│ └── types/ # TypeScript 类型定义
├── package.json
└── README.md
index.js 核心逻辑:
javascript
import { detectEnv } from './env';
let adapter = null;
async function loadAdapter() {
if (adapter) return adapter;
const env = detectEnv();
// 动态加载对应适配器
switch (env) {
case 'app1':
adapter = await import('./adapters/app1.js');
break;
case 'app2':
adapter = await import('./adapters/app2.js');
break;
// ...
default:
adapter = await import('./adapters/web.js');
}
return adapter;
}
// 创建代理方法,确保每次调用前适配器已加载
export async function pay(params) {
const mod = await loadAdapter();
return mod.pay(params);
}
export async function share(params) {
const mod = await loadAdapter();
return mod.share(params);
}
5.3 业务方使用
bash
npm install @company/multi-end-sdk
javascript
import { pay } from '@company/multi-end-sdk';
pay({ orderId: '123' }).then(...);
5.4 优点
- 复用性:一次编写,多项目使用,统一升级。
- 解耦更彻底:业务代码完全不关心适配逻辑,只需依赖 SDK 的 API。
- 便于团队协作:由基础设施团队维护 SDK,业务团队专注业务。
- 版本管理:通过 npm 版本控制,可以平滑升级,降级回退。
六、最佳实践建议
- 环境识别要健壮:不仅靠 UA,还要探测特有对象,并处理边缘情况(如 iOS 与 Android 的差异)。
- 统一错误处理 :定义一套错误码,例如
E_PAY_FAILED、E_NOT_SUPPORTED,便于业务方统一处理。 - 提供同步与异步:有些方法可能同步返回,但建议统一使用 Promise 或 async/await,保持一致性。
- 编写单元测试:使用 Jest 等工具 mock 不同端的全局对象,测试适配器逻辑。
- 文档完善:详细说明每个 API 的参数、返回值、各端支持情况,以及降级策略。
- 考虑扩展性:未来可能出现新端,设计时要预留扩展点,比如通过插件机制注册新适配器。
七、总结
多端统一适配的核心思想是分离变化。从业务代码中抽离出环境差异,通过适配器模式封装变化,再进一步封装成 SDK,实现了从混乱到清晰、从耦合到解耦的演进。
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| if else | 简单直接 | 维护成本高、代码冗余 | 仅 1-2 个端,极少调用点 |
| 适配器模式 | 业务统一、易扩展 | 初期设计成本 | 多端并存,调用点多 |
| SDK 封装 | 复用、解耦、易维护 | 需要额外打包发布 | 中大型项目,多个业务复用 |
最终,我们得到的不仅是一套技术方案,更是一种工程思维:面向接口编程,而非面向实现编程。当你的 H5 需要跑在越来越多的端上时,这套方案将帮助你保持代码的优雅与可维护性。
希望本文能为你提供切实可行的多端适配思路。如果你有更好的实践,欢迎在评论区交流讨论!