2022年05月16日

Uniapp开发过程中的那些坑

作者 非鱼

新项目的移动端决定使用Uniapp来开发,最主要的是它能自动适配H5网页,微信小程序,安卓和iOS App多端,只要写一次代码,能够直接发布到所有需要的地方,虽然有些页面需要按平台做一定的适配,但是至少所有的底层逻辑和大的布局都不需要重复工作,以后升级维护起来也容易很多。而且它打包出来的各端页面路径是一致的,所以我们在需要跳转到一些指定页面的时候,各端可以使用同一个跳转地址,包括参数都可以正确处理,

而且做到最后要发布的时候,发现它的云打包功能实在是太好用了,不用自己去配置安卓和IOS的开发环境,证书,项目框架,各种所需要的插件等等等等,这种感觉,就像搬了三十年砖的人第一次见到自动输送线和堆垛机。

但是,不得不说,因为官方文档和示例方面的问题,还是有不少坑,会让你在一个个小问题上浪费非常非常多的时间。仅以此文章纪念我失去的三十个小时。

一、微信支付问题

Uniapp对各种支付通道做了二次封装,前端里面你只需要调用一个uni.requestPayment方法,把不同场景下所需要的参数传给它,它会自动处理各种场景下的支付,比如H5下面的跳转支付宝或弹出微信支付,小程序或原生App下面的直接弹到支付宝或微信的支付界面等等。而且在打包原生App的时候,打包框架里已经内置了所有你需要的组件的lib,你只要配置自己的appid等参数,其它什么都不需要操心。这简直比优秀更优秀了。

我们的支付后端使用了Ping++的服务,所以服务器端接口没有任何需要费心的地方,调用Ping++接口创建微信和支付宝订单,只要传一个通道参数,剩下的Ping++全部帮你搞定,并自动返回前端所需要的支付参数,包括各种加密签名算法。因为二维码支付的情况下返回的就是一个URL,前端自己生成二维码就可以,这一步不会出任何问题。但是需要直接弹出到微信和支付宝App的支付界面的地方就会复杂一些。按照官方文档,后端返回的参数内容跟前端需要的是一样的,直接原样传给支付的前端方法就可以。结果试验下来,小程序完全没问题,后来在App上测试的时候,支付宝也完全没问题,但是,微信死活没反应,点击以后无法调起微信,也没有任何报错信息,iOS和安卓都是。而同样的参数传给我原来的App(里面用Ping++的前端组件跳转支付)就完全没有问题。所以基本排除了参数原因。

折腾了很久,换用各种云打包和本地打包,甚至中间放弃了好几天,都不想继续搞了。最后在重新查找解决方案的时候,终于在一篇不起眼的文章里看到了一句提示,后端返回的参数是不能直接使用的,微信要求的参数是全部小写,于是修改了之前把接收到的后端参数直接JSON.parse出来传给uni.requestPayment的orderInfo的逻辑,改成new一个新对象,把每个变量值重新赋值一下,然后就解决了。

let charge = JSON.parse(res.charge);
let wx = charge.credential.wx;
uni.requestPayment({
    provider: 'wxpay',
    orderInfo:  {  
        sign: wx.sign,  
        prepayid: wx.prepayId,  
        partnerid: wx.partnerId,  
        appid: wx.appId,  
        package: wx.packageValue,  
        timestamp: wx.timeStamp,  
        noncestr: wx.nonceStr  
    },
})

其中的packageValue曾经一度引起过注意,还在parse之前把它替换成package,依然不管用。

不过这个只解决了第一步,iOS做到这样就完整了,但是安卓上调起微信以后会报-1错误,这个错误包含了所有可能的错误原因。不过只要iOS成功了,说明参数没问题,剩下的就是包签名问题。安卓App想要使用微信支付,需要在腾讯的开放平台上注册应用,并且需要输入应用的签名。这次打包使用uniapp的线上生成安卓签名证书,改变了之前的签名文件,需要去开放平台上替换这个签名,实际就是打包使用的keystore签名文件的MD5值(可以在uniapp管理平台上查看证书的签名,里面的MD5部分),并删除里面的冒号。而且修改以后还要等待开放平台的审核,审核通过以后新签名才会生效,微信会自动获取到新信息,然后支付才能用。

