彻底理解weex原理

2,375 阅读5分钟

思路

我们要从weex的runtime源码出发理解weex的实现原理。

runtime

image.png

entries

看文件夹的名字也知道,entries文件夹就是入口文件定义的位置了,我们点进去看他的index.js的定义,发现它是各种前端框架的入口文件,以index.js为入口会编译所有框架的版本。

import * as Vanilla from './vanilla/index'
import * as Vue from 'weex-vue-framework'
import * as Weex from './legacy/index'
import Rax from 'weex-rax-framework'

export default {
  Vanilla,
  Vue,
  Rax,
  Weex
}

比如vue.js:

import setup from './setup'
import * as Vue from 'weex-vue-framework'

setup({ Vue })

api

api文件夹主要是暴露api给注入vue的前端框架。
做的事情包括,初始化第三方框架,传入weex提供的config参数中的一些对象;返回全局api供第三方前端框架。

const config = {
  Document, Element, Comment, Listener,
  TaskCenter,
  sendTasks (...args) {//调用原生代码
    if (typeof callNative === 'function') {
      return callNative(...args)
    }
    return (global.callNative || (() => {}))(...args)
  }
}

Document.handler = config.sendTasks

第三方前端框架可以根据config中参数进行初始化。 暴露的全局api供第三方前端框架调用

  createInstance,//创建实例
  createInstanceContext,//创建实力上下文对象
  getRoot,//获得根实例
  getJSFMVersion,//获得jsm版本
  getDocument: getDoc,//获得当前的document全局对象
  registerService: register,//注册服务
  unregisterService: unregister,//取消注册
  callJS (id, tasks) {//callJS,native通过这个方法统一调用js方法
    const framework = frameworks[getFrameworkType(id)]
    if (framework && typeof framework.receiveTasks === 'function') {
      return framework.receiveTasks(id, tasks)
    }
    return receiveTasks(id, tasks)
  }
  adaptMethod('registerComponents', registerComponents)//注册组件
  adaptMethod('registerModules', registerModules)//注册模块
  adaptMethod('registerMethods');
  ['destroyInstance', 'refreshInstance'].forEach(genInstance)//注册生命周期函数

在init函数中我们发现还调用了initHandler函数,这个函数初始化任务处理中心的方法,是brige/TaskCenter.js中的init函数,这个函数为TaskCenter类的原型上挂载了诸多对外的方法,这些方法最终是调用的原生方法处理。

const DOM_METHODS = {
    createFinish: global.callCreateFinish,
    updateFinish: global.callUpdateFinish,
    refreshFinish: global.callRefreshFinish,

    createBody: global.callCreateBody,

    addElement: global.callAddElement,
    removeElement: global.callRemoveElement,
    moveElement: global.callMoveElement,
    updateAttrs: global.callUpdateAttrs,
    updateStyle: global.callUpdateStyle,

    addEvent: global.callAddEvent,
    removeEvent: global.callRemoveEvent,
    __updateComponentData: global.__updateComponentData
  }

WeexInstance 是暴露给开发者使用的weex的对外界接口,我们可以从其构造函数概览可以全局访问的接口:

constructor (id, config, data) {
    setId(this, String(id))
    this.config = config || {}//全局配置,包括环境变量,设备信息等
    this._nativeData = data || {}//原生初始化传递过来的数据
    this.document = new Document(id, this.config.bundleUrl)//模拟dom的document对象
    this.requireModule = this.requireModule.bind(this)//引入模块
    this.importScript = this.importScript.bind(this)//引入js代码,立即执行
    this.isRegisteredModule = isRegisteredModule //检测模块是否存在
    this.isRegisteredComponent = isRegisteredComponent //检测组件是否存在
  }

这些是我们开发weex界面可以直接调用的。

brige

是js与native的连接器,用于js调用native代码,处理管理事件回调函数 CallbackManager.js:回调函数管理器,包括组件hook函数 Handler:任务处理器,用于调用原生方法

//可调用的原生方法
const handlerMap = {
  createBody: 'callCreateBody',
  addElement: 'callAddElement',
  removeElement: 'callRemoveElement',
  moveElement: 'callMoveElement',
  updateAttrs: 'callUpdateAttrs',
  updateStyle: 'callUpdateStyle',
  addEvent: 'callAddEvent',
  removeEvent: 'callRemoveEvent'
}

Listener:Handler对外的访问接口(封装),为调用native的方法或者说发送调用信号提供可分批的操作。 Receiver.js:接收原生发送的事件和组件回调

