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 =newSet();// 初始化要启动的服务,因为每个服务都不同,所以用了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;}}returnnull;}
4.3 、启动服务,并填充到env的services中
在start函数中,findNext函数返回的服务,第一步要最的就是从toStart 删除,这样遍历的次数会越来越少,我看过,odoo17一共有69个服务,while一共还要遍历69次,每次加载一个服务。 通过这种方式,很好的处理了服务之间的依赖关系,并且最大限度的实现了并行。
// start as many services in parallel as possible 并行启动尽可能多的服务asyncfunctionstart(){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 instanceofPromise){// 如果value是一个Promise proms.push(newPromise((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){returnstart();}}
到这里,前端js就完成了所有service的加载,要注意的是, env.services中保存的不是 services对象本身,而是service对象的start函数返回的对象。
这点很重要,每个service都要有start函数,而且要有返回值。
4.4、后面还有一段是异常处理
if(toStart.size){const names =[...toStart].map((s)=> s.name);const missingDeps =newSet();[...toStart].forEach((s)=> s.dependencies.forEach((dep)=>{if(!(dep in services)&&!names.includes(dep)){ missingDeps.add(dep);}}));const depNames =[...missingDeps].join(", ");thrownewError(`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} */exportfunctionmakeEnv(){return{bus:newEventBus(),services:{},debug: odoo.debug,getisSmall(){thrownewError("UI service not initialized!");},};}// -----------------------------------------------------------------------------// Service Launcher// -----------------------------------------------------------------------------const serviceRegistry = registry.category("services");exportconstSERVICES_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>} */exportasyncfunctionstartServices(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.debuggerawait Promise.resolve();const toStart =newSet(); 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);}asyncfunction_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 possibleasyncfunctionstart(){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 instanceofPromise){ proms.push(newPromise((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){returnstart();}} startServicesPromise =start();await startServicesPromise; startServicesPromise =null;if(toStart.size){const names =[...toStart].map((s)=> s.name);const missingDeps =newSet();[...toStart].forEach((s)=> s.dependencies.forEach((dep)=>{if(!(dep in services)&&!names.includes(dep)){ missingDeps.add(dep);}}));const depNames =[...missingDeps].join(", ");thrownewError(`Some services could not be started: ${names}. Missing dependencies: ${depNames}`);}functionfindNext(){for(const s of toStart){if(s.dependencies){if(s.dependencies.every((d)=> d in services)){return s;}}else{return s;}}returnnull;}}