当前位置: 首页 > news >正文

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

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

相关文章:

  • 【Delphi】 各个平台使用 ntfy 效果说明
  • laravel8-rabbitmq消息队列-实时监听跨服务器消息
  • 使用venv 创建虚拟环境
  • 如何安装Node.js? 创建Vue脚手架
  • 【Springboot】基于注解式开发Springboot-Vue3整合Mybatis-plus实现分页查询(二)——前端el-pagination实现
  • 深度学习_11_softmax_图片识别代码原理解析
  • helm 常用命令搜集 —— 筑梦之路
  • Nginx(五)
  • SpringBoot 使用EasyExcel 导出Excel报表(单元格合并)
  • linux入门---自旋锁和读写锁
  • Python 使用tkinter复刻Windows记事本UI和菜单功能(二)
  • MCU平台使用SPI-DirectC实现FPGA在线升级
  • 人工智能基础——图像认知与OpenCV
  • 在ant构建脚本中调用maven的命令
  • Git 进阶使用
  • 【跃迁之路】【519天】程序员高效学习方法论探索系列(实验阶段276-2018.07.09)...
  • 77. Combinations
  • 8年软件测试工程师感悟——写给还在迷茫中的朋友
  • Angular 响应式表单 基础例子
  • Java小白进阶笔记(3)-初级面向对象
  • laravel with 查询列表限制条数
  • quasar-framework cnodejs社区
  • Spring Cloud Feign的两种使用姿势
  • SQL 难点解决:记录的引用
  • weex踩坑之旅第一弹 ~ 搭建具有入口文件的weex脚手架
  • 高度不固定时垂直居中
  • 基于游标的分页接口实现
  • 快速构建spring-cloud+sleuth+rabbit+ zipkin+es+kibana+grafana日志跟踪平台
  • 聊聊sentinel的DegradeSlot
  • 模型微调
  • 区块链将重新定义世界
  • #DBA杂记1
  • (zz)子曾经曰过:先有司,赦小过,举贤才
  • (附源码)ssm跨平台教学系统 毕业设计 280843
  • (黑马出品_高级篇_01)SpringCloud+RabbitMQ+Docker+Redis+搜索+分布式
  • (免费领源码)Java#ssm#MySQL 创意商城03663-计算机毕业设计项目选题推荐
  • (十)【Jmeter】线程(Threads(Users))之jp@gc - Stepping Thread Group (deprecated)
  • (已解决)什么是vue导航守卫
  • (原+转)Ubuntu16.04软件中心闪退及wifi消失
  • (转)Android中使用ormlite实现持久化(一)--HelloOrmLite
  • (转载)OpenStack Hacker养成指南
  • (轉貼) 資訊相關科系畢業的學生,未來會是什麼樣子?(Misc)
  • ******IT公司面试题汇总+优秀技术博客汇总
  • .desktop 桌面快捷_Linux桌面环境那么多,这几款优秀的任你选
  • .net 4.0 A potentially dangerous Request.Form value was detected from the client 的解决方案
  • .net websocket 获取http登录的用户_如何解密浏览器的登录密码?获取浏览器内用户信息?...
  • .NET 中使用 Mutex 进行跨越进程边界的同步
  • .NET6 命令行启动及发布单个Exe文件
  • .Net的C#语言取月份数值对应的MonthName值
  • .Net组件程序设计之线程、并发管理(一)
  • @entity 不限字节长度的类型_一文读懂Redis常见对象类型的底层数据结构
  • @RequestMapping-占位符映射
  • @Responsebody与@RequestBody
  • [20171106]配置客户端连接注意.txt
  • [ajaxupload] - 上传文件同时附件参数值