export function receiveTasks (id, tasks) {
  const document = getDoc(id)
  if (!document) {
    return new Error(`[JS Framework] Failed to receiveTasks, `
      + `instance (${id}) is not available.`)
  }
  if (Array.isArray(tasks)) {
    return tasks.map(task => {
      switch (task.method) {
        case 'callback': return callback(document, ...task.args)
        case 'fireEventSync':
        case 'fireEvent': return fireEvent(document, ...task.args)
        case 'componentHook': return componentHook(document, ...task.args)
      }
    })
  }
}

TaskCenter: 是任务处理中心,处理以下事务:

  • 通过CallbackManager管理回调函数
  • 通过CallbackManager管理hook回调
  • 处理dom操作(通过向原生发送指令)
  • 处理component组件方法调用
  • 处理module模块方法调用

其中回调相关即回调函数和hook回调都是通过callbackManager管理,dom操作通过全局config对象上的sendTasks调用global.callNative向原生发送指令,component和module通过各自的handler处理,分别为定义到global上的callNativeComponent和callNativeModule方法,不存在的话就同样调用sendTasks方法。

vdom

index.js:入口文件

Commen.js:注释结点

Document:对应浏览器document的vdom实现

Element:元素结点的vdom实现

Node.js:vdom的基础实现,基类

WeexElement.js:注册weex组件的文件,有一个map集合保存所有注册了的组件,并为组件添加组件对应的方法。

operation.js:所有的dom操作方法定义在此文件中

思维导图

根据我们上面的整理和理解,接下来把知识点通过思维导图的形式进行梳理,使得知识点关联起来,脉络更清晰,方便我们理解和记忆。

上面就是我们对runtime-jsFramework的梳理和理解了,接下来我们通过阅读weex-vue-framework的原理进一步理解这部分代码的使用。

weex-vue-framework

首先我们发现weex-vue-framework主要做了如下几件事:

  1. createInstanceContext

    创建实例,处理weex实例

  2. createVueModuleInstance

    创建Vue实例,并做对weex的适配

  3. 挂载weex提供的api到Vue实例上,如document,taskCenter等

通过上面的理解我们发现,weex的js运行时框架实际上就是为前端框架提供了一系列api,包括dom操作、事件处理等。 通过封装好的vdom操作、事件管理系统、service服务等初始化诸如Vue的前端框架。 weex原生初始化时weexjs运行时在v8引擎中运行,相当于我们的js运行环境中全局有了一个weexjs运行时声明的Vue构造函数(Vue前端实例初始化),weexjs运行时中的Vue实例与web端的不同,它是经过特殊处理的,主要区别是它的vdom操作不是浏览器的dom操作,而是原生封装的通过weexjs运行时暴露给它的对应的view操作;事件体系是基于原生封装暴露给它的事件体系。
实际上,我们可以编写自己的前端框架或者改写已有的其他前端框架,对weexjs运行时框架进行适配(主要是vdom和事件系统)。需要在框架上定义init(config),createInstance(config,weexInstanceContext),receiveTasks(id,task)等原型方法基于weexjs运行时暴露的api进行一些初始化的处理。

进一步理解

根据这个思路我们看一下Vue编译之后的源码加以理解,我们编写的Vue文件被weex-loader或者weex-builder编译后是如下形式:

