基于 RxJs 的前端数据层实践

2023-10-28

近来前端社区有越来越多的人开始关注前端数据层的设计。DaoCloud 也遇到了这方面的问题。我们调研了很多种解决方案,最终采用 RxJs 来设计一套数据层。这一想法并非我们的首创,社区里已有很多前辈、大牛分享过关于用 RxJs 设计数据层的构想和实践。站在巨人的肩膀上,才能走得更远。因此我们也打算把我们的经验公布给大家,也算是对社区的回馈吧。

作者简介


瞬光

DaoCloud 前端工程师

一名中文系毕业的非典型程序员。

我们遇到了什么困难

DaoCloud Enterprise(下文简称 DCE) 是 DaoCloud 的主要产品,它是一个应用云平台,也是一个非常复杂的单页面应用。它的复杂性主要体现在数据和交互逻辑两方面上。在数据方面,DCE 需要展示大量数据,数据之间依赖关系繁杂。在交互逻辑方面,DCE 中有着大量的交互操作,而且几乎每一个操作几乎都是牵一发而动全身。但是交互逻辑的复杂最终还是会表现为数据的复杂。因为每一次交互,本质上都是在处理数据。一开始的时候,为了保证数据的正确性,DCE 里写了很多处理数据、检测数据变化的代码,结果导致应用非常地卡顿,而且代码非常难以维护。

在整理了应用数据层的逻辑后,我们总结出了以下几个难点。本文会用较大的篇幅来描述我们所遇到的场景,这是因为如此复杂的前端场景比较少见,只有充分理解我们所遇到的场景,才能充分理解我们使用这一套设计的原因,以及这一套设计的优势所在。

>>>> 应用的难点

数据来源多

DCE 的获取数据的来源很多,主要有以下几种:

  1. 后端、 Docker 和 Kubernetes 的 API

    API 是数据的主要来源,应用、服务、容器、存储、租户等等信息都是通过 API 获取的。

  2. WebSocket

    后端通过 WebSocket 来通知应用等数据的状态的变化。

  3. LocalStorage

    保存用户信息、租户等信息。

  4. 用户操作

    用户操作最终也会反应为数据的变化,因此也是一个数据的来源。

数据来源多导致了两个问题:

  1. 复用处理数据的逻辑比较困难

    由于数据来源多,因此获取数据的逻辑常常分布在代码各处。比如说容器列表,展示它的时候我们需要一段代码来格式化容器列表。但是容器列表之后还会更新,由于更新的逻辑和获取的逻辑不一样,所以就很难再复用之前所使用的格式化代码。

  2. 获取数据的接口形式不统一

    如今我们调用 API 时,都会返回一个 Promise。但并不是所有的数据来源都能转换成 Promise,比如 WebSocket 怎么转换成 Promise 呢?结果就是在获取数据的时候,要先调用 API,然后再监听 WebSocket 的事件。或许还要同时再去监听用户的点击事件等等。等于说有多个数据源影响同一个数据,对每一个数据源都要分别写一套对应的逻辑,十分啰嗦。

聪明的读者可能会想到:只要把处理数据的逻辑和获取数据的逻辑解耦不就可以了吗?很好,现在我们有两个问题了。

数据复杂

DCE 数据的复杂主要体现在下面三个方面:

  1. 从后端获取的数据不能直接展示,要经过一系列复杂逻辑的格式化。

  2. 其中部分格式化逻辑还包括发送请求。

  3. 数据之间存在着复杂的依赖关系。所谓依赖关系是指,必须要有 B 数据才能格式化 A 数据。下图是 DCE 数据依赖关系的大体示意图。


以格式化应用列表为例,总共有这么几个步骤。读者不需要完全搞清楚,领会大意即可:

  1. 获取应用列表的数据

  2. 获取服务列表的数据。这是因为应用是由服务组成的,应用的状态取决于服务的状态,因此要格式化应用的状态,就必须获取服务列表的数据。

  3. 获取任务列表的数据。服务列表里其实也不包含服务的状态,服务的状态取决于服务的任务的状态,因此要格式化服务的状态,就必须获取任务列表的数据。

  4. 格式化任务列表。

  5. 根据服务的 id 从任务列表中找到服务所对应的任务,然后根据任务的状态,得出服务的状态。

  6. 格式化 服务列表。

  7. 根据应用的 id 从服务列表中找到应用所对应的服务,然后根据服务的状态,得出应用的状态。顺便还要把每个应用的服务的数据塞到每个应用里,因为之后还要用到。

  8. 格式化应用列表。

  9. 完成!

