vite 深入浅出

vite 深入浅出

在这里插入图片描述

简介

vite(轻量,轻快的意思) 是一个由原生 ES Module 驱动的 Web 开发前端构建工具。

浏览器原生 ESM:浏览器支持的 JavaScript 模块化标准,可以直接使用 <script type="module"> 标签加载模块,无需打包或转译。

在开发环境下基于浏览器原生 ES Module 的支持实现了 no-bundle 服务。另一方面借助 esbuild 超快的编译速度来做第三方库构建和 ts/jsx 语法编译,从而能够有效提高开发效率。在生产环境下基于 rollup 打包来构建代码。

除了开发效率,在其他维度上 vite 也表现不错:

  • 模块化方面:vite 基于浏览器原生 ESM 的支持实现模块加载,并且无论是开发环境还是生产环境,都可以将其他格式的产物(如 CommonJS )转换为 ESM。

  • 语法转译方面:vite 内置了对 TypeScriptJSXSass 等高级语法的支持,也能够加载各种各样的静态资源,如 imageWorker 等等。

  • 产物质量方面:vite 基于成熟的打包工具 Rollup 实现生产环境打包,同时可以配合 TerserBabel 等工具链,可以极大程度保证构建产物的质量。

优点

工具名称开发环境(Dev)热更新 (HMR)生产环境(Production)
Webpack会先打包生成 bundle,再启动开发服务器HMR 时需要把改动模块及相关依赖全部编译打包生成 bundle
vite先启动开发服务器,利用新一代浏览器的 ESM 能力,无需打包,直接请求所需模块并实时编译HMR时只需让浏览器重新请求该模块,同时利用浏览器的缓存(源码模块协商缓存,依赖模块强缓存)来优化请求;使得无论应用大小如何,HMR 始终能保持快速更新。通过成熟的 rollup 打包工具来生成 bundle

vite 在开发环境下冷启动无需打包,无需分析模块之间的依赖,同时也无需在启动开发服务器前进行编译,启动时还会使用 esbuild 来进行预构建。下图基于原生 ESM 的开发服务流程图。

在这里插入图片描述

webpack 在启动后会做一堆事情,经历一条很长的编译打包链条,从入口开始需要逐步经历语法解析、依赖收集、代码转译、打包合并、代码优化,最终将高版本的、离散的源码编译打包成低版本、高兼容性的产物代码,这些都是 IO 操作,在 Node 运行时下性能必然是有问题。下图基于 bundle 的开发服务流程图。

在这里插入图片描述

性能利器—esbuild

格式转换

由于 vite 是基于浏览器原生的 ESM 规范来实现的,这就要求整个项目中涉及的所有源代码必须符合 ESM 规范。而在实际开发过程中,业务代码我们可以严格按照 ESM 规范来编写,但第三方依赖就无法保证了。举个🌰:作为一个流行的 JavaScript 实用工具库 lodash 是以 CommonJS 的规范导出的。在开发环境下 vite 会对 lodash 进行依赖转换,这里我们可以通过配置 optimizeDeps: { exclude: ['lodash']} 在预构建中强制排除依赖项。配置完之后,编写一段测试代码:

// App.vue
import _ from 'lodash'
console.log(_.cloneDeep({}))

我们在 App.vue 中以 ESM 的方式导入 lodash 并调用其中的方法,通过控制台可以看到由于导出规范的不同使用 ESM 方式导入会报错。
在这里插入图片描述

依赖转换之后可以使用 ESM 规范引入:

在这里插入图片描述

减少 HTTP 请求数量

vite 会将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面加载性能。例如,lodash-es 有超过 600 个内置模块。当我们执行 import { debounce } from 'lodash-es' 时,浏览器同时发出 600 多个 HTTP 请求。如果不做处理就直接使用,那么就会引发请求瀑布流,这对页面性能来说,简直就是一场灾难。通过预构建 lodash-es 成为一个模块,我们就只需要一个 HTTP 请求。

