odoo16前端框架源码阅读——启动、菜单、动作

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

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

1、定义了odoo.info

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

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

相关推荐
熊的猫2 小时前
DOM 规范 — MutationObserver 接口
前端·javascript·chrome·webpack·前端框架·node.js·ecmascript
mez_Blog2 小时前
Vue之插槽(slot)
前端·javascript·vue.js·前端框架·插槽
放逐者-保持本心,方可放逐2 小时前
vue3 中那些常用 靠copy 的内置函数
前端·javascript·vue.js·前端框架
放逐者-保持本心,方可放逐8 小时前
微信小程序=》基础=》常见问题=》性能总结
前端·微信小程序·小程序·前端框架
Mr.E514 小时前
odoo17 owl 前端 顶部导航栏右侧添加自定义按钮
前端·odoo·odoo17·owl
Sapphire~16 小时前
odoo-040 odoo17前端的js方法调用后端py方法action报错
前端·javascript·odoo
多客软件佳佳19 小时前
校园交友系统的设计与实现(开源版+三端交付+搭建+售后)
小程序·前端框架·uni-app·开源·php·交友
豆华20 小时前
React 中 为什么多个 JSX 标签需要被一个父元素包裹?
前端·react.js·前端框架
练习两年半的工程师20 小时前
使用React和Vite构建一个AirBnb Experiences克隆网站
前端·react.js·前端框架
林太白20 小时前
❤React-JSX语法认识和使用
前端·react.js·前端框架