使用uniapp开发跨平台app

之前一段时间尝试过使用 uniapp 来开发安卓/ios 的 APP,踩过很多坑,为了解决问题几乎翻遍了整个 dcloud 社区,最后成功上线了几款产品。因此对一些功能的实现来做一个总结,方便以后查看。

前提注明

注明:官方推荐长列表使用 nvue 做性能更好,问题在于 nvue 的限制极其离谱,如果长列表项目不是很多,建议不要用 nvue,如果列表项目很多,建议 flutter 或者原生开发。

设备相关

  1. 获取设备 ID 或唯一标识符:
  2. 获取平台:

自定义隐私政策

manifest.json 里配置

1
2
3
4
5
"app-plus" : {
"privacy" : {
"prompt" : "custom"
}
}

onLoad中使用

1
2
3
4
5
6
if (!plus.runtime.isAgreePrivacy()) {
// 如果没有同意隐私政策,同意后不再触发
this.showCustomPrivacy = true; // 展示自定义隐私授权框
return;
}
// 正常流程,获取信息等

隐私政策弹窗的两个按钮需要两个方法:

1
2
3
4
5
6
7
8
9
agreePrivacy() {
plus.runtime.agreePrivacy();
this.showCustomPrivacy = false; // 隐藏弹框
// 正常流程,获取信息等
},
disagreePrivacy() {
plus.runtime.disagreePrivacy();
plus.runtime.quit(); // 退出APP
},

文档 : Android 平台隐私与政策提示框配置方法

适配

状态栏适配(使用自定义导航栏)

  1. 非全屏 APP 下外部容器设置 padding-top: var(--status-bar-height);

    --status-bar-height是官方提供的 css 变量,表示状态栏高度

  2. 单独写一个元素做占位符,高度为var(--status-bar-height)

  3. 但这个 api 只能拿到状态栏高度,要想适配异性还需要进行其他配置。

状态栏样式

默认值:manifest.json文件的 app-plus->statusbar->style 配置 支持 light 和 dark (根据文档设置了,然而没用)

page.json里设置 "navigationBarTextStyle ":"white",完美解决所有问题。

动态设置: plus.navigator.setStatusBarStyle(style); 需要在页面 onReady 和 onShow 里调用,这个 api 在 onLoad 里调用无效,切换页面导致颜色改变后返回不会触发 onReady,因此需要在 onShow 里再次调用,但是只在 onShow 里调用又会失效。

沉浸式状态栏 page.json 里配置:

1
2
3
"app-plus": {
"immersed": true
}

导航栏

  • 使用原生导航栏

    page.json相关页面中设置

    1
    2
    3
    "style": {
    "navigationStyle": "default"
    }
  • 自定义导航栏

    1
    2
    3
    4
    5
    6
    7
    8
    "style": {
    "navigationStyle": "custom",
    "navigationBarTextStyle": "black", // 导航栏颜色
    "app-plus": {
    "titleNView": false, // 关闭原生导航栏
    "scrollIndicator": "none"
    }
    }

ios 安全区

见第一个文档。

文档

uni-app 全面屏、刘海屏适配(iphoneX 适配)及安全区设置

Android 平台设置沉浸式状态栏显示效果

状态栏大全-状态栏透明(沉浸式)、变色及全屏的区别

iOS 平台设置系统状态栏(通知栏、顶部状态栏)样式背景颜色或透明

uni-app 导航栏开发指南

支付

  1. manifest.json 中打开支付相关选项。微信支付需要填写与 APP 包名绑定的 appid 值,否则无法支付。
  2. 调用 uni.getProvider(安卓) 或 plus.payment.getChannels => requestOrder(IOS)获取支持的支付提供商 / 渠道。
  3. 传入参数至uni.requestPayment,不同支付渠道参数格式不同。

关于苹果内购的问题

苹果经常出现调用 api 后无法有弹窗的问题,参考文档:

苹果支付(内购项目)回调验证php 菜鸟技术天地-CSDN 博客苹果支付回调

苹果 ISO 内购支付,支付成功,但是回调不执行问题 - DCloud 问答

苹果内购 的经验分享 - DCloud 问答

支付接口问题, 终于搞定了! 特写这封感谢信! - DCloud 问答

分享苹果内购 requestPayment 没有任何返回 - DCloud 问答

详细见文档。

支付 - uni-app 官网

登录

云函数

这里使用 uniapp 的云函数实现短信发送和获取一键登录的手机号:

大致流程 : 右键项目文件夹 => 创建 uniCloud 云开发环境 => 创建一个并关联 => 右键 cloudfunctions 文件夹,新建云函数 => 完成后右键上传云函数。

