Flutter鸿蒙化中的Plugin

Flutter鸿蒙化中的Plugin

  • 前言
  • 鸿蒙项目内Plugin
    • Flutter端实现
    • 鸿蒙端实现
      • 创建Plugin的插件类
      • 注册Plugin
  • 开发纯Dart的package
  • 为现有插件项目添加ohos平台支持
    • 创建插件
    • 配置插件
    • 编写插件内容
  • 参考资料

前言

大家知道Flutter和鸿蒙通信方式和Flutter和其他平台通信方式都是一样的, 都是使用Platform Channel API来通信。

那么鸿蒙中这些通信的代码是写在哪里? 如何编写的了?
下面我们简单的学习下。

鸿蒙项目内Plugin

在我们开发App的过程中,可能有这样的需求:
在鸿蒙平台上特有的,并且需要调用鸿蒙原生的API来完成的。那么我们可以在在ohos平台上创建一个Plugin的方式来支持这个功能。

示例的通信方式使用:MethodChannel的方式。

Flutter端实现

// flutter端创建一个MethodChannel的通道,通道名称必须和鸿蒙指定, 如果创建的名称不一致,会导致无法通信
final channel = const MethodChannel("com.test.channel");
// flutter给鸿蒙端发送消息
channel.invokeMapMethod("testData");

鸿蒙端实现

创建Plugin的插件类

首先我们需要创建一个插件类, 继承自FlutterPlugin类, 并实现其中的方法

export default class TestPlugin implements FlutterPlugin {
// 通道private channel?: MethodChannel;
//获取唯一的类名 类似安卓的Class<? extends FlutterPlugin ts无法实现只能用户自定义getUniqueClassName(): string {return 'TestPlugin'}// 当插件从engine上分离的时候调用onDetachedFromEngine(binding: FlutterPluginBinding): void {this.channel?.setMethodCallHandler(null);}// 当插件挂载到engine上的时候调用onAttachedToEngine(binding: FlutterPluginBinding): void {this.channel = new MethodChannel(binding.getBinaryMessenger(), "com.test.channel");//  给通道设置回调监听 this.channel.setMethodCallHandler({onMethodCall(call: MethodCall, result: MethodResult) {switch (call.method) {case "testData":console.log(`接收到flutter传递过来的参shu ===================`)break;default:result.notImplemented();break;}}})}
}

注册Plugin

我们创建完Plugin了, 我们还需要再EntryAbility中去注册我们的插件

export default class EntryAbility extends FlutterAbility {configureFlutterEngine(flutterEngine: FlutterEngine) {super.configureFlutterEngine(flutterEngine)this.addPlugin(new TestPlugin());}
}

完成上述两步之后,我们就可以使用这个专属鸿蒙的插件来完成鸿蒙上特有的功能了。

开发纯Dart的package

我们知道,flutter_flutter的仓库对于纯Dart开发的package是完全支持的, 对于纯Dart的package我们主要关注Dart的版本支持。

开发纯Dart的命令:flutter create --template=package hello

对于具体如何开发纯Dart的package,Flutter官方已经讲的非常详细, 开发,集成详细 可以参考官方文档。Flutter中的Package开发

为现有插件项目添加ohos平台支持

在我们开发Flutter项目适配鸿蒙平台的时候,会有些插件还没有适配ohos平台, 这个时候我们等华为适配, 或者我们自己下载插件的源码, 然后我们自己在源码中编写适配ohos平台的代码

下面以image_picker插件为示例,来学习下如何为已有插件项目添加ohos的平台支持

创建插件

首先我们需要下载image_picker源码, 然后使用Android studio打开flutter项目。 可以查看到项目的结构

然后通过命令行进入到项目根目录, 执行命令:flutter create . --template=plugin --platforms=ohos

执行完后的目录结构如下:
在这里插入图片描述

配置插件

我们创建完ohos平台的插件之后, 我们需要再Plugin工程的pubspec.yaml配置文件中配置ohos平台的插件。
在这里插入图片描述
当我们配置完成之后, 我们接下来就可以开始编写ohos平台插件相关内容了。

