述
在混合应用开发中,一种常见且成熟的技术方案是将原生应用与 WebView 结合,使得复杂的业务逻辑可以通过网页技术实现。实现这种类型的混合应用时,就需要解决H5与Native之间的双向通信。JSBridge 是一种在混合应用中实现 Web 和原生代码之间通信的重要机制。
混合开发
混合开发(Hybrid)是一种开发模式,指使用多种开发模型开发App,通常会涉及到两大类技术:原生 Native、Web H5。
混合开发的意义就在于吸取两者的优点,而且随着手机硬件的升级迭代、系统(Android 5.0+、ISO 9.0+)对于Web特性的较好支持,H5的劣势被逐渐缩小。
JSBridge 的概念和作用
为什么在混合应用开发中 JSBridge 如此重要
JSBridge 做了什么
在Hybrid模式下,H5会需要使用Native的功能,比如打开二维码扫描、调用原生页面、获取用户信息等,同时Native也需要向Web端发送推送、更新状态等,而JavaScript是运行在单独的 JS Context 中(Webview容器)与原生有运行环境的隔离,所以需要有一种机制实现Native端和Web端的 双向通信 ,这就是JSBridge:以JavaScript引擎或Webview容器作为媒介,通过协定协议进行通信,实现Native端和Web端双向通信的一种机制。
通过JSBridge,Web端可以调用Native端的Java接口,同样Native端也可以通过JSBridge调用Web端的JavaScript接口,实现彼此的双向调用。
JSBridge 实现原理
把 Web 端和 Native 端的通信比作 Client/Server 模式。JSBridge 充当了类似于 HTTP 协议的角色,实现了 Web 端和 Native 端之间的通信。
将 Native 端原生接口封装成 JavaScript 接口:在 Native 端将需要被调用的原生功能封装成 JavaScript 接口,让 JavaScript 代码可以调用。JavaScript 接口会被注册到全局对象中,以供 JavaScript 代码调用。
将 Web 端 JavaScript 接口封装成原生接口:这一步是在 Web 端将需要被调用的 JavaScript 功能封装成原生接口。这些原生接口会通过 WebView 的某些机制暴露给原生代码,以供原生代码调用。
Native -> Web
Native端调用Web端,JavaScript作为解释性语言,最大的一个特性就是可以随时随地地通过解释器执行一段JS代码,所以可以将拼接的JavaScript代码字符串,传入JS解析器执行就可以,JS解析器在这里就是webView。
1. Android
Android 提供了 evaluateJavascript 来执行JS代码,并且可以获取返回值执行回调:
String jsCode = String.format("window.showWebDialog('%s')", text);
webView.evaluateJavascript(jsCode, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
}
});
2. IOS
IOS的 WKWebView 使用 evaluateJavaScript:
[webView evaluateJavaScript:@"执行的JS代码"
completionHandler:^(id _Nullable response, NSError * _Nullable error) {
//
}];
Web -> Native
Web调用Native端主要有两种方式:
1. URL Schema
URL Schema是类URL的一种请求格式,格式如下:
<protocol>://<host>/<path>?<qeury>#fragment
// 我们可以自定义JSBridge通信的URL Schema,比如:
hellobike://showToast?text=hello
Native加载WebView之后,Web发送的所有请求都会经过WebView组件,所以Native可以重写WebView里的方法,从来拦截Web发起的请求,我们对请求的格式进行判断:
例如:
get existOrderRedirect() {
let url: string;
if (this.env.isHelloBikeApp) {
url = 'hellobike://hellobike.com/xxxxx_xxx?from_type=xxxx&selected_tab=xxxxx';
} else if (this.env.isSFCApp) {
url = 'hellohitch://hellohitch.com/xxx/xxxx?bottomTab=xxxx';
}
return url;
}
这种方式从早期就存在,兼容性很好,但是由于是基于URL的方式,长度受到限制而且不太直观,数据格式有限制,而且建立请求有时间耗时。
2. 在Webview中注入JS API
通过webView提供的接口,App将Native的相关接口注入到JS的Context(window)的对象中
Web端就可以直接在全局 window 下使用这个暴露的全局JS对象,进而调用原生端的方法。
Android注入方法:
IOS注入方法:
例如:
// 注入全局JS对象
webView.addJavascriptInterface(new NativeBridge(this), "NativeBridge");
class NativeBridge {
private Context ctx;
NativeBridge(Context ctx) {
this.ctx = ctx;
}
// 绑定方法
@JavascriptInterface
public void showNativeDialog(String text) {
new AlertDialog.Builder(ctx).setMessage(text).create().show();
}
}
// 调用nativeBridge的方法
window.NativeBridge.showNativeDialog('hello');
H5具体实现
将功能抽象为一个 AppBridge 类,封装两个方法,处理交互和回调。
具体步骤:
具体实现代码:
// 定义一个名为 callNative 的方法,用于在 JavaScript 中调用原生方法
callNative<P, R>(classMap: string, method: string, params: P): Promise<R> {
return new Promise<R>((resolve, reject) => {
// 生成一个唯一的回调 ID
const id = v4();
// 将当前的回调函数保存到 __callbacks 对象中,以 callbackId 作为键
this.__callbacks[id] = { resolve, reject, method: `${classMap} - ${method}` };
// 构造通信数据,包括原生类映射、要调用的方法、参数和 callbackId
const data = {
classMap,
method,
params: params === null ? '' : JSON.stringify(params),
callbackId: id,
};
const dataStr = JSON.stringify(data);
// 根据当前环境判断是 iOS 还是 Android,并调用相应平台的原生方法
if (this.env.isIOS && isFunction(window?.webkit?.messageHandlers?.callNative?.postMessage)) {
// 如果是 iOS 平台,则调用 iOS 的原生方法
window.webkit.messageHandlers.callNative.postMessage(dataStr);
} else if (this.env.isAndroid && isFunction(window?.AppFunctions?.callNative)) {
// 如果是 Android 平台,则调用 Android 的原生方法
window.AppFunctions.callNative(dataStr);
}
});
}
// 初始化桥接回调函数,该参数在 constructor 中调用
private initBridgeCallback() {
// 保存旧的回调函数到 oldCallback 变量中
const oldCallback = window.callBack;
// 重新定义 window.callBack 方法,用于处理原生应用的回调数据
window.callBack = (data) => {
// 如果存在旧的回调函数,则调用旧的回调函数
if (isFunction(oldCallback)) {
oldCallback(data);
}
// 获取原生应用的回调信息,包括数据和回调 ID
console.info('native callback', data, data.callbackId);
// 从回调数据中获取回调 ID
const { callbackId } = data;
// 根据回调 ID 查找对应的回调函数
const callback = this.__callbacks[callbackId];
// 如果找到了对应的回调函数
if (callback) {
// 如果回调数据中的 code 为 0,则表示执行成功,调用 resolve 方法处理成功的结果
if (data.code === 0) {
callback.resolve(data.data);
} else {
// 否则,表示执行失败,构造一个错误对象并调用 reject 方法处理错误信息
const error = new Error(data.msg) as Error & {response:unknown};
error.response = data;
callback.reject(error);
}
// 删除已经处理过的回调函数
delete this.__callbacks[callbackId];
}
};
}
// 调用原生方法的封装函数
callNative<P, R>(classMap: string, method: string, params: P) {
// 从容器中解析出 AppBridge 实例
const bridge = container.resolve<AppBridge>(AppBridge);
// 使用 bind 方法将 AppBridge 实例中的 callNative 方法绑定到 bridge 对象上,并保存到 func 变量中
const func = bridge.callNative.bind(bridge);
// 调用 func 方法,并传入 classMap、method 和 params 参数,实现调用原生方法的功能
return func<P, R>(classMap, method, params);
}
// 打开 webview
// 调用 callNative 方法,传入参数 url,classMap 为 'xxxxx/hitch',method 为 'openWebview'
openWebView(url: string): Promise<void> {
return this.callNative<{url:string}, void>('xxxxx/hitch', 'openWebview', { url });
}
// 获取驾驶证 OCR 信息
getDriverLicenseOcrInfo(
params: HBNative.getDriverLicenseOcrInfo.Params,
): Promise<HBNative.getDriverLicenseOcrInfo.Result> {
// 调用 callNative 方法,传入参数 params,classMap 为 'xxxxx/hitch',method 为 'getOcrInfo'
// 返回一个 Promise 对象,该 Promise 对象用于处理异步结果
return this.callNative<
HBNative.getDriverLicenseOcrInfo.Params,
HBNative.getDriverLicenseOcrInfo.Result>(
'xxxxx/hitch', 'getOcrInfo', params,
);
}
作者:佟健
来源-微信公众号:哈啰技术
出处:https://mp.weixin.qq.com/s/h6vlkf5rgyI9GpEpPxO9cg
素显示模式就是元素(标签)以什么方式进行显示,比如div标签自己占一行,一行可以放多个span标签。
HTML 元素分为块元素、行内元素、行内块元素,它们具体的用法和区别如下:
块元素是用于定义一个独立的区域,比如一个段落、一个标题、一个表格等。
常见的块元素有:h1-h6、div(最典型)、p、ol、ul、li等。
特点
行内元素是用于在同一行内显示文本,并不需要包含在其他元素内。
常用的行内元素有:a、strong、em、del、ins、b、i、u、span(最典型)等。
特点:
注意:
行内块元素是用于在同一行内显示文本,并不需要包含在其他元素内,但是可以包含图片或其他多个文本元素。
常用的行内块元素有:
特点:
在行内元素中有几个特殊的标签,img、input、td,它们同时具有行内元素和块元素的特点。
1.块元素定义了一个独立的区域,不会和其他元素融合在一起,而行内元素和行内块元素都属于行内元素,它们都可以包含其他元素。
2.块元素可以设置高度和外边距,而行内元素和行内块元素都不能设置高度和外边距。
3.块元素只能包含文本或其他图片等元素,而行内元素和行内块元素都可以包含其他任意类型的元素。
4.块元素的外边距、填充、宽度等属性可以根据实际需求设置,而行内元素和行内块元素的样式则完全根据实际需求设置。
特殊情况下,我们需要元素模式的转换,比如要增加a标签链接的触发范围。
今开发出一款成功的APP已经成为公司运营的重要一环,但APP的开发模式很多,不同模式的实现机理不同,因而会在APP的开发成本、运行性能、升级维护和用户体验等方面造成不同的影响。
目前来说,APP的主要开发模式主要分为四类:
第一类是原生应用开发,即NativeApp。基于Android平台的Java语言开发和基于iOS平台的Objective-C语言/Swift语言开发。
第二类是移动网页应用开发,即WebApp。网页应用开发是利用Web技术,使用HTML、CSS和JavaScript开发用于移动端显示的网页。
第三类是把NativeApp和WebApp结合的混合开发模式,即HybridApp。
第四类是基于ReactNative框架的JavaScript语言开发的类原生应用,即ReactNativeApp。
下面主要是对比较常用到的NativeApp开发模式做一个具体的分析,希望有助于大家对于APP开发有一个了解!
NativeApp是本地开发方式,基于手机操作系统进行开发,利用Java、Objective-C或Swift语言进行程序开发,然后编译成字节码或机器码后经操作系统调度运行。
由于操作系统不同以及开发语言的不同,当今最流行的两大移动端平台Android和iOS各有自己的一套独立的开发模式,两大平台差异较大。
现以iOS平台为例简述下NativeApp的开发模式。
首先需要在MacOSX系统上安装苹果公司开发的IDE――Xcode。Xcode内拥有开发iOSNativeApp快捷高效的CocoaTouch框架,是开发原生iOS程序的不二选择。
在Xcode中创建一个SingleViewApplication工程后选用Swift语言开发,会自动生成以Main.storyboard、AppDelegate.swift、ViewController.swift开发核心的若干文件。其中Main.storyboard用于构建各个页面之间跳转关系和具体页面布局,在Xcode的右下角有可拖拽的若干控件,拖拽控件到Main.storyboard后结合AutoLayout为各个控件添加各种布局约束,来保证界面在不同大小的屏幕上的适配。
AppDelegate.swift是为iOS程序做一些初始化设置,主要用于在APP启动时为重要的数据结构进行初始化,以及响应APP运行时事件,如程序启动、程序运行内存不足、程序切换等。程序员可在AppDelegate.swift中的相关函数里对事件响应进行操作。
ViewController.swift就是iOS程序中的初始界面,是UI控件和程序逻辑的控制器。程序员主要通过ViewController.swift中的viewDidLoad和viewWillAppear等方法对iOS程序的数据层和视图层进行控制,来表达APP的业务逻辑。
NativeApp开发模式的优势在于NativeApp是编译后的文件,执行速度快,界面动画十分流畅,对网络的依赖性小,用户体验很好。基于平台层可以非常方便地调用操作系统提供的各种功能,如调用摄像头、推送信息和读取本地通讯录等。
NativeApp开发模式的劣势主要是:开发周期长,两套独立的知识体系复杂且学习成本高,依赖操作系统而无法进行跨平台开发,APP版本升级繁琐需要重新把源文件编译打包再由用户下载覆盖安装。
移动互联网的浪潮,推动着移动开发技术的不断发展,移动App的开发模式也丰富了起来。每个开发模式都有自身的优势与不足,在实际开发前应仔细权衡开发人员的知识体系和开发成本。
延伸阅读
————————
“广州优匠科技”是一家由经验丰富的技术设计开发团队创办的软件外包公司。专注于小程序、公众号、APP、软件系统等相关技术的定制与开发。
优匠科技深耕在互联⽹服务领域多年,有足够的成功案例与资深专业的技术团队,能够帮助企业解决很多技术上的难题。多年来一直都是用“匠心精神”去做好技术和产品服务。
如有兴趣合作或是产品开发需求的朋友,欢迎前来咨询了解!!
*请认真填写需求信息,我们会在24小时内与您取得联系。