odoo16前端框架源码阅读——env.js
env.js(env的初始化以及服务的加载)
路径:addons\web\static\src\env.js
这个文件的作用就是初始化env,主要是加载所有的服务。如orm, title, dialog等。
1、env.js 的加载时机
前文我们讲过前端的启动函数,start.js,其中有这么两句,这里有两个函数makeEnv和startServices,都在同级目录的env.js里
const env = makeEnv();
await startServices(env);
2、 makeEnv()
export function makeEnv() {
return {
bus: new EventBus(),
services: {},
debug: odoo.debug,
get isSmall() {
throw new Error("UI service not initialized!");
},
};
}
从代码可以看出,env有4个属性:
- bus: 全局数据总线
- services:全局服务对象
- debug: 是否debug模式
- isSmall:判断手机端还是移动端
这里抛出一个异常是为了在后面会覆盖这个方法。
从中可以看出,env最重要的其实就两个对象,一个是全局数据总线,负责组件之间通信,另一个就是service。
3、startServices
启动所有在注册表中注册的服务,并且保证所有的依赖都得到满足。
首先获取所有在注册表中注册的服务serviceRegistry
初始化元数据SERVICES_METADATA, 主要是保存service中sync的内容,有什么作用还不清楚
主角开始上场: startServices
启动所有在注册表中注册的服务,并且保证每个服务的依赖都得到满足。
await Promise.resolve(); // 等待之前的异步操作都完成
const toStart = new Set(); // 初始化要启动的服务,因为每个服务都不同,所以用了set
serviceRegistry.addEventListener // 这段没看懂,这里添加了一个事件监听,动态增加注册服务的
await _startServices(env, toStart); // 将env和toStart对象传入,干正事了
4._startServices(env, toStart)
4.1 填充 toStart 集合
const services = env.services; //获取了env中的service对象,这时候还是空的,啥也没有
// 根据注册表中的服务信息,初始化了toStart集合,里面都是创建好的实例对象,并且有一个name属性。
for (const [name, service] of serviceRegistry.getEntries()) {
if (!(name in services)) {
const namedService = Object.assign(Object.create(service), { name });
toStart.add(namedService);
}
}
4.2、findNext()
关键的地方来了, 这里有一个关键的函数findNext(),先看看它的定义:
// 从toStart集合中遍历,有过集合中的服务有依赖并且依赖都已经满足了,则返回这个service,如果没有依赖,那么直接返回,找到第一个就返回,不叨叨,直到找不到,返回null
function {
for (const s of toStart) {
if (s.dependencies) {
if (s.dependencies.every((d) => d in services)) {
return s;
}
} else {
return s;
}
}
return null;
}
4.3 、启动服务,并填充到env的services中
在start函数中,findNext函数返回的服务,第一步要最的就是从toStart 删除,这样遍历的次数会越来越少,我看过,odoo17一共有69个服务,while一共还要遍历69次,每次加载一个服务。 通过这种方式,很好的处理了服务之间的依赖关系,并且最大限度的实现了并行。
// start as many services in parallel as possible 并行启动尽可能多的服务
async 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); // 调用start函数,并将返回值付给value
} catch (e) {
value = e;
console.error(e);
}
if ("async" in service) {
SERVICES_METADATA[name] = service.async; // 保存服务的元数据,后面可能会有用
}
if (value instanceof Promise) { // 如果value是一个Promise
proms.push(
new Promise((resolve) => {
value
.then((val) => {
services[name] = val || null; // 将promise的返回值保存到services中
})
.catch((error) => {
services[name] = error;
console.error("Can't load service '" + name + "' because:", error);
})
.finally(resolve);
})
);
} else {
services[service.name] = value || null; // 如果不是promise,直接将value保存到services中
}
}
await Promise.all(proms); // 等待所有的proms完成
if (proms.length) {
return start();
}
}
到这里,前端js就完成了所有service的加载,要注意的是, env.services中保存的不是 services对象本身,而是service对象的start函数返回的对象。
这点很重要,每个service都要有start函数,而且要有返回值。
4.4、后面还有一段是异常处理
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}`
);
}
如果toStart.size >0 ,说明这里面的服务的依赖想没有得到满足,所以无法加载,会抛出一个Error
5、附录:odoo17 env.js
/** @odoo-module **/
import { registry } from "./core/registry";
import { EventBus } from "@odoo/owl";
// -----------------------------------------------------------------------------
// Types
// -----------------------------------------------------------------------------
/**
* @typedef {Object} OdooEnv
* @property {import("services").Services} services
* @property {EventBus} bus
* @property {string} debug
* @property {(str: string) => string} _t
* @property {boolean} [isSmall]
*/
// -----------------------------------------------------------------------------
// makeEnv
// -----------------------------------------------------------------------------
/**
* Return a value Odoo Env object
*
* @returns {OdooEnv}
*/
export function makeEnv() {
return {
bus: new EventBus(),
services: {},
debug: odoo.debug,
get isSmall() {
throw new Error("UI service not initialized!");
},
};
}
// -----------------------------------------------------------------------------
// Service Launcher
// -----------------------------------------------------------------------------
const serviceRegistry = registry.category("services");
export const SERVICES_METADATA = {};
let startServicesPromise = null;
/**
* Start all services registered in the service registry, while making sure
* each service dependencies are properly fulfilled.
*
* @param {OdooEnv} env
* @returns {Promise<void>}
*/
export async function startServices(env) {
// 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.
debugger
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 possible
async 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;
}
}