LiteApp Native Component实现原理

引述

LiteApp中有一种类似微信小程序Native的控件,像地图、Canvas等等,覆盖在WebView层级之上,比所有网页的层级都高。这种原生控件是怎么实现的呢?我们今天就从LiteApp的qy-input控件入手分析下原理

分析

先看下qy-input的关键源码

<template>  
    <qy-native-base :hover="hover" :nativeData="nativeData" nativeTag="QiyiInput" 
        @bindinput=""/>
</template>  

布局里面引用了qy-native-base, 并输入了hovernativeDatanativeTag 3个属性,对于属性作用后面会介绍到,还是看下qy-native-base的实现, 实现类在mp-fe-core/src/platforms/qy/runtime/components/qy-native-base.js

import * as nativeOps from '../../bridge/qnode-ops.native';  
import { isDef , isUndef } from 'shared/util'

export default {  
  props : ['hover','nativeTag','nativeData'],
  name : 'QYNativeBase',
  mounted : function(){
    nativeOps.createNative({
      _uid : this._vnode.elm._uid,
      nativeTag : this.nativeTag,
      hover : isDef(this.hover) ? true : false,
      data : this.nativeData || {}
    });
    this.$watch('nativeData',()=>{
      nativeOps.removeNative({
        _uid : this._vnode.elm._uid
      })
      nativeOps.createNative({
        _uid : this._vnode.elm._uid,
        nativeTag : this.nativeTag,
        hover : isDef(this.hover) ? true : false,
        data : this.nativeData || {}
      });
    })
  },
  destroyed : function(){
    nativeOps.removeNative({
      _uid : this._vnode.elm._uid
    })
  },
  render : function( _c ){
    return _c('div',{class:'qy-native-component',attrs:{data:JSON.stringify(this.nativeData)}});
  },
  updated : function(){
      debugger;
  }
}

这是一个Vue组件。属性包含['hover','nativeTag','nativeData'], mounted方法调用了nativeOps.createNative, 并传递了一个对象

{
      _uid : this._vnode.elm._uid,
      nativeTag : this.nativeTag,
      hover : isDef(this.hover) ? true : false,
      data : this.nativeData || {}
    }

继续看createNative方法

export function createNative( nativeCom : Object ){  
  // judge nativeCom here 
  addDirect( 'native' , 'createNative' , nativeCom)
}

加了两个固定参数继续传递给addDirect方法处理

const patchData = {  
  // set root for root element
  direct_dom : [],
  direct_attr : [],
  direct_api : [],
  direct_com : [],
  direct_native : []
};

export function addDirect(  
  type : string ,
  op : string , 
  filteredNode : object
) : void {
  /* istanbul ignore if */
  if(!patchData[`direct_${type}`]){
    if( process.env.NODE_ENV !== 'production' && typeof console !== 'undefined' ){
      console.error(`[qy error] : type ${type} error `)
    }
  }else{
    patchData[`direct_${type}`].push({ op , val : filteredNode })
  }
}

addDirect 也就是把刚才的数据装到patchData对象里,大概patchData数据结构如下:

const patchData = {  
  direct_dom : [],
  direct_attr : [],
  direct_api : [],
  direct_com : [],
  direct_native : [
     {createNative: {
      _uid : this._vnode.elm._uid,
      nativeTag : this.nativeTag,
      hover : isDef(this.hover) ? true : false,
      data : this.nativeData || {}
    }}
 ]
};

那哪里处理了patchData呢,这要追溯到mp-fe-core/src/platforms/qy/bridge/bridge.js

const bridge = new Object({  
  api,
  component,
  callback,
  wait : false,
  event,
  // patch to webview , patchData is the main carrie
  readyToPatch : ():void=>{
    if(!bridge.wait){
      bridge.wait = true;
      nextTick(()=>{
        bridge.doPatch()
        bridge.wait = false
      })
    }
  },
  doPatch : ():void =>{
    if(bridgePatch.isEmpty()){
      return;
    }
    if( inApp ){
      const patchJson = JSON.stringify(bridgePatch.patchData);
      global.__base__.postPatch(`__bridge__.on_recv_patch_command(${patchJson})`);
    }
    if( inWeb ){
      // if webview is not ready
      if(typeof window.__bridge__ === 'undefined'){
        window.__patchQueue__ = (window.patchQueue||[]).push(bridgePatch.patchData);
      }else{
        window.__bridge__.on_recv_patch_command(bridgePatch.patchData:object);
      }
    }        
    bridgePatch.clear();
  },
  // get event call from webview
  getEvent : (eventObj):void=>{
    typeof console !== 'undefined' && console.log(eventObj)
    event.triggerEvent( eventObj );
  },
  getBaseEvent : (eventObj) : void =>{
    typeof console !== 'undefined' && console.log(eventObj)
    event.triggerBaseEvent( eventObj );
  }
})

nextTick回调里面执行了doPatch, 然后判断了如果在App调用了

