Javascript 模块

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

assets management page 所述,所有的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 code uses the native javascript module system. This is simpler, and brings the benefits of a better developer experience with a better integration with the IDE.

Let us consider the following module, located in web/static/src/file_a.js:

import { someFunction } from "./file_b";

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

There is a very important point to know: by default Odoo transpiles files under /static/src and /static/tests into Odoo modules. This file will then be transpiled into an Odoo module that looks like this:

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

So, as you can see, the transformation is basically adding odoo.define on top and updating the import/export statements. This is an opt-out system, it’s possible to tell the transpiler to ignore the file.

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

Note the comment in the first line: it describes that this file should be ignored.

In other folders, files aren’t transpiled by default, it is opt-in. Odoo will look at the first line of a JS file and check if it contains a comment with @odoo-module and without the tag ignore. If so, it will automatically be converted to an Odoo module.

/** @odoo-module **/
export function sum(a, b) {
  return a + b;
}

Another important point is that the transpiled module has an official name: @web/file_a. This is the actual name of the module. Every relative imports will be converted as well. Every file located in an Odoo addon some_addon/static/src/path/to/file.js will be assigned a name prefixed by the addon name like this: @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 类似语法的其他文件将无法引用该模块。别名用于通过创建一个小的代理函数将旧名称映射到新名称。然后可以通过新的 旧的名称调用该模块。

要添加这样的别名,文件顶部的注释标签应该像这样:

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

如果某些依赖项缺失/未准备好,则该模块将不会被加载。几秒钟后,控制台会出现警告。

请注意,不支持循环依赖。这很有道理,但这意味着需要小心处理。

定义一个模块

The odoo.define 方法接受三个参数:

  • moduleName: the name of the javascript module. It should be a unique string. The convention is to have the name of the odoo addon followed by a specific description. For example, web.Widget describes a module defined in the web addon, which exports a Widget class (because the first letter is capitalized)

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

  • dependencies: It should be a list of strings, each corresponding to a javascript module. This describes the dependencies that are required to be loaded before the module is executed.

  • finally, the last argument is a function which defines the module. Its return value is the value of the module, which may be passed to other modules requiring it.

    odoo.define('module.Something', ['web.ajax'], function (require) {
        "use strict";
    
        var ajax = require('web.ajax');
    
        // some code here
        return something;
    });
    

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

  • Missing dependencies: These modules do not appear in the page. It is possible that the JavaScript file is not in the page or that the module name is wrong

  • Failed modules: A javascript error is detected

  • Rejected modules: The module returns a rejected Promise. It (and its dependent modules) is not loaded.

  • Rejected linked modules: Modules who depend on a rejected module

  • Non loaded modules: Modules who depend on a missing or a failed module