odoo javascript

本文介绍了odoo javascript框架。从代码行的角度来看,这个框架不是一个大的应用程序,但它是很是通用的,由于它基本上是一个将声明性接口描述转换为活动应用程序的机器,可以与数据库中的每一个模型和记录交互。甚至可使用Web客户端修改Web客户端的接口。javascript

这里有一个有用的html版本的文档:Javascript APIcss

概览

这个Javascript框架主要设计用于三个地方使用:html

  • web客户端:这是一个私有的web应用,能够在其中查看和编辑业务数据。这是一个单页应用程序(永远不会从新加载该页,只在须要时从服务器提取新数据)。
  • 网站:这是Odoo的公共部分。它容许身份不明的用户做为客户端浏览某些内容、购物或执行许多操做。这是一个经典的网站:各类各样的带有控制器的路由和共同协做的Javascript代码。
  • POS:这是销售点的接口。它是一个特定的但也应用程序。

Web客户端

单页应用

简而言之,webclient实例是整个用户界面的根组件。它的职责是协调全部的子组件,并提供服务,如RPC、本地存储等等。java

在运行时,Web客户端是单页应用程序。每次用户执行操做时,它不须要从服务器请求整页。相反,它只请求它所须要的,而后替换/更新视图。此外,它还管理URL:它与Web客户机状态保持同步。python

这意味着,当用户在处理odoo时,Web客户机类(和动做管理器)实际上建立并销毁了许多子组件。状态是高度动态的,每一个小部件均可以随时销毁。jquery

Web客户端JS代码概览

这里,咱们在web/static/src/js插件中快速概述了web客户机代码。注意,这是故意不详尽的,咱们只涉及最重要的文件/文件夹。web

  • boot.js : 这是定义模块系统的文件,它须要首先加载。
  • core/ : 这是较低级别的构建基块的集合。值得注意的是,它包含类系统、小部件系统、并发实用程序和许多其余类/函数。
  • chorm/ :在这个文件夹中,咱们有大多数大的小部件,它们构成了大部分用户界面。
  • chrome/abstract_web_client.js and chrome/web_client.js : 这些文件一块儿定义了WebClient小部件(widget),它是Web客户机的根小部件(wideget)。
  • chrome/action_manager.js : 这是将动做(action)转换为小部件(widget)(例如看板或表单视图)的代码。
  • chrome/search_X.js : 全部这些文件定义了搜索视图(它不是Web客户机视图中的视图,仅从服务器视图)
  • fields : 这里定义了全部主要字段视图小部件(widget)
  • views : 这是视图所在的位置

资源管理

在Odoo中管理资源并不像在其余应用程序中那样简单。其中一个缘由是,在其中一些状况中咱们有各类各样的状态,但不是全部的资源都是必需的。例如,Web客户端、销售点、网站甚至移动应用程序的需求是不一样的。此外,有些资源可能很大,但不多须要。在这种状况下,咱们有时但愿它们被懒惰地加载。ajax

主要思想是咱们用XML定义一组包。捆绑包在这里定义为一组文件(javascript、css、scss)。在odoo中,最重要的包在addons/web/views/webclient_templates.xml文件中定义。看起来是这样的:chrome

<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"/> 

下面是当服务器使用如下指令呈现模板时发生的状况:

  • 包中描述的全部SCSS文件都编译为CSS文件。名为file.scss的文件将编译在名为file.scss.css的文件中。
  • 若是咱们在debug=assets模式
       * t-js属性设置为false的t-call-assets指令将替换为指向css文件的样式表标记列表。
       * t-css属性设置为false的t-call-assets指令将替换为指向JS文件的脚本标记列表。
  • 若是咱们不在debug=assets模式
       * CSS文件将被链接并缩小,而后拆分为不超过4096个规则的文件(以绕过IE9的旧限制)。而后,咱们根据须要生成尽量多的样式表标签
       * JS文件被链接并缩小,而后生成一个脚本标记。

请注意,资源文件是缓存的,所以从理论上讲,浏览器应该只加载它们一次。

主包

当odoo服务器启动时,它检查包中每一个文件的时间戳,若是须要,它将建立/从新建立相应的包。

如下是大多数开发人员须要知道的一些重要包:

  • web.assets_common : 此包包含Web客户端、网站以及销售点(POS)所共有的大多数资源。这应该包含用于Odoo框架的较低级别的构建块。注意,它包含boot.js文件,它定义了odoo模块系统。
  • web.assets_backend :这个包包含特定于Web客户端的代码(特别是Web客户端/动做管理器/视图)
  • web.assets_frontend :这个包是关于全部特定于公共网站的:电子商务、论坛、博客、事件管理…

