- 本文所有代码案例均基于Api12。
对于已经上线的项目,和前端的JS交互往往成为兼容鸿蒙端的绊脚石。如果已经依赖了三方框架,好多人选择等已经使用的库推出支持鸿蒙端的方案再进入开发。
dsbridge作为一个轻量化的交互库,粉丝不少,我们在开发初期也遇到了让web端兼容的难题,现有的交互重度依赖dsbridge,让web端交互无缝过渡到鸿蒙成了一个关键节点。
经过研究,我们决定重写一个鸿蒙版本。我维护过两年手机浏览器项目,承担了这项工作的主要任务。
任务分成三个阶段:
1、dsbridge库改写成Ark TS版本
参照iOS版本转写成的鸿蒙代码,iOS版本的仓库在github上,直接clone到本地即可
转写后的代码会放在文末。
方案选择上,我选择了侵入式设计,让DsbridgeController继承了webview.WebviewController。也尝试过单独起一个控制器的方案,发现没必要。web组件已经提供了Controller的绑定,我们在实际项目中所有web都是一套的,也不会再单独给Controller添加别的功能,如果有特殊需要,再单独适配即可。
2、客户端已有的交互方法预埋
参照dsbridge自带的检测模型InternalApis,自己埋监听对象的时候,也一样,注入对象时,把"_dsb"换成自己的命名空间即可。
进入页面后,webController?.creat() 方法必须要调用,才可以注册自己实现的交互对象
aboutToAppear(): void {
// 添加监听
this.webController?.creat()
this.webController?.addJavascriptObject(this.jsObject, "xxxx")
}
对象的类命名在本地可以任意,命名空间"xxxx"需要跟web端约定一致。
3、web获取鸿蒙端的识别标记
dsbridge分为web端和客户端,客户端已经接入dsbridge的消息,需要让web端识别到,查看web端源码,他们在UA中额外拼接了"_dsbridge"字段,客户端也要实现这个方案,在web组件绑定好Controller后,需要及时将UA更新:
// web页面
Web({ src: this.urlPath, controller: this.webController })
.width(FULL_WIDTH)
.height(FULL_HEIGHT)
.onControllerAttached(() => {
// 需要在这里注入UA
this.webController?.insertWindowInfo()
})
insertWindowInfo方法内容如下,这个方法写在文末的源码里
/**
* 给web追加UA的方法
*/
insertWindowInfo() {
// 获取UA信息
let uaStr = this.getUserAgent()
if (uaStr.includes(" _dsbridge")) {
//如果已经包含,避免重复注入
return
}
// 修改UA信息
this.setCustomUserAgent(uaStr + " _dsbridge")
}
还有一些额外要注意的,针对web容器,还需要额外补充如下代码:
.onAppear(() => {
if (this.webController != undefined) {
this.webController.isBindUI = true
}
})
.onDisAppear(() => {
if (this.webController != undefined) {
this.webController.isBindUI = false
}
// 销毁页面时需要清空timer,未清空可能会引起闪退
this.webController?.clearTimer()
this.webController = undefined
})
这段代码是因为有timer延迟,会导致崩溃,额外加注的。
末尾:dsbridge类转写后的代码:
参照iOS版本转写成的鸿蒙代码
disableJavascriptDialogBlock因为没使用,所以就没实现
额外加了参数isBindUI和timerArray,加了方法clearTimer方法,离开页面后,dsbridgeController对象因为有延迟回调,加了这个处理方案。
dsbridge主要是用来数据交互,非代码注入工具,我们在项目中发生过严重卡顿,有一个交互方法通过dsbridge,向web注入长达30M的JSON,卡顿数秒,最后排查只能拆分处理。
import webview from '@ohos.web.webview'
export enum DSB_API {
HASNATIVEMETHOD,
CLOSEPAGE,
RETURNVALUE,
DSINIT,
DISABLESAFETYALERTBOX
}
export class DSCallInfo {
method?: string
id?: number
args?: Object[]
}
export class JSBUtil {
static parseNamespace(method: string): string[] {
let arr = method.split(".")
if (arr.length > 1) {
return [arr[0], arr[1]]
} else {
return ["", method]
}
}
}
// 注入的元交互模型
class InternalApis {
webController?: DsbridgeController
/**
* 某个方法是否可执行
* @param args
* @returns
*/
hasNativeMethod(args: Record): Object | undefined {
return this.webController?.onMessage(args, DSB_API.HASNATIVEMETHOD) ?? undefined
}
/**
* 关闭页面闭包,愿意的话自己实现闭包,不实现也没关系
* @param args 只有 disable一个参数 bool类型
* @returns
*/
closePage(args: Record): Object | undefined {
return this.webController?.onMessage(args, DSB_API.CLOSEPAGE) ?? undefined
}
/**
* 通用的交互,包括调用某对象的方法,传参,返回结果等
* @param args
* @returns
*/
returnValue(args: Record): Object | undefined {
return this.webController?.onMessage(args, DSB_API.RETURNVALUE) ?? undefined
}
/**
* 初始化完成后的交互,刚进入页面时会直接调用元模型的对象,然后触发客户端调用JS的未执行队列
* @param args
* @returns
*/
dsinit(args: Record): Object | undefined {
return this.webController?.onMessage(args, DSB_API.DSINIT) ?? undefined
}
/**
* 弹窗模态设置,暂时不做拦截,web那边自己实现
* @param args
* @returns
*/
disableJavascriptDialogBlock(args: Record): Object | undefined {
return this.webController?.onMessage(args, DSB_API.DISABLESAFETYALERTBOX) ?? undefined
}
}
export class DsbridgeController extends webview.WebviewController {
// 持有关闭web窗口的实现闭包
private javascriptCloseWindowListener: (() => void) | undefined = undefined
// 回调闭包的自增key,方便查找
private callId: number = 0
// JS命名空间对象存储字典
private javaScriptNamespaceInterfaces: Map = new Map()
// 闭包的回调暂存数组
private handerMap: Map = new Map()
// 原生调用JS的模型暂存数组,在web加载初始化之前,需要先暂存到这里,初始化完再调用
private callInfoList: DSCallInfo[] | undefined = []
jsCache: string = ""
lastCallTime: number = 0
isPending: boolean = false
// 存储timer的数组,离开页面时需要清空
timerArray: number[] = []
isBindUI: boolean = false
/**
* 离开页面时需要清空timer,未清空可能会引起闪退
*/
clearTimer() {
this.timerArray.forEach((timer) => {
clearTimeout(timer)
})
}
// 创建初始化交互
creat() {
// add internal Javascript Object
let interalApis = new InternalApis()
interalApis.webController = this
this.addJavascriptObject(interalApis, "_dsb")
}
/**
* 给web添加必要的注入信息
*/
insertWindowInfo() {
// 获取UA信息
let uaStr = this.getUserAgent()
if (uaStr.includes(" _dsbridge")) {
//如果已经包含,避免重复注入
return
}
// 修改UA信息
this.setCustomUserAgent(uaStr + " _dsbridge")
}
// 原生调用js
public callHandler(
methodName: string,
args?: Object[],
completionHandler?: (value: Object) => void
) {
// 将交互事件转换成model
let callInfo: DSCallInfo = new DSCallInfo()
callInfo.id = this.callId++
callInfo.args = args == undefined ? [] : args
callInfo.method = methodName
if (completionHandler != undefined) {
this.handerMap.set("" + callInfo.id, completionHandler)
}
if (this.callInfoList != undefined) {
this.callInfoList.push(callInfo)
} else {
this.dispatchJavascriptCall(callInfo)
}
}
// 设置JS调用关闭当前web的闭包
public setJavascriptCloseWindowListener(callback: () => void) {
this.javascriptCloseWindowListener = callback
}
/**
* 添加命名空间的监听和对象
* @param object
* which implemented the javascript interfaces
* @param namespace
* if empty, the object have no namespace.
**/
public addJavascriptObject(object?: Object, namespace?: string) {
if (namespace == undefined) {
namespace = ""
}
if (object != undefined) {
this.javaScriptNamespaceInterfaces.set(namespace, object)
}
}
// 删除命名空间的监听
public removeJavascriptObject(namespace: string) {
if (namespace == undefined) {
namespace = ""
}
this.javaScriptNamespaceInterfaces.delete(namespace)
}
// 检查JS方法是否可用
public hasJavascriptMethod(handlerName: string, callback: (exist: boolean) => void) {
this.callHandler(
"_hasJavascriptMethod",
[handlerName],
(value: Object) => {
callback(value > 0)
}
)
}
// 判断方法是否可用
hasNativeMethod(args: Record): boolean {
let argsname: string = args["name"] as string ?? ""
let nameStr: string[] = JSBUtil.parseNamespace(argsname.trim())
let JavascriptInterfaceObject: ESObject = undefined
if (this.javaScriptNamespaceInterfaces.has(nameStr[0])) {
JavascriptInterfaceObject = this.javaScriptNamespaceInterfaces.get(nameStr[0])
}
if (JavascriptInterfaceObject != undefined) {
let funcname: string = nameStr[1] ?? ""
let has: boolean = JavascriptInterfaceObject[funcname] != undefined
return has
}
return false
}
// _dsb对象的消息转发方法
public onMessage(msg: Record, type: DSB_API): Object | undefined {
let ret: Object | undefined = undefined
switch (type) {
case DSB_API.HASNATIVEMETHOD:
ret = this.hasNativeMethod(msg) ? 1 : 0
break
case DSB_API.CLOSEPAGE:
this.closePage(msg)
break
case DSB_API.RETURNVALUE:
ret = this.returnValue(msg)
break
case DSB_API.DSINIT:
ret = this.dsinit(msg)
break
case DSB_API.DISABLESAFETYALERTBOX:
let disable: boolean = msg["disable"] as boolean ?? false
this.disableJavascriptDialogBlock(disable)
break
default:
break
}
return ret
}
public disableJavascriptDialogBlock(disable: boolean) {
}
// 注册交互,并且把注册过的数组缓存清空
dispatchStartupQueue() {
if (this.callInfoList == undefined) {
return;
}
for (let i = 0; i < this.callinfolist.length i this.dispatchjavascriptcallthis.callinfolisti this.callinfolist='undefined' _dsbridge=',包含就需要拦截,不包含就当成普通弹窗事件处理' canopendsbridgeprompt: string: boolean return prompt.includes_dsbridge=') } // JS调用原生解析的核心方法 call(prompt: string, argStr: string): string { console.log(' prompt:='=='>"+prompt +",==>"+argStr)
let method: string = prompt.slice(10)
let nameStr: string[] = JSBUtil.parseNamespace(method.trim())
let jsInterfaceObject: ESObject = this.javaScriptNamespaceInterfaces.get(nameStr[0])
let result: Record = {
"code": -1,
"data": ""
}
if (jsInterfaceObject == undefined) {
console.info("桥接方法已通过,但是没找到命名空间的对象, 需要排查注入代码")
} else {
// 参数
let args: Record = JSON.parse(argStr)
method = nameStr[1]
let res: Object | undefined = undefined
let arg: Object | undefined = args["data"]
if (args != undefined && args["_dscbstub"] != undefined) {
let cb: string = args["_dscbstub"] as string ?? ""
// 如果包含 _dscbstub ,就需要生成闭包
let completionHandler = (value: Object, complete: boolean) => {
let del: string = ""
result["code"] = 0
if (value != undefined) {
result["data"] = value
}
let resultvalue = encodeURIComponent(JSON.stringify(result)) ?? ""
if (complete) {
del = "delete window." + cb
}
let js = "try {"
+ cb
+ "(JSON.parse(decodeURIComponent(\""
+ resultvalue
+ "\")).data);"
+ del
+ "; } catch(e){};"
let t: number = Number(new Date())
this.jsCache = this.jsCache + js
if (t - this.lastCallTime < 50 if this.ispending this.evaljavascript50 this.ispending='true' else this.evaljavascript0 if jsinterfaceobjectnamestr1 res='jsInterfaceObject[method](arg,' completionhandler else jsinterfaceobjectnamestr1 log if jsinterfaceobjectnamestr1 let res: object='jsInterfaceObject[method](arg)' resultcode='0' if res resultdata='res' return json.stringifyresult evaljavascriptdelay: number let timer='setTimeout(()'> {
if (this.jsCache.length != 0 && this.isBindUI) {
this.runJavaScript(this.jsCache)
this.isPending = false
this.jsCache = ""
this.lastCallTime = Number(new Date())
}
}, delay * 1000)
this.timerArray.push(timer)
}
// _dsb对象使用
private closePage(args: Record): Record | undefined {
if (this.javascriptCloseWindowListener != undefined) {
this.javascriptCloseWindowListener()
}
return undefined
}
// _dsb对象使用
private returnValue(args: Record): Record | undefined {
let completionHandler: ((obj: Object) => void) | undefined = undefined
if (this.handerMap.has(args["id"] as string)) {
completionHandler = this.handerMap.get(args["id"] as string) as ((obj: Object) => void)
}
if (completionHandler != undefined) {
completionHandler(args["data"])
let complete: boolean = args["complete"] as boolean
if (complete) {
this.handerMap.delete(args["id"] as string)
}
}
return undefined
}
// _dsb对象使用
private dsinit(args: Record): undefined {
this.dispatchStartupQueue()
return undefined
}
// 将消息模型转发成可解析的js字符串到web
private dispatchJavascriptCall(info: DSCallInfo) {
let map: Record = {}
map["method"] = info.method ?? ""
map["callbackId"] = info.id ?? 0
map["data"] = JSON.stringify(info.args)
// 生成json串
let json = JSON.stringify(map)
// 调用方法
this.runJavaScript("window._handleMessageFromNative(" + json + ")")
}
}