一、模式理解(用快递驿站比喻)
想象你网购了5件商品,分别来自不同快递公司。
外观模式就像小区门口的快递驿站,你不需要知道中通怎么分拣、顺丰怎么运输,只要到驿站报取件码就能拿到所有包裹。
在前端开发中,这种模式通过统一入口简化复杂子系统调用。
二、代码实现示例
- 浏览器事件处理(兼容性封装)
// 事件处理外观
const EventHandler = {
// 缓存能力检测结果
_hasAddEventListener: 'addEventListener' in window,
addEvent(element, type, handler) {
if (this._hasAddEventListener) {
element.addEventListener(type, handler, false)
} else if (element.attachEvent) { // IE8及以下
element.attachEvent(`on${type}`, handler)
} else {
element[`on${type}`] = handler
}
},
removeEvent(element, type, handler) {
if (this._hasAddEventListener) {
element.removeEventListener(type, handler, false)
} else if (element.detachEvent) {
element.detachEvent(`on${type}`, handler)
} else {
element[`on${type}`] = null
}
}
}
// 使用示例
const btn = document.querySelector('#myBtn')
EventHandler.addEvent(btn, 'click', () => {
console.log('按钮被点击')
})
- API请求封装(统一错误处理)
class ApiFacade {
constructor(baseURL) {
this.baseURL = baseURL
}
async _request(method, endpoint, data) {
try {
const response = await fetch(`${this.baseURL}${endpoint}`, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
if (!response.ok) throw new Error(`HTTP错误 ${response.status}`)
return response.json()
} catch (error) {
console.error('请求失败:', error)
// 统一错误处理(可扩展为通知用户)
throw error
}
}
get(endpoint) {
return this._request('GET', endpoint)
}
post(endpoint, data) {
return this._request('POST', endpoint, data)
}
}
// 使用示例
const api = new ApiFacade('https://api.example.com')
// 业务代码只需调用简洁接口
api.get('/users')
.then(data => console.log(data))
.catch(() => console.log('显示友好错误提示'))
三、实战场景建议
- 适用场景
- 浏览器兼容处理(事件/样式/API差异)
- 复杂组件库入口(如封装数据表格的复杂配置)
- 第三方SDK整合(统一支付接口对接微信/支付宝)
- 微前端应用通信(通过外观隔离子应用细节)
- 使用技巧
// 动画控制示例(封装requestAnimationFrame)
class AnimationManager {
constructor() {
this.animations = new Map()
this._animate = this._animate.bind(this)
}
add(key, callback) {
this.animations.set(key, callback)
if (this.animations.size === 1) {
requestAnimationFrame(this._animate)
}
}
_animate(timestamp) {
this.animations.forEach(cb => cb(timestamp))
if (this.animations.size > 0) {
requestAnimationFrame(this._animate)
}
}
remove(key) {
this.animations.delete(key)
}
}
// 使用:多个组件共享动画循环
const animator = new AnimationManager()
animator.add('widget1', (t) => { /* 组件1动画 */ })
animator.add('widget2', (t) => { /* 组件2动画 */ })
四、注意事项与坑点
- 避免过度封装(反面示例)
// 错误示范:外观层变成上帝类
class BadFacade {
constructor() {
this.http = new HttpService()
this.cache = new CacheManager()
this.analytics = new Analytics()
// 继续添加更多依赖...
}
// 混杂不相关的功能
getUser(id) {
this.analytics.track('USER_REQUEST')
return this.cache.get(`user_${id}`) || this.http.get(`/users/${id}`)
}
// 后续添加越来越多无关方法...
}
- 合理设计原则
- 单一职责:每个外观类只封装一个子系统(如DOM操作、网络请求、状态管理)
- 接口最小化:保持方法数量在7±2范围内(认知负荷理论)
- 透明访问:允许高级用户绕过外观直接访问子系统
// 好的设计示例
class DOMFacade {
static createElement(tag, options = {}) {
const el = document.createElement(tag)
if (options.classes) el.className = options.classes.join(' ')
if (options.attrs) {
Object.entries(options.attrs).forEach(([k, v]) => el.setAttribute(k, v))
}
return el
}
// 暴露原生方法供高级使用
static native = {
createElement: tag => document.createElement(tag),
// 其他原生方法...
}
}
五、性能优化技巧
- 缓存检测结果
// 样式前缀检测优化
class CssPrefixDetector {
constructor() {
this._prefix = this._detectPrefix()
}
_detectPrefix() {
const styles = window.getComputedStyle(document.documentElement)
const prefixes = ['Webkit', 'Moz', 'ms']
for (let prefix of prefixes) {
if (styles[`${prefix}Transition`] !== undefined) {
return prefix.toLowerCase()
}
}
return ''
}
getPrefix() {
return this._prefix
}
}
六、测试策略建议
- 测试重点:
- 外观接口的输入输出正确性
- 异常路径处理(如网络错误、无效参数)
- 浏览器兼容性测试矩阵
- 测试示例(Jest):
describe('ApiFacade', () => {
let api
beforeAll(() => {
api = new ApiFacade('https://api.example.com')
// Mock网络请求
})
test('GET请求应正确拼接URL', async () => {
await api.get('/users')
expect(fetch).toHaveBeenCalledWith(
'https://api.example.com/users',
expect.anything()
)
})
test('POST请求应包含JSON头', async () => {
await api.post('/login', { user: 'test' })
expect(fetch.mock.calls[0][1].headers)
.toEqual({ 'Content-Type': 'application/json' })
})
})
外观模式在前端工程中如同"操作手册",核心是平衡易用性与灵活性。
建议在子系统存在3个以上相关接口时考虑引入,同时注意通过TS类型定义或JSDoc明确接口契约。
当发现外观类频繁修改时,可能需要重新评估封装粒度。