在一个资源包里添加文件

将位于addons/web中的文件添加到bundle的正确方法很简单:只需将脚本或样式表标记添加到文件webclient_templates.xml中的bundle便可。可是当咱们使用不一样的插件(addon)时,咱们须要从该插件添加一个文件。在这种状况下,应分三步进行:

  1. 添加一个 assets.xml 文件到views/文件夹
  2. 添加字符'views/assets.xml' 到manifest文件的键'data'的值里
  3. 建立所需包的继承视图,并使用xpath表达式添加文件。例如:
<template id="assets_backend" name="helpdesk assets" inherit_id="web.assets_backend"> <xpath expr="//script[last()]" position="after"> <link rel="stylesheet" type="text/scss" href="/helpdesk/static/src/scss/helpdesk.scss"/> <script type="text/javascript" src="/helpdesk/static/src/js/helpdesk_dashboard.js"></script> </xpath> </template> 

请注意,当用户加载odoo web客户端时,包中的全部文件都会当即加载。这意味着每次经过网络传输文件(浏览器缓存处于活动状态时除外)。在某些状况下,最好使用Lazyload的一些资产。例如,若是一个小部件须要一个大的库,而这个小部件不是体验的核心部分,那么在实际建立小部件时,最好只加载库。widget类实际上已经为这个用例内置了支持。(查阅QWeb模板引擎部分)

若是文件没有加载/更新应该怎么办

文件可能没法正确加载有许多不一样的缘由。您能够尝试如下几点来解决此问题:

  • 一旦服务器启动,它就不知道资源文件是否已被修改。所以,您能够简单地从新启动服务器来从新生成资源。
  • 检查控制台(在开发工具中,一般用F12打开),确保没有明显的错误
  • 尝试在文件的开头添加console.log(在任何模块定义以前),这样您就能够查看文件是否已加载。
  • 在用户界面中,在调试模式下(在此处插入连接到调试模式),有一个选项能够强制服务器更新其资源文件。
  • 使用debug=assets模式。这实际上会绕过资源包(请注意,它实际上并不能解决问题,服务器仍然使用过期的包)
  • 最后,对于开发人员来讲,最方便的方法是使用--dev=all选项启动服务器。这将激活文件监视程序选项,必要时将自动使资源无效。请注意,若是操做系统是Windows,它就不能很好地工做。
  • 记住刷新页面!
  • 或者保存代码文件…

从新建立资源文件后,须要刷新页面,从新加载正确的文件(若是不起做用,文件可能被缓存了)。

Javascript模块系统

一旦咱们可以将咱们的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插件(addon)的名字后面加上一个具体的描述。例如,“web.widget”描述在web插件中定义的模块,该模块导出一个widget类(由于第一个字母大写)。
    若是名称不惟一,将引起异常并显示在控制台中
  • dependencies : 第二个参数是可选的。若是给定,它应该是一个字符串列表,每一个字符串对应一个JavaScript模块。这描述了在执行模块以前须要加载的依赖项。若是这里没有明确地给出依赖项,那么模块系统将经过调用ToString从函数中提取它们,而后使用regexp查找全部Require语句。
  • 最后一个参数是定义模块的函数。它的返回值是模块的值,能够传递给其余须要它的模块。注意,异步模块有一个小的异常,请参见下一节。
    若是发生错误,将在控制台中记录(在调试模式下):
  • Missing dependencies: 这些模块不会出如今页面中。多是javascript文件不在页面中或模块名称错误
  • Failed Modules : 一个Javascript错误被检测到
  • Rejected modules :块返回拒绝的延迟。它(及其相关模块)未加载。
  • Rejected linked modules: 依赖被拒绝模块的模块
  • Non loaded modules : 模块依赖了一个缺失/失败的模块

异步模块

模块可能须要在准备就绪以前执行一些工做。例如,它能够作一个RPC来加载一些数据。在这种状况下,模块只需返回一个deferred(promise)。在这种状况下,模块系统只需等待deferred完成,而后注册模块。

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实际就绪时,这个deferred 将被解决。所以,另外一个须要dom的模块能够在某个地方简单地有一个require(“web.dom_ready”)语句,而且只有当dom准备好时才会执行代码。
  • 尽可能避免在一个文件中定义多个模块。这在短时间内可能很方便,但实际上很难维护。