编写插件内容

在我们编写ohos平台的插件内容时, 我们首先需要知道这个插件是通过什么通道, 调用什么方法来和个个平台通信的。 ohos平台的通道名称、调用方法尽量和原来保持一致,有助于理解。

// 执行flutter指令创建plugin插件时, 会自动创建这个类
export default class ImagesPickerPlugin implements FlutterPlugin, MethodCallHandler, AbilityAware {private channel: MethodChannel | null = null;private pluginBinding: FlutterPluginBinding | null = null;// 当前处理代理对象private delegate: ImagePickerDelegate | null = nullconstructor() {}getUniqueClassName(): string {return "ImagesPickerPlugin"}onAttachedToEngine(binding: FlutterPluginBinding): void {// 后续用到的页面context,都是需要重binding对象中获取, 如果你直接this.getcontext 等方法获取, 可能不是页面的contextthis.pluginBinding = binding;this.channel = new MethodChannel(binding.getBinaryMessenger(), "chavesgu/images_picker");this.channel.setMethodCallHandler(this)}onDetachedFromEngine(binding: FlutterPluginBinding): void {this.pluginBinding = null;if (this.channel != null) {this.channel.setMethodCallHandler(null)}}//  插件挂载到ablitity上的时候onAttachedToAbility(binding: AbilityPluginBinding): void {if (!this.pluginBinding) {return}this.delegate = new ImagePickerDelegate(binding.getAbility().context, this.pluginBinding.getApplicationContext());}onDetachedFromAbility() {}onMethodCall(call: MethodCall, result: MethodResult): void {if (call.method === "pick") {// 解析参数let count = call.argument("count") as number;// let language = call.argument("language") as string;let pickType = call.argument("pickType") as string;// let supportGif = call.argument("gif") as boolean;// let maxTime = call.argument("maxTime") as number;let maxSize = call.argument("maxSize") as number;if (this.delegate !== null) {this.delegate.pick(count, pickType, maxSize, result)}} else if (call.method === "saveImageToAlbum" || call.method === "saveVideoToAlbum") {// 保存图片let filePath = call.argument("path") as string; // 图片路径if (this.delegate !== null) {this.delegate.saveImageOrVideo(filePath, call.method === "saveImageToAlbum", result)}} else if (call.method === "openCamera") {let pickType = call.argument("pickType") as string;let maxSize = call.argument("maxSize") as number;if (this.delegate !== null) {this.delegate.openCamear(pickType, maxSize, result)}} else {result.notImplemented()}}
}

注意:这个插件内开发代码是没有代码提示的, 也不会自动检车报错, 只有你运行测试demo时, 编译时才会报错,所以建议大家把插件的功能在一个demo中完成,在把代码拷贝过来。

逻辑实现代码:

