如何优雅的实现跨应用的代码共享

news2024/10/6 1:44:10

在 2020 年上半年,Webpack 发布了一项非常激动人心的特性:Module Federation(译为模块联邦),这个特性一经推出就获得了业界的广泛关注,甚至被称为前端构建领域的Game Changer。实际上,这项技术确实很好地解决了多应用模块复用的问题,相比之前的各种解决方案,它的解决方式更加优雅和灵活。但从另一个角度来说,Module Federation 代表的是一种通用的解决思路,并不局限于某一个特定的构建工具,因此,在 Vite 中我们同样可以实现这个特性,并且社区已经有了比较成熟的解决方案。

一、模块共享之痛

对于一个互联网产品来说,一般会有不同的细分应用,比如腾讯文档可以分为word、excel、ppt等等品类,抖音 PC 站点可以分为短视频站点、直播站点、搜索站点等子站点,而每个子站又彼此独立,可能由不同的开发团队进行单独的开发和维护,看似没有什么问题,但实际上会经常遇到一些模块共享的问题,也就是说不同应用中总会有一些共享的代码,比如公共组件、公共工具函数、公共第三方依赖等等。对于这些共享的代码,除了通过简单的复制粘贴,还有没有更好的复用手段呢?

下面介绍几种常见的代码复用手段:

1.1 发布 npm 包

发布 npm 包是一种常见的复用模块的做法,我们可以将一些公用的代码封装为一个 npm 包,然后在其他项目中引用这个npm包。具体的发布更新流程如下:

  1. 公共库 lib1 改动,发布到 npm;
  2. 所有的应用安装新的依赖,并进行联调。

image.png

封装 npm 包可以解决模块复用的问题,但它本身又引入了新的问题:

  • 开发效率问题。每次改动都需要发版,并所有相关的应用安装新依赖,流程比较复杂。
  • 项目构建问题。引入了公共库之后,公共库的代码都需要打包到项目最后的产物后,导致产物体积偏大,构建速度相对较慢。

因此,这种方案并不能作为最终方案,只是暂时用来解决问题的无奈之举。

1.2 Git Submodule

通过 git submodule 的方式,我们可以将代码封装成一个公共的 Git 仓库,然后复用到不同的应用中,但也需要经历如下的步骤:

  1. 公共库 lib1 改动,提交到 Git 远程仓库;
  2. 所有的应用通过git submodule命令更新子仓库代码,并进行联调。

可以看到,整体的流程其实跟发 npm 包相差无几,仍然存在 npm 包方案所存在的各种问题。

1.3 依赖外部化+ CDN 引入

所谓依赖外部化(external),指的是对于某些第三方依赖我们并不需要让其参与构建,而是使用某一份公用的代码。按照这个思路,我们可以在构建引擎中对某些依赖声明external,然后在 HTML 中加入依赖的 CDN 地址,比如:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="root"></div>
    <!-- 从 CDN 上引入第三方依赖的代码 -->
    <script src="https://cdn.jsdelivr.net/npm/react@17.0.2/index.min.js"><script>
    <script src="https://cdn.jsdelivr.net/npm/react-dom@17.0.2/index.min.js"><script>
  </body>
</html>

如上面的例子所示,我们可以对react和react-dom使用 CDN 的方式引入,一般使用UMD格式产物,这样不同的项目间就可以通过window.React来使用同一份依赖的代码了,从而达到模块复用的效果。不过,这种方案也有一定的局限性:

  • 兼容性问题。并不是所有的依赖都有 UMD 格式的产物,因此这种方案不能覆盖所有的第三方 npm 包。
  • 依赖顺序问题。我们通常需要考虑间接依赖的问题,如对于 antd 组件库,它本身也依赖了 react 和 moment,那么react和moment 也需要 external,并且在 HTML 中引用这些包,同时也要严格保证引用的顺序,比如说moment如果放在了antd后面,代码可能无法运行。而第三方包背后的间接依赖数量一般很庞大,如果逐个处理,对于开发者来说简直就是噩梦。
  • 产物体积问题。由于依赖包被声明external之后,应用在引用其 CDN 地址时,会全量引用依赖的代码,这种情况下就没有办法通过 Tree Shaking 来去除无用代码了,会导致应用的性能有所下降。

1.4 Monorepo

作为一种新的项目管理方式,Monorepo 也可以很好地解决模块复用的问题。在 Monorepo 架构下,多个项目可以放在同一个 Git 仓库中,各个互相依赖的子项目通过软链的方式进行调试,代码复用显得非常方便,如果有依赖的代码变动,那么用到这个依赖的项目当中会立马感知到。

