uniapp开发app应用从创建到上架
目录
前言
一、项目初始化
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的值就会变成clickuni.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的iduni.$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 = 1break;} else if (curV1 < curV2) {result = -1break;}}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 = -1break;}}}return result;},
下面代码判断应用版本号和服务器端版本号,如果服务器版本号大于本地版本号,则显示更新弹窗
if (this.compare(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.cn/应用名.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.com/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.com/?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;// 判断是否IOSlet 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做为唯一标识。
小米市场审核的坑:
uniapp提供了获取手机oaid的方法:plus.device.getOAID,但是该方法在小米低版本系统中会引发获取手机电话管理权限的弹窗,导致审核不过,所以需要使用uni.getSystemInfoSync().oaid替代。
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,这样只返回一个dataresolve(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应用的关键点解释说明,后续再遇到新问题会持续更新。
相关文章:

uniapp开发app应用从创建到上架
目录 前言 一、项目初始化 1.初始化方式 2.账号注册 3.插件安装 二、项目结构及重点文件介绍 1.项目基本结构 2.项目文件介绍 三、应用打包 1. 安卓打包 2.苹果打包 四、应用发布 1. 安卓市场发布 用户权限和隐私政策 注销 软著和App备案证书 2. 苹果市场发布 …...
为什么使用Golang而非Rust开发桌面应用?
MoonGuard 团队选择 Golang 而不是 Rust 作为他们的 Krater 桌面应用程序,因为 Golang 中更容易进行内存管理、类型安全和 ORM 支持。 使用 Rust 和 Tauri 时面临的一些挑战包括: 难以理解 Rust 的所有权和借用规则、其严格的类型安全有时会限制开发速…...
问题复盘|MySQL 数据记录中明明有值,使用 concat() 后得到的却一直是 null
背景 MySQL 的数据数据记录中明明有值,在使用 concat() 查询时却一直得到 null SELECT CONCAT(first_name, , last_name) FROM users;排查后发现 MySQL 的 concat 函数拼接规则是 当多个拼接的字段的字段值中存在 null 时,返回的一定是 null 解决方…...

正点原子嵌入式linux驱动开发——Linux IIO驱动
工业场合里面也有大量的模拟量和数字量之间的转换,也就是常说的ADC和DAC。而且随着手机、物联网、工业物联网和可穿戴设备的爆发,传感器的需求只持续增强。比如手机或者手环里面的加速度计、光传感器、陀螺仪、气压计、磁力计等,这些传感器本…...

利用角色roles上线wordpress项目
角色订制:roles ① 简介 对于以上所有的方式有个弊端就是无法实现复用假设在同时部署Web、db、ha 时或不同服务器组合不同的应用就需要写多个yml文件。很难实现灵活的调用。 roles 用于层次性、结构化地组织playbook。roles 能够根据层次型结构自动装载变量文…...

4.0 Linux进程前导知识
个人主页:Lei宝啊 愿所有美好如期而遇 冯.诺依曼体系 CPU:运算器,控制器 输入设备:键盘,麦克风,摄像头,鼠标,网卡,磁盘等。 输出设备:显示器࿰…...

推荐一份适合所有人做的副业,尤其是程序员。
我建议每个人都去尝试一下网上接单,这是一个门槛低、类型多样的方式,尤其适合程序员! 在接单平台上,你可以看到各种类型的兼职。以freelancer为例,你可以在这里找到技术、设计、写作等类型的兼职,只要发挥…...

Linux中字符设备的打开、写入
一个内核模块应该由以下几部分组成。 第一部分,头文件部分。一般的内核模块,都需要 include 下面两个头文件: #include <linux/module.h> #include <linux/init.h> 第二部分,定义一些函数,用于处理内核…...

3d max软件中的缓存垃圾该如何清理?
使用3d max建模到渲染操作,来回对效果图调整的次数过多时,就会出现一下看不到的垃圾缓存,影响保存的速度,影响效率! 对于这类的3d垃圾清理的有什么高效方法呢? 3dmax垃圾清理的常规操作如下: 1、…...

11.13 牛客刷题8/10
11.13 信号完整性 指针地址 的加减,注意 最后转为16进制...
CI/CD简介
CI/CD简介 1、CI/CD流水线2、什么是CI/CD3、CI/CD的优点4、CI/CD的工作原理5、CI/CD流水线工具6、CI/CD的应用7、CI/CD的未来趋势 1、CI/CD流水线 从最初的瀑布模型,到后来的敏捷开发,再到今天的DevOps,这是现代开发人员构建出色产品的技术路…...
python opencv 读取文件夹下所有MP4文件并解析成jpg图像
你可以使用Python的OpenCV库来读取文件夹中的所有MP4文件,并将其解析为JPG图像。以下是一个示例代码,演示了如何实现这个功能,并设置解析间隔为3帧: import os import cv2def extract_frames(input_folder, output_folder, inter…...

MySQL binlog 日志解析后的exec_time导致表示什么时间?
1. exec_time 到底表示什么时间? MySQL binlog日志解析后,我们能看到会有 exec_time ,从字面意思理解这个记录的是执行时间,那这个记录的到底是单条sql的执行时间?还是事务的执行时间?下面通过测试来解读一…...

【Linux】:git基本操作_添加文件_两种场景_查看.git文件 || git修改文件 || 版本回退
🎯添加⽂件–场景⼀ 🎯在包含.git的⽬录下新建⼀个ReadMe⽂件,我们可以使⽤ git add 命令可以将⽂件添加到暂存区: • 添加⼀个或多个⽂件到暂存区: git add [file1] [file2] … • 添加指定⽬录到暂存区,…...

Django 基于ORM的CURD、外键关联,请求的生命周期
文章目录 基于ORM进行的CURDORM外键关联Django请求的生命周期流程图 基于ORM进行的CURD 本质上就是通过面向对象的方式,对数据库的数据进行增、删、改、查。 这里将会将我们之前所有内容结合到一起,首先确保基于上序操作已经建立好了UserInfo表ÿ…...
集合贴4——QA机器人设计与优化
基础课21——知识库管理-CSDN博客文章浏览阅读342次,点赞6次,收藏2次。知识库中有什么信息内容,决定了智能客服机器人在回答时可以调用哪些信息内容,甚至可以更简单地理解为这是智能客服机器人的话术库。https://blog.csdn.net/22…...

【Verilog语法】
Verilog语法 1. Verilog语法1.1 拼接运算符1.2 运算符优先级1.3 注释1.4 关键字1.5 模块结构1.6 结构语句1.7 赋值语句1.8 条件语句1.9 状态机1.10 OSI七层模型 1. Verilog语法 1.1 拼接运算符 1.2 运算符优先级 1.3 注释 1.4 关键字 1.5 模块结构 1.6 结构语句 1.7 赋值语句 …...

阿里云通用算力型u1服务器和e实例有什么区别?选择攻略
阿里云服务器ECS经济型e实例和通用算力型u1实例有什么区别?如何选择?ECS经济型e实例是共享型云服务器,通用算力型u实例是企业级独享型云服务器,e实例性价比高,现在2核2G3M带宽一年99元,云服务器u1价格相对要…...

modbus-TCP协议详解
modbus-TCP协议详解 1996年施耐德公司推出基于以太网TCP/IP的modbus协议:modbus-TCP。 MODBUS-TCP使MODBUS-RTU协议运行于以太网,MODBUS-TCP使用TCP/IP以太网在站点间传送MODBUS报文,MODBUS-TCP结合了以太网物理网络和网络标准TCP/IP以及以…...

爬虫项目(12):正则、多线程抓取腾讯动漫,Flask展示数据
文章目录 书籍推荐正则抓取腾讯动漫数据Flask展示数据 书籍推荐 如果你对Python网络爬虫感兴趣,强烈推荐你阅读《Python网络爬虫入门到实战》。这本书详细介绍了Python网络爬虫的基础知识和高级技巧,是每位爬虫开发者的必读之作。详细介绍见ὄ…...

【JavaEE】-- HTTP
1. HTTP是什么? HTTP(全称为"超文本传输协议")是一种应用非常广泛的应用层协议,HTTP是基于TCP协议的一种应用层协议。 应用层协议:是计算机网络协议栈中最高层的协议,它定义了运行在不同主机上…...

python/java环境配置
环境变量放一起 python: 1.首先下载Python Python下载地址:Download Python | Python.org downloads ---windows -- 64 2.安装Python 下面两个,然后自定义,全选 可以把前4个选上 3.环境配置 1)搜高级系统设置 2…...
【C语言练习】080. 使用C语言实现简单的数据库操作
080. 使用C语言实现简单的数据库操作 080. 使用C语言实现简单的数据库操作使用原生APIODBC接口第三方库ORM框架文件模拟1. 安装SQLite2. 示例代码:使用SQLite创建数据库、表和插入数据3. 编译和运行4. 示例运行输出:5. 注意事项6. 总结080. 使用C语言实现简单的数据库操作 在…...
JDK 17 新特性
#JDK 17 新特性 /**************** 文本块 *****************/ python/scala中早就支持,不稀奇 String json “”" { “name”: “Java”, “version”: 17 } “”"; /**************** Switch 语句 -> 表达式 *****************/ 挺好的ÿ…...

Spring数据访问模块设计
前面我们已经完成了IoC和web模块的设计,聪明的码友立马就知道了,该到数据访问模块了,要不就这俩玩个6啊,查库势在必行,至此,它来了。 一、核心设计理念 1、痛点在哪 应用离不开数据(数据库、No…...

Linux 内存管理实战精讲:核心原理与面试常考点全解析
Linux 内存管理实战精讲:核心原理与面试常考点全解析 Linux 内核内存管理是系统设计中最复杂但也最核心的模块之一。它不仅支撑着虚拟内存机制、物理内存分配、进程隔离与资源复用,还直接决定系统运行的性能与稳定性。无论你是嵌入式开发者、内核调试工…...

iview框架主题色的应用
1.下载 less要使用3.0.0以下的版本 npm install less2.7.3 npm install less-loader4.0.52./src/config/theme.js文件 module.exports {yellow: {theme-color: #FDCE04},blue: {theme-color: #547CE7} }在sass中使用theme配置的颜色主题,无需引入,直接可…...
NPOI操作EXCEL文件 ——CAD C# 二次开发
缺点:dll.版本容易加载错误。CAD加载插件时,没有加载所有类库。插件运行过程中用到某个类库,会从CAD的安装目录找,找不到就报错了。 【方案2】让CAD在加载过程中把类库加载到内存 【方案3】是发现缺少了哪个库,就用插件程序加载进…...

R 语言科研绘图第 55 期 --- 网络图-聚类
在发表科研论文的过程中,科研绘图是必不可少的,一张好看的图形会是文章很大的加分项。 为了便于使用,本系列文章介绍的所有绘图都已收录到了 sciRplot 项目中,获取方式: R 语言科研绘图模板 --- sciRplothttps://mp.…...

【 java 虚拟机知识 第一篇 】
目录 1.内存模型 1.1.JVM内存模型的介绍 1.2.堆和栈的区别 1.3.栈的存储细节 1.4.堆的部分 1.5.程序计数器的作用 1.6.方法区的内容 1.7.字符串池 1.8.引用类型 1.9.内存泄漏与内存溢出 1.10.会出现内存溢出的结构 1.内存模型 1.1.JVM内存模型的介绍 内存模型主要分…...