在这里插入图片描述
在这里插入图片描述

文件缓存系统

预构建结果缓存

默认情况下,预构建结果会保存到 node_modules/.vite/deps 目录下。它根据几个源来决定是否需要重新运行预构建步骤: package.json 中的 dependencies 列表、包管理器的 lockfile,例如 package-lock.json, yarn.lock ,或者 pnpm-lock.yaml 可能在 vite.config.js 相关字段中配置过的。如果这些都没有变,那么 vite 会复用上一次 预构建的结果。如果不想让 vite 复用上一次预构建的结果,我们可以通过配置 server: { force: true },使得每次启动的时候都强制进行预构建。

依赖模块强缓存

vite 会将项目中的依赖库通过外部引入的方式加载,而不是将其打包到最终的构建文件中。这样可以利用浏览器缓存,减少资源请求,提高页面加载速度。在请求时,可以通过修改请求的版本号 v=xxxx 来规避强缓存。

在这里插入图片描述

其中 immutable 的含义是就算用户刷新页面,浏览器也不会发起请求去服务,浏览器会直接从本地磁盘或者内存中读取缓存并返回 200 状态。

源码模块协商缓存

vite 会对源码模块采用协商缓存策略。

在这里插入图片描述

其中 no-cache 并不意味着“不缓存”,而是允许缓存,但是必须首先向源服务器提交验证请求,并且通常是通过使用 ETag 完成的。

Bundle 工具

早期 vite 1.x 版本中使用 rollup 来做这件事情,但 esbuild 的性能实在是太恐怖了,vite 2.x 果断采用 esbuild 来完成第三方依赖的预构建,至于性能到底有多强。这里引用一张来自 esbuild 官网的图片。

在这里插入图片描述

从上图可以看出来相较于其他的打包工具 esbuild 完全是碾压的存在,既然 esbuild 性能这么出众那为什么 vite 不把它用做生产环境的打包工具呢?具体有如下几个原因:

  • vite 当前的插件 API 与使用 esbuild 作为打包器并不兼容。rollup 的插件 API 和基础设施更加完善,因此在生产环境中,使用 rollup 打包会更稳定。

  • 不提供操作打包产物的接口,像 rollup 中灵活处理打包产物的能力(如 renderChunk 钩子)在 esbuild 当中完全没有。

  • 不支持自定义 Code Splitting 策略。传统的 Webpackrollup 都提供了自定义拆包策略的 API,而 esbuild 并未提供,从而降级了拆包优化的灵活性。

尽管 esbuild 有如此多的局限性,但依然不妨碍 vite 在开发阶段使用它成功启动项目并获得极致的性能提升,生产环境处于稳定性考虑当然是采用功能更加丰富、生态更加成熟的 rollup 作为依赖打包工具了。

TS(X) 和 JS(X) 编译工具

vite 已经将 esbuild 的 Transformer 能力用到了生产环境

TS(X)/JS(X) 单文件编译上面,vite 也使用 esbuild 进行语法转译,也就是将 esbuild 作为 Transformer 来用。需要注意的是 esbuild 并没有实现 TS 的类型系统,在编译 TS(X)/JS(X) 文件时仅仅抹掉了类型相关的代码,暂时没有能力实现类型检查。

代码压缩

vite 从 2.6 版本开始默认使用 esbuild 来进行生产环境的代码压缩,包括 JS 代码和 CSS 代码

这里有一个🌰比较不同最新版本
JavaScript minifiers 在压缩速度和压缩质量方面的区别。

在这里插入图片描述

由于优异的性能 esbuild (基于 Golang 开发)完成,而不是传统的 webpack/rollup,也不会有明显的打包性能问题,反而是 vite 项目启动飞快(秒级启动)的一个核心原因。总的来说,viteesbuild 作为自己的性能利器,将 esbuild 各个垂直方向的能力(Bundler、Transformer、Minifier)利用的淋漓尽致,给 vite 的高性能提供了有利的保证。

构建基石—rollup