不过不说,微信支付对开发者的友好性,比支付宝差了100倍。而且,腾讯自己的开放平台已经多到自己都搞不清了,open.qq.com, open.tencent.com , open.weixin.qq.com……有些平台的功能腾讯自己也不打算维护了。所有涉及到App能不能使用微信支付,微信分享的权限,都在open.weixin.qq.com里的移动应用里,App调用微信分享的时候想分享成小程序卡片的话,也要在这里面把App和小程序添加到同一个账号里就算完成了绑定。

二、打包以后安卓App的Tabbar上图片不显示

开发同学按照文档上来的,而且H5网页,小程序,iOS App都很正常,就是安卓上不显示,搜索以后各种修改图片路径,static,/static,./static等等都试了一遍,不管用。官方建议图标用81像素的,于是把图片的各种尺寸又试了一遍,没有任何效果。就差官方说的替换开发工具版本和打包版本了,因为已经用的是最新版了,也没有什么两边版本不一致的情况。

最后的最后,发现tabbar配置里面的iconfont参数是不可少的,加上了这个参数,安卓的图标就正常了。而另外三个平台都不依赖这个参数。

三、web-view的问题

发送短信验证码的地方对接了腾讯的云验证码TCaptcha用来做人机验证,在H5和小程序上都完全没有问题,因为官方提供了这两种插件,用条件编译在H5和小程序的平台上调用不同的插件就可以了。

但是到了App上,真是要了老命了。腾讯并没有提供App方面的原生插件,在官方文档里要求App上使用网页方式来调用这个验证码。在原生应用里面,需要加一个系统的webview组件来调用一个自己写的网页,并且自己加代码来实现原生代码和网页间的JS的数据交互。但是uniapp提供了一个web-view组件,可以在vue页面里嵌入,并让它加载一个URL,本地的或者远程的都可以,但是没办法自己去实现交互问题。按照uniapp的官方文档,他们已经提供了这个交互方式,于是,非常开心的按照官方文档做了一个本地HTML页面,在app里新建一个页面,把webview嵌入进去,结果,在网页里面对app进行postMessage,外面死活收不到。不管是把HTML放在本地static目录还是在远程线上用网络地址访问,也不管HTML里面引用的那个uni.webview.js是在本地的还是远程的,也不管是文档里的0.x的版本还是别的地方看到的1.5.2还是1.5.3的,都不管用。在网页里面能看到uni对象,也能调用uni.postMessage,外面就是收不到事件。然后在网页里调用uni.NavigateBack,也无法退回到调用这个嵌入页面的上一个页面,只会导致这个HTML页面本身刷新一次。不过delta设成多少。这个uni.NavigateBack方法必须在嵌入webview组件的这个页面上调用才行,而不能在被嵌入的这个HTML网页里调用。但是外面的页面什么时候能调用这个方法,又取决于需要收到它已经验证完成的通知。

又是陷入了迷茫的一天。最后甚至想出了两个解决方案。一个方案是不自动返回,用户输入了手机号点击获取验证码的时候,push进这个webview的页面,把手机号传进去,然后在这个页面里完成人机交互验证,并直接调用发送短信的接口,然后显示一个提示文字让用户手动去点击返回上一页,这样就会回到输入验证码的界面上。但是用户每次验证完都要手动返回。另一个方案是需要验证的页面push进入了这个验证页面以后,立即开始计时,6秒左右自动返回,不管用户有没有验证完成或者发送短信完成。按照我自己操作的时间来看,6秒差不多刚刚好,既能验证成功,又不会在验证完成后空等待太久。但是如果用户网络不好,可能就没来得及验证页面就自动返回了,或者验证了没来得及发短信就返回了。

在这个问题上花了太多的时间,而且网络上各种postMessage无法收到消息的提问都没有答案。最后的最好,又是在一个不起眼的角落,看到了另一个解决方案,就是在嵌入的HTML页面里修改页面标题,然后嵌入webview组件的页面可以获取到这个webview,并监听页面title变化的事件,当标题发生变化的时候就可以调用uni.NavigateBack回到上一页了。

