混合开发是一种开发模式,混合了原生技术和 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)

低版本Mac OS安装合适xcode的方法 (opens new window)

低版本 Xcode 下载 (opens new window)