本文档介绍了Odoo Javascript框架。 这个框架在代码行方面不是一个大型应用程序,但它很是通用,由于它基本上是一个将声明性接口描述转换为实时应用程序的机器,可以与数据库中的每一个模型和记录进行交互。 甚至可使用Web客户端来修改Web客户端的界面。javascript
Odoo中全部文档字符串的html版本可在如下位置得到:
Javascript APIcss
Javascript框架旨在处理三个主要用例:html
主要是有帐户的用户登陆 ,无帐户数据浏览 , 销售的接口java
简而言之,WebClient的WebClient实例是整个用户界面的根组件。 它的责任是协调全部各类子组件,并提供服务,如rpcs,本地存储等。python
在运行时,Web客户端是单页面应用程序。 每次用户执行操做时,都不须要从服务器请求完整页面。 相反,它只会请求它所需的内容,而后替换/更新视图。 此外,它管理URL:它与Web客户端状态保持同步。jquery
这意味着当用户正在使用Odoo时,Web客户端类(和操做管理器)实际上会建立并销毁许多子组件。 状态是高度动态的,每一个小部件均可以随时销毁。web
在这里,咱们在web / static / src / js插件中快速概述了Web客户端代码。 请注意,故意不是详尽无遗的。 咱们只覆盖最重要的文件/文件夹。ajax
在Odoo中管理资产并不像在其余一些应用程序中那样简单。 其中一个缘由是咱们有各类状况须要一些但不是全部资产。 例如,Web客户端,销售点,网站甚至移动应用程序的需求是不一样的。 此外,一些资产可能很大,但不多须要。 在这种状况下,咱们有时但愿它们被懒惰地加载。chrome
主要思想是咱们在xml中定义一组bundle。 捆绑包在此定义为文件集合(javascript,css,less)。 在Odoo中,最重要的包在addons / web / views / webclient_templates.xml文件中定义。 它看起来像这样:数据库
<template id="web.assets_common" name="Common Assets (used in backend interface and website)"> <link rel="stylesheet" type="text/css" href="/web/static/lib/jquery.ui/jquery-ui.css"/> ... <script type="text/javascript" src="/web/static/src/js/boot.js"></script> ... </template>
而后可使用t-call-assets指令将包中的文件插入到模板中:
<t t-call-assets="web.assets_common" t-js="false"/> <t t-call-assets="web.assets_common" t-css="false"/>
如下是服务器使用这些指令呈现模板时发生的状况:
捆绑包中描述的全部较少的文件都被编译成css文件。 名为file.less的文件将在名为file.less.css的文件中编译。
if we are in debug=assets mode,
将t-js属性设置为false的t-call-assets指令将替换为指向css文件的样式表标记列表
将t-css属性设置为false的t-call-assets指令将替换为指向js文件的脚本标记列表
if we are not in debug=assets mode,
css文件将被链接和缩小,而后分红不超过4096条规则的文件(以克服IE9的旧限制)。 而后,咱们根据须要生成尽量多的样式表标签
将js文件链接并缩小,而后生成脚本标记
请注意,资产文件是缓存的,所以理论上,浏览器只应加载一次。
启动Odoo服务器时,它会检查捆绑包中每一个文件的时间戳,若有必要,将建立/从新建立相应的捆绑包。
如下是大多数开发人员须要了解的一些重要捆绑包:
web.assets_common:此捆绑包包含Web客户端,网站以及销售点通用的大多数资产。 这应该包含odoo框架的低级构建块。 请注意,它包含boot.js文件,该文件定义了odoo模块系统。
web.assets_backend:此捆绑包含特定于Web客户端的代码(特别是Web客户端/操做管理器/视图)
web.assets_frontend:这个包是全部特定于公共网站的:电子商务,论坛,博客,活动管理,......
将位于addons / web中的文件添加到包中的正确方法很简单:只需将文件webclient_templates.xml中的脚本或样式表标记添加到包中便可。 可是当咱们在不一样的插件中工做时,咱们须要从该插件添加一个文件。 在这种状况下,应该分三步完成:
<template id="assets_backend" name="helpdesk assets" inherit_id="web.assets_backend"> <xpath expr="//script[last()]" position="after"> <link rel="stylesheet" href="/helpdesk/static/src/less/helpdesk.less"/> <script type="text/javascript" src="/helpdesk/static/src/js/helpdesk_dashboard.js"></script> </xpath> </template>
请注意,当用户加载odoo Web客户端时,捆绑包中的文件都会当即加载。 这意味着每次都经过网络传输文件(浏览器缓存处于活动状态时除外)。 在某些状况下,延迟加载某些资产可能更好。 例如,若是窗口小部件须要大型库,而且该窗口小部件不是体验的核心部分,那么在实际建立窗口小部件时仅加载库多是个好主意。 widget类实际上内置了对此用例的支持。 (参见QWeb模板引擎部分)
文件可能没法正确加载的缘由有不少。 如下是您能够尝试解决此问题的一些事项:
从新建立资产文件后,您须要刷新页面,从新加载正确的文件(若是不起做用,能够缓存文件)。
一旦咱们可以将javascript文件加载到浏览器中,咱们须要确保它们以正确的顺序加载。 为了作到这一点,Odoo定义了一个小模块系统(位于addons / web / static / src / js / boot.js文件中,须要先加载)。
受AMD启发的Odoo模块系统经过在全局odoo对象上定义函数define来工做。 而后咱们经过调用该函数来定义每一个javascript模块。 在Odoo框架中,模块是一段将尽快执行的代码。 它有一个名称,可能还有一些依赖项。 加载其依赖项后,也会加载一个模块。 而后,模块的值是定义模块的函数的返回值。
例如,它可能以下所示:
// in file a.js odoo.define('module.A', function (require) { "use strict"; var A = ...; return A; }); // in file b.js odoo.define('module.B', function (require) { "use strict"; var A = require('module.A'); var B = ...; // something that involves A return B; });
定义模块的另外一种方法是在第二个参数中明确给出依赖项列表。
odoo.define('module.Something', ['module.A', 'module.B'], function (require) { "use strict"; var A = require('module.A'); var B = require('module.B'); // some code });
若是某些依赖项缺失/未就绪,则不会加载该模块。 几秒钟后控制台会出现警告。
请注意,不支持循环依赖项。 这是有道理的,但这意味着须要当心。
odoo.define方法有三个参数:
moduleName:javascript模块的名称。它应该是一个独特的字符串。惯例是使用odoo插件的名称,而后是特定的描述。例如,'web.Widget'描述了在web插件中定义的模块,该模块导出Widget类(由于第一个字母是大写的)
若是名称不惟一,则会抛出异常并在控制台中显示。
最后,最后一个参数是一个定义模块的函数。它的返回值是模块的值,能够传递给须要它的其余模块。请注意,异步模块有一个小例外,请参阅下一节。
若是发生错误,将在控制台中记录(在调试模式下):
Missing dependencies
:这些模块不会出如今页面中。 JavaScript文件可能不在页面中或模块名称错误Failed modules
:检测到javascript错误Rejected modules
:该模块返回被拒绝的延迟。 它(及其相关模块)未加载。Rejected linked modules
:依赖被拒绝模块的模块Non loaded modules
:依赖于丢失或失败模块的模块模块在准备好以前须要执行一些工做。 例如,它能够执行rpc来加载一些数据。 在这种状况下,模块能够简单地返回延迟(promise)。 在这种状况下,模块系统将在注册模块以前等待延迟完成。
odoo.define('module.Something', ['web.ajax'], function (require) { "use strict"; var ajax = require('web.ajax'); return ajax.rpc(...).then(function (result) { // some code here return something; }); });
web.dom_ready
模块返回一个deferred,当dom实际就绪时将解析。 所以,须要DOM的另外一个模块可能只是在某处有一个require('web.dom_ready')
语句,而代码只会在DOM准备就绪时执行。Odoo是在ECMAScript 6课程开始以前开发的。 在Ecmascript 5中,定义类的标准方法是定义一个函数并在其原型对象上添加方法。 这很好,可是当咱们想要使用继承,mixins时,它有点复杂。
出于这些缘由,Odoo决定使用本身的类系统,灵感来自John Resig。 基类位于web.Class中,位于文件class.js中。
让咱们讨论如何建立类。 主要机制是使用extend方法(这或多或少至关于ES6类中的extend)。
var Class = require('web.Class'); var Animal = Class.extend({ init: function () { this.x = 0; this.hunger = 0; }, move: function () { this.x = this.x + 1; this.hunger = this.hunger + 1; }, eat: function () { this.hunger = 0; }, });
在此示例中,_init_函数是构造函数。 它将在建立实例时调用。 使用new关键字完成实例。
可以继承现有类很方便。 这能够经过在超类上使用extend方法来完成。 调用方法时,框架将秘密从新绑定一个特殊方法:_super到当前调用的方法。 这容许咱们在须要调用父方法时使用this._super。
var Animal = require('web.Animal'); var Dog = Animal.extend({ move: function () { this.bark(); this._super.apply(this, arguments); }, bark: function () { console.log('woof'); }, }); var dog = new Dog(); dog.move()
odoo类系统不支持多重继承,可是对于那些咱们须要共享某些行为的状况,咱们有一个mixin系统:extend方法实际上能够接受任意数量的参数,并将全部这些参数组合在新类中。
var Animal = require('web.Animal'); var DanceMixin = { dance: function () { console.log('dancing...'); }, }; var Hamster = Animal.extend(DanceMixin, { sleep: function () { console.log('sleeping'); }, });
在这个例子中,Hamster类是Animal的子类,但它也混合了DanceMixin。
这并不常见,但咱们有时须要修改另外一个类。 目标是有一个机制来改变一个类和全部将来/如今的实例。 这是经过使用include方法完成的:
var Hamster = require('web.Hamster'); Hamster.include({ sleep: function () { this._super.apply(this, arguments); console.log('zzzz'); }, });
Widget类其实是用户界面的重要构建块。 几乎用户界面中的全部内容都在窗口小部件的控制之下。 Widget类在widget.js中的模块web.Widget中定义。
简而言之,Widget类提供的功能包括:
如下是基本计数器小部件的示例:
var Widget = require('web.Widget'); var Counter = Widget.extend({ template: 'some.template', events: { 'click button': '_onClick', }, init: function (parent, value) { this._super(parent); this.count = value; }, _onClick: function () { this.count++; this.$('.val').text(this.count); }, });
对于此示例,假设模板some.template(而且已正确加载:模板位于文件中,该模板在模块清单中的qweb键中正肯定义)由下式给出:
<div t-name="some.template"> <span class="val"><t t-esc="widget.count"/></span> <button>Increment</button> </div>
此示例窗口小部件能够按如下方式使用:
// Create the instance var counter = new Counter(this, 4); // Render and insert into DOM counter.appendTo(".some-div");
此示例说明了Widget类的一些功能,包括事件系统,模板系统,具备初始父参数的构造函数。
与许多组件系统同样,widget类具备明肯定义的生命周期。 一般的生命周期以下:调用init,而后启动,而后进行渲染,而后启动并最终销毁。
Widget.init(parent)
这是构造函数。 init方法应该初始化小部件的基本状态。 它是同步的,能够被覆盖以从小部件的建立者/父级中获取更多参数
Arguments
parent (Widget()
)-- 新窗口小部件的父窗口,用于处理自动销毁和事件传播。 对于没有父项的窗口小部件,能够为null
。
Widget.willStart()
在建立窗口小部件时以及在附加到DOM的过程当中,此方法将由框架调用一次。 willStart方法是一个应该返回延迟的钩子。 在继续渲染步骤以前,JS框架将等待延迟完成。 请注意,此时,窗口小部件没有DOM根元素。 willStart钩子对于执行某些异步工做(例如从服务器获取数据)很是有用
[Rendering]()
此步骤由框架自动完成。 会发生什么是框架检查是否在窗口小部件上定义了模板键。 若是是这种状况,那么它将使用绑定到渲染上下文中的窗口小部件的窗口小部件键来呈现该模板(请参阅上面的示例:咱们在QWeb模板中使用widget.count来读取窗口小部件中的值)。 若是没有定义模板,咱们读取tagName键并建立相应的DOM元素。 渲染完成后,咱们将结果设置为窗口小部件的$ el属性。 在此以后,咱们会自动绑定events和custom_events键中的全部事件。
Widget.start()
渲染完成后,框架将自动调用start方法。 这对于执行一些专门的后期渲染工做颇有用。 例如,设置库。
必须返回延迟以指示其工做什么时候完成。
Returns: deferred object
Widget.destroy()
这始终是小部件生命中的最后一步。 当一个小部件被销毁时,咱们基本上执行全部必要的清理操做:从组件树中删除小部件,解除全部事件的绑定,......
当窗口小部件的父窗体被销毁时自动调用,若是窗口小部件没有父窗口,或者若是它被删除但其父窗口仍然存在,则必须显式调用。
请注意,不必定要调用willStart和start方法。 能够建立一个小部件(将调用init方法),而后销毁(destroy方法),而没必要将其附加到DOM。 若是是这种状况,则甚至不会调用willStart和start。
Widget.tagName
若是窗口小部件未定义模板,则使用 默认为div,将用做标记名称以建立DOM元素以设置为窗口小部件的DOM根。 可使用如下属性进一步自定义今生成的DOM根:
Widget.id
用于在生成的DOM根上生成id属性。 请注意,这不多须要,若是窗口小部件能够屡次使用,可能不是一个好主意。
Widget.className
用于在生成的DOM根上生成类属性。 请注意,它实际上能够包含多个css类:'some-class other-class'
Widget.attributes
将属性名称(对象文字)映射到属性值。 这些k:v对中的每个将被设置为生成的DOM根上的DOM属性。
Widget.el
原始DOM元素设置为窗口小部件的根(仅在启动生命周期方法以后可用)
Widget.template
应设置为QWeb模板的名称。 若是设置,模板将在窗口小部件初始化以后但在启动以前呈现。 模板生成的根元素将设置为窗口小部件的DOM根。
xmlDependencies
在呈现窗口小部件以前须要加载的xml文件的路径列表。 这不会致使加载任何已加载的东西。
events
事件是事件选择器(事件名称和由空格分隔的可选CSS选择器)到回调的映射。 回调能够是窗口小部件方法或函数对象的名称。 在任何一种状况下,this
都将设置为小部件:
events: { 'click p.oe_some_class a': 'some_method', 'change input': function (e) { e.stopPropagation(); } },
选择器用于jQuery的事件委托,只有与选择器匹配的DOM根的后代才会触发回调。 若是省略了选择器(仅指定了事件名称),则将直接在窗口小部件的DOM根上设置事件。
注意:不鼓励使用内联函数,未来有时可能会删除它。
custom_events
这几乎与events属性相同,但键是任意字符串。 它们表示由某些子窗口小部件触发的业务事件。 当一个事件被触发时,它将“冒泡”小部件树(有关更多详细信息,请参阅有关组件通讯的部分)。
Widget.isDestroyed()
Returns:
true
若是小部件正在被销毁或被销毁,不然为假
Widget.$(selector)
将指定为参数的CSS选择器应用于窗口小部件的DOM根目录:
this.$(selector);
在功能上与:
this.$el.find(selector);
Arguments:
selector (String) -- CSS selector
Returns: jQuery object
这个帮助方法相似于Backbone.View.$
Widget.setElement(element)
将窗口小部件的DOM根从新设置为提供的元素,还处理从新设置DOM根的各类别名以及取消设置和从新设置委派事件。
Arguments:
element (Element) -- a DOM element or jQuery object to set as the widget's DOM root 要设置为窗口小部件DOM根的DOM元素或jQuery对象
Widget.appendTo(element)
呈现窗口小部件并将其做为目标的最后一个子项插入,使用.appendTo()
Widget.prependTo(element)
呈现窗口小部件并将其做为目标的第一个子项插入,使用.prependTo()
Widget.insertAfter(element)
渲染窗口小部件并将其做为目标的前一个兄弟插入,使用.insertAfter()
Widget.insertBefore(element)
渲染窗口小部件并将其做为目标的如下兄弟插入,使用.insertBefore()
全部这些方法都接受相应的jQuery方法接受的任何内容(CSS选择器,DOM节点或jQuery对象)。 他们都返回延期,并负责三项任务:
rendering the widget's root element via(渲染窗口小部件的根元素经过)
renderElement()
启动窗口小部件,并返回启动它的结果
id
限制了组件的可重用性,而且每每使代码更脆弱。 大多数状况下,它们能够替换为任何内容,类或保持对DOM节点或jQuery元素的引用。若是id
是绝对必要的(由于第三方库须要一个),则应使用_.uniqueId()
部分生成id,例如:
this.id = _.uniqueId('my-widget-');
避免使用可预测/常见的CSS类名。 诸如“内容”或“导航”之类的类名称可能与所需的含义/语义相匹配,但极可能其余开发人员具备相同的需求,从而产生命名冲突和意外行为。 通用类名称应以例如前缀为例。 它们所属组件的名称(建立“非正式”命名空间,就像在C或Objective-C中同样)。
应避免使用全局选择器。 因为组件可能在单个页面中屡次使用(Odoo中的示例是仪表板),所以查询应限制在给定组件的范围内。 未过滤的选择(例如$(selector)
或document.querySelectorAll(selector)
一般会致使意外或不正确的行为。 Odoo Web的Widget()
具备提供其DOM根($ el
)的属性,以及直接选择节点的快捷方式($()
)。
更通常地说,永远不要假设您的组件拥有或控制超出其我的$ el
的任何内容(所以,避免使用对父窗口小部件的引用)
除非绝对琐碎,不然Html模板/渲染应该使用QWeb。
全部交互式组件(向屏幕显示信息或拦截DOM事件的组件)必须从Widget()
继承并正确实现和使用其API和生命周期。
Web客户端使用QWeb模板引擎来呈现窗口小部件(除非它们覆盖renderElement方法以执行其余操做)。 Qweb JS模板引擎基于XML,而且主要与python实现兼容。
如今,让咱们解释一下如何加载模板。 每当Web客户端启动时,都会对/ web / webclient / qweb路由创建一个rpc。 而后,服务器将返回每一个已安装模块的数据文件中定义的全部模板的列表。 每一个模块清单中的qweb条目中都列出了正确的文件。
在启动第一个小部件以前,Web客户端将等待加载该模板列表。
这种机制能够很好地知足咱们的需求,但有时候,咱们想要延迟加载模板。 例如,假设咱们有一个不多使用的小部件。 在这种状况下,咱们可能不但愿在主文件中加载其模板,以使Web客户端稍微轻一些。 在这种状况下,咱们可使用Widget的xmlDependencies键:
var Widget = require('web.Widget'); var Counter = Widget.extend({ template: 'some.template', xmlDependencies: ['/myaddon/path/to/my/file.xml'], ... });
有了这个,Counter小部件将在其willStart方法中加载xmlDependencies文件,所以在执行渲染时模板将准备就绪。
目前Odoo支持两种事件系统:一个容许添加监听器和触发事件的简单系统,以及一个更完整的系统,它也会使事件“冒泡”。
这两个事件系统都在事件mixins.js中的EventDispatcherMixin中实现。 这个mixin包含在Widget类中。
这个事件系统在历史上是第一个。 它实现了一个简单的总线模式。 咱们有4种主要方法:
如下是有关如何使用此事件系统的示例:
var Widget = require('web.Widget'); var Counter = require('myModule.Counter'); var MyWidget = Widget.extend({ start: function () { this.counter = new Counter(this); this.counter.on('valuechange', this, this._onValueChange); var def = this.counter.appendTo(this.$el); return $.when(def, this._super.apply(this, arguments); }, _onValueChange: function (val) { // do something with val }, }); // in Counter widget, we need to call the trigger method: ... this.trigger('valuechange', someValue);