(function(modules){
   var installedModules={};
   function _webpack_require_(moduleId){
        // Check if module is in cache
       if(installedModules[moduleId]) {
            return installedModules[moduleId].exports;
       }
       // Create a new module (and put it into the cache)
       var module = installedModules[moduleId] = {
                id: moduleId,
            loaded: false,
            exports: {}
       };
            // Execute the module function
       modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
       // Flag the module as loaded
       module.loaded = true;
       // Return the exports of the module
       return module.exports;
   }
  //省略无关代码......
   return __webpack_require__(__webpack_require__.s = 647);
})(
   //省略无关代码......
   647:(function(module, exports, __webpack_require__) {
        var __vue_exports__, __vue_options__;
        var __vue_styles__ = [];
        /* styles 生成样式相关的数据*/
        __vue_styles__.push(__webpack_require__(648)
        );
        /* script 生成vue的options */
        __vue_exports__ = __webpack_require__(649);
        /* template 生成的渲染函数 */
        var __vue_template__ = __webpack_require__(651);
        __vue_options__ = __vue_exports__ = __vue_exports__ || {};
        //下面对__vue_options__的操作就相当于与操作__vue_exports__了
        __vue_options__.render = __vue_template__.render;
        __vue_options__._scopeId = 'data-v-07574e69';
        __vue_options__.style = __vue_options__.style || {};
        __vue_styles__.forEach(function (module) {
            for (var name in module) {
                __vue_options__.style[name] = module[name];
            }
        });
        module.exports = __vue_exports__;
        module.exports.el = 'true';
        //传入的options是一个把template编译成render函数的选项对象
        //下面就是Vue的初始化了,数据监听,挂载vdom结点创建真实视图
        new Vue(module.exports);
     }),
    //样式
    648:(function(module, exports) {
        module.exports = {
            'deleteImg': {
                'position': 'absolute',
                'width': '36',
                'height': '36',
                'top': '37',
                'right': '20'
            },
            'deleteImg1': {
                'position': 'absolute',
                'width': '36',
                'height': '36',
                'top': '37',
                'right': '65'
            },
            'deleteImg2': {
                'position': 'absolute',
                'width': '36',
                'height': '36',
                'top': '37',
                'right': '20'
            },
            'bgt': {
                'width': '750'
            },
            'img': {
                'width': '100',
                'height': '100'
            },
            'topimg': {
                'width': '520',
                'height': '160',
                'position': 'absolute',
                'top': '180',
                'left': '115'
            },
            'bgimg': {
                'width': '750',
                'marginTop': '148',
                'alignItems': 'center',
                'textAlign': 'center',
                'flexDirection': 'column',
                'justifyContent': 'center'
            },
            'inputdiv': {
                'marginLeft': '50',
                'width': '650',
                'height': '100',
                'flexDirection': 'row',
                'borderBottomWidth': '1',
                'borderBottomColor': '#d3d3d3',
                'borderBottomStyle': 'solid',
                'paddingTop': '30'
            },
            'inputimg': {
                'fontSize': '30',
                'color': '#333333',
                'width': '100',
                'lineHeight': '50'
            },
            'content': {
                'marginTop': '140'
            },
            'input': {
                'width': '500',
                'height': '50',
                'marginLeft': '30'
            },
            'textchar': {
                'fontSize': '29',
                'color': '#ffffff',
                'fontWeight': 'bold'
            },
            'btn': {
                'marginTop': '100',
                'marginLeft': '40',
                'backgroundColor': '#2250FF',
                'width': '670',
                'height': '86',
                'justifyContent': 'center',
                'alignItems': 'center',
                'borderRadius': '43'
            },
            'btns': {
                'marginTop': '30',
                'marginLeft': '40',
                'width': '670',
                'flexDirection': 'row',
                'justifyContent': 'space-between'
            },
            'lichar': {
                'color': '#2250FF',
                'fontSize': '26'
            }
        };
    }),
    //js代码导出vue选项
    /***/ 649:
    /***/ (function(module, exports, __webpack_require__) {
        'use strict';
        var _updateDialog = __webpack_require__(39);
        var _updateDialog2 = _interopRequireDefault(_updateDialog);
        var _type_net = __webpack_require__(17);
        var _type_net2 = _interopRequireDefault(_type_net);
        var _configEnv = __webpack_require__(2);
        function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
        var notify = weex.requireModule('notify'); 
        var nav = weex.requireModule('navigator');
        var pref = weex.requireModule('pref');
        var utils = weex.requireModule('utils');
        var st = weex.requireModule('static');
        var modal = weex.requireModule('modal');
        var updater = weex.requireModule('updater');
        var config = __webpack_require__(650);
      
        exports.default = {
            components: {
                UpdateDialog: _updateDialog2.default
            },
            data: {
                userInfo: {},
                remember: false,
                userName: '',
                password: '',
                dialogShow: false,
                pogressShow: false,
                nowVertion: '',
                must: false,
                updateInfo: {},
                value: 0,
                progress: 0,
                showPwd: false
            },
            computed: {
                showName: function showName() {
                    if (this.userName == '') {
                        return false;
                    } else {
                        return true;
                    }
                },
                delPwd: function delPwd() {
                    if (this.password == '') {
                        return false;
                    } else {
                        return true;
                    }
                }
            },
            methods: {
                // thirdLogin(){
                //   modal.toast({message:'正在开发~~'})
                // },
                // ksdl(){
                //     modal.toast({message:'正在开发~~'})
                // },
                deleteImg: function deleteImg(index) {
                    if (index == 1) {
                        this.userName = '';
                    } else {
                        this.password = '';
                    }
                },
                cencels: function cencels() {
                    this.dialogShow = false;
                },
                confirms: function confirms() {
                    var self = this;
                    self.update();
                    this.dialogShow = false;
                },
                login: function login() {
                    //记住密码
                    var self = this;
                    utils.getPhoneInfo(function (res) {
                        self.netLogin(res);
                    });
                    // var info={
                    //     brand:'111',
                    //     phoneModel:'1',
                    //     osName:'1',
                    //     osVersion:'1'
                    // };
                    // self.netLogin(info);
                },
                netLogin: function netLogin(deviceInfo) {
                    var self = this;
                    var nav = weex.requireModule('navigator');
                    var rsaAuthInfo = utils.encryptAuthInfo(self.userName, self.password).authInfo;
                    _type_net2.default.post(_configEnv.env.login, { authorization: rsaAuthInfo, clientCode: 'mb-app',
                        equipmentBrand: deviceInfo.brand, equipment: deviceInfo.phoneModel, systemName: deviceInfo.osName,
                        systemVersion: deviceInfo.osVersion }, function (res) {
                        var data = res.res;
                        if (data.code == 0) {
                            modal.toast({ message: '登陆成功' });
                            pref.set('userInfo', data.data.userContext);
                            st.set('userInfo', data.data.userContext);
                            pref.set('key', { key: rsaAuthInfo });
                            var token = '';
                            if (weex.config.env.platform === 'iOS') {
                                token = res.headers['Authorization'];
                            } else {
                                token = res.token;
                            }
                            //掌上工作台扫码调试掌上工作台需要添加此代码
                            // utils.setSchema('assets');
                            st.setPUserInfo({ key: rsaAuthInfo, token: token, data: data.data.userContext.currentUser });
                            nav.pushParam('host.js', { from: 'login' });
                            utils.JPushLogin(_configEnv.env.env + '_' + 'mobile_' + data.data.userContext.currentUser.userCode);
                        } else {
                            modal.toast({ message: data.msg });
                        }
                    }, 'login');
                }
            },
            created: function created() {
                var self = this;
                if (weex.config.env.platform != 'iOS') {
                    notify.regist('login_dismiss', function (res) {
                        nav.dismiss();
                    });
                }
                var globalEvent = weex.requireModule('globalEvent');
                globalEvent.addEventListener('onPageInit', function (e) {
                    self.userInfo = pref.get('userInfo');
                    var notify = weex.requireModule('notify');
                    notify.send('guide_dismiss', { a: '1' });
                    notify.send('index_dismiss', { a: '1' });
                });
            }
        };
    }),
    //根据template模板生成的渲染函数
    /***/ 651:
    /***/ (function(module, exports) {
        module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
            return _c('div', {
                staticClass: ['bgt']
            }, [_vm._m(0), _c('div', {
                staticClass: ['content']
            }, [_c('div', {
                staticClass: ['inputdiv']
            }, [_c('text', {
                staticClass: ['inputimg']
            }, [_vm._v('账号')]), _c('input', {
                staticClass: ['input'],
                attrs: {
                    'placeholder': '输入账号',
                    'value': (_vm.userName)
                },
                on: {
                    'input': function($event) {
                        _vm.userName = $event.target.attr.value;
                    }
                }
            }), (_vm.showName) ? _c('image', {
                staticClass: ['deleteImg'],
                attrs: {
                    'src': 'root:img/delete.png'
                },
                on: {
                    'click': function($event) {
                        _vm.deleteImg(1);
                    }
                }
            }) : _vm._e()]), _c('div', {
                staticClass: ['inputdiv']
            }, [_c('text', {
                staticClass: ['inputimg']
            }, [_vm._v('密码')]), (!_vm.showPwd) ? _c('input', {
                staticClass: ['input'],
                attrs: {
                    'placeholder': '输入密码',
                    'type': 'password',
                    'value': (_vm.password)
                },
                on: {
                    'input': function($event) {
                        _vm.password = $event.target.attr.value;
                    }
                }
            }) : _c('input', {
                staticClass: ['input'],
                attrs: {
                    'placeholder': '输入密码',
                    'value': (_vm.password)
                },
                on: {
                    'input': function($event) {
                        _vm.password = $event.target.attr.value;
                    }
                }
            }), (_vm.showPwd && _vm.delPwd) ? _c('image', {
                staticClass: ['deleteImg1'],
                attrs: {
                    'src': 'root:img/user/see.png'
                },
                on: {
                    'click': function($event) {
                        _vm.showPwd = !_vm.showPwd;
                    }
                }
            }) : (!_vm.showPwd && _vm.delPwd) ? _c('image', {
                staticClass: ['deleteImg1'],
                attrs: {
                    'src': 'root:img/user/noSee.png'
                },
                on: {
                    'click': function($event) {
                        _vm.showPwd = !_vm.showPwd;
                    }
                }
            }) : _vm._e(), (_vm.delPwd) ? _c('image', {
                staticClass: ['deleteImg'],
                attrs: {
                    'src': 'root:img/delete.png'
                },
                on: {
                    'click': function($event) {
                        _vm.deleteImg(2);
                    }
                }
            }) : _vm._e()])]), _c('div', {
                staticClass: ['btn'],
                on: {
                    'click': _vm.login
                }
            }, [_c('text', {
                staticClass: ['textchar']
            }, [_vm._v('登录')])])]);
        },staticRenderFns: [function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
            return _c('div', {
                staticClass: ['bgimg']
            }, [_c('image', {
                staticClass: ['img'],
                attrs: {
                    'src': 'root:img/logo1.png'
                }
            }), _c('text', {
                staticStyle: {
                    fontSize: '34px',
                    fontWeight: 'bold',
                    marginTop: '30px'
                }
            }, [_vm._v('掌上工作台')])]);
        }]};
        module.exports.render._withStripped = true;
     })
