# Hybrid开发及原理

# 1、Hybrid开发

  • Hybrid开发:原生app嵌入web页面的开发模式
  • jsBidge:原生app与web页面的交互沟通桥梁,一种通信方式,类比于JSONP的交互方式,只是类比的对象放到了js与native身上,native通过桥调用js的一些方法,js也能通过桥调用js的一些方法和功能。

# 2、Js调用Native

# 1、注入api方式

通过webview提供的接口,向JavaScript的window中注入对象或方法,让js调用时,直接执行相应的Native代码逻辑。webview在native中,native可控制webview。

  • 后段注入api:jsSendMessage
//ios:使用UIWebView
JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; context[@"jsSendMessage"]=getJsData;

//andriod:使用addJavascriptInterface接口
class getJsData { 
		@JavascriptInterface //标注的方法里面是子线程,而不是主线程 
    public String getNativeData() { 
      return "nativeData"; 
    }
}
webView.addJavascriptInterface(getJsData, "jsSendMessage");
1
2
3
4
5
6
7
8
9
10
11
  • 前端调用native
window.jsSendMessage(message)  //ios
window.jsSendMessage.getNativeData()  //andriod
1
2

# 2、拦截URL Scheme

  • 浏览器输入url即可打开app:weixin://

  • 协议类型:scheme(应用标识)://[path](行为即应用的某个功能)[?query](功能需要的参数)

  • 后端代码:约定bridge://loaded

