admin管理员组

文章数量:1655691

目录

前言

一、项目初始化

1.初始化方式

2.账号注册

3.插件安装

二、项目结构及重点文件介绍

1.项目基本结构

2.项目文件介绍

三、应用打包

1. 安卓打包

2.苹果打包

四、应用发布

1. 安卓市场发布

用户权限和隐私政策

注销

软著和App备案证书

2. 苹果市场发布

五、开发过程中的坑及解决方案

1. 消息推送(uni-push2)

2. 微信审核

3. 全屏遮罩

4. 状态栏高度

5. 应用更新

6. 清除缓存

7. webview

8. 分享

9. 地图

10. 访问量统计

11. 隐私政策

12. Universal Links

13. 模拟器

14. 通过外部链接打开App并跳转到目标页面

15.网络状态监测

16.路由拦截

17.请求封装

18.UrlSchemes

总结 


前言

通过uniapp官方开发文档,可以完成app开发,但开发过程中难免遇到疑难杂症,以下是本人开发过程中遇到的各种问题及应对方式,仅供参考。

一、项目初始化

1.初始化方式

首次初始化项目过程,详见uniapp官方文档。(uni-app官网)

2.账号注册

uniapp开发,需要在Dcloud上注册一个账号,账号用于项目打包发布、云服务相关。最好在项目开始之前就把账号申请好,如果是企业项目开发,就要公司申请一个账号,避免后期应用转让比较麻烦。使用云服务是需要交费的,比如消息推送、版本更新等等,按流量收费。可以申请一个月的免费云空间,到期后可以继续申请一个月的免费云空间。Dcloud注册地址:开发者中心

3.插件安装

HBuilderX开发工具自带一些官方插件,如git相关、代码格式化相关、云打包相关、真机运行相关、内置浏览器运行相关,能装的都装上。安装方式为:点击HBuilderX上方的“工具”——>“插件安装”

二、项目结构及重点文件介绍

1.项目基本结构

项目初始化之后,基本目录文件如下:

2.项目文件介绍

uniCloud:项目初始化时,是没有这个目录的,但是如果项目中需要使用云服务,比如需要使用云函数、应用版本管理与更新、消息推送等,就需要通过相关操作创建此目录(下文有详细说明)。

.hbuilderx:当项目引入云服务时,会自动创建此目录,该目录下存在一个launch.json文件,文件中内容主要是配置云函数的连接方式,比如是本地调用还是连接云服务调用。

api:该目录是自己创建的,用于存放调用后端接口的js文件,可以根据自己的项目判断是否需要创建此目录,非必要。

common:用于存放全局通用文件。

components:用于存放全局通用组件。

pages:用于存放页面文件,一般是.vue文件和.nvue文件。

static:用于存放静态资源文件,比如图片文件、字体文件等。

uni_modules:存放uniapp插件,插件可在官网下载,并直接安装。该目录自动生成。

unpackage:存放非代码资源,不需要上传到代码管理工具。该目录自动生成。

androidPrivacy.json:隐私条款文件,在安卓手机生效。效果是当app首次打开时,在进入主页面之前,系统会自动弹出隐私条款,询问用户是否同意,同意后才会进入到app中。该功能用于安卓应用市场审核,无此隐私条款,无法上架安卓应用市场。(苹果商店没有这些臭毛病)。

apple-app-site-association:配置苹果Universal Link,使用户在浏览器中输入配置的ulink地址后,可以看到打开应用的提示。

manifest.json:app配置项文件,支持界面配置和源码配置两种方式。

三、应用打包

1. 安卓打包

HBuilder提供了云打包方式,在开发工具中点击发行->原生App-云打包,即可进入打包界面。安卓app打包需要Android证书,生成证书的方式见官方提供的链接:

Android平台签名证书(.keystore)生成指南 - DCloud问答

生成证书后,即可在打包页面进行打包了,需要上传证书,填写证书别名、私钥密码(这些东西在生成证书的时候已经填写过,需要记下来,每次打包都需要用到)。

打包界面有一个渠道包的选项,意思是如果需要在不同的安卓平台发布,并且统计不同平台的下载量的话,可以勾选对应的渠道,打包后会打出对应的渠道包,发布在相应的应用市场。

最后,打包有传统打包和安心打包两种选择,区别就是是否将源码上传到Hbuilder平台上。我选的是安心打包,不需要上传代码。

打包成功后,会在文件目录的unpackage/release/apk目录下生成本次打包的apk文件。

2.苹果打包

苹果打包过程与安卓打包过程相同,主要是生成证书的方式不同,另苹果打包要在mac上进行。

苹果证书分为开发证书和发布证书,另外还有对应的描述文件。生成证书和描述文件的方法见官网提供的链接:iOS证书(.p12)和描述文件(.mobileprovision)申请 - DCloud问答

拿到证书和描述文件后,即可打包。使用开发证书打出的包,只能用于测试,不能发布应用市场,使用发布证书打出的包,可以发布应用市场。此外,如果想在手机里使用测试包,需要将测试手机的udid加入到开发证书对应的描述文件中(在生成描述文件时有这一步的配置) 