这其中掺杂了同步和异步的逻辑,非常繁琐,非常难以维护(肺腑之言)。况且,这还只是处理应用列表的逻辑,服务、容器、存储、网络等等列表需要获取呢,并且逻辑也不比应用列表简单。所以说,要想解耦获取和处理数据的逻辑并不容易。因为处理数据这件事本身,就包括了获取数据的逻辑。

如此复杂的依赖关系,经常会发送重复的请求。比如说我之前格式化应用列表的时候请求过服务列表了,下次要获取服务列表的时候又得再请求一次服务列表。

聪明的读者会想:我把数据缓存起来保管到一个地方,每次要格式化数据的时候,不要重新去请求依赖的数据,而是从缓存里读取数据,然后一股脑传给格式化函数,这样不就可以了吗?很好!现在我们有三个问题了!

数据更新困难

缓存是个很好的想法。但是在 DCE 里很难做,DCE 是一个对数据的实时性和一致性要求非常高的应用。

DCE 中几乎所有数据都是会被全局使用到的。比如说应用列表的数据,不仅要在应用列表中显示,侧边栏里也会显示应用的数量,还有很多下拉菜单里面也会出现它。所以如果一处数据更新了,另一处没更新,那就非常尴尬了。

还有就是之前提到的应用和服务的依赖关系。由于应用是依赖服务的,理论上来说服务变了,应用也是要变的,这个时候也要更新应用的缓存数据。但事实上,因为数据的依赖树实在是太深了(比如上图中的应用和主机),有些依赖关系不那么明显,结果就会忘记更新缓存,数据就会不一致。

什么时候要使用缓存、缓存保存在哪里、何时更新缓存,这些是都是非常棘手的问题。

聪明读者又会想:我用 redux 之类的库,弄个全局的状态树,各个组件使用全局的状态,这样不就能保证数据的一致了吗?这个想法很好的,但是会遇到上面两个难点的阻碍。redux 在面对复杂的异步逻辑时就无能为力了。

>>>> 结论

结果我们会发现这三个难点每个单独看起来都有办法可以解决,但是合在一起似乎就成了无解死循环。因此,在经过广泛调研之后,我们选择了 RxJs。


为什么 RxJs 可以解决我们的困难

在说明我们如何用 RxJs 解决上面三个难题之前,首先要说明 RxJs 的特性。毕竟 RxJs 目前还是个比较新的技术,大部分人可能还没有接触过,所以有必要给大家普及一下 RxJs。

  1. 统一了数据来源

    RxJs 最大的特点就是可以把所有的事件封装成一个 Observable,翻译过来就是可观察对象。只要订阅这个可观察对象,就可以获取到事件源所产生的所有事件。想象一下,所有的 DOM 事件、ajax 请求、WebSocket、数组等等数据,统统可以封装成同一种数据类型。这就意味着,对于有多个来源的数据,我们可以每个数据来源都包装成 Observable,统一给视图层去订阅,这样就抹平了数据源的差异,解决了第一个难题。

  2. 强大的异步同步处理能力

    RxJs 还提供了功能非常强大且复杂的操作符( Operator) 用来处理、组合 Observable,因此 RxJs 拥有十分强大的异步处理能力,几乎可以满足任何异步逻辑的需求,同步逻辑更不在话下。它也抹平了同步和异步之间的鸿沟,解决了第二个难题。

  3. 数据推送的机制把拉取的操作变成了推送的操作

    RxJs 传递数据的方式和传统的方式有很大不同,那就是改“拉取”为“推送”。原本一个组件如果需要请求数据,那它必须主动去发送请求才能获得数据,这称为“拉取”。如果像 WebSocket 那样被动地接受数据,这称为“推送”。如果这个数据只要请求一次,那么采用“拉取”的形式获取数据就没什么问题。但是如果这个数据之后需要更新,那么“拉取”就无能为力了,开发者不得不在代码里再写一段代码来处理更新。

    但是 RxJs 则不同。RxJs 的精髓在于推送数据。组件不需要写请求数据和更新数据的两套逻辑,只要订阅一次,就能得到现在和将来的数据。这一点改变了我们写代码的思路。我们在拿数据的时候,不是拿到了数据就万事大吉了,还需要考虑未来的数据何时获取、如何获取。如果不考虑这一点,就很难开发出具备实时性的应用。

    如此一来,就能更好地解耦视图层和数据层的逻辑。视图层从此不用再操心任何有关获取数据和更新数据的逻辑,只要从数据层订阅一次就可以获取到所有数据,从而可以只专注于视图层本身的逻辑。

  4. BehaviorSubject 可以缓存数据。

    BehaviorSubject 是一种特殊的 Observable。如果 BehaviorSubject 已经产生过一次数据,那么当它再一次被订阅的时候,就可以直接产生上次所缓存的数据。比起使用一个全局变量或属性来缓存数据,BehaviorSubject 的好处在于它本身也是 Observable,所以异步逻辑对于它来说根本不是问题。这样一来第三个难题也解决了。

