This page looks best with JavaScript enabled

Odoo JavaScript - Phần 1: Tổng quan về classes và kiến trúc MVC trong Odoo

 ·  ☕ 9 phút đọc · 👀... views

Trong bài viết này, chúng ta sẽ đi qua những kiến thức cơ bản về Odoo JavaScript Framework với mục tiêu cuối cùng là tạo
một module trong OWL.

Kể từ Odoo 14 (và cả Odoo 15, 16), hệ thống JavaScript MVC cốt lõi trong Odoo vẫn chưa được viết lại bằng OWL. Vì vậy chúng ta sẽ tìm hiểu kiến trúc MVC của Odoo nhưng trước tiên, chúng ta phải xem xét 2 thành phần chính của các class trong JavaScript mà mọi phần đều được xây dựng dựa trên ‘web.Class’ và ‘web.Widget’.

web.Class trong Odoo

Lớp web.js được định nghĩa trong odoo /addons/web/static/src/js/core/class.js là một đoạn code khá cũ
Việc kế thừa các lớp trong javascript trong Odoo được viết như sau:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 * @class Class
 */
function OdooClass(){}

var initializing = false;
var fnTest = /xyz/.test(function(){xyz();}) ? /\b_super\b/ : /.*/;

/**
 * Subclass an existing class
 *
 * @param {Object} prop class-level properties (class attributes and instance methods) to set on the new class
 */