四、应用发布

1. 安卓市场发布

安卓应用市场,常见的有华为、小米、OPPO、VIVO、应用宝等等,发布前需要到各个应用市场注册账号,填写相关资料,提交审核即可。

注意点:

用户权限和隐私政策

安卓应用市场审核比较严格,尤其对用户权限、隐私政策等等要求比较高。对于用户权限和隐私问题,一定要清清楚楚的写明应用索要了哪些权限,引用了哪些SDK,涉及到哪些用户隐私,只要说明合理,一定会审核过的,不要试图隐瞒,瞒不住。

安卓市场对隐私政策比较执着,要求进入App之前,要先弹出隐私政策窗口,只有用户同意隐私政策之后,才能进入应用。此外,有些应用市场对隐私政策特别执着,不仅要有链接地址可以进入隐私政策查看,还有在应用内部,有随时可以查看隐私政策地方。

注销

有些应用市场要求应用必须提供注销功能,所以该功能必须提供。

软著和App备案证书

应用发布需要软著证书,软著证书可以是实体软著的扫描件,也可以是电子版软著证书(实体软著和电子版软著不是一个东西,电子软著下证比较快)。App备案证书是2023年9月工信部新要求,到相关部门备案即可。

2. 苹果市场发布

苹果市场发布相对简单,官方仅需要一个软著证明(电子版即可),另外如果应用中索要用户的哪些权限,也需要说明。对隐私条款等内容,没有明确要求。应用截图要与应用实际页面内容保持一致,否则也会审核不过。

苹果对应用外的第三方内容审查比较严格,比如项目中引用了第三方的页面,或有跳转第三方应用的地方,必须保证同一账号能够登录第三方(即单点登录),或保证第三方页面或应用没有二次登录的地方。

微信登录:

对微信登录审核要求较高。苹果要求,如果项目中引用了微信登录,那么同时要保证提供苹果登录功能;但还有一个矛盾点是,如果使用微信登录,需要手机上有微信,否则就无法登录,这种情况下苹果的审核人员是不会审核通过的,因为苹果审核人员的手机上,没有微信。目前解决方式是,监测手机上是否安装了微信,如果没有安装微信,则不暴露微信登录功能,相应的,如果应用中有跳转微信小程序的相关入口,也要隐藏。

闪屏页面制作:

苹果应用的闪屏页面,是一个storyboard文件,不能仅仅是一张图片,需要按照格式要求制作。这里需要用到xcode工具,也就是说,只能在苹果电脑上制作该文件。详细过程如下:

Uniapp ios制作自定义启动图storyboard文件_caijiangnan1121的博客-CSDN博客

重点:

保证应用的服务器能被境外访问到,有些国内的项目如政府项目,对安全要求比较高,服务器禁止境外访问,这样的话苹果应用被审核的时候,测试人员是无法访问的,一定会被退回。

五、开发过程中的坑及解决方案

1. 消息推送(uni-push2)

向App应用推送消息通知,需要用到第三方推送服务,uniapp集成了个推服务,不需要额外引入即可使用消息推送功能。 

消息推送分为在线推送和离线推送。在线推送即App应用处于打开状态或在后台运行时进行的消息推送,该部分功能通过uni-push内部即可实现。而离线推送需要在uni-push管理端进行厂商配置,配置成功后会由手机厂商推送到用户手机。

App端实现消息推送的过程和思路:

uni-push配置过程见官网:uni-push2统一推送 | uni-app官网

注意:苹果推送配置,需要在苹果开发者平台申请专门的推送证书,推送证书不是开发证书,也不是发布证书,就是推送证书。

消息推送其实是通过第三方服务将服务端信息推送给移动设备,关键点是推送时需要明确目标设备是什么,也就是说,需要获取客户端的设备识别码。

①绑定用户设备与用户Id

uniapp提供了获取设备推送标识的api——uni.getPushClientId函数,这样我们就拿到了设备的识别码,但是仅仅有这样一个识别码是不够的,有这个识别码,仅仅能做到后端服务将消息推送到这个设备上,但无法区分每个设备对应的用户是谁,比如推送新闻,是不需要用户是谁的,但如果推送业务消息,比如银行的余额变更,就需要这个设备的用户是谁,因此,在获取到设备识别码之后,还要将用户Id和设备信息绑定在一起。unipush提供了一个将设备编码绑定别名的函数,这个别名可以是我们的用户Id。

绑定别名的云函数示例(云函数创建过程可参考官网):

函数文件名:uniBindAlias/index.js(文件名可根据业务自定义)

'use strict';
const uniPush = uniCloud.getPushManager({
	appId: "xxxx" 
}) //注意这里需要传入你的应用appId
exports.main = async (event, context) => {
	return await uniPush.cidBindAlias([{
		cid: event.push_clientid, // 设备推送编码
		alias: event.userId  // 用户Id
	}])
};

云函数调用:

在vue文件中可以直接调用云函数,调用示例如下:

uniCloud.callFunction({
	name: 'uniBindAlias',
	data: {
			push_clientid: this.push_clientid,
			userId: this.userId
		}
 })

