odoo V10中文参考手册(八:Javascript)

Javascript

Widgets

web.Widget输出class Widget(),是所有可视组件的基类,相当于mvc的view层,提供一系列的处理页面的方法

  • 处理多widget之间的继承、被继承关系
  • 提供可扩展的生命周期安全管理(当父类被destruct时自动将对应子类清除)
  • 自动使用qweb引擎渲染
  • 与backbone兼容的快捷方法

DOM根元素

Widget()负责的是根DOM下的一部分widget页面,可以通过两个属性来获取widget的DOM:

  • Widget.el – widget对应的原始根DOM
  • Widget.$el – 使用jQuery选择的el

有两种方法来定义生成DOM根元素:

  • Widget.template – qweb的模板名,指定模板会在widget初始化之后、实际渲染之前渲染。该模板生成的根元素就会作为对应widget的DOM根元素
  • Widget.tagName – 当没有设置模板名的时候使用,默认是div,它会被设置成widget的DOM根元素,可以通过以下属性来自定义对应DOM根元素:
  • Widget.id – 在DOM根元素上生成一个id属性
  • Widget.className – 在DOM根元素上生成一个class属性
  • Widget.attributes – 属性映射表,会自动将里面的键值对设置为根元素的对应属性
  • 当设置了模板名,上述参数将不能使用
  • Widget.renderElement() – 可以通过此方法来渲染widget的根DOM并设置,使用的是template或tagName,并调用setElement() 来设置
  • 可以覆盖renderElement() 方法来实现自定义的渲染,但是如果没有在里面调用__super的话必须要调用setElement() 方法

使用widget

widget的生命周期分三个阶段:

  • 1.创建并初始化
    Widget.init(parent) – widget的初始化方法,可以接收更多的参数来覆盖父级widget

参数:
parent (Widget()) – 新创建的widget的父级,如果某widget没有父级可传null

  • 2.注入DOM并启动,通过调用以下方法中的一个来完成
  • Widget.appendTo(element) – 渲染widget并用jquery的appendTo添加到对应DOM最后一个后元素前
  • Widget.prependTo(element) 渲染widget并用jquery的prependTo插入到对应DOM第一个子元素前
  • Widget.insertAfter(element) – 渲染widget并用jquery的insertAfter添加到对应dom之后
  • Widget.insertBefore(element) – 渲染widget并用jquery的insertBefore添加到对应dom之前

上述方法接收的参数和对应jquery方法接收的参数一致,会返回一个 deferred延迟执行对象,并赋予三个任务:

1.使用renderElement()来渲染widget的根元素
2.用对应的jquery方法将widget插入到dom中
3.启动widget并将启动的结果返回
Widget.start():当widget被插入到DOM之后异步启动,一般用于异步的rpc调用以获取远端数据用于widget中,完成后需要返回一个deferred对象。在start方法执行完成之前widget的功能不一定是完整的。

  • 3.销毁并清除widget对象
    Widget.destroy() – 销毁它的子类,解绑所有事件,将它的根元素从DOM移除。当父widget被销毁时会自动调用,如果它没有父类 或需要将当前widget移除但保留父级widget时 就必须显示调用

与widget销毁相关的函数:

  • Widget.alive(deferred[, reject=false])
    由于RPC调用一般比较耗时,可能在它执行完成的时候widget已经被销毁了,这时在会在一个无效的widget对象上做操作,alive可用于处理rpc调用,并保证RPC调用返回后只在有效的widget上执行对应操作:
this.alive(this.model.query().all()).then(function (records) {
    // would break if executed after the widget is destroyed, wrapping
    // rpc in alive() prevents execution
    _.each(records, function (record) {
        self.$el.append(self.format(record));
    });
});

参数:
deferred – deferred对象
reject – 默认情况下如rpc调用完成后widget已被销毁的话对应的deferred对象只是被封锁了,如果设置为True的话会将其调用拒绝

  • Widget.isDestroyed()
    如果widget已经被销销毁了,会返回true,否则返回false

获取DOM内容