image.png

不得不承认,对于应用间模块复用的问题,Monorepo 是一种非常优秀的解决方案,但与此同时,它也有一些使用上的限制。

  • 所有的应用代码必须放到同一个仓库。如果是旧有项目,并且每个应用使用一个 Git 仓库的情况,那么使用 Monorepo 之后项目架构调整会比较大,也就是说改造成本会相对比较高。
  • Monorepo 本身也存在一些天然的局限性,如项目数量多起来之后依赖安装时间会很久、项目整体构建时间会变长等等,我们也需要去解决这些局限性所带来的的开发效率问题。而这项工作一般需要投入专业的人去解决,如果没有足够的人员投入或者基建的保证,Monorepo 可能并不是一个很好的选择。
  • 项目构建问题。跟 发 npm 包的方案一样,所有的公共代码都需要进入项目的构建流程中,产物体积还是会偏大。

二、Module Federation核心概念

下面我们就来正式介绍Module Federation,即模块联邦解决方案,看看它到底是如何解决模块复用问题的。模块联邦中主要有两种模块: 本地模块和远程模块。

本地模块即为普通模块,是当前构建流程中的一部分,而远程模块不属于当前构建流程,在本地模块的运行时进行导入,同时本地模块和远程模块可以共享某些依赖的代码,如下图所示:

image.png

值得强调的是,在模块联邦中,每个模块既可以是本地模块,并导入其它的远程模块,又可以作为远程模块,被其他的模块导入。如下面这个例子所示:

image.png

以上就是模块联邦的主要设计原理,现在我们来好好分析一下这种设计究竟有哪些优势:

  • 实现任意粒度的模块共享。这里所指的模块粒度可大可小,包括第三方 npm 依赖、业务组件、工具函数,甚至可以是整个前端应用!而整个前端应用能够共享产物,代表着各个应用单独开发、测试、部署,这也是一种微前端的实现。
  • 优化构建产物体积。远程模块可以从本地模块运行时被拉取,而不用参与本地模块的构建,可以加速构建过程,同时也能减小构建产物。
  • 运行时按需加载。远程模块导入的粒度可以很小,如果你只想使用 app1 模块的add函数,只需要在 app1 的构建配置中导出这个函数,然后在本地模块中按照诸如import(‘app1/add’)的方式导入即可,这样就很好地实现了模块按需加载。
  • 第三方依赖共享。通过模块联邦中的共享依赖机制,我们可以很方便地实现在模块间公用依赖代码,从而避免以往的external + CDN 引入方案的各种问题。

从以上的分析你可以看到,模块联邦近乎完美地解决了以往模块共享的问题,甚至能够实现应用级别的共享,进而达到微前端的效果。下面,我们就来以具体的例子来学习在 Vite 中如何使用模块联邦的能力来解决代码复用。

三、Module Federation应用实战

社区中已经提供了一个比较成熟的 Vite 模块联邦方案: vite-plugin-federation,这个方案基于 Vite(或者 Rollup) 实现了完整的模块联邦能力。接下来,我们基于它来实现模块联邦应用。首先,初始化两个 Vue 的脚手架项目host和remote,然后分别安装vite-plugin-federation插件,命令如下:

npm install @originjs/vite-plugin-federation -D

然后在配置文件vite.config.ts中加入如下的配置代码:

// 远程模块配置
// remote/vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import federation from "@originjs/vite-plugin-federation";


// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    // 模块联邦配置
    federation({
      name: "remote_app",
      filename: "remoteEntry.js",
      // 导出模块声明
      exposes: {
        "./Button": "./src/components/Button.js",
        "./App": "./src/App.vue",
        "./utils": "./src/utils.ts",
      },
      // 共享依赖声明
      shared: ["vue"],
    }),
  ],
  // 打包配置
  build: {
    target: "esnext",
  },
});


// 本地模块配置
// host/vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import federation from "@originjs/vite-plugin-federation";


export default defineConfig({
  plugins: [
    vue(),
    federation({
      // 远程模块声明
      remotes: {
        remote_app: "http://localhost:3001/assets/remoteEntry.js",
      },
      // 共享依赖声明
      shared: ["vue"],
    }),
  ],
  build: {
    target: "esnext",
  },
});

