JavaScript 模块

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

  • :ref:`普通 JavaScript 文件 <frontend/modules/plain_js>`(无模块系统),

  • 原生 JavaScript 模块

  • :ref:`Odoo 模块 <frontend/modules/odoo_module>`(使用自定义模块系统),

资产管理页面 所述,所有 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,并更新导入/导出语句。这是一个可选择退出的系统,可以告诉编译器忽略该文件。

/** @odoo-module ignore **/
(function () {
  const sum = (a, b) => a + b;
  console.log(sum(1, 2));
)();

请注意第一行的注释:它说明该文件应被忽略。

在其他文件夹中,文件默认不会被转译,这是可选的。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 解析器来转换原生模块。因此,存在一些限制,包括但不限于:

  • 一个 importexport 关键字不能被非空格字符前缀。

  • 一个多行注释或字符串不能以 importexport 开头的行开始

    // 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;
    });
    

如果发生错误,它将在控制台中记录(在调试模式下):

  • 缺少依赖项:这些模块不会出现在页面上。可能是 JavaScript 文件未包含在页面中,或者模块名称有误。

  • 失败的模块:检测到 JavaScript 错误

  • 已拒绝的模块:该模块返回一个已拒绝的 Promise。它(以及其依赖的模块)不会被加载。

  • 已拒绝的关联模块:依赖于已拒绝模块的模块

  • 未加载的模块:依赖于缺失或失败模块的模块