import ArrayList from '@ohos.util.ArrayList';
import common from '@ohos.app.ability.common';
import photoAccessHelper from '@ohos.file.photoAccessHelper';
import { BusinessError } from '@kit.BasicServicesKit';
import picker from '@ohos.multimedia.cameraPicker';
import camera from '@ohos.multimedia.camera';
import dataSharePredicates from '@ohos.data.dataSharePredicates';
import { fileUri } from '@kit.CoreFileKit';
import FileUtils from './FileUtils'
import fs from '@ohos.file.fs';
import {MethodResult,
} from '@ohos/flutter_ohos';
import abilityAccessCtrl, { PermissionRequestResult } from '@ohos.abilityAccessCtrl';export default class ImagePickerDelegate {// 当前UIAblitity的contextprivate context: common.Context// 插件绑定的contextprivate bindContext: common.Context// 构造方法constructor(context: common.Context, bindContext: common.Context) {this.context = contextthis.bindContext = bindContext}// 选择相册图片和视频pick(count: number, pickType: string, maxSize: number, callback: MethodResult) {// 创建一个选择配置const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();// 媒体选择类型let mineType: photoAccessHelper.PhotoViewMIMETypes = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;if (pickType === "PickType.all") {mineType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_VIDEO_TYPE;} else if (pickType === "PickType.video") {mineType = photoAccessHelper.PhotoViewMIMETypes.VIDEO_TYPE;}photoSelectOptions.MIMEType = mineTypephotoSelectOptions.maxSelectNumber = count; // 选择媒体文件的最大数目let uris: Array<string> = [];const photoViewPicker = new photoAccessHelper.PhotoViewPicker();// 通过photoViewPicker对象来打开相册图片photoViewPicker.select(photoSelectOptions).then((photoSelectResult: photoAccessHelper.PhotoSelectResult) => {uris = photoSelectResult.photoUris;console.info('photoViewPicker.select to file succeed and uris are:' + uris);this.hanlderSelectResult(uris, callback)}).catch((err: BusinessError) => {console.error(`Invoke photoViewPicker.select failed, code is ${err.code}, message is ${err.message}`);})}// 处理打开相机照相/录制async openCamear(type: string, maxSize: number, callback: MethodResult) {// 定义一个媒体类型数组let mediaTypes: Array<picker.PickerMediaType> = [picker.PickerMediaType.PHOTO];if (type === "PickType.all") {mediaTypes = [picker.PickerMediaType.PHOTO, picker.PickerMediaType.VIDEO]} else if (type === "PickType.video") {mediaTypes = [picker.PickerMediaType.VIDEO]}try {let pickerProfile: picker.PickerProfile = {cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK};let pickerResult: picker.PickerResult = await picker.pick(this.context,mediaTypes, pickerProfile);console.log("the pick pickerResult is:" + JSON.stringify(pickerResult));// 获取uri的路径和媒体类型let resultUri = pickerResult["resultUri"] as stringlet mediaTypeTemp = pickerResult["mediaType"] as string// 需要把uri转换成沙河路径let realPath = FileUtils.getPathFromUri(this.bindContext, resultUri);if (mediaTypeTemp === "video") {// 需要获取缩略图callback.success([{thumbPath: realPath, path: realPath, size: maxSize}])} else {// 图片无需设置缩略图callback.success([{thumbPath: realPath, path: realPath, size: maxSize}])}} catch (error) {let err = error as BusinessError;console.error(`the pick call failed. error code: ${err.code}`);}}// 处理保存图片async saveImageOrVideo(path: string, isImage: boolean, callback: MethodResult) {let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();atManager.requestPermissionsFromUser(this.context,['ohos.permission.WRITE_IMAGEVIDEO', 'ohos.permission.READ_IMAGEVIDEO'],async (err: BusinessError, data: PermissionRequestResult) => {if (err) {console.log(`requestPermissionsFromUser fail, err->${JSON.stringify(err)}`);} else {console.info('data:' + JSON.stringify(data));console.info('data permissions:' + data.permissions);console.info('data authResults:' + data.authResults);//转换成urilet uriTemp = fileUri.getUriFromPath(path);//打开文件let fileTemp = fs.openSync(uriTemp, fs.OpenMode.READ_ONLY);//读取文件大小let info = fs.statSync(fileTemp.fd);//缓存照片数据let bufferImg: ArrayBuffer = new ArrayBuffer(info.size);//写入缓存fs.readSync(fileTemp.fd, bufferImg);//关闭文件流fs.closeSync(fileTemp);let phHelper = photoAccessHelper.getPhotoAccessHelper(this.context);try {const uritemp = await phHelper.createAsset(isImage ? photoAccessHelper.PhotoType.IMAGE :photoAccessHelper.PhotoType.VIDEO, isImage ? 'jpg' : "mp4"); // 指定待创建的文件类型、后缀和创建选项,创建图片或视频资源const file = await fs.open(uritemp, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);await fs.write(file.fd, bufferImg);await fs.close(file.fd);callback.success(true);} catch (error) {console.error(`error=========${JSON.stringify(error)}`)callback.success(false);}}});}// 处理选中结果hanlderSelectResult(uris: Array<string>, callback: MethodResult) {// 定义一个path数组let pathList: ArrayList<string> = new ArrayList();for (let path of uris) {// if (path.search("video") < 0) {//   path = await this.getResizedImagePath(path, this.pendingCallState.imageOptions);// }this.getVideoThumbnail(path)let realPath = FileUtils.getPathFromUri(this.bindContext, path);pathList.add(realPath);}let uriModels: UriModel[] = [];pathList.forEach(element => {uriModels.push({thumbPath: element,path: element,size: 500})});callback.success(uriModels)}// 获取视频的缩略图async getVideoThumbnail(uri: string) {//建立视频检索条件,用于获取视频console.log("开始获取缩略图==========")let predicates: dataSharePredicates.DataSharePredicates = new dataSharePredicates.DataSharePredicates();predicates.equalTo(photoAccessHelper.PhotoKeys.URI, uri);let fetchOption: photoAccessHelper.FetchOptions = {fetchColumns: [],predicates: predicates};// let size: image.Size = { width: 720, height: 720 };let phelper = photoAccessHelper.getPhotoAccessHelper(this.context)let fetchResult: photoAccessHelper.FetchResult<photoAccessHelper.PhotoAsset> = await phelper.getAssets(fetchOption);console.log(`fetchResult=========${JSON.stringify(fetchResult)}`)let asset = await fetchResult.getFirstObject();console.info('asset displayName = ', asset.displayName);asset.getThumbnail().then((pixelMap) => {console.info('getThumbnail successful ' + pixelMap);}).catch((err: BusinessError) => {console.error(`getThumbnail fail with error: ${err.code}, ${err.message}`);});}
}// 定义一个返回的对象
interface UriModel {thumbPath: string;path: string;size: number;
}

