尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

uniapp原生插件开发全解析:从环境搭建到上架避坑

uniapp原生插件开发全解析:从环境搭建到上架避坑 写uniapp时间长了你会发现一个挺有意思的现象业务层的JS代码写得再顺到了系统底层能力面前大概率还是要低头。后台持续定位、息屏语音播报、自动截屏、自定义分享好友这类需求前端怎么调官方API都是隔靴搔痒最后绕了一圈还是得研究uniapp原生插件。原生插件说白了就是把你的Android代码Java/Kotlin或iOS代码Objective-C/Swift封装成uni-app能调用的模块前端通过uni.requireNativePlugin一行代码调用底层能力握在自己手里。这篇文章就把原生插件从开发到使用的完整链路拆开讲什么时候该碰原生代码、环境怎么搭、双端插件骨架怎么写、调试日志为什么不打、后台定位和息屏播报这类真实业务怎么落地最后再聊上架打包的坑。适合已经跑通uni-app基本开发、正准备做App端底层能力的团队也适合被插件市场各种黑盒折磨到想自研的技术人。1. 动手之前先认清原生插件的适用边界1.1 三类典型场景JS层真的扛不住写uni-app项目90%的功能靠JS和官方模块能搞定。可一旦涉及后台持续定位、息屏语音播报、系统级截屏、原生分享这类能力JS层和现有的plus API往往会给你两种结果要么在Android机型上七零八落要么在iOS直接静默失效。原因很简单uni-app的JS跑在解释层之上它调用系统能力必须经过封装好的桥而这个桥不可能覆盖全部原生API尤其是涉及系统服务、权限窗口、后台运行、音视频会话这类和系统深度绑定的东西。我见过一个真实案例某个出行类小团队需要App退到后台后每隔30秒上报一次经纬度他们先用plus.geolocation.watchPosition做了Android后台跑三四分钟就被系统杀掉iOS切后台后定位更新直接停了。后来在Android侧写了原生前台服务iOS侧启用后台定位模式问题才真正落地。这类问题不是调几个参数能解决的必须让原生代码接管。另外一类是息屏播报——让App锁屏之后继续朗读内容或播放语音。你以为前端只要setTimeout循环就行实际上iOS在锁屏后很快会挂起JS线程你必须用原生音频会话把App“钉”在活跃状态。自动截屏也一样Android需要申请媒体投影授权iOS干脆不开放后台全局截屏能力。这些边界只有了解原生机制之后才能判断能不能做。1.2 插件市场的现成方案为什么不能无脑买遇到原生能力需求第一反应肯定是去插件市场搜。现成插件确实多定位、截屏、推送、分享、语音都有。但自己做原生插件前要按这几点评估可维护性、体积、隐私合规、数据出口。现成插件最大的问题是改不动。定位插件带不带自有统计SDK截屏插件里是不是偷偷上传了图片推送插件依赖哪个厂商通道这些都是黑盒。尤其上架应用市场时审核方对权限和第三方SDK的审查越来越严插件引入的每个权限都要写清楚用途出了问题只能干瞪眼等作者更新。自研插件的成本也没想象中高。写一个最小可用的模块Android端一个Java类、iOS端一个类、注册一下配置前端就能调起来。这个骨架一旦打通后面加功能都是往里面填代码。我的建议是与核心业务强相关、涉及用户隐私的能力优先自研边缘功能、验证类功能可以先用现成插件顶一阵。1.3 原生插件不等于uni-app X也不作用于小程序端讨论前先把概念理顺本文说的原生插件是经典uni-app工程vue2/vue3里通过uni.requireNativePlugin调用的原生模块它只对App平台生效。有些团队听到“uni-app X”就以为新项目应该换过去其实uni-app X是一套基于独立渲染引擎和Dart语法的新技术栈它也有自己的原生插件方案但生态和写法还没完全对齐老项目不建议轻易迁移。另一个常见误解是想在小程序端复用原生插件的代码。小程序运行在微信容器里根本没有原生模块注入通道你写的那套requireNativePlugin在小程序端会直接报错。平时开发一定要用条件编译把原生插件调用包起来否则一个不小心就把原生相关代码打进小程序包里。这其实也是“小程序包超过2MB”这类报错的常见来源之一后面第6章我会专门讲。2. 环境搭建最容易走错的一步云端和离线的插件开发路线2.1 先分清两条路云打包和离线打包决定你的调试方式开发原生插件之前必须先搞清楚一个事实uni-app项目默认跑的是标准基座里面只有官方封装好的模块你在项目目录里塞的原生代码标准基座根本不认。想要让自写插件生效要么自己做自定义基座要么走离线打包想靠HBuilderX云打包一键搞定是行不通的。这里有两套打包路线云端打包不需要本地安装Android Studio或Xcode但原生插件只能通过插件市场购买或提交公有/私有插件的方式接入本地AAR或私有Pod库很不灵活离线打包则把整个原生工程拉到你电脑上原生代码改完直接编译调试链路最短。我强烈建议只要机器条件允许原生插件开发一律走离线打包路线。读完官方离线SDK的文档你会发现Android端要下载对应版本的离线SDK解压后用Android Studio打开里面的App工程iOS端则是一个Xcode工程依赖CocoaPods管理。两个工程都和你的uni-app项目版本存在严格对应关系版本对不上会出现各种奇怪问题尤其iOS的UniPlugin版本错了就直接编译失败。2.2 Android侧工程结构从UniPlugin-Hello模板开始下载Android离线SDK后你会看到一个标准App壳工程。官方推荐的入门模板是UniPlugin-Hello里面已经预置了模块注册、原生调试的完整示例。你要做的不是从零建工程而是把里面的Example模块改成自己的业务模块。安卓侧原生插件最核心的目录是assets/dcloud_uniplugins.json这个文件告诉uni-app运行时你有哪些模块、每个模块对应的类名是什么。以我常用的结构为例工程里新增一个com.xxx.mylocation包下面放自定义Service和Module类然后在build.gradle里引入相关依赖。这里有个容易踩的坑离线SDK的buildTools版本、compileSdk版本不要随手改保持DCloud官方版本否则会出现莫名其妙的资源合并冲突。AndroidStudio的JDK版本建议跟着SDK文档走用太新的JDK编译老工程会报一堆“source/target 1.8”之类的错误看着像代码问题其实是环境不匹配。2.3 iOS侧工程要点Xcode、CocoaPods与插件代理iOS侧离线SDK麻烦点在于CocoaPods。打开官方工程后第一步是pod install过程中经常卡在第三方源下载上公司网络慢的时候能让人崩溃。我的建议是先把Podfile里的source镜像换成国内可访问的仓库再执行安装。iOS原生插件除了实现功能模块还有一个UniPluginProtocol生命周期协议可以处理App启动事件比如把定位Manager初始化、推送SDK注册等动作放在原生启动阶段完成。这个协议不是必须实现的但如果你的插件有全局初始化需求它就派上用场了。注意iOS的工程配置里Bitcode一定要关掉否则上架和打包都会遇到链接报错这是每年都有新同学掉进去的坑。工程验证的标准动作先不改任何代码直接用官方工程打包一个空App跑起来能看到uni-app的启动画面再开始往里写插件。如果这一步都跑不通说明环境问题还没解决先和环境死磕别急着写代码。3. 从零敲一个原生模块Android与iOS双端骨架源码3.1 Android端UniModule注解驱动的JS桥Android上写一个能被uni-app前端调用的模块核心是继承UniModule类然后用注解标记可调用方法。下面这个例子我在项目里反复用你完全可以抄走改成自己的逻辑package com.example.myplugin; import com.alibaba.fastjson.JSONObject; import io.dcloud.feature.uniapp.annotation.UniJSMethod; import io.dcloud.feature.uniapp.bridge.UniJSCallback; import io.dcloud.feature.uniapp.common.UniModule; public class CustomModule extends UniModule { // uiThreadtrue 表示方法在UI线程执行适合操作界面的逻辑 UniJSMethod(uiThread true) public void sayHello(String name, UniJSCallback callback) { JSONObject data new JSONObject(); data.put(message, hello name); data.put(code, 0); if (callback ! null) { callback.invoke(data); } } // uiThreadfalse 时方法在子线程执行适合耗时任务 UniJSMethod(uiThread false) public void doHeavyTask(JSONObject options, UniJSCallback callback) { // 这里做耗时操作最后通过callback回调 if (callback ! null) { callback.invokeAndKeepAlive(/* 连续回调时使用 */); } } }这里有几个细节值得展开说。UniJSMethod(uiThread true)决定方法跑在哪个线程需要弹窗、获取当前Activity、操作UI的必须写true做网络请求、文件读写、复杂计算的写false否则会卡主线程。回调参数UniJSCallback是异步结果的通道invoke调用一次就结束想要连续上报进度比如下载百分比用invokeAndKeepAlive前端能收到多次回调。参数传递方面前端传过来的对象在Android侧是JSONObject数组是JSONArray标量就直接是String/Integer。不要试图传自定义Java对象桥的序列化能力只支持JSON体系。3.2 iOS端模块继承WXModule用宏导出方法iOS端的写法看着更“Weex”因为uni-app的iOS运行时本身就建立在Weex的桥接机制上。一个最小模块类长这样#import WeexSDK/WXModule.h #import WeexSDK/WeexSDK.h interface CustomModule : WXModule end implementation CustomModule WX_EXPORT_METHOD(selector(sayHello:callback:)) - (void)sayHello:(NSString *)name callback:(WXModuleCallback)callback { if (callback) { callback({ message: [NSString stringWithFormat:hello %, name], code: 0 }); } } - (void)dealloc { // 清理资源 } end和其他纯Weex工程不同uni-app的iOS插件还需要在Podfile和主工程注册表里声明模块否则运行时根本不会把你的类加载进来。常见做法是在工程的info.plist里维护uni插件数组或者在Podfile里做额外声明这个千万不能忘很多人写完代码调了半天结果就是少了这步注册。iOS回调线程也要说一句WXModuleCallback默认在子线程回调如果前端想在回调里直接操作DOM和页面变量最好自己在原生侧切到主线程再回调也就是包一层dispatch_get_main_queue避免踩线程问题的坑。这个坑在定位、录音这种频繁回调的插件里经常出现。3.3 前端调用uni.requireNativePlugin的正确姿势写好原生模块前端调用非常简单三步搞定// 1. 获取模块名称必须和原生注册名完全一致 const customModule uni.requireNativePlugin(CustomModule) // 2. 调用方法参数以对象形式传入执行完走回调 customModule.sayHello(张三, (res) { console.log(插件返回, JSON.stringify(res)) }) // 3. 如果原生方法带返回值也可以直接拿到同步结果调用时三个高频翻车点提前告诉你模块名大小写和注册配置不一致直接报模块不存在参数数量不匹配原生侧拿到的字段是undefined回调不执行先怀疑原生方法有没有被调用再怀疑线程问题不要一上来就改前端。还有一点uni-app的vue3和vue2在底层桥上兼容前端API都是uni.requireNativePlugin不用区分版本。不过如果你在微信小程序端也打包这份代码就必须用条件编译#ifdef APP-PLUS包起来否则小程序编译阶段直接报错。3.4 注册配置dcloud_uniplugins.json和插件的声明写完双端代码还要做注册。Android离线包的注册文件在app/src/main/assets/dcloud_uniplugins.json格式如下{ modules: [ { name: CustomModule, class: com.example.myplugin.CustomModule } ] }iOS侧则是在Xcode工程的资源配置里登记插件名与类的映射。如果你的插件需要前端一次性注册成“组件”或“页面”配置方式又不一样但绝大多数业务模块只需要上面的module配置。这里顺便提一下项目内nativeplugins目录的作用HBuilderX工程根目录下放一个nativeplugins文件夹里面按插件名建子目录放好package.json和对应双端文件HBuilderX打包时就能识别。这个目录主要给云打包和自定义基座用离线打包时则要在原生工程里手动合入两条路二选一即可混着用容易乱。4. 调试阶段的高频翻车现场日志不打印、插件找不到、基座不对4.1 自定义基座是调试的入场券新手调原生插件时第一大错觉是“我改完代码运行一下就生效了”。在uni-app里前端JS可以热更新原生代码却不行。你运行到手机时如果选的是标准基座那里面根本没有你写的插件类前端再怎么调都只会得到module not found。正确做法是先在HBuilderX里“运行→运行到手机或模拟器→制作自定义调试基座”把原生插件编译进基座里然后在运行设备时选择“自定义调试基座”。每次改了原生代码都要重新制作一次。虽然过程繁琐但它能让你脱离签名约束快速在真机上验证是原生插件开发绕不开的工作流。我见过最惨的情况是某同事改了Android插件代码跑的还是标准基座前端却一直报错他以为是缓存问题清了半天缓存最后才想起来没做自定义基座。浪费时间不说人也被磨得没脾气。所以我把这句话放在最前面先确认自己跑的是不是自定义基座再开始排查问题。4.2 原生日志与console.log为什么对不上很多人在搜“uniapp不打印日志信息”我在给团队做技术支持时也经常碰到。先说结论前端console.log在真机上的输出会进Logcattag是uni-app原生侧Log.i的输出则要用你自己写的tag比如unplugin两者不在同一个tag下你按默认过滤当然看不到。我常用的命令是adb logcat -s uni-app:V uniplugin:V这样能同时看到前端和原生日志。更直接的办法是在原生代码里加一个tag为unplugin的Log开关排查问题时用adb logcat -s unplugin:V单独过滤。iOS端则是在Xcode控制台看原生NSLog前端console.log会以类似标识出现。还有一个常见误判release包默认不输出低等级日志你打了一个正式包在真机上测怎么打日志都不出这不是插件问题而是发布配置把日志截断了。调试期尽量用debug包或自定义基座。4.3 三个高频报错的定位链路我把平时遇到最多的三个报错完整梳理一下。第一个是“Module not found”或“undefined is not a function”。排查顺序是确认前端uni.requireNativePlugin里写的名字和注册文件里的name完全一致确认当前运行的包是自定义基座而不是标准基座如果是Android离线工程检查dcloud_uniplugins.json有没有被正确打进assets如果这些都没问题杀进程重装一次避免旧包缓存干扰。第二个是前端“调用方法不回调”。这种情况大概率不是模块没找到而是方法内部抛了异常或者回调被放在子线程且前端有UI操作。可以先用一个只回调固定字符串的测试方法验证桥的通路再把真正的业务逻辑一点点加进去用二分法定位。第三个是iOS编译链接错误比如“Undefined symbols”。原因通常是Xcode工程里少了WeexSDK依赖或者注册的类名没有出现在Podfile声明的target中。处理方法全量pod install后重启Xcode检查Podfile里插件target是否真的被include了。记住Xcode工程历史遗留的引用文件也会导致链接错误必要时候删掉DerivedData重编一次。5. 两个真实业务插件改造后台定位与息屏播报5.1 后台持续定位watchPosition 原生Service的双保险后台定位是原生插件使用频率最高的业务场景之一。前端侧很多人用plus.geolocation.watchPosition持续监听这个API在App前台表现不错但App一旦切到后台Android可能为了省电直接把进程挂掉iOS也会停止回调。我推荐的方案是双保险前端继续用plus.geolocation.watchPosition或uni.onLocationChange做前台高频更新同时由原生插件启动一个前台服务Foreground Service维持定位能力。Android前台服务至少要有一个常驻通知并在startForeground时声明定位类型。简化逻辑如下public class LocationService extends Service { Override public int onStartCommand(Intent intent, int flags, int startId) { Notification notification buildNotification(); // 前台通知 startForeground(1, notification); // 这里启动定位Manager并定时上报 return START_STICKY; } }这里必须同步更新AndroidManifest和权限配置FOREGROUND_SERVICE、FOREGROUND_SERVICE_LOCATION、ACCESS_FINE_LOCATION一个不能少Android 13及以上还需要POST_NOTIFICATIONS运行时权限。iOS侧则要在Info.plist里声明NSLocationAlwaysUsageDescription并开启后台定位模式UIBackgroundModes: location这样调用CLLocationManager的持续更新才能在锁屏后继续触发。前端要注意一个坑不要同时开多个定位监听watchPosition、uni.startLocation、原生定位各开一路不仅耗电翻倍还容易出现坐标串台。定位源统一走到原生前端只负责展示。上架时这部分是重灾区。应用市场会优先盯“后台定位”这类权限如果你的App业务场景和定位没有强关联申请权限被拒会很常见。做法是权限动态申请只在进入对应功能页面时弹出隐私政策里明确写清定位用途、数据是否上报。这些在插件开发阶段就要想好别等审核打回来再补。5.2 iOS息屏播报本质是后台音频会话“iOS息屏播报”这个需求经常被提给原生插件开发但你得先理解iOS的运行机制系统不允许App在锁屏后随便跑线程除非App声明了后台音频audio模式且当前确实在播放音频。所以息屏播报的底层实现不是“定时器持续运行”而是“让App变成音频播放中的状态”。原生插件里需要这样处理把AVAudioSession设置为AVAudioSessionCategoryPlayback并激活会话然后在UIBackgroundModes里加上audio。一个可用的代码片段是AVAudioSession *session [AVAudioSession sharedInstance]; [session setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:nil]; [session setActive:YES error:nil];设置完之后前端播放任意音频包括静音音频时App就处于“后台播放”状态JS线程不会被立刻挂起播报队列才能继续跑。要注意MixWithOthers的选择要谨慎不加会抢占其他应用的音频焦点加了之后后台播报可能被别的App声音盖住。具体业务自己测试权衡。App Store审核对“后台音频”模式盯得很紧如果你的App没有实际音频播放内容只是借这个模式保活被拒概率极高。所以息屏播报功能一定要和真实的语音播报、朗读业务绑定尽量让审核员在App里能看到明确的音频使用场景。5.3 自动截屏的边界Android可以iOS别硬来自动截屏经常被产品经理描述成“一句话需求”实际操作起来是个小工程。Android上你需要通过MediaProjectionManager发起一个系统授权弹窗用户点同意后拿到MediaProjection实例再创建VirtualDisplay配合ImageReader把屏幕内容转为Bitmap。授权动作必须在Activity里触发原生插件里拿到当前上下文后要把它转成Activity上下文才能启动授权流程。这个流程本身不难难点在内存。全屏分辨率下的截图数据非常大频繁截屏会导致ImageReader缓冲区溢出或OOM我的建议是控制截图频率并及时释放VirtualDisplay和ImageReader资源。真机上跑长时间压测是必须的别只在模拟器上点两下就交付。iOS端希望你能直接给产品说清楚系统并不开放第三方App获取其他应用画面的能力你最多只能截取自己App的前台界面后台全局截屏是做不到的。很多人以为原生插件万能其实这里就是平台规则的硬边界。如果业务必须全局截屏那就只能研究企业签名设备管理那套私有方案普通开发者不要碰。6. 插件落地上架的最后一公里打包、热更新与市场审核6.1 云打包、离线打包与热更新的边界关系插件写完总要打成正式包。这又回到第2章的路线选择。离线打包时你把原生插件工程和uni-app编译的JS资源合并到一起签名、混淆、渠道包都由自己控制灵活性最高。云打包则简单省事但原生插件必须符合DCloud的接入规则私有库和特殊依赖常常会卡壳。这里一定要给团队讲清楚热更新的边界uni-app的wgt热更新只能更新前端JS和静态资源原生插件代码一旦改动必须重新制作基座、重新打包整包wgt包救不了你。如果产品经理习惯了前端那种“改完即刻上线”的节奏遇到原生插件改版沟通成本会非常高。比较好的实践是尽量把频繁变化的逻辑留在JS层把原生模块做成稳定接口这样大部分需求热更就能覆盖。还有别忘记版本号对齐。离线SDK版本、uni-app编译器版本、原生插件编译用的SDK版本三者在正式发版时必须一致否则会出现部分用户升级后插件闪退的诡异问题。这个我写进发布检查清单里每次发版都要核对一遍。6.2 小程序包的2MB限制为什么原生插件会“连累”小程序很多项目是同一套uni-app代码多端编译这时候原生插件代码如果不做条件编译会和小程序产生冲突。小程序有经典的2MB包体积上限报错一般是source size 2612kb exceed max limit 2mb。打开包分析一看里面混进了一大堆原生插件的JS封装代码和资源既不能让小程序用又白白占体积。解决方案核心就是把所有uni.requireNativePlugin相关调用都包进#ifdef APP-PLUS里小程序端根本不执行这段代码。小程序端的类似能力用微信自己的API或云函数替代。此外分包加载能有效降低主包体积图片和字体资源尽量外链。这条对纯App开发者同样有参考价值即使你不发小程序条件编译的习惯也值得养成因为多端是uni-app的底层卖点别让原生插件限制住其他端的发布范围。6.3 安卓应用市场上架前必查的几项最后聊上架。原生插件场景下市场审核翻车点集中在权限声明和后台行为。第一把AndroidManifest和Info.plist里所有权限列一个清单逐条对照业务写清楚用途第二后台定位、后台音频这类敏感权限必须在App内能看到对应的功能入口和说明文案不能只在代码里声明第三targetSdkVersion尽量跟随最新要求某些老版本在市场新规下会被直接拒审。签名一致性也要单独强调很多团队测试包用一个签名正式包换另一个签名老用户升级时就报“签名不一致无法覆盖安装”。原生插件本身不会改变签名行为但它会让整包体积和安装流程更复杂排查成本更高所以发布前一定要固化签名配置。还有一个容易被忽略的点是插件里如果带了第三方统计或者崩溃上报SDK市场会要求填写第三方SDK目录清单。这个在自研插件时经常被跳过最后审核邮件一封接一封。建议开发阶段就把插件涉及的所有第三方库记录下来字段包括库名、版本、用途、数据收集范围上架时直接复制填写。说实话原生插件开发真正烧时间的不是写Java和Objective-C而是打通“双端工程配置、自定义基座、桥接调试、市场合规”这一整套链路。我自己最大的体会是第一次严格按上面的流程走通一遍之后再做第二个插件速度起码提升三倍。遇到报错不要慌先确认环境版本、基座类型、日志输入这三件事问题基本就浮出水面了。
返回列表