Flutter 的缓存策略

news2024/11/25 21:34:48

Flutter 的缓存策略

alt

原文 https://medium.com/@romaingreaume/implementing-a-cache-strategy-in-your-flutter-app-5db3e316e7c9

前言

在移动应用程序中,缓存管理是一件非常重要的事情。

在本文中,我将告诉您如何在我的公司 Beapp 中设置策略缓存。

alt

正文

W 怎么了?

如果你读了这篇文章,我想你知道缓存是什么,但是以防万一..。

缓存基本上是将数据存储在设备的存储器中。

W 为什么使用缓存?

  • 如果用户连接不好或者没有互联网
  • 限制 API 调用,特别是对于不需要经常刷新的数据
  • 存储敏感数据(我们稍后讨论)

一张图片胜过千言万语:

alt

Cache Strategy Scheme

缓存策略计划

如您所见,缓存的主要用途是始终尝试向用户显示数据。

关于敏感数据,出于以下原因,我将用户缓存与网络缓存分离:

  • 网络缓存比用户缓存更短暂。
  • 相反,用户缓存存储敏感数据,如访问令牌、刷新令牌,这些数据必须是安全的,用户不能访问。
  • 更具体地说,刷新令牌的有效期可能很长(长达几个月) ,而经典数据可能在一小时后刷新,这将导致不必要的 API 调用。

因此,将这些策略分离开来是一种很好的做法,即使它们可以被合并。

现在我们了解了什么是缓存,让我们深入研究代码吧!

H 如何建立这些策略?

文件树如下所示:

-- lib

----- core

------- cache

--------- storage

--------- strategy

在子文件夹存储中,我们创建了一个文件 Storage.dart,其中包含一个抽象类 Storage

这个类是一个“契约 contrac”,我们在其中声明操作数据的方法。

abstract class Storage {
  Future<void> write(String key, String value);

  Future<String?> read(String key);

  Future<void> delete(String key);

  Future<int> count({String? prefix});

  Future<void> clear({String? prefix});
}

正如我所说,我们将通过我们的应用程序操纵它们,但为此,我们需要在设备中存储它们的方法。

我们使用 Hive 包,它是一个基于键/值的存储解决方案。

总而言之,Hive 在设备的存储中创建了一个文件夹,您可以在其中存储一个 hiveBox,其中包含 key: value 数据。

我们可以很容易地通过它的名字进入这个盒子。

现在我们可以从 Storage 抽象类中实现这些方法。

class CacheStorage implements Storage {
  static const _hiveBoxName = "cache";

  CacheStorage()  {
    Hive.initFlutter() ;
  }

  @override
  Future<void> clear({String? prefix}) async {
    final box = await Hive.openBox(_hiveBoxName);
    if (prefix == null) {
      await box.clear() ;
    } else {
      for (var key in box.keys) {
        if (key is String && key.startsWith(prefix)) {
          await box.delete(key);
        }
      }
    }
  }

  @override
  Future<void> delete(String key) async {
    final box = await Hive.openBox(_hiveBoxName);
    return box.delete(key);
  }

  @override
  Future<String?> read(String key) async {
    final box = await Hive.openBox(_hiveBoxName);
    return box.get(key);
  }

  @override
  Future<void> write(String key, String value) async {
    final box = await Hive.openBox(_hiveBoxName);
    return box.put(key, value);
  }

  @override
  Future<int> count({String? prefix}) async {
    final box = await Hive.openBox(_hiveBoxName);
    if (prefix == null) {
      return box.length;
    } else {
      var count = 0;
      for (var key in box.keys) {
        if (key is String && key.startsWith(prefix)) {
          count++;
        }
      }
      return count;
    }
  }
}

原则很简单:

  • 我们在创建 CacheStorage 时创建一个 hive 实例。
  • 每次我们操作数据时,我们将打开我们的 Hive 框(使用它的名称)并执行触发的方法(获取、写入、删除...)。
  • 我们可以很容易地通过它的键来访问数据值。

现在我们已经有了操作数据的方法,我们可以设置不同的策略,使用统一的调用语法来适应应用程序中的不同用例。