工具类代码 :

/** Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd.* Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with the License.* You may obtain a copy of the License at**     http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.*/
import common from '@ohos.app.ability.common';
import fs from '@ohos.file.fs';
import util from '@ohos.util';
import Log from '@ohos/flutter_ohos/src/main/ets/util/Log';const TAG = "FileUtils";export default class FileUtils {static getPathFromUri(context: common.Context | null, uri: string, defExtension?: string) {Log.i(TAG, "getPathFromUri : " + uri);let inputFile: fs.File;try {inputFile = fs.openSync(uri);} catch (err) {Log.e(TAG, "open uri file failed err:" + err)return null;}if (inputFile == null) {return null;}const uuid = util.generateRandomUUID();if (!context) {return}{const targetDirectoryPath = context.cacheDir + "/" + uuid;try {fs.mkdirSync(targetDirectoryPath);let targetDir = fs.openSync(targetDirectoryPath);Log.i(TAG, "mkdirSync success targetDirectoryPath:" + targetDirectoryPath + " fd: " + targetDir.fd);fs.closeSync(targetDir);} catch (err) {Log.e(TAG, "mkdirSync failed err:" + err);return null;}const inputFilePath = uri.substring(uri.lastIndexOf("/") + 1);const inputFilePathSplits = inputFilePath.split(".");Log.i(TAG, "getPathFromUri inputFilePath: " + inputFilePath);const outputFileName = inputFilePathSplits[0];let extension: string;if (inputFilePathSplits.length == 2) {extension = "." + inputFilePathSplits[1];} else {if (defExtension) {extension = defExtension;} else {extension = ".jpg";}}const outputFilePath = targetDirectoryPath + "/" + outputFileName + extension;const outputFile = fs.openSync(outputFilePath, fs.OpenMode.CREATE);try {Log.i(TAG, "copyFileSync inputFile fd:" + inputFile.fd + " outputFile fd:" + outputFile.fd);fs.copyFileSync(inputFile.fd, outputFilePath);} catch (err) {Log.e(TAG, "copyFileSync failed err:" + err);return null;} finally {fs.closeSync(inputFile);fs.closeSync(outputFile);}return outputFilePath;}}
}

编写完上述代码就可以运行example工程去测试相关功能了。 当测试完成之后 , 我们可以把整个源码工程拷贝到flutter工程中, 通过集成本地package的方式来集成这个package。或者你可以在发一个新的pacage到pub.dev上, 然后在按照原有方式集成即可。

参考资料

Flutter官方的package开发和使用
开发Plugin

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