rollupvite 中的重要性一点也不亚于 esbuild,它既是 vite 用作生产环境打包的核心工具,同时为了在生产环境中也能取得优秀的产物性能。在打包阶段 vite 到底基于 rollup 做了哪些事情?

CSS 代码分割

如果某个异步模块中引入了一些 CSS 代码,vite 就会自动将这些 CSS 抽取出来生成单独的文件,这个 CSS 文件将在该异步 chunk 加载完成时自动通过一个 <link> 标签载入,该异步 chunk 会保证只在 CSS 加载完毕后再执行,避免发生页面先加载出来,样式后加载出来,导致出现闪屏的状况。举个🌰:

// About.vue
<template>
  <div @click="switchTab(tab)" v-for="(tab, index) in tabData" :key="index" >
    {{ tab.name }}
  </div>
  <component :is="currentTab.tabComp"></component>
</template>

<script setup lang="ts">
import { reactive, markRaw, defineAsyncComponent } from 'vue';
const A = defineAsyncComponent(() => import('./A.vue'))
const B = defineAsyncComponent(() => import('./B.vue'))
const tabData = reactive([
  { name: 'A组件', tabComp: markRaw(A) },
  { name: 'B组件', tabComp: markRaw(B) },
]);
const currentTab = reactive({
  tabComp:tabData[1].tabComp
})
const switchTab = (tab) => {
  currentTab.tabComp = tab.tabComp;
};
</script>
// A.vue
<template>
  <div class="name">
    我是A组件的内容
  </div>
</template>

<script setup lang="ts">
import '../styles/index.css'
import '../utils/index'
</script>
// B.vue
<template>
  <div class="name">
    我是B组件的内容
  </div>
</template>

<script setup lang="ts">
import '../utils/index'
</script>
// utils/index.js
console.log('我是模块C的内容')
// styles/index.css
.name {
  color: red;
}

在这里插入图片描述

异步 Chunk 加载优化

在实际项目中,通常会存在共用 chunk (被两个或以上的其他 chunk 共享的 chunk)。还是以上面的代码为🌰:

在无优化的情境下,当异步 chunk B 被导入时,浏览器将必须请求和解析B,然后它才能弄清楚它需要共用 index.ts。通过控制台可以看到额外的网络往返。

在这里插入图片描述

vite 将使用一个预加载步骤自动重写代码来分割动态导入调用,以实现当B被请求时 index.ts 也将同时被请求。

在这里插入图片描述

构建过程

对于一次完整的构建过程而言, rollup 会先进入到 build 阶段,解析各模块的内容及依赖关系,然后进入 output 阶段,完成打包及输出的过程。对于不同的阶段,rollup 插件会有不同的插件工作流程,拆解一下 rollup 插件在 buildoutput 两个阶段的详细工作流程。

build 阶段工作流

在这个阶段主要进行模块代码的转换、AST 解析以及模块依赖的解析,那么这个阶段的 Hook 对于代码的操作粒度一般为模块级别,也就是单文件级别

在这里插入图片描述

  1. 首先经历 options 钩子进行配置的转换,得到处理后的配置对象。

  2. 随之 rollup 会调用 buildStart 钩子,正式开始构建流程。(从 input 配置指定的入口文件开始)

  3. rollup 先进入到 resolveId 钩子中解析文件路径。

  4. rollup 通过调用 load 钩子加载模块内容。

  5. 紧接着 rollup 执行所有的 transform 钩子来对模块内容进行进行自定义的转换,比如 babel 转译。

  6. 现在 rollup 拿到解析后的模块内容,进行 AST 分析,得到所有的 import 内容,调用 moduleParsed 钩子:

    1. 如果是普通的 import,则执行 resolveId 钩子,继续回到步骤3。

    2. 如果是动态 import,则执行 resolveDynamicImport 钩子解析路径,如果解析成功,则回到步骤4加载模块,否则回到步骤3通过 resolveId 解析路径。

  7. 直到所有的 import 都解析完毕,rollup 执行buildEnd 钩子,build 阶段结束。