我们开始创建一个契约缓存_策略。缓存根中的 Dart 。该合同允许我们应用其中一种策略并对其进行配置。

import 'dart:convert';

import 'package:flutter/foundation.dart';

import 'cache_manager.dart';
import 'cache_wrapper.dart';
import 'storage/storage.dart';

abstract class CacheStrategy {
  static const defaultTTLValue = 60 * 60 * 1000;

  Future _storeCacheData<T>(String key, T value, Storage storage) async {
    final cacheWrapper = CacheWrapper<T>(value, DateTime.now() .millisecondsSinceEpoch);
    await storage.write(key, jsonEncode(cacheWrapper.toJsonObject() ));
  }

  _isValid<T>(CacheWrapper<T> cacheWrapper, bool keepExpiredCache, int ttlValue) => keepExpiredCache || DateTime.now() .millisecondsSinceEpoch < cacheWrapper.cachedDate + ttlValue;

  Future<T> invokeAsync<T>(AsyncBloc<T> asyncBloc, String key, Storage storage) async {
    final asyncData = await asyncBloc() ;
    _storeCacheData(key, asyncData, storage);
    return asyncData;
  }

  Future<T?> fetchCacheData<T>(String key, SerializerBloc serializerBloc, Storage storage, {bool keepExpiredCache = falseint ttlValue = defaultTTLValue}) async {
    final value = await storage.read(key);
    if (value != null) {
      final cacheWrapper = CacheWrapper.fromJson(jsonDecode(value));
      if (_isValid(cacheWrapper, keepExpiredCache, ttlValue)) {
        if (kDebugMode) print("Fetch cache data for key $key${cacheWrapper.data}");
        return serializerBloc(cacheWrapper.data);
      }
    }
    return null;
  }

  Future<T?> applyStrategy<T>(AsyncBloc<T> asyncBloc, String key, SerializerBloc serializerBloc, int ttlValue, Storage storage);
}
  • DefaultTTLValue 是存储在缓存中的数据的实时值。换句话说: 在这段时间之后,数据被认为是无效的。 -_storeCacheData() 通过 CacheWrapper 允许存储数据,我们将在后面看到它。 -_isValid() 与 defaultTTLValue 相比,检查缓存获取是否仍然有效
  • InvkeAsync() 将使用作为参数传递的 syncBloc 方法从远程位置(通常来自 Web 服务)获取数据,并存储和返回检索到的数据。
  • FetchCacheData() 将通过 key 参数从缓存中获取数据,转换 Cache Wrapper 接收到的 JSON 来检查它是否仍然有效,如果有效,则返回具有相应类型的 Dart 对象中的序列化数据,这要感谢 seralizerBloc。
  • ApplicyStrategy() 将执行要选择的策略,其中包含所需的所有参数。

通过这些解释,我们可以看到任何战略的实施路径:

  • 我们调用 applicyStrategy() 来指出我们想要应用哪个策略,以及所需的参数。
  • 要检查缓存的数据 fetchCacheData() ,该方法使用_isValid() 检查有效性并返回数据或 null。
  • 为了从 WS 获取数据,我们触发了 invekAsync() ,一旦接收到数据,就将它们与_storeCacheData() 一起放到 cache 中。
class CacheWrapper<T{
  final T data;
  final int cachedDate;

  CacheWrapper(this.data, this.cachedDate);

  CacheWrapper.fromJson(json)
      : cachedDate = json['cachedDate'],
        data = json['data'];

  Map toJson()  => {'cachedDate': cachedDate, 'data': data};

  @override
  String toString()  => "CacheWrapper{cachedDate=$cachedDate, data=$data}";
}

关于 CacheWrapper,您可以在根缓存文件夹中创建一个文件 cache_wrapper. dart。

正如其名称所示,CacheWrapper 是一个允许包装接收数据的类。它有两个参数,一个是允许包装任何类型数据的通用类型数据,另一个是在数据存储在缓存中的日期和时间自动设置的 cachedDate。

From JSON() 和 toJson() 方法将接收到的数据转换为用于缓存的 JSON 或者在代码中使用它的 Map。