探索JavaScript前端开发:开启交互之门的神奇钥匙(二)

目录 引言 四、事件处理 4.1 事件类型 4.2 事件监听器 五、实战案例&#xff1a;打造简易待办事项列表 5.1 HTML 结构搭建 5.2 JavaScript 功能实现 六、进阶拓展&#xff1a;异步编程与 Ajax 6.1 异步编程概念 6.2 Ajax 原理与使用 七、前沿框架&#xff1a;Vue.js …

DeepSeek-R1:性能对标 OpenAI,开源助力 AI 生态发展

DeepSeek-R1&#xff1a;性能对标 OpenAI&#xff0c;开源助力 AI 生态发展 在人工智能领域&#xff0c;大模型的竞争一直备受关注。最近&#xff0c;DeepSeek 团队发布了 DeepSeek-R1 模型&#xff0c;并开源了模型权重&#xff0c;这一举动无疑为 AI 领域带来了新的活力。今…

假期day1

第一天&#xff1a;请使用消息队列实现2个终端之间互相聊天 singal1.c #include <stdio.h>#include <string.h>#include <unistd.h>#include <stdlib.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include &l…

go-zero框架基本配置和错误码封装

文章目录 加载配置信息配置 env加载.env文件配置servicecontext 查询数据生成model文件执行查询操作 错误码封装配置拦截器错误码封装 接上一篇&#xff1a;《go-zero框架快速入门》 加载配置信息 配置 env 在项目根目录下新增 .env 文件&#xff0c;可以配置当前读取哪个环…

考研机试:买房子

描述 某程序员开始工作&#xff0c;年薪 N万&#xff0c;他希望在中关村公馆买一套 60平米的房子&#xff0c;现在价格是 200 万&#xff0c;假设房子价格以每年百分之 K 增长&#xff0c;并且该程序员未来年薪不变&#xff0c;且不吃不喝&#xff0c;不用交税&#xff0c;每年…

Ansible fetch模块详解:轻松从远程主机抓取文件

在自动化运维的过程中&#xff0c;我们经常需要从远程主机下载文件到本地&#xff0c;以便进行分析或备份。Ansible的fetch模块正是为了满足这一需求而设计的&#xff0c;它可以帮助我们轻松地从远程主机获取文件&#xff0c;并将其保存到本地指定的位置。在这篇文章中&#xf…

前端开发中的模拟后端与MVVM架构实践[特殊字符][特殊字符][特殊字符]

平时&#xff0c;后端可能不能及时给接口给前端进行数据调用和读取。这时候&#xff0c;前端想到进行模拟后端接口。本文将介绍如何通过vite-plugin-mock插件模拟后端接口&#xff0c;并探讨MVVM架构在前端开发中的应用。此外&#xff0c;我们还将讨论Vue2与Vue3的区别&#xf…

JAVA毕业设计210—基于Java+Springboot+vue3的中国历史文化街区管理系统(源代码+数据库)

毕设所有选题&#xff1a; https://blog.csdn.net/2303_76227485/article/details/131104075 基于JavaSpringbootvue3的中国历史文化街区管理系统(源代码数据库)210 一、系统介绍 本项目前后端分离(可以改为ssm版本)&#xff0c;分为用户、工作人员、管理员三种角色 1、用户…

docker的前世今生

docker来自哪里&#xff1f; 从我们运维部署的历史来看&#xff0c;宿主机从最初的物理机到虚拟机&#xff0c;再到docker&#xff0c;一步步演进到现在。技术演进其实是为了解决当前技术的痛点&#xff0c;那我们来看看有哪些痛点以及如何克服痛点的。 物理机 一般来说&…

电脑办公技巧之如何在 Word 文档中添加文字或图片水印

Microsoft Word是全球最广泛使用的文字处理软件之一&#xff0c;它为用户提供了丰富的编辑功能来美化和保护文档。其中&#xff0c;“水印”是一种特别有用的功能&#xff0c;它可以用于标识文档状态&#xff08;如“草稿”或“机密”&#xff09;、公司标志或是版权信息等。本…

【机器学习案列】探索各因素对睡眠时间影响的回归分析