在如上的配置中,我们完成了远程模块的模块导出及远程模块在本地模块的注册,对于远程模块的具体实现,可以参考Github 仓库的代码。接下来,我们终点关注下如何使用远程模块。

首先,我们需要对远程模块进行打包,在 remote 路径下依赖执行命令:

// 打包产物
pnpm run build
// 模拟部署效果,一般会在生产环境将产物上传到 CDN 
npx vite preview --port=3001 --strictPort

然后,我们在 host项目中使用远程模块,示例代码如下。

<script setup lang="ts">
import HelloWorld from "./components/HelloWorld.vue";
import { defineAsyncComponent } from "vue";
// 导入远程模块
// 1. 组件
import RemoteApp from "remote_app/App";
// 2. 工具函数
import { add } from "remote_app/utils";
// 3. 异步组件
const AysncRemoteButton = defineAsyncComponent(
  () => import("remote_app/Button")
);
const data: number = add(1, 2);
</script>


<template>
  <div>
    <img alt="Vue logo" src="./assets/logo.png" />
    <HelloWorld />
    <RemoteApp />
    <AysncRemoteButton />
    <p>应用 2 工具函数计算结果: 1 + 2 = {{ data }}</p>
  </div>
</template>


<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

然后,使用npm run dev启动项目后就可以看到如下的结果。

image.png

应用 2 的组件和工具函数逻辑已经在应用 1 中生效,也就是说,我们完成了远程模块在本地模块的运行时引入。让我们来梳理一下整体的使用流程:

  1. 远程模块通过exposes 注册导出的模块,本地模块通过 remotes 注册远程模块地址。
  2. 远程模块进行构建,并部署到云端。
  3. 本地通过import '远程模块名称/xxx’的方式来引入远程模块,实现运行时加载。

四、Module Federation实现原理

从以上示例中大家可以看到,Module Federation 使用比较简单,对已有项目来说改造成本并不大。那么,这么强大而易用的特性是如何在 Vite 中得以实现的呢?接下来,我们来深入探究一下 MF 背后的实现原理,分析vite-plugin-federation这个插件背后究竟做了些什么。

总体而言,实现模块联邦有三大主要的要素:

  • Host模块: 即本地模块,用来消费远程模块。
  • Remote模块: 即远程模块,用来生产一些模块,并暴露运行时容器供本地模块消费。
  • Shared依赖: 即共享依赖,用来在本地模块和远程模块中实现第三方依赖的共享。

首先,我们来看看本地模块是如何消费远程模块的。之前,我们在本地模块中写过这样的引入语句。

import RemoteApp from "remote_app/App";

我们来看看 Vite 将这段代码编译成了什么样子呢。

// 为了方便阅读,以下部分方法的函数名进行了简化
// 远程模块表
const remotesMap = {
  'remote_app':{url:'http://localhost:3001/assets/remoteEntry.js',format:'esm',from:'vite'},
  'shared':{url:'vue',format:'esm',from:'vite'}
};


async function ensure() {
  const remote = remoteMap[remoteId];
  // 做一些初始化逻辑,暂时忽略
  // 返回的是运行时容器
}


async function getRemote(remoteName, componentName) {
  return ensure(remoteName)
    // 从运行时容器里面获取远程模块
    .then(remote => remote.get(componentName))
    .then(factory => factory());
}


// import 语句被编译成了这样
// tip: es2020 产物语法已经支持顶层 await
const __remote_appApp = await getRemote("remote_app" , "./App");

可以看到,除了 import 语句被编译之外,在代码中还添加了remoteMap和一些工具函数,它们的目的很简单,就是通过访问远端的运行时容器来拉取对应名称的模块。而运行时容器其实就是指远程模块打包产物remoteEntry.js的导出对象,我们来看看它的逻辑是怎样的:

// remoteEntry.js
const moduleMap = {
  "./Button": () => {
    return import('./__federation_expose_Button.js').then(module => () => module)
  },
  "./App": () => {
    dynamicLoadingCss('./__federation_expose_App.css');
    return import('./__federation_expose_App.js').then(module => () => module);
  },
  './utils': () => {
    return import('./__federation_expose_Utils.js').then(module => () => module);
  }
};


// 加载 css
const dynamicLoadingCss = (cssFilePath) => {
  const metaUrl = import.meta.url;
  if (typeof metaUrl == 'undefined') {
    console.warn('The remote style takes effect only when the build.target option in the vite.config.ts file is higher than that of "es2020".');
    return
  }
  const curUrl = metaUrl.substring(0, metaUrl.lastIndexOf('remoteEntry.js'));
  const element = document.head.appendChild(document.createElement('link'));
  element.href = curUrl + cssFilePath;
  element.rel = 'stylesheet';
};


