ODOO中Web组件解说(系列一)

原创
2021/03/21 10:04
阅读数 1.4K

大家好,

     今天通过分析odoo 的Web组件原生代码,对web 组件进行解说。

     首先,web 组件,主要由以下组件构成:

  •      core 模块:引用config、class、web、registry等组件
  •      bus 模块:动作绑定
  •      registery 模块:注册表
  •      config 模块:配置
  •      dom 模块:进行绑定
  •      env模块:环境变量
  •       action_manaer 模块:这个是核心,下一章单独讲解
  •       models模块:模型
  •       ....

 

我们会在接下来的介绍中详细说明。

Core 模块

//首先声明core

odoo.define('web.core', function (require) {

"use strict";

// 引用 BUS、Config、Class、QWeb、Registry

// 这里说明一下这些组件的含义:

// - BUS(总线):更加准确地讲,是动作总线。用来绑定动作当当前实例来。

// - Config (配置):这个非常好理解,任何系统都需要初始化配置。

// - Class(类):因为,JavaScript中没有继承这个功能,所以,需要依靠Class组件来完成这个需求。

// - QWeb: 用来渲染Web视图的模板引擎。

// - Registry(注册表):注册表实际上只是从一些键到一些值的映射。

// 在后面的章节中,会详细的讲述它们的功能。

var Bus = require('web.Bus');

var config = require('web.config');

var Class = require('web.Class');

var QWeb = require('web.QWeb');

var Registry = require('web.Registry');

var translation = require('web.translation');

var bus = new Bus();



// 在有 点击、向下键、按键等等动作时,触发’动作总线’,并传达是由哪个动作触发的

_.each('click,dblclick,keydown,keypress,keyup'.split(','), function (evtype) {

    $('html').on(evtype, function (ev) {

        bus.trigger(evtype, ev); });

});

_.each('resize,scroll'.split(','), function (evtype) {

    $(window).on(evtype, function (ev) {

        bus.trigger(evtype, ev);

    });

});



return {

    qweb: new QWeb(config.isDebug()),

    // 核心类与方法

    Class: Class,

    bus: bus,

    main_bus: new Bus(),

    // 注册表

    action_registry: new Registry(),

    crash_registry: new Registry(),

    serviceRegistry: new Registry(),

};

});

BUS模块

//声明 BUS
odoo.define('web.Bus', function (require) {
"use strict";
//引用 Class 及 mixins
var Class = require('web.Class');
var mixins = require('web.mixins');
/*更加准确的说法是,Event Bus(动作总线),用来将动作绑定至当前实例的域下。 */
return Class.extend(mixins.EventDispatcherMixin, {
init: function (parent) {
    //下面这个方法,来自 minxin 里
        mixins.EventDispatcherMixin.init.call(this);
        this.setParent(parent);
    },});});

 

Registry 模块

odoo.define("web.Registry", function (require) {

    "use strict";

    const { sortBy } = require("web.utils");

    /**

     

注册表实际上只是从一些键到一些值的映射。Registry类只添加了一些简单的方法,以使其更好、更安全。



     * 请注意,注册表有一个基本问题:您试图在注册表中获取的值可能尚未添加,因此当然,您需要确保您的依赖关系是可靠的。因此,如果您可以简单地用'require'语句导入所需内容,那么最好避免使用注册表。

     *

    然而,另一方面,有时你不能简单地导入一些东西,因为我们会有一个依赖循环。在这种情况下,注册可能会有所帮助。

     */

    class Registry {

        /**函数predicate,参数 {any} 值 ,返回 {boolean}

         * 参数{Object} [mapping] 注册表中的初始值 @param {predicate} [predicate=(() => true)] 预测每个添加的值都需要通过来被注册

         */

        constructor(mapping, predicate = () => true) {

            this.map = Object.create(mapping || null);

            this._scoreMapping = Object.create(null);

            this._sortedKeys = null;

            this.listeners = []; // 监听新加项目的回调 

            this.predicate = predicate;

        }

        /**

         * 向注册表添加键值(一个值). 通知监听器,关于注册表中的新加入项目。

         * @param {string} 键

         * @param {any} 值

         * @param {number} [score] 如果提供,此值会被用于键值排序.

         * @returns {Registry} 可以用于链调用.

         */

        add(key, value, score) {

            if (!this.predicate(value)) {

                throw new Error(`Value of key "${key}" does not pass the addition predicate.`);

            }

            this._scoreMapping[key] = score === undefined ? key : score;

            this._sortedKeys = null;

            this.map[key] = value;

            for (const callback of this.listeners) {

                callback(key, value);

            }

            return this;

        }



        /**

         * 检查注册表中是否已经包含此值 。@param {string} 键 @returns {boolean}

         */

        contains(key) {

            return key in this.map;

        }



        /**

         * 返回注册表中的内容(与键值所对应的对象)

         * @returns {Object}

         */

        entries() {

            const entries = {};

            const keys = this.keys();

            for (const key of keys) {

                entries[key] = this.map[key];

            }

            return entries;

        }



        /**

         * 返回与键所对应的值.

         * @param {string} key

         * @returns {any}

         */

        get(key) {

            return this.map[key];

        }



        /**

尝试一连串键,返回第一个匹配的键.

         * @param {string[]} 检索对象的一系列键 @returns {any} 找到的第一个结果与对象匹配

         */

        getAny(keys) {

            for (const key of keys) {

                if (key in this.map) {

                    return this.map[key];

                }

            }

            return null;

        }



        /**

         * 返回映射对象中的键列表。注册表保证键具有一致的顺序,在添加项时由“score”值定义。

         * @returns {string[]}

         */

        keys() {

            if (!this._sortedKeys) {

                const keys = [];

                for (const key in this.map) {

                    keys.push(key);

                }

                this._sortedKeys = sortBy(keys,

                    key => this._scoreMapping[key] || 0

                );

            }

            return this._sortedKeys;

        }



        /**

         * @function onAddCallback

         * @param {string} 键

         * @param {any} 值

         * 注册一个回调,以便在将项添加到注册表时执行。         * @param {onAddCallback} 带参数(键、值)的回调函数。         */

        onAdd(callback) {

            this.listeners.push(callback);

        }



        /**

         *返回映射对象中的值列表         * @returns {string[]}

         */

        values() {

            return this.keys().map((key) => this.map[key]);

        }

    }



    return Registry;

});

 

