动态卡片是以前端开发页面(视频放在客户端),内嵌在客户端的 webview 中的形式实现的,即混合开发。涉及到两大类技术:原生Native、Web H5
- 原生技术主要指iOS、Android,原生开发效率较低,两端同步,开发完成需要重新打包整个App,发布后依赖用户的更新,性能较高功能覆盖率更高,但是成本也更高
- Web H5主要由HTML、CSS、JavaScript组成(react、vue),Web可以更好的实现发布更新,跨平台也更加优秀,但性能较低,特性也受到限制
混合开发的意义就在于吸取两者的优点,原生为 h5 赋能。而且随着手机硬件的升级迭代、系统对于Web新特性的支持更好,H5的劣势被逐渐缩小 。日常开发中 h5 的改动较多,需求频繁,客户端相对改动较小,h5 打包发布可以与客户端分开独立运行,提升开发效率。
常见渲染形式
- 原生渲染的混合App(ReactNative、UniApp)
- 小程序
其中的原生、Web相互通信都离不开JSBridge,这里面小程序比较特殊,对于 UI 渲染和 JS 逻辑的执行环境做了隔离,纯 native 层做数据通信,网络请求,数据检测,这里先不进行讨论。
桥
在混合开发下,页面和客户端需要通信,例如 h5 页面需要获取通讯录,调用相机、蓝牙等功能;客户端执行完操作后,也需要通知前端页面执行回调操作,和js进行通信(js 运行在 webview 中或者 jsCore 中),实现两端的通讯方式就是 jsbridge。客户端和前端约定好协议,通过 jsbridge,前端可以调用客户端的接口,客户端也可以调用前端的方法。
介绍两种通过 webview 实现桥的方式
- 拦截 url原理
因为我们是在 webview 中发出的请求,而 weiview 是客户端创建的,所以客户端侧能监听到发出请求的具体路径,前端和 native 侧约定好协议名,比如:kuaishou://showToast?text=hello&callback=showToastCB,通过 iframe 发出请求时,拼接上字符串参数,客户端拿到数据执行原生操作,最后调用前端的回调函数,完成通信。
为什么使用 iframe 方式发送请求呢?
常见发送请求的方式
- a 标签
- location.href 跳转
- iframe.src 发送 ajax 请求
这些方法,a 标签需要用户主动操作,location.href 可能会引起页面的跳转丢失调用,发送 ajax 请求 Android 没有相应的拦截方法,所以使用 iframe.src 是经常会使用的方案
这种方式兼容性很好,但是由于是基于 URL 的方式,连接长度会受到限制,数据格式有限制,而且建立请求有时间耗时。
Android 的话,Webview 提供了 shouldOverrideUrlLoading 方法来提供给 Native 拦截 H5 发送的 URL Scheme 请求。代码如下:
public class CustomWebViewClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
// 拦截请求、接收 scheme
if (url.equals("xxx")) {
// handle
...
// callback
view.loadUrl("javascript:actionJSMethod(" + json + ");")
returntrue;
}
returnsuper.shouldOverrideUrlLoading(url);
}
}
iOS 的 WKWebview 可以根据拦截到的 URL Scheme 和对应的参数执行相关的操作。代码如下: ```objectivec
- (void)webView:(WKWebView )webView decidePolicyForNavigationAction:(WKNavigationAction )navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
// url 是否 xxx 开头
if ([navigationAction.request.URL.absoluteString hasPrefix:@”xxx”]) {
} decisionHandler(WKNavigationActionPolicyAllow); } ```[[UIApplication sharedApplication] openURL:navigationAction.request.URL];
- 全局注入
这种方式会通过 webView 提供的接口,App 将 Native 的相关接口注入到 JS 的 Context(window)的对象中,一般来说这个对象内的方法名与 Native 相关方法名是相同的,Web 端就可以直接在全局 window 下使用这个全局 JS 对象,进而调用原生端的方法。
js 调用安卓方法
Android 的 Webview 提供了 addJavascriptInterface 方法,支持 Android 4.2 及以上系统
// 安卓侧
// 注入全局 JS 对象 KwaiAd
webView.addJavascriptInterface(new KSAdJSBridge(this), "KwaiAd");
class KSAdJSBridge {
private Context ctx;
KSAdJSBridge(Context ctx) {
this.ctx = ctx;
}
// 增加JS调用接口
@JavascriptInterface
public void showToast(String text) {
new AlertDialog.Builder(ctx).setMessage(text).create().show();
}
}
// 前端侧调用
window.KwaiAd.showToast('hello'); // 参数可以穿个对象,告诉客户端回调函数的名字
安卓调用 js 方法
在 4.4 以前,通过 loadUrl 方法,执行一段 JS 代码来实现。在 4.4 以后,可以使用 evaluateJavascript 方法实现。loadUrl 方法使用起来方便简洁,但是效率低无法获得返回结果且调用的时候会刷新 WebView
1.evaluateJavascript 执行 JS 方法时,网页必须加载完毕
2.网页中的 JS 方法是全局方法
// 前端定义全局方法
function getGreetings(str) {
return str;
}
// 安卓侧
private void testEvaluateJavascript(WebView webView) {
webView.evaluateJavascript("javascript:getGreetings('"+"hello world!"+"')",newValueCallback() {
@Override
publicvoidonReceiveValue(String value) {
Log.i(LOGTAG,"onReceiveValue value="+ value);
}
});
}
从上面的用法中很明显看到,通过 evaluateJavascript 调用 JS 中的方法,
可以向其中添加结果回调,来接收 JS 的 return 值。
iOS 的 UIWebview 提供了 JavaScriptScore 方法,支持 iOS 7.0 及以上系统。WKWebview 提供了 window.webkit.messageHandlers 方法,支持 iOS 8.0 及以上系统。UIWebview 在几年前常用,目前已不常见。以下为创建 WKWebViewConfiguration 和 创建 WKWebView 示例:
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
WKPreferences *preferences = [WKPreferences new];
preferences.javaScriptCanOpenWindowsAutomatically = YES;
preferences.minimumFontSize = 40.0;
configuration.preferences = preferences;
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.webView.configuration.userContentController addScriptMessageHandler:self name:@"share"];
[self.webView.configuration.userContentController addScriptMessageHandler:self name:@"pickImage"];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"share"];
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"pickImage"];
}
// js 调用
window.webkit.messageHandlers.share.postMessage(xxx);
ios 调用 js
// 前端定义全局方法
function getGreetings(str) {
return str;
}
[jsContext evaluateJavaScript:@"getGreetings(111)"]
前端
- window.KwaiAd,获取全局的桥 JsBridge
- jsBridge 只执行 当前 handler 下的 action 函数,执行完成调用 callback 函数(这里只传给客户端一个函数名),回调绑定在了 window 上。(action 名在前端和客户端都维护了一套)
注意点
promise 时做了回收,每个桥只调用一次,每个模块使用的时候也都会重新 createBridge,避免 promise 一直内存占用