OdooClass.extend = function() {
    var _super = this.prototype;
    // Support mixins arguments
    var args = _.toArray(arguments);
    args.unshift({});
    var prop = _.extend.apply(_,args);

    // Instantiate a web class (but only create the instance,
    // don't run the init constructor)
    initializing = true;
    var This = this;
    var prototype = new This();
    initializing = false;

Điều này giúp Odoo có thể làm những việc như:

1
2
3
4
5
6
7
8
9
var Class = require('web.Class');
var mixins = require('web.mixins');
var ServicesMixin = require('web.ServicesMixin');


var MyCustomClass = core.Class.extend(mixins.PropertiesMixin, ServicesMixin, {
    myProperty: 'test'
    // ...
}

Kết luận lại thì Web.Class là thứ cho phép bạn thực hiện .extend() và .include trên mọi Widget JavaScript của Odoo, v.v.

web.Widget Odoo Class.

Widget Class là lớp mở rộng của web.class kết hợp cùng với DOM thông qua Jquery và được định nghĩa trong /addons/web/static/src/js/core/widget.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var Widget = core.Class.extend(mixins.PropertiesMixin, ServicesMixin, {
    // Backbone-ish API
    tagName: 'div',
    id: null,
    className: null,
    attributes: {},
    events: {},
    /**
     * The name of the QWeb template that will be used for rendering. Must be
     * redefined in subclasses or the default render() method can not be used.
     *
     * @type {null|string}
     */
    template: null,

Bằng cách mở rộng web.class, Class Widget này có thể có 2 mixin, PropertiesMixin và ServicesMixin.

web.Widget extends the PropertiesMixin

PropertiesMixin được cho là xử lý các thuộc tính(properties) tuy nhiên khi chúng ta xem code của mixin này:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

/**
 * @name PropertiesMixin
 * @mixin
 */
var PropertiesMixin = _.extend({}, EventDispatcherMixin, {
    init: function () {
        EventDispatcherMixin.init.call(this);
        this.__getterSetterInternalMap = {};
    },
    set: function (arg1, arg2, arg3) {
        var map;
        var options;
        if (typeof arg1 === "string") {
            map = {};
            map[arg1] = arg2;
            options = arg3 || {};
        } else {
            map = arg1;
            options = arg2 || {};
        }
        var self = this;
        var changed = false;
        _.each(map, function (val, key) {
            var tmp = self.__getterSetterInternalMap[key];
            if (tmp === val)
                return;
            // seriously, why are you doing this? it is obviously a stupid design.
            // the properties mixin should not be concerned with handling fields details.
            // this also has the side effect of introducing a dependency on utils.  Todo:
            // remove this, or move it elsewhere.  Also, learn OO programming.
            if (key === 'value' && self.field && self.field.type === 'float' && tmp && val){
                var digits = self.field.digits;
                if (_.isArray(digits)) {
                    if (utils.float_is_zero(tmp - val, digits[1])) {
                        return;
                    }
                }
            }
            changed = true;
            self.__getterSetterInternalMap[key] = val;
            if (! options.silent)
                self.trigger("change:" + key, self, {
                    oldValue: tmp,
                    newValue: val
                });
        });
        if (changed)
            self.trigger("change", self);
    },
    get: function (key) {
        return this.__getterSetterInternalMap[key];
    }
});

return {
    ParentedMixin: ParentedMixin,
    EventDispatcherMixin: EventDispatcherMixin,
    PropertiesMixin: PropertiesMixin,
};

PropertiesMixin lại extends EventDispatcherMixin, mục đích chính của PropertiesMixin đó chính là xử lý các trigger event trên UI
Có một đoạn code khá hài hước mà hiện tại nó vẫn còn trong core của odoo

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
_.each(map, function (val, key) {
            var tmp = self.__getterSetterInternalMap[key];
            if (tmp === val)
                return;
            // seriously, why are you doing this? it is obviously a stupid design.
            // the properties mixin should not be concerned with handling fields details.
            // this also has the side effect of introducing a dependency on utils.  Todo:
            // remove this, or move it elsewhere.  Also, learn OO programming.
            if (key === 'value' && self.field && self.field.type === 'float' && tmp && val){
                var digits = self.field.digits;

Also, learn OO programming. =)))
Bản thân EventDispatcherMixin cũng extend từ ParentedMixin do đó, nó cũng có thể gọi được các function như getParent hoặc getChildren

web.Widget extends the ServicesMixin

Mixin này cung cấp cho Widget khả năng loadViews, do_action, get_session và thực hiện hàm RPC thông qua hàm _rpc.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
odoo.define('web.ServicesMixin', function (require) {
"use strict";

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

/**
 * @mixin
 * @name ServicesMixin
 */
var ServicesMixin = {
    /**
     * @param  {string} service
     * @param  {string} method
     * @return {any} result of the service called
     */
    call: function (service, method) {
        var args = Array.prototype.slice.call(arguments, 2);
        var result;
        this.trigger_up('call_service', {
            service: service,
            method: method,
            args: args,
            callback: function (r) {
                result = r;
            },
        });
        return result;
    },
    /**
     * @private
     * @param {Object} libs - @see ajax.loadLibs
     * @param {Object} [context] - @see ajax.loadLibs
     * @param {Object} [tplRoute=this._loadLibsTplRoute] - @see ajax.loadLibs
     * @returns {Promise}
     */
    _loadLibs: function (libs, context, tplRoute) {
        return this.call('ajax', 'loadLibs', libs, context, tplRoute || this._loadLibsTplRoute);
    },
    /**
     * Builds and executes RPC query. Returns a promise resolved with
     * the RPC result.
     *
     * @param {string} params either a route or a model
     * @param {string} options if a model is given, this argument is a method
     * @returns {Promise}
     */
    _rpc: function (params, options) {
        var query = rpc.buildQuery(params);
        var prom = this.call('ajax', 'rpc', query.route, query.params, options, this);
        if (!prom) {
            prom = new Promise(function () {});
            prom.abort = function () {};
        }
        var abort = prom.abort ? prom.abort : prom.reject;
        if (!abort) {
            throw new Error("a rpc promise should always have a reject function");
        }
        prom.abort = abort.bind(prom);
        return prom;
    },
    loadFieldView: function (modelName, context, view_id, view_type, options) {
        return this.loadViews(modelName, context, [[view_id, view_type]], options).then(function (result) {
            return result[view_type];
        });
    },
    loadViews: function (modelName, context, views, options) {
        var self = this;
        return new Promise(function (resolve) {
            self.trigger_up('load_views', {
                modelName: modelName,
                context: context,
                views: views,
                options: options,
                on_success: resolve,
            });
        });
    },
    loadFilters: function (modelName, actionId, context) {
        var self = this;
        return new Promise(function (resolve, reject) {
            self.trigger_up('load_filters', {
                modelName: modelName,
                actionId: actionId,
                context: context,
                on_success: resolve,
            });
        });
    },
    createFilter: function (filter) {
        var self = this;
        return new Promise(function (resolve, reject) {
            self.trigger_up('create_filter', {
                filter: filter,
                on_success: resolve,
            });
        });
    },
    deleteFilter: function (filterId) {
        var self = this;
        return new Promise(function (resolve, reject) {
            self.trigger_up('delete_filter', {
                filterId: filterId,
                on_success: resolve,
            });
        });
    },
    // Session stuff
    getSession: function () {
        var session;
        this.trigger_up('get_session', {
            callback: function (result) {
                session = result;
            }
        });
        return session;
    },
    /**
     * Informs the action manager to do an action. This supposes that the action
     * manager can be found amongst the ancestors of the current widget.
     * If that's not the case this method will simply return an unresolved
     * promise.
     *
     * @param {any} action
     * @param {any} options
     * @returns {Promise}
     */
    do_action: function (action, options) {
        var self = this;
        return new Promise(function (resolve, reject) {
            self.trigger_up('do_action', {
                action: action,
                options: options,
                on_success: resolve,
                on_fail: reject,
            });
        });
    },
    /**
     * Displays a notification.
     *
     * @param {Object} options
     * @param {string} options.title
     * @param {string} [options.subtitle]
     * @param {string} [options.message]
     * @param {string} [options.type='warning'] 'info', 'success', 'warning', 'danger' or ''
     * @param {boolean} [options.sticky=false]
     * @param {string} [options.className]
     */
    displayNotification: function (options) {
        return this.call('notification', 'notify', options);
    },
    /**
     * @deprecated will be removed as soon as the notification system is reviewed
     * @see displayNotification
     */
    do_notify: function (title, message, sticky, className) {
        return this.displayNotification({
            type: 'warning',
            title: title,
            message: message,
            sticky: sticky,
            className: className,
        });
    },
    /**
     * @deprecated will be removed as soon as the notification system is reviewed
     * @see displayNotification
     */
    do_warn: function (title, message, sticky, className) {
        console.warn(title, message);
        return this.displayNotification({
            type: 'danger',
            title: title,
            message: message,
            sticky: sticky,
            className: className,return
        });
    },
};

return ServicesMixin;

});

Ở function do_action chúng ta có thể thấy rằng bản chất vẫn là gọi đến hàm trigger_up, tuy nhiên mixin này lại không extend EventDispatcherMixin do đó để có thể dụng hết các function của mixin này bạn phải extend cả EventsDispatcherMixin vào class của bạn, khá là phiền phức nhỉ?😁

OK, Hãy tóm tắt lại mớ hỗn độn này, vậy web.Widget có thể làm gì?😄

web.Widget có thể truy cập vào:

  • ParentedMixin với các phương thức như getChildren, getParent…
  • EventDispatcherMixin để kiểm soát các event thông qua trigger_up
  • PropertiesMixin với get và set
  • ServicesMixin với các phương thức như rpc, load_view…
Chia sẻ
Support the author with

Hùng Phạm
Viết bởi
Hùng Phạm
Web/Mobile Developer