由于widget负责其DOM元素下的内容,可以用一个简便的方法去获取它DOM元素内的子片段:
Widget.$(selector) 将css选择器应用到widget的根DOM上

this.$(selector);
相当于this.$el.find(selector);

重置DOM根元素

Widget.setElement(element)
将widget的根DOM设置为指定的DOM,参数element需为一个DOM元素或相应的jquery对象

DOM事件处理

widget一般需要在相应页面内响应用户的动作,这需要通过将事件绑定到DOM元素上来实现。

  • Widget.events
    事件是一个事件选择器(事件名和css选择器之间以空格分开)- 回调函数的映射,回调函数可以是widget内置函数或一个函数对象,this表示相应widget
events: {
    'click p.oe_some_class a': 'some_method',
    'change input': function (e) {
        e.stopPropagation();
    }
},

回调函数只会被对应根DOM的匹配子元素触发。如果事件选择器留空的话,该事件是会被自动绑定到widget的根DOM上。

  • Widget.delegateEvents()
    该方法用于将事件绑定到DOM,当设置好widget的根dom后会自动被调用,可以通过重写它来设置比events映射表指定的更为复杂的事件,但父级方法必须被显式调用,否则events不会被处理。

  • Widget.undelegateEvents()
    用于在dom被销毁或重设时解绑events,当delegateEvents被覆盖时,它也需要进行覆盖。

该方法需要与backbone的delegateEvents相兼容

Widget子类

可以通过extend来创建Widget()的子类,并提供了一些抽象方法和具体方法用于使用。

var MyWidget = Widget.extend({
    // 渲染对象时使用的qweb模板
    template: "MyQWebTemplate",
    events: {
        // 事件绑定示例
        'click .my-button': 'handle_click',
    },

    init: function(parent) {
        this._super(parent);
        // 在渲染之前执行的内容
        // initialization
    },
    start: function() {
        var sup = this._super();
        // 渲染初始化逻辑

        // 允许多重deferred对象
        return $.when(
            // 从父类获取异步信号
            sup,
            // 返回自己的异步信号
            this.rpc(/* … */))
    }
});


##使用
// 创建实例
var my_widget = new MyWidget(this);
// 渲染并插入到dom
my_widget.appendTo(".some-div");

##销毁
my_widget.destroy();

开发规范

  • 避免使用id属性,用id会让部件重用变得很麻烦。可以使用class、dom节点或jquery来替代使用。如果一定要用的情况下,需要使用_.uniqueId()来特别声明:this.id = _.uniqueId('my-widget-')
  • 避免使用很通用的css名如content、navigation,以防止命名冲突。一般以根据它对应的部分来命名。
  • 避免使用全局选择器,因为某个组件可能在同个页面重复使用如仪表板,像$(selector) or document.querySelectorAll(selector) 可能会导致错误的操作,使用widget对应的($el)$()来选择
  • 不要认为你的部件拥有或控制它自己的$el
  • html模板和渲染需要合用qweb
  • 所有用于显示信息或处理事件的交互性质的组件必须继承自Widget() 并且使用它的api正确的实现

RPC

为了进行显示和交互,需要使用rpc与odoo服务器通信,odoo提供两种api来处理:

  • 底层基于JSON rpc与模块对应python片段通信
  • 高级别的直接odoo模块调用
    所有api都是异步调用的,所以它们都会返回deferred延期执行对象

高级别API 直接调用odoo模块

通过Model()来访问odoo的对象方法,通过call方法(来自web.Model)和 query方法(来自web.DataModel)来访问odoo服务器对象

  • call()是直接被映射到odoo服务端对象的同名方法的,用法跟odoo模型的api使用只有以下三点区别:
  • 交互是异步的,所以在rpc中是返回的deferred延期对象(它们会自己处理对应rpc调用结果)
  • 由于javascript规范没有__getattr__method_missing特性,需要指定调度rpc的方法
  • 没有池的概念,当需要用的时候就实例化model代理,而不是从另一个(如全局)中获取
var Users = new Model('res.users');

