第四章 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和params。 parent是父类的Widget,从代码中可以看出,Model并非Widget的子类,而是EventDispatcherMixin和ServiceMixin的子类,因此parent的也就限定了是这两个类的子类。 params是一个对象,供子类传递参数使用。

Model只定义了两个方法:get和load。get方法用来获取完整的数据,以供Render展示当前数据。load方法只在我们还在数据的时候运行一次,此方法返回一个可以resolve的promise对象。

Render

Render的作用只有一个,渲染用户界面,并响应用户作出的操作。

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

从代码上可以看出,Render的本质是一个Widget,因此决定了后面的各种视图的本质都是Widget。Render的初始化函数接收三个参数: parent、state和params。parent是父类的Widget、state是用来渲染的数据,params是供子类使用的参数。

Controller

Controller是用来协调父类Widget、Render和Model的工作。

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、模型数据model、渲染器render和供子类使用的参数params。初始化方法将model和renderer赋值给同名属性。

Controller的start方法返回一个Promise对象,在Promise的all方法中调用了Render的appendTo方法,后面Render部分会讲到,appendTO方法将启动Render的渲染工作。

工厂类

前面提到过,工厂类(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

工厂类的主方法,创建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;
    });
},

从代码中我们可以看出,getController方法调用了_loadData方法从model中获取数据,使用ajax.LoadLibs方法加载依赖库,等这两个动作完成之后,开始执行实例化。实例化过程中,先实例化了render对象,然后使用model实例和render实例实例化了Controller,最后将Controller的实例作为model和render的父类,返回了Controller实例。

这个方法内部有个比较特殊的地方,首先使用parent参数实例化了render和model,然而在方法最后又重新设置了这两个实例的parent。这么做的目的是为了解决Controller实例化的条件就是需要有model和render实例而model需要一个能够自己加载的父类对象、Render的构造函数需要数据的问题。

getModel

getModel方法的作用主要是返回一个实例化的Model对象。

getModel: function (parent) {
    var Model = this.config.Model;
    return new Model(parent, this.modelParams);
},

getRenderer

getRenderer方法主要是返回一个Render的实例对象

getRenderer: function (parent, state) {
    var Renderer = this.config.Renderer;
    return new Renderer(parent, state, this.rendererParams);
},

_loadData

_loadData方法的主要作用是从Model模型中加载初始化数据。

_loadData: function (model) {
    return model.load(this.loadParams).then(function () {
        return model.get.apply(model, arguments);
    });
},

总结

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