app分类
app主要指的是在手机上运行的第三方应用程序~主要分为一下三类
- Native App (原生应用):指的是基于手机操作系统开发出来的第三方应用程序
- 需要下载(每次更新都需下载)
- ios/android 操作系统不同,代码不兼容(需要开发两套不同的代码) -> 开发慢,开发成本高
- 用户体验度好
- Web App(网页应用):指的是使用web技术来开发的app
- 不需要下载
- 跨平台的,一套代码可以在多个浏览器使用->开发快、快发成本低
- 用户体验不太好,不同的浏览器展示效果可能存在差异,部分功能无法实现
- HyBird App(混合应用)
- 将原生应用与网页应用相结合,集原生应用 与 网页应用的优点于一身
- 本质就是将web页面内嵌在原生app中,然后产生一系列的交互~
h5与原生进行交互
[1] 判断h5页面打开的环境是ios/android
通过navigator.userAgent判断当前页面的运行环境
[2] JS与客户端互相调用
js调用ios的方法
-
在ios中:在ios中定义的方法若是想被嵌入的h5页面调用
- [1] 声明一个方法 如getAppInfo
- [2] 注册对这个方法的监听
wkWebView.configuration.userContentController.add(self, name: getAppInfo)
-
在js中
在js中使用固定语法进行调用
window.webkit.messageHandlers.方法名.postMessage(传参数据)
window.webkit.messageHandlers.getAppInfo.postMessage()
js调用android的方法
-
在android中
在android中,需要暴露出一个全局变量xxxBridge,类似浏览器环境中的Window
// 获取webview的设置对象 WebSettings webSettings = mWebView.getSettings() // 设置Android允许js脚本 webSettings.setJavaScriptEnabled(true) // 暴露出JSBridge的对象到webView的全局环境 mWebView.addJavascriptInterface(getJSBridge(), 'JSBridge')
嵌入的web页面中就多了一个全局变量JSBridge,通过此全局变量可以调用android客户端的方法或属性啦。
-
在js中
在js中的调用是非常简单的,比如说在android中暴露的全局变量名为JSBridge,该变量上存在getAppInfo方法, 通过如下代码即可调用
Window.JSBridge.getAppInfo()
error- android接收不到参数
window.JSBridge.showShareIcon({
id: 1,
type: 1
})
我按照如上方式进行传参,客户端是接收不到的,客户端要求直接传两个参数,参数类型都为简单数据类型,如下:
window.JSBridge.showShareIcon(1,1)
这样就可以正常接收啦。
h5调用android的方法时传递的参数必须为
简单数据类型
。
js调用ios与android的区别
- 调用ios的方法时是异步的;而调用android的方法时是同步的!
- 调用ios方法传参时可以传对象;而调用android的方法时只能传简单数据类型!
示例:js调用ios与android完整示例代码
function info() {
if (navigator.userAgent.search(/(iphone|ipad|ipod)/i) >= 0) {
return 'iOS' // 手机iOs
} else if (navigator.userAgent.search(/android/i) >= 0) {
return 'Android' // 手机安卓
} else if (navigator.userAgent.search(/windows phone/i) >= 0) {
return 'WinPhone'
} else {
return 'PC'
}
}
const platform = info()
const params = {...}
if (platform === 'iOS' && window.webkit) {
// showShareIcon 为ios抛出的方法
window.webkit.messageHandlers.showShareIcon.postMessage(params)
} else if (platform === 'Android' && window.JSBridge) {
// JSBridge为Android抛出的全局对象
window.JSBridge.showShareIcon(JSON.stringify(params))
}else {
return false
}
客户端调用js方法
若是客户端想调用web页面中的方法,需要我们先将方法暴露出去
tips: 有时客户端拿不到window, 因此需要做下兼容~
const pluginFun = (function(){
// 兼容全局对象
const _global = (function () {
return window || this
}())
const plugin = {
方法1(){},
方法2(){},
...
}
// 将和客户端约定的方法暴露给全局对象
for (const i in plugin) {
!('jsFunc' in _global) && (_global[i] = plugin[i])
}
}())
示例: 客户端与web交互之回调
需求场景:web页面上有提现功能,此功能的逻辑是
- [1] 用户在h5页面点击提现
- [2] h5调用客户端方法(客户端调用支付宝支付通道判断是否可以提现),判断结果返回到web页面
- [2] web页面获取到是否可以提现之后:若是可以提现调取提现接口,若是不可以提现则提示不可提现原因;
在vue中可以使用$on进行事件监听,具体做法如下
- [1]调用客户端isDrawal方法->判断是否可以提现并监听客户端返回结果
created(){ vm.$on('drawal', data=>{ // 获取到客户端返回的信息data, 若是可以提现则调取提现接口,若是不可以提现则提示不可提现原因; }) } methods:{ isDrawal(){ if(isAndroid && window.JSBridge){ window.xxxJSBridge.isDrawal(url) // url是支付宝url } } }
- [2] 封装callbackme方法供客户端调用
const pluginFun = (function(){ // 兼容全局对象 const _global = (function () { return window || this }()) function callbackme(data){ vm.$emit('drawal', data) } _global.drawal = callbackme }())
[3]页面跳转
vue项目中正常的页面跳转代码如下
this.$router.push(url)
当代码执行之后将在原来的浏览器窗口中打开该页面。
客户端嵌入的web页面在进行跳转时可能跳转到客户端页面也有可能跳转到其他h5页面。
此时引入了一个新的概念webView
。
webView
webView可以看作是本地应用程序内嵌的一个浏览器,它拥有渲染引擎,可以通过http/https请求加载内容,加载回来的代码就可以被执行和渲染。
web端与客户端约定协议
当web页面嵌入客户端时,需要约定一个跳转协议
bd-xx://yy
xx为项目协议名:同一个项目xx名唯一
yy为协议名:不同跳转可以约定不同的yy名(大部分跳转都是用一个,特殊情况可以约定不同的yy名)
假设约定的协议名为bd-triger://web
- 跳转到h5页面
// url为要跳转的h5页面的全路径 jumpPage(url){ location.href = 'bd-triger://web?url='+encodeURIComponent(url) }
- 若是要跳转到客户端内的页面
location.href='bd-triger://客户端提供的页面名'
那么问题来了—> 嵌入客户端的页面还能使用原本的方法($router.push)进行页面跳转吗?
答案是可以的!
-
若是使用$router.push等方法去进行页面跳转时,该页面会在
当前
webView(相当于是同一个浏览器)中打开。 -
若是使用h5与客户端预先约定的协议进行页面跳转时,则该页面会在
新的webView(新的浏览器页面)
中打开。
听起来好像是没有很大区别,但是问题的本质是在同一个webView打开的页面状态一致(以第一个页面为准),如是否全屏,是否监听控制页面跳转等等....
是否全屏打开页面
一个完整的页面包含 状态栏+导航栏+内容部分。
- 若是全屏显示,则web页面的嵌入面积包含这三部分的面积(tips: 此时需要自己设置page padding-top 防止内容和状态栏重叠)
- 若是非全屏显示,则web页面的嵌入面积仅有内容content部分,页面自带客户端的导航栏和状态栏间距。
页面全屏/非全屏展示
在客户端中分为一级页面和二级页面。在客户端中一般情况下存在几个tab页面,tab页面就是一级页面,由一级页面跳转进入二级页面。
一级页面(tab页面)默认
为全屏展示,若是h5页面作为tab页面嵌入,那么就必须设置好状态栏的高度,否则样式就会混乱!
若是二级页面则可以和客户端约定一个路径参数来判断当前web页面是否需要全屏展示(能不用全屏就不用全屏)。
若是需要全屏展示,还有一个新的问题,那就是状态栏高度(设置padding防止页面与状态栏重叠)
- 若是ios的状态栏高度可以通过css参数直接设置
- 若是android的状态栏高度在h5页面是直接获取不到的,需要客户端获取传递。此时需要再和客户端约定一个路径参数(若是全屏客户端在路径上拼接此参数)。
举例说明:假设和客户端约定的是否全屏参数为need_full_screen,状态栏高度为height,那么
- 全屏展示
'bd-triger://web?url='+encodeURIComponent('http://xxx?need_full_screen=1&height=40')
- 非全屏展示
'bd-triger://web?url='+encodeURIComponent('http://xxx')
跳转
在同一个webView打开的页面是否全屏 设置相同并且以第一个页面为准!
举例说明
- [1] 假设存在h5页面page1,该页面嵌入在tab页面(默认全屏);在page1中存在按钮“支付”,点击跳转到支付页面(h5页面),此时存在以下几种情况
- 情况1
此时页面在当前webView打开,该webView第一个页面为page1(全屏),因此支付页也为全屏打开this.$router.push('/web/pay')
- 情况2
此时页面在新的webView打开,双方约定的是否全屏参数值为false,因此支付页为非全屏打开location.href = 'bd-triger://web?url='+encodeURIComponent(`${location.origin}/web/pay`)
- 情况3
此时页面在新的webView打开,双方约定的是否全屏参数值为ture,因此支付页为全屏打开location.href = 'bd-triger://web?url='+encodeURIComponent(`${location.origin}/web/pay?need_full_screen=1`)
- 情况1
- [2] 支付页面存在弹框提示“是否支付成功”,若是支付成功跳转到h5页面订单列表页面,此时的页面跳转也存在以下情况(情况1-3分别对应[1]中的1-3)
- 情况1
是否全屏只和当前webView的第一个页面有关,因此看page1(默认全屏),因为订单列表页也为全屏打开this.$router.push('/web/orderlist')
- 情况2
在新的webView打开的页面是否全屏和上一级无关,只看路径携带参数,路径没有携带参数,因此订单列表页为非全屏打开。location.href = `bd-triger://web?url=${location.origin}/web/orderlist`
- 情况3
在新的webView打开的页面是否全屏和上一级无关,只看路径携带参数,路径携带参数need_full_screen为true,因此订单列表页为全屏打开。location.href = 'bd-triger://web?url='+encodeURIComponent(`${location.origin}/web/orderlist?need_full_screen=1`)
- 情况2
- 情况1
总结
当使用bd协议去打开页面时若是设置全屏(非全屏),则在当前webview中再打开其他页面都是全屏(非全屏)直至再次使用bd协议在新的webView去打开页面。
返回
和跳转相同,返回在vue中也存在自己的方法this.$router.back()
在同一个webView内进行页面跳转
this.$router.push(url)
当点击返回按钮时可以使用自己的返回方法
this.$router.back()
当打开一个新的webView进行跳转时,可以想成重新打开一个浏览器页面进行跳转(a标签设置target属性值为_blank)。
location.href = bd-triger+ '://web?url=' + encodeURIComponent(url)
但是此时点击返回按钮不能使用back方法了(因为现在打开的是一个全新的窗口栈,里面没有之前的跳转记录)。
在返回时也不能判断前面有没有已经打开的webView,因此自己的返回方法在混合开发中不能使用
!
客户端获取的浏览栈记录是全的,只需要和客户端协商方法,当用户点击“返回”按钮时调用客户端方法
即可。
// 假定约定方法名为back
window.xxxJSBridge.back()
返回-禁止返回
用户可能通过 导航栏的返回按钮、左滑、手机下方的操作按钮返回到上一页面。
但是有时用户点击返回按钮,我们并不期望用户返回上一级而是期望挽留一下用户。
举例说明: 吸引用户到开通会员页面以99.9的价格开通会员,若是用户不想购买返回上一级,我希望在用户有“返回”这个动作时,给用户来一个挽留弹框,表示现在购买价格为89.9(以一个优惠的价格吸引用户购买)。
此时需要禁止返回操作功能并监听用户返回—>其实客户端可以监听用户退出页面的情况,无论用户以何种方式退出页面客户端都可以监听到并且可以禁止用户退出页面
!
示例
在当前页面做挽留弹窗
-
[1] 在进入页面时调用客户端方法,禁止用户退出页面;
-
[2] 在进入页面时监听用户退出操作,并挽留
- 当用户退出页面时就会被客户端监听到,客户端会调用H5页面的方法。
- H5页面监听此方法是否被调用,并作出操作。
-
[3] 当挽留次数为0时,调用客户端方法取消用户禁止返回操作并返回
-
准备:注册监听并挂在到全局
import Vue from 'vue' export default const wBus = new Vue()
Vue.prototype.wBus = wBus
暴露出去监听方法,方便客户端调用
function callback(data){ this.wBus.$emit('back', data) }
-
当前页面
// [1][2] created(){ // [1] start开始禁止返回 window.PlayLetJSBridge.start() // [2] 监听用户返回操作 this.wBus.$on('back', () => { this.onClickLeft() }) }
onClickLeft () { if(超过挽留次数){ // 结束禁止返回 window.PlayLetJSBridge.end() // 返回 window.PlayLetJSBridge.back() }else{ // 执行挽留逻辑 } }
-
打开app内的login页面(客户端页面)
error-没有结束禁止返回
需求
在当前页面page1无论以何种返回方式进行返回,都跳转至挽留页面(/retention),当在挽留页面点击返回时再返回上一级页面。
实现
- 当前页面
created(){ window.PlayLetJSBridge.start() this.wBus.$on('back', () => { this.onClickLeft() }) } onClickLeft () { this.$router.replace('/retention') // 替换掉当前栈 }
- 挽留页面
// 返回方法 onBack(){ window.PlayLetJSBridge.back() }
效果
进入page1,点击返回按钮进入挽留页面没有问题,但是在挽留页面点击返回按钮就会报错。
原因
在进入挽留页面时没有打开新的webView,在同一个webView中的状态相同!
,也就是说在page1页面禁止用户返回之后,在挽留页面的状态也是禁止用户返回的!
修改
onClickLeft () {
window.PlayLetJSBridge.end()
this.$router.replace('/retention') // 替换掉当前栈
}
返回-生命周期
如果是使用$router.push方法进行跳转,那么返回没有任何问题。
但是若是使用约定协议进行跳转,那么相当于当前页面没有被关闭
,当点击返回按钮时不会重新进行初始化
(不会走beforeCreate,created,beforeMount, mounted),当然重新跳转是没有问题的
生命周期-将h5页面作为客户端的tab页面
web页面作为客户端的tab页,切换tab的时候并不会销毁页面,也就是说再次打开页面的时候并不会走created生命周期函数(等同于上面的返回页面
),导致数据不刷新(不是最新状态)。此时可以走回调函数解决问题
- [1] 封装一个监听函数供客户端调用
import Vue from 'vue' export default const wBus = new Vue()
Vue.prototype.wBus = wBus
const fun = function(){ const _global = (function () { return window || this }()) const plugin = { function callback(data){ this.wBus.$emit('refresh', data) // 哪里使用去哪里出发,此处统一封装 } } for (const i in plugin) { !('jsFunc' in _global) && (_global[i] = plugin[i]) } }()
- [2] 在需要刷新的tab页面监听refresh方法
vm.$on('refresh', (data)=>{ if(data === 'refresh'){ // 重新调去接口 } })
error - 客户端调用h5页面的方法
温馨提示:可以先看[3]页面跳转-生命周期-将h5页面作为客户端的web页面
-
需求:将h5页面作为tab页面
-
期望:每次切换tab的时候能够重新刷新页面
-
实际:每次切换tab时页面都不会走H5的生命周期—也就无法刷新页面
切换tab是通过bd协议进行跳转-重新打开一个webView,当前页面不会销毁,再次打开也不会重新加载
类似与keep-alive组件—> 只有在第一次进入页面会走beforeCreate、created、beforeMount、mounted再次进入不会走这四个生命周期。
-
解决(期望1):希望客户端在进入页面时能够调用H5页面的方法,我在方法的里面去刷新页面。
客户端也不知道用户什么时候进入了页面
-
解决( 期望2):当前tab的数据只和另一个tab页面的按钮有关(相当于点击此按钮会修改当前页面的数据)—> 期望用户点击此按钮的时候去调用H5页面的方法。
------解决了
encodeUrlComponent
在进行跳转时,路径如下
const url ='xxx'
bd-triger://web?url=encodeUrlComponent(url)
发现url需要经过encodeUrlComponent编码。
最初我个人认为是由于 怕url中存在特殊字符 所以编码之后再进行传递,客户端解码之后再打开页面。
但是客户端开发告诉我他们根本没有解码。 what?
然后我发送了一版没编码的链接,在客户端也可以打开,但是页面会放大!
好吧。 还是老老实实编码之后再进行跳转吧~
[4] 在web页面下载app
下载地址是客户端给我们的(android与ios下载地址不同),因此在下载之前需要先判断浏览器的所属环境
if(navigator.userAgent.search(/(iphone|ipad|ipod)/i) >= 0){
// ios
location.href = 'xxx'
}else if(navigator.userAgent.search(/android/i) >= 0){
// Android
location.href='xxx.apk'
}else{
// 跳转到应用宝下载
}
[5]支付宝授权
若是某些场景下需要给用户打款,需要先申请支付宝授权。
场景
本页面为H5页面,当前页面中存在给用户打款的情况,于是后端返回支付宝授权链接url,我通过
location.href = url
跳转到支付宝进行授权,但是跳转到的页面如下
实际上在当前页面(h5页面)跳转到的支付宝授权页面同样为H5页面而非app内的支付宝授权页面,所以需要登录。
H5跳转H5,app跳转app
虽然这样做可以但是为了用户体验度最好是调用app方法,通过app跳转到支付宝app授权页面进行授权。
授权成功后支付宝会返回一个code,让客户端调用H5页面的方法将code返回给H5页面,H5页面内再做后续处理!