odoo16前端框架源码阅读------启动、菜单、动作
目录:addons/web/static/src
1、main.js
odoo实际上是一个单页应用,从名字看,这是前端的入口文件,文件内容也很简单。
js
/** @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);
关键的是最后一行代码 ,调用了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的做法类似。
js
/** @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;
}
}
3、WebClient
很明显,webclient是一个owl组件,这就是我们看到的odoo的主界面,值得好好分析。
这里的重点就是:
在onMounted钩子中调用了 this.loadRouterState();
而这个函数呢,一开始就获取了两个变量:
let stateLoaded = await this.actionService.loadState();
let menuId = Number(this.router.current.hash.menu_id || 0);
后面就是根据这两个变量的值的不同的组合进行处理。 如果menuId 为false,则返回第一个应用。
js
/** @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";
4、web.WebClient
webclient的模板文件,简单的狠,用了三个组件
NavBar: 顶部的导航栏
ActionContainer: 除了导航栏之外的其他可见的部分
MainComponentsContainer: 这其实是不可见的,包含了通知之类的东东,在一定条件下可见
xml
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.WebClient" owl="1">
<t t-if="!state.fullscreen">
<NavBar/>
</t>
<ActionContainer/>
<MainComponentsContainer/>
</t>
</templates>
5、menus\menu_service.js
Webclient中用到了menuservice,现在来看看这个文件
js
/** @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);
重点是这个函数:
js
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);
它调用了action的doAction。
6、actions\action_service.js
这里只截取了该文件的一部分,根据不同的action类型,进行不同的处理。
js
/**
* 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}`
);
}
}
}
action是一个Component, 这个函数会返回一个action然后塞到页面上去。
我们重点关注ir.actions.act_window
js
case "ir.actions.act_window":
if (action.target !== "new") {
const canProceed = await clearUncommittedChanges(env);
if (!canProceed) {
return new Promise(() => {});
}
}
return _executeActWindowAction(action, options);
_executeActWindowAction 函数
....
省略1000字
return _updateUI(controller, updateUIOptions);
最后调用了_updateUI,这个函数会动态生成一个Component,最后通过总线发送ACTION_MANAGER:UPDATE 消息
js
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]);
我们继续看是谁接收了这个消息
7、action_container.js
action_container 接收了ACTION_MANAGER:UPDATE消息,并做了处理,调用了render函数 ,而ActionContainer组件是webClient的一个子组件,
这样,整个逻辑就自洽了。
addons\web\static\src\webclient\actions\action_container.js
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>`;
上面整个过程, 就完成了客户端的启动,以及菜单=》动作=》页面渲染的循环。 当然里面还有很多细节的东西值得研究,不过大概的框架就是这样了。