H5 与App的相互调用/传值
h5 与 App 交互有以下几种方式:
方法一:URL 传参
App 通过 URL 传递参数,例如: https://xxxx?id=123
。
这种方式有很大的局限,比如:
- URL 的长度有最大限制:在传递参数的时候,需要保证 URL 的长度不能超过最大限制。
- 考虑编码:在传递参数的时候,需要考虑编码的问题,例如在传递中文时需要对参数进行 URL 编码
- 单向:只适合 App 向 H5 传值
- 不能定制:App 只能将 H5 所有可能需要用到的参数传递过去,不能按需传递
方法二:拦截 URL Schemes
H5 和 App 首先约定一个特定的 URL Schemes,然后 App 将 H5 中约定的 URL Schemes 进行拦截。
例如:约定的 URL Schemes 为 zoneyet
,当 H5 向 App 传值时,H5 向 zoneyet://jumpToHomePage?id=123
地址进行跳转,App 将 URL Schemes 为 zoneyet
的地址进行拦截,然后分析其中的 URL 和传递的参数。
可以实现的功能:
- H5 向 App 传递参数,App 接收后进行处理或页面跳转
前提条件:
- App 和 H5 需要提前约定好 URL Schemes,URL 地址,参数名称,然后 App 才能对指定的 URL Schemes 进行拦截
局限:只能单向传值,即只能 H5 向 App 传递参数。所以一般用于 H5 控制 App 的跳转
方法三:JavaScriptBridge
使用 WebViewJavaScriptBridge(Start 13.5K), DSBridge(Android(Start 2.9K),iOS(Start 1.5K) ) 可以实现 H5 与 App 的双向调用和双向传值。
可以实现下面的功能:
- H5 调用 App 的方法,可以用于实现:
- H5 向 App 传递参数
- H5 控制 App 从一个页面跳转到另一个页面
- App 调用 H5 的方法,可以用于实现:
- App 修改 H5 的参数
- App 控制 H5 的逻辑
- H5 调用 App 的方法,App 处理结束后,将结果传递给 H5,可以用于实现:
- H5 将复杂的运算交给 App 进行处理,然后将处理结果传递给 H5
- H5 将网络请求的参数交给 App,然后将请求结果传递给 H5
前提条件:
- 在使用 WebViewJavaScriptBridge 时,H5 与 App 需要提前约定好相互调用的方法名称(和参数名称),然后才可以相互调用。
实现原理:
(1)App 调用 H5 的方法时,首先将传递的数据转换成一个字符串 messageJSON
,
// message 为 App 向 H5 传递的一个字典数据
- (NSString *)_serializeMessage:(id)message pretty:(BOOL)pretty{
return [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:message options:(NSJSONWritingOptions)(pretty ? NSJSONWritingPrettyPrinted : 0) error:nil] encoding:NSUTF8StringEncoding];
}
NSString *messageJSON = [self _serializeMessage:message pretty:NO];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\'" withString:@"\\\'"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2028" withString:@"\\u2028"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2029" withString:@"\\u2029"];
然后在 WebView 执行这段字符串
NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
[_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
(2)H5 调用 App 的方法的实现原理是:在 WKWebView 跳转代理中,判断是不是特定的 url(https://__wvjb_queue_message__
),如果是的话,将其拦截,然后从特定的地方获取 H5 调用的方法名和参数,再交给 App 进行处理。
// WKWebView 的代理
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
if (webView != _webView) { return; }
NSURL *url = navigationAction.request.URL;
if ([_base isWebViewJavascriptBridgeURL:url]) {
if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
} else if ([_base isQueueMessageURL:url]) {
[self WKFlushMessageQueue];
} else {
[_base logUnkownMessage:url];
}
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
// *** 删除了非核心代码 **
}
// _base 中判断是否拦截 url 的方法
- (BOOL)isWebViewJavascriptBridgeURL:(NSURL*)url {
if (![self isSchemeMatch:url]) {
return NO;
}
return [self isBridgeLoadedURL:url] || [self isQueueMessageURL:url];
}
- (BOOL)isSchemeMatch:(NSURL*)url {
NSString* scheme = url.scheme.lowercaseString;
return [scheme isEqualToString:kNewProtocolScheme] || [scheme isEqualToString:kOldProtocolScheme];
}
- (BOOL)isQueueMessageURL:(NSURL*)url {
NSString* host = url.host.lowercaseString;
return [self isSchemeMatch:url] && [host isEqualToString:kQueueHasMessage];
}
- (BOOL)isBridgeLoadedURL:(NSURL*)url {
NSString* host = url.host.lowercaseString;
return [self isSchemeMatch:url] && [host isEqualToString:kBridgeLoaded];
}
方法四:JSCore
在iOS 7之后,苹果将 JSCore 作为一个系统级 Framework 提供给开发者。可以通过 JSCore 传递参数。可以通过 深入理解JSCore 了解技术原理。
实现方法:
H5:
function scan() {
// 判断iOS 、Android
const isAndroid = navigator.userAgent.indexOf('Android') > -1 || navigator.userAgent.indexOf('Linux') > -1;
const isIOS = !!navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/);
// 传递的数据
data = {'id' : 123}
if (isAndroid) {
// 'scan'为标志符 app定义
window.WebViewJavascriptBridge.callHandler('scan', data,
function(responseData) {});
} else if (isIOS) {
// iOS 如果不需要传值 则data要传null
window.webkit.messageHandlers.scan.postMessage(data)
}
}
iOS 定义 JSCoderViewController 并实现 WKScriptMessageHandler 协议:
enum ScriptMessageName {
case scan //
var name: String {
switch self {
case .scan:
return "scan"
}
}
}
class JSCoderViewController: UIViewController {
// 定义 WKWebView
lazy var webview: WKWebView = {
let configuration = WKWebViewConfiguration()
let userContentController = WKUserContentController()
userContentController.add(self, name: ScriptMessageName.scan.name)
configuration.userContentController = userContentController
return WKWebView(frame: .zero, configuration: configuration)
}()
// ....
}
extension JSCoderViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
switch message.name {
case ScriptMessageName.scan.name:
// 参数从 message.body 中获取
default:
debugPrint("undefined message name:\(message.name)")
}
}
}
提升 H5 的加载速度
方法一:对 H5 中的静态资源进行缓存
在用户打开 H5 页面之后,App 对 H5 中需要加载的静态资源进行缓存,比如:对 js、css、image 资源进行缓存。
实现方案有两种:
- App 利用 WebView 控件的缓存策略,对 H5 中的资源进行缓存,下次打开的时候,从缓存中读取数据。
- App 拦截 WebView 的网络请求,自己实现一套缓存机制
缓存策略为:App 将资源的 url 地址作为资源的唯一标识,对资源进行缓存。当 HTML 中引用的资源发生变动时,需要保证在 HTML 中的引用地址需要发生变化,比如 url 变更或者 url 中的参数变更。App 发现本地没有对应的资源时,需要再次缓存。
方法二: 预加载
对用户将来可能会访问到的 H5 页面,App 可以对 H5 中的资源进行预加载和缓存,当用户访问 H5 页面的时候,App 从本地加载 HTML 和相关资源的数据,然后进行显示,从而提高打开速度。
实现步骤:
- App 提前请求 H5 页面数据,获取对应的 HTML 文件,然后提取 HTML 中的静态资源(js,css, image),然后进行缓存。
- 当 WebView 加载 H5 时,App 拦截资源的请求,从本地进行加载
方法三:资源的打包下载
将用户将来会访问的 H5 资源进行打包(打包为 .zip 文件),实现版本控制,App 对资源进行下载、解压,放在缓存目录当中。当用户访问时,从本地加载对应的资源。当 H5 资源发生变更后,App 根据版本实现增量更新。
实现步骤:
- 将 H5 资源进行整体打包,实现版本控制
- App 启动后,查看是否有需要下载的 H5 资源,
- 如果有需要下载的资源,则首先查看 App 当前是否有缓存对应的资源,
- 如果没有缓存,请求资源时,不携带缓存版本号,服务器返回全量更新的资源和对应的版本号,App 缓存进行
- 如果有缓存,请求资源时,携带缓存的版本号,服务器返回增量更新的资源,App 对之前缓存的资源进行增加、替换,并保存当前版本号、
方法四:动态资源的处理
对 H5 中需要使用的动态数据,比如:H5 中的列表数据,可以使用下面的方法进行加速:
- H5 将通过网络请求到的动态数据传递给 App,App 进行缓存处理。下次打开 H5 时就可以从 App 的缓存中读取数据
- H5 将网络请求的交给 App,App 请求结束后,将结果返回给 H5。
原生应用中动态页面
在运营过程中,App 的一些活动页面需要在不发新版本的情况下,实现动态变动,比如:动态的弹窗页面,活动提醒页面。
为了实现这种需求,可以通过下面的方式进行实现:
通过 WebView 加载动态链接
动态的内容可以通过 WebView 进行动态展示。
实现方案:具体的展示效果通过 H5 完成,App 通过 WebView 加载动态的 URL 进行展示。
预设模板
在 App 中预设几种动态模板,在需要暂时动态内容时,从预设模板中选择一个模板,然后设置相关数据。
限制:必须先在 App 中完成可能会用到的模板,然后从中选择相应的模板,进行展示。
富文本
富文本中可以加载图片,文字,按钮,可以通过富文本来定制动态的 UI。
可以使用的富文本框架 YYKit
HTML 转原生页面
动态页面的样式由 HTML 设计完成,在需要展示动态的页面时,App 从 H5 或者服务获取 HTML、css 数据,App 将 HTML 数据翻译成原生 UI 进行展示。
实现步骤:
- App 将 HTML 文本翻译成 dom 树
- App 通过 CSS 设置 dom 树的属性
- App 通过 dom 树的信息构建原生 UI,然后进行展示
可以使用的布局框架 Texture(原名:AsyncDisplayKit)
JSON 配置原生页面
使用 JSON 来描述 UI 的样式,在需要展示动态的页面时,App 从 H5 或者服务获取 JSON 数据,App 将 JSON 数据翻译成原生 UI 进行展示。
实现步骤:
- 通过约定好的 JSON 格式,来配置需要展示的 UI
- App 通过解析 JSON 格式,来绘制原生的 UI