&#x1f9d1; 博主简介&#xff1a;曾任某智慧城市类企业算法总监&#xff0c;目前在美国市场的物流公司从事高级算法工程师一职&#xff0c;深耕人工智能领域&#xff0c;精通python数据挖掘、可视化、机器学习等&#xff0c;发表过AI相关的专利并多次在AI类比赛中获奖。CSDN…

2024年度总结

迟来的2024年度总结&#xff0c;本文主要包括创作经历的回顾、个人成长与突破、以及职业与生活的平衡。 文章目录 1、 创作经历回顾2、 成长回顾3、 职业与生活的平衡4、 展望未来 1、 创作经历回顾 从高中开始就喜欢给别人解答疑问&#xff0c;大学学习模电、数电时&#xff…

vim在命令模式下的查找功能

/ab 从上往下 n 下一个 N 上一个 示例&#xff1a; 在命令模式下直接点击键盘上的/就可以进行查找&#xff0c;比如我要查找a&#xff0c;输入a后再回车&#xff0c;就可以检索出文件中所有和a有关的内容。 ?ab 从下往上 N 下一个 n 上一个 示例&#xff1a;和上图相同…

机器学习-使用梯度下降最小化均方误差

前面有一篇文章《机器学习-常用的三种梯度下降法》&#xff0c;这篇文章中对于均方误差的求偏导是错误的&#xff0c;为了澄清这个问题&#xff0c;我再写一篇文章来纠正一下&#xff0c;避免误导大家。 一、批量梯度下降法 我们用 批量梯度下降法 来求解一个简单的 线性回归…

基于quartz,刷新定时器的cron表达式

文章目录 前言基于quartz&#xff0c;刷新定时器的cron表达式1. 先看一下测试效果2. 实现代码 前言 如果您觉得有用的话&#xff0c;记得给博主点个赞&#xff0c;评论&#xff0c;收藏一键三连啊&#xff0c;写作不易啊^ _ ^。   而且听说点赞的人每天的运气都不会太差&…

LabVIEW智能胎压监测

汽车行车安全是社会关注焦点&#xff0c;轮胎压力异常易引发交通事故&#xff0c;开发胎压监测系统可保障行车安全、降低事故发生率。 系统组成与特点 &#xff08;一&#xff09;硬件组成 BMP - 280 气体压力传感器&#xff1a;高精度、稳定性好、能耗低&#xff0c;适合车载…

C语言教程——文件处理(1)

目录 前言 二、什么是文件 2.1文件的概念 2.2程序文件 2.3数据文件 2.4文件名 2.5二进制文件和文本文件 三、文件操作 3.1文件指针 3.2文件的打开与关闭 四、文件的顺序读写 4.1fgetc 4.2fputc 4.3fputs 4.4fgets 总结 前言 我们知道电脑上有许许多多的文件&a…

【橘子ES】Kibana的分析能力Analytics简易分析

一、kibana是啥&#xff0c;能干嘛 我们经常会用es来实现一些关于检索&#xff0c;关于分析的业务。但是es本身并没有UI,我们只能通过调用api来完成一些能力。而kibana就是他的一个外置UI&#xff0c;你完全可以这么理解。 当我们进入kibana的主页的时候你可以看到这样的布局。…

c#的tabControl控件实现自定义标签颜色

最近项目需要自定义tabControl控件颜色&#xff0c;而默认这个控件是不支持自定义标签颜色的&#xff0c;于是想办法实现了这个功能&#xff0c;效果如下图所示&#xff1a; 直接上代码&#xff1a; using System; using System.Collections.Generic; using System.ComponentM…

从零到一:Spring Boot 与 RocketMQ 的完美集成指南

1.Rocket的概念与原理 RocketMQ 是一款由阿里巴巴开源的分布式消息中间件&#xff0c;最初用于支持阿里巴巴的海量业务。它基于发布-订阅模型&#xff0c;具备高吞吐、低延迟、高可用和强一致性的特点&#xff0c;适用于消息队列、大规模数据流处理等场景。以下是对 RocketMQ …