因此,可以将 CacheWrapper 解释为包含缓存数据并允许对这些数据进行编码/解码的“包装器”。

在本文的这个步骤中,我们的结构文件夹如下所示:

-- lib

----- core

------- cache

--------- storage

----------- storage.dart

----------- cache_storage.dart

--------- cache_strategy.dart

现在我们已经看到了我们的策略可以做什么的定义,让我们深入研究它们的实现。

在缓存根目录中的新策略文件夹中,我们将创建所有策略的文件。

每个策略都是单例的,所以应用程序中每个策略只有一个实例。

我们可以使用 get_it 来注入我们的策略,但是这增加了对包的依赖以及我们所知道的第三方的所有缺点,所以我们自己创建了它们。

每个策略都将继承自抽象的 CacheStrategy 类,它们将分别使用 applicyStrategy() 方法实现各自的策略。

AsyncOrCache

这个策略将首先调用端点来检索数据。如果抛出错误(出于各种原因: 错误 401,403,500...) ,我们将检索存储在设备缓存中的最后数据。如果缓存中没有任何内容或无效数据,我们将返回先前引发的错误,以便在状态管理器中处理它(稍后将看到它)。

class AsyncOrCacheStrategy extends CacheStrategy {
  static final AsyncOrCacheStrategy _instance = AsyncOrCacheStrategy._internal() ;

  factory AsyncOrCacheStrategy()  {
    return _instance;
  }

  AsyncOrCacheStrategy._internal() ;

  @override
  Future<T?> applyStrategy<T>(AsyncBloc<T> asyncBloc, String key, SerializerBloc<T> serializerBloc, int ttlValue, Storage storage) async => await invokeAsync(asyncBloc, key, storage).onError(
        (RestException restError, stackTrace) async {
          if (restError.code == 403 || restError.code == 404) {
            storage.clear(prefix: key);
            return Future.error(restError);
          } else {
            return await fetchCacheData(key, serializerBloc, storage, ttlValue: ttlValue) ?? Future.error(restError);
          }
        },
      );
}

CacheOrAsync

最后一个策略和前一个一样,只是反过来而已。首先,我们检查数据是否存储在缓存中,如果结果为 null,则触发 WS 调用。如果抛出错误,我们在状态管理器中处理它。

class CacheOrAsyncStrategy extends CacheStrategy {
  static final CacheOrAsyncStrategy _instance = CacheOrAsyncStrategy._internal() ;

  factory CacheOrAsyncStrategy()  {
    return _instance;
  }

  CacheOrAsyncStrategy._internal() ;

  @override
  Future<T?> applyStrategy<T>(AsyncBloc<T> asyncBloc, String key, SerializerBloc<T> serializerBloc, int ttlValue, Storage storage) async =>
      await fetchCacheData(key, serializerBloc, storage, ttlValue: ttlValue) ?? await invokeAsync(asyncBloc, key, storage);
}

只是同步

此策略调用 Web 服务来获取数据。

class JustAsyncStrategy extends CacheStrategy {
  static final JustAsyncStrategy _instance = JustAsyncStrategy._internal() ;

  factory JustAsyncStrategy()  {
    return _instance;
  }

  JustAsyncStrategy._internal() ;

  @override
  Future<T?> applyStrategy<T>(AsyncBloc<T> asyncBloc, String key, SerializerBloc<T> serializerBloc, int ttlValue, Storage storage) async => await invokeAsync(asyncBloc, key, storage);
}

JustCache

class JustCacheStrategy extends CacheStrategy {
  static final JustCacheStrategy _instance = JustCacheStrategy._internal() ;

  factory JustCacheStrategy()  {
    return _instance;
  }

  JustCacheStrategy._internal() ;
  @override
  Future<T?> applyStrategy<T>(AsyncBloc<T> asyncBloc, String key, SerializerBloc<T> serializerBloc, int ttlValue, Storage storage) async =>
      await fetchCacheData(key, serializerBloc, storage, ttlValue: ttlValue);
}

此策略仅使用存储在设备缓存中的数据。缺点是如果应用程序找不到数据,则返回 null。

对于最后两种策略,它们可以直接由对缓存或网络的直接调用来替代,但是这里我们保留了一种统一的调用方式。