// 关键方法,暴露模块
const get =(module) => {
  return moduleMap[module]();
};


const init = () => {
  // 初始化逻辑,用于共享模块,暂时省略
}


export { dynamicLoadingCss, get, init }

从运行时容器的代码中我们可以得出一些关键的信息:

  • moduleMap用来记录导出模块的信息,所有在exposes参数中声明的模块都会打包成单独的文件,然后通过 dynamic import 进行导入。
  • 容器导出了十分关键的get方法,让本地模块能够通过调用这个方法来访问到该远程模块。

至此,我们就梳理清楚了远程模块的运行时容器与本地模块的交互流程,如下图所示。

image.png

接下来,我们继续分析共享依赖的实现。拿之前的示例项目来说,本地模块设置了shared: [‘vue’]参数之后,当它执行远程模块代码的时候,一旦遇到了引入vue的情况,会优先使用本地的 vue,而不是远端模块中的vue。

image.png

让我们把焦点放到容器初始化的逻辑中,回到本地模块编译后的ensure函数逻辑。

// host


// 下面是共享依赖表。每个共享依赖都会单独打包
const shareScope = {
  'vue':{'3.2.31':{get:()=>get('./__federation_shared_vue.js'), loaded:1}}
};
async function ensure(remoteId) {
  const remote = remotesMap[remoteId];
  if (remote.inited) {
    return new Promise(resolve => {
        if (!remote.inited) {
          remote.lib = window[remoteId];
          remote.lib.init(shareScope);
          remote.inited = true;
        }
        resolve(remote.lib);
    });
  }
}

可以发现,ensure函数的主要逻辑是将共享依赖信息传递给远程模块的运行时容器,并进行容器的初始化。接下来我们进入容器初始化的逻辑init中。

const init =(shareScope) => {
  globalThis.__federation_shared__= globalThis.__federation_shared__|| {};
  // 下面的逻辑大家不用深究,作用很简单,就是将本地模块的`共享模块表`绑定到远程模块的全局 window 对象上
  Object.entries(shareScope).forEach(([key, value]) => {
    const versionKey = Object.keys(value)[0];
    const versionValue = Object.values(value)[0];
    const scope = versionValue.scope || 'default';
    globalThis.__federation_shared__[scope] = globalThis.__federation_shared__[scope] || {};
    const shared= globalThis.__federation_shared__[scope];
    (shared[key] = shared[key]||{})[versionKey] = versionValue;
  });
};

当本地模块的共享依赖表能够在远程模块访问时,远程模块内也就能够使用本地模块的依赖(如 vue)了。现在我们来看看远程模块中对于import { h } from 'vue’这种引入代码被转换成了什么样子了呢,如下所示。

// __federation_expose_Button.js
import {importShared} from './__federation_fn_import.js'
const { h } = await importShared('vue')

不难看到,第三方依赖模块的处理逻辑都集中到了 importShared 函数,让我们来一探究竟。

// __federation_fn_import.js
const moduleMap= {
  'vue': {
     get:()=>()=>__federation_import('./__federation_shared_vue.js'),
     import:true
   }
};
// 第三方模块缓存
const moduleCache = Object.create(null);
async function importShared(name,shareScope = 'default') {
  return moduleCache[name] ? 
    new Promise((r) => r(moduleCache[name])) : 
    getProviderSharedModule(name, shareScope);
}


async function getProviderSharedModule(name, shareScope) {
  // 从 window 对象中寻找第三方包的包名,如果发现有挂载,则获取本地模块的依赖
  if (xxx) {
    return await getHostDep();
  } else {
    return getConsumerSharedModule(name); 
  }
}


async function getConsumerSharedModule(name , shareScope) {
  if (moduleMap[name]?.import) {
    const module = (await moduleMap[name].get())();
    moduleCache[name] = module;
    return module;
  } else {
    console.error(`consumer config import=false,so cant use callback shared module`);
  }
}

由于远程模块运行时容器初始化时已经挂载了共享依赖的信息,远程模块内部可以很方便的感知到当前的依赖是不是共享依赖,如果是共享依赖则使用本地模块的依赖代码,否则使用远程模块自身的依赖产物代码,示意图如下。

image.png

五、小结