这样一来三个问题是不是都没有了呢?不,这下其实我们有了四个问题。


我们是怎么用 RxJs 解决困难的

相信读者看到这里肯定是一脸懵逼。这就是第四个问题。RxJs 学习曲线非常陡峭,能参考的资料也很少。我们在开发的时候,甚至都不确定怎么做才是最佳实践,可以说是摸着石头过河。建议大家阅读下文之前先看一下 RxJs 的文档,不然接下来肯定十脸懵逼。

RxJs 真是太 TM 难啦!Observable、Subject、Scheduler 都是什么鬼啦!Operator 怎么有这么多啊!每个 Operator 后面只是加个 Map 怎么变化这么大啊!都是 map,为什么这个 map_.map 还不一样啦!文档还只有英文哒(现在有中文了)!我昨天还在写 jQuery,怎么一下子就要写这么难的东西啊啊啊!!!(划掉)

——来自实习生的吐槽

首先,给大家看一个整体的数据层的设计。熟悉单向数据流的读者应该不会觉得太陌生。


  1. 从 API 获取一些必须的数据

  2. 由事件分发器来分发事件

  3. 事件分发器触发控制各个数据管道

  4. 视图层拼接数据管道,获得用来展示的数据

  5. 视图层通过事件分发器来更新数据管道

  6. 形成闭环

可以看到,我们的数据层设计基本上是一个单向数据流,确切地说是“单向数据树”。

树的最上面是树根。树根会从各个 API 获得数据。树根的下面是树干。从树干分岔出一个个树枝。每个树枝的终点都是一个可以供视图层订阅的 BehaviorSubject,每个视图层组件可以按自己的需求来订阅各个数据。数据和数据之间也可以互相订阅。这样一来,当一个数据变化的时候,依赖它的数据也会跟着变化,最终将会反应到视图层上。