//ios操作
(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{ 
  if ([request.URL.absoluteString hasPrefix:@"bridge://loaded"]) { 
    //执行native端的一些操作 
    return NO; //返回NO是为了不再执行点击原链接的跳转 
  }
  return YES; 
}

//andriod:使用shouldOverrideUrlLoading方法对url协议进行解析
webView.setWebViewClient(new WebViewClient(){ 
  @Override 
  public boolean shouldOverrideUrlLoading(WebView view, String url) { 
    if (isNativeUrl(url)) { 
      // 执行native端的一些操作 
      return true; 
    } 
  } 
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 前端操作:直接请求定义好的bridge://loaded 协议就能触发native端的拦截
<a href="bridge://loaded"> 触发app </a>
1
  • 缺点:目前不建议只使用拦截url scheme解析参数的形式,主要存在如下问题:
    • 连续调用location.href会出现消息丢失,因为webview限制了连续跳转,会过滤掉后续的请求
    • url会有长度限制,一旦过长就会出现信息丢失,一般结合注入api的形式一起使用

# 3、Native调用Js

  • 调用UIWebView的stringByEvaluatingJavaScriptFromString:方法

  • 调用WKWebView的evaluateJavaScript:completionHandler:方法

  • 调用WebView.loadUrl()方法

//ios适用于UIWebView和WKWebView
[self.webView stringByEvaluatingJavaScriptFromString:@"jsFunction('发送给js的数据')"]; 
//WKWebView 
[webView evaluateJavaScript:@"jsFunction('我是ios')" completionHandler:^(id item, NSError * _Nullable error) { // Block中处理是否通过了或者执行JS错误的代码 
}];

//andriod
public void javaCallJS(){ 
  webView.loadUrl("javascript:jsFunction('我是Android')"); 
}
1
2
3
4
5
6
7
8
9
10

# 4、现有开源解决方案

  • iOS:WebViewJavascriptBridge
  • andriod:JsBridge
  • andriod在启动webview时加载脚步,ios在webview发送scheme请求时加载脚步
//notation: js file can only use this kind of comments
//since comments will cause error when use in webview.loadurl,
//comments will be remove by java use regexp
(function() {
    // 错误优先-判断是否WebViewJavascriptBridge已经被注册好
    if (window.WebViewJavascriptBridge) {
        return;
    }
    //native传过来的回调队列
    var receiveMessageQueue = [];
    // 提供给native调js的 key=>handlerName(就是native端定义的函数名)  value=>页面注册的回调函数(就是页面调用native端方法 registerHandler时候的回调函数)
    var messageHandlers = {};
    // 页面调用native使用 key=>callbackId (页面生成的唯一标示供native找到的函数) value=>页面提供native调用的函数
    var responseCallbacks = {};
    var uniqueId = 1;

    //set default messageHandler  初始化默认的消息线程
    //init方法就是执行一遍native传给页面的回调队列-receiveMessageQueue队列
    function init(messageHandler) {
        if (WebViewJavascriptBridge._messageHandler) {
            throw new Error('WebViewJavascriptBridge.init called twice');
        }
        WebViewJavascriptBridge._messageHandler = messageHandler;
        var receivedMessages = receiveMessageQueue;
        receiveMessageQueue = null;
        for (var i = 0; i < receivedMessages.length; i++) {
              // 将native传过来的队列依次执行一遍
            _dispatchMessageFromNative(receivedMessages[i]);
        }
    }

    // 发送
    // 直接调用native中的send方法
    function send(data, responseCallback) {
        _doSend('send', data, responseCallback);
    }

    /**
     * 页面提供函数为native调用的
     * @param { string } handlerName native中定义的方法名
     * @param { Function } handler 用来接收native方法执行完得到返回值的页面回调函数
     */
    function registerHandler(handlerName, handler) {
        messageHandlers[handlerName] = handler;
    }
    /**
     * 页面调用native的方法
     * @param { string } handlerName native中定义的方法名
     * @param { string } data 页面中函数体所需要的参数
     * @param { Function } responseCallback 页面中函数体
     */
    function callHandler(handlerName, data, responseCallback) {
        // 如果方法不需要参数,只有回调函数,简化JS中的调用
        if (arguments.length == 2 && typeof data == 'function') {
			responseCallback = data;
			data = null;
		}
        _doSend(handlerName, data, responseCallback);
    }
    callHandler('nativeMethod',function(data){
        console.log(data)
    })
    //sendMessage add message, 触发native处理 sendMessage
    /** 
     * @param { string } handlerName 页面中函数名
     * @param { string } message 页面中函数体所需要的参数
     * @param { Function } responseCallback 页面中函数体
     */
    function _doSend(handlerName, message, responseCallback) {
        var callbackId;
        if(typeof responseCallback === 'string'){
            callbackId = responseCallback;
        } else if (responseCallback) {
            callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
            responseCallbacks[callbackId] = responseCallback;
        }else{
            // 没有回调时候
            callbackId = '';
        }
        try {
             var fn = eval('window.android.' + handlerName);
         } catch(e) {
             console.log(e);
         }
         if (typeof fn === 'function'){
             var responseData = fn.call(this, JSON.stringify(message), callbackId);
             if(responseData){
              console.log('response message: '+ responseData);
                 responseCallback = responseCallbacks[callbackId];
                 if (!responseCallback) {
                     return;
                  }
                 responseCallback(responseData);
                 delete responseCallbacks[callbackId];
             }
         }
    }

    //提供给native使用,
    function _dispatchMessageFromNative(messageJSON) {
        setTimeout(function() {
            var message = JSON.parse(messageJSON);
            var responseCallback;
            //java call finished, now need to call js callback function 
            //页面调native 执行回调
            if (message.responseId) {
                responseCallback = responseCallbacks[message.responseId];
                if (!responseCallback) {
                    return;
                }
                responseCallback(message.responseData);
                delete responseCallbacks[message.responseId];
            } else {
                 //有callbackId直接发送
                if (message.callbackId) {
                    var callbackResponseId = message.callbackId;
                    responseCallback = function(responseData) {
                        _doSend('response', responseData, callbackResponseId);
                    };
                }
                //messageHandlers中有对应回调就执行没有其实执行的是init()方法所传入的的回调
                var handler = WebViewJavascriptBridge._messageHandler;
                if (message.handlerName) {
                    handler = messageHandlers[message.handlerName];
                }
                //查找指定handler
                try {
                    handler(message.data, responseCallback);
                } catch (exception) {
                    if (typeof console != 'undefined') {
                        console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception);
                    }
                }
            }
        });
    }

    //提供给native调用,receiveMessageQueue 在会在页面加载完后赋值为null,所以
    function _handleMessageFromNative(messageJSON) {
        console.log('handle message: '+ messageJSON);
        if (receiveMessageQueue) {
            receiveMessageQueue.push(messageJSON);
        }
        _dispatchMessageFromNative(messageJSON);

    }

    var WebViewJavascriptBridge = window.WebViewJavascriptBridge = {
        init: init,
        send: send,
        registerHandler: registerHandler,
        callHandler: callHandler,
        _handleMessageFromNative: _handleMessageFromNative
    };

    var doc = document;
    var readyEvent = doc.createEvent('Events');
    var jobs = window.WVJBCallbacks || [];
    readyEvent.initEvent('WebViewJavascriptBridgeReady');
    readyEvent.bridge = WebViewJavascriptBridge;
    window.WVJBCallbacks = []
    jobs.forEach(function (job) {
        job(WebViewJavascriptBridge)
    })
    doc.dispatchEvent(readyEvent);
})();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167

# 5、面试题

Hybrid混合开发:使用jsBridge,通常使用两种方法:URL schema和注入api

  • url Schema:客户端通过拦截wevview请求来完成通信

  • native向webview中的js执行环境,注入api,以此来完成通信

# 1、Url schema

# 1、原理

webview中发出的网络请求,都会被客户端监听和捕获到,H5与webview交互的私有协议为url schema

  • webview发出请求:使用iframe发送请求或使用location.href的方式(不适合并发场景)

  • 客户端拦截协议请求:解析到请求协议时,进行相关操作,设置处理逻辑

    • IOS:shouldStartLoadWithRequest
    • Android:shouldOverrideUrlLoading
  • 客户端请求处理后回调:

    • webview请求时使用window.addEventListener设置自定义事件
    • 客户端处理完成后,调用jsBridge中的dispatch携带回调的数据触发自定义事件window.dispatchEvent

# 2、问题

  • 连续调用location.href会出现消息丢失,因为webview限制了连续跳转,会过滤掉后续的请求
  • url会有长度限制,一旦过长就会出现信息丢失,一般结合注入api的形式一起使用

# 2、注入api

# 1、原理

通过webview提供的接口,向JavaScript的window中注入对象或方法,让js调用时,直接执行相应的Native代码逻辑。

  • 客户端给webview中的window中注入方法
  • webview中调用注入的方法,并传入回调函数
  • 客户端处理完成后,调用回调函数
Last Updated: 10/3/2021, 9:23:14 PM