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 …...

JavaSec-RCE
简介 RCE(Remote Code Execution),可以分为:命令注入(Command Injection)、代码注入(Code Injection) 代码注入 1.漏洞场景:Groovy代码注入 Groovy是一种基于JVM的动态语言,语法简洁,支持闭包、动态类型和Java互操作性,…...

C++初阶-list的底层
目录 1.std::list实现的所有代码 2.list的简单介绍 2.1实现list的类 2.2_list_iterator的实现 2.2.1_list_iterator实现的原因和好处 2.2.2_list_iterator实现 2.3_list_node的实现 2.3.1. 避免递归的模板依赖 2.3.2. 内存布局一致性 2.3.3. 类型安全的替代方案 2.3.…...

Xshell远程连接Kali(默认 | 私钥)Note版
前言:xshell远程连接,私钥连接和常规默认连接 任务一 开启ssh服务 service ssh status //查看ssh服务状态 service ssh start //开启ssh服务 update-rc.d ssh enable //开启自启动ssh服务 任务二 修改配置文件 vi /etc/ssh/ssh_config //第一…...
DockerHub与私有镜像仓库在容器化中的应用与管理
哈喽,大家好,我是左手python! Docker Hub的应用与管理 Docker Hub的基本概念与使用方法 Docker Hub是Docker官方提供的一个公共镜像仓库,用户可以在其中找到各种操作系统、软件和应用的镜像。开发者可以通过Docker Hub轻松获取所…...

srs linux
下载编译运行 git clone https:///ossrs/srs.git ./configure --h265on make 编译完成后即可启动SRS # 启动 ./objs/srs -c conf/srs.conf # 查看日志 tail -n 30 -f ./objs/srs.log 开放端口 默认RTMP接收推流端口是1935,SRS管理页面端口是8080,可…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院查看报告小程序
一、开发环境准备 工具安装: 下载安装DevEco Studio 4.0(支持HarmonyOS 5)配置HarmonyOS SDK 5.0确保Node.js版本≥14 项目初始化: ohpm init harmony/hospital-report-app 二、核心功能模块实现 1. 报告列表…...

DBAPI如何优雅的获取单条数据
API如何优雅的获取单条数据 案例一 对于查询类API,查询的是单条数据,比如根据主键ID查询用户信息,sql如下: select id, name, age from user where id #{id}API默认返回的数据格式是多条的,如下: {&qu…...

【Zephyr 系列 10】实战项目:打造一个蓝牙传感器终端 + 网关系统(完整架构与全栈实现)
🧠关键词:Zephyr、BLE、终端、网关、广播、连接、传感器、数据采集、低功耗、系统集成 📌目标读者:希望基于 Zephyr 构建 BLE 系统架构、实现终端与网关协作、具备产品交付能力的开发者 📊篇幅字数:约 5200 字 ✨ 项目总览 在物联网实际项目中,**“终端 + 网关”**是…...
工业自动化时代的精准装配革新:迁移科技3D视觉系统如何重塑机器人定位装配
AI3D视觉的工业赋能者 迁移科技成立于2017年,作为行业领先的3D工业相机及视觉系统供应商,累计完成数亿元融资。其核心技术覆盖硬件设计、算法优化及软件集成,通过稳定、易用、高回报的AI3D视觉系统,为汽车、新能源、金属制造等行…...

关键领域软件测试的突围之路:如何破解安全与效率的平衡难题
在数字化浪潮席卷全球的今天,软件系统已成为国家关键领域的核心战斗力。不同于普通商业软件,这些承载着国家安全使命的软件系统面临着前所未有的质量挑战——如何在确保绝对安全的前提下,实现高效测试与快速迭代?这一命题正考验着…...