>>>> 设计详细说明

  1. root(树根)

    root 是树根。树根有许多触须,用来吸收养分。我们的 root 也差不多。一个应用总有一些数据是关键的数据,比如说认证信息、许可证信息、用户信息。要使用我们的应用,我们首先得知道你登录没登录,付没付过钱对不对?所以,这一部分数据是最底层数据,如果不先获取这些数据,其他的数据便无法获取。而这些数据一旦改变,整个应用其他的数据也会发生根本的变化。比方说,如果登录的用户改变了,整个应用展示的数据肯定也会大变样。

    在具体的实现中,root 通过 zip 操作符汇总所有的 api 的数据。为了方便理解,本文中的代码都有所简化,实际场景肯定远比这个复杂。

    // 从各个 API 获取数据
    const license$ = Rx.Observable.fromPromise(getLicense());
    const auth$ = Rx.Observable.fromPromise(getAuth());
    const systemInfo$ = Rx.Observable.fromPromise(getSystemInfo());
    // 通过 zip 拼接三个数据,当三个 API 全部返回时,root$ 将会发出这三个数据
    const root$ = Rx.Observable.zip(license$, auth$, systemInfo$);复制代码

    当所有必须的的数据都获取到了,就可以进入到树干的部分了。

  2. trunk(树干)

    trunk 是我们的树干,所有的数据都首先流到 trunk ,trunk 会根据数据的种类,来决定这个数据需要流到哪一个树枝中。简而言之,trunk 是一个事件分发器。所有事件首先都汇总到 trunk 中。然后由 trunk 根据事件的类型,来决定哪些数据需要更新。有点类似于 redux 中根据 action 来触发相应 reducer 的概念。

    之所以要有这么一个事件分发器,是因为 DCE 的数据都是牵一发而动全身的,一个事件发生时,往往需要触发多个数据的更新。此时有一个统一的地方来管理事件和数据之间的对应关系就会非常方便。一个统一的事件的入口,可以大大降低未来追踪数据更新过程的难度。

    在具体的实现中,trunk 是一个 Subject。因为 trunk 不但要订阅 WebSocket,同时还要允许视图层手动地发布一些事件。当有事件发生时,无论是 WebSocket 事件还是视图层发布的事件,经过 trunk 的处理后,我们都可以一视同仁。

    //一个产生 WebSocket 事件的 Observable
    const socket$ = Observable.webSocket('ws://localhost:8081');
    // trunk 是一个 Subject
    const trunk$ = new Rx.Subject()
     // 在 root 产生数据之前,trunk 不会发布任何值。trunk 之后的所有逻辑也都不会运行。
     .skipUntil(root$)
     // 把 WebSocket 推送过来的事件,合并到 trunk 中
     .merge(socket$)
     .map(event => {
       // 在实际开发过程中,trunk 可能会接受来自各种事件源的事件
       // 这些事件的数据格式可能会大不相同,所以一般在这里还需要一些格式化事件的数据格式的逻辑。
     });复制代码
  3. branch(树枝)

    trunk 的数据最终会流到各个 branch。branch 究竟是什么,下面就会提到。

    在具体的实现中,我们在 trunk 的基础上,用操作符对 trunk 所分发的事件进行过滤,从而创建出各个数据的 Observable,就像从树干中分出的树枝一样。

    // trunk 格式化好的事件的数据格式是一个数组,其中是需要更新的数据的名称
    // 这里通过 filter 操作符来过滤事件,给每个数据创建一个 Observable。相当于于从 trunk 分岔出多条树枝。
    // 比如说 trunk 发布了一个 ['app', 'services'] 的事件,那么 apps$ 和 services$ 就能得到通知
    const apps$ = trunk$.filter(events => events.includes('app'));
    const services$ = trunk$.filter(events => events.includes('service'));
    const containers$ = trunk$.filter(events => events.includes('container'));
    const nodes$ = trunk$.filter(events => events.includes('node'));复制代码

    仅仅如此,我们的 branch 还没有什么实质性的内容,它仅仅能接受到数据更新的通知而已,后面还需要加上具体的获取和处理数据的逻辑,下面就是一个容器列表的 branch 的例子。

    // containers$ 就是从 trunk 分出来的一个 branch。
    // 当 containers$ 收到来自 trunk 的通知的时候,containers$ 后面的逻辑就会开始执行
    containers$
     // 当收到通知后,首先调用 API 获取容器列表
     .switchMap(() => Rx.Observable.fromPromise(containerApi.list()))
     // 获取到容器列表后,对每个容器分别进行格式化。
     // 每个容器都是作为参数传递给格式化函数的。格式化函数中不包含任何异步的逻辑。
     .map(containers => containers.map(container, container => formatContainer(container)));复制代码

    现在我们就有了一个能够产生容器列表的数据的 containers$。我们只要订阅 containers$就可以获得最新的容器列表数据,并且当 trunk 发出更新通知的时候,数据还能够自动更新。这是巨大的进步。

    现在还有一个问题,那就是如何处理数据之间的依赖关系呢?比如说,格式化应用列表的时候假如需要格式化好的容器列表和服务列表应该怎么做呢?这个步骤在以前一直都十分麻烦,写出来的代码犹如意大利面。因为这个步骤需要处理不少的异步和同步逻辑,这其中的顺序还不能出错,否则可能就会因为关键数据还没有拿到导致格式化时报错。

    实际上,我们可以把 branch 想象成一个“管道”,或者“”。这两个概念都不是新东西,大家应该比较熟悉。

    We should have some ways of connecting programs like garden hose—screw in another segment when it becomes necessary to massage data in another way.

    ——Douglas McIlroy

    如果数据是以管道的形式存在的,那么当一个数据需要另一个数据的时候,只要把管道接起来不就可以了吗?幸运的是,借助 RxJs 的 Operator,我们可以非常轻松地拼接数据管道。下面就是一个应用列表拼接容器列表的例子。

    // apps$ 也是从 trunk 分出来的一个 branch
    apps$
     // 同样也从 API 获取数据
     .switchMap(() => Rx.Observable.fromPromise(appApi.list()))
     // 这里使用 combineLatest 操作符来把容器列表和服务列表的数据拼接到应用列表中
     // 当容器或服务的数据更新时,combineLatest 之后的代码也会执行,应用的数据也能得到更新。
     .combineLatest(containers$, services$)
       // 把这三个数据一起作为参数传递给格式化函数。
       // 注意,格式化函数中还是没有任何异步逻辑,因为需要异步获取的数据已经在上面的 combineLatest 操作符中得到了。
     .map(([apps, containers, services]) => apps.map(app => formatApp(app, containers, services)));复制代码
  4. 格式化函数

    格式化函数就是上文中的 formatAppformatContainer。它没有什么特别的,和 RxJs 没什么关系。

    唯一值得一提的是,以前我们的格式化函数中充斥着异步逻辑,很难维护。所以在用 RxJs 设计数据层的时候我们刻意地保证了格式化函数中没有任何异步逻辑。即使有的格式化步骤需要异步获取数据,也是在 branch 中通过数据管道的拼接获取,再以参数的形式统一传递给格式化函数。这么做的目的就是为了将异步和同步解耦,毕竟异步的逻辑由 RxJs 处理更加合适,也更便于理解。

  5. fruit

    现在我们只差缓存没有做了。虽然我们现在只要订阅 apps$containers$ 就能获取到相应的数据,但是前提是 trunk 必需要发布事件才行。这是因为 trunk 是一个 Subject,假如 trunk 不发布事件,那么所有订阅者都获取不到数据。所以,我们必须要把 branch 吐出来的数据缓存起来。 RxJs 中的 BehaviorSubject 就非常适合承担这个任务。

    BehaviorSubject 可以缓存每次产生的数据。当有新的订阅者订阅它时,它就会立刻提供最近一次所产生的数据,这就是我们要的缓存功能。所以对于每个 branch,还需要用 BehaviorSubject 包装一下。数据层最终对外暴露的接口实际上是 BehaviorSubject,视图层所订阅的也是 BehaviorSubject。在我们的设计中,BehaviorSubject 叫作 fruit,这些经过层层格式化的数据,就好像果实一样。

    具体的实现并不复杂,下面是一个容器列表的例子。

    // 每个数据流对外暴露的一个借口是 BehaviorSubject,我们在变量末尾用$$,表示这是一个BehaviorSubject
    const containers$$ = new Rx.BehaviorSubject();
    // 用 BehaviorSubject 去订阅 containers$ 这个 branch
    // 这样 BehaviorSubject 就能缓存最新的容器列表数据,同时当有新数据的时它也能产生新的数据
    containers$.subscribe(containers$$);复制代码
  6. 视图层

    整个数据层到上面为止就完成了,但是在我们用视图层对接数据层的时候,也走了一些弯路。一般情况下,我们只需要用 vue-rx 所提供的 subscriptions 来订阅 fruit 就可以了。

    <template>
     <app-list :data="apps"></app-list>
    </template>
    
    <script>
    import app$$ from '../branch/app.branch';
    
    export default {
     name: 'app',
     subscriptions: {
       apps: app$$,
     },
    };
    </script>复制代码

    但有些时候,有些页面的数据很复杂,需要进一步处理数据。遇到这种情况,那就要考虑两点。一是这个数据是否在别的页面或组件中也要用,如果是的话,那么就应该考虑把它做进数据层中。如果不是的话,那其实可以考虑在页面中单独再创建一个 Observable,然后用 vue-rx 去订阅这个 Observable。

    还有一个问题就是,假如视图层需要更新数据怎么办?之前已经提到过,整个数据层的事件分发是由 trunk 来管理的。因此,视图层如果想要更新数据,也必须取道 trunk。这样一来,数据层和视图层就形成了一个闭环。视图层根本不用担心数据怎么处理,只要向数据层发布一个事件就能全部搞定。

    methods: {
     updateApp(app) {
       appApi.update(app)
         .then(() => {
           trunk$.next(['app'])
         })
     },
    },复制代码

