前端之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類來處理格式等等,可以很好的拓展和集成。其實兩端交互本質上很簡單,但如果交互非常頻繁就會導致頁面冗餘判斷過多,難於維護,以上是我自己的一點經驗和總結,也算是拋磚引玉,如有不足的地方,希望大家多多指正、評價。