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

Flask RESTful 示例
目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题: 下面创建一个简单的Flask RESTful API示例。首先,我们需要创建环境,安装必要的依赖,然后…...
rknn优化教程(二)
文章目录 1. 前述2. 三方库的封装2.1 xrepo中的库2.2 xrepo之外的库2.2.1 opencv2.2.2 rknnrt2.2.3 spdlog 3. rknn_engine库 1. 前述 OK,开始写第二篇的内容了。这篇博客主要能写一下: 如何给一些三方库按照xmake方式进行封装,供调用如何按…...

Swift 协议扩展精进之路:解决 CoreData 托管实体子类的类型不匹配问题(下)
概述 在 Swift 开发语言中,各位秃头小码农们可以充分利用语法本身所带来的便利去劈荆斩棘。我们还可以恣意利用泛型、协议关联类型和协议扩展来进一步简化和优化我们复杂的代码需求。 不过,在涉及到多个子类派生于基类进行多态模拟的场景下,…...

uniapp微信小程序视频实时流+pc端预览方案
方案类型技术实现是否免费优点缺点适用场景延迟范围开发复杂度WebSocket图片帧定时拍照Base64传输✅ 完全免费无需服务器 纯前端实现高延迟高流量 帧率极低个人demo测试 超低频监控500ms-2s⭐⭐RTMP推流TRTC/即构SDK推流❌ 付费方案 (部分有免费额度&#x…...

RNN避坑指南:从数学推导到LSTM/GRU工业级部署实战流程
本文较长,建议点赞收藏,以免遗失。更多AI大模型应用开发学习视频及资料,尽在聚客AI学院。 本文全面剖析RNN核心原理,深入讲解梯度消失/爆炸问题,并通过LSTM/GRU结构实现解决方案,提供时间序列预测和文本生成…...

搭建DNS域名解析服务器(正向解析资源文件)
正向解析资源文件 1)准备工作 服务端及客户端都关闭安全软件 [rootlocalhost ~]# systemctl stop firewalld [rootlocalhost ~]# setenforce 0 2)服务端安装软件:bind 1.配置yum源 [rootlocalhost ~]# cat /etc/yum.repos.d/base.repo [Base…...
tomcat指定使用的jdk版本
说明 有时候需要对tomcat配置指定的jdk版本号,此时,我们可以通过以下方式进行配置 设置方式 找到tomcat的bin目录中的setclasspath.bat。如果是linux系统则是setclasspath.sh set JAVA_HOMEC:\Program Files\Java\jdk8 set JRE_HOMEC:\Program Files…...

stm32wle5 lpuart DMA数据不接收
配置波特率9600时,需要使用外部低速晶振...
前端调试HTTP状态码
1xx(信息类状态码) 这类状态码表示临时响应,需要客户端继续处理请求。 100 Continue 服务器已收到请求的初始部分,客户端应继续发送剩余部分。 2xx(成功类状态码) 表示请求已成功被服务器接收、理解并处…...

C++--string的模拟实现
一,引言 string的模拟实现是只对string对象中给的主要功能经行模拟实现,其目的是加强对string的底层了解,以便于在以后的学习或者工作中更加熟练的使用string。本文中的代码仅供参考并不唯一。 二,默认成员函数 string主要有三个成员变量,…...