vue3.0 根据富文本html页面生成压缩包(含视频在线地址、图片在线地址、前端截图、前端文档)
vue3.0生成压缩包(含在线地址、前端截图、前端文档)
- 需求描述
- 效果
- 开始
- 下载插件包
- 基本代码构造
- 点击下载按钮
- 1.截图content元素,并转化为pdf
- canvas putImageData、getImageData
- getImageData 获取指定矩形区域的像素信息
- putImageData 将这些数据放回画布,从而实现对画布像素的编辑
- 2.提取富文本视频
- 正则 str.match(regex) regex.exec(str)知识补充
- 3.base64和在线地址转blob
- 4.下载成压缩包代码
- 全部代码
需求描述
- 内容区为富文本html渲染的内容
- 要求点击下载后 需要有以下文件
- 1.当前内容的页面,即渲染内容截图,且需要将截图转化成pdf
- 2.提取html内容区的视频,单独下载
- 3.后端返回的附件地址,下载附件文档
- 4.再将以上文件总结成压缩包
效果
开始
下载插件包
- html2canvas 截图
npm install html2canvas --save
//或
yarn add html2canvas
- jspd 生成pdf插件
npm install jspdf --save
//或
yarn add jspdf
- jszip压缩文件
npm install jszip --save
//或
yarn add jszip
- 保存文件的JavaScript库
npm install file-saver --save
//或
yarn add file-saver
- 一起安装
npm i file-saver jszip html2canvas jspdf --save
基本代码构造
<div v-html="contentValue" ></div> //用于展示<div ref="content" v-html="contentValue" id="content"></div> //用于截图并转化成pdf 我们调整使他不在可视范围内(不需要视频 同时把视频隐藏)-----// js
import html2canvas from "html2canvas";
import { jsPDF } from "jspdf";
import JSZip from "jszip";
import FileSaver from "file-saver";
-----const content = ref<any>(null); //ref实例const contentValue= ref<string>(""); //contentValue富文本html数据const downloadFileUrl = ref<string[]>([]); //压缩包下载数组const pdfValue=ref<string>("")// 获取详情const getDetail = async (id: string) => {const res = await 你的详情api({id,});if (res) {contentValue.value = res;// 添加视频链接downloadFileUrl.value = getVideo(res.content);if (res?.annexList?.length) {// 添加附件链接downloadFileUrl.value.push(res.annexList[0].url);}}};----- //less#content {position: absolute;left: 100000px;top: 0;/deep/video {display: none;}
}
点击下载按钮
1.截图content元素,并转化为pdf
const exportToPDF = () => {const dom = content.value;html2canvas(dom, {useCORS: true, //解决网络图片跨域问题width: dom.width,height: dom.height,windowWidth: dom.scrollWidth,dpi: window.devicePixelRatio * 4, // 将分辨率提高到特定的DPI 提高四倍scale: 4, // 按比例增加分辨率backgroundColor: "#fff", // 背景}).then((canvas) => {const pdf = new jsPDF("p", "mm", "a4"); // A4纸,纵向const ctx = canvas.getContext("2d");const a4w = 170;const a4h = 250; // A4大小,210mm x 297mm,四边各保留20mm的边距,显示区域170x257const imgHeight = Math.floor((a4h * canvas.width) / a4w); // 按A4显示比例换算一页图像的像素高度let renderedHeight = 0;while (renderedHeight < canvas.height) {const page = document.createElement("canvas");page.width = canvas.width;page.height = Math.min(imgHeight, canvas.height - renderedHeight); // 可能内容不足一页// 用getImageData剪裁指定区域,并画到前面创建的canvas对象中page.getContext("2d").putImageData(ctx.getImageData(0,renderedHeight,canvas.width,Math.min(imgHeight, canvas.height - renderedHeight)),0,0);pdf.addImage(page.toDataURL("image/jpeg", 1.0),"JPEG",20,20,a4w,Math.min(a4h, (a4w * page.height) / page.width)); // 添加图像到页面,保留10mm边距renderedHeight += imgHeight;if (renderedHeight < canvas.height) {pdf.addPage(); // 如果后面还有内容,添加一个空页}}pdfValue.value = pdf.output("datauristring"); // 获取base64Pdf});
canvas putImageData、getImageData
getImageData 获取指定矩形区域的像素信息
ctx.getImageData(x,y,width,height)
属性 | 描述 |
---|---|
x | 开始复制的左上角位置的 x 坐标(以像素计)。 |
y | 开始复制的左上角位置的 y 坐标(以像素计)。 |
width | 要复制的矩形区域的宽度。 |
height | 要复制的矩形区域的高度。 |
putImageData 将这些数据放回画布,从而实现对画布像素的编辑
ctx.putImageData(imgData,x,y,dirtyX,dirtyY,dirtyWidth,dirtyHeight)
属性 | 描述 |
---|---|
imgData | 规定要放回画布的 ImageData 对象 ; |
x | ImageData 对象左上角的 x 坐标,以像素计; |
y | ImageData 对象左上角的 y 坐标,以像素计; |
dirtyX | 可选。水平值(x),以像素计,在画布上放置图像的位置; |
dirtyY | 可选。水平值(y),以像素计,在画布上放置图像的位置; |
dirtyWidth | 可选。在画布上绘制图像所使用的宽度; |
dirtyHeight | 可选。在画布上绘制图像所使用的高度 |
- ImageData
结构:每个ImageData对象包含三个属性:
width:图像数据的宽度(以像素为单位)。
height:图像数据的高度(以像素为单位)。
data:一个一维数组,包含图像数据的RGBA值。每个像素由四个连续的数组元素表示,分别对应红、绿、蓝和透明度(alpha)通道。每个通道的值都是一个0到255之间的整数。
2.提取富文本视频
//单独提取富文本视频链接const getVideo = (str: string) => {const regex = /<video.*?src=["']([^"']+)["']/g;const videoTags = str.match(regex);// console.log(videoTags) arr[0]代表第一个匹配项,arr[1]代表第二个匹配项...,数组length代表有几个匹配项// ["<video poster="" controls="true" width="auto" height="auto"><source src="你的地址""]const videoUrls = [];if (videoTags) {for (let i = 0; i < videoTags.length; i++) {const match = regex.exec(videoTags[i]);// console.log(match) [0]代表匹配项,[≥1]代表捕获的group。index是匹配的第一个字符索引,input代表str字符串// 0: "<video poster=\"\" controls=\"true\" width=\"auto\" height=\"auto\"><source src=\"你的地址\""// 1: "你的地址"// index: 0//input: "<video poster=\"\" controls=\"true\" width=\"auto\" height=\"auto\"><source src=\"你的地址\""if (match) {videoUrls.push(match[1]); // match[1] 匹配到的视频地址}}}return videoUrls;};//ps 单独提取文字正则 str.replace(/<[^>]+>/g, "")
正则 str.match(regex) regex.exec(str)知识补充
3.base64和在线地址转blob
const dataURLtoFile = (dataurl: string, type: string) => {return new Promise((resolve, reject) => {if (type === "http") {//通过请求获取文件blob格式let xmlhttp = new XMLHttpRequest();xmlhttp.open("GET", url, true);xmlhttp.responseType = "blob";xmlhttp.onload = function () {if (xmlhttp.status == 200) {resolve(xmlhttp.response);} else {reject(xmlhttp.response);}};xmlhttp.send();} else {let arr = dataurl.split(",");let bstr = atob(arr[1]);let n = bstr.length;let u8arr = new Uint8Array(n);while (n--) {u8arr[n] = bstr.charCodeAt(n);}resolve(u8arr);}});};
4.下载成压缩包代码
// 下载全部附件const downloadFile = async () => {var blogTitle = `附件批量下载`; // 下载后压缩包的名称var zip = new JSZip();var promises = [];for (let item of downloadFileUrl.value) {if (item) {// 在线地址转blob 添加至进程const promise = dataURLtoFile(item, "http").then((data) => {// 下载文件, 并存成ArrayBuffer对象(blob)let fileName = getFileName(item); //文件名 这里可以自己命名 不用调这个方法 博主需求是截取地址后面的zip.file(fileName, data, { binary: true });});promises.push(promise);} else {// answer地址不存在时提示alert(`附件地址错误,下载失败`);}}// 单独加富文本pdf blobif (pdfUrl.value) {const contentPromise = dataURLtoFile(pdfUrl.value, "base64").then((data) => {zip.file("content.pdf", data, { binary: true });});promises.push(contentPromise);}Promise.all(promises).then(() => {zip.generateAsync({type: "blob",}).then((content) => {// 生成二进制流FileSaver.saveAs(content, blogTitle); // 利用file-saver保存文件 blogTitle:自定义文件名});}).catch((res) => {alert("文件压缩失败");});};// 获取文件名const getFileName = (filePath: string) => {var startIndex = filePath.lastIndexOf("/");if (startIndex != -1)return filePath.substring(startIndex + 1, filePath.length).toLowerCase();else return "";};
全部代码
<template><div><div><div><p@click="downloadAllFile()"><a-icon type="icon-xiazai"></w-icon> 下载</p></div></div><divclass="text-content"v-html="detaileInfo.content"></div><divclass="text-content"ref="content"id="content"><div v-html="detaileInfo.content"></div></div></div>
</template><script lang="ts">
import { defineComponent, onMounted, ref } from "vue";
import html2canvas from "html2canvas";
import { jsPDF } from "jspdf";
import JSZip from "jszip";
import FileSaver from "file-saver";export default defineComponent({name: "announcementDetail",setup() {const detaileInfo = ref<any>({});const content = ref<any>(null);const downloadFileUrl = ref<string[]>([]);const pdfUrl = ref<string>("");// 获取详情const getDetail = async () => {const res = await 你的api({id: "你的id",});if (res) {detaileInfo.value = res;// 添加视频链接downloadFileUrl.value = getVideo(res.content);if (res?.annexList?.length) {// 添加附件链接downloadFileUrl.value.push(res.annexList[0].url);}}};//单独提取富文本视频链接const getVideo = (str: string) => {const regex = /<video.*?src=["']([^"']+)["']/g;const videoTags = str.match(regex);const videoUrls = [];if (videoTags) {for (let i = 0; i < videoTags.length; i++) {const match = regex.exec(videoTags[i]);if (match) {videoUrls.push(match[1]); // match[1] 匹配到的视频地址}}}return videoUrls;};const downloadAllFile = () => {exportToPDF();};const exportToPDF = () => {const dom = content.value;html2canvas(dom, {useCORS: true, //解决网络图片跨域问题width: dom.width,height: dom.height,windowWidth: dom.scrollWidth,dpi: window.devicePixelRatio * 4, // 将分辨率提高到特定的DPI 提高四倍scale: 4, // 按比例增加分辨率backgroundColor: "#fff", // 背景}).then((canvas) => {// eslint-disable-next-line new-capconst pdf = new jsPDF("p", "mm", "a4"); // A4纸,纵向const ctx = canvas.getContext("2d");const a4w = 170;const a4h = 250; // A4大小,210mm x 297mm,四边各保留20mm的边距,显示区域170x257const imgHeight = Math.floor((a4h * canvas.width) / a4w); // 按A4显示比例换算一页图像的像素高度let renderedHeight = 0;while (renderedHeight < canvas.height) {const page = document.createElement("canvas");page.width = canvas.width;page.height = Math.min(imgHeight, canvas.height - renderedHeight); // 可能内容不足一页// 用getImageData剪裁指定区域,并画到前面创建的canvas对象中page.getContext("2d").putImageData(ctx.getImageData(0,renderedHeight,canvas.width,Math.min(imgHeight, canvas.height - renderedHeight)),0,0);pdf.addImage(page.toDataURL("image/jpeg", 1.0),"JPEG",20,20,a4w,Math.min(a4h, (a4w * page.height) / page.width)); // 添加图像到页面,保留10mm边距renderedHeight += imgHeight;if (renderedHeight < canvas.height) {pdf.addPage(); // 如果后面还有内容,添加一个空页}}pdfUrl.value = pdf.output("datauristring"); // 获取base64PdfdownloadFile();});};//返回blob值 在线地址和前端生成的base64编码const dataURLtoFile = (dataurl: string, type: string) => {return new Promise((resolve, reject) => {if (type === "http") {//通过请求获取文件blob格式let xmlhttp = new XMLHttpRequest();xmlhttp.open("GET", url, true);xmlhttp.responseType = "blob";xmlhttp.onload = function () {if (xmlhttp.status == 200) {resolve(xmlhttp.response);} else {reject(xmlhttp.response);}};xmlhttp.send();} else {let arr = dataurl.split(",");let bstr = atob(arr[1]);let n = bstr.length;let u8arr = new Uint8Array(n);while (n--) {u8arr[n] = bstr.charCodeAt(n);}resolve(u8arr);}});};// 下载全部附件const downloadFile = async () => {var blogTitle = `附件批量下载`; // 下载后压缩包的名称var zip = new JSZip();var promises = [];for (let item of downloadFileUrl.value) {if (item) {// 在线地址转blob 添加至进程const promise = dataURLtoFile(item, "http").then((data) => {// 下载文件, 并存成ArrayBuffer对象(blob)let fileName = getFileName(item); //文件名zip.file(fileName, data, { binary: true });});promises.push(promise);} else {alert(`附件地址错误,下载失败`);}}// 单独加富文本blobif (pdfUrl.value) {const contentPromise = dataURLtoFile(pdfUrl.value, "base64").then((data) => {zip.file("content.pdf", data, { binary: true });});promises.push(contentPromise);}Promise.all(promises).then(() => {zip.generateAsync({type: "blob",}).then((content) => {// 生成二进制流FileSaver.saveAs(content, blogTitle); // 利用file-saver保存文件 blogTitle:自定义文件名});}).catch((res) => {alert("文件压缩失败");});};// 获取文件名const getFileName = (filePath: string) => {var startIndex = filePath.lastIndexOf("/");if (startIndex != -1)return filePath.substring(startIndex + 1, filePath.length).toLowerCase();else return "";};onMounted(() => {getDetail();});return {content,detaileInfo,downloadAllFile,};},
});
</script><style lang="less" scoped>
.text-content {font-family: "PingFang SC";font-weight: 400;font-size: 15px;letter-spacing: 0.06px;line-height: 30px;text-align: left;color: #666;width: 100%;/deep/video,/deep/img {width: 100%;}
}
#content {position: absolute;left: 100000px;top: 0;.label {display: inline-block;}/deep/video {display: none;}
}
</style>
相关文章:

vue3.0 根据富文本html页面生成压缩包(含视频在线地址、图片在线地址、前端截图、前端文档)
vue3.0生成压缩包(含在线地址、前端截图、前端文档) 需求描述效果开始下载插件包基本代码构造 点击下载按钮1.截图content元素,并转化为pdfcanvas putImageData、getImageDatagetImageData 获取指定矩形区域的像素信息putImageData 将这些数据…...

WPF+LibVLC开发播放器-LibVLC在C#中的使用
LibVLC在C#中的使用 安装包Nuget使用控件使用播放器初始化加载视频文件 视频教程: 使用WPFLibVLC快速开发一个播放器 安装包Nuget 安装下面两个包,必须安装两个 一个是相关框架对应的包,Winform就安装LibVLCSharp.Winform;WPF就安装LibVLCSharp.WPF&am…...

消息中间件-Kafka1-实现原理
消息中间件-Kafka 一、kafka简介 1、概念 Kafka是最初由Linkedin公司开发,是一个分布式、支持分区(partition)、多副本的(replica),基于zookeeper协调的分布式消息系统,它的最大的特性就是可以…...

2023年华数杯数学建模B题不透明制品最优配色方案设计解题全过程文档及程序
2023年华数杯全国大学生数学建模 B题 不透明制品最优配色方案设计 原题再现: 日常生活中五彩缤纷的不透明有色制品是由着色剂染色而成。因此,不透明制品的配色对其外观美观度和市场竞争力起着重要作用。然而,传统的人工配色存在一定的局限性…...
Mysql事务常见面试题 -- 事务的特性 ,并发事务问题 , undo_log和redo_log , 分布式事务
一. 事务的特性 ACID 原子性 --> 事务操作被视为一个整体 , 要么全部成功 , 要么全部失败一致性 --> 事务操作前后数据的变化是一致的隔离性 --> 事务的执行不受其他事务的影响持久性 --> 事务执行完毕会对数据永久保存 比如我们在转账的过程中 , A给B转账1000元…...

【数据库系列】Spring Boot如何配置Flyway的回调函数
Flyway 提供了回调机制,使您能够在特定的数据库迁移事件发生时执行自定义逻辑。通过实现 Flyway 的回调接口,可以在迁移前后执行操作,如记录日志、执行额外的 SQL 语句等。 1. 创建自定义回调类 要配置 Flyway 的回调函数,需要创…...

分布式推理框架 xDit
1. xDiT 简介 xDiT 是一个为大规模多 GPU 集群上的 Diffusion Transformers(DiTs)设计的可扩展推理引擎。它提供了一套高效的并行方法和 GPU 内核加速技术,以满足实时推理需求。 1.1 DiT 和 LLM DiT(Diffusion Transformers&am…...

DR.KNOWS:医疗图谱UMLS + 图神经网络 + LLM 模拟医生的诊断推理过程, 从症状出发找到可能的诊断结果
DR.KNOWS:医疗图谱UMLS 图神经网络 LLM 模拟医生的诊断推理过程, 从症状出发找到可能的诊断结果 理解要点解法拆解全流程分析图神经网络的训练论文大纲核心模式真实应用中,为什么说俩跳推理过于简化? 论文:Leveraging A Medical…...
缓存雪崩 详解
缓存雪崩详解 缓存雪崩是分布式系统中一种常见的问题,它指的是缓存中大量数据在同一时间失效,导致所有的请求都直接涌向数据库或后端服务,进而导致系统负载骤增,甚至引发系统宕机或崩溃。 1. 缓存雪崩的原因 缓存雪崩通常由以下…...
使用 Vite 创建 Vue3+TS 项目并整合 ElementPlus、Axios、Pinia、Less、Vue-router 等组件或插件
前言 记录一下使用 Vite 创建 Vue3TS 项目并整合 ElementPlus、Axios、Pinia、Less、Vue-router 等组件或插件。 一、使用 Vite 创建 Vue3TS 项目 1.新建一个 temp 文件夹 (1)在桌面新建一个 temp 文件夹,然后在 VS Code 中打开此文件夹&…...
Flink随笔 20241203 Flink重点内容
Flink 是一个强大的流处理框架,它的设计理念是高吞吐量、低延迟的流式计算。你提到的这些重点是 Flink 的核心组成部分,下面我将详细解析每一个方面。 1. 窗口(Window) 窗口是 Flink 流处理中一个非常重要的概念,主要…...

shell脚本实战
学习视频来自B站UP主泷羽sec,如涉及侵权马上删除文章。 笔记只是方便学习,以下内容只涉及学习内容,切莫逾越法律红线。 安全见闻,包含了各种网络安全,网络技术,旨在明白自己的渺小,知识的广博&a…...
【机器学习】分类任务: 二分类与多分类
二分类与多分类:概念与区别 二分类和多分类是分类任务的两种类型,区分的核心在于目标变量(label)的类别数: 二分类:目标变量 y 只有两个类别,通常记为 y∈{0,1} 或 y∈{−1,1}。 示例ÿ…...
FreeSWITCH mod_conference 的按键会控
又是一篇命题作文 mod_conference 官方文档: https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Modules/mod_conference_3965534/ 英文不好的可以看中文: http://www.freeswitch.org.cn/books/references/1.7-mod_conference.html…...

串口工作方式
串口工作方式 方式0方式0输出方式0输入 方式1方式1输出方式1输入 方式2或方式3输出输入 串口使用方法如何计算波特率串口初始化步骤串口回传实验模拟printf实验串口接收数据不丢失实验 方式0 方式 0 时,串行口为同步移位寄存器的输入输出方式。主要用于扩展并行输 入…...
统计Nginx的客户端IP,可以通过分析Nginx的访问日志文件来实现
要统计Nginx的客户端IP,可以通过分析Nginx的访问日志文件来实现。以下是一些常见的方法和步骤: 一、通过命令行工具统计 查看Nginx访问日志: Nginx的访问日志通常默认存储在/var/log/nginx/access.log,但具体位置可能因安装和配置…...

Apache Airflow 快速入门教程
Apache Airflow已经成为Python生态系统中管道编排的事实上的库。与类似的解决方案相反,由于它的简单性和可扩展性,它已经获得了普及。在本文中,我将尝试概述它的主要概念,并让您清楚地了解何时以及如何使用它。 Airflow应用场景 …...

42 基于单片机的智能浇花系统
目录 一、主要功能 二、硬件资源 三、程序编程 四、实现现象 一、主要功能 基于51单片机,采样DHT11温湿度传感器检测温湿度,通过LCD1602显示 4*4按键矩阵可以设置温度湿度阈值,温度大于阈值则开启水泵,湿度大于阈值则开启风扇…...
乐橙云小程序插件接入HbuilderX
乐橙插件使用: 1.配置app.json文件,uniapp中在mainfest.json中配置 https://uniapp.dcloud.net.cn/collocation/manifest.html#mp-weixin ** 2、集成插件页面.json文件 ** uniapp在 pages.json 对应页面的 style -> usingComponents 引入组件&…...
VoCo-LLaMA: Towards Vision Compression with Large Language Models
视觉语言模型在各种多模态任务上取得了显著的成功,但经常受到有限上下文窗口和处理高分辨率图像输入和视频的高计算成本的瓶颈。视觉压缩可以通过减少视觉令牌数量避免该问题。先前方法使用额外模块压缩视觉令牌并强制LLM理解压缩的令牌。然而,LLM对视觉…...
SkyWalking 10.2.0 SWCK 配置过程
SkyWalking 10.2.0 & SWCK 配置过程 skywalking oap-server & ui 使用Docker安装在K8S集群以外,K8S集群中的微服务使用initContainer按命名空间将skywalking-java-agent注入到业务容器中。 SWCK有整套的解决方案,全安装在K8S群集中。 具体可参…...

docker详细操作--未完待续
docker介绍 docker官网: Docker:加速容器应用程序开发 harbor官网:Harbor - Harbor 中文 使用docker加速器: Docker镜像极速下载服务 - 毫秒镜像 是什么 Docker 是一种开源的容器化平台,用于将应用程序及其依赖项(如库、运行时环…...

盘古信息PCB行业解决方案:以全域场景重构,激活智造新未来
一、破局:PCB行业的时代之问 在数字经济蓬勃发展的浪潮中,PCB(印制电路板)作为 “电子产品之母”,其重要性愈发凸显。随着 5G、人工智能等新兴技术的加速渗透,PCB行业面临着前所未有的挑战与机遇。产品迭代…...

.Net框架,除了EF还有很多很多......
文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...

解决Ubuntu22.04 VMware失败的问题 ubuntu入门之二十八
现象1 打开VMware失败 Ubuntu升级之后打开VMware上报需要安装vmmon和vmnet,点击确认后如下提示 最终上报fail 解决方法 内核升级导致,需要在新内核下重新下载编译安装 查看版本 $ vmware -v VMware Workstation 17.5.1 build-23298084$ lsb_release…...

家政维修平台实战20:权限设计
目录 1 获取工人信息2 搭建工人入口3 权限判断总结 目前我们已经搭建好了基础的用户体系,主要是分成几个表,用户表我们是记录用户的基础信息,包括手机、昵称、头像。而工人和员工各有各的表。那么就有一个问题,不同的角色…...
css的定位(position)详解:相对定位 绝对定位 固定定位
在 CSS 中,元素的定位通过 position 属性控制,共有 5 种定位模式:static(静态定位)、relative(相对定位)、absolute(绝对定位)、fixed(固定定位)和…...

C++ 求圆面积的程序(Program to find area of a circle)
给定半径r,求圆的面积。圆的面积应精确到小数点后5位。 例子: 输入:r 5 输出:78.53982 解释:由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982,因为我们只保留小数点后 5 位数字。 输…...
3403. 从盒子中找出字典序最大的字符串 I
3403. 从盒子中找出字典序最大的字符串 I 题目链接:3403. 从盒子中找出字典序最大的字符串 I 代码如下: class Solution { public:string answerString(string word, int numFriends) {if (numFriends 1) {return word;}string res;for (int i 0;i &…...

select、poll、epoll 与 Reactor 模式
在高并发网络编程领域,高效处理大量连接和 I/O 事件是系统性能的关键。select、poll、epoll 作为 I/O 多路复用技术的代表,以及基于它们实现的 Reactor 模式,为开发者提供了强大的工具。本文将深入探讨这些技术的底层原理、优缺点。 一、I…...