首先,我给你介绍了模块复用的问题有哪些历史解决方案,主要包括发布 npm 包、Git Submodule、依赖外部化 + CDN 导入和 Monorepo 架构,也分析了各自的优势与局限性,然后引出 Module Federation(MF) 的概念,并分析了它为什么能近乎完美地解决模块共享问题,主要原因包括实现了任意粒度的模块共享、减少构建产物体积、运行时按需加载以及共享第三方依赖这四个方面。

接下来,我用一个具体的项目示例来告诉你如何在 Vite 中使用模块联邦的特性,即通过vite-plugin-federation这个插件来完成 MF 的搭建。最后,我也给你详细介绍了 MF 底层的实现原理,从本地模块、远程模块、共享依赖三个视角来给你剖析 MF 的实现机制和核心编译逻辑。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/698783.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一步一步指导如何使用 ESP 深度学习在 ESP32-S3 上进行手势识别

在本文中,我们将了解如何使用ESP-DL并在ESP32-S3上部署深度学习模型。文末附免费源代码下载链接 人工智能改变了计算机与现实世界交互的方式。决策是通过将微型低功耗设备和传感器的数据获取到云端来进行的。连接性、高成本和数据隐私是这种方法的一些缺点。边缘人工智…

Character类(Java)

文章目录 1. 介绍2. 分析3. 方法3.1 isDigit()方法 --- isLetter()方法3.2 xxxx()方法3.2 xxxx()方法 1. 介绍 A. 类介绍&#xff1a;   Character 类在对象中包装一个基本类型 char 的值。Character 类型的对象包含类型为 char 的单个字段。 2. 分析 A. 类包结构&#xff1a…

SpringBoot整合redis并使用缓存注解

SpringBoot整合redis并使用缓存注解 直接上代码 添加Redis依赖&#xff0c;在pom.xml文件中添加以下依赖&#xff1a; <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId> &l…

《找 bug 的活动》VIP 会员免费的视频,PC Web 端无法观看

《找 bug 的活动》VIP 会员免费的视频&#xff0c;PC Web 端无法观看 文章目录 《找 bug 的活动》VIP 会员免费的视频&#xff0c;PC Web 端无法观看问题描述期望 问题描述 CSDN 学习中心的视频课&#xff0c;有部分是 VIP 会员免费的课程&#xff0c;但是会重复跳转到 确认订…

支持CT、彩色超声、内窥镜检查的医院影像PACS系统源码(三维重建技术)

首先&#xff0c;PACS影像存取与传输系统是以实现医学影像数字化存储、诊断为核心任务&#xff0c;从医学影像设备&#xff08;如CT、CR、DR、MR、DSA、RF等&#xff09;获取影像&#xff0c;集中存储、综合管理医学影像及病人相关信息&#xff0c;建立数字化工作流程。 其次&…

思必驰:以对话式语言计算大模型为核心的大模型体系,才是未来!

2023年6月26日&#xff0c;思必驰联合创始人、首席科学家俞凯在第五届全球智博会发表主题演讲《对话式通用人工智能与专业化语言大模型》&#xff0c;他表示大模型是人工智能的新时代&#xff0c;语言大模型、对话式的语言大模型是整个人工智能大模型进一步突破的核心。而专业化…

Unity UGUI Canvas Overlay模式获取屏幕坐标

UGUI Canvas Overlay模式获取屏幕坐标 &#x1f354;效果&#x1f371;获取 &#x1f354;效果 &#x1f371;获取 ui的position就是屏幕坐标(●’◡’●) var screenPos new Vector2(transform.position.x, transform.position.y);

什么是直放站

直放站是无线通信系统中信号向地下空间延伸覆盖设备的总称&#xff0c;直放站分近端机和远端机两部分&#xff0c;近端机通过基站或空间耦合信号并进行选频、滤波处理后变换为光信号&#xff0c;通过光纤传输到地下空间&#xff08;隧道&#xff09;内&#xff0c;由光直放站远…

LeetCode_Day7 | 三数之和、四数之和

LeetCode_哈希表 15.三数之和1.题目描述2.双指针法2.1思路及注意点2.2代码实现 3.哈希法(有待修正)3.1 思路3.2 代码实现 18.四数之和1. 题目描述 15.三数之和 1.题目描述 详情leetcode链接 2.双指针法 2.1思路及注意点 将数组排序&#xff0c;有一层for循环&#xff0c;i从…

uniapp 中 引入vant组件 和 vant 报错Unclosed bracket 的问题解决