output 阶段工作流

ouput Hook (官方称为 Output Generation Hook),则主要进行代码的打包,对于代码而言,操作粒度一般为 chunk级别(一个 chunk 通常指很多文件打包到一起的产物)。

在这里插入图片描述

  1. 执行所有插件的 outputOptions 钩子函数,对 output 配置进行转换。

  2. 执行 renderStart 钩子,正式开始打包。

  3. 并发执行所有插件的 banner、footer、intro、outro 钩子,这四个钩子功能很简单,就是往打包产物的固定位置(比如头部和尾部)插入一些自定义的内容,比如协议声明内容、项目介绍等等。

  4. 从入口模块开始扫描,针对动态 import 语句执行 renderDynamicImport 钩子,来自定义动态 import 的内容。

  5. 对每个即将生成的 chunk,执行 augmentChunkHash 钩子,来决定是否更改 chunk 的哈希值。

  6. 如果没有遇到 import.meta 语句,则进入下一步,否则:

    1. 对于 import.meta.url 语句调用 resolveFileUrl 来自定义 url 解析逻辑

    2. 对于其他 import.meta 属性,则调用 resolveImportMeta 来进行自定义的解析。

  7. 接着 rollup 会生成所有 chunk 的内容,针对每个 chunk 会依次调用插件的 renderChunk 方法进行自定义操作,也就是说,在这里时候你可以直接操作打包产物了。

  8. 最后会调用 generateBundle 钩子,这个钩子的入参里面会包含所有的打包产物信息,包括 chunk (打包后的代码)、asset(最终的静态资源文件)。你可以在这里删除一些 chunk 或者 asset,最终这些内容将不会作为产物输出。

插件系统

由于 vite 的插件有一套简单的顺序控制机制,用户可以通过 enforce: 'pre'(在核心插件之前被调用) 和 enforce: 'post'(在核心插件之后被调用) 是用来强制修改插件的执行顺序。如果没有定义 enforce 属性那么插件将在 vite 核心插件之后被调用。

plugins: [
  testPlugin('post'),
  testPlugin(),
  testPlugin('pre')
],
import type { PluginOption } from 'vite'

export default function vitePluginTemplate(enforce?: 'pre' | 'post'): PluginOption {
   return {
    // 插件名称
    name: 'rollup-plugin-test',

    enforce,

    // 在每次开始构建时调用,只会调用一次
    buildStart (options) {
      console.log('buildStart', enforce)
    },
    // 在每个传入模块请求时被调用,创建自定义确认函数,可以用来定位第三方依赖
    resolveId (source, importer, options) {
      console.log('resolveId', enforce, source)
    },
    // 服务器关闭时
    buildEnd () {
      console.log('buildEnd', enforce)
    },

    // 指明它们仅在 'build' 或 'serve' 模式时调用
    apply: 'serve', // apply 亦可以是一个函数
    
    // 可以在 vite 被解析之前修改 vite 的相关配置。钩子接收原始用户配置 config 和一个描述配置环境的变量env
    config (config, env) {
      return {
        resolve: {
          alias: {
            '@aaa': enforce ? enforce : '/src/styles'
          }
        }
      }
    },
    // 在解析 vite 配置后调用。使用这个钩子读取和存储最终解析的配置。当插件需要根据运行的命令做一些不同的事情时,它很有用
    configResolved (config) {
      console.log(config.resolve)

    },
    // 主要用来配置开发服务器,为 dev-server (connect 应用程序) 添加自定义的中间件
    configureServer (server: any) {
      server.middlewares.use((req, res,next) => {
        if(req.url === '/test') {
            res.end('hello vite plugin')
        } else {
          next()
        }
      })
    },
    
    // 转换 index.html 的专用钩子。钩子接收当前的 HTML 字符串和转换上下文
    transformIndexHtml (html) {
      console.log(html)
      return html.replace('<div id="app"></div>', '<div id="root"></div>')
    },

    // 执行自定义HMR更新,可以通过ws往客户端发送自定义的事件
    handleHotUpdate (ctx) {
      ctx.server.ws.send({
        type: 'custom',
        event: 'test',
        data: {
          text: 'hello vite'
        }
      })
    }
  }
}