Config 模块

//声明config

odoo.define('web.config', function (require) {

"use strict";

const Bus = require('web.Bus');

const bus = new Bus();

/*此模块包含(绝大部分)的静态 '环境' 信息. 这些信息,大多数时候是让 Web 客户端实现正确渲染的必要元素。

 注意:大部分存储在 session 中的文件,将在未来移动至此。 */

var config = {

    device: {

        / 总线, 用来处理设备配置相关的活动

         - 'size_changed' : 当窗口大小被新的关联bootstrap断点触发时,新的尺寸类会被加载。*/

        bus: bus,

        /*触摸判断,True 表示此设备支持触摸*/

        touch: 'ontouchstart' in window || 'onmsgesturechange' in window,

        /* size_class 是: 0, 1, 2, 3 或者 4的整数, 取决于当前设备尺寸. 此属性是动态的,在当浏览器尺寸变化时更新*/

        size_class: null,

        /*常见的使用场景是不同的“移动端”模式. 当尺寸是移动端XS/VSM/SM. 会被动态更新。@type Boolean/

        isMobile: null,

        /*通过使用 userAgent判断是否为移动端. 此标记不取决于屏幕的size/resolution.主要瞄准有虚拟键盘的移动设备。@return {boolean}*/

        isMobileDevice: navigator.userAgent.match(/Android/i) ||

            navigator.userAgent.match(/webOS/i) ||

            navigator.userAgent.match(/iPhone/i) ||

            navigator.userAgent.match(/iPad/i) ||

            navigator.userAgent.match(/iPod/i) ||

            navigator.userAgent.match(/BlackBerry/i) ||

            navigator.userAgent.match(/Windows Phone/i),

        /**数字0,1,2,3,4,5,6 与描述的映射         */

        SIZES: { XS: 0, VSM: 1, SM: 2, MD: 3, LG: 4, XL: 5, XXL: 6 },

    },

    /**表明当前模式为 Debug 模式或者不是. @param 不同的Debug 模式Check,为空表示为基础Debug 模式 @returns {boolean}

     */

    isDebug: function (debugMode) {

        if (debugMode) {

            return odoo.debug && odoo.debug.indexOf(debugMode) !== -1;

        }

        return odoo.debug;

    },

};



var medias = [

    window.matchMedia('(max-width: 474px)'),

    window.matchMedia('(min-width: 475px) and (max-width: 575px)'),

    window.matchMedia('(min-width: 576px) and (max-width: 767px)'),

    window.matchMedia('(min-width: 768px) and (max-width: 991px)'),

    window.matchMedia('(min-width: 992px) and (max-width: 1199px)'),

    window.matchMedia('(min-width: 1200px) and (max-width: 1533px)'),

    window.matchMedia('(min-width: 1534px)'),

];

/*返回当前尺寸的类别. @returns {integer} 在 0 和 5之间/

function _getSizeClass() {

    for (var i = 0 ; i < medias.length ; i++) {

        if (medias[i].matches) {

            return i;

        }

    }

}

/**更新在 config 对象中的 尺寸依赖属性. 每次尺寸更新时,都需要调用。 */

function _updateSizeProps() {

    var sc = _getSizeClass();

    if (sc !== config.device.size_class) {

        config.device.size_class = sc;

        config.device.isMobile = config.device.size_class <= config.device.SIZES.SM;

        config.device.bus.trigger('size_changed', config.device.size_class);

    }

}



_.invoke(medias, 'addListener', _updateSizeProps);

_updateSizeProps();



return config;



});

 

