Solid框架节点级响应性

news2024/12/24 9:58:11

前言

随着组件化、响应式、虚拟DOM等技术思想引领着前端开发的潮流,相关的技术框架大行其道,就以目前主流的Vue、React框架来说,它们都基于组件化、响应式、虚拟DOM等技术思想的实现,但是具有不同开发使用方式以及实现原理,这里就不再赘述了相关内容,这里关注的焦点在于虚拟DOM。

无论是Vue还是React都应用虚拟DOM,通过虚拟DOM从而来减少频繁的DOM操作,优化页面性能。随着虚拟DOM应用到实际生产中后,无论是Vue还是React都少不了增加虚拟DOM对象以及相应Diff过程,特别是Diff过程恰恰是影响速度的重要点。人们渐渐思考虚拟DOM真的就比直接操作DOM要快吗?渐渐出现了放弃虚拟DOM的现代化技术框架,例如Solid、Svelte等。

Svelte和Solid都是放弃虚拟DOM但是应用组件化、响应式等技术的现代化框架,Svelte则比较偏向于Vue形式风格,Solid框架则偏向于React形式风格,但是底层响应式实现完全不同,这篇文章就聊聊Solid框架以及背后相关思想。

Solid基本说明

实际上最初Solid吸引我的点就是所谓号称比React还react的言论,作为多年的React框架使用者,我了解背后的实现思想以及实际开发中的痛点,而Solid框架有很多让我想要探索的点:

  • 放弃虚拟DOM下的快速更新效率,没有虚拟DOM 或广泛的差异对比
  • 保持React Hooks风格下的没有所谓的Hook规则
  • 细粒度的响应性控制,函数组件只执行一次以及节点级的UI视图更新策略

Solid最吸引我的点就是细粒度的响应性,相比于React以及Vue更新策略,Solid可以做到节点级的视图更新:

  • React更新策略基本描述:父组件的状态变更会导致当前子树下所有组件重新运行,需要对无需更新的子组件进行memo操作
  • Vue更新策略的基本描述:每一个组件都对应一个Watcher对象,组件响应性状态与视图Watcher对象关联,组件的状态变更只会影响与之关联的对应视图Watcher对象,即只更新与状态建立联系的组件
  • Solid更新策略的基本描述:组件的状态变更只会影响使用该状态的节点UI视图的更新

对于Solid的使用可以查看其官网,Solid的基本使用案例如下:

import { render } from 'solid-js/web';
import { createSignal } from 'solid-js';

function App() {
  const [loggedIn, setLoggedIn] = createSignal(false);
  const toggle = () => setLoggedIn(!loggedIn())
  
  return (
    <>
      <button onClick={toggle}>Log out</button>
      <button onClick={toggle}>Log in</button>
    </>
  );
}

render(() => <App />, document.getElementById('app'))

render处理

组件经过Solid相关工具编译处理得到的最终代码逻辑如下:

// render(() => <Counter />, document.getElementById("app")!);
render(() => _$createComponent(Counter, {}), document.getElementById("app"));

上面实例初始化阶段的处理逻辑,render函数的逻辑如下:

function render(code, element, init, options = {}) {
  let disposer;
  createRoot(dispose => {
    disposer = dispose;
    element === document ? code() : insert(element, code(), element.firstChild ? null : undefined, init);
  }, options.owner);
  return () => {
    disposer();
    element.textContent = "";
  };
}

从上面逻辑可以如下调用链:

render -> createRoot -> runUpdates -> 顺序执行createRoot传入的回调函数、completeUpdates

createRoot回调逻辑主要逻辑如下:

  element === document ? code() : insert(element, code(), element.firstChild ? null : undefined, init);

主要就是两点逻辑:

  • 执行code函数:code就是render传入的第一个参数,这里就会被执行,通常来说就是函数组件,即函数组件会被执行
  • 执行insert函数:实现组件内容挂载到页面上
insert函数

insert函数具体处理逻辑如下:

function insert(parent, accessor, marker, initial) {
  if (marker !== undefined && !initial) initial = [];
  if (typeof accessor !== "function") return insertExpression(parent, accessor, initial, marker);
  createRenderEffect(current => {
  	insertExpression(parent, accessor(), current, marker);
  }, initial);
}

insert函数的accessor参数有两种情况:

  • 非函数类型:意味着是DOM挂载点,就会调用insertExpression,即将节点插入到页面DOM中,从而显示视图
  • 函数类型:意味着是响应性状态,需要调用createRenderEffect函数

Solid响应性原理

类比于React的useState、useEffect,Solid提供createSignal、createEffect这两个API:

  • createSignal提供响应性状态定义
  • createEffect提供监听响应性状态变更后需要运行的副作用处理

createSignal是响应性状态定义来源,这里以下面实例来看看其背后的响应性原理逻辑:

import { render } from "solid-js/web";
import { createSignal } from "solid-js";

function Counter() {
  const [getCount, setCount] = createSignal(1);
  const increment = () => setCount(getCount() + 1);
  // 只会执行一次
  console.log('hello');
  
  return (
    <>
      <button type="button" onClick={increment}>
        点击
      </button>
      <div>{getCount()}</div>
    </>
  );
}

render(() => <Counter />, document.getElementById("app")!);

很简单的点击按钮增加计数的逻辑,当点击按钮后计数加1,而对应的div节点就会更新但是button节点不会更新。

实际上上面实例中Counter组件经过处理会变成下面形式:

unction Counter() {
  const [getCount, setCount] = createSignal(1);
  const increment = () => setCount(getCount() + 1);
  return [(() => {
    const _el$ = _tmpl$();
    _el$.$$click = increment;
    return _el$;
  })(), (() => {
    const _el$2 = _tmpl$2();
    _$insert(_el$2, getCount);
    return _el$2;
  })()];
}

Counter组件的视图部分被处理成一个个自执行函数了,即button节点和div节点分别对应一个自执行函数,如果嵌套多级子节点呢?难道每一个节点都对应一个自执行函数吗?实际上并不是如此逻辑,从实际调试得到的两点逻辑如下:

  • 只有组件的一级子节点才会一一对应一个自执行函数,次级子节点不会
  • 只要节点包含响应性状态内容则会调用_$insert函数来处理

实际上render过程会触发根组件,即Counter函数组件执行,而函数组件内部中对于Signal状态的节点的处理需要调用_$insert,实际上该函数就是insert函数。此时insert函数的accessor参数就是响应性状态的getter函数,整个调用链如下:

  • 函数组件内部调用insert函数 -> createRenderEffect -> 顺序执行createComputation 、 updateComputation
  • updateComputation -> runComputation -> 执行createRenderEffect传递的回调函数

createRenderEffect回调函数的逻辑如下:

  createRenderEffect(current => {
  	insertExpression(parent, accessor(), current, marker);
  }, initial);

其逻辑分为两点:

  • accessor函数执行:对应着Signal状态下上文getter函数,即响应性状态的getter函数会在runComputation后被调用处理
  • insertExpression:会将Signal状态值生成的DOM插入到父节点中

在accessor函数执行之前,需要了解createSignal创建响应性状态的具体逻辑,该API主要处理逻辑代码如下:

function createSignal(value, options) {
  ...
  const s = {
    value,
    observers: null,
    observerSlots: null,
    comparator: options.equals || undefined
  };
  const setter = value => {
  	...
    return writeSignal(s, value);
  };
  return [readSignal.bind(s), setter];
}

createSignal的主要逻辑很简单,就是返回响应的getter、setter函数并没有其他复杂逻辑执行:

  • 定义包含初始值的s对象,表示上下文对象,后面的getter函数的this对象就是该上下文对象,setter函数也需要使用该上下文对象
  • 上下文对象中的observers本意是观察者,必然是发布订阅模式实现的节点级更新逻辑,后面会与相关对应进行关联从而实现的,和Vue底层逻辑相似,但是不像Vue使用Proxy来实现,Solid仅仅就是单纯的函数
getter函数执行

当accessor函数执行时本质上就是执行Signal状态的getter函数,此时就会执行readSignal函数,下面是简化的逻辑:

function readSignal() {
  ...
  if (Listener) {
    const sSlot = this.observers ? this.observers.length : 0;
    if (!Listener.sources) {
      Listener.sources = [this];
      Listener.sourceSlots = [sSlot];
    } else {
      Listener.sources.push(this);
      Listener.sourceSlots.push(sSlot);
    }
    if (!this.observers) {
      this.observers = [Listener];
      this.observerSlots = [Listener.sources.length - 1];
    } else {
      this.observers.push(Listener);
      this.observerSlots.push(Listener.sources.length - 1);
    }
  }
  ...
  return this.value;
}

主要逻辑就是:当Listener全局变量存在的情况下,就会将Listener存入上下文对象的observers属性中,那么Listener什么时候有值呢?源码中全局查找Listenerd的赋值操作,就有一个地方,即updateComputation函数的处理,但是该函数的调用来源很多。在初始化阶段updateComputation函数的调用链上面就已经描述清楚了,其来源于createRenderEffect。

updateComputation中Listener的相关处理逻辑如下:

function updateComputation(node) {
  if (!node.fn) return;
  ...
  Listener = Owner = node;
  runComputation(node, Transition && Transition.running && Transition.sources.has(node) ? node.tValue : node.value, time);
  ...
}

这里的node参数就是createComputation函数的对象,即Computation对象,该对象的属性有:

  const c = {
    fn,
    state: state,
    updatedAt: null,
    owned: null,
    sources: null,
    sourceSlots: null,
    cleanups: null,
    value: init,
    owner: Owner,
    context: Owner ? Owner.context : null,
    pure
  };

所以Listener本质上就是Computation对象,其中该对象的fn属性存放的就是触发响应性状态getter函数的回调函数。

所以当初始化调用链触发getter函数执行时,Listener就已经存在,之后的处理逻辑就是:

  • 对应Signal状态的上下文对象中保存Listener,即Signal状态上下文中保存Listener到Observes属性中
  • Listener指向的Computation对象的sources属性会保存对应Signal状态的上下文

此时Computation对象与Signal状态上下文对象就互相关联起来了。后续初始化的处理逻辑就是将生成的节点通过DOM插入方法添加到节点中,没有虚拟DOM Diff的过程,这里就不继续说明了。

故而函数组件在初始化阶段内部的处理逻辑就非常清晰,具体如下:

  • insert -> createRenderEffect -> 顺序执行createComputation、updateComputation
  • updateComputation -> runComputation -> 执行createRenderEffect传递的回调函数
  • createRenderEffect传递的回调函数内部逻辑会顺序执行Signal状态getter函数、insertExpression,insertExpression会将Signal状态节点插入到父节点中
更新阶段

现在点击了按钮,此时上面实例中:首先是先调用getter函数,然后再调用setter函数。此时Signal状态的getter再次被执行,但是Listener会在每次赋值操作后被重置为之前的状态,即初始化阶段updateComputation最后处理时被重置为null,所以getter函数此时执行仅仅返回当前值而已。

setter函数执行

执行了setter函数,此时就会执行writeSignal函数,去看看该函数做了什么处理,下面是简化的逻辑(移除了Transition相关的逻辑):

function writeSignal(node, value, isComp) {
  let current = node.value;
  if (!node.comparator || !node.comparator(current, value)) {
  	...
  	node.value = value;
    if (node.observers && node.observers.length) {
      runUpdates(() => {
        for (let i = 0; i < node.observers.length; i += 1) {
          const o = node.observers[i];
          ...
          if (TransitionRunning ? !o.tState : !o.state) {
            if (o.pure) Updates.push(o);else Effects.push(o);
            if (o.observers) markDownstream(o);
          }
          if (!TransitionRunning) o.state = STALE;else o.tState = STALE;
        }
        if (Updates.length > 10e5) {
          Updates = [];
          if (false) ;
          throw new Error();
        }
      }, false);
    }
  }
  return value;
}

在初始化处理阶段,Signal状态对应的上下文对象中observers已经保存了Listener对应的Computation对象。在更新阶段时setter函数被调用,此时observers是有值的。故而其调用栈逻辑如下:

  • writeSignal -> runUpdates -> 顺序执行传入的回调函数、completeUpdates
  • 这里的回调函数的逻辑实际上就是将Computation对象更加对应的条件条件到Updates、Effects队列中
  • completeUpdates -> runQueue(对Updates、Effects 进行处理) -> 循环遍历队列依次处理,即依次调用runTop -> 一般情况下就会调用updateComputation函数

需要注意的是更新阶段updateComputation的处理有一个重要的逻辑,即cleanNode函数调用:

function updateComputation(node) {
  ...
  cleanNode(node);
  ...
  Listener = Owner = node;
  ...
}

function cleanNode() {
  if (node.sources) {
    while (node.sources.length) {
      const source = node.sources.pop(),
      index = node.sourceSlots.pop(),
      obs = source.observers;
      if (obs && obs.length) {
       const n = obs.pop(), s = source.observerSlots.pop();
       if (index < obs.length) {
         n.sourceSlots[s] = index;
         obs[index] = n;
         source.observerSlots[index] = s;
       }
      }
    }
  }
}

Computation对象与Signal状态上下文对象建立的关联在cleanNode对象中被解绑了,即当某个Signal状态更新后,Solid会将对应Signal状态的旧Computation解绑,这就是更新阶段cleanNode主要处理逻辑。

updateComputation之后的处理就是设置新的Listener对象,之后的处理逻辑就更初始化时相同,即:

updateComputation -> runComputation -> 执行createRenderEffect传递的回调函数

从而实现最新的Computation对象与Signal状态上下文关联,之后更新到对应的DOM节点,这个过程就实现了节点级别的视图更新。

在初始化阶段因为createRenderEffect传递的回调函数的参数中就有当前响应性状态所在位置的父节点,通过闭包特性保证了每次更新都是同一节点位置,从而实现Solid的节点级别的视图更新逻辑。

副作用处理

createEffect API是Solid用来处理副作用的方法,该方法类比React的useEffect,不同于useEffect的是不需要指明依赖项,它会自动收集依赖项。

通过下面实例来了解createEffect的执行过程:

import { render } from "solid-js/web";
import { createSignal } from "solid-js";

function Counter() {
  const [getCount, setCount] = createSignal(1);
  const increment = () => setCount(getCount() + 1);
  createEffect(() => {
  	console.log(getCount());
  });
  
  return (
    <>
      <button type="button" onClick={increment}>
        点击
      </button>
      <div>{getCount()}</div>
    </>
  );
}

render(() => <Counter />, document.getElementById("app")!);
初始化阶段

createEffect方法的逻辑具体如下:

function createEffect(fn, value, options) {
  runEffects = runUserEffects;
  const c = createComputation(fn, value, false, STALE),
    s = SuspenseContext && useContext(SuspenseContext);
  if (s) c.suspense = s;
  if (!options || !options.render) c.user = true;
  Effects ? Effects.push(c) : updateComputation(c);
}

在之前分析createSignal就梳理了初始化过程的关键处理过程,知道相关函数的作用:

  • createComputation:就是创建新的Computation对象
  • updateComputation:就是解绑对应Signal状态上下文对象与旧的Computation对象之间关联,之后将当前新的Computation对象重新与Signal状态上下文对象绑定

在初始化阶段createEffect逻辑主要就是两点:

  • 创建一个新的Computation对象,即每一个createEffect都会对应一个Computation对象
  • 判断Effects队列是否存在,Effects为数组就会将Computation对象保存到Effects队列中

当render函数执行时其内部会调用runUpdates函数,之后会执行completeUpdates函数,该函数内部逻辑就是处理Effects、Updates队列中内容。根据render函数的处理过程可知:

createEffect内部保存在Effects队列中副作用逻辑,会在组件挂载到节点之后被执行,初始化阶段createEffect副作用函数会立即执行,此时并没有于Signal状态上下文建立关联

当副作用逻辑执行时,如果内部存在Signal状态对象就会执行其getter函数,从而将当前Computation对象与Signal状态上下文建立关联,从而实现依赖的收集。