//省略无关代码......
)

此端编译之后的js代码首先是一个模块加载的实现,关于模块加载相关的理解之前在理解webpack模块加载原理 中介绍过,这里不再赘述,这里着重说weex原理相关的。 我们看到自执行函数的最后一行执行了__webpack_require__(__webpack_require__.s = 647),即加载了传入参数中id为647的对象,我们找到它:

647:(function(module, exports, __webpack_require__) {
        var __vue_exports__, __vue_options__;
        var __vue_styles__ = [];
        /* styles 生成样式相关的数据*/
        __vue_styles__.push(__webpack_require__(648)
        );
        /* script 生成vue的options */
        __vue_exports__ = __webpack_require__(649);
        /* template 生成的渲染函数 */
        var __vue_template__ = __webpack_require__(651);
        __vue_options__ = __vue_exports__ = __vue_exports__ || {};
        //下面对__vue_options__的操作就相当于与操作__vue_exports__了
        __vue_options__.render = __vue_template__.render;
        __vue_options__._scopeId = 'data-v-07574e69';
        __vue_options__.style = __vue_options__.style || {};
        __vue_styles__.forEach(function (module) {
            for (var name in module) {
                __vue_options__.style[name] = module[name];
            }
        });
        module.exports = __vue_exports__;
        module.exports.el = 'true';
        //传入的options是一个把template编译成render函数的选项对象
        //下面就是Vue的初始化了,数据监听,挂载vdom结点创建真实视图
        new Vue(module.exports);
     }),