一键登录

首先通过云函数获取手机号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
'use strict';
exports.main = async function (event) {
const res = await uniCloud.getPhoneNumber({
appid: '__UNI__D2A5305', // 替换成自己开通一键登录的应用的DCloud appid,使用callFunction方式调用时可以不传(会自动取当前客户端的appid),如果使用云函数URL化的方式访问必须传此参数
provider: 'univerify',
apiKey: '', // 在开发者中心开通服务并获取apiKey (这里是Luka大叔星座馆的)
apiSecret: '', // 在开发者中心开通服务并获取apiSecret
access_token: event.access_token,
openid: event.openid,
});
return {
code: 0,
message: 'success',
data: res,
};
};

客户端调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
uni.login({
provider: 'univerify', // 一键登录的标识
univerifyStyle: {...},// 自定义登录框样式,参考文档中的配置
success(res){ // 登录成功
let result = self.quickLogin(res.authResult); // {openid:'登录授权唯一标识',access_token:'接口返回的 token'}
let phoneNum = result.result.data.phoneNumber; // 获取到用户手机号
...
},
fail(res){}
})
async quickLogin(data) {
try {
const res = await uniCloud.callFunction({
name: 'quickLogin',
data: data
});
return res;
} catch (err) {
return err;
}
}

成功或失败时都使用 uni.closeAuthView() 关闭一键登录弹窗

手机短信验证码登录

通过云函数发送验证码

1
2
3
4
5
6
7
8
9
'use strict';
exports.main = async (event, context) => {
try {
const res = await uniCloud.sendSms(event);
return res;
} catch (err) {
return err;
}
};

uniCloud.sendSms()的参数格式参考文档,这里为后端接口的返回值

1
2
3
4
5
6
7
8
9
10
11
async sendSms(data) {
try {
const res = await uniCloud.callFunction({
name: 'sendSms',
data: data
});
return res;
} catch (err) {
return err;
}
}

第三方渠道登录

首先需要在进入页面时获取登录渠道

1
2
3
4
5
6
7
8
9
10
uni.getProvider({
service: 'oauth',
success: (res) => {
console.log('oauth success:' + JSON.stringify(res));
res.provider.map((value) => {
self.providerList.push(value);
});
},
fail: (e) => {},
});

通过 providerList 中的值展示三方渠道登录按钮

调用 api 实现登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
uni.login({
provider: provider,
success: (loginRes) => {
// 登录成功后可以获取用户信息,每个平台信息核实不太一样,这里选择传给后端接口获取一个统一的信息格式
uni.getUserInfo({
provider: provider,
success: (res) => {
// ...
},
fail: (e) => {},
});
},
fail: (e) => {},
});

其他问题

1
2
3
4
5
6
7
8
9
10
11
12
13
uni.getSystemInfo({
success(res) {
if (res.platform == 'ios') {
self.canUseAppleLogin = parseInt(res.system) >= 13; // ios13+ 支持苹果登录,低版本和安卓手机做兼容处理
plus.oauth.getServices(() => {
// 判断是否安装微信选择展示微信登录
try {
self.isWXInstalled = plus.ios.import('WXApi').isWXAppInstalled();
} catch (e) {}
});
}
},
});

参考文档

uni 一键登录

短信发送

iOS 苹果授权登录(Sign in with Apple)/Apple 登录/苹果登录集成教程

安卓打包后,微信登录提示“认证失败,原因:用户取消”

QQ 登录鉴权报错

该怎么判断是否已安装微信和 QQ?

uni-app 如何判断是否安装腾讯 QQ 微信微博支付宝淘宝客户端

uni-app 怎么注销微信或者 qq 的授权登录?有 uni.logout 这个方法吗?

分享

文档:分享

参考代码(分享至微信朋友圈)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
shareWX() {
uni.share({
provider: 'weixin',
scene: 'WXSenceTimeline',
type: 2,
imageUrl: self.savetopimgurl, // 需要分享的图片地址
success: function(res) {
uni.showToast({
title: '分享成功'
});
},
fail: function(err) {
uni.showToast({
title: err.errMsg.includes('未安装') ? '微信客户端未安装' : '分享失败,请检查您的网络',
icon: 'none'
});
}
});
}

推送

获取客户端位移标识,用于定向推送,并在首次进入 APP 时上报

1
2
var pinf = plus.push.getClientInfo();
var cid = pinf.clientid; //客户端标识

使用plus.push.createMessage()方法创建本地推送消息。

使用plus.push.addEventListener('receive',()=>{})接受推送信息并在回调中创建本地推送。

