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

news/2024/11/20 8:28:58/

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 environmentconst env = makeEnv();await startServices(env);// start web clientawait 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 thisodoo.__WOWL_DEBUG__ = { root };odoo.isReady = true;// Update Faviconsconst 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 grepuseBus(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 menuconst 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 actionconst 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 actionthis.menuService.setCurrentMenu(menuId);}if (!stateLoaded) {// If no action => falls back to the default appawait this._loadDefaultApp();}}_loadDefaultApp() {// Selects the first root menu if anyconst 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 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,现在来看看这个文件

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

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

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


http://www.ppmy.cn/news/1211517.html

相关文章

【Linux】Centos7 shell实现MySQL5.7 tar 一键安装

&#x1f984; 个人主页——&#x1f390;个人主页 &#x1f390;✨&#x1f341; &#x1fa81;&#x1f341;&#x1fa81;&#x1f341;&#x1fa81;&#x1f341;&#x1fa81;&#x1f341; 感谢点赞和关注 &#xff0c;每天进步一点点&#xff01;加油&#xff01;&…

利用 Google Artifact Repository 构建docker 镜像仓库

参考了google 官方文档 https://cloud.google.com/artifact-registry/docs/docker/store-docker-container-images 首先 enable GAR api gcloud services enable artifactregistry.googleapis.com gcloud services list | grep -i artifact artifactregistry.googleapis.com …

在使用ubuntu18.04的时候使用阿里源或者清华源后安装mysql5.7时出现dpkg提示的错误信息

在使用ubuntu18.04的时候使用阿里源或者清华源后安装mysql5.7时出现dpkg提示的错误信息 经过排查发现该问题可能跟本地库依赖文件导致mysql安装不上。 清除刚下载保留的mysql&#xff0c;然后删除、清除在更新本地依赖库。 sudo apt purge mysql* sudo apt autoremove sudo …

[PHP]Kodexplorer可道云 v4.47

KodExplorer可道云&#xff0c;原名芒果云&#xff0c;是基于Web技术的私有云和在线文件管理系统&#xff0c;由上海岱牧网络有限公司开发&#xff0c;发布于2012年6月。致力于为用户提供安全可控、可靠易用、高扩展性的私有云解决方案。 用户只需通过简单环境搭建&#xff0c;…

【chat】 1:Ubuntu 20.04.3 编译安装moduo master分支

muduo 基于reactor反应堆模型的多线程C++网络库大佬的官方仓库有cpp17分支看了下cmakelist文件里面还是要依赖不少库,比如boost protobuf而且cpp17 似乎 是2021年的master 是2022更新的那么还是选择master吧。ubuntu版本 Ubuntu 20.04.3 root@k8s-master-2K4G:~# uname -a Lin…

CSS特效006:绘制不断跳动的心形

css实战中&#xff0c;怎么绘制不断跳动的心形呢&#xff1f; 绘图的时候主要用到了transform: rotate(-45deg); transform-origin: 0 100%; transform: rotate(45deg); transform-origin: 100% 100%; 动画使用keyframes 时间上为infinite。 效果图 源代码 /* * Author: 大剑…

ElasticSearch的集群、节点、索引、分片和副本

Elasticsearch是面向文档型数据库&#xff0c;一条数据在这里就是一个文档。为了方便大家理解&#xff0c;我们将Elasticsearch里存储文档数据和关系型数据库MySQL存储数据的概念进行一个类比 ES里的Index可以看做一个库&#xff0c;而Types相当于表&#xff0c;Documents则相当…

Java 幼儿园(20231111)读取 json 文件

1、功能场景 &#xff08;1&#xff09;多人合作开发一个功能模块时&#xff0c;需要调用外部接口 &#xff08;2&#xff09;对方接口的开发工作还没有完成&#xff0c;只能提供一个返回值的示例文件 json 文件。 &#xff08;3&#xff09;返回的 json 数据多达几百个字段。 …