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