第三章 资产
Odoo的资产概念并没有像其他引用那样直接了当,这是因为odoo需要应对不同的场景, WC和POS的需求不一样,Website和移动端的需求又不一样, 因此, Odoo采用了按需加载的模式来管理这些资产(Assets)
资产类型
一共有三种类型的资产
- js
- css和scss
- xml
code
Odoo支持三种类型的Javascript代码, 这些代码在非资产调试(debug=assets)模式下将会被压缩成一个附件文件存储起来, 在渲染的时候加载到\
style
同js文件一样, css和scss在非资产调试模式下也会被压缩, 并存储到附件, 并使用\标签包裹
template
模板文件的处理逻辑不同于js和css, odoo直接从文件系统中读取这些文件并拼接起来, 不论何时,odoo都从/web/webclient/qweb这个controller中获取模板文件.
绑定
Odoo的资产根据bundles进行分类, 每个bundle都在__mainfest\.py文件中进行声明. 使用assets关键字的字典存储了bundle和文件的映射.
'assets': {
'web.assets_backend': [
'web/static/src/xml/**/*',
],
'web.assets_common': [
'web/static/lib/bootstrap/**/*',
'web/static/src/js/boot.js',
'web/static/src/js/webclient.js',
],
'web.qunit_suite_tests': [
'web/static/src/js/webclient_tests.js',
],
}
bundle的分类:
- web.assets_common: 大多数的资产在WebClient和POS,Website间通用, 其中boot.js定义了Odoo的模块系统.
- web.assets_backend: WebClient专用的bundle.
- web.assets_frontend: 前端Website, 商城,博客等使用的bundle.
- web.assets_qweb: 在后台环境和销售点中使用的所有静态XML模板
- web.qunit_suite_tests: 单元测试的代码
- web.qunit_mobile_suite_tests: 移动端的单元测试
操作
通常情况下,给bundle添加文件是非常简单的,但是也有一些特殊的场景,需要做一些例外的操作
- append: 向bundle中添加一个新文件
- prepend: 在bundle的第一行添加文件
- before: 在bundle中的某一个文件前添加
- after: 在bundle中的某一个文件后添加
- include: 在bundles中填加一个bundle
- remove: 在bundle中删除一个文件
- replace: 在bundle中替换一个或多个文件
加载顺序
资产的加载顺序有时很关键,必须是确定的,主要是针对样式表的优先级和设置脚本。在Odoo中资产的处理方式如下:
- 当一个资产包被调用时(例如,t-call-assets="web.assets_common"),会产生一个空的资产列表
- 所有与捆绑资产相匹配的ir.asset类型的记录被获取,并按序列号进行排序。然后,所有序列号严格小于16的记录被处理,并应用到当前的资产列表中
- 所有在其清单中为所述捆绑物声明资产的模块都将其资产操作应用于该列表。这是按模块的依赖性顺序进行的(例如,web assets在website之前被处理)。如果一个指令试图添加一个已经存在于列表中的文件,则不会对该文件进行任何操作。换句话说,只有第一次出现的文件被保留在列表中
- 剩余的 ir.asset 记录(序列大于或等于 16 的记录)也会被处理和应用
清单中声明的资产可能需要按特定顺序加载,例如,加载 lib文件夹时,jquery.js 必须先于所有其他 jquery 脚本加载。一种解决方案是用较低的顺序或 "prepend "指令来创建ir.asset记录,但还有另一种更简单的方法可以做到.
由于资产列表中的每个文件路径的统一性得到了保证,你可以在包含它的glob之前提到任何特定的文件。这样,这个文件就会出现在列表中,在所有其他包含在glob中的文件之前:
'web.assets_common': [
'my_addon/static/lib/jquery/jquery.js',
'my_addon/static/lib/jquery/**/*',
],
延迟加载
有时动态加载文件和/或资产包是很有用的,例如只在需要时才加载一个库。要做到这一点,Odoo框架提供了一些辅助函数,位于@web/core/assets
await loadAssets({
jsLibs: ["/web/static/lib/stacktracejs/stacktrace.js"],
});
loadAssets(assets): 加载assets参数中描述的资产。它是一个对象,可能包含以下键:
- jsLibs: string[] javascript文件列表
- cssLibs: string[] css文件列表
useAssets(assets): 当组件需要在其 onWillStart 方法中加载一些资产时,此钩子非常有用。它在内部调用 loadAssets
资产模型
在大多数情况下,清单中声明的资产基本上就足够了。然而,为了获得更大的灵活性,框架还支持在数据库中声明的动态资产.
这是通过创建ir.asset记录实现的。这些记录会被处理,就像在模块清单中发现的一样,而且它们的表达能力与清单中的记录相同
class odoo.addons.base.models.ir_asset.IrAsset
...
- name: 资产记录的名称(用于识别目的)
- bundle: 将应用该资产的bundle
- directive(default= append): 此字段确定如何解释path和target
- active: 是否激活
- sequence: default= 16): 加载序列.
Javascript模块类型
Odoo支持三种Javascript模块类型, 分别是:
- 普通 javascript 文件(无模块架构)
- 原生Javascript模块
- Odoo模块(定制化系统)
如资产管理页面所述,所有 javascript 文件都捆绑在一起并提供给浏览器。请注意,本机 javascript 文件由 Odoo 服务器处理并转换为 Odoo 自定义模块.
让我们简要解释一下每种 javascript 文件背后的用途。普通 javascript 文件应该只保留给外部库和一些小的特定低级用途。所有新的 javascript 文件都应在本机 javascript 模块系统中创建。自定义模块系统仅对旧的、尚未转换的文件有用
普通 javascript 文件
普通的 javascript 文件可以包含任意内容。
(function () {
// some code here
let a = 1;
console.log(a);
})();
此类文件的优点是我们避免将局部变量泄漏到全局范围。
显然,普通的 javascript 文件没有提供模块系统的好处,因此需要注意包中的顺序(因为浏览器会按照该顺序精确地执行它们)
Odoo会将所有的外部文件作为普通javascript处理
原生Javascript模块
绝大多数的新Odoo代码都应该使用原生Javascript模块, 它能带来更好的开发体验和IDE的集成支持.
有一点需要注意: Odoo需要知道哪些代码需要翻译成为Odoo模块,为此, 需要在代码的第一行添加@odoo-module字符串用以标识, 这样odoo会自动将这些代码翻译成为可用的模块.
举例,
/** @odoo-module **/
import { someFunction } from './file_b';
export function otherFunction(val) {
return someFunction(val + 3);
}
上面的代码将被翻译成为下面的代码:
odoo.define('@web/file_a', function (require) {
'use strict';
let __exports = {};
const { someFunction } = require("@web/file_b");
__exports.otherFunction = function otherFunction(val) {
return someFunction(val + 3);
};
return __exports;
)};
所以, 我们知道,所谓的翻译实际上就是在文件头部添加odoo.define, 并将代码内的导入/导入代码重构.
翻译的代码将模块名带上了"@web/file_a", 所有addons中的模块都会被添加到路径前边.
相对路径也是可以的
addons/
web/
static/
src/
file_a.js
file_b.js
stock/
static/
src/
file_c.js
file_b可以在file_a中进行导入:
/** @odoo-module **/
import {something} from `./file_a`
file_c只能使用全路径
/** @odoo-module **/
import {something} from `@web/file_a`
别名系统
由于Odoo模块遵循不同的模块命名模式,因此存在一个系统,以允许向新系统平稳过渡。目前,如果一个文件被转换为模块(因此遵循新的命名惯例),项目中其他尚未转换为类似ES6语法的文件将无法要求该模块。别名在这里通过创建一个小的代理函数将旧名称与新名称进行映射。然后就可以用新旧名称来调用该模块
要添加这样的别名,文件上面的注释标签应该是这样的
/** @odoo-module alias=web.someName**/
import { someFunction } from './file_b';
export default function otherFunction(val) {
return someFunction(val + 3);
}
然后,翻译后的模块也会用要求的名字创建一个别名
odoo.define(`web.someName`, function(require) {
return require('@web/file_a')[Symbol.for("default")];
});
别名的默认行为是重新导出它们所别名的模块的默认值。这是因为 "经典 "模块通常会导出一个直接使用的单一值,这与默认导出的语义大致相符。然而,也可以更直接地委托,并遵循被别名模块的确切行为
/** @odoo-module alias=web.someName default=0**/
import { someFunction } from './file_b';
export function otherFunction(val) {
return someFunction(val + 3);
}
在这种情况下,这将定义一个别名,其值与原模块导出的值完全相同
odoo.define(`web.someName`, function(require) {
return require('@web/file_a');
});
局限性
由于性能原因,Odoo没有使用完整的javascript解析器来转换本地模块。因此,有一些限制,包括但不限于
- 一个import或export的关键词前面不能有非空格字符
- 多行注释或字符串不能以import/export开头
- 当你导出一个对象时,它不能包含一个注释
- Odoo需要一种方法来确定一个模块是由路径(如./views/form_view)还是名称(如web.FormView)描述的。它必须使用一种启发式方法来做到这一点:如果名称中有一个/,它就被视为一个路径。这意味着Odoo不再真正支持带/的模块名称.
由于"经典 "模块没有被废弃,目前也没有计划删除它们,如果你遇到原生模块的问题,或者受到原生模块的限制,你可以而且应该继续使用它们。这两种风格可以在同一个Odoo插件中共存
Odoo模块系统
Odoo 定义了一个小模块系统(位于文件 addons/web/static/src/js/boot.js 中,需要先加载)。受 AMD 启发的 Odoo 模块系统通过在全局 odoo 对象上定义函数来工作。然后我们通过调用该函数来定义每个 javascript 模块。在 Odoo 框架中,模块是一段将尽快执行的代码。它有一个名称和可能的一些依赖项。当它的依赖项被加载时,一个模块也将被加载。模块的值就是定义模块的函数的返回值.
// in file a.js
odoo.define('module.A', function (require) {
"use strict";
var A = ...;
return A;
});
// in file b.js
odoo.define('module.B', function (require) {
"use strict";
var A = require('module.A');
var B = ...; // something that involves A
return B;
});
另一种定义模块的方法是在第二个参数中明确给出一个依赖性列表
odoo.define('module.Something', ['module.A', 'module.B'], function (require) {
"use strict";
var A = require('module.A');
var B = require('module.B');
// some code
});
如果某些依赖项丢失/未准备好,则根本不会加载模块。几秒钟后控制台中会出现警告。
请注意,不支持循环依赖。这是有道理的,但这意味着人们需要小心.
定义一个模块
odoo.define方法定义了三个参数:
- moduleName: javascript模块的名称。它应该是一个独特的字符串。惯例是在odoo addon的名字后面加上一个具体的描述。例如,web.Widget描述了一个定义在web addon中的模块,它导出了一个Widget类(因为第一个字母是大写的)。如果名字不唯一,就会产生一个异常,并显示在控制台中.
- dependencies: 参数是可选的。如果给定,它应该是一个字符串的列表,每个字符串对应一个javascript模块。这描述了在执行模块之前需要加载的依赖关系。如果这里没有明确给出依赖关系,那么模块系统将通过调用toString来从函数中提取这些依赖关系,然后用一个regexp来找到所有的require语句
- 最后,最后一个参数是一个定义模块的函数。它的返回值是模块的值,可以传递给需要它的其他模块。注意,对于异步模块有一个小小的例外,见下一节
如果发生错误,它将被记录在控制台中(在调试模式下):
- Missing dependencies: 这些模块没有出现在页面中。有可能是JavaScript文件不在页面中,或者模块名称不对
- Failed modules: 检测到一个javascript错误
- Rejected modules: 该模块返回一个拒绝的Promise。它(以及它的依赖模块)没有被加载。
- Rejected linked modules: 依赖于被拒绝模块的模块
- Non loaded modules: 依赖于缺失或失败模块的模块
异步模块
可能发生的情况是,一个模块在准备好之前需要执行一些工作。例如,它可以做一个rpc来加载一些数据。在这种情况下,该模块可以简单地返回一个promise。模块系统将简单地等待promise的完成,然后再注册该模块
odoo.define('module.Something', function (require) {
"use strict";
var ajax = require('web.ajax');
return ajax.rpc(...).then(function (result) {
// some code here
return something;
});
});