global.__base__.postPatch(`__bridge__.on_recv_patch_command(${patchJson})`  

这里已经进入原生代码阶段了,先告诉大家结论:global.__base__.postPatch会在WebView中执行传递过来的js内容(注意是webView, 不是JSCore)

那就继续看__bridge__.on_recv_patch_command(${patchJson}) , 这是qy.webview.js里面注入的一个方法

function registerBridge(){  
  // regist patch receiver
  window.__bridge__ = {
    on_recv_patch_command : function (patch){
      directJsonToDom(patch);
    }
  };
}

直接看directJsonToDom

var directSort = ['direct_dom','direct_attr','direct_api','direct_com','direct_native'];  
function directJsonToDom(patch){  
  directSort.forEach(function (key) {
    patch[key].forEach(function (direct){
      nodeOps[direct.op].call(null,direct.val);
    });
    if(key === 'direct_dom'){
      init();
    }
  });
}

多个循环,调用了nodeOps里面的方法

var nodeOps = Object.freeze({  
    init: init,
    appendCh: appendCh,
    removeCh: removeCh,
    insertBefore: insertBefore,
    setAttr: setAttr,
    removeAttr: removeAttr,
    setClass: setClass,
    setStyle: setStyle,
    setText: setText,
    createNative: createNative,
    removeNative: removeNative,
    updateNative: updateNative,
    addEvent: addEvent,
    removeEvent: removeEvent,
    webviewApiCall: webviewApiCall,
    webviewComCall: webviewComCall
});

直接看 createNative方法, 参数是最上面那个包含_uid, nativeTag等属性的对象。直接把createNativeBox写一块了

function createNative( qnode ){  
    createNativeBox( qnode._uid , getElm(qnode) , qnode.nativeTag , qnode.hover , qnode.data );
}

function createNativeBox(id,el,type,hover,viewData) {  
    //create a native box on a element that has
    var data = {};
    data.top = hover ? getOffset(el).hoverTop : getOffset(el).top;
    data.left = hover ? getOffset(el).hoverLeft : getOffset(el).left;
    data.height = el.offsetHeight;
    data.width = el.offsetWidth;
    data.type = type;
    data.viewData = viewData;
    data.hover = hover;
    data.id = id;
    data.action = "create";
    callNativeEvent("nativeBox",data);
}

获取组件的top、left、width、height熟悉,有这四个就完全能定义component的位置了。还有其他属性封装就不一一介绍了,继续往下看callNativeEvent

function callNativeEvent(  
    typeContent,
    dataContent
){
    var event = {
        type:typeContent,
        data:dataContent
    };
    executeJS('native',JSON.stringify(event));
}

封装对象,并调用executeJS

function executeJS(target,scriptContent){  
    console.log(("[executeJS] target : " + target + " ; scriptContent : " + scriptContent));
    if(target === 'thread'){
        isIosApp && window.webkit.messageHandlers.emit.postMessage(scriptContent);
        isAndroidApp && console.log("execute:" + scriptContent);
        isBrowser && (new Function(scriptContent))();
    }else if(target === 'native'){
        isIosApp && window.webkit.messageHandlers.native_call.postMessage(scriptContent);
        isAndroidApp && console.log("hal:" + scriptContent);
        isBrowser && console.log('[webview] error : native call ' + scriptContent);
    }
}

在Android中最终调用了console.log("hal:" + scriptContent) 稍微懂点Hybrid开发的应该知道,console也是一种WebView和JS通信的方式,这里就是利用这个把event对象转成string传递给Native处理。可以看下实现代码:

mWebView.setWebChromeClient(new WebChromeClient(){  
            @Override
            public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
                String messageContent = consoleMessage.message();
                if(messageContent.startsWith("hal:")){
                    String eventString = messageContent.substring(4,messageContent.length());
                    new ConsoleEventTask(context, eventString).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
                }
                return true;
            }
        });

那就继续看Android ConsoleEventTask实现咯

JSONObject jsonObject = new JSONObject(eventString);  
String eventData = jsonObject.optString(jsConsoleEventData);  
String eventType = jsonObject.optString(jsConsoleEventType);  
boolean intercepted = jsonObject.optBoolean(jsConsoleEventIntercepted);  
BridgeEvent event = new BridgeEvent();  
event.setContext(mContext);  
event.setType(eventType);  
event.setData(eventData);  
event.setIntercepted(intercepted);  
event.setLocal(true);  
                EventBridgeImpl.getInstance().triggerEvent(event);

EventBridgeImpl这里暂时不介绍,类似于广播实现。我们直接看监听这个广播的代码吧, 代码在com/iqiyi/halberd/liteapp/plugin/widget/NativeViewPlugin.java

JSONObject dataObj = new JSONObject(event.getData());  
                    String action = dataObj.optString("action");
                    final String id = dataObj.optString("id");
                    final String type = dataObj.optString("type");

                    if ("create".equals(action)) {
                        final int top = dataObj.optInt("top");
                        final int left = dataObj.optInt("left");
                        final int height = dataObj.optInt("height");
                        final int width = dataObj.optInt("width");
                        final boolean hover = dataObj.optBoolean("hover");
                        final JSONObject viewData = dataObj.optJSONObject("viewData");

                        nativeLayout.post(new Runnable() {
                            @Override
                            public void run() {
                                LiteAppNativeViewHolder contentViewHolder;
                                try {
                                    contentViewHolder = LiteAppNativeWidgetProvider.getInstance().createNativeView(
                                            top, left, width, height, type, viewData, event.getContext().getAndroidContext().getApplicationContext(), event.getContext());
                                    if (contentViewHolder != null) {
                                        contentViewHolder.getNativeView().setTag(id);
                                        contentViewHolder.getNativeView().setTag(-1, contentViewHolder);
                                        View nativeView = contentViewHolder.getNativeView();
                                        if (hover) {
                                            nativeHoverLayout.addView(nativeView);
                                        } else {
                                            nativeLayout.addView(nativeView);
                                        }
                                        nativeView.requestFocus();
                                        nativeView.setEnabled(true);
                                    }
                                } catch (JSONException e) {
                                    LogUtils.logError(LogUtils.LOG_MINI_PROGRAM_ERROR,"error native view data json",e);
                                }
                            }
                        });
                    }

也就是接收刚才那些参数而已,然后就是View的创建和添加了。到这里Vue Component 就已经转化为相同位置的Native View了。