ماژول‌های Javascript

Odoo از سه نوع مختلف فایل جاوااسکریپت پشتیبانی می‌کند:

As described in the assets management page, all javascript files are bundled together and served to the browser. Note that native javascript files are processed by the Odoo server and transformed into Odoo custom modules.

Let us briefly explain the purpose behind each kind of javascript file. Plain javascript files should be reserved only for external libraries and some small specific low level purposes. All new javascript files should be created in the native javascript module system. The custom module system is only useful for old, not yet converted files.

فایل‌های Javascript ساده

Plain javascript files can contain arbitrary content. It is advised to use the iife immediately invoked function execution style when writing such a file:

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

The advantages of such files is that we avoid leaking local variables to the global scope.

روشن است که فایل‌های javascript ساده مزایای یک سیستم ماژول را ارائه نمی‌دهند، بنابراین باید در ترتیب قرارگیری در bundle دقت کرد (زیرا مرورگر آن‌ها را دقیقاً به همان ترتیب اجرا خواهد کرد).

توجه

در Odoo، همهٔ کتابخانه‌های خارجی به‌صورت فایل‌های javascript ساده بارگذاری می‌شوند.

ماژول‌های Javascript بومی

کد javascript Odoo از سیستم ماژول javascript بومی استفاده می‌کند. این روش ساده‌تر است و مزایای تجربهٔ توسعه‌دهندهٔ بهتر و یکپارچگی بهتر با 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.

Relative imports work, but only if the modules are in the same Odoo addon. So, imagine that we have the following file structure:

addons/
    web/
        static/
            src/
                file_a.js
                file_b.js
    stock/
        static/
            src/
                file_c.js

فایل file_b می‌تواند file_a را به این صورت import کند:

import {something} from `./file_a`;

اما file_c باید از نام کامل استفاده کند:

import {something} from `@web/file_a`;

ماژول‌های Alias شده

Because Odoo modules follow a different module naming pattern, a system exists to allow a smooth transition towards the new system. Currently, if a file is converted to a module (and therefore follow the new naming convention), other files not yet converted to ES6-like syntax in the project won't be able to require the module. Aliases are here to map old names with new ones by creating a small proxy function. The module can then be called by its new and old name.

To add such alias, the comment tag on top of the file should look like this:

/** @odoo-module alias=web.someName**/
import { someFunction } from './file_b';

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

Then, the translated module will also create an alias with the requested name:

odoo.define(`web.someName`, ['@web/file_a'], function(require) {
    return require('@web/file_a')[Symbol.for("default")];
});

The default behaviour of aliases is to re-export the default value of the module they alias. This is because "classic" modules generally export a single value which would be used directly, roughly matching the semantics of default exports. However it is also possible to delegate more directly, and follow the exact behaviour of the aliased module:

/** @odoo-module alias=web.someName default=0**/
import { someFunction } from './file_b';

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

In that case, this will define an alias with exactly the values exported by the original module:

odoo.define(`web.someName`, ["@web/file_a"], function(require) {
    return require('@web/file_a');
});

توجه

Only one alias can be defined using this method. If you were to need another one to have, for example, three names to call the same module, you would have to add a proxy manually. This is not good practice and should be avoided unless there is no other options.

محدودیت‌ها

For performance reasons, Odoo does not use a full javascript parser to transform native modules. There are, therefore, a number of limitations including but not limited to:

  • an import or export keyword cannot be preceded by a non-space character,

  • a multiline comment or string cannot have a line starting by import or 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 ...
    */
    
  • هنگامی که یک شیء را export می‌کنید، نمی‌تواند حاوی یک comment باشد

    // 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 needs a way to determine if a module is described by a path (like ./views/form_view) or a name (like web.FormView). It has to use a heuristic to do just that: if there is a / in the name, it is considered a path. This means that Odoo does not really support module names with a / anymore.

As "classic" modules are not deprecated and there is currently no plan to remove them, you can and should keep using them if you encounter issues with, or are constrained by the limitations of, native modules. Both styles can coexist within the same Odoo addon.

سیستم ماژول Odoo

Odoo has defined a small module system (located in the file addons/web/static/src/js/boot.js, which needs to be loaded first). The Odoo module system, inspired by AMD, works by defining the function define on the global odoo object. We then define each javascript module by calling that function. In the Odoo framework, a module is a piece of code that will be executed as soon as possible. It has a name and potentially some dependencies. When its dependencies are loaded, a module will then be loaded as well. The value of the module is then the return value of the function defining the module.

به‌عنوان مثال، می‌تواند این‌گونه به نظر برسد:

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

If some dependencies are missing/non ready, then the module will simply not be loaded. There will be a warning in the console after a few seconds.

Note that circular dependencies are not supported. It makes sense, but it means that one needs to be careful.

تعریف یک ماژول

متد 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)

    اگر نام یکتا نباشد، یک exception پرتاب شده و در کنسول نمایش داده می‌شود.

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

اگر خطایی رخ دهد، (در حالت debug) در کنسول گزارش می‌شود:

  • 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: یک خطای javascript تشخیص داده شده است

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

  • Rejected linked modules: ماژول‌هایی که به یک ماژول رد شده وابسته‌اند

  • Non loaded modules: ماژول‌هایی که به یک ماژول گم‌شده یا ناموفق وابسته‌اند