参考代码已经封装成 push.js,在 main.js 中 import 即可。
push.js 内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/* 
透传消息内容格式
{
"title":"测试标题",
"type":"receive",
"content":"测试内容",
"payload":"{\"id\":\"1\",\"jump\":\"1\",\"url\":\"/pages/home/find\"}"
}
*/
(function () {
function pushReady() {
// plus.push.createMessage('点击进入页面', JSON.stringify({
// id: '61534',
// url: "/pages/home/index",
// jump: '1'
// }), {
// title: '这是自定义标题哦'
// })
// plus.nativeUI.toast('plus ready')
function jumpToPage(payload) {
// plus.nativeUI.toast('push jump:' + JSON.stringify(payload))
const url = payload.url + '?pushId=' + payload.id;
// plus.nativeUI.toast(url)
if (payload.jump == '1') {
uni.switchTab({
url: url,
success(r) {
// plus.nativeUI.toast('switch succ' + JSON.stringify(r))
},
fail(e) {
// plus.nativeUI.toast('switch err' + JSON.stringify(e))
},
});
} else {
uni.navigateTo({
url: url,
success(r) {
plus.nativeUI.toast('navigate succ' + JSON.stringify(r));
},
fail(e) {
plus.nativeUI.toast('navigate err' + JSON.stringify(e));
},
});
}
}

function createPushOBj(msg, type) {
return {
title: msg.title,
content: msg.content,
id: msg.payload.id,
jump: msg.payload.jump,
url: msg.payload.url,
};
}

plus.push.addEventListener('click', function (msg) {
// IOS后台的消息是msg.payload对象,前台的消息是msg.payload字符串
// plus.nativeUI.toast('click:' + JSON.stringify(msg))
if (msg.payload.id == null) {
jumpToPage(JSON.parse(msg.payload));
} else {
jumpToPage(msg.payload);
}
});

// 安卓的推送,点击后触发receive,IOS的通知点击的时候触发click监听器
// ios使用透传,接收后触发receive
plus.push.addEventListener('receive', function (msg) {
plus.nativeUI.toast('receive:' + JSON.stringify(msg));
let payload = msg.payload;
// IOS接收后创建本地消息,本地消息会再次触发receive,所以判断是否本地创建的,将其它数据封装在payload(需为字符串)中
// 直发时,content = payload = {}
if (plus.os.name !== 'Android' && msg.type === 'receive') {
let content = msg.content;
let title = msg.title;
let pl = {
title: title,
content: content,
id: payload.id,
jump: payload.jump,
url: payload.url,
};
plus.push.createMessage(content, JSON.stringify(pl), {
title: title,
});
}
if (plus.os.name === 'Android' && msg.content.indexOf('{') === 0) {
plus.nativeUI.toast('payload:' + payload);
payload = JSON.parse(payload);
let content = msg.content;
let title = msg.title;
let pl = createPushOBj(msg);
// plus.nativeUI.toast(content + '_' + JSON.stringify(pl))
plus.push.createMessage(content, JSON.stringify(pl), {
title: title,
});
}

if (plus.os.name === 'Android' && msg.content.indexOf('{') === -1) {
let payload = JSON.parse(msg.payload);

let pl = {
title: msg.title,
content: msg.content,
id: payload.id,
jump: payload.jump,
url: payload.url,
};
plus.push.createMessage(msg.content, JSON.stringify(pl), {
title: title,
});
}
});
}

//#ifdef APP-PLUS
pushReady();
//#endif
})();

参考:

API Reference Push

UniPush 使用指南 - DCloud 问答

其他参考

iOS App 第一次安装启动后,会弹出是否允许联网的询问框,在用户点击同意前,调用联网 API 会失败。因此如果有信息需要在 create 或者 mount 方法中获取的话,就要做兼容处理,否则会导致用户首次进入 APP 时获取不到信息。

manifest.json 文档说明

使用 HTML5+ 注意事项 - uni-app 官网

H5 正常但 App 异常的可能性

iOS 云打包如何设置通用链接等 Capabilities 配置

iOS 平台微信 SDK 更新需要配置通用链接(Universal Links)

关于 IOS 缓存本地图片读取显示空白的问题解决办法

前万小心,安卓跟 IOS 获取本地文件的区别

Uploader(文件上传)问题汇总

uni-app 运行环境版本和编译器版本不一致的问题

5+API 错误代码

监听短信验证码并自动提交表单

Android 平台设置 UrlSchemes,实现被第三方应用调用

Android 平台云端打包权限配置 - DCloud 问答

push.js