第四章 MVC

odoo的前端同样使用了MVC的架构,不过不同的是,由于历史原因,odoo中的view指定的是视图的概念,MVC中的V在odoo前端中是Renderer的概念。对于像简单的widget或者小组件,使用mvc架构显然没有必要,但是对于odoo视图这种包含了控制面板等复杂部件的大型系统,使用MVC架构就非常必要了,使用MVC架构的好处之一就是能够将代码清晰地分开。

Odoo的前端的MVC架构是从13.0版本开始引入的,因此,本章的基础是依据13.0的版本odoo,其他版本虽然有不同,但大体的思路及总体架构是一致的。

odoo的MVC设计了4个类,分别是工厂类(Factory)、模型类(Model)、渲染器(Render)和控制器(Controller)。MVC中的视图概念这里对应的是Render而非View是因为Odoo的系统中大量使用了View关键字,如果继续使用View作为视图概念,将引起很多不必要的混淆。

这四个类的分别的作用如下:

  • Model: 系统主状态及各种数据参数存储的位置,负责与服务器通信,处理请求结果。
  • Render: 负责系统UI类的工作,只关系与系统渲染和事件处理相关的任务。
  • Controller: 协调Model、Render和父类Widgets的协同工作。
  • Factory: 设置MVC的组件是个复杂的任务,每个部件都有不同的参数和选项,有的需要可拓展,有的需要按顺序创建等等,而工厂类的工作就是负责处理这种繁杂的工作,并尽量每个部件最简化。

Model

数据层的实现,其任务就是获取,处理并更新数据。Model并不是一个Widget,它是一个没有UI的类。


var Model = Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
    /**
     * @param {Widget} parent
     * @param {Object} params
     */
    init: function (parent, params) {
        mixins.EventDispatcherMixin.init.call(this);
        this.setParent(parent);
    },

    /**
     * This method should return the complete state necessary for the renderer
     * to display the current data.
     *
     * @returns {*}
     */
    get: function () {
    },
    /**
     * The load method is called once in a model, when we load the data for the
     * first time.  The method returns (a promise that resolves to) some kind
     * of token/handle.  The handle can then be used with the get method to
     * access a representation of the data.
     *
     * @param {Object} params
     * @returns {Promise} The promise resolves to some kind of handle
     */
    load: function () {
        return Promise.resolve();
    },

Model的初始化函数接两个参数:

  • parent:数据绑定的部件(widget)
  • params:初始化参数对象(object)

从代码中可以看出,Model并非Widget的子类,而是事件分发混合类(EventDispatcherMixin)和服务混合类(ServiceMixin)的子类,这也就暗示了Model类的作用,响应事件和提供服务调用(RPC)。

Model只定义了两个方法:get和load。

  • get方法用来获取完整的数据,然后提供给渲染器(Renderer)的state属性进行使用。
  • load方法只在模型第一次加载数据的时候调用一次,此方法返回一个可以resolve的promise对象。

Renderer

渲染器(Renderer)的作用只有一个,渲染用户界面,并响应用户作出的操作。

var Renderer = Widget.extend({
    /**
     * @override
     * @param {any} state
     * @param {Object} params
     */
    init: function (parent, state, params) {
        this._super(parent);
        this.state = state;
    },
});

从代码上可以看出,渲染器(Renderer)的本质是一个部件(Widget),因此决定了后面的各种视图的本质都是Widget。Renderer的初始化函数接收三个参数:

  • parent: 绑定的部件(widget)
  • state: 从模型获取的数据对象
  • params: 初始化需要的参数

Controller

控制器(Controller)的作用是用来协调数据模型(Model)和渲染器(Renderer)协同工作。

var Controller = Widget.extend({
    /**
     * @override
     * @param {Model} model
     * @param {Renderer} renderer
     * @param {Object} params
     */
    init: function (parent, model, renderer, params) {
        this._super.apply(this, arguments);
        this.model = model;
        this.renderer = renderer;
    },
    /**
     * @returns {Promise}
     */
    start: function () {
        return Promise.all(
            [this._super.apply(this, arguments),
            this._startRenderer()]
        );
    },

    //--------------------------------------------------------------------------
    // Private
    //--------------------------------------------------------------------------

    /**
     * Appends the renderer in the $el. To override to insert it elsewhere.
     *
     * @private
     */
    _startRenderer: function () {
        return this.renderer.appendTo(this.$el);
    },
});

Controller的初始化方法接收4个参数,分别是:

  • parent: 绑定的部件(widget)
  • model: 数据模型实例
  • renderer: 渲染器实例
  • params: 初始化参数

控制器在启动时,会调用_startRenderer方法将初始化过程中绑定的渲染器(this.renderer)实例加载到DOM中。

工厂类

现在我们了解并简要认识了odoo前端工作的三大元素,模型(Model),渲染器(Controller)和控制器(Controller)。那么,他们是如何在odoo中进行运作的呢?

要想知道这个问题的答案,我们就要认识下面这个类:工厂类。

