odoo16前端框架源码阅读——启动、菜单、动作
  lh6O4DgR0ZQ8 2023年11月19日 18 0

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不同而已。

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

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

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

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

上一篇: shell 函数 下一篇: nodejs相关
  1. 分享:
最后一次编辑于 2023年11月19日 0

暂无评论

推荐阅读
  zLxnEsMLk4BL   2023年11月19日   15   0   0 变量名字符串bc
  lh6O4DgR0ZQ8   2023年11月22日   16   0   0 Memory字段sed
  zLxnEsMLk4BL   2023年11月19日   16   0   0 变量名字符串bclinux
  zLxnEsMLk4BL   2023年11月19日   12   0   0 赋值字符串bc
lh6O4DgR0ZQ8