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);

关键的是最后一行代码 ,调用了startWebClient函数,启动了一个WebClient。 非常简单,而且注释也说明了,企业版可以启动专有的webclient。

2、start.js

这个模块中只有一个函数startWebClient,注释也说明了,它的作用就是启动一个webclient,而且企业版和社区版都会执行这个函数,只是webclient不同而已。

这个文件大概干了这么几件事:

1、定义了odoo.info

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;

}

}

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,则返回第一个应用。

/** @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 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: 这其实是不可见的,包含了通知之类的东东,在一定条件下可见

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);

重点是这个函数:

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类型,进行不同的处理。

/**

* Main entry point of a 'doAction' request. Loads the action and executes it.

*

* @param {ActionRequest} actionRequest

* @param {ActionOptions} options

* @returns {Promise}

*/

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

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 消息

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

/** @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`

`;

上面整个过程, 就完成了客户端的启动,以及菜单=》动作=》页面渲染的循环。 当然里面还有很多细节的东西值得研究,不过大概的框架就是这样了。

参考阅读

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: