Javascript 模块

Odoo 支持三种不同类型的 javascript 文件:

如 资源管理页面 中所述,所有 javascript 文件都会打包在一起并提供给浏览器。请注意,原生 javascript 文件会由 Odoo 服务器处理并转换为 Odoo 自定义模块。

让我们简要解释每种 javascript 文件的用途。普通 javascript 文件应仅保留给外部库和一些小型的特定底层用途。所有新的 javascript 文件都应以原生 javascript 模块系统创建。自定义模块系统仅对旧的、尚未转换的文件有用。

普通 Javascript 文件

普通 javascript 文件可以包含任意内容。建议在编写此类文件时使用 iife 立即调用的函数执行 风格:

(function () {
  // some code here
  let a = 1;
  console.log(a);
})();

这类文件的优点是可以避免将局部变量泄漏到全局作用域。

显然,普通 javascript 文件不具备模块系统的优势,因此需要小心处理包中的顺序(因为浏览器将严格按照该顺序执行它们)。

Note

在 Odoo 中,所有外部库都以普通 javascript 文件的形式加载。

原生 Javascript 模块

大多数新的 Odoo javascript 代码都应使用原生 javascript 模块系统。它更简单,并带来更好的开发者体验以及与 IDE 的更好集成。

有一个非常重要的点需要知道:Odoo 需要知道哪些文件应被转换为 Odoo 模块,哪些文件不应转换。这是一个可选启用的系统:Odoo 会查看 JS 文件的第一行,检查它是否包含字符串 @odoo-module。如果包含,就会自动将其转换为 Odoo 模块。

例如,我们来看位于 web/static/src/file_a.js 中的以下模块:

/** @odoo-module **/
import { someFunction } from './file_b';

export function otherFunction(val) {
    return someFunction(val + 3);
}

请注意第一行的注释:它说明该文件应当被转换。任何没有此注释的文件都会原样保留(这很可能是错误的)。随后该文件会被转换为如下所示的 Odoo 模块:

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,并更新 import/export 语句。

另一个重要之处在于,转换后的模块有一个正式名称:@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:

/** @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")];
});

别名的默认行为是重新导出其所别名模块的 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');
});

Note

使用此方法只能定义一个别名。如果您需要另一个别名,例如需要三个名称来调用同一个模块,则必须手动添加代理。这不是好的做法,除非别无选择,否则应避免。

限制

出于性能原因,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', 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 插件的名称,后跟一个具体的描述。例如,web.Widget 描述了定义在 web 插件中的一个模块,它导出一个 Widget 类(因为首字母大写)

    如果名称不唯一,将会抛出异常并显示在控制台中。

  • dependencies:第二个参数是可选的。如果提供,它应为一个字符串列表,每个字符串对应一个 javascript 模块。它描述了在模块执行之前需要加载的依赖项。如果没有在此处显式给出依赖项,模块系统会通过调用 toString 从函数中提取它们,然后使用正则表达式查找所有 require 语句。

    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:依赖于缺失或失败模块的那些模块

异步模块

有时模块在就绪之前需要执行一些工作。例如,它可能通过 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;
    });
});