下面是整个数据层设计的全貌,供大家参考。


总结

之后的开发过程证明,这一套数据层很大程度上解决了我们的问题。它最大的好处在于提高了代码的可维护性,从而使得开发效率大大提高,bug 也大大减少。

我们对 RxJs 的实践也是刚刚开始,这一套设计肯定还有很多可改进的地方。如果大家对本文有什么疑惑或建议,可以写邮件给 bowen.tan@daocloud.io,还望大家不吝赐教。

点击了解 DaoCloud Enterprise


本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

基于 RxJs 的前端数据层实践 的相关文章

  • 前端基础:回顾es6相关知识

    Author note 题记 ECMAscript is international standard of javascript ECMA 是 js的国际标准版语言 let and const 为什么之前用var现在需要用let cons
  • 【信道估计】【MIMO】【FBMC】未来移动通信的滤波器组多载波调制方案(Matlab代码实现)

    欢迎来到本博客 博主优势 博客内容尽量做到思维缜密 逻辑清晰 为了方便读者 座右铭 行百里者 半于九十 本文目录如下 目录 1 概述 2 运行结果 3 参考文献 4 Matlab代码及文章
  • 【网安神器篇】——WPScan漏洞扫描工具

    目录 一 Wordpress简介 二 WPScan介绍 三 安装 四 获取token 1 注册账号 2 拿到token 五 使用教程 1 常用选项 2 组合命令 1 模糊扫描 2 指定扫描用户 3 插件漏洞扫描 4 主题漏洞扫描 5 Tim
  • 基于java的web仓库管理系统设计与实现

    基于java的web仓库管理系统设计与实现 I 引言 A 研究背景和动机 基于Java的Web仓库管理系统是近年来快速发展的领域之一 它提供了丰富的功能 如数据存储 数据检索 数据分析和数据可视化等 本文将重点介绍基于Java的Web仓库管
  • 软件测试|web自动化测试神器playwright教程(三十八)

    简介 在我们使用selenium时 我们可以获取元素的属性 元素的文本值 以及输入框的内容等 作为比selenium更为强大的web自动化测试神器 playwright也可以实现对元素属性 文本值和输入框内容的抓取 并且实现比seleniu
  • 探索Web开发的未来——使用KendoReact服务器组件

    Kendo UI 是带有jQuery Angular React和Vue库的JavaScript UI组件的最终集合 无论选择哪种JavaScript框架 都可以快速构建高性能响应式Web应用程序 通过可自定义的UI组件 Kendo UI可
  • 点击存储到固定时间清除存储

    这段代码 无意间想到的 随便写了下来 运行 根据点击之后传递一个参数 将他存入本地存储 方便测试为10秒 10秒后触发下一个事件 清除本地存储 结束
  • 低代码配置-属性配置面板设计

    模块设计 tab项切换 组件基础属性 组件数据属性 组件事件属性 表单属性 模块输出函数设计 tab切换函数 列表表单属性 数据来源 调用接口时一次赋予 无需使用selectItem 如需使用 归入基础属性 列表标题 是否展示筛选区域
  • 新手也能看懂的【前端自动化测试入门】!

    前言 最近在网上搜索前端自动化测试相关的文档 但是发现网上的文章都是偏使用 没有把一些基础概念说清楚 导致后续一口气遇到一些 karma Jasmine jest Mocha Chai BDD 等词汇的时候很容易一头雾水 这次一方面整理一下
  • JVM优化之 -Xss -Xms -Xmx -Xmn 参数设置

    JVM优化之 Xss Xms Xmx Xmn 参数设置 XmnXmsXmxXss有什么区别 Xmn Xms Xmx Xss都是JVM对内存的配置参数 我们可以根据不同需要区修改这些参数 以达到运行程序的最好效果 Xms 堆内存的初始大小 默
  • vue实现 marquee(走马灯)

    样式 代码 div class marquee prompt div class list prompt span class prompt item span div div data return listPrompt xxx xxxx
  • SpringBoot中整合MybatisPlus快速实现Mysql增删改查和条件构造器

    场景 Mybatis Plus 简称MP 是一个Mybatis的增强工具 只是在Mybatis的基础上做了增强却不做改变 MyBatis Plus支持所有Mybatis原生的特性 所以引入Mybatis Plus不会对现有的Mybatis构
  • 网络安全(黑客)自学启蒙

    一 什么是网络安全 网络安全是一种综合性的概念 涵盖了保护计算机系统 网络基础设施和数据免受未经授权的访问 攻击 损害或盗窃的一系列措施和技术 经常听到的 红队 渗透测试 等就是研究攻击技术 而 蓝队 安全运营 安全运维 则研究防御技术 作
  • 考虑光伏出力利用率的电动汽车充电站能量调度策略研究(Matlab代码实现)

    欢迎来到本博客 博主优势 博客内容尽量做到思维缜密 逻辑清晰 为了方便读者 座右铭 行百里者 半于九十 本文目录如下 目录 1 概述 2 运行结果 3 参考文献 4 Matlab代码 数据
  • 考虑光伏出力利用率的电动汽车充电站能量调度策略研究(Matlab代码实现)

    欢迎来到本博客 博主优势 博客内容尽量做到思维缜密 逻辑清晰 为了方便读者 座右铭 行百里者 半于九十 本文目录如下 目录 1 概述 2 运行结果 3 参考文献 4 Matlab代码 数据
  • 【无标题】

    大家都知道该赛项的规程和样题向来都是模棱两可 从来不说具体的内容 导致选手在备赛时没有头绪 不知道该怎么训练 到了赛时发现题目和备赛的时候完全不一样 那么本文将以往年信息安全管理与评估赛项经验来解读今年2023年国赛的规程 帮助选手们指明方
  • 每天10个前端小知识 <Day 14>

    前端面试基础知识题 1 CSSOM树和DOM树是同时解析的吗 浏览器会下载HTML解析页面生成DOM树 遇到CSS标签就开始解析CSS 这个过程不会阻塞 但是如果遇到了JS脚本 此时假如CSSOM还没有构建完 需要等待CSSOM构建完 再去
  • ESP10B 锁定连接器

    ESP10B 锁定连接器 ESP10B 电机新增内容包括双极型号标准 NEMA 尺寸 17 23 和 34 的步进电机现在包括输出扭矩范围从 61 盎司英寸到 1291 盎司英寸的双极型号 该电机配有带锁定连接器的尾缆 可轻松连接 每转可步
  • 【js学习之路】遍历数组api之 `filter `和 `map`的区别

    一 前言 数组是我们在项目中经常使用的数据类型 今天我们主要简述作用于遍历数组的api filter 和 map 的区别 二 filter和map的共同点 首先 我们主要阐述一下 filter 和 map 的共同点 api的参数都是回调函数
  • 网工内推 | 上市公司同程、科达,五险一金,年终奖,最高12k*15薪

    01 同程旅行 招聘岗位 网络工程师 职责描述 1 负责职场 门店网络规划 建设 维护 2 负责网络安全及访问控制 上网行为管理和VPN设备的日常运维 3 负责内部相关网络自动化和系统化建设 4 优化与提升网络运行质量 制定应急预案 人员培