源码浅析

createServer

在执行 npm run dev 时在源码内部会调用 createServer 方法创建一个服务,这个服务利用中间件(第三方)支持了多种能力(如 跨域、静态文件服务器等),并且内部创建了 watcher 持续监听着文件的变更,进行实时编译和热重载。

// packages/vite/src/node/server/index.ts
export async function createServer(
    inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
    // 加载项目目录的配置文件 vite.config.js
    // 如果没有找到配置文件,则直接会中止程序。
    const config = await resolveConfig(inlineConfig, 'serve')

    // 创建http服务
    const httpServer = await resolveHttpServer(serverConfig, middlewares, httpsOptions)

    // 创建ws服务
    const ws = createWebSocketServer(httpServer, config, httpsOptions)

    // 创建watcher,设置代码文件监听
    const watcher = chokidar.watch(
      path.resolve(root),
      resolvedWatchOptions,
    ) as FSWatcher

    // 文件监听变动,websocket向前端通信
    watcher.on('change', async (file) => {
      // 进行实时编译和热重载
      await handleHMRUpdate(file, server)
    })
    
    // 创建插件容器,用于在构建的各个阶段调用插件的钩子
    // PluginContainer 内部调用每个插件的 buildStart 方法
    const container = await createPluginContainer(config, moduleGraph, watcher)

    // 创建server对象
    const server: ViteDevServer = {
      config,
      middlewares,
      httpServer,
      watcher,
      ws,
      listen,
      ...
    }

    // 注册各种中间件
    // request timer
    if (process.env.DEBUG) {
      middlewares.use(timeMiddleware(root))
    }

    const initServer = async () => {
      initingServer = (async function () {
        await container.buildStart({})
        // optimize: 预构建
        await initDepsOptimizer(config, server)
      })()
      return initingServer
    }
    
    // 监听端口,启动服务
    httpServer.listen = (async (port: number, ...args: any[]) => {
      await initServer()
      return listen(port, ...args)
    }) as any
    
    return server
}

runOptimizeDeps

第一次启动时,对项目依赖进行构建这里就会使用 esbuild.build 去编译文件,其中 esbuildDepPlugin 就是打包的插件

import { build } from 'esbuild'

export async function runOptimizeDeps() {
  const plugins = [...pluginsFromConfig]
  if (external.length) {
    plugins.push(esbuildCjsExternalPlugin(external, platform))
  }
  plugins.push(esbuildDepPlugin(flatIdDeps, external, config, ssr))

  const result = await build()
}

在这里插入图片描述

浏览器请求

在这里插入图片描述

可以看到 pluginContainer 会执行插件中的钩子。对于不同的资源会有不同的插件去处理。

vite的内置插件:

路径解析插件(packages/vite/src/node/plugins/resolve.ts)

路径解析插件 是 vite 中比较核心的插件,几乎所有重要的 vite 特性都离不开这个插件的实现,诸如依赖预构建、HMR、SSR 等等。

CSS 编译插件(packages/vite/src/node/plugins/css.ts)

import 分析插件(packages/vite/src/node/plugins/importAnalysis.ts)
重写import语句,如 import Vue from 'vue' 导入路径会重写为预构建文件夹的路径;注入HMR客户端脚本。

esbuild 转译插件(packages/vite/src/node/plugins/esbuild.ts)
用来进行 .js.ts.jsx.tsx,代替了传统的 Babel 或者 TSC 的功能,这也是 vite 开发阶段性能强悍的一个原因。

HMR流程

打包工具实现热更新的思路都大同小异:主要是通过 WebSocket 创建浏览器和服务器的通信监听文件的改变,当文件被修改时,服务端发送消息通知客户端修改相应的代码,客户端对应不同的文件进行不同的操作的更新。