工厂类(Factory)的主要任务就是简化MVC的使用方式,工厂类负责获取Controller、Model和Render的实例,并组织他们协同工作。

var Factory = Class.extend({
    config: {
        Model: Model,
        Renderer: Renderer,
        Controller: Controller,
    },
    /**
     * @override
     */
    init: function () {
        this.rendererParams = {};
        this.controllerParams = {};
        this.modelParams = {};
        this.loadParams = {};
    },

    //--------------------------------------------------------------------------
    // Public
    //--------------------------------------------------------------------------

    /**
     * Main method of the Factory class. Create a controller, and make sure that
     * data and libraries are loaded.
     *
     * There is a unusual thing going in this method with parents: we create
     * renderer/model with parent as parent, then we have to reassign them at
     * the end to make sure that we have the proper relationships.  This is
     * necessary to solve the problem that the controller needs the model and
     * the renderer to be instantiated, but the model need a parent to be able
     * to load itself, and the renderer needs the data in its constructor.
     *
     * @param {Widget} parent the parent of the resulting Controller (most
     *      likely an action manager)
     * @returns {Promise<Controller>}
     */
    getController: function (parent) {
        var self = this;
        var model = this.getModel(parent);
        return Promise.all([this._loadData(model), ajax.loadLibs(this)]).then(function (result) {
            var state = result[0];
            var renderer = self.getRenderer(parent, state);
            var Controller = self.Controller || self.config.Controller;
            var controllerParams = _.extend({
                initialState: state,
            }, self.controllerParams);
            var controller = new Controller(parent, model, renderer, controllerParams);
            model.setParent(controller);
            renderer.setParent(controller);
            return controller;
        });
    },
    /**
     * Returns a new model instance
     *
     * @param {Widget} parent the parent of the model
     * @returns {Model} instance of the model
     */
    getModel: function (parent) {
        var Model = this.config.Model;
        return new Model(parent, this.modelParams);
    },
    /**
     * Returns a new renderer instance
     *
     * @param {Widget} parent the parent of the renderer
     * @param {Object} state the information related to the rendered data
     * @returns {Renderer} instance of the renderer
     */
    getRenderer: function (parent, state) {
        var Renderer = this.config.Renderer;
        return new Renderer(parent, state, this.rendererParams);
    },

    //--------------------------------------------------------------------------
    // Private
    //--------------------------------------------------------------------------

    /**
     * Loads initial data from the model
     *
     * @private
     * @param {Model} model a Model instance
     * @returns {Promise<*>} a promise that resolves to the value returned by
     *   the get method from the model
     * @todo: get rid of loadParams (use modelParams instead)
     */
    _loadData: function (model) {
        return model.load(this.loadParams).then(function () {
            return model.get.apply(model, arguments);
        });
    },
});

从工厂类的源代码中,我们可以看出,工厂类直接继承自web.Class,也就是工厂类也不是Widget。工厂类在初始化过程中会定义4个属性:

  • renderParams: 实例化Render时需要的参数
  • controllerParmas: 实例化Controller时需要的参数
  • modelParmas: 实例化Model时需要的参数
  • loadParams: 数据加载过程中需要的参数

工厂类的主要作用是,通过对外提供一个获取控制器的方法(getController),确保相应的数据模型和渲染器都能够完成实例化。

通过对getController方法的分析,我们可以看到,工厂类在getController方法中会完成模型数据的实例化、渲染器的实例化,并加载必要的外部依赖库。

具体地,

  • 方法内部首先会使用_loadData方法加载数据模型和外部依赖库
  • 获取到数据以后,将数据封装到state属性并用来初始化渲染器(Renderer)
  • 同样地,将数据state传递给Controller完成控制器的初始化。
  • 最后,调用数据模型和渲染器的setParent方法将父类对象替换为生成的控制器对象。

_loadData方法内部使用了前面我们在模型一节提到过的load方法。

getController

工厂类的主方法,创建controller实例,并确保数据和依赖库加载完成。

getController: function (parent) {
    var self = this;
    var model = this.getModel(parent);
    return Promise.all([this._loadData(model), ajax.loadLibs(this)]).then(function (result) {
        var state = result[0];
        var renderer = self.getRenderer(parent, state);
        var Controller = self.Controller || self.config.Controller;
        var controllerParams = _.extend({
            initialState: state,
        }, self.controllerParams);
        var controller = new Controller(parent, model, renderer, controllerParams);
        model.setParent(controller);
        renderer.setParent(controller);
        return controller;
    });
},

总结

Odoo从13.0版本开始把MVC模式单独抽离出来形成了一个web.mvc模块,主要目的就是协调MVC的工作,简化使用方法。从前面的介绍中可以看出来,MVC的起点是Factory类,而Factory的核心是Controller。Controller在Factory类中初始化,Controller的初始化过程中会利用Model获取数据,实例化Render对象。最后Controller的start方法会将Render的实例添加到页面中,从而触发Render的渲染过程。

mvc

接下来,我们将深入模型的世界,详细探究模型是如何加载和处理数据的。

results matching ""

    No results matching ""