其中name的值是被调用的云函数文件名。

②推送消息

推送云函数示例如下:

云函数文件名:uniPush/index.js

'use strict';
const uniPush = uniCloud.getPushManager({
	appId: "xxxxxx" //app的id
})
exports.main = async (event, context) => {
	let obj = JSON.parse(event.body) // obj是后端传入的参数
	let data = {
		"title": obj.title,
		"content": obj.content,
		"payload": obj.data,
		"force_notification": true,
		"channel": { // 华为消息通道
			"HW": "LOW" // 默认一天推送两条消息,公共消息
		},
		"options": { // 小米、oppo、vivo的消息通道,各自有默认条数限制,都是公共消息
			"XM": { 
				"/extra.channel_id": "xxxx"
			},
			"OP": {
				"/channel_id": "xxxxx"
			},
			"VV": {
				"/classification": 0
			}
		}
	}
	if (obj.hasOwnProperty("alias")) { // 接收后端发送的目标设备别名
		let alia = obj.alias;
		if (alia instanceof Array && alia.length > 0) {
			data.getui_alias = obj.alias;
		}
		if (typeof(alia) == 'string' && alia.length > 0) {
			data.getui_alias = obj.alias;
		}
	}
	if (obj.hasOwnProperty("type")) {
		let type = obj.type;
		if (type != 1) { //私信消息,不限条数
			data.channel.HW = "NORMAL"; //华为私信通道,设置为"NORMAL"
			let options = {
				"HW": {
					"/message/android/notification/importance": "NORMAL",
					"/message/android/category": "WORK"
				},
				"XM": {
					"/extra.channel_id": "xxx"
				},
				"OP": {
					"/channel_id": "xxxx"
				},
				"VV": {
					"/classification": 1
				}
			}
			data.options = options
		}
	}
	return await uniPush.sendMessage(data)

};

推送消息是由后端发起的,当云函数上传到云空间后,在云空间上查看这个云函数,会有一个链接地址,后端调用这个地址即相当于调用云函数了。 

③解绑

如果同一台手机登录多个账号,那么最好将之前的账号和设备解绑,然后再将新账号和设备绑定。

解绑函数示例(云函数文件名:uniUnBindAlias/index.js):

'use strict';
const uniPush = uniCloud.getPushManager({
	appId: "_xxxxxx"
}) //注意这里需要传入你的应用appId
exports.main = async (event, context) => {
	return await uniPush.unboundAlias([{
		cid: event.push_clientid,
		alias: event.userId
	}])
};

 vue文件中调用解绑云函数示例:

uniCloud.callFunction({
	name: 'uniUnBindAlias',
	data: {
	    push_clientid: _this.push_clientid,
		userId: this.userId
		}
	})

④移动端通知弹窗

 在App.vue的onLaunch中:

uni.onPushMessage((res) => {
			console.log('收到推送消息:', res); //监听推送消息
			if (res.type == 'click') {
				let time = new Date().getTime();
				uni.navigateTo({
					url: '' //跳转到对应的通知页面
				});
			} else if (res.type == 'receive') {
				//如果发送通知的参数force_notification不是true,此时res.type的值是receive,需要手动创建一个本地通知,当点击这个通知的时候,会再次触发onPushMessage这个事件,而此时res.type的值就会变成click
				uni.createPushMessage({
					payload: res.data.payload,
					content: res.data.content,
					title: res.data.title
				});
			} else {
			}
		});

2. 微信审核

 如果项目中要求有微信登录、微信支付、打开微信小程序等功能,就需要到微信开发者平台注册账号、添加应用。只要按要求提供资料即可。唯一比较麻烦的是,应用开发主体需要有官方网站,官网上要有该App应用的名字及App相关介绍,并且该网站要进行备案,否则微信不予审核通过。

3. 全屏遮罩

在开发中,经常会用到弹窗效果,一般是背景黑色半透明,遮住全屏。但是在uniapp项目中,普通的写法是不能达到这个效果的,在vue文件中写弹窗效果,不能遮挡住原生导航和底部菜单,只能遮住中间部分,显然不是我们想要的效果,针对这个问题,uniapp提供了nvue的解决方案。

以最简单的退出登录功能为例,正常的操作流程是,在某个页面中点击退出按钮,然后会弹出退出确认框,确认框的背景应该是全屏半透明,代码示例如下:

①首先要在退出按钮所在文件(pages/my/my.vue)的同级目录下创建一个subNvue文件夹(pages/my/subNvue),然后在该文件夹内新建Logout.nvue文件,注意是nvue文件。

②然后,在pages.json文件中增加配置,即在path值为“pages/my/my”的对象下的style下的app-plus下,增加subNvues配置项,代码示例如下:

{
			"path": "pages/my/my",
			"style": {
				"navigationStyle": "custom",
				"enablePullDownRefresh": false,
				"app-plus": {
					"subNVues": [
						{
							"id": "popupLogout", // 唯一标识
							"path": "pages/my/subNvue/Logout", // 页面路径
							"type": "popup", // 类型为弹窗
							"style": {
								"margin": "auto",
								"width": "750upx",
								"height": "1334upx",
								"color": "#3476FE",
								"background": "transparent"
							}
						}
					]
				}
			}
		}

 ③在退出按钮所在文件(my.vue)中,增加对弹窗nvue文件的初始化:

initLogoutSubNvue() {
			let _this = this;
			this.logoutSubNVue = uni.getSubNVueById('popupLogout'); //popupLogout是在pages.json中注册的subNvue的id
			uni.$on('popup-logout-message', (data) => { // popup-logout-message 是Logout.nvue中向外抛出的信息,在本页面中通过uni.$on监听,从参数data中可以取到值。
				if (data.isClose) {
					_this.logoutSubNVue.hide('fade-out', 300);
				}
				if (data.isConfirm) {
					_this.logoutSubNVue.hide('fade-out', 300);
					_this.doLogout();
				}
			});
		},

uni.$on方法用于接收subnvue文件向外层传递的参数,通过data来接收参数。 

 ④nvue文件部分代码:

<template>
	<view class="popup-logout-page" @click.stop="">
		<view class="logout-wrapper">
			<view class="popup-header">
				<text class="title">确定退出登录吗?</text>
			</view>
			<view class="popup-bottom">
				<view class="btn left-btn" @click="debounce(onClose)()">
					<text class="text">点错了</text>
				</view>
				<view class="btn right-btn" @click="debounce(onConfirm)()">
					<text class="text">确定</text>
				</view>
			</view>
		</view>
	</view>
</template>

methods: {
		onClose() {
			uni.$emit('popup-logout-message', {
				isClose: true
			});
		},
		onConfirm() {
			uni.$emit('popup-logout-message', {
				isConfirm: true
			});
		}
	}

uni.$emit方法用于向外部发送事件参数,外层通过uni.$on来接收对应的事件参数。

注意: .nvue文件中的像素单位要使用upx。此外,在.nvue文件中写代码时,会发现缓存问题非常严重,现象就是写完代码,在模拟器或者手机上样式不更新。这时需要重启Hbuilder,重启模拟器,再看开发效果。

4. 状态栏高度

 如果使用uniapp自带的导航栏,是不需要设置状态栏的,但有些情况下,我们需要修改导航栏的样式,或者在导航栏上设置多个功能按钮,这样就需要自定义导航栏。自定义导航栏,就需要额外设置状态栏。

设置状态栏高度代码如下:

<view :style="{ height: statusBarHeight + 'px' }"></view>
created() {
		//获取手机状态栏高度
		this.statusBarHeight = uni.getSystemInfoSync()['statusBarHeight'];
	},

 注意:如果在app内打开webview,并且webview内的页面自带导航头,那么需要将设备的状态栏高度传递给webview,保证webview内页的状态栏高度显示正常,传递方式见后面webview功能介绍。

5. 应用更新

 app应用的更新,分为整包更新和wgt包更新。整包更新即重新下载安装应用,wgt包更新相当于补漏洞,不需要重新下载安装包,仅更新部分内容,例如王者荣耀的小版本更新,就属于wgt包更新。但wgt包的更新有一定限制,不是所有的功能变更都能通过wgt包更新完成,详情见官网。

这里要介绍一下app更新的实现逻辑和整包更新的方法。

实现逻辑:

①app端获取本机应用版本号,和服务器端做对比,如果低于服务器端,则弹出更新提示,部分代码如下:

获取本机应用版本号:

this.versionName = uni.getAppBaseInfo().appVersion

 版本号对比函数(v1是服务器返回版本号,v2是本机应用版本号):

compare(v1 = '0', v2 = '0') {
			console.log(v1,v2);
			v1 = String(v1).split('.')
			v2 = String(v2).split('.')
			const minVersionLens = Math.min(v1.length, v2.length);
		
			let result = 0;
			for (let i = 0; i < minVersionLens; i++) {
				const curV1 = Number(v1[i])
				const curV2 = Number(v2[i])
		
				if (curV1 > curV2) {
					result = 1
					break;
				} else if (curV1 < curV2) {
					result = -1
					break;
				}
			}
		
			if (result === 0 && (v1.length !== v2.length)) {
				const v1BiggerThenv2 = v1.length > v2.length;
				const maxLensVersion = v1BiggerThenv2 ? v1 : v2;
				for (let i = minVersionLens; i < maxLensVersion.length; i++) {
					const curVersion = Number(maxLensVersion[i])
					if (curVersion > 0) {
						v1BiggerThenv2 ? result = 1 : result = -1
						break;
					}
				}
			}
			return result;
		},

 下面代码判断应用版本号和服务器端版本号,如果服务器版本号大于本地版本号,则显示更新弹窗

if (thispare(this.updateVersion, this.versionName) > 0) {
					uni.$emit('send-version-message', { //将服务器版本传递给更新窗口subNvue,在更新窗口中接收versionInfo信息
						versionInfo: this.updateVersion
					});
					this.checkSubNVue.show('fade-in', 300, function() { //显示更新弹窗,该弹窗在本页面onShow时初始化:this.checkSubNVue = uni.getSubNVueById('settingCheckUpdate');
					});
				}else {
					uni.showToast({
						title: '当前已是最新版本~',
						icon: 'none',
						mask: true,
						duration: 2000
					});
					return;
				}

 ②更新应用并安装

