大家好,
今天通过分析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