odoo16前端框架源码阅读——启动、菜单、动作 目录:addons/web/static/src
1、main.js odoo实际上是一个单页应用,从名字看,这是前端的入口文件,文件内容也很简单。
/** @odoo-module **/
import { startWebClient } from "./start"; import { WebClient } from "./webclient/webclient";
/**
- This file starts the webclient. It is in its own file to allow its replacement
- in enterprise. The enterprise version of the file uses its own webclient import,
- which is a subclass of the above Webclient. */
startWebClient(WebClient);
1 2 3 4 5 6 7 8 9 10 11 12 13 关键的是最后一行代码 ,调用了startWebClient函数,启动了一个WebClient。 非常简单,而且注释也说明了,企业版可以启动专有的webclient。
2、start.js 这个模块中只有一个函数startWebClient,注释也说明了,它的作用就是启动一个webclient,而且企业版和社区版都会执行这个函数,只是webclient不同而已。
这个文件大概干了这么几件事:
2、生成env并启动相关服务
3、定义了一个app对象,并且把Webclient 做了构造参数传递进去,并且将app挂载到body上
4、根据不同的环境,给body设置了不同的class
5、最后设置odoo.ready=true
总体来说,就是准备环境,启动服务,生成app。 这个跟vue的做法类似。
/** @odoo-module **/
import { makeEnv, startServices } from "./env"; import { legacySetupProm } from "./legacy/legacy_setup"; import { mapLegacyEnvToWowlEnv } from "./legacy/utils"; import { localization } from "@web/core/l10n/localization"; import { session } from "@web/session"; import { renderToString } from "./core/utils/render"; import { setLoadXmlDefaultApp, templates } from "@web/core/assets"; import { hasTouch } from "@web/core/browser/feature_detection";
import { App, whenReady } from "@odoo/owl";
/**
- Function to start a webclient.
- It is used both in community and enterprise in main.js.
- It's meant to be webclient flexible so we can have a subclass of
- webclient in enterprise with added features.
- @param {Component} Webclient
*/
export async function startWebClient(Webclient) {
odoo.info = { db: session.db, server_version: session.server_version, server_version_info: session.server_version_info, isEnterprise: session.server_version_info.slice(-1)[0] === "e", }; odoo.isReady = false;
// setup environment const env = makeEnv(); await startServices(env);
// start web client await whenReady(); const legacyEnv = await legacySetupProm; mapLegacyEnvToWowlEnv(legacyEnv, env); const app = new App(Webclient, { env, templates, dev: env.debug, translatableAttributes: ["data-tooltip"], translateFn: env._t, }); renderToString.app = app; setLoadXmlDefaultApp(app); const root = await app.mount(document.body); const classList = document.body.classList; if (localization.direction === "rtl") { classList.add("o_rtl"); } if (env.services.user.userId === 1) { classList.add("o_is_superuser"); } if (env.debug) { classList.add("o_debug"); } if (hasTouch()) { classList.add("o_touch_device"); } // delete odoo.debug; // FIXME: some legacy code rely on this odoo.WOWL_DEBUG = { root }; odoo.isReady = true;
// Update Favicons const favicon =/web/image/res.company/${env.services.company.currentCompany.id}/favicon
; const icons = document.querySelectorAll("link[rel*='icon']"); const msIcon = document.querySelector("meta[name='msapplication-TileImage']"); for (const icon of icons) { icon.href = favicon; } if (msIcon) { msIcon.content = favicon; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 3、WebClient 很明显,webclient是一个owl组件,这就是我们看到的odoo的主界面,值得好好分析。
这里的重点就是:
在onMounted钩子中调用了 this.loadRouterState();
而这个函数呢,一开始就获取了两个变量:
let stateLoaded = await this.actionService.loadState();
let menuId = Number(this.router.current.hash.menu_id || 0);
1 2 后面就是根据这两个变量的值的不同的组合进行处理。 如果menuId 为false,则返回第一个应用。
/** @odoo-module **/
import { useOwnDebugContext } from "@web/core/debug/debug_context"; import { DebugMenu } from "@web/core/debug/debug_menu"; import { localization } from "@web/core/l10n/localization"; import { MainComponentsContainer } from "@web/core/main_components_container"; import { registry } from "@web/core/registry"; import { useBus, useService } from "@web/core/utils/hooks"; import { ActionContainer } from "./actions/action_container"; import { NavBar } from "./navbar/navbar";
import { Component, onMounted, useExternalListener, useState } from "@odoo/owl";
export class WebClient extends Component { setup() { this.menuService = useService("menu"); this.actionService = useService("action"); this.title = useService("title"); this.router = useService("router"); this.user = useService("user"); useService("legacy_service_provider"); useOwnDebugContext({ categories: ["default"] }); if (this.env.debug) { registry.category("systray").add( "web.debug_mode_menu", { Component: DebugMenu, }, { sequence: 100 } ); } this.localization = localization; this.state = useState({ fullscreen: false, }); this.title.setParts({ zopenerp: "Odoo" }); // zopenerp is easy to grep useBus(this.env.bus, "ROUTE_CHANGE", this.loadRouterState); useBus(this.env.bus, "ACTION_MANAGER:UI-UPDATED", ({ detail: mode }) => { if (mode !== "new") { this.state.fullscreen = mode === "fullscreen"; } }); onMounted(() => { this.loadRouterState(); // the chat window and dialog services listen to 'web_client_ready' event in // order to initialize themselves: this.env.bus.trigger("WEB_CLIENT_READY"); }); useExternalListener(window, "click", this.onGlobalClick, { capture: true }); }
async loadRouterState() {
let stateLoaded = await this.actionService.loadState();
let menuId = Number(this.router.current.hash.menu_id || 0);
if (!stateLoaded && menuId) {
// Determines the current actionId based on the current menu
const menu = this.menuService.getAll().find((m) => menuId === m.id);
const actionId = menu && menu.actionID;
if (actionId) {
await this.actionService.doAction(actionId, { clearBreadcrumbs: true });
stateLoaded = true;
}
}
if (stateLoaded && !menuId) {
// Determines the current menu based on the current action
const currentController = this.actionService.currentController;
const actionId = currentController && currentController.action.id;
const menu = this.menuService.getAll().find((m) => m.actionID === actionId);
menuId = menu && menu.appID;
}
if (menuId) {
// Sets the menu according to the current action
this.menuService.setCurrentMenu(menuId);
}
if (!stateLoaded) {
// If no action => falls back to the default app
await this._loadDefaultApp();
}
}
_loadDefaultApp() {
// Selects the first root menu if any
const root = this.menuService.getMenu("root");
const firstApp = root.children[0];
if (firstApp) {
return this.menuService.selectMenu(firstApp);
}
}
/**
* @param {MouseEvent} ev
*/
onGlobalClick(ev) {
// When a ctrl-click occurs inside an <a href/> element
// we let the browser do the default behavior and
// we do not want any other listener to execute.
if (
ev.ctrlKey &&
!ev.target.isContentEditable &&
((ev.target instanceof HTMLAnchorElement && ev.target.href) ||
(ev.target instanceof HTMLElement && ev.target.closest("a[href]:not([href=''])")))
) {
ev.stopImmediatePropagation();
return;
}
}
} WebClient.components = { ActionContainer, NavBar, MainComponentsContainer, }; WebClient.template = "web.WebClient";
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 4、web.WebClient webclient的模板文件,简单的狠,用了三个组件
NavBar: 顶部的导航栏
ActionContainer: 除了导航栏之外的其他可见的部分
MainComponentsContainer: 这其实是不可见的,包含了通知之类的东东,在一定条件下可见
<t t-name="web.WebClient" owl="1">
<t t-if="!state.fullscreen">
<NavBar/>
</t>
<ActionContainer/>
<MainComponentsContainer/>
</t>
1 2 3 4 5 6 7 8 9 10 11 12 13 5、menus\menu_service.js Webclient中用到了menuservice,现在来看看这个文件
/** @odoo-module **/
import { browser } from "../../core/browser/browser"; import { registry } from "../../core/registry"; import { session } from "@web/session";
const loadMenusUrl = /web/webclient/load_menus
;
function makeFetchLoadMenus() {
const cacheHashes = session.cache_hashes;
let loadMenusHash = cacheHashes.load_menus || new Date().getTime().toString();
return async function fetchLoadMenus(reload) {
if (reload) {
loadMenusHash = new Date().getTime().toString();
} else if (odoo.loadMenusPromise) {
return odoo.loadMenusPromise;
}
const res = await browser.fetch(${loadMenusUrl}/${loadMenusHash}
);
if (!res.ok) {
throw new Error("Error while fetching menus");
}
return res.json();
};
}
function makeMenus(env, menusData, fetchLoadMenus) { let currentAppId; return { getAll() { return Object.values(menusData); }, getApps() { return this.getMenu("root").children.map((mid) => this.getMenu(mid)); }, getMenu(menuID) { return menusData[menuID]; }, getCurrentApp() { if (!currentAppId) { return; } return this.getMenu(currentAppId); }, getMenuAsTree(menuID) { const menu = this.getMenu(menuID); if (!menu.childrenTree) { menu.childrenTree = menu.children.map((mid) => this.getMenuAsTree(mid)); } return menu; }, async selectMenu(menu) { menu = typeof menu === "number" ? this.getMenu(menu) : menu; if (!menu.actionID) { return; } await env.services.action.doAction(menu.actionID, { clearBreadcrumbs: true }); this.setCurrentMenu(menu); }, setCurrentMenu(menu) { menu = typeof menu === "number" ? this.getMenu(menu) : menu; if (menu && menu.appID !== currentAppId) { currentAppId = menu.appID; env.bus.trigger("MENUS:APP-CHANGED"); // FIXME: lock API: maybe do something like // pushState({menu_id: ...}, { lock: true}); ? env.services.router.pushState({ menu_id: menu.id }, { lock: true }); } }, async reload() { if (fetchLoadMenus) { menusData = await fetchLoadMenus(true); env.bus.trigger("MENUS:APP-CHANGED"); } }, }; }
export const menuService = { dependencies: ["action", "router"], async start(env) { const fetchLoadMenus = makeFetchLoadMenus(); const menusData = await fetchLoadMenus(); return makeMenus(env, menusData, fetchLoadMenus); }, };
registry.category("services").add("menu", menuService);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 重点是这个函数:
async selectMenu(menu) {
menu = typeof menu === "number" ? this.getMenu(menu) : menu;
if (!menu.actionID) {
return;
}
await env.services.action.doAction(menu.actionID, { clearBreadcrumbs: true });
this.setCurrentMenu(menu);
1 2 3 4 5 6 7 它调用了action的doAction。
6、actions\action_service.js 这里只截取了该文件的一部分,根据不同的action类型,进行不同的处理。
/**
* Main entry point of a 'doAction' request. Loads the action and executes it.
*
* @param {ActionRequest} actionRequest
* @param {ActionOptions} options
* @returns {Promise<number | undefined | void>}
*/
async function doAction(actionRequest, options = {}) {
const actionProm = _loadAction(actionRequest, options.additionalContext);
let action = await keepLast.add(actionProm);
action = _preprocessAction(action, options.additionalContext);
options.clearBreadcrumbs = action.target === "main" || options.clearBreadcrumbs;
switch (action.type) {
case "ir.actions.act_url":
return _executeActURLAction(action, options);
case "ir.actions.act_window":
if (action.target !== "new") {
const canProceed = await clearUncommittedChanges(env);
if (!canProceed) {
return new Promise(() => {});
}
}
return _executeActWindowAction(action, options);
case "ir.actions.act_window_close":
return _executeCloseAction({ onClose: options.onClose, onCloseInfo: action.infos });
case "ir.actions.client":
return _executeClientAction(action, options);
case "ir.actions.report":
return _executeReportAction(action, options);
case "ir.actions.server":
return _executeServerAction(action, options);
default: {
const handler = actionHandlersRegistry.get(action.type, null);
if (handler !== null) {
return handler({ env, action, options });
}
throw new Error(
The ActionManager service can't handle actions of type ${action.type}
);
}
}
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 action是一个Component, 这个函数会返回一个action然后塞到页面上去。
我们重点关注ir.actions.act_window
case "ir.actions.act_window":
if (action.target !== "new") {
const canProceed = await clearUncommittedChanges(env);
if (!canProceed) {
return new Promise(() => {});
}
}
return _executeActWindowAction(action, options);
1 2 3 4 5 6 7 8 _executeActWindowAction 函数
.... 省略1000字 return _updateUI(controller, updateUIOptions); 1 2 3 最后调用了_updateUI,这个函数会动态生成一个Component,最后通过总线发送ACTION_MANAGER:UPDATE 消息
controller.__info__ = {
id: ++id,
Component: ControllerComponent,
componentProps: controller.props,
};
env.bus.trigger("ACTION_MANAGER:UPDATE", controller.__info__);
return Promise.all([currentActionProm, closingProm]).then((r) => r[0]);
1 2 3 4 5 6 7 我们继续看是谁接收了这个消息
7、action_container.js action_container 接收了ACTION_MANAGER:UPDATE消息,并做了处理,调用了render函数 ,而ActionContainer组件是webClient的一个子组件,
这样,整个逻辑就自洽了。
addons\web\static\src\webclient\actions\action_container.js
/** @odoo-module **/
import { ActionDialog } from "./action_dialog";
import { Component, xml, onWillDestroy } from "@odoo/owl";
// -----------------------------------------------------------------------------
// ActionContainer (Component)
// -----------------------------------------------------------------------------
export class ActionContainer extends Component {
setup() {
this.info = {};
this.onActionManagerUpdate = ({ detail: info }) => {
this.info = info;
this.render();
};
this.env.bus.addEventListener("ACTION_MANAGER:UPDATE", this.onActionManagerUpdate);
onWillDestroy(() => {
this.env.bus.removeEventListener("ACTION_MANAGER:UPDATE", this.onActionManagerUpdate);
});
}
}
ActionContainer.components = { ActionDialog };
ActionContainer.template = xml <t t-name="web.ActionContainer"> <div class="o_action_manager"> <t t-if="info.Component" t-component="info.Component" className="'o_action'" t-props="info.componentProps" t-key="info.id"/> </div> </t>
;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 上面整个过程, 就完成了客户端的启动,以及菜单=》动作=》页面渲染的循环。 当然里面还有很多细节的东西值得研究,不过大概的框架就是这样了。