odoo16前端框架源码阅读——启动、菜单、动作
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>`;
上面整个过程, 就完成了客户端的启动,以及菜单=》动作=》页面渲染的循环。 当然里面还有很多细节的东西值得研究,不过大概的框架就是这样了。
相关文章:
odoo16前端框架源码阅读——启动、菜单、动作
odoo16前端框架源码阅读——启动、菜单、动作 目录:addons/web/static/src 1、main.js odoo实际上是一个单页应用,从名字看,这是前端的入口文件,文件内容也很简单。 /** odoo-module **/import { startWebClient } from "…...
C/C++(a/b)*c的值 2021年6月电子学会青少年软件编程(C/C++)等级考试一级真题答案解析
目录 C/C(a/b)*c的值 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、程序说明 五、运行结果 六、考点分析 C/C(a/b)*c的值 2021年6月 C/C编程等级考试一级编程题 一、题目要求 1、编程实现 给定整数a、b、c,计算(a / b)*c的值&…...
CIFAR-100数据集的加载和预处理教程
一、CIFAR-100数据集介绍 CIFAR-100(Canadian Institute for Advanced Research - 100 classes)是一个经典的图像分类数据集,用于计算机视觉领域的研究和算法测试。它是CIFAR-10数据集的扩展版本,包含了更多的类别,用…...
C#,数值计算——函数计算,Eulsum的计算方法与源程序
1 文本格式 using System; namespace Legalsoft.Truffer { public class Eulsum { private double[] wksp { get; set; } private int n { get; set; } private int ncv { get; set; } public bool cnvgd { get; set; } pri…...
ChatGLM3 langchain_demo 代码解析
ChatGLM3 langchain_demo 代码解析 0. 背景1. 项目代码结构2. 代码解析2-1. utils.py2-2. ChatGLM3.py2-3. Tool/Calculator.py2-4. Tool/Weather.py2-5. main.py 0. 背景 学习 ChatGLM3 的项目内容,过程中使用 AI 代码工具,对代码进行解释,…...
asp.net学院网上报销系统VS开发sqlserver数据库web结构c#编程Microsoft Visual Studio
一、源码特点 asp.net学院网上报销系统是一套完善的web设计管理系统,系统具有完整的源代码和数据库,系统主要采用B/S模式开发。开发环境为vs2010,数据库为sqlserver2008,使用c#语言 开发 asp.net学院网上报销系统 应用技术…...
ElasticSearch知识点
什么是ElasticSearch ElasticSearch: 智能搜索,分布式的搜索引擎,是ELK的一个非常完善的产品,ELK代表的是: E就是ElasticSearch,L就是Logstach,K就是kibana Elasticsearch是一个建立在全文搜索引擎 Apache Lucene基础…...
STM32 GPIO
STM32 GPIO GPIO简介 GPIO(General Purpose Input Output)通用输入输出口,也就是我们俗称的IO口 根据使用场景,可配置为8种输入输出模式 引脚电平:0V~3.3V,部分引脚可容忍5V 数据0就是低电平,…...
Electron 开发页面应用
简介 Electron集成了包括chromium(理解为具备chrom浏览器的工具),nodejs,native apis chromium:支持最新特性的浏览器。 nodejs:js运行时,可实现文件读写等。 native apis :提供…...
CSDN写博文的128天
起因 为什么要写博文? 写博文是因为当我还是编程小白时,我那会啥也不懂,不懂函数调用,不懂指针,更不懂结构体,别更说Linux,平时不会也没有可以问的人,也幸好有CSDN,遇到…...
Linux学习教程(第二章 Linux系统安装)1
第二章 Linux系统安装 学习 Linux,首先要学会搭建 Linux 系统环境,也就是学会在你的电脑上安装 Linux 系统。 很多初学者对 Linux 望而生畏,多数是因为对 Linux 系统安装的恐惧,害怕破坏电脑本身的系统,害怕硬盘数据…...
vue2手机项目如何使用蓝牙功能
要在Vue2手机项目中使用蓝牙功能,你需要先了解基本的蓝牙知识和API。以下是一些基本的步骤: 确认你的手机设备支持蓝牙功能。在Vue2项目中安装蓝牙插件或库,例如vue-bluetooth或vue-bluetooth-manager。你可以通过npm安装它们。在Vue2项目中…...
魔兽服务器学习-笔记1
文章目录 一、环境准备1)依赖安装2)源码下载和编译 二、生成数据信息1)地图数据信息(客户端信息)2)数据库信息 三、启动服务器四、日志模块五、数据库模块六、场景模块1)地图管理2)A…...
代码随想录day60|84.柱状图中最大的矩形
84.柱状图中最大的矩形(找到右边第一个更小的元素) 1、对于每一个柱子:找到左边第一个比他矮的,再找到右边第一个比他矮的。 2、首尾加0: 为什么要在末尾加0:否则如果原数组就是单调递增的话,就…...
常见面试题-分布式锁
Redisson 分布式锁?在项目中哪里使用?多久会进行释放?如何加强一个分布式锁? 答: 什么时候需要使用分布式锁呢? 在分布式的场景下,使用 Java 的单机锁并不可以保证多个应用的同时操作共享资源…...
vue开发 安装一些工具
下载 node.js环境 nodeJs 官网 命令行输入 node -v 和 npm -v 出现版本号 代表nodejs 安装成功选择安装pnpm npm install -g pnpmpnpm -v 出现版本号即成功安装安装 scss vue3 组件库 Element Plus Element 官网 安装 pnpm install Element-Plus --save第一次使用开发v…...
Vue.js 组件 - 自定义事件
Vue.js 组件 - 自定义事件 父组件是使用 props 传递数据给子组件,但如果子组件要把数据传递回去,就需要使用自定义事件! 我们可以使用 v-on 绑定自定义事件, 每个 Vue 实例都实现了事件接口(Events interface),即: …...
深度学习 python opencv 火焰检测识别 计算机竞赛
文章目录 0 前言1 基于YOLO的火焰检测与识别2 课题背景3 卷积神经网络3.1 卷积层3.2 池化层3.3 激活函数:3.4 全连接层3.5 使用tensorflow中keras模块实现卷积神经网络 4 YOLOV54.1 网络架构图4.2 输入端4.3 基准网络4.4 Neck网络4.5 Head输出层 5 数据集准备5.1 数…...
PHP中传值与引用的区别
在PHP中,变量的传递方式主要分为传值和传引用两种。这两种方式在操作中有一些重要的区别,影响着变量在函数调用或赋值操作中的表现。下面详细解释一下这两种传递方式的区别。 传值(By Value) 传值是指将变量的值复制一份传递给函…...
Go常见数据结构的实现原理——map
(一)基础操作 版本:Go SDK 1.20.6 1、初始化 map分别支持字面量初始化和内置函数make()初始化。 字面量初始化: m : map[string] int {"apple": 2,"banana": 3,}使用内置函数make()初始化: m …...
零门槛NAS搭建:WinNAS如何让普通电脑秒变私有云?
一、核心优势:专为Windows用户设计的极简NAS WinNAS由深圳耘想存储科技开发,是一款收费低廉但功能全面的Windows NAS工具,主打“无学习成本部署” 。与其他NAS软件相比,其优势在于: 无需硬件改造:将任意W…...
Zustand 状态管理库:极简而强大的解决方案
Zustand 是一个轻量级、快速和可扩展的状态管理库,特别适合 React 应用。它以简洁的 API 和高效的性能解决了 Redux 等状态管理方案中的繁琐问题。 核心优势对比 基本使用指南 1. 创建 Store // store.js import create from zustandconst useStore create((set)…...
高频面试之3Zookeeper
高频面试之3Zookeeper 文章目录 高频面试之3Zookeeper3.1 常用命令3.2 选举机制3.3 Zookeeper符合法则中哪两个?3.4 Zookeeper脑裂3.5 Zookeeper用来干嘛了 3.1 常用命令 ls、get、create、delete、deleteall3.2 选举机制 半数机制(过半机制࿰…...
【Java_EE】Spring MVC
目录 Spring Web MVC 编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 编辑参数重命名 RequestParam 编辑编辑传递集合 RequestParam 传递JSON数据 编辑RequestBody …...
Reasoning over Uncertain Text by Generative Large Language Models
https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829 1. 概述 文本中的不确定性在许多语境中传达,从日常对话到特定领域的文档(例如医学文档)(Heritage 2013;Landmark、Gulbrandsen 和 Svenevei…...
蓝桥杯 冶炼金属
原题目链接 🔧 冶炼金属转换率推测题解 📜 原题描述 小蓝有一个神奇的炉子用于将普通金属 O O O 冶炼成为一种特殊金属 X X X。这个炉子有一个属性叫转换率 V V V,是一个正整数,表示每 V V V 个普通金属 O O O 可以冶炼出 …...
佰力博科技与您探讨热释电测量的几种方法
热释电的测量主要涉及热释电系数的测定,这是表征热释电材料性能的重要参数。热释电系数的测量方法主要包括静态法、动态法和积分电荷法。其中,积分电荷法最为常用,其原理是通过测量在电容器上积累的热释电电荷,从而确定热释电系数…...
GitFlow 工作模式(详解)
今天再学项目的过程中遇到使用gitflow模式管理代码,因此进行学习并且发布关于gitflow的一些思考 Git与GitFlow模式 我们在写代码的时候通常会进行网上保存,无论是github还是gittee,都是一种基于git去保存代码的形式,这样保存代码…...
[ACTF2020 新生赛]Include 1(php://filter伪协议)
题目 做法 启动靶机,点进去 点进去 查看URL,有 ?fileflag.php说明存在文件包含,原理是php://filter 协议 当它与包含函数结合时,php://filter流会被当作php文件执行。 用php://filter加编码,能让PHP把文件内容…...
TSN交换机正在重构工业网络,PROFINET和EtherCAT会被取代吗?
在工业自动化持续演进的今天,通信网络的角色正变得愈发关键。 2025年6月6日,为期三天的华南国际工业博览会在深圳国际会展中心(宝安)圆满落幕。作为国内工业通信领域的技术型企业,光路科技(Fiberroad&…...