安卓应用和苹果应用的更新方式不同,安卓应用可以直接从服务器端下载安装包,而苹果应用一定要跳转的苹果应用市场才能更新下载。

核心代码如下:

if (this.platform == 'android') {
				const downloadTask = uni.downloadFile({
					url: 'https://www.xxxx/应用名.apk',//服务器端apk包地址
					success: (downloadResult) => {
						if (downloadResult.statusCode === 200) {
							plus.runtime.install(
								downloadResult.tempFilePath,
								{
									force: false
								},
								function () {
									console.log('install success...');
									_this.onClose();
								},
								function (e) {
									console.error('install fail...');
								}
							);
						}
					},
					fail: (res) => {
					},
					complete: () => {
						downloadTask.offProgressUpdate(); //取消监听加载进度
					}
				});
				downloadTask.onProgressUpdate((res) => { //监听下载进度,同时显示在界面上。
					this.percent = res.progress;
					this.download = Number(res.totalBytesWritten / 1024 / 1024).toFixed(2);
					this.total = Number(res.totalBytesExpectedToWrite / 1024 / 1024).toFixed(2);
					console.log('已经下载的数据长度' + res.totalBytesWritten);
					console.log('预期需要下载的数据总长度' + res.totalBytesExpectedToWrite);
				});
			} else {
				plus.runtime.launchApplication(
					{
						action: `itms-apps://itunes.apple/cn/app/id${this.versionInfo.appleId}` //打开苹果应用市场,this.versionInfo.appleId的值就是应用的苹果市场上的id,上架应用后在苹果开发平台能看到
					},
					function (e) {
						console.log('Open system default browser failed: ' + e.message);
					}
				);
			}

备注: uniapp本身提供了一套应用更新的体系,但是需要走流量、收费,对于预算充足的项目来说,可以直接使用uniapp提供的功能,省时省力,安全可靠,而对于没有预算的穷酸项目,只能自行开发。

6. 清除缓存

一般的app应用都需要具备清除缓存功能,否则会导致应用体积越来越大,占用手机空间。清除缓存需要两步:

①获取缓存大小

formatSize() {
			let that = this;
			plus.cache.calculate(function (size) {
				let sizeCache = parseInt(size);
				if (sizeCache == 0) {
					that.fileSizeString = '0B';
				} else if (sizeCache < 1024) {
					that.fileSizeString = sizeCache + 'B';
				} else if (sizeCache < 1048576) {
					that.fileSizeString = (sizeCache / 1024).toFixed(2) + 'KB';
				} else if (sizeCache < 1073741824) {
					that.fileSizeString = (sizeCache / 1048576).toFixed(2) + 'MB';
				} else {
					that.fileSizeString = (sizeCache / 1073741824).toFixed(2) + 'GB';
				}
			});
		},

②清除缓存

onClearCache() {
			uni.showLoading({
				title: '缓存清理中...'
			});
			let that = this;
			let os = plus.os.name;
			if (os == 'Android') {
				let main = plus.android.runtimeMainActivity();
				let sdRoot = main.getCacheDir();
				let files = plus.android.invoke(sdRoot, 'listFiles');
				let len = files.length;
				if (len == 0) {
					uni.hideLoading();
				}
				for (let i = 0; i < len; i++) {
					let filePath = '' + files[i]; // 没有找到合适的方法获取路径,这样写可以转成文件路径
					plus.io.resolveLocalFileSystemURL(
						filePath,
						function (entry) {
							if (entry.isDirectory) {
								entry.removeRecursively(
									function (entry) {
										uni.hideLoading();
										//递归删除其下的所有文件及子目录
										uni.showToast({
											title: '缓存清理完成',
											duration: 2000
										});
										that.formatSize(); // 重新计算缓存
										setTimeout(function () {
											uni.navigateBack();
										}, 2000);
									},
									function (e) {
										uni.hideLoading();
										uni.showToast({
											icon: 'none',
											title: e.message,
											duration: 2000
										});
									}
								);
							} else {
								uni.hideLoading();
								entry.remove();
							}
						},
						function (e) {
							uni.hideLoading();
							uni.showToast({
								icon: 'none',
								title: '文件路径读取失败',
								duration: 2000
							});
						}
					);
				}
			} else {
				plus.cache.clear(function () { //一次清理不干净,需要清理两次
					plus.cache.clear(function () {
						uni.hideLoading();
						uni.showToast({
							title: '缓存清理完成',
							duration: 2000
						});
						that.formatSize();
					});
				});
			}
		}

7. webview

uniapp中提供webview组件,通过此组件可以在app内打开外部网页。使用webview嵌套在app内,可以降低app的开发成本和更新效率。app更新需要上架应用市场等待审核,但是如果使用webview,webview中的内容只需要自行在服务器端更新发布即可,无需上架应用市场审核,对于紧急bug修改、应用快速更新有很大的帮助。

webview使用示例:

<web-view :src="url" @message="onReceiveData"></web-view>