Users.call('change_password', ['oldpassword', 'newpassword'],
                  {context: some_context}).then(function (result) {
    // do something with change_password result
});
  • query()方法是搜索(odoo的search和read)的接口,返回一个Query()对象,该对象不可改变但是可以基于它创建新的query对象,添加新的属性和方法到原始对象。
Users.query(['name', 'login', 'user_email', 'signature'])
     .filter([['active', '=', true], ['company_id', '=', main_company]])
     .limit(15)
     .all().then(function (users) {
    // do work with users records
});

query在调用all()first()方法之前是不会实际执行的,这两个方法每次调用都会触发一个rpc请求。可以用来进行实时查询。

class Model(name)

  • Model.name 对象所绑定的model名
  • Model.call(method[, args][, kwargs]) 使用对应参数调用当前模型的对应方法

参数:

  1. method (String) 通过rpc调用的模型方法
  2. args (Array<>) 位置匹配的参数列表
  3. kwargs (Object<>) 传递的关键字参数
  • Model.query(fields)

参数:fields (Array<String>) 搜索时需要获取的字段列表

class odoo.web.Query(fields)

第一部分方法是读取方法,它们使用所调用对象的数据来响应rpc请求
  • odoo.web.Query.all() – 读取当前Query对象所对应的数据,返回一个deferred的数组
  • odoo.web.Query.first() – 读取当前Query对象对应的第一个 结果,如果没有结果返回null,有结果返回deferred对象
  • odoo.web.Query.count() – 获取当前Query对象可得到的记录数量
  • odoo.web.Query.group_by(grouping…) – 获取查询数据的分组,

参数: grouping (Array<String>) – 分组列表
返回: Deferred<Array<odoo.web.QueryGroup>> | null

第二部分方法是设置方法,它们会创建一个新Query对象并对相关属性进行扩展或替换
  • odoo.web.Query.context(ctx) 添指定的环境变量添加到搜索中
  • odoo.web.Query.filter(domain) 将指定的domain表达式添加到查询条件中,通过and与已存在的domain联接
  • odoo.web.Query.offset(offset) 设置查询的起始位置,会将原有的offset替换
  • odoo.web.Query.limit(limit) 设置查询的数据量,会将原来的limit替换
  • odoo.web.Query.order_by(fields…) 覆盖原来的排序规则,像Django的QuerySet.order_by一样
  • 接收多个排序字段,按重要性高到低排序,第一个优先级最高,字段以字符串提供
  • 每个字段默认是按升序,可以在字段前加-表示倒序
    与django不同,它没有用?来进行随机乱序和对关联字段排序方法

**分组聚合 **
odoo有非常强大的分组运算功能,但是它是递归的,并且第N+1层依赖于第n层所提供的数据,所以当odoo.models.Model.read_group()工作时这个api就不是那么直观。
odoo一般用Query()方法来代替 read_group()方法。

some_query.group_by(['field1', 'field2']).then(function (groups) {
    // do things with the fetched groups
});

该方法可以接受一个字段列表参数、也可以不带参数执行,无参数时直接返回null而不是deferred对象
当分组条件从其他地方来的时候,可以通过两种方法来测试:

  • 对group_by所得到的结果进行检查:
var groups;
if (groups = some_query.group_by(gby)) {
    groups.then(function (gs) {
        // groups
    });
}
// no groups
  • 使用when()将返回值强制转换为deferred对象
$.when(some_query.group_by(gby)).then(function (groups) {
    if (!groups) {
        // No grouping
    } else {
        // grouping, even if there are no groups (groups
        // itself could be an empty array)
    }
});

成功的情况下group_by返回的结果是一个 QueryGroup()数组

class odoo.web.QueryGroup() 返回分组的属性key,有以下几种
  • grouped_on — 基于哪个字段进行分组计算
  • value — 当前分组的grouped_on的值
  • length — 分组内的记录数量
  • aggregates — 分组聚合结果的 {field: value} 映射