类系统

Odoo是在ECMAScript 6类可用以前开发的。在ECMAScript 5中,定义类的标准方法是定义一个函数并在其原型对象上添加方法。这很好,可是当咱们想要使用继承、混合时,它稍微复杂一些。
出于这些缘由,Odoo决定使用本身的类系统,这是受到约翰·雷西格的启发。基类位于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类系统不支持多重继承,可是对于那些须要共享某些行为的状况,咱们有一个混合系统:extend方法实际上能够接受任意数量的参数,并将它们组合到新的类中。

var Animal = require('web.Animal'); var DanceMixin = { dance: function () { console.log('dancing...'); }, }; var Hamster = Animal.extend(DanceMixin, { sleep: function () { console.log('sleeping'); }, }); 

在这个例子中,Hamter 类是Animal的子类,可是它也混合了DanceMixin.

修改现有的类

这并不常见,但有时咱们须要在适当的位置修改另外一个类。目标是有一个机制来改变一个类和全部将来/如今的实例。这是经过使用include方法完成的:

var Hamster = require('web.Hamster'); Hamster.include({ sleep: function () { this._super.apply(this, arguments); console.log('zzzz'); }, }); 

这显然是一个危险的操做,应该当心操做。可是,按照odoo的结构,有时须要在一个插件中修改在另外一个插件中定义的widget/class的行为。请注意,它将修改类的全部实例,即便它们已经建立。

小部件(Widget)

widget类其实是用户界面的一个重要构建块。几乎用户界面中的全部内容都在小部件(widget)的控制之下。widget类在widget.js中的module web.widget中定义。
简而言之,widget类提供的特性包括:

  • 小部件之间的父/子关系(PropertiesMixin)
  • **具备安全功能的普遍生命周期管理 **(e.g. 在销毁父级期间自动销毁子窗口小部件)
  • 自动渲染qweb模板
  • 帮助与外部环境交互的各类实用功能。
    一个计数的小部件例子:
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> 

这个例子说明了小部件类的一些特性,包括事件系统、模板系统、带有初始父参数的构造函数。

小部件的生命周期

与许多组件系统同样,widget类有一个定义良好的生命周期。一般的生命周期以下:调用init,而后willStart,而后rendering,而后start,最后destroy。

Widget.init(parent)
这是构造函数。init方法应该初始化小部件的基本状态。它是同步的,能够被重写以从小部件的建立者/父对象获取更多参数。

Arguments : parent(Widget())–新的widget的父级,用于处理自动销毁和事件传播。对于没有父级的小部件,能够为null

Widget.willStart()
当一个小部件被建立并被附加到DOM的过程当中,框架将调用这个方法一次。willstart方法是一个钩子,它应该返回一个deferred。JS框架将等待这个deferred完成,而后再继续渲染步骤。注意,此时小部件没有dom根元素。willstart钩子主要用于执行一些异步工做,例如从服务器获取数据。

[Rendering]()
此步骤由框架自动完成。框架会检查小部件上是否认义了template键。若是定义了,那么它将在呈现上下文中使用绑定到小部件的widget键呈现该模板(请参见上面的示例:咱们在QWeb模板中使用widget.count来读取小部件的值)。若是没有定义模板,则读取 tagName 键并建立相应的DOM元素。渲染完成后,咱们将结果设置为小部件的$el属性。在此以后,咱们将自动绑定events和custom_events键中的全部事件。

Widget.start()
渲染完成后,框架将自动调用Start方法。这对于执行一些特殊的后期渲染工做颇有用。例如,设置库。
必须返回deferred以指示其工做什么时候完成。

Returns: deferred 对象

Widget.destroy()
这始终是小部件生命周期中的最后一步。当小部件被销毁时,咱们基本上执行全部必要的清理操做:从组件树中删除小部件,取消绑定全部事件,…
当小部件的父级被销毁时自动调用,若是小部件没有父级,或者若是它被删除但父级仍然存在,则必须显式调用。

请注意,没必要调用willstart和start方法。能够建立一个小部件(将调用init方法),而后销毁(destroy方法),而不须要附加到DOM。若是是这种状况,将不会调用will start和start。

Widget API

Widget.tagName
若是小部件没有定义模板,则使用。默认为DIV,将用做标记名来建立要设置为小部件的dom根的dom元素。可使用如下属性进一步自定义生成的dom根目录:

Widget.id
用于在生成的dom根上生成id属性。请注意,这是不多须要的,若是一个小部件能够屡次使用,这可能不是一个好主意。

Widget.className
用于在生成的dom根上生成class属性。请注意,它实际上能够包含多个css类:“some-class other-class”

Widget.attributes
属性名到属性值的映射(对象文本)。这些k:v对中的每个都将被设置为生成的dom根上的dom属性。

Widget.el
将原始dom元素设置为小部件的根(仅在start lifecyle方法以后可用)

Widget.$el
jquery封装的el,(仅在Start Lifecyle方法以后可用)

Widget.template
应设置为QWeb模板的名称。若是设置了,模板将在小部件初始化以后但在其启动以前呈现。模板生成的根元素将被设置为小部件的dom根。

Widget.xmlDependencies
呈现小部件以前须要加载的XML文件的路径列表。这不会致使加载已加载的任何内容。若是您想延迟加载模板,或者想要在网站和Web客户机界面之间共享一个小部件,这颇有用。

var EditorMenuBar = Widget.extend({ xmlDependencies: ['/web_editor/static/src/xml/editor.xml'], ... 

Widget.events
事件是事件选择器(由空格分隔的事件名称和可选CSS选择器)到回调的映射。回调能够是小部件方法或函数对象的名称。在这两种状况下,这都将设置为小部件:

'click p.oe_some_class a': 'some_method', 'change input': function (e) { e.stopPropagation(); } }, 

选择器用于jquery的事件委托,回调只对与选择器匹配的dom根的后代触发。若是选择器被省略(只指定了一个事件名),那么事件将直接设置在小部件的dom根上。
注意:不鼓励使用内联函数,未来可能会删除它。

Widget.custom_events

returns: true 若是小部件正在或者已经被销毁,不然false

Widget.$(selector)
将指定为参数的CSS选择器应用于小部件的dom根:
this.$(selector);
功能上与如下相同:
this.$el.find(selector);

arguments: selector(string)-CSS选择器
return:jQuery 对象

这个助手方法相似于Backbone.View.$

Widget.setElement(element)
将小部件的dom根从新设置为提供的元素,还处理从新设置dom根的各类别名以及取消设置和从新设置委托事件。

arguments: element(Element) -一个DOM元素或者jQuery对象设置为小部件的根DOM

在DOM中插入一个小部件

Widget.appendTo(element)
渲染小部件并将它做为子元素插入到目标元素后面,使用.appentTo()

Widget.prependTo(element)
渲染小部件并将它做为子元素插入到目标元素前面,使用.prependTo()

Widget.insertAfter(element)
渲染小部件并将它做为目标元素的前一个同级插入,使用.insertAfter()

Widget.insertBefore(element)
渲染小部件并将其做为目标的后一个同级插入,使用.insertBefore()

全部这些方法都接受相应jquery方法接受的任何内容(css选择器、dom节点或jquery对象)。他们都会返回一个 deferred,并承担三个任务:

  • 经过如下方式呈现小部件的根元素:
    renderElement()
  • 使用jquery在DOM中插入小部件的根元素
    匹配的方法
  • 启动小部件并返回启动结果

小部件指南

  • 在通常应用和模块中中,标识 (id 属性)应该避免使用,ID限制了组件的可重用性,并使代码更加脆弱。大多数状况下,它们能够替换为Nothing、Classes或保留对dom节点或jquery元素的引用。
    若是ID是绝对必要的(由于第三方库须要一个),则应使用_.uniqueId()部分生成ID,例如:
    this.id = _.uniqueId('my-widget-');
  • 避免使用可预测/通用的CSS类名。类名称(如“content”或“navigation”)可能与所需的含义/语义匹配,但其余开发人员可能也有相同的需求,从而形成命名冲突和意外行为。通用类名的前缀应该是它们所属组件的名称(建立“非正式”名称空间,就像在C或Objective-C中那样)。
  • 应避免使用全局选择器。由于一个组件能够在一个页面中屡次使用(ODoo中的一个例子是仪表板),因此查询应该限制在给定组件的范围内。未筛选的选择(如$(selector)document.querySelectorAll(selector))一般会致使意外或错误的行为。odoo web的widget()有一个提供dom根(el)的属性,以及直接选择节点的快捷方式(_().)
  • 更通常地说,不要假设您的组件拥有或控制任何超出其我的$el的东西(所以,避免使用对父部件的引用)。
  • HTML模板/渲染应该使用QWeb,除非很是简单。
  • 全部交互组件(向屏幕显示信息或截取DOM事件的组件)必须继承自widget(),并正确实现和使用其API和生命周期。

Qweb模板引擎

Web客户端使用QWeb模板引擎来呈现小部件(除非它们重写renderelement方法来执行其余操做)。QWebJS模板引擎基于XML,主要与Python实现兼容。

如今,让咱们解释如何加载模板。每当Web客户端启动时,都会对/web/web client/qweb路由进行RPC。而后,服务器将返回在每一个已安装模块的数据文件中定义的全部模板的列表。正确的文件列在每一个模块清单的QWeb条目中。

在启动第一个小部件以前,Web客户机将等待加载该模板列表。

这个机制能够很好地知足咱们的需求,但有时咱们但愿懒加载模板。例如,假设咱们有一个不多使用的小部件。在这种状况下,咱们可能不但愿将其模板加载到主文件中,以便使Web客户机稍微轻一些。在这种状况下,咱们可使用小部件的xmlpendencies键:

var Widget = require('web.Widget'); var Counter = Widget.extend({ template: 'some.template', xmlDependencies: ['/myaddon/path/to/my/file.xml'], ... }); 

有了这个,计数器小部件将以willstart方法加载xmlpendencies文件,这样在执行呈现时模板就能够准备好了。

事件系统

目前,odoo支持两个事件系统:一个容许添加侦听器和触发事件的简单系统,以及一个更完整的系统,它还可使事件“冒泡”。

这两个事件系统都在文件mixins.js的eventspatchemixin中实现。这个mixin包含在widget类中。

基础事件系统

这是历史上第一个事件系统。它实现了一个简单的总线模式。咱们有4种主要方法:

  • on : 在一个事件上注册监听器
  • off: 移除事件的监听器
  • once: 注册一个只使用一次的监听器
  • trigger:跟踪一个事件。这会调用全部监听器。
    一下是一个怎么使用事件系统的例子:
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); 