现在我们已经看到了不同的策略,让我们使用它们!

在根缓存文件夹中,我们创建一个 cache_manager.dart 文件。

这个文件将包含构建缓存策略的所有逻辑。它将被直接注入到我们的代码中(稍后我将回到这一点)。

import 'cache_strategy.dart';
import 'storage/cache_storage.dart';

typedef AsyncBloc<T> = Function;
typedef SerializerBloc<T> = Function(dynamic);

class CacheManager {
  final CacheStorage cacheStorage;

  CacheManager(
    this.cacheStorage,
  );

  String? defaultSessionName;

  StrategyBuilder from<T>(String key) => StrategyBuilder<T>(key, cacheStorage).withSession(defaultSessionName);

  Future clear({String? prefix}) async {
    if (defaultSessionName != null && prefix != null) {
      await cacheStorage.clear(prefix: "${defaultSessionName}_$prefix");
    } else if (prefix != null) {
      await cacheStorage.clear(prefix: prefix);
    } else if (defaultSessionName != null) {
      await cacheStorage.clear(prefix: defaultSessionName);
    } else {
      await cacheStorage.clear() ;
    }
  }
}

class StrategyBuilder<T{
  final String _key;
  final CacheStorage _cacheStorage;

  StrategyBuilder(this._key, this._cacheStorage);

  late AsyncBloc<T> _asyncBloc;
  late SerializerBloc<T> _serializerBloc;
  late CacheStrategy _strategy;
  int _ttlValue = CacheStrategy.defaultTTLValue;
  String? _sessionName;

  StrategyBuilder withAsync(AsyncBloc<T> asyncBloc) {
    _asyncBloc = asyncBloc;
    return this;
  }

  StrategyBuilder withStrategy(CacheStrategy strategyType) {
    _strategy = strategyType;
    return this;
  }

  StrategyBuilder withTtl(int ttlValue) {
    _ttlValue = ttlValue;
    return this;
  }

  StrategyBuilder withSession(String? sessionName) {
    _sessionName = sessionName;
    return this;
  }

  StrategyBuilder withSerializer(SerializerBloc serializerBloc) {
    _serializerBloc = serializerBloc;
    return this;
  }

  String buildSessionKey(String key) => _sessionName != null ? "${_sessionName}_$key" : key;

  Future<T?> execute()  async {
    try {
      return await _strategy.applyStrategy<T?>(_asyncBloc, buildSessionKey(_key), _serializerBloc, _ttlValue, _cacheStorage);
    } catch (exception) {
      rethrow;
    }
  }
}

让我解释一下这个文件:

→ 它分为两个类: CacheManager 和 Strategies yBuilder

→ CacheManager 使用 from() 方法保存入口点。Strategies yBuilder 拥有其他一些方法,这些方法允许我们通过一些参数(如异步函数、序列化器等)来构建缓存会话。.

  • DefaultSessionName 允许我们将一个全局名称放到将要打开的缓存会话中。例如,如果我们为每个登录的用户创建一个缓存会话,我们可以将用户的 firstName + lastName + id 设置为 defaultSessionName,这样我们就可以使用这个名称轻松地操作整个缓存会话。
  • From() : 该方法创建一个通用类型 < T > 的 Strategies yBuilder 实例,该实例允许返回任何类型: List、 String、 Object... 一个键参数被传递,它将在 buildSessionKey() 方法中用于 hive 框的名称。AcheStorage 实例也作为参数传递,以便 Strategies yBuilder 可以使用它并将其传递给 CacheStrategy。最后,Strategies yBuilder 的 withSession() 方法用于命名当前缓存会话。
  • Clear() : 允许以不同方式清除缓存。我们可以使用 Strategy Builder 的 defaultSessionName 或前缀参数清理缓存会话,或者清理创建的所有缓存。

一旦调用 from() 方法,就轮到调用 Strategies yBuilder 方法了:

  • With Async() : 我们为构建器提供 AsyncBloc < T > 函数,构建器将从远程源(比如 API)获取数据。
  • WithSerializer() : 我们为构建器提供序列化器/反序列化器,它负责将接收到的 JSON 数据转换为 dart 对象,反之亦然,使用 SerializerBloc < T > 函数。

