JavaScript 模块¶
Odoo 支持三种不同的 JavaScript 文件:
普通 JavaScript 文件,不使用模块系统,
Odoo 模块,使用自定义模块系统,
如 资源管理页面 所述,所有 JavaScript 文件会被打包在一起并提供给浏览器。请注意,原生 JavaScript 文件会由 Odoo 服务器处理,并转换为 Odoo 自定义模块。
下面简要说明各类 JavaScript 文件的用途。普通 JavaScript 文件应仅保留给外部库以及某些特定的底层小用途。所有新的 JavaScript 文件都应使用原生 JavaScript 模块系统创建。自定义模块系统只对尚未转换的旧文件有用。
普通 JavaScript 文件¶
普通 JavaScript 文件可以包含任意内容。建议在编写此类文件时采用 iife 立即调用函数执行 风格:
(function () {
// some code here
let a = 1;
console.log(a);
})();
此类文件的优点在于可以避免将局部变量泄漏到全局作用域。
显然,普通 JavaScript 文件不具备模块系统的好处,因此需要注意资源包中的顺序(因为浏览器会严格按该顺序执行它们)。
注解
在 Odoo 中,所有外部库都以纯 JavaScript 文件的形式加载。
原生 JavaScript 模块¶
Odoo 的 JavaScript 代码使用原生 JavaScript 模块系统。这种方式更简单,并带来更好的开发体验,以及与 IDE 更好的集成。
让我们来看下面这个模块,它位于 web/static/src/file_a.js:
import { someFunction } from "./file_b";
export function otherFunction(val) {
return someFunction(val + 3);
}
有一个非常重要的一点需要知道:默认情况下,Odoo 会将 /static/src 和 /static/tests 下的文件转译为 Odoo 模块。然后该文件会被转译为如下所示的 Odoo 模块:
odoo.define('@web/file_a', ['@web/file_b'], 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,并更新 import/export 语句。这是一个可选择退出(opt-out)的机制,可以告知转译器忽略该文件。
/** @odoo-module ignore **/
(function () {
const sum = (a, b) => a + b;
console.log(sum(1, 2));
)();
请注意第一行中的注释:它说明该文件应被忽略。
在其他文件夹中,文件默认不会被转译,而是需要主动开启(opt-in)。Odoo 会查看 JS 文件的第一行,检查其中是否包含带有 @odoo-module 且不含 ignore 标记的注释。如果是,就会自动将其转换为 Odoo 模块。
/** @odoo-module **/
export function sum(a, b) {
return a + b;
}
另一个要点是,转译后的模块有一个正式名称:@web/file_a。这才是该模块的真实名称。所有相对导入也会被相应转换。Odoo 插件中的每个文件,只要它位于 some_addon/static/src/path/to/file.js,都会被分配一个以插件名称为前缀的名称,如下所示:@some_addon/path/to/file。
相对导入可以使用,但前提是这些模块位于同一个 Odoo 插件中。那么,假设我们有如下的文件结构:
addons/
web/
static/
src/
file_a.js
file_b.js
stock/
static/
src/
file_c.js
文件 file_b 可以这样导入 file_a:
import {something} from `./file_a`;
但 file_c 需要使用完整名称:
import {something} from `@web/file_a`;
别名模块¶
由于 Odoo 模块 遵循不同的模块命名模式,系统提供了一套机制,以便平滑过渡到新系统。目前,如果某个文件被转换为模块(因而遵循新的命名约定),项目中其他尚未转换为类 ES6 语法的文件将无法 require 该模块。别名的作用就是通过创建一个小型代理函数,把旧名称映射到新名称。之后,模块就可以用它的新名称 和 旧名称来调用。
要添加这样的别名,文件顶部的注释标签应如下所示:
/** @odoo-module alias=web.someName**/
import { someFunction } from './file_b';
export default function otherFunction(val) {
return someFunction(val + 3);
}
然后,转译后的模块还会用所请求的名称创建一个别名:
odoo.define(`web.someName`, ['@web/file_a'], function(require) {
return require('@web/file_a')[Symbol.for("default")];
});
别名的默认行为是重新导出被别名模块的 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`, ["@web/file_a"], function(require) {
return require('@web/file_a');
});
注解
使用此方法只能定义一个别名。如果你还需要另一个别名,例如想用三个名称调用同一个模块,就必须手动添加代理。这不是好的做法,除非别无选择,否则应当避免。
限制¶
出于性能考虑,Odoo 并未使用完整的 JavaScript 解析器来转换原生模块。因此存在一些限制,包括但不限于:
import或export关键字前面不能是非空白字符,多行注释或字符串中不能有以
import或export开头的行// supported import X from "xxx"; export X; export default X; import X from "xxx"; /* * import X ... */ /* * export X */ // not supported var a= 1;import X from "xxx"; /* import X ... */
导出对象时,对象中不能包含注释
// supported export { a as b, c, d, } export { a } from "./file_a" // not supported export { a as b, // this is a comment c, d, } export { a /* this is a comment */ } from "./file_a"
Odoo 需要一种方式来判断模块是由路径(如
./views/form_view)还是名称(如web.FormView)来描述的。为此它必须使用一种启发式规则:如果名称中包含/,就把它视为路径。这意味着 Odoo 实际上已不再支持包含/的模块名称。
由于“经典”模块并未被弃用,目前也没有移除它们的计划,因此当你遇到原生模块的问题、或受其限制时,可以而且应当继续使用它们。两种风格可以在同一个 Odoo 插件中共存。
Odoo 模块系统¶
Odoo 定义了一个小型模块系统(位于文件 addons/web/static/src/js/boot.js 中,需要最先加载)。Odoo 模块系统受 AMD 启发,其工作方式是在全局 odoo 对象上定义 define 函数。然后我们通过调用该函数来定义每个 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', ['module.A'], function (require) {
"use strict";
var A = require('module.A');
var B = ...; // something that involves A
return B;
});
如果某些依赖缺失或尚未就绪,那么该模块就不会被加载。几秒钟后,控制台中会出现一条警告。
请注意,不支持循环依赖。这合乎情理,但意味着需要格外小心。
定义模块¶
odoo.define 方法接受三个参数:
moduleName:JavaScript 模块的名称。它应当是一个唯一的字符串。约定是使用 odoo 插件名称后跟具体描述。例如,web.Widget描述的是web插件中定义的一个模块,它导出Widget类(因为首字母大写)如果名称不唯一,就会抛出异常并显示在控制台中。
dependencies:它应当是一个字符串列表,每个字符串对应一个 JavaScript 模块。这描述了模块执行前必须加载的依赖项。最后,最后一个参数是定义该模块的函数。它的返回值就是模块的值,可能会传递给需要该模块的其他模块。
odoo.define('module.Something', ['web.ajax'], function (require) { "use strict"; var ajax = require('web.ajax'); // some code here return something; });
如果发生错误,它会被记录(在调试模式下)到控制台中:
Missing dependencies:这些模块没有出现在页面中。可能是 JavaScript 文件不在页面中,或者模块名称有误Failed modules:检测到 JavaScript 错误Rejected modules:模块返回了一个被拒绝的 Promise。它(以及依赖它的模块)不会被加载。Rejected linked modules:依赖于被拒绝模块的模块Non loaded modules:依赖于缺失或失败模块的模块