【记】前端Hybrid开发

注:记录整理自https://www.cnblogs.com/yexiaochai/p/5813248.html https://www.cnblogs.com/dailc/p/5930231.html等文章中的内容。

app开发技术的对比和选择

当进入移动互联时代后,移动端设备开始成为流量的主要入口,app的重要性很快被凸显出来。多平台多终端的特点使得app的开发技术五花八门。多个方案的对比:

作为前端开发人员最容易上手的是第二、第三种方式,RN的方式也算hybird方式,但是和大部分的hybrid有区别,这里展开记录一些hybird开发的原理和方案。在我们使用Hybrid技术前要注意一个边界问题,什么项目适合Hybrid什么项目不适合,这个要搞清楚.

适合Hybrid的项目为:

1.有60%以上的业务为H5. | 2.对更新(开发效率)有一定要求的APP

不适合使用Hybrid技术的项目有以下特点:

1.只有20%不到的业务使用H5做. | 2.交互效果要求较高(动画多)


Hybrid技术的实现和选型

h5与Native交互有几种交互原理:

1.原生方法WebView.loadUrl等 | 2.URL Schema | 3.JavaScriptCore

而几种方式在使用上各有利弊,首先来说URL Schema是比较稳定而成熟的,会比较灵活;而从设计的角度来说JavaScriptCore似乎更加合理,但是我们在实际使用中却发现,注入的时机得不到保障。原生方法则有很多版本兼容问题。

大部分JSBridge的实现就是利用URL Schema实现的,基本原理是:

H5->通过某种方式触发一个url->Native捕获到url,进行分析->原生做处理->Native调H5的JSBridge对象传递回调。如下图

为什么要用URL Schema来实现JSBridge.

Native和原生之间有基本通信方式,既然Native和原生已经能够实现通信了,那为什么还要这种通过url scheme的JSBridge方式呢,原因大致如下:

1.Android4.4之前Native通过loadUrl来调用JS方法,只能让某个JS方法执行,但是无法获取该方法的返回值

2.Android4.2以下,addJavascriptInterface方式有安全漏掉

3.iOS7以下,JS无法调用Native

url scheme交互方式是一套现有的成熟方案,可以完美兼容各种版本,不存在上述问题.

另外,请注意,可以理解为JSBridge是一种交互理念,而上述的url scheme则是其中的一种实现,所以也就是说,就算后面实现变为了addJavascriptInterface,JavaScriptCore,也一样是JSBridge交互


url scheme实现JSBridge的原理

我们可以进行关键步骤分析:

第一步:设计出一个Native与JS交互的全局桥对象

第二步:JS如何调用Native

第三步:Native如何得知api被调用

第四步:分析url-参数和回调的格式

第五步:Native如何调用JS

第六步:H5中api方法的注册以及格式

JS部分的实现代码如下:

(function() {
    (function() {
        var hasOwnProperty = Object.prototype.hasOwnProperty;
        var JSBridge = window.JSBridge || (window.JSBridge = {});
        //jsbridge协议定义的名称
        var CUSTOM_PROTOCOL_SCHEME = 'CustomJSBridge';
        //最外层的api名称
        var API_Name = 'namespace_bridge';
        //进行url scheme传值的iframe
        var messagingIframe = document.createElement('iframe');
        messagingIframe.style.display = 'none';
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + API_Name;
        document.documentElement.appendChild(messagingIframe);
        //定义的回调函数集合,在原生调用完对应的方法后,会执行对应的回调函数id
        var responseCallbacks = {};
        //唯一id,用来确保每一个回调函数的唯一性
        var uniqueId = 1;
        //本地注册的方法集合,原生只能调用本地注册的方法,否则会提示错误
        var messageHandlers = {};
        //实际暴露给原生调用的对象
        var Inner = {
            /**
             * @description 注册本地JS方法通过JSBridge给原生调用
             * 我们规定,原生必须通过JSBridge来调用H5的方法
             * 注意,这里一般对本地函数有一些要求,要求第一个参数是data,第二个参数是callback
             * @param {String} handlerName 方法名
             * @param {Function} handler 对应的方法
             */
            registerHandler: function(handlerName, handler) {
                messageHandlers[handlerName] = handler;
            },
            /**
             * @description 调用原生开放的方法
             * @param {String} handlerName 方法名
             * @param {JSON} data 参数
             * @param {Function} callback 回调函数
             */
            callHandler: function(handlerName, data, callback) {
                //如果没有 data
                if(arguments.length == 3 && typeof data == 'function') {
                    callback = data;
                    data = null;
                }
                _doSend({
                    handlerName: handlerName,
                    data: data
                }, callback);
            },
            /**
             * @description 原生调用H5页面注册的方法,或者调用回调方法
             * @param {String} messageJSON 对应的方法的详情,需要手动转为json
             */
            _handleMessageFromNative: function(messageJSON) {
                setTimeout(_doDispatchMessageFromNative);
                /**
                 * @description 处理原生过来的方法
                 */
                function _doDispatchMessageFromNative() {
                    var message;
                    try {
                        if(typeof messageJSON === 'string'){
                            message = JSON.parse(messageJSON);
                        }else{
                            message = messageJSON;
                        }   
                    } catch(e) {
                        //TODO handle the exception
                        console.error("原生调用H5方法出错,传入参数错误");
                        return;
                    }
                    //回调函数
                    var responseCallback;
                    if(message.responseId) {
                        //这里规定,原生执行方法完毕后准备通知h5执行回调时,回调函数id是responseId
                        responseCallback = responseCallbacks[message.responseId];
                        if(!responseCallback) {
                            return;
                        }
                        //执行本地的回调函数
                        responseCallback(message.responseData);
                        delete responseCallbacks[message.responseId];
                    } else {
                        //否则,代表原生主动执行h5本地的函数
                        if(message.callbackId) {
                        //先判断是否需要本地H5执行回调函数
                        //如果需要本地函数执行回调通知原生,那么在本地注册回调函数,然后再调用原生
                        //回调数据有h5函数执行完毕后传入
                            var callbackResponseId = message.callbackId;
                            responseCallback = function(responseData) {
                                //默认是调用EJS api上面的函数
                                //然后接下来原生知道scheme被调用后主动获取这个信息
                                //所以原生这时候应该会进行判断,判断对于函数是否成功执行,并接收数据
                                //这时候通讯完毕(由于h5不会对回调添加回调,所以接下来没有通信了)
                                _doSend({
                                    handlerName: message.handlerName,
                                    responseId: callbackResponseId,
                                    responseData: responseData
                                });
                            };
                        }
                        //从本地注册的函数中获取
                        var handler = messageHandlers[message.handlerName];
                        if(!handler) {
                            //本地没有注册这个函数
                        } else {
                            //执行本地函数,按照要求传入数据和回调
                            handler(message.data, responseCallback);
                        }
                    }
                }
            }
        };
        /**
         * @description JS调用原生方法前,会先send到这里进行处理
         * @param {JSON} message 调用的方法详情,包括方法名,参数
         * @param {Function} responseCallback 调用完方法后的回调
         */
        function _doSend(message, responseCallback) {
            if(responseCallback) {
                //取到一个唯一的callbackid
                var callbackId = Util.getCallbackId();
                //回调函数添加到集合中
                responseCallbacks[callbackId] = responseCallback;
                //方法的详情添加回调函数的关键标识
                message['callbackId'] = callbackId;
            }
            //获取 触发方法的url scheme
            var uri = Util.getUri(message);
            //采用iframe跳转scheme的方法
            messagingIframe.src = uri;
        }
        var Util = {
            getCallbackId: function() {
                //如果无法解析端口,可以换为Math.floor(Math.random() * (1 << 30));
                return 'cb_' + (uniqueId++) + '_' + new Date().getTime();
            },
            //获取url scheme
            //第二个参数是兼容android中的做法
            //android中由于原生不能获取JS函数的返回值,所以得通过协议传输
            getUri: function(message) {
                var uri = CUSTOM_PROTOCOL_SCHEME + '://' + API_Name;
                if(message) {
                    //回调id作为端口存在
                    var callbackId, method, params;
                    if(message.callbackId) {
                        //第一种:h5主动调用原生
                        callbackId = message.callbackId;
                        method = message.handlerName;
                        params = message.data;
                    } else if(message.responseId) {
                        //第二种:原生调用h5后,h5回调
                        //这种情况下需要原生自行分析传过去的port是否是它定义的回调
                        callbackId = message.responseId;
                        method = message.handlerName;
                        params = message.responseData;
                    }
                    //参数转为字符串
                    params = this.getParam(params);
                    //uri 补充
                    uri += ':' + callbackId + '/' + method + '?' + params;
                }
                return uri;
            },
            getParam: function(obj) {
                if(obj && typeof obj === 'object') {
                    return JSON.stringify(obj);
                } else {
                    return obj || '';
                }
            }
        };
        for(var key in Inner) {
            if(!hasOwnProperty.call(JSBridge, key)) {
                JSBridge[key] = Inner[key];
            }
        }
    })();
    //注册一个测试函数
    JSBridge.registerHandler('testH5Func', function(data, callback) {
        alert('测试函数接收到数据:' + JSON.stringify(data));
        callback && callback('测试回传数据...');
    });
    /*
     ***************************API********************************************
     * 开放给外界调用的api
     * */
    window.jsapi = {};
    /**
     ***app 模块 
     * 一些特殊操作
     */
    jsapi.app = {
        /**
         * @description 测试函数
         */
        testNativeFunc: function() {
            //调用一个测试函数
            JSBridge.callHandler('testNativeFunc', {}, function(res) {
                callback && callback(res);
            });
        }
    };
})();

方便快捷的cordova

cordova是hybird开发技术中对前端最友好的技术栈。自从cordova从PhoneGap分离出来后,它更加灵活,不管你用什么js框架开发h5,都可以和cordova集成开发app。国内也有很多基于cordova封装的hybrid技术栈,如ionic就是angular和cordova的组合技术栈。

cordova的插件非常强大,用户可以用到什么原生方法就去下载对应的插件。项目的h5部分(html,css,js)可以打包在app包内,也可以将h5部署在服务端。h5在调用本地cordova方法时用url scheme来实现.

包括的app的唤醒功能,也用url scheme来实现。如果两个app的url scheme重复,一般情况下后安装的会覆盖之前的。

 欢迎转载:转载时请注明本文出处及文章链接

标签:


仅有 1 条评论

  1. benson benson

    how about flutter?

添加新评论

captcha
请输入验证码