代理的妙用:uni-app 小程序是怎样用 `Proxy` 和 `wrapper` 抹平平台差异的

张开发
2026/6/17 15:07:32 15 分钟阅读
代理的妙用:uni-app 小程序是怎样用 `Proxy` 和 `wrapper` 抹平平台差异的
前言大家好我是前端笨笨狗uni-app、varlet、nrm 等众多知名仓库的核心开发专注于分享前端技术和 AI实践知识本篇文章是uni-app小程序源码解读的第一期欢迎关注我的微信公众号前端笨笨狗很多人在使用uni-app时会很自然地写出uni.showToast()、uni.login()这样的代码但很少会继续追问一个问题为什么同样是uni.xxx到了微信能落到wx.xxx到了支付宝又能落到my.xxx这个问题背后藏着一个巧妙的设计思路。它既不是简单的字符串替换也不是把所有平台 API 硬编码映射一遍接下来我们慢慢揭秘。问题背景在 uni-app 小程序端开发者写的是uni.showToast({title:Hello})uni.login()但真正运行时微信里调用的是wx.xxx支付宝里调用的是my.xxx这一层统一核心就靠两样东西Proxy决定uni.xxx这次到底取哪个实现wrapper把不同平台之间的参数、方法名、返回值差异包起来对应源码入口在 packages/uni-mp-core/src/api/index.ts。Proxy统一入口分发先看核心代码exportfunctioninitUni(api,protocols,platform__GLOBAL__){constwrapperinitWrapper(protocols)constUniProxyHandlers{get(target,key){if(hasOwn(api,key)){returnpromisify(key,api[key])}if(hasOwn(baseApis,key)){returnpromisify(key,baseApis[key])}returnpromisify(key,wrapper(key,platform[key]))},}returnnewProxy({},UniProxyHandlers)}这段代码的意思很简单uni不是一个提前写死所有方法的对象而是一个“访问属性时再决定返回什么”的代理对象。比如你写uni.showToast这时会触发get(target, key)其中key就是showToast代理会临时判断这个方法应该从哪里来也就是说Proxy的作用不是执行 API而是做一次统一分发。为什么这里一定要用Proxy因为uni的目标不是“绑定某个平台”而是“对外提供稳定名字对内按平台切换实现”。如果不用Proxy那就只能提前把所有 API 全部挂到uni上或者每个平台都维护一套映射逻辑这样会很笨重。用了Proxy之后框架就可以做到开发者永远写uni.xxx真正访问到uni.xxx时再动态决定用谁同一个入口底层可以切到不同平台对象这正是代理最适合的场景入口统一底层实现可变。这段get其实分了三层源码里的分发顺序很清楚1. 先看apiif(hasOwn(api,key)){returnpromisify(key,api[key])}如果这个方法 uni 自己实现过就优先返回 uni 自己的实现。2. 再看baseApisif(hasOwn(baseApis,key)){returnpromisify(key,baseApis[key])}像事件总线、拦截器这类基础能力不一定来自小程序原生对象也统一挂在uni上。3. 最后兜底到平台对象returnpromisify(key,wrapper(key,platform[key]))这里的platform默认是__GLOBAL__可以理解成当前平台全局对象微信环境下接近wx支付宝环境下接近my所以Proxy最终做成了这件事uni.showToast-当前平台的 showToast uni.login-当前平台的 loginwrapper翻译平台差异如果只是returnplatform[key]表面上也能调用但问题是不同小程序平台并不完全一致常见差异有三种方法名不一致参数名不一致返回值结构不一致所以Proxy只能解决“分发给谁”解决不了“怎么适配差异”。这就是wrapper的职责。源码在 packages/uni-mp-core/src/api/wrapper.ts。wrapper在这里干了什么看最关键的一句constreturnValue__GLOBAL__[options.name||methodName].apply(__GLOBAL__,args)这里能看出两件事。第一wrapper允许改方法名。默认调用methodName如果协议里配置了options.name就改成另一个平台方法名第二wrapper会先处理参数再调用平台方法。前面的processArgs、后面的processReturnValue本质上都在做一件事把 uni 暴露给开发者的统一接口翻译成当前平台真正认识的接口。所以可以把wrapper理解成一个“翻译层”。举一个最容易理解的例子假设 uni 对外想统一成这样uni.showToast({title:保存成功})但某个平台底层不是title而是content。那Proxy只能做到uni.showToast-找到这个平台的方法真正把参数从{title:保存成功}改成{content:保存成功}这一步必须由wrapper做。也就是说Proxy负责“找到人”wrapper负责“翻译话”这两个配合起来开发者才能始终写统一的uni.xxx。简易实现下面写一个极简版只保留Proxy和wrapper两层核心思想。constwx{showToast(options){console.log(wx.showToast,options)},}functionwrapper(name,method){if(nameshowToast){returnfunction(options){constnewOptions{content:options.title,}returnmethod(newOptions)}}returnmethod}functioninitUni(platform){returnnewProxy({},{get(target,key){constmethodplatform[key]if(typeofmethod!function){returnundefined}returnwrapper(key,method)},})}constuniinitUni(wx)uni.showToast({title:保存成功,})执行时虽然外部写的是uni.showToast({title:保存成功})但实际到平台方法时已经被改成了wx.showToast({content:保存成功})总结不是“用了Proxy很高级”而是职责拆得很清楚Proxy只做入口分发wrapper只做平台适配这样设计的好处是uni对开发者始终保持统一平台差异被收口在框架内部后续要支持更多小程序平台时只需要补适配逻辑不需要改开发者写法微信交流群我有个 uni-app 微信交流群大家有想进群的可以扫码

更多文章