随机推荐

  • 关于UE4 vs2017 SpawnActor编译通过,调试运行崩溃的问题

    在制作VR模式代码编写的时候 使用一些API采用UWorld SpawnActor的时候出现代码编译编译通过无报错 但是调试运行失败的原因 找了很久才找到原因 原来是构造器的问题 就是把SpawnActor放到到BeginPlay 中 不能
  • oracle全文索引之commit与DML操作

    我们知道 无论对多大的数据做DML操作 执行commit都可以很快完成 但如何删除建有全文索引的记录 在commit时可能会很慢 根据推断可以知道是由于域索引造成的 那么在有域索引的情况下 commit时 oracle还做了那些额外工作呢
  • chatgpt赋能python:Python求全排列的介绍

    Python求全排列的介绍 在计算机科学中 全排列是一种排列的形式 将一组元素按照固定的顺序安排 在Python中 可以使用递归和迭代来求解全排列问题 本文将介绍如何用Python求全排列以及如何在SEO方面优化文章 递归方法 递归方法是通
  • 新形势下,企业如何进行数字化转型 附下载地址

    2020年谈企业数字化转型 有一个不变和四个变 不变的是企业面临的整体宏观环境和企业 多年发展积累的运营模式和管理能力 因此企业数字化转型面临的固有难点依然存在 四个 变化因素是疫情影响 5G部署 人工智能 AI 加快应用 以及中美技术加速
  • springboot中 拦截器无法访问数据库解决方法

    在springboot中使用拦截器时 拦截器中还需要访问数据库 会出现实例化数据库访问对象失败的现象 不管是添加 Componse还是添加 Servie 或者 Configuration 均不可以 需要做如下处理 方法如下 1 在集成Web
  • yolov5环境搭建与pytorch中torch、torchvision、torchaudio安装

    python软件安装 可以不单独安装 2条消息 Python安装教程 2022最新 学Python的阿杜的博客 CSDN博客 安装anaconda3 2020 2对应的python版本为3 7 不推荐 2020 11对应的python版本为
  • 数据库MySql python读取插入数据,insert对那些类型加单引号,表单自己参考自己(外键),空值和NULL

    今天做了下数据库作业 好多出错 记录下 查漏补缺 本次只是实现一个employee表单 特殊在有一个外键参考自身主键 并且老师给的数据该外键可为null 表结构直接用workbench 里面构造的 Navicat还没解决不修改密码策略的条件
  • Python中常用的运算符

    1 算数运算符 最常见的 标准算数运算符 加减乘除 取余运算符 幂运算符 2 赋值运算符 3 比较运算符 4 布尔运算符 5 位运算符 1 算数运算符 2 赋值运算符 3 比较运算符 对变量或表达式的结果进行大小 真假等比较 一个 称为赋值
  • python怎么用print打出赋值_python print 输出赋值到变量

    In 52 import io In 53 row ACME 50 91 5 In 54 join row TypeError Traceback most recent call last in gt 1 join row TypeErr
  • 在 SQL 里描述数据分布情况的时候,有 Cardinality 和 Selectivity 两个概念,有什么区别?

    What is the difference between cardinality and selectivity In SQL cardinality refers to the number of unique values in p
  • 分享一波粉丝面试真题-主要是偏管理方面的

    怎么改善团队低迷现状 改善团队低迷的现状是一个重要的管理挑战 以下是一些可能有助于改善团队状态的方法 深入了解问题 首先 需要了解低迷的原因 这可能涉及与团队成员的个人会谈 收集反馈 观察工作流程等 明确问题的性质对于采取适当的措施至关重要
  • 使用python批量将svg转换成PNG

    CairoSVG介绍 CairoSVG是一个将SVG转为PNG PDF PS格式的库 当前版本的CairoSVG至少需要Python 3 5以上版本 CairoSVG安装和使用 pip install cairosvg 通过命令行你就可以使
  • 数据结构课设:学生信息管理系统(完整版)

    系统介绍 学生信息管理系统是针对学校人事处的大量业务处理工作而开发的管理软件 主要用于学校学生信息管理 总体任务是实现学生信息关系的系统化 科学化 规范化和自动化 其主要任务是用计算机对学生各种信息进行日常管理 如查询 修改 增加 删除 另
  • HTTP协议和web工作原理

    HTTP协议 是web学习的核心 学东东切忌只学配置 不学原理 只学会框架有什么用 要会自己写框架 web学习直接关系到J2EE的学习一 HTTP 超文本传输协议 人类之所发展得如此快 就是因为有自己的语言 1 所谓超文本 即纯文本语言 不
  • 使用git push太慢怎么办

    使用git push太慢怎么办 修改host文件 windows 的路径应该在 C Windows System32 drivers etc hosts 在host文件的最后一行加上 151 101 72 249 github global
  • 现代密码学之安全多方计算

    Secure Multi Party Computation 什么是Secure Multiparty Computation 安全定义 Ideal real model Oblivious transfer OT 1 out of 2 s
  • 拓撲排序學習

    确定比赛名次 Problem Description 有N个比赛队 1 lt N lt 500 编号依次为1 2 3 N进行比赛 比赛结束后 裁判委员会要将所有参赛队伍从前往后依次排名 但现在裁判委员会不能直接获得每个队的比赛成绩 只知道每
  • connection ftp refused_ftp: connect: Connection refused 解决方法

    redhat下使用ftp出现connection refused报错 通过查询是端口未开启的缘故 以下为解决方法 root localhost vsftpd usr sbin vsftpd 500 OOPS vsftpd does not
  • Flutter 版本更新 和 dart SDK版本跟新

    The current Dart SDK version is 2 19 5 Because pivot chat requires SDK version gt 3 0 0 lt 4 0 0 version solving failed
  • 基于 RxJs 的前端数据层实践

    近来前端社区有越来越多的人开始关注前端数据层的设计 DaoCloud 也遇到了这方面的问题 我们调研了很多种解决方案 最终采用 RxJs 来设计一套数据层 这一想法并非我们的首创 社区里已有很多前辈 大牛分享过关于用 RxJs 设计数据层的