前端之H5与App交互总结
交互方式
前端通过将自身的方法挂载到window对象上,App端可以找到并异步回调,通过方法参数的形式将数据传到前端,挂载到window上的方法名字需要两端协议约定。
JS:
let token = '';
function foo(token_: string) {
token = token_;
}
//当App端调用getToken的时候触发绑定的foo方法
window.getToken = foo;
App:
// 伪代码
window.getToken('123abc');
封装—让页面更简洁、易维护
封装的目的:1、App有两种系统(IOS、Android),如果直接写在页面上,会使页面有很多冗余的判断。
if ( /(Android)/i.test(navigator.userAgent)) {
//Android
//...
} else if (/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)) {
//IOS
//...
}
所以我们单独将IOS和Android封装成两个独立的类,再通过工厂模式判断当前的系统类型,自动生产对应的类(以下代码统一用TS做演示)
TS:
class AppFactory {
static getUserInstance() {
//这里考虑到全局使用的情况
//所以采用单例的模式
if (this.isIos()) {
return IosUser.getInstance();
} else if(this.isAndroid()) {
return AndroidUser.getInstance();
} else {
return PcUser.getInstance();
}
}
private static isIos() {
return /(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)
}
private static isAndroid() {
return /(Android)/i.test(navigator.userAgent)
}
}
//现在,我们在页面上再也不用写if判断了
//只需要这样写就可以拿到当前的系统对应的类了
AppFactory.getUserInstance()
下面我们继续完善IOS类和Android类,我们需要遵循面向抽象类编程的思维,而不是具体的某一类,所以它们还需要一个公共的父类User。
TS:
//抽象父类
abstract class User {
protected some:string|null = null;
//通过init将协议约定的方法挂载到window上
//这里this指针会发生隐式绑定到window,需要使用bind强制绑定到自身
init() {
window.setSome = this.setSome.bind(this);
}
private setSome(some_: string) {
this.some = some_;
}
public getSome_() {
return this.some;
}
abstract applySome():void; //这是由H5主动调用App端的方法,由各自子类去实现
}
//Android类
class AndroidUser extends User{
//上面提到两个类会使用单例模式,以下不在赘述
private static androidUser: AndroidUser|null;
private constructor() {
super();
}
static getInstance() {
if (!this.androidUser) {
this.androidUser = new AndroidUser();
return this.androidUser;
}
return this.androidUser;
}
public applySome() {
//someFunc两端协议约定的方法名
window.someFunc.applySome()
}
}
//Ios类
class IosUser extends User {
private static iosUser: IosUser|null;
private constructor() {
super();
}
static getInstance() {
if (!this.iosUser) {
this.iosUser = new IosUser();
return this.iosUser;
}
return this.iosUser;
}
public applySome() {
//对比AndroidUser类的applySome方法
//可以看出调用App端方法时,两个系统的处理方式有差别
window.webkit.messageHandlers.applySome.postMessage(null)
}
}
//底层封装完毕后,在页面调用就会非常清晰、优雅
//例如我们需要拿some这个字段
AppFctory.getUserInstance.init(); //这一段代码在全局只需要初始化一次
let some = AppFctory.getUserInstance.getSome();
如何完美抓住异步调用的时机—发布订阅模式
上面的代码,已经可以支持我们优雅的在页面与App端进行交互,但是还存在一个严重的Bug,App端是异步调用我们的方法,按照上面的写法,App端还没调用我们的方法就进行赋值操作,这样是肯定拿不到值的。
如何解决呢?这里我使用的是发布订阅模式,当监听到App端调用我们的方法后,通知页面。
TS:
//首先我们需要一个调度中心的类 NotificationCenter
class NotificationCenter {
public eventId: symbol;
constuctor() {
this.eventId = Symbol('eventId');
}
public register<T extends Event>(observe: object|symbol, event: Class<T>, cb: ()=>void) {
let map = (<any>event).prototype[this.eventId]
= (<any>event).prototype[this.eventId] || new Map<object|symbol, ()=>void>();
map.set(observe, cb);
}
public notify(event: Event) {
let map: Map<object|symbol, () => void>
= (<any>event)[this.eventId] || new Map();
for (let [key, value] of map) {
value();
}
}
public unRegister(evemt: Event, observe: object|symbol) {
let map = Map<object|symbol, () => void>
= (<any>event)[this.eventId] || new Map();
if(!map.has(observe)) {
return;
}
map.delect(observe);
}
}
interface Class<T> {
prototype: T;
}
这里实现方式大家可以不用太在意细节,主要的思想还是运用发布订阅模式的注册和监听,将这个类挂到我们的User类上,在页面注册监听,当方法被App端调用后,在最后发起通知,这样就可以监听到方法被成功调用了。
TS:
class User {
public nc_: NotificationCenter = new NotificationCenter();
public setSome() {
//...
this.nc_.notify() //通知页面方法已经调用完毕
}
}
//页面
AppFactory.getUserInstance.nc_.register(..., () => {
//监听到变化后执行
let some = AppFactory.getUserInstance.getSome_();
})
拓展和总结
以上基本的架构就成型了,可能交互的时候会通过JSON的形式,那么就需要定义一个JSON类来处理格式等等,可以很好的拓展和集成。其实两端交互本质上很简单,但如果交互非常频繁就会导致页面冗余判断过多,难于维护,以上是我自己的一点经验和总结,也算是抛砖引玉,如有不足的地方,希望大家多多指正、评价。