变量url是要打开的网页地址,传递给目标网页的参数只能放在url中; onReceiveData是从webview内的网页接收数据的函数。

 url传参示例:

uni.getSystemInfo({
				success: (sysinfo) => {
					statusbar = sysinfo.statusBarHeight; //当前设备状态栏高度
			        _this.url = _this.url + '&statusbar=' + statusbar;
				}
			});

 为保证每次打开webview中的内容都是最新的,在url中还需要传入时间戳: 

this.url = 'https://www.xxxxx/?time=939848933434/#/你的路由';

说明:目前只有通过<web-view>组件的写法才能从外部网页获取参数,其它webview的实现方式,无法获取外部网页传递进来的参数。

外部网页向webview传递参数代码示例:

uni.postMessage({
                    data: {
                        type: 'miniProgram',
                        url: 'gh_56b2c43416a4'
                    }
                });

 data中的参数,就是要传递给app的参数,在app中,通过之前的onReceiveData方法接收参数:

onReceiveData(event) {
    console.log(event.detail.data[0].type); // miniProgram
}

8. 分享

核心代码:

onShare(type) {
			let _this = this;
			if (type == 'weixin') { //分享到微信
				uni.share({
					provider: 'weixin',
					scene: 'WXSceneSession',
					type: 0,
					href: _this.shareUrl, //分享的url地址
					title: '欢迎来到 xxx APP',
					imageUrl: 'https://xxxxx/logo.png', //分享时显示的图片缩略图
					summary: '我正在使用 xxx APP,快来加入吧!',
					success: function (res) {
						console.log('success:' + JSON.stringify(res));
					},
					fail: function (err) {
						uni.showToast({
							title: JSON.stringify(err),
							icon: 'none',
							mask: true,
							duration: 3000
						});
					}
				});
			} else if (type == 'friend') {//分享到朋友圈
				uni.share({
					provider: 'weixin',
					scene: 'WXSceneTimeline',
					type: 0,
					href: _this.shareUrl,
					title: '欢迎来到 xxx APP',
					summary: '我正在使用 xxx APP,快来加入吧!',
					imageUrl: 'https://xxxx/logo.png',
					success: function (res) {
						console.log('success:' + JSON.stringify(res));
					},
					fail: function (err) {
						uni.showToast({
							title: JSON.stringify(err),
							icon: 'none',
							mask: true,
							duration: 3000
						});
					}
				});
			} else if (type == 'link') { // 复制到剪贴板
				uni.setClipboardData({
					data: _this.shareUrl,
					success: () => {
						uni.showToast({
							title: '已复制到剪贴板!',
							icon: 'none',
							mask: true,
							duration: 3000
						});
					}
				});
			}
		}

9. 地图

在h5中打开高德、百度地图的方法:

openMapApp(type) {
            /* Start  判断手机是IOS还是安卓 */
            let u = navigator.userAgent;
            //判断是否安卓
            let isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1;
            // 判断是否IOS
            let isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/);
            /* End */
            if (type === 'gaode') {
                //高德地图
                if (isAndroid) {
                    window.location.href = `androidamap://poi?sourceApplication=softname&keywords=${this.location}&dev=0`; //location是要导航的位置,是汉字
                } else if (isIOS) {
                    window.location.href = `iosamap://poi?sourceApplication=applicationName&name=${this.location}&dev=0`;
                }
            } else if (type === 'baidu') {
                //百度地图
                if (isAndroid) {
                    window.location.href = `bdapp://map/place/search?query=${this.location}&src=andr.baidu.openAPIdemo`;
                } else if (isIOS) {
                    window.location.href = `baidumap://map/place/search?query=${this.location}&src=ios.baidu.openAPIdemo`;
                }
            }
        },

10. 访问量统计

uniapp提供了一套完整的访问量统计服务,同样是付费使用,有钱的项目可以直接拿来用,没钱的请继续阅读。

参考uniapp官方的访问量统计功能的统计维度,可以从手机品牌、型号、日活、月活、装机量等维度来设计功能。

统计访问量的一个关键点是要知道访问终端的唯一识别码,如果一个app需要登录才能使用,那么可以把登录账号做为该终端唯一识别码,如果app不需要登录也能使用,那么就需要通过获取手机设备的标识做为唯一识别码,代码示例如下:

let deviceBrands = ['huawei', 'xiaomi', 'oppo', 'vivo', 'lenovo'];//主流安卓手机