在被嵌入的HTML页面里面,人机验证完成,并且发送短信完成以后,直接修改页面标题:

document.title = '发送成功';

在嵌入webview的这个uniapp页面里监听子页面标题的变化:

onLoad(option) {
    setTimeout(this.checkTitleUpdate, 500)
},
methods: {
    checkTitleUpdate(){
         const currentWebview = this.$scope.$getAppWebview()
         var web = currentWebview.children()[0]
         web.addEventListener('titleUpdate',({title}) => {
             if (title === '发送成功') { 
                 uni.navigateBack()
             }
         },false)
    }
}

用这个方案来取代postMessage的方法,而且内嵌的HTML也不需要再引用那个官方都不知道应该用什么版本的uni.webview.js了,非常简单可行。

四、markdown内容展示问题

新闻模块在后台使用了markdown编辑器,一方面更安全,另外相对HTML对各种不同平台的展示会更友好一些。但是uniapp如何显示markdown内容反而成了一个问题。最终找到了一个解决方案:towxml组件。这个组件可以解析markdown,而且对语法的支持非常丰富,还支持数学公式和表情、代码高亮等等,虽然我们在新闻里用不到。

但是towxml的官方版本(https://github.com/sbfkcel/towxml)是针对小程序,需要自己在本地编译一下,编译后得到的是小程序的wxml/wxss/js文件,uniapp无法直接作为公用组件来使用。虽然官方文档里有一篇如何在uniapp里面使用towxml,但我测试下来这个方案无法正常使用。另外有人做了一个uniapp的版本(https://github.com/zhongbr/towxml-uniapp),虽然项目里写的是支持App、H5和微信小程序平台,但是我把它放进项目里编译成微信小程序以后,小程序里面会报错,里面的有些语句没有成功翻译成小程序的代码,但是在H5和App里倒是可用。

多次尝试以后放弃了一套代码解决问题的想法,最终把两套方案全部打包进去,官方版本直接放进static目录,不转译,组件版本放进组件目录里,使用条件编译在小程序和非小程序里用不同的引用方式,小程序引用static目录下的版本,其它平台引用组件版本。测试下来功能一切正常,只是导致小程序打包的体积超过了2M,而且我也没找到怎么让uniapp在打包成小程序的时候跳过个组件不编译。如果每次编译完再去手动删除小程序的代码目录里面的组件下面的这套东西,平时发布的时候就太麻烦了。于是进到原来的towxml目录里删除了一堆暂时用不到的组件,比如表情和高亮,基本够了。

但是有个不解的新问题,小程序在正式打包上传的时候,会提示static下面的towxml目录下的各个js文件没有被引用,所以没有自动上传,但实际上在页面里是通过require的方式引用的这里面的js文件,并且这些js是层层嵌套调用的,如果不上传,理论上markdown解析不了也显示不了,然而实际上用下来发现,虽然它提示这些文件没上传,但是线上版本的这个功能却没有受影响,原因未知,暂时只能理解为开发工具的Bug,文件实际被打包进去了。

另外这个东西解析出来的内容在各种平台上的样式有些差异,每个平台都需要通过条件编译加一些CSS来处理样式,而且在App平台上无论怎么做都改变不了图片的宽度到100%,网页和小程序倒是可以的。

然后想给页面内的图片加上点击放大的功能,虽然towxml组件本身提供了@tap的事件,但是测试下来也无法在外层接收到这个事件,原因未知。无耐,最后直接进到组件里面,在组件最内层的image上直接加了@click事件来调用uni.previewImage事件,单张点击放大是没问题的,只是不能点击的时候直接把文章内所有的图片取出来一个数组展示成可以滑动翻页的放大效果了。

其他问题想到了再加。虽然用起来走了不少弯路,但总结一句话,uniapp还是一个非常优秀的整合框架,需要你需要多平台发布的话,可以节省至少2/3的开发工作量。