在这里插入图片描述

总结

以上针对 vite 从简单的原理、优点、源码、钩子函数、生命周期,经行了一个大体的介绍,文章字数较多,阅读需要花费较长的时间,但是读完之后一定会让你对 vite 这个前端构建工具有了更深层次的了解。由于篇幅限制具体的配置可以参考官网。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

第二证券:定增价公布后第二天股价表现?

近年来&#xff0c;定增成为一种较为老练的公司融资方法&#xff0c;它通过向指定政策定向发行股份来筹集资金&#xff0c;相关于非公开发行股票或增发股份&#xff0c;定增的市场轰动和价格变化相对较小。但是&#xff0c;定增股票发行通常会推动股价的不坚决和出资者的心境崎…

Prometheus+Ansible+Consul实现服务发现

一、简介 1、Consul简介 Consul 是基于 GO 语言开发的开源工具&#xff0c;主要面向分布式&#xff0c;服务化的系统提供服务注册、服务发现和配置管理的功能。Consul 提供服务注册/发现、健康检查、Key/Value存储、多数据中心和分布式一致性保证等功能。 在没有使用 consul 服…

【社会网络分析第5期】gephi使用指南

gephi数据可视化 gephi数据可视化1、软件安装2、数据处理与导入&#xff08;1&#xff09;导入节点&#xff08;2&#xff09;导入边&#xff08;3&#xff09;改变节点的颜色&#xff08;4&#xff09;根据pagerank调整节点的大小&#xff08;5&#xff09;根据pagerank调整边…

上海亚商投顾:沪指缩量调整跌 高位强势股继续退潮

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 一.市场情绪 三大指数11月10日弱势震荡&#xff0c;上证50盘中跌超1%&#xff0c;以保险为首的权重板块走势较弱。 高位强…

SpringCloudalibaba

一、分布式和微服务 分布式系统和服务是现代软件开发中的两个重要概念。它们为复杂的应用程序提供了模块化和可扩展性&#xff0c;使其能够在多台机器上运行&#xff0c;并为大量用户提供服务。 分布式系统 定义: 分布式系统是由多个独立组件组成的系统&#xff0c;这些组件…

Maven 插件统一修改聚合工程项目版本号

目录 引言直接修改 pom.xml 的版本号的问题Maven 插件修改版本号开源项目微服务商城项目前后端分离项目 引言 在Maven项目中&#xff0c;我们通常有两种常见的方式来修改版本号&#xff1a;直接在pom.xml文件中手动编辑和利用Maven插件进行版本号调整。 本文将比较这两种修改…

R语言编写代码示例

R语言编写的爬虫程序&#xff0c;使用了requests库来发送请求&#xff0c;使用BeautifulSoup库来解析HTML。 r # 第一步&#xff0c;安装必要的库 install.packages("xml2") install.packages("requests") install.packages("httr") install.pac…

【系统安装】ubuntu20.04安装,正经教程,小白安装教程,百分百成功安装

1、安装的前提是有启动盘&#xff0c;这个比较好处理&#xff0c;清华源找到ubuntu20.04.iso镜像文件下载&#xff0c;然后用Rufus来制作启动盘就可以了&#xff0c;需要注意的是目标文件系统需要是UEFI&#xff0c;其他的话就没太多要求了&#xff0c;如果卡在这一步的话&…

助力燃气安全运行:智慧燃气管网背景延展

关键词&#xff1a;城市燃气管网、智慧燃气管网、智慧管网、智慧燃气管网解决方案、智慧燃气 01背景 当前&#xff0c;随着我国城市化进程不断加快&#xff0c;城市燃气管网也不断延伸&#xff0c;运行规模庞大&#xff0c;地下管线复杂&#xff0c;不少城市建设“重地上轻地…

Windows系统下使用docker部署redis