onBehavior() {
			let _this = this;
			const deviceBrand = uni.getSystemInfoSync().deviceBrand; //获取设备品牌
			const deviceModel = uni.getSystemInfoSync().deviceModel; //获取设备型号
			if (uni.getSystemInfoSync().platform == 'ios') {
				console.log('ios系统');
				//ios系统
				plus.device.getInfo({
					success: function (e) {
						console.log('getDeviceInfo success: ' + e.uuid);
						let paras = { deviceBrand, deviceType: deviceModel, deviceCode: e.uuid };
						_this.callTongji(paras);//调用后端的统计接口,发送相关参数
					},
					fail: function (e) {
						console.log('getDeviceInfo failed: ' + JSON.stringify(e));
					}
				});
			} else {
				//android系统
				// let deviceBrand = uni.getSystemInfoSync().deviceBrand;
				if (deviceBrands.includes(deviceBrand)) {
					//主流机型
					let OAID = uni.getSystemInfoSync().oaid;
					console.log('getOAID success: ' + OAID);
					if(OAID) {
						let paras = { deviceBrand, deviceType: deviceModel, deviceCode: OAID };
						_this.callTongji(paras);
					}

				} else {
					//非主流机型
					plus.device.getInfo({
						success: function (e) {
							console.log('getDeviceInfo success: ' + e.uuid);
							let paras = { deviceBrand, deviceType: deviceModel, deviceCode: e.uuid };
							_this.callTongji(paras);
						},
						fail: function (e) {
							console.log('getDeviceInfo failed: ' + JSON.stringify(e));
						}
					});
				}
			}
		},

 说明:苹果设备和非主流安卓手机通过plus.device.getInfo获取uuid做为唯一标识,主流安卓手机(华为、小米、OPPO、VIVO、lenovo)通过uni.getSystemInfoSync().oaid获取oaid做为唯一标识。

小米市场审核的坑1.0:

uniapp提供了获取手机oaid的方法:plus.device.getOAID,但是该方法在小米低版本系统中会引发获取手机电话管理权限的弹窗,导致审核不过,所以需要使用uni.getSystemInfoSync().oaid替代。

小米市场审核的坑2.0:

经验证,上述1.0的解决方案可能会不好用,有可能和审核人员使用的小米系统版本或审核严谨度有关。查阅资料后,有人分析称uni.getSystemInfoSync().deviceBrand这个获取手机品牌的方法可能会引起小米旧版系统的弹窗,因此只能想办法绕过审核,比如在审核期间不调用获取手机相关标识的代码,审核通过之后再开启。目前能想到的是在服务端增加额外的配置项,通过配置项来控制。另小米官方客服给出的解决方案是,可以在用户登录之后再获取手机标识,但不适合我们自己的业务要求。

11. 隐私政策

 只有安卓应用市场对隐私政策有要求,至少三个地方需要有隐私政策:

①进入App应用之前,需要用户先阅读并同意隐私政策,然后才能进入到应用内。uniapp提供了解决方案,详见uni-app官网

②需要提供一个可访问的隐私政策地址,用于应用市场审核。

③需要在应用内有一个明显的可以随时阅读隐私政策的地方。

隐私政策内容:

①要明确引入了哪些sdk,每个sdk都获取了用户的哪些权限;

核心内容示例:

(一)我们可能向您收集的个人信息

个人信息是指以电子或者其他方式记录的能够单独或者与其他信息结合识别特定自然人身份或者反映特定自然人活动情况的各种信息。本隐私政策下,您必须授权我们收集和使用您的个人信息包括:

1)个人基本信息

手机验证码进行认证

2)定位信息

进行附近商户推荐和查询

3)设备信息

我们的产品基于DCloud uni-app(5+ App/Wap2App)开发,应用运行期间需要收集您的设备唯一识别码(IMEI/android ID/DEVICE_ID/IDFA、SIM 卡 IMSI 信息、OAID)以提供统计分析服务,并通过应用启动数据及异常错误日志分析改进性能和用户体验,为用户提供更好的服务。详情内容请访问 《DCloud用户服务条款》

消息推送服务供应商:由每日互动股份有限公司提供推送技术服务,我们可能会将您的设备平台、设备厂商、设备品牌、设备识别码等设备信息,应用列表信息、网络信息以及位置相关信息提供给每日互动股份有限公司,用于为您提供消息推送技术服务。我们在向您推送消息时,我们可能会授权每日互动股份有限公司进行链路调节,相互促活被关闭的SDK推送进程,保障您可以及时接收到我们向您推送的消息。详细内容请访问 《个推用户隐私政策》 

我们涉及用户信息使用的SDK相关情况:

(一)UniPush模块集成第三方SDK说明

SDK名称:个推·消息推送

包名信息:com.igexin、com.heytap、com.vivo.push、com.xiaomi.push

使用目的:UniPush消息推送

涉及个人信息:存储的个人文件、设备信息(IMEI、MAC、ANDROID_ID、DEVICE_ID、IMSI)、应用已安装列表、网络信息

个推隐私协议地址: 个推用户隐私政策-个推文档中心

②隐私政策的发布主体要与app应用的发布主体一致; 

12. Universal Links

 Universal Links的效果是在浏览器中输入ulink链接地址,如果手机内安装了该应用,则浏览器上方会自动出现打开xxx应用的提示按钮,仅在iphone上有效。

uniapp提供了一套生成Universal Links的方案,但是收费,不差钱的可以直接用,差钱的继续阅读。

设置Universal Links操作流程:

这篇文章写的不错,可参考:Uniapp IOS universal link 配置流程_ios ulink-CSDN博客

注意:生产环境服务器一定要可以被境外访问,否则苹果检测不到服务器上的apple-app-site-association文件,导致此功能不生效。

