odoo17 web.assets_web.min.js 赏析
前文说到,odoo17的前端js做了大量裁剪优化,最终打包成了一个文件assets.web.js
稍微看一下这个文件的结构
web.assets_web.min.js
行 1: /* /web/static/src/module_loader.js */
行 173: /* /web/static/lib/luxon/luxon.js */
行 4585: /* /web/static/lib/owl/owl.js */
行 9954: /* /web/static/lib/owl/odoo_module.js */
行 9960: /* /web/static/src/env.js */
行 10085: /* /web/static/src/session.js */
行 10094: /* /web/static/src/core/action_swiper/action_swiper.js */
行 10273: /* /web/static/src/core/assets.js */
中间省略几百行
行 119389: /* /web/static/src/main.js */
行 119399: /* /web/static/src/start.js */
中间的不太重要,几百个文件也看不过来,稍微分析一下开头和结尾。
1、module_loader.js
这是一个引导程序,也是一个立即执行函数,odoo执行的第一个js文件。
这个文件的作用:
定义了odoo这个全局变量,并且定义了它的三个属性:
- odoo.debug
- odoo.loader
- odoo.define
最重要的莫过于odoo.define, 因为后面有几百个模块都是调用这个函数进行加载。而加载好的模块都放到了odoo.loader.modules字典中。大概有七百多个
web.js中定义的全局变量除了odoo,还有两个,分别是luxon 和owl, owl我们都清楚,luxon是一个时间处理库,有时间研究一下。
odoo.define 有三个参数,分别是名称 ,依赖, 函数
ModuleLoader类中定义了一个jobs集合,每执行一次odoo.define函数,都会去jobs中遍历一遍,将满足依赖关系的模块加载。
然后将name和实例化后的对象作为key和value放在了modules字典中。
这个跟服务的加载差不多。都要解决依赖的问题。
/***------------------------------------------------------------------------------* Odoo Web Boostrap Code*------------------------------------------------------------------------------*/
(function () {"use strict";class ModuleLoader {/** @type {Map<string,{fn: Function, deps: string[]}>} mapping name => deps/fn */factories = new Map();/** @type {Set<string>} names of modules waiting to be started */jobs = new Set();/** @type {Set<string>} names of failed modules */failed = new Set();/** @type {Map<string,any>} mapping name => value */modules = new Map();bus = new EventTarget();checkErrorProm = null;/*** @param {string} name* @param {string[]} deps* @param {Function} factory*/define(name, deps, factory) {if (typeof name !== "string") {throw new Error(`Invalid name definition: ${name} (should be a string)"`);}if (!(deps instanceof Array)) {throw new Error(`Dependencies should be defined by an array: ${deps}`);}if (typeof factory !== "function") {throw new Error(`Factory should be defined by a function ${factory}`);}if (!this.factories.has(name)) {this.factories.set(name, {deps,fn: factory,ignoreMissingDeps: globalThis.__odooIgnoreMissingDependencies,});this.addJob(name);this.checkErrorProm ||= Promise.resolve().then(() => {this.checkAndReportErrors();this.checkErrorProm = null;});}}addJob(name) {this.jobs.add(name);this.startModules();}findJob() {for (const job of this.jobs) {if (this.factories.get(job).deps.every((dep) => this.modules.has(dep))) {return job;}}return null;}startModules() {let job;while ((job = this.findJob())) {this.startModule(job);}}startModule(name) {const require = (name) => this.modules.get(name);this.jobs.delete(name);const factory = this.factories.get(name);let value = null;try {value = factory.fn(require);} catch (error) {this.failed.add(name);throw new Error(`Error while loading "${name}":\n${error}`);}this.modules.set(name, value);this.bus.dispatchEvent(new CustomEvent("module-started", { detail: { moduleName: name, module: value } }));}// 省略几万字。。。if (!globalThis.odoo) {globalThis.odoo = {};}const odoo = globalThis.odoo;if (odoo.debug && !new URLSearchParams(location.search).has("debug")) {// remove debug mode if not explicitely set in urlodoo.debug = "";}const loader = new ModuleLoader();odoo.define = loader.define.bind(loader);odoo.loader = loader;
})();
2、luxon.js
moment.js 的升级版,这里会生成一个全局的对象luxon, 用于处理时间和日期
3、owl.js
猫头鹰组件系统,熟悉odoo的都懂,这里会生成一个全局对象owl
4、odoo_module.js
这个文件有点意思,它将owl全局对象定义成了@odoo/owl 模块,我们经常从js中代码中看到下面这种ES6的写法,这样做可能是为了保证风格的统一吧
import { Component } from "@odoo/owl";
/* /web/static/lib/owl/odoo_module.js */
odoo.define("@odoo/owl", [], function() {"use strict";return owl;
});
5、env.js
这个文件非常重要,它定义了两个函数
makeEnv: 初始化env对象。
startServices: 启动所有的webService, 这个函数的实现非常巧妙,因为它要在不考虑服务加载顺序的前提下解决服务之间的依赖问题。 其中细节,值得一读。
不过这里只有这两个函数的定义,并没有执行这两个函数, 真正执行这两个函数的位置在 start.js中
const env = makeEnv();await startServices(env);
服务启动完后, 会放入env的services对象中。 注意其中放的不是服务对象本身,而是start函数的返回值。
env.js
export function makeEnv() {return {bus: new EventBus(),services: {},debug: odoo.debug,get isSmall() {throw new Error("UI service not initialized!");},};
}// -----------------------------------------------------------------------------
// Service Launcher
// -----------------------------------------------------------------------------export async function startServices(env) {await Promise.resolve();const toStart = new Set();serviceRegistry.addEventListener("UPDATE", async (ev) => {// Wait for all synchronous code so that if new services that depend on// one another are added to the registry, they're all present before we// start them regardless of the order they're added to the registry.await Promise.resolve();const { operation, key: name, value: service } = ev.detail;if (operation === "delete") {// We hardly see why it would be usefull to remove a service.// Furthermore we could encounter problems with dependencies.// Keep it simple!return;}if (toStart.size) {const namedService = Object.assign(Object.create(service), { name });toStart.add(namedService);} else {await _startServices(env, toStart);}});await _startServices(env, toStart);
}async function _startServices(env, toStart) {if (startServicesPromise) {return startServicesPromise.then(() => _startServices(env, toStart));}const services = env.services;for (const [name, service] of serviceRegistry.getEntries()) {if (!(name in services)) {const namedService = Object.assign(Object.create(service), { name });toStart.add(namedService);}}// start as many services in parallel as possibleasync function start() {let service = null;const proms = [];while ((service = findNext())) {const name = service.name;toStart.delete(service);const entries = (service.dependencies || []).map((dep) => [dep, services[dep]]);const dependencies = Object.fromEntries(entries);let value;try {value = service.start(env, dependencies);} catch (e) {value = e;console.error(e);}if ("async" in service) {SERVICES_METADATA[name] = service.async;}if (value instanceof Promise) {proms.push(new Promise((resolve) => {value.then((val) => {services[name] = val || null;}).catch((error) => {services[name] = error;console.error("Can't load service '" + name + "' because:", error);}).finally(resolve);}));} else {services[service.name] = value || null;}}await Promise.all(proms);if (proms.length) {return start();}}startServicesPromise = start();await startServicesPromise;startServicesPromise = null;if (toStart.size) {const names = [...toStart].map((s) => s.name);const missingDeps = new Set();[...toStart].forEach((s) =>s.dependencies.forEach((dep) => {if (!(dep in services) && !names.includes(dep)) {missingDeps.add(dep);}}));const depNames = [...missingDeps].join(", ");throw new Error(`Some services could not be started: ${names}. Missing dependencies: ${depNames}`);}function findNext() {for (const s of toStart) {if (s.dependencies) {if (s.dependencies.every((d) => d in services)) {return s;}} else {return s;}}return null;}
}
6、session.js
// odoo.__session_info__ 是后端通过渲染模板生成的, 这也是前后端交互的一种方式,用的很少。export const session = odoo.__session_info__ || {};
delete odoo.__session_info__;
7、随便看一个odoo.define定义的模块
有几百个通过odoo.define定义的模块, 我们且看一个最简单的,也就是上面的这个session.js
下面这个文件是由py代码自动生成的,将ES6风格的js文件转化成 commonjs格式
其中require 是一个函数,在module_loader中定义的,根据名称,返回指定的模块的实例。
const require = (name) => this.modules.get(name);
/* /web/static/src/session.js */
odoo.define('@web/session', [], function(require) {'use strict';let __exports = {};const session = __exports.session = odoo.__session_info__ || {};delete odoo.__session_info__;return __exports;
});
每调用一次odoo.define,系统都会去循环遍历所有需要加载的模块,查找那些满足依赖关系的模块进行加载。
8、main.js
现在看看最后的两个文件。
这个文件是入口文件,按理说应该放在最后,可是odoo却把它放在了倒数第二,难道是为了证明模块的加载顺序并不会因为模块之间的依赖关系而影响模块加载吗?
/** @odoo-module **/import { startWebClient } from "./start";
import { WebClient } from "./webclient/webclient";startWebClient(WebClient);
startWebClient(WebClient); 当所有模块都加载到内存中之后执行这一句。前端的UI页面也就渲染出来了。
9、start.js
定义了startWebClient 函数供main.js调用
在startWebClient 中,还做了两件事,前文提到过
调用了env.js中的两个函数,初始化了env并加载了所有的服务。
const env = makeEnv();await startServices(env);