由于 Dart 中的默认序列化/反序列化没有针对复杂对象进行优化,因此 Flutter 建议使用一个包(json_seralizable)。它将为每个 DTO 自动生成方法,然后将这些方法直接注入 seralizerBloc,用于序列化从缓存接收的数据。

  • WithTtl() : 为缓存提供生存时间,默认情况下我们将其设置为 1 小时。
  • WithStrategy() : 接收所选择的策略单例。直接注入一个单例模式允许定制/添加不同的策略,例如,它比枚举更灵活。
  • Execute() : 后一个方法触发 applicyStrategy() 方法来执行缓存策略。

H 如何使用这种策略?

现在我们已经了解了这个理论,让我们来看看在应用程序中实现缓存策略的实际情况。

我向你保证,这是最简单的部分。

首先,我们需要注入我们创建的 CacheManager。为了做到这一点,我们使用 get_it 包,它将使用依赖注入来创建一个可以在整个代码库中使用的单例模式。

我建议您在应用程序的核心文件夹中创建一个 service_locator. dart 文件。

final getIt = GetIt.instance;

void setupGetIt()  {
  // Cache
  getIt.registerSingleton<CacheManager>(CacheManager(CacheStorage() ));

}

因此,我们使用 CacheManager 来管理策略并保存 CacheStorage 实例用于存储。

这个 setupGetIt() 方法将在 app root starter 中触发,以注入 CacheManager 单实例。

当我们尝试在 简洁项目架构 clean architecture 中工作时,我们的故障看起来是这样的:

-- data

----- datasource

----- domain

----- dto

----- repository

我们最感兴趣的是存储库文件夹,因为它在从数据源接收输入 dto 时充当网关,将其转换为来自域的实体。

让我们以一个应用程序为例,它将显示学生要完成的工作。 我们需要一个方法来检索分配。

class HomeworkAssignmentRepository {

  final apiProvider = getIt<HomeworkDataSource>() ;
  final _cacheManager = getIt<CacheManager>() ;

  Future<List<HomeworkEntity>?> getHomeworkAssignment(String courseId, String studentId) async {
    final List<HomeworkDto>? result = await _cacheManager
        .from<List<HomeworkDto>>("homework-assignment-$courseId-$studentId")
        .withSerializer((result) => HomeworkDto.fromJson(result))
        .withAsync(()  => apiProvider.fetchHomeworkAssignment(courseId, studentId))
        .withStrategy(AsyncOrCache() )
        .execute() ;

    if (result != null) {
      return List<HomeworkEntity>.from(result.map((dto) => dto.toEntity() ));
    }
    return null;

  }
}

首先,我们将我们的 HomeworkDataSource 和 CacheManager 注入 get_it。

数据源将用于调用端点,管理器用于配置策略。

在将来的 getHomeworkAsmission 中,我们希望得到一个 HomeworkD 的列表,它将在 HomeworkEntity 中被转换。我们看到我们的战略得到了应用,我们解释道:

  • From() 设置将使用哪个 dto 并给出缓存的密钥。
  • WithSerializer() 注入将反序列化数据的方法。
  • WithAsync() 注入带有必要参数的 API 调用。
  • WithStrategy() 允许定义要选择的策略。
  • Execute() 将通过将定义的参数发送给 Strategies yBuilder 来触发我们的策略。

现在,有了这个配置,我们的策略将首先触发一个 API 调用来从服务器检索数据。如果调用引发错误,则该策略将尝试从缓存中检索数据,最后,它将向 UI 返回数据(无论是否为新鲜数据)或 null。

结束语

如果本文对你有帮助,请转发让更多的朋友阅读。

也许这个操作只要你 3 秒钟,对我来说是一个激励,感谢。

祝你有一个美好的一天~


© 猫哥

  • 微信 ducafecat

  • https://wiki.ducafecat.tech

  • https://video.ducafecat.tech

本文由 mdnice 多平台发布

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

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

相关文章

ES-分词器