在微信开放平台,有配置Universal Links的地方(仅IOS应用需要配置),因为传统的通过schemeUrl的方式打开app,在微信中是不好用的,只有通过Universal Links打开,在微信中才可用。(仅IOS可用)。如果手机中没有安装app,则会直接打开Universal Links对应地址的网页。

那么问题来了,如果安卓系统想在微信中打开app应用,有什么办法?答案是,不能直接在微信中打开app,只能提示用户,在浏览器中打开本页面,然后通过schemeUrl的方式打开app。

典型案例是,比如有人通过微信分享给你一个盒马上商品的链接,打开页面后,顶部会有一个立即打开的按钮,如果是苹果系统,点击按钮后就会直接打开盒马app,如果是安卓系统,则会打开一个新页面,在新页面提示用户在浏览器中打开。

13. 模拟器

本人使用夜神模拟器,模拟器下载及使用可自行百度。这里着重说明的是,使用模拟器会比较卡,有时缓存比较严重,建议使用真机开发。

14. 通过外部链接打开App并跳转到目标页面

首先判断是否在微信环境中,判断代码如下:

is_weixin() {
            let ua = window.navigator.userAgent.toLowerCase();
            if (ua.match(/MicroMessenger/i) == 'micromessenger') {
                console.log('微信浏览器');
                return true;
            } else {
                console.log('不是微信浏览器');
                return false;
            }
        }

 如果是在微信环境中,跳转链接前,要提示用户在浏览器中打开。

在h5页面中,可以通过schemeUrl的方式打开app应用,同时可以传递参数,这样在app中可以通过获取参数进而打开指定的页面。

打开app示例:

window.location = 'xxx://page=user/detail?id=' + this.id //xxx是app的scheme的值,//后面的是传递给app的参数。

在app中接收参数关键代码:

plus.runtime.arguments //此段代码可以获取到url链接跳转到app的完整地址,然后根据需要从中获取关键参数。

 一般外部链接打开app应用,然后在应用首页判断需要进入到哪个具体的目标页面,为避免从目标页面回到首页后,再次自动跳转目标页,需要在首页进行判断,如果第一次已经成功进入目标页,那么就要给plus.runtime.arguments赋空值:

plus.runtime.arguments = null;
plus.runtime.arguments = '';

15.网络状态监测

 app如果出现网络异常,要给用户明显的提示或显示专门的无网络连接页面。此外,苹果手机首次打开app应用时,会获取网络权限,在这个过程中,app应用是无法连网的,就需要通过app判断连网状态,给出对应的提示内容。

uniapp提供了两种获取网络连接状态的方法

uni.getNetworkType可以主动判断当前是否连网,uni.onNetworkStatusChange用来进行网络连接状态监听,不同的方法可以用于不同的情景判断。

16.路由拦截

let list = ['navigateTo', 'redirectTo', 'reLaunch', 'switchTab'];
		list.forEach((item) => {
			uni.addInterceptor(item, {
				invoke(e) { //在这里面进行相关逻辑的编写。参数e中包含了路由信息。
					console.log('e', e);
					return e;
				},
				fail(err) {
					console.log('err', err);
				}
			});
		});

17.请求封装与拦截

import {
	url
} from '@/common/constant.js'
const request = (config) => {
	// 拼接完整的接口路径
	// config.url = '/api' + config.url;
	config.url = url + '/api' + config.url;
	//判断是都携带参数
	if (!config.data) {
		config.data = {};
	}
	let promise = new Promise(function(resolve, reject) {
		uni.request({
			method: config.method || "GET",
			url: config.url,
			data: config.data || {},
			timeout: 6000,
			header: config.header || {
				"content-type": "application/json",
				Authorization: 'Bearer ' + uni.getStorageSync('token'), //请求头添加token,uni.getStorageSync('token')的数据,是登录时存放在本地存储中的token数据
			},
		}).then(responses => {
			// 异常
			if (responses.statusCode != 200) {
				console.log('请求bug', config.url, responses);
				if (responses.data?.msg) {
					uni.showToast({
						title: responses.data.msg,
						icon: 'none',
						mask: true,
						duration: 3000
					});
				}
				reject({
					message: responses.data.msg
				});
			} else {
				let response = responses.data; // 如果返回的结果是data.data的,嫌麻烦可以用这个,return res,这样只返回一个data
				resolve(response);
			}
		}).catch(error => {
			reject(error);
		})
	})
	return promise;
};
uni.addInterceptor('request', {
	invoke(args) { //在args参数中,可以设置request中的各项参数,例如下面代码可以设置token信息;
		// console.log('args',args);
		try {
			args.header["adq-token"] = uni.getStorageSync('token');
		} catch (e) {
			console.log('err', e);
		}
	}
})

18.UrlSchemes

 UrlSchemes可以在manifest.json进行配置,安卓和苹果需要分别配置。UrlSchemes主要是用于第三方app打开本app,或者通过外部链接地址打开本app。具体可参考官方文档:

uni-app官网

总结

以上是通过uniapp开发app应用的关键点解释说明,后续再遇到新问题会持续更新。
 

本文标签: 上架uniappapp