在uniapp 中引入vant组件&#xff0c;遇到一个报错&#xff0c;所以在此记录一下完整过程 一、引入vant组件 方式一&#xff1a;前往 GitHub官网 Vant 下载压缩文件&#xff0c;获取下载中的dist 文件 方式二&#xff1a;通过npm install 方式引入 npm i vant/weapp -S --pr…

【Android开发日常】一文弄懂桌面图标快捷菜单 桌面小组件

本文将介绍如何创建和管理应用快捷方式、如何创建和管理应用桌面小组件。 目录 一、桌面菜单1.1 概览1.2、为什么需要桌面图标快捷菜单1.3、如何实现桌面图标快捷菜单1.3.1 创建静态快捷方式1.3.2 创建动态快捷方式1.3.3 创建固定快捷方式1.3.4 使用快捷方式的最佳做法 1.4 注…

DOTA-c(RGDyK)和DOTA-cyclo(RGDyK)对αVβ3的亲和力和选择性的影响

&#xff08;文章编辑来源于&#xff1a;西安凯新生物科技有限公司小编WMJ&#xff09; ●英文名&#xff1a;DOTA-cyclo(RGDyK)&#xff0c;DOTA-c(RGDyK) ●外观以及性质&#xff1a; DOTA-cyclo(RGDyK)中Cyclo(RGDyK)作用于αVβ3比作用于αVβ5和αIIbβ3表现更高的亲和力…

Prometheus 指标存储 观测 dubbo /windows_exporter指标 windows 版本 其他系统换个语法思路一样

目录 下载 Prometheus 访问Prometheus Targets 发现服务 对应的 dubbo 指标就出来了 Dubbo脚手架生成个最简单的项目 导入 Prometheus 相关包 或者使用这个包即可 启动后就自动上报指标了 Windows_exporter or node_exporter 端口 9182 Prometheus 配置 windows_exp…

第37节:cesium 下雪效果(含源码+视频)

结果示例: 完整源码: <template><div class="viewer"><vc-viewer @ready="ready" :logo="false"><!

SpringBoot - 在IDEA中经常发现:Could not autowire. No beans of ‘xxx‘ type found的错误

错误描述 在SPRINGBOOT的项目中&#xff0c;使用IDEA时经常会遇到Could not autowire. No beans of ‘xxxx’ type found的错误提示&#xff0c;但是程序的编译和运行都没有问题&#xff0c;这个错误提示并不影响项目的生产。 解决方案

Unity3D:专属 Inspector

推荐&#xff1a;将 NSDT场景编辑器 加入你的3D工具链 3D工具集&#xff1a; NSDT简石数字孪生 专属 Inspector 专属 Inspector 是专门用于特定游戏对象、Unity 组件或资源的 Inspector 窗口。 它始终显示为其打开的项目的属性&#xff0c;即使您在场景或项目中选择了其他内容…

Flask框架之WTForms(详解)

目录 WTForms介绍和基本使用 WTForms介绍 基本使用 WTForms表单验证的基本使用 WTForms常用验证器 WTForms自定义验证器 场景&#xff1a;验证码实现 WTForms渲染模版 常用的方法 代码示例 WTForms介绍和基本使用 WTForms介绍 这个插件库主要有两个作用。 第一个是做…

十九、socket套接字编程(一)——UDP

文章目录 一、socket套接字编程接口&#xff08;一&#xff09;socket头文件&#xff08;二&#xff09;socket 常见API&#xff08;套接字编程接口&#xff09;1. 创建 socket 文件描述符 (TCP/UDP, 客户端 服务器 )2.绑定网络信息 (TCP/UDP, 服务器 )3.开始监听 socket (TCP…

如何使用 OCI Artifacts、ORAS 和 Docker Hub 标准化软件交付

Docker Hub 是最著名的用于分发和共享容器映像的注册表。不过, Docker Hub 和其他符合 OCI 的注册表现在可以做的不仅仅是容器镜像。ORAS (OCI 注册表作为存储)项目将注册表转换为通用工件存储,能够发布与您的应用程序相关的任何资产。 在本文中,您将了解 ORAS 是什么、它…

echarts-热力图+高亮+轨迹

var mapData [{name: "北京",devicesCount: 100, //总数value: 10, //使用个数},{name: "天津",devicesCount: 50,value: 20,},{name: "上海",devicesCount: 80,value: 30,},{name: "重庆",devicesCount: 90,value: 40,},{name: "…