更新阶段

当createEffect对应的Computation与Signal状态对象建立关联后,所谓的更新阶段就是Signal状态更新时的处理逻辑流程了,实际上就是处理createEffect对应的Computation对象而已。这里需要注意的是更新阶段createEffect的处理有以下几点说明:

  • createEffect对应的Computation对象的处理总是在视图节点的Computation对象之后的
  • Solid中createEffect整个的处理过程是同步而非异步,不同于React useEffect的异步处理

总结

实际上从Solid的风格以及底层原理实现,你可以看到其他框架的影子:

  • Solid底层实现和Vue底层响应性实现非常相似,都是基于发布订阅模式实现两个对象的绑定从而构建响应的基石,只不过在Vue中Dep对象与Watcher对象,Solid中是Signal状态上下文对象和Computation对象
  • 除了采用React Hooks,Solid只支持函数组件这一种形式,还支持并发渲染即时间分片,底层实现方式与React相似,这里不再赘述了

这里梳理下Solid整体的主要处理流程,具体如下图:

Solid流程

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

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

相关文章

霍夫曼编码,计算霍夫曼编码树(数字图像处理大题复习 P7)

文章目录 计算出 x1~x6 的树计算 x1~x6 的编码得出霍夫曼编码 w1~w6 计算出 x1~x6 的树 我们先对 x1~x6 进行排序把最底下的加起来&#xff0c;上面写0 下面写1 然后一层一层加上去&#xff0c;注意加上去以后要 重新排序 计算 x1~x6 的编码 举例&#xff0c;从 x5 出发&…

计算机竞赛 深度学习LSTM新冠数据预测

文章目录 0 前言1 课题简介2 预测算法2.1 Logistic回归模型2.2 基于动力学SEIR模型改进的SEITR模型2.3 LSTM神经网络模型 3 预测效果3.1 Logistic回归模型3.2 SEITR模型3.3 LSTM神经网络模型 4 结论5 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 …

「UG/NX」Block UI 从列表选择部件SelectPartFromList

✨博客主页何曾参静谧的博客📌文章专栏「UG/NX」BlockUI集合📚全部专栏「UG/NX」NX二次开发「UG/NX」BlockUI集合「VS」Visual Studio「QT」QT5程序设计「C/C+&#

硬实力+软实力!2023功能测试进阶之路!

作为一名游戏功能测试&#xff0c;偶尔和朋友聊起工作&#xff0c;他们会说&#xff1a;“你这个工作好呀&#xff0c;平时玩玩游戏&#xff0c;一边玩一边就把工作做了”&#xff1b;有时为了避免别人这样理解&#xff0c;我干脆说自己是搞游戏开发的&#xff0c;这个时候又有…

微软推出统一 Copilot AI 助手;谷歌拟自主研发 AI 芯片;苹果拟在国内引进新的 OLED 供应商丨RTE开发者日报 Vol.55

开发者朋友们大家好&#xff1a; 这里是「RTE 开发者日报」&#xff0c;每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE &#xff08;Real Time Engagement&#xff09; 领域内「有话题的新闻」、「有态度的观点」、「有意思的数据」、「有思考的文章」、「…

「聊设计模式」之 设计模式的前世今生

&#x1f3c6;本文收录于《聊设计模式》专栏&#xff0c;专门攻坚指数级提升&#xff0c;助你一臂之力&#xff0c;带你早日登顶&#x1f680;&#xff0c;欢迎持续关注&&收藏&&订阅&#xff01; 目录&#xff1a; 一、什么是设计模式设计模式的定义设计模式的…

Caddy Web服务器深度解析与对比:Caddy vs. Nginx vs. Apache

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

QT---day2---9.18

完善登录框 点击登录按钮后&#xff0c;判断账号&#xff08;admin&#xff09;和密码&#xff08;123456&#xff09;是否一致&#xff0c;如果匹配失败&#xff0c;则弹出错误对话框&#xff0c;文本内容“账号密码不匹配&#xff0c;是否重新登录”&#xff0c;给定两个按钮…