简介 分词器是es中的一个组件&#xff0c;通俗意义上理解&#xff0c;就是将一段文本按照一定的逻辑&#xff0c;分析成多个词语&#xff0c;同时对这些词语进行常规化的一种工具&#xff1b;ES会将text格式的字段按照分词器进行分词&#xff0c;并编排成倒排索引&#xff0c;…

Vben Admin 的介绍以及使用

Vben Admin 的介绍以及使用初识 Vben Admin一、安装vue-vben-admin二、vben admin 目录介绍1.运行项目好用的插件启动项目打开终端三 项目分析1. 路由配置新增路由四 : vben admin 的使用初识 Vben Admin Vben Admin官方文档地址 好像官网挂了 !! 嘤嘤嘤嘤嘤 因为最近业务需要…

蓝牙无线自制串口模块连接穿越机配置工具

蓝牙无线自制串口模块连接穿越机配置工具1. 目的2. 验证环境3. BLE SPP验证4. BT SPP验证5. 参考资料穿越机配置工具 SpeedyBeeBetaFlightConfigurator 目前&#xff0c;市面上连接BetaFlight给飞控固件调参比较顺手的工具大体上是官网的BetaFlight Configurator(国内都叫BF地…

OFD转PDF ~java实现

OFD转PDF ~java实现 当你在查这个问题时&#xff0c;说明大家都已经接触到OFD这个新的版式文档了。OFD格式对标的PDF&#xff0c;目前关于PDF的转换都比较成熟&#xff0c;但是当OFD出现后&#xff0c;切发现能实现转换却似乎有所困难。尝试网上搜了一下这方面的资料&#xff…

别再当大冤种了,揭开3D建模报班6个常见套路

1⃣承诺有就业保障 任何机构给你承诺✊所谓的包就业都是耍流氓&#x1f621;&#xff0c;请不要信❗️ 承诺就业保障的机构无非3种情况&#xff1a; &#x1f539;赚取学员的信任感&#xff0c;吸引学员报名和交钱&#xff1b; &#x1f538;提高机构的“专业度”和所谓的“…

JWT详解(文章内嵌jwt工具类)

JWT 基础概念详解&#xff0c;工具类和使用方法放在最后 什么是 JWT? JWT &#xff08;JSON Web Token&#xff09; 是目前最流行的跨域认证解决方案&#xff0c;是一种基于 Token 的认证授权机制。 从 JWT 的全称可以看出&#xff0c;JWT 本身也是 Token&#xff0c;一种规…

【现代密码学原理】——消息认证码(学习笔记)

&#x1f4d6; 前言&#xff1a;消息认证码 MAC&#xff08;Message Authentication Code&#xff09;是经过特定算法后产生的一小段信息&#xff0c;检查某段消息的完整性&#xff0c;以及作身份验证。它可以用来检查在消息传递过程中&#xff0c;其内容是否被更改过&#xff…

Appium 移动端自动化测试(Mac)

目录 adb调试工具&#xff08;Android Debug Bridge&#xff09; adb常用命令 Appium使用 快速体验步骤 Appium常用方法 UIAutomatorViewer 获取元素 元素等待&#xff08;在指定时间内一直等待 元素操作 Swipe 滑动和拖拽事件&#xff08;Scroll、drag_and_drop&#…

pytorch深度学习实战19

第十九课 卷积层的填充和步幅 目录 理论部分 实践部分 理论部分 首先看一下卷积层的填充。 上图所示的情况会有个问题&#xff0c;如果卷积核不变的话&#xff08;一直是5*5的卷积核&#xff09;&#xff0c;那么我的网络最多也就只能弄到第七层了&#xff0c;如果我想搭建更…

恒太照明在北交所上市:募资规模缩水三成,第三季度收入下滑

11月17日&#xff0c;江苏恒太照明股份有限公司&#xff08;下称“恒太照明”&#xff0c;NQ:873339&#xff09;在北京证券交易所&#xff08;即“北交所”&#xff09;上市。本次上市&#xff0c;恒太照明的发行价格为6.28元/股&#xff0c;发行数量为2220万股&#xff0c;募…

Linux|centos7下部署安装alertmanager并实现邮箱和微信告警