使用虚拟机部署redis&#xff0c;虚拟机很占用电脑资源&#xff0c;所以选择使用docker对redis进行部署。 一、安装docker 安装链接&#xff1a;https://docker.p2hp.com/ 二、配置redis.conf文件 下载配置文件&#xff1a;https://download.redis.io/redis-stable/redis.con…

Js 语句

JavaScript 语句向浏览器发出的命令&#xff0c;语句的作用是告诉浏览器该做什么&#xff1b;分号用于分隔 JavaScript 语句&#xff0c;通常我们在每条可执行的语句结尾添加分号&#xff1b;使用分号的另一用处是在一行中编写多条语句。 JavaScript 语句通常以一个 语句标识符…

【C语言】深入解开指针(二)

&#x1f308;write in front :&#x1f50d;个人主页 &#xff1a; 啊森要自信的主页 &#x1f308;作者寄语 &#x1f308;&#xff1a; 小菜鸟的力量不在于它的体型&#xff0c;而在于它内心的勇气和无限的潜能&#xff0c;只要你有决心&#xff0c;就没有什么事情是不可能的…

Hubspot是如何发展到今天的?有哪些实用工具?

HubSpot&#xff0c;作为一家全球领先的数字化市场营销和销售平台提供商&#xff0c;通过其强大的生态圈和创新的解决方案&#xff0c;帮助企业实现高效运营、客户吸引和业务增长。运营坛今天将详细介绍HubSpot的发展历程以及其三大核心产品&#xff1a;CMS Hub、Marketing Hub…

雷达波形及MATLAB仿真

文章目录 前言一、雷达波形二、Matlab 仿真1、SFW 的距离分辨率和距离模糊①、MATLAB 源码②、仿真结果 三、资源自取 前言 本文对雷达波形的内容以思维导图的形式呈现&#xff0c;有关仿真部分进行了讲解实现。 一、雷达波形 思维导图如下图所示&#xff0c;如有需求请到文章…

No198.精选前端面试题,享受每天的挑战和学习

🤍 前端开发工程师(主业)、技术博主(副业)、已过CET6 🍨 阿珊和她的猫_CSDN个人主页 🕠 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 🍚 蓝桥云课签约作者、已在蓝桥云课上架的前后端实战课程《Vue.js 和 Egg.js 开发企业级健康管理项目》、《带你从入…

@Async注解的坑

问题描述 一个方法调用另一个方法(该方法使用Async注解)在同一个类文件中&#xff0c;该注解会失效&#xff01; 问题复现 TestAsyncController 类 import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Async; im…

社区新零售:改变生活方式的创新商业模式

社区新零售&#xff1a;改变生活方式的创新商业模式 社区新零售&#xff0c;顾名思义&#xff0c;以社区为核心&#xff0c;利用互联网、大数据、人工智能等先进技术&#xff0c;将线上购物和线下体验有机结合&#xff0c;形成一种全新的零售模式。它特别强调地理位置的便利性&…

2023亚太杯数学建模C题思路解析

文章目录 0 赛题思路1 竞赛信息2 竞赛时间3 建模常见问题类型3.1 分类问题3.2 优化问题3.3 预测问题3.4 评价问题 4 建模资料5 最后 0 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 1 竞赛信息 2023年第十三…

【Axure高保真原型】3D饼图_移入显示数据标签

今天和大家分享3D饼图_移入显示数据标签的原型模板&#xff0c;鼠标移入扇形区域时&#xff0c;对应区域会变绿&#xff0c;可以查看该区域对应的项目、数据和占比&#xff0c;这个原型模板是用Axure原生元件制作的&#xff0c;所以无需联网&#xff0c;而且可以自由修改样式、…

windows服务器热备、负载均衡配置

安装网络负载平衡 需要加入的服务器上全部需要安装网络负载平衡管理器 图形化安装&#xff1a;使用服务器管理器安装 在服务器管理器中&#xff0c;使用“添加角色和功能”向导添加网络负载均衡功能。 完成向导后&#xff0c;将安装 NLB&#xff0c;并且不需要重启计算机。 …
最新文章