odoo.web.QueryGroup.query([fields...]) 相当于Model.query() ,但只包含当前分组内的记录,返回一个Query对象供后续使用
odoo.web.QueryGroup.subgroups() 返回一个指向当前的子QueryGroup()的数组的deferred对象

底层API:RPC调用python程序

Session()对象(通过web.Session实例化)的rpc方法提供了一个低级别api用来直接调用python程序,该方法接收一个完整URL、一个参数key=>value映射表 作为参数,并将对应获取的结果转换成json格式

session.rpc('/web/dataset/resequence', {
    model: some_model,
    ids: array_of_ids,
    offset: 42
}).then(function (result) {
    // resequence didn't error out
}, function () {
    // an error occured during during call
});

web client

javascript模块系统

从odoo v8开始使用一套跟requirejs类似的js模块系统,它有以下优点:

  • 依赖关系可以保证按顺序加载
  • 更容易将文件分割成更小的逻辑单元
  • 没有全局变量
  • 很容易检查依赖关系,让重构变得容易很多

缺点:

  • 如果要通过odoo交互就必须用模块系统加载,因为有很多对象只在模块系统中能用
  • 不支持循环依赖

在这种模式下,通过require来导入需要的模块,并且显示声明所输出的对象。

odoo.define('addon_name.service', function (require) {
    var utils = require('web.utils');
    var Model = require('web.Model');

    // do things with utils and Model
    var something_useful = 15;
    return  {
        something_useful: something_useful,
    };
});

上面的代码创建了一个名叫addon_name.service的模块,使用odoo.define函数定义。

odoo.define函数有两个参数:
1.name – 新定义的模块名

2.function – 在里面定义该模块实际包含的内容,接收一个require参数,如果需要输出内容就需要有对应返回,require函数用于获取依赖的模块。

用javascript来导入需要的模块就声明输出内容,web客户端会自动进行加载。模块在文件中定义,一般最好一个文件对应一个模块。模块可以返回一个deferred对象,这样该模块只在deferred执行后才加载,而且模块可以被废弃,并在控制台记录对应信息:

Missing dependencies – 该模块不会出现在页面中,可能是javascript文件不在页面中或模块名有错误

Failed modules – 有javascript错误
Rejected modules – 模块返回的是一个废弃的deferred
Rejected linked modules – 该模块依赖于已废弃的模块
Non loaded modules – 模块所依赖的模块不存在或有错误

Web client结构

  • framework/文件夹包含所有底层的模块
  • web.ajax 用于处理rpc调用
  • web.core 核心模块,给出很多有用的对象如qweb,_t
  • web.Widget 包含widget类
  • web.Model 抽象化的web.ajax,用于直接调用服务端模型的方法
  • web.sessionodoo.session
  • web.utils 有用的代码段
  • web.time 时间相关函数
  • views/文件夹包含所有视图定义
  • widgets/包含独立的部件
  • js/文件夹包含一些重要的文件
  • action_manager.js ActionManager 类
  • boot.js 模块系统的入口
  • menu.js 顶级菜单的定义
  • web_client.js 部件WebClient
  • view_manager.js 包含ViewManager

还有其他两个文件:tour.js用于tour,compatibility.js用于将旧系统与新系统兼容,在这个文件中每个模块名被输出到全局变量odoo中。理论上模块可以不通过变量odoo使用。

javascript习惯

  • 在模块最前面声明所有依赖关系,一般按模块名字母顺序来排列
  • 在最后声明所有输出
  • 在模块开始时添加use strict 声明
  • 以合适的名字命名模块如:addon_name.description
  • 类名首字母大写如:ActionManager ,但其他的小写如web.ajax
  • 每个文件只定义一个模块

odoo web client测试

详见:http://www.odoo.com/documentation/10.0/reference/javascript.html#testing-in-odoo-web-client

译自odoo官方文档:http://www.odoo.com/documentation/10.0/reference/javascript.html ,不当之处欢迎批评指正。

内容发布自http://www.jianshu.com/u/6fdae8ec06bc,转载请注明出处

    原文作者:玖零网络科技
    原文地址: https://www.jianshu.com/p/64f45415b2e8
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