混合开发是一种开发模式,混合了原生技术和 Web 技术进行开发的移动应用。
现在的混合开发方案有很多:
- 基于WebView UI(JSBridge)的方案,这种方案是最初也是最核心的一种解决方案,主要是通过JSBridge来完成web端和native端的通信,从而赋予web端原生能力。
- 基于Native UI(ReactNative、Week)的方案,这种方案是在赋予了web原生能力基础之上进一步的通过JSBridge将JS解析成虚拟节点树,传递到native端,并使用原生进行渲染的一种解决方案
- 小程序方案,本质也是基于JSBridge进行实现的,只不过对JSBridge进行了更细致化的定制,并且隔离了JS逻辑层和UI的渲染层,形成了一个特殊的开发环境,从而加强了web和native的融合程度,提高了web端的执行性能。
# Hybrid技术原理
Hybrid App 的本质:在原生应用中,使用webView作为容器,来承载一个web页面。
Hybrid App 的核心:原生和web端的双向通讯层 - JSBridge
# JSBridge
一座用JS搭建起来的桥,一端是web一端是native,目的是为了让原生可以调用web的JS代码,让web可以调用原生端的原生代码。实现JSBridge的关键就是原生端的web容器 - Webview,JSBridge 的原理都是通过 webview 的机制完成的。
在原生端调用 web 端方法的时候,这个方法必须挂载到 web 端 window 对象上,web 端调用原生方法的时候也需要通过 window.xxx (原生端注册的对象).原生端方法名。
下面我们以安卓端和IOS端分别和web端通信为例简单介绍了分别怎么实现?
# 安卓端与web相互通信
安卓端只能接收基本类型参数,不能接收引用类型的数据。如果web端想要传递一个Object类型的数据,就要通过JSON.stringify()转换为字符串。
在国内的手机厂商里,都会对安卓进行特殊的定制。这样会导致虽然是同一个安卓系统,但是不同定制系统可能会存在差异。在实际开发中,为了减少这种差异,我们一般会使用腾讯X5封装的webview组件,这个组件它与原生webview在使用的方式上没有什么不同。
# 安卓端调用web端的方法
在原生端引入webview,并增加一个按钮,给按钮绑定一个callJSFunction
方法,点击这个按钮调用web端的方法。
<!-- /app/src/main/res/layout/activity_main.xml -->
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="调用web端方法"
android:onClick="callJSFunction"/>
<cn.sunday.hybridappdemo.views.X5WebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorAccent"></cn.sunday.hybridappdemo.views.X5WebView>
注意
原生端调用 web端的方法,这个方法必须是挂载到 web 端 window 对象下面的方法。
在原生端通过callJSFunction
方法调用web端的onFunction
方法,并给web端传递这是安卓调用JS方法成功,传递给web端的数据
做为原生端给web的数据,并通过弹窗显示web端方法被调用传给原生端的数据。
// /app/src/main/java/cn/sunday/hybridappdemo/MainActivity.java
/**
* 调用 JS 中的方法:callJSFunction,并传递一个字符串
*/
public void callJSFunction (View v) {
mWebView.evaluateJavascript("javascript:onFunction('这是安卓调用JS方法成功,传递给web端的数据')", new ValueCallback<String>() {
@Override
public void onReceiveValue(String s) {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setMessage(s);
builder.setNegativeButton("确定", null);
builder.create().show();
}
});
}
web端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
span {
color: red;
}
</style>
</head>
<body>
<div>安卓调用JS方法是否成功?<span id="res"></span></div>
<script>
window.onFunction = function(data) {
res.textContent = data
return '安卓调用JS方法成功,这是web端传递给安卓的数据'
}
</script>
</body>
</html>
在运行项目前还需需修改 /app/src/main/java/cn/sunday/hybridappdemo/constants/Constants.java 中 WEB_URL 地址,将WEB_URL设置为web项目的访问地址
package cn.sunday.hybridappdemo.constants;
public class Constants {
public static final String WEB_URL = "http://192.168.0.100:8080/";
}
如果web端项目地址是http协议的,在安卓9.0及以上设备还需要对app进行安全设置
/app/src/main/res/xml/network_security_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<!-- 在这里加上访问 网页的IP地址 -->
<domain includeSubdomains="true">192.168.0.102</domain>
</domain-config>
</network-security-config>
在Android Studio中启动Demo程序后,界面如下。
当我们点击调用web端方法的按钮时,界面结果如下
可以看到,双端进行了完美的通信。
# web端调用安卓端的方法
上面我们实现了原生端调用web端的方法,并给web端传递参数。下面我们来说一下Web端调用原生端的方法。
web端调用原生端方法,要在原生端构建 JSBridge 对象,提供的 JSBridge 字符串会被挂载到网页中的 window 对象下面。web端可以通过window.这个JSBridge对象.原生端暴露出的方法
来调用原生端的方法。
构建 JSBridge 对象:
// /app/src/main/java/cn/sunday/hybridappdemo/views/X5WebView.java
private void init (Context context) {
this.mContext = context;
/**
* 基础配置
*/
initWebViewSettings();
initWebViewClient();
initChromeClient();
/**
* 构建 JSBridge 对象
*
* 在web端可以通过window.AndroidJSBridge来拿到我们注入的JSBridge对象,从而调用安卓端提供给 web 的方法
*/
addJavascriptInterface(
new MyJaveScriptInterface(mContext, this),
"AndroidJSBridge");
}
原生端暴露的被网页端调用的方法:
// /app/src/main/java/cn/sunday/hybridappdemo/jsInterface/MyJaveScriptInterface.java
/**
*
* window.AndroidJSBridge.androidTestFunction1('xxxx')
* 调用该方法,APP 会弹出一个 Alert 对话框,
* 对话框中的内容为 JavaScript 传入的字符串
* @param str android 只能接收基本数据类型参数
* ,不能接收引用类型的数据(Object、Array)。
* JSON.stringify(Object) -> String
*/
@JavascriptInterface
public void androidTestFunction1 (String str) {
AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
builder.setMessage(str);
builder.setNegativeButton("确定", null);
builder.create().show();
}
/**
* 调用该方法,方法会返回一个返回值给 javaScript 端
* @return 返回值的内容为:"调用安卓端方法,并返回数据给web端"
*/
@JavascriptInterface
public String androidTestFunction2 () {
return "调用安卓端方法,并返回数据给web端";
}
web端代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
span {
color: red;
}
</style>
</head>
<body>
<button onclick="sendData()">调用安卓方法并给安卓传递数据</button>
<button onclick="onReceived()">调用安卓方法给并给web端传递数据</button>
<div>安卓传递过来的数据是<span id="data"></span></div>
<script>
function sendData() {
window.AndroidJSBridge.androidTestFunction1('这是web端给app传的数据-I come from web')
}
function onReceived() {
const res = window.AndroidJSBridge.androidTestFunction2()
data.textContent = res
}
</script>
</body>
</html>
点击调用安卓方法并给安卓传递数据
按钮
点击调用安卓方法给并给web端传递数据
按钮
从上面运行的结果来看,Web端调用安卓端的方法也是成功的。
# IOS端和web相互通信
IOS和安卓端还是有一些区别的:
- IOS端可以直接接收一个对象数据
- IOS端无法直接提供一个返回值给web端,需要通过回调web方法的方式传递参数
IOS 端代码如下:
// ViewController.m
//
// ViewController.m
// ImoocHybridIOSNative
#import "ViewController.h"
#import <WebKit/WebKit.h>
#import "Constants.h"
@interface ViewController ()<WKNavigationDelegate,WKUIDelegate, WKScriptMessageHandler>
@property (nonatomic, strong) WKWebView *webView;
@property (nonatomic, strong) WKWebViewConfiguration *wkWebViewConfiguration;
@property (nonatomic, strong) WKUserContentController *wkUserContentController;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self initWebView];
}
- (void)initWebView {
//配置wkWebViewConfiguration
[self wkConfiguration];
//初始化webView
self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:self.wkWebViewConfiguration];
self.webView.backgroundColor = [UIColor whiteColor];
self.webView.navigationDelegate = self;
self.webView.UIDelegate = self;
NSURL *url = [NSURL URLWithString:WEB_URL];
// 为保证演示效果设置缓存策越为不缓存
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:10];
[self.webView loadRequest: request];
[self.view addSubview:self.webView];
//OC注册供JS调用的方法
[self addScriptFunction];
}
- (void)wkConfiguration {
self.wkWebViewConfiguration = [[WKWebViewConfiguration alloc]init];
WKPreferences *preferences = [[WKPreferences alloc] init];
preferences.javaScriptCanOpenWindowsAutomatically = YES;
self.wkWebViewConfiguration.preferences = preferences;
}
#pragma mark - OC注册供JS调用的方法,IOS 注入 jsbridge 对象名为 webkit
- (void)addScriptFunction {
self.wkUserContentController = [self.webView configuration].userContentController;
[self.wkUserContentController addScriptMessageHandler:self name:@"IOSTestFunction1"];
[self.wkUserContentController addScriptMessageHandler:self name:@"IOSTestFunction2"];
}
#pragma mark - Alert弹窗
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
UIAlertController * alertController = [UIAlertController alertControllerWithTitle:@"提示" message:message ? : @"" preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction * action = [UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
completionHandler();
}];
[alertController addAction:action];
[self presentViewController:alertController animated:YES completion:nil];
}
#pragma mark --- WKScriptMessageHandler ---
//OC在JS调用方法做的处理
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
//前端主动JS发送消息,前端指令动作
if ([@"IOSTestFunction1" isEqualToString:message.name]) {
[self IOSTestFunction1:message.body];
} else if ([@"IOSTestFunction2" isEqualToString:message.name]) {
[self IOSTestFunction2:message.body];
}
}
#pragma mark - 与 Android 不同,IOS可以直接接收一个对象Object数据
- (void)IOSTestFunction1:(id)body {
NSDictionary *dict = body;
NSString *msg = [dict objectForKey:@"msg"];
UIAlertController * alertController = [UIAlertController alertControllerWithTitle:@"提示" message:msg preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction * action = [UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleDefault handler:nil];
[alertController addAction:action];
[self presentViewController:alertController animated:YES completion:nil];
}
#pragma mark - 调用该方法,回调 web 端 onFunctionIOS 方法,并传递字符串
- (void)IOSTestFunction2:(id)body {
[self.webView evaluateJavaScript:@"onFunctionIOS('IOSTestFunction2方法执行完成')" completionHandler:^(id result, NSError * _Nullable error) {
NSLog(@"%@", result);
}];
}
@end
Web 端代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
div {
margin-top: 100px;
}
</style>
</head>
<body>
<div>
<input type="button" value="调用IOS端方法1并传递参数" onclick="invokeIOSFn1()"/>
<input type="button" value="调用IOS端方法2并传递参数" onclick="invokeIOSFn2()"/>
</div>
<script>
// 调用IOS端方法并传递参数
function invokeIOSFn1() {
// window.webkit.messageHandlers 是固定写法
const obj = {
'msg': '来自web端的数据'
}
window.webkit.messageHandlers.IOSTestFunction1.postMessage(obj)
}
function invokeIOSFn2() {
window.webkit.messageHandlers.IOSTestFunction2.postMessage({})
}
// IOS端回调Web端方法
window.onFunctionIOS = function(str) {
alert(str)
return '执行完成'
}
</script>
</body>
</html>
点击 调用IOS端方法1并传递参数
按钮,运行结果如下:
点击 调用IOS端方法2并传递参数
按钮,运行结果如下
# 安卓、IOS和web端双向通讯的对比
相同点:
- 都是通过WebView来完成网页的加载。
- 都是通过向window注入对象的方式,来提供被Web端调用的方法。
- 都可以直接调用Web端挂载到window上的方法,并且都可以向被调用的函数传入参数和接收函数返回的内容。
不同点:
- 注入对象的不同。安卓端可以提供注入的对象名,IOS端固定为webkit。
- JS调用native的方式不同。安卓端可以直接获取注入对象,调用方法;IOS端固定写法(window.webkit.messageHandlers.方法名.postMessage(入参对象))
- 传递数据的格式不同。安卓端只能接收基本数据类型数据;IOS端可以接收任意数据类型数据。
- 返回值不同。安卓端可以直接接收返回值。IOS端没有办法直接获取返回值,需要通过回调一个Web端函数的方式。
参考:
在 WebView 中编译 Web 应用 (opens new window)
低版本xcode打开高版本xcode项目:incompatible project version错误 (opens new window)
A build only device cannot be used to run this target异常 (opens new window)