\color{#FFC125}{警告}
不鼓励使用此事件系统,咱们计划用扩展事件系统中的trigger-up方法替换每一个trigger方法。

扩展的事件系统

自定义事件小部件是一个更高级的系统,它模拟DOM事件API。每当一个事件被触发时,它将“冒泡”组件树,直到它到达根小部件,或者中止。

  • trigger_up:这是一种方法,它将建立一个小的odooEvent并将其分派到组件树中。请注意,它将从触发事件的组件开始
  • custom_events:这至关于事件字典,可是对于odoo事件来讲。
    OdoEvent类很是简单。它有三个公共属性:target(触发事件的小部件)、name(事件名称)和data(有效负载)。它还有两种方法:stopPropagation 和 is_stopped.。
    上一个示例能够更新为使用自定义事件系统:
var Widget = require('web.Widget'); var Counter = require('myModule.Counter'); var MyWidget = Widget.extend({ custom_events: { valuechange: '_onValueChange' }, start: function () { this.counter = new Counter(this); var def = this.counter.appendTo(this.$el); return $.when(def, this._super.apply(this, arguments); }, _onValueChange: function(event) { // do something with event.data.val }, }); // in Counter widget, we need to call the trigger_up method: ... this.trigger_up('valuechange', {value: someValue}); 

注册

Odoo生态系统的一个常见需求是从外部扩展/更改基本系统的行为(经过安装应用程序,即不一样的模块)。例如,可能须要在某些视图中添加新的小部件类型。在这种状况下,以及其余许多状况下,一般的过程是建立所需的组件,而后将其添加到注册表(注册步骤),以使Web客户机的其他部分知道它的存在。
一下是一些在系统中可用的注册:

  • 字段注册表(由“web.field_registry”导出)。字段注册表包含Web客户端已知的全部字段小部件。每当视图(一般是表单或列表/看板)须要字段小部件时,这就是它将要查找的地方。典型的用例以下所示:
var fieldRegistry = require('web.field_registry'); var FieldPad = ...; fieldRegistry.add('pad', FieldPad); 

注意,每一个值都应该是AbstractField的子类

  • 视图注册表:此注册表包含Web客户端已知的全部JS视图

(尤为是视图管理器)。此注册表的每一个值都应该是AbstractView的子类

  • 动做注册表:咱们跟踪此注册表中的全部客户端动做。这个是动做管理器在须要建立客户端操做时查找的位置。在版本11中,每一个值应该只是小部件的一个子类。可是,在版本12中,值必须是abstractAction。

小部件之间的通讯

有许多组件之间的通讯方式

  • 从父级到它的子级
    一个简单的例子。父不见能够简单的调用子部件方法:
    this.someWidget.update(someInfo);

  • 从一个小部件到它的父/某个祖先
    在这种状况下,小部件的工做只是通知其环境发生了什么事情。因为咱们不但愿小部件具备对其父部件的引用(这将使小部件与其父部件的实现相结合),所以继续操做的最佳方法一般是使用触发器trigger_up方法触发一个事件,该事件将冒泡到组件树中:
    this.trigger_up('open_record', { record: record, id: id});
    此事件将在小部件上触发,而后将冒泡并最终被某些上游小部件捕获:

var SomeAncestor = Widget.extend({ custom_events: { 'open_record': '_onOpenRecord', }, _onOpenRecord: function (event) { var record = event.data.record; var id = event.data.id; // do something with the event. }, }); 
  • 交叉组件
    经过总线能够实现跨组件通讯。这不是首选的通讯形式,由于它有使代码难以维护的缺点。可是,它具备分离组件的优点。在这种状况下,这只是经过触发和监听总线上的事件来完成的。例如:
// in WidgetA var core = require('web.core'); var WidgetA = Widget.extend({ ... start: function () { core.bus.on('barcode_scanned', this, this._onBarcodeScanned); }, }); // in WidgetB var WidgetB = Widget.extend({ ... someFunction: function (barcode) { core.bus.trigger('barcode_scanned', barcode); }, }); 

在本例中,咱们使用web.core导出的总线,但这不是必需的。能够为特定目的建立总线。

服务services

在11.0版中,咱们引入了服务的概念。主要的想法是给子组件一种受控制的方式来访问它们的环境,这种方式容许框架进行足够的控制,而且是可测试的。
服务系统围绕三个理念进行组织:services、service providers和widget。它的工做方式是小部件触发(使用trigger_up)事件,这些事件冒泡到服务提供者,服务提供者将要求服务执行任务,而后可能返回一个答案。

服务service

服务是AbstractService类的实例。它基本上只有一个名称和一些方法。它的工做是执行一些工做,一般是一些依赖于环境的工做。
例如,咱们有Ajax服务(任务是执行RPC)、本地存储(与浏览器本地存储交互)和许多其余服务。
如下是有关如何实现Ajax服务的简化示例:

var AbstractService = require('web.AbstractService'); var AjaxService = AbstractService.extend({ name: 'ajax', rpc: function (...) { return ...; }, }); 

这个服务被叫作‘ajax’并且定义了一个方法,rpc.

服务提供者Service Provider

为了使服务正常工做,有必要让一个服务提供者准备好分派定制事件。在后端(Web客户端),这是由主Web客户端实例完成的。请注意,服务提供程序的代码来自ServiceProviderMin。

部件widget

小部件是请求服务的部分。为了作到这一点,它只需触发一个事件调用服务(一般经过使用helper函数调用)。此事件将冒泡并将意图传达给系统的其他部分。
在实践中,有些函数被频繁地调用,以致于咱们有一些助手函数使它们更容易使用。例如,_rpc方法是帮助生成rpc的助手。

var SomeWidget = Widget.extend({ _getActivityModelViewID: function (model) { return this._rpc({ model: model, method: 'get_activity_view_id' }); }, }); 

\color{#FFC125}{警告}
若是一个小部件被销毁,它将从主组件树中分离出来,而且没有父组件。在这种状况下,事件不会冒泡,这意味着工做不会完成。这一般正是咱们从一个被破坏的小部件中想要的。

RPCs

RPC功能由Ajax服务提供。但大多数人可能只会与_rpc助手进行交互。
在处理odoo时,一般有两个用例:一个须要在(python)模型上调用方法(这须要经过控制器call_kw),或者一个须要直接调用控制器(在某些路由上可用)。

  • 在python模型中调用方法
return this._rpc({ model: 'some.model', method: 'some_method', args: [some, args], }); 
  • 直接调用控制器
return this._rpc({ route: '/some/route/', params: { some: kwargs}, }); 

通知

odoo框架有一种标准的方式来向用户传递各类信息:通知,它显示在用户界面的右上角。
通知有两种类型:

  • notification: 有助于显示一些反馈。例如,每当用户取消订阅某个频道时。
  • warning:用于显示一些重要/紧急信息。一般是系统中的大多数(可恢复的)错误。

此外,通知还能够用于向用户询问问题,而不会干扰其工做流。想象一下,经过VoIP接收到的一个电话呼叫:能够显示一个带有两个按钮的粘性通知:接受和拒绝。

通知系统

Odoo中的通知系统设计有如下组件:

  • a Notification widget:这是一个简单的小部件,用于建立和显示所需的信息。
  • a NotificationService:一种服务,其职责是在请求完成时(使用custom_event)建立和销毁通知。请注意,Web客户端是一个服务提供者。
  • ServiceMixin中的两个助手功能:do_notify和do_warn

显示通知

显示通知的最多见方法是使用来自ServiceMixin的两种方法:

  • do_notify(title, message, sticky, className):
    显示一个通知类型的通知:
       * title:string. 将会在顶部显示为标题
       * message:string. 通知的内容
       * sticky : boolean,optional. 若是为真,这个通知将会一直保留直到用户解除。不然,通知将会在一段很短的时间以后自动关闭。
       * calssname:string,optional.这是一个css类的名字,将会自动添加到通知中。这对样式有用,即便不鼓励使用它。

  • do_warn(title, message, sticky, className):
    显示一个警告类型的通知。
       * title:string.将会在顶部显示为标题
       * message:string.通知的内容
       * sticky : boolean,optional. 若是为真,这个通知将会一直保留直到用户解除。不然,通知将会在一段很短的时间以后自动关闭。
       * calssname:string,optional.这是一个css类的名字,将会自动添加到通知中。这对样式有用,即便不鼓励使用它。
    这里有两个如何使用这两个方法的例子:

// note that we call _t on the text to make sure it is properly translated. this.do_notify(_t("Success"), _t("Your signature request has been sent.")); this.do_warn(_t("Error"), _t("Filter name is required.")); 

任务栏Systray

Systray是界面菜单栏的右侧部分,Web客户端在其中显示一些小部件,如消息菜单。
当菜单建立SystrayMenu时,它将查找全部已注册的小部件,并将它们做为子小部件添加到适当的位置。
目前没有针对Systray小工具的特定API。它们应该是简单的小部件,而且能够像使用trigger-up方法的其余小部件同样与环境通讯。

添加一个新得任务栏项目

没有Systray注册表。添加小部件的正确方法是将其添加到类变量systraymenu.items中。

var SystrayMenu = require('web.SystrayMenu'); var MySystrayWidget = Widget.extend({ ... }); SystrayMenu.Items.push(MySystrayWidget); 

排序Ordering

在将小部件添加到本身以前,Systray菜单将按Sequence属性对项目进行排序。若是原型上不存在该属性,则将使用50。所以,要将Systray项目定位在靠后,能够设置一个很是高的序列号(反之,将其放在靠前的是一个较低的序列号)。
MySystrayWidget.prototype.sequence = 100;

翻译管理

有些翻译是在服务器端进行的(基本上是由服务器呈现或处理的全部文本字符串),可是静态文件中有须要翻译的字符串。它目前的工做方式以下:

  • 每一个可翻译字符串都带有特殊的函数_t(可在js模块web.core中找到)
  • 服务器使用这些字符串生成正确的PO文件。
  • 每当加载Web客户机时,它将调用route/web/web client/translations,它返回全部可翻译术语的列表
  • 在运行时,每当调用函数时,它都会在该列表中查找以查找转换,若是找不到转换,则返回它或原始字符串。

请注意,在文档翻译模块中,从服务器的角度对翻译进行了更详细的解释。
javascript中的翻译有两个重要功能:_t和_lt。区别在于_lt是以懒惰的方式进行的。

var core = require('web.core'); var _t = core._t; var _lt = core._lt; var SomeWidget = Widget.extend({ exampleString: _lt('this should be translated'), ... someMethod: function () { var str = _t('some text'); ... }, }); 

在本例中,因为在加载模块时翻译还没有准备就绪,所以必须使用_lt。
注意,翻译功能须要注意。参数中给定的字符串不该是动态的。

会话Session

Web客户端提供了一个特定的模块,其中包含一些特定于用户当前会话的信息。一些显著的键是:

  • uid:当前用户的id(来自于表 res.users 的ID)
  • user_name: 用户的名字,字符串类型
  • 用户上下文(context [用户id,语言和时区])
  • partner_id: 与当前用户关联的合做伙伴的ID
  • db:当前使用的数据库名字

添加信息到会话中

加载/web路由后,服务器将在模板中插入一些会话信息和脚本标记。信息将从模型ir.http的方法session_info中读取。所以,若是要添加特定信息,能够经过重写session_info方法并将其添加到字典中来完成。

from odoo import models from odoo.http import request class IrHttp(models.AbstractModel): _inherit = 'ir.http' def session_info(self): result = super(IrHttp, self).session_info() result['some_key'] = get_some_value_from_db() return result 

如今,经过在会话中读取该值,能够在javascript中获取该值:

var session = require('web.session'); var myValue = session.some_key; ... 

请注意,此机制旨在减小Web客户端准备就绪所需的通讯量。它更适合于计算成本较低的数据(缓慢的session_info调用将延迟为每一个人加载Web客户端),以及在初始化过程早期须要的数据。

视图

“视图”一词有多种含义。本节是关于视图的JavaScript代码的设计,而不是Arch的结构或其余任何内容。
2017年,Odoo用新架构替换了先前的视图代码。主要须要将呈现逻辑与模型逻辑分开。
视图(通常意义上)如今用4个部分描述:视图、控制器、渲染器和模型。这4个部分的API在AbstractView、AbstractController、AbstractRenderer和AbstractModel类中进行了描述。


 
视图结构.png
  • 视图是工厂。它的工做是获取一组字段、arch、上下文和其余一些参数,而后构造一个控制器/渲染器/模型三元组。
    视图的做用是用正确的信息正确地设置MVC模式的每一部分。一般,它必须处理arch字符串,并提取视图中彼此所需的数据。
    请注意,视图是一个类,而不是一个小部件。一旦它的工做完成,它就能够被丢弃。
  • 渲染器有一个做业:表示在DOM元素中查看的数据。每一个视图均可以以不一样的方式呈现数据。此外,它应该监听适当的用户操做,并在必要时通知其父级(Controller)。
    渲染器是MVC模式中的V.
  • 模型:它的工做是获取并保持视图的状态。一般,它以某种方式表示数据库中的一组记录。该模型是“业务数据”的全部者。它是MVC模式中的M.
  • Controller:它的工做是协调渲染器和模型。此外,它是Web客户端其他部分的主要入口点。例如,当用户在搜索视图中更改某些内容时,将使用适当的信息调用控制器的更新方法。
    它是MVC模式中的C.

视图的JS代码已设计为可在视图管理器/操做管理器的上下文以外使用。它们能够用于客户端操做,也能够显示在公共网站上(对资源进行一些处理)

字段部件

该AbstractField类是在一个视图中的全部控件的基类,用于支持他们全部的视图(目前为:表格,列表,看板)。
v11字段小部件与先前版本之间存在许多差别。让咱们提一下最重要的一些:

  • 小部件在全部视图之间共享(表单/列表/看板)。无需再复制实现。请注意,能够为视图设置特定版本的窗口小部件,方法是在视图注册表中为视图名称添加前缀:list.many2one将优先于many2one选择。
  • 小部件再也不是字段值的全部者。它们仅表示数据并与视图的其他部分进行通讯。
  • 小部件再也不须要可以在编辑和只读模式之间切换。如今,当须要进行此类更改时,窗口小部件将被销毁并再次从新呈现。这不是问题,由于他们不管如何都不拥有本身的价值
  • 字段小部件能够在视图以外使用。他们的API略显笨拙,但它们的设计是独立的。

装饰与列表视图同样,字段小部件对装饰具备简单的支持。装饰的目标是有一种简单的方法来根据记录当前状态指定文本颜色。例如,

<field name = “state” decoration-danger = “amount&lt; 10000” /> 

有效的装饰名字有:

  • decoration-bf
  • decoration-it
  • decoration-danger
  • decoration-info
  • decoration-muted
  • decoration-primary
  • decoration-success
  • decoration-warning 每一个装饰decoration-X将映射到css类text-X,这是一个标准的bootstrap css类(text-it和text-bf除外,它们由odoo处理并分别对应于斜体和粗体)。请注意,decoration属性的值应该是一个有效的python表达式,它将使用记录做为评估上下文进行评估。
做者:dooms21day 连接:https://www.jianshu.com/p/1a47fac01077 来源:简书 简书著做权归做者全部,任何形式的转载都请联系做者得到受权并注明出处。
相关文章
相关标签/搜索