vue antv g6 编辑器

第一&#xff1a;官方推荐代码 &#xff08;下载后最好删除eslint&#xff08;可以用第二个&#xff09;&#xff0c;再进行install&#xff09; https://github.com/claudewowo/welabx-g6https://download.csdn.net/download/qq_30940855/88343559?spm1001.2014.3001.5501 …

RK3568驱动指南|第五篇 中断-第42章 软中断实验

瑞芯微RK3568芯片是一款定位中高端的通用型SOC&#xff0c;采用22nm制程工艺&#xff0c;搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码&#xff0c;支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU&#xff0c;可用于轻量级人工…

MySQL数据库的explain关键字之type字段详解

这篇文章主要介绍一下mysql数据库常用的sql优化时使用的explain/desc关键字中&#xff0c;执行结果的type字段的取值。 目录 数据准备 const eq_ref ref range index all type字段的值主要有以下7种&#xff1a;其中system的性能最高&#xff0c;但是很难遇到&#xff0…

Spring实例化源码解析之ConfigurationClassParser(三)

前言 上一章我们分析了ConfigurationClassPostProcessor的postProcessBeanDefinitionRegistry方法的源码逻辑&#xff0c;其中核心逻辑do while中调用parser.parse(candidates)方法&#xff0c;解析candidates中的候选配置类。然后本章我们主要分析ConfigurationClassParser的…

linux服务器部署项目

1.linux安装配置JDK 以Java1.8为例&#xff0c;下载安装包 将安装包提交到服务器中并解压 tar -zxvf your_file.gz 修改/etc/profile文件 vi /etc/profile 文件中添加两条 export JAVA_HOME/path/to/your/java export PATH$JAVA_HOME/bin:$PATH 输入指令刷新文件 source /etc…

Python150题day08

2.基础语法篇 2.1 if 条件句 ①单个条件分支 使用input函数接收用户的输入&#xff0c;如果用户输入的整数是偶数&#xff0c;则使用print函数输出"你输入的整数是:{value],它是偶数”&#xff0c;[value]部分要替换成用户的输入。 解答: value input("请输⼊⼀…

java 阿里云上传照片

获取对象 Resourceprivate ALiYunConfig aLiYunConfig;代码配置类 import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component;/*** 描述:** author zhaofeng* date 2023-09-05*/ D…

数字孪生技术如何提升工厂生产效率?

数字孪生技术是一项引领工业界数字化转型的创新力量。随着工业4.0时代的到来&#xff0c;制造业正经历着巨大的变革&#xff0c;数字孪生技术在这个变革中发挥了关键作用。它不仅仅是一种技术&#xff0c;更是一种理念&#xff0c;将现实世界与数字世界相结合&#xff0c;为工厂…

2022年3月13日安装和启动ActiveMQ遇到问题

今天我学习了一天周阳老师的ActiveMQ课程&#xff0c;讲的还不错&#xff0c;但是&#xff0c;碰到一个问题就是&#xff1a; 我Linux里可以通过 http://IP地址:8161/访问到ActiveMQ的一个界面&#xff0c;并且能够登录成功&#xff0c;但是呢&#xff0c;windows里面无法访问到…

python 串口发送图片给arduino

python 代码 import serial import threading import time from PIL import Image from PIL import ImageSequence## im.show()##print(img) ## ####img b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00…

单例模式有几种写法?

作者&#xff1a;猴子007 选自&#xff1a;https://monkeysayhi.github.io “你知道茴香豆的‘茴’字有几种写法吗&#xff1f;” 纠结单例模式有几种写法有用吗&#xff1f;有点用&#xff0c;面试中经常选择其中一种或几种写法作为话头&#xff0c;考查设计模式和coding styl…

springboot2.7.15+thymeleaf

如下使用了thymeleaf的基础应用&#xff1a;th:text, th:each, th:if, th:unless, th:value等标签的使用 页面效果&#xff1a;未登录状态 登录状态&#xff1a; 如下的所有html放在templates 下&#xff0c; 配置文件不需要做任何配置 只需要在pom.xml中增加 <dependen…