ENV模块(引用 common_env)

odoo.define("web.env", function (require) {

    "use strict";

    /**此文件定义用于webclient 的env变量 */

    const commonEnv = require('web.commonEnv');

    const dataManager = require('web.data_manager');

    const { blockUI, unblockUI } = require("web.framework");



    const env = Object.assign(commonEnv, { dataManager });

    env.services = Object.assign(env.services, { blockUI, unblockUI });



    return env;

});

COMMON ENV模块

odoo.define("web.commonEnv", function (require) {

    "use strict";

    /**

这个文件定义了公共环境,它包含了env中后端和前端所需的一切(Odoo术语)。此模块不应按原样使用。它只能由定义要使用的最终env的模块导入(在前端或后端)。例如,模块网站.env'导入它,向其中添加内容,并导出整个webclient应用程序使用的最终env。env对象中应该有尽可能多的依赖项。这样可以更容易地测试组件。     *

     */



    const { jsonRpc } = require("web.ajax");

    const { device, isDebug } = require("web.config");

    const { bus } = require("web.core");

    const rpc = require("web.rpc");

    const session = require("web.session");

    const { _t } = require("web.translation");

    const utils = require("web.utils");



    const browser = {

        clearInterval: window.clearInterval.bind(window),

        clearTimeout: window.clearTimeout.bind(window),

        Date: window.Date,

        fetch: (window.fetch || (() => { })).bind(window),

        Notification: window.Notification,

        requestAnimationFrame: window.requestAnimationFrame.bind(window),

        setInterval: window.setInterval.bind(window),

        setTimeout: window.setTimeout.bind(window),

    };

    Object.defineProperty(browser, 'innerHeight', {

        get: () => window.innerHeight,

    });

    Object.defineProperty(browser, 'innerWidth', {

        get: () => window.innerWidth,

    });



    // 构建基础 env

    const env = {

        _t,

        browser,

        bus,

        device,

        isDebug,

        qweb: new owl.QWeb({ translateFn: _t }),

        services: {

            ajaxJsonRPC() {

                return jsonRpc(...arguments);

            },

            getCookie() {

                return utils.get_cookie(...arguments);

            },

            httpRequest(route, params = {}, readMethod = 'json') {

                const info = {

                    method: params.method || 'POST',

                };

                if (params.method !== 'GET') {

                    const formData = new FormData();

                    for (const key in params) {

                        if (key === 'method') {

                            continue;

                        }

                        const value = params[key];

                        if (Array.isArray(value) && value.length) {

                            for (const val of value) {

                                formData.append(key, val);

                            }

                        } else {

                            formData.append(key, value);

                        }

                    }

                    info.body = formData;

                }

                return fetch(route, info).then(response => response[readMethod]());

            },

            navigate(url, params) {

                window.location = $.param.querystring(url, params);

            },

            reloadPage() {

                window.location.reload();

            },

            rpc(params, options) {

                const query = rpc.buildQuery(params);

                return session.rpc(query.route, query.params, options);

            },

            setCookie() {

                utils.set_cookie(...arguments);

            },

        },

        session,

    };



    return env;

});





环境信息中的内容

环境中选填键值的用例是:

  • 一些配置密钥
  • 会话信息
  • 通用服务(如做RPC)

样做意味着组件很容易测试:我们可以简单地用模拟服务创建一个测试环境。

例如:

async function myEnv() {
  const templates = await loadTemplates();
  const qweb = new QWeb({ templates });
  const session = getSession();

  return {
    _t: myTranslateFunction,
    session: session,
    qweb: qweb,
    services: {
      localStorage: localStorage,
      rpc: rpc,
    },
    debug: false,
    inMobileMode: true,
  };}
async function start() {
  const env = await myEnv();
  mount(App, { target: document.body, env });}

 

好了,今天就到这里。

欢迎关注我们,下一节更加精彩!

OpenERP.HK Team

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
0 评论
0 收藏
0
分享
返回顶部
顶部