第四章 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的渲染过程。
接下来,我们将深入模型的世界,详细探究模型是如何加载和处理数据的。