所以这个jsbundle首先会执行这块儿代码,这段代码核心是为vue选项上挂上render函数,然后通过options创建一个Vue实例。 就是Vue的初始化了,数据监听,挂载vdom结点创建真实视图等vue的内部操作了,只是这里的vdom操作创建视图不是浏览器的dom操作而是原生封装的原生创建视图的操作。

weex框架分层理解

  • Vue.js、Rax.js:前端框架

  • weex-js-frameworlk:调用C++native接口,通过JSBrige调用Android和IOS,封装了一系列的dom操作,taskCenter管理回调、模块和组件方法。

  • weexsdk(c++):weex的c++引擎,用于连接Android/ios,封装了统一的dom体系,回调事件管理,模块、组件管理和调用。

  • Android/ios:原生UI、组件,功能模块等。

总结

通过上面的学习和理解,我们可以这样理解weex的实现原理:

  • 首先我们通过weexsdk(c++)把Android和IOS的UI封装成统一的vdom和事件体系;
  • runtime-jsFramework则是对这个vdom和事件体系的进一步封装和实现,它可以直接调用native代码与Android和IOS等平台交互,我们会在weexSdk初始化时运行jsframework的运行时,相当于初始化js执行环境,全局添加weex框架api。
  • 最后是诸如Vue.js和Rax.js的前端框架,它们可以使用runtime-jsFramework封装的vdom和事件体系,就像我们在浏览器端一样使用。

以上就是自己在阅读了weex的runtime-jsFramework和weex-vue-framework的源码后对weex实现原理的总结,如果对您有帮助,还望不吝点赞,如有错误,还望指正,多谢~~~