前言&#xff1a; 一个成熟的符合一般人预想的资源监控平台应该是能够多维度的展示各种各样的数据&#xff0c;比如&#xff0c;服务器的内存使用率&#xff0c;磁盘io状态&#xff0c;磁盘使用率&#xff0c;CPU的负载状态&#xff0c;某个服务的状态&#xff08;比如&#x…

Pandas数据分析33——数据多条件筛选(点估计和区间估计评价指标)

本次是写论文代码区间估计评价指标有感..... 数据框有两列的时候&#xff0c;我们想筛选A列大于B列的样本出来时&#xff0c;只需要用布尔值索引就行&#xff1a; df[df[A]>df[B]] 可是多条件的筛选的时候怎么办&#xff0c;比如我需要A大于B列&#xff0c;还有A小于C列。…

软件测试“摆烂”已经成为常态化,我们应该怎样冲出重围?

网络日新月异发展的今天&#xff0c;每隔一段时间就会出现一些新的网络热词&#xff0c;最近“摆烂”成功突出重围&#xff0c;成为大家热议的中心。什么是“摆烂”&#xff1f;“摆烂”就是事情无法向好发展的时候直接选择妥协&#xff0c;不采取任何措施加以改变&#xff0c;…

兼容 信创鲲鹏/M1 arm64架构的kafka镜像

当前热度比较高的kafka镜像是wurstmeister/kafka&#xff0c;在dockerhub上有很多的使用次数。我起初很开心最新版支持arm64架构&#xff0c;然后拉到本地用Mac M1跑也很完美 但是&#xff01;我放到信创鲲鹏的生产环境&#xff0c;导入镜像没问题&#xff0c;但一用docker-com…

IPWorks Zip Delphi 流式压缩组件

IPWorks Zip Delphi 流式压缩组件 IPWorks Zip允许开发人员使用Zip、Tar、Gzip、7-Zip、Bzip2、ZCompress或Jar压缩标准轻松地将压缩和解压缩集成到应用程序中。IPWorks Zip组件是从头开始构建的&#xff0c;将卓越的速度与出色的数据压缩比相结合。 IPWorks Zip功能 文件压…

Allegro如何给铜皮导弧操作详解

Allegro如何给铜皮导弧操作详解 当需要给如下铜皮导弧的,是一件比较麻烦的事情,但是可以用以下两个方法来实现 方法一 具体操作如下 shape-decompose shape Find选择shapes Options选择层面,比如top层,选择delete shape after decompose 框选铜皮 得到下图效果,然后…

SNMP 协议解析(一)

♥️作者&#xff1a;小刘在C站 ♥️每天分享课堂笔记&#xff0c;一起努力&#xff0c;共赴美好人生&#xff01; ♥️夕阳下&#xff0c;是最美的&#xff0c;绽放。 一.SNMP是什么 是基于TCP/IP协议族的网络管理标准&#xff0c;它的前身是简单网关监控协议(SGMP)&#xf…

HOST前后端分离小操作

“本地测试项目配置域名小操作” ​ 相关文章【欢迎关注该公众号“一路向东”】&#xff08;CORS处理跨域问题&#xff09;&#xff1a; CORS跨域问题配置白名单CORS_ORIGIN_WHITELIST HOSTS 本地测试域名必然少不了修改&#xff1a;C:/Windows/System32/driver/etc/host文件&…

MVVM的构建(javakotlin)

概念性的描述就不写了&#xff0c;直接上代码 MVVM框架&#xff0c;主要是构建基类&#xff0c;实现ViewModel绑定&#xff0c;ViewBinding的绑定&#xff0c;在构建好基类后&#xff0c;后续开发中新建activity或fragment的viewModel和viewBinding就会在基类的方法中实现绑定…

Python virtualenv工具设置虚拟环境和VS code调试Python

Python virtualenv工具设置虚拟环境和VS code调试Python1. Window环境下采用VS code调试Python和虚拟环境1.1 安装Python1.2 安装虚拟环境工具virtualenv1.3 Windows上使用虚拟环境的基本操作1.4 Windows上VS code使用虚拟环境2. Linux环境下采用VS code调试Python和虚拟环境2.…