引述
LiteApp中有一种类似微信小程序Native的控件,像地图、Canvas等等,覆盖在WebView层级之上,比所有网页的层级都高。这种原生控件是怎么实现的呢?我们今天就从LiteApp的qy-input
控件入手分析下原理
分析
先看下qy-input
的关键源码
<template>
<qy-native-base :hover="hover" :nativeData="nativeData" nativeTag="QiyiInput"
@bindinput=""/>
</template>
布局里面引用了qy-native-base
, 并输入了hover
、nativeData
、nativeTag
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了。