前端开发中,二进制数据是处理文件、图像、音视频、网络通信等场景的基础。以下是核心概念和用途的通俗解释:
前端二进制数据介绍
1. 什么是前端二进制数据?
指计算机原始的 0
和 1
格式的数据(比如一张图片的底层代码),前端通过特定 API 操作这些数据。
2. 核心用途
-
文件操作
上传/下载文件(如用户传图片)、切片大文件(断点续传)。 -
图像处理
修改图片像素(滤镜、裁剪)、Canvas 绘图。 -
网络通信
WebSocket 传输二进制帧、AJAX 上传二进制数据。 -
高性能计算
WebAssembly 运行编译型代码(如游戏、加密算法)。
3. 关键 API 速览
API | 作用 | 示例场景 |
---|---|---|
ArrayBuffer | 存储原始二进制数据 | 文件解析、网络数据接收 |
Uint8Array | 按字节操作数据(8位无符号整数) | 图像像素处理 |
Blob | 表示文件类数据(不可变) | 文件分片上传 |
FileReader | 读取本地文件内容 | 图片预览(转Base64) |
Canvas | 操作图像像素数据 | 实时滤镜效果 |
4. 为什么需要二进制操作?
-
性能更高:直接操作内存,比文本处理快。
-
功能更强:实现文件编辑、音视频解码等复杂功能。
-
节省带宽:二进制比文本格式(如JSON)体积更小。
前端二进制数据核心类型对比表
类型/API | 继承关系 | 使用场景 | 核心优势 |
---|---|---|---|
ArrayBuffer | 基础容器,无继承关系 | 存储原始二进制数据,如网络传输、文件解析、WebAssembly 内存操作 | 直接操作内存,性能极高 |
TypedArray | 基于 ArrayBuffer 的视图(如 Uint8Array ) | 类型化数据处理(图像像素、音频采样、网络协议解析) | 类型明确,自动处理字节对齐,简化操作 |
DataView | 基于 ArrayBuffer 的视图 | 处理混合数据类型或需要控制字节序的场景(如解析二进制文件格式、网络协议) | 灵活读写不同数据类型,支持手动控制字节序 |
Blob | 无继承关系 | 处理不可变的类文件数据(文件分片上传、生成临时 URL) | 支持大文件分片(Blob.slice() ),可直接用于 fetch 或 FormData |
File | 继承自 Blob | 表示用户上传的本地文件(通过 <input type="file"> 获取) | 包含文件名、类型、修改时间等元信息 |
FileReader | 无继承关系 | 异步读取 Blob 或 File 内容(转为 ArrayBuffer 、DataURL 、文本等) | 支持多种读取格式,兼容浏览器文件操作 |
ImageData | 包含 Uint8ClampedArray | 操作 Canvas 像素数据(图像滤镜、特效处理) | 直接映射到 Canvas 像素,RGBA 格式标准化 |
Streams API | 基于 ReadableStream /WritableStream | 流式处理大文件(如视频分块加载、实时数据传输) | 分块处理数据,避免内存溢出,提升性能 |
WebAssembly | 通过 ArrayBuffer 加载模块 | 高性能计算(游戏、音视频编码、密码学算法) | 接近原生代码性能,支持 C/C++/Rust 编译运行 |
二进制数据核心类型关键关联说明
-
继承链
-
File
→Blob
→ 所有文件相关操作基于Blob
扩展。 -
TypedArray
/DataView
→ArrayBuffer
→ 所有类型化操作依赖原始二进制缓冲区。
-
-
协作流程示例
-
文件上传:
File
→FileReader
→ArrayBuffer
→TypedArray
(处理数据) →fetch
上传。 -
图像处理:
Blob
→Image
→Canvas
→ImageData
(像素操作) → 显示结果。 -
WebAssembly:
.wasm
→ArrayBuffer
→ 编译实例化 → 调用高性能函数。
-
快速对比示例
// 1. Blob → ArrayBuffer → TypedArray
const blob = new Blob([new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f])]); // "Hello"
blob.arrayBuffer().then(buffer => {
const uint8 = new Uint8Array(buffer);
console.log(uint8); // Uint8Array(5) [72, 101, 108, 108, 111]
});
// 2. File → FileReader → ArrayBuffer
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = () => {
const buffer = reader.result;
const dataView = new DataView(buffer);
console.log("文件头:", dataView.getUint32(0));
};
reader.readAsArrayBuffer(file);
});
二进制数据核心类型总结
-
底层核心:
ArrayBuffer
是二进制操作的基础,其他类型均围绕它扩展。 -
文件处理:
Blob
和File
用于文件级操作,FileReader
提供读取能力。 -
数据视图:
TypedArray
和DataView
提供灵活的内存操作方式。 -
高性能场景:
WebAssembly
和Streams API
解决计算和流式处理需求。
前端常用二进制数据介绍
1. ArrayBuffer
作用:最基础的二进制数据容器,表示通用的、固定长度的原始二进制数据缓冲区,不能直接操作,需通过 TypedArray 或 DataView 访问
优势:直接操作内存,性能极高,适合处理音频、视频等二进制数据。
使用场景:网络传输、文件解析、WebAssembly 内存操作。
区分:Buffer是Node.js 特有的二进制类型(等同于 Uint8Array),前端通常通过浏览器 API 或工具库(如 buffer polyfill)使用
<!DOCTYPE html>
<html>
<head>
<title>ArrayBuffer 示例</title>
</head>
<body>
<script>
// 1. 创建一个 16 字节的 ArrayBuffer
const buffer = new ArrayBuffer(16);
// 2. 使用 TypedArray 操作数据
const int32View = new Int32Array(buffer); // 每个元素占 4 字节
int32View[0] = 42; // 写入数据
int32View[1] = 1024;
// 3. 通过另一个视图验证数据
const uint8View = new Uint8Array(buffer);
console.log("ArrayBuffer 内容(十六进制):");
for (let i = 0; i < uint8View.length; i++) {
console.log(uint8View[i].toString(16).padStart(2, '0'));
}
</script>
</body>
</html>
2. TypedArray
作用:
-
基于
ArrayBuffer
的视图,提供特定类型的二进制数据操作,包括:-
Int8Array
、Uint8Array
(无符号字节) -
Uint8ClampedArray
(限制在 0-255,常用于图像处理) -
Int16Array
、Uint16Array
-
Int32Array
、Uint32Array
-
Float32Array
、Float64Array
-
BigInt64Array
、BigUint64Array
(ES2020 引入,用于大整数)
-
优势:类型明确,无需手动处理字节细节。
使用场景:图像像素操作、音频数据处理。
<!DOCTYPE html>
<html>
<head>
<title>TypedArray 示例</title>
</head>
<body>
<script>
// 1. 创建一个 4 字节的 ArrayBuffer
const buffer = new ArrayBuffer(4);
// 2. 使用不同类型视图操作同一 buffer
const uint8View = new Uint8Array(buffer);
uint8View[0] = 0x12; // 写入字节数据
uint8View[1] = 0x34;
const uint16View = new Uint16Array(buffer);
console.log("Uint16 视图的值:", uint16View[0].toString(16)); // 输出 3412(小端序)
const uint32View = new Uint32Array(buffer);
console.log("Uint32 视图的值:", uint32View[0].toString(16)); // 输出 34120000
</script>
</body>
</html>
3. DataView
作用:灵活读写 ArrayBuffer,支持混合数据类型和字节序控制。
优势:处理复杂二进制格式时更灵活。
使用场景:解析自定义协议、处理网络数据包
<!DOCTYPE html>
<html>
<head>
<title>DataView 示例</title>
</head>
<body>
<script>
// 1. 创建一个 8 字节的 ArrayBuffer
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
// 2. 写入不同数据类型(大端序)
view.setInt32(0, 0x12345678, false); // 大端序写入 4 字节整数
view.setFloat32(4, Math.PI, false); // 大端序写入浮点数
// 3. 读取数据
console.log("整数:", view.getInt32(0, false).toString(16));
console.log("浮点数:", view.getFloat32(4, false));
</script>
</body>
</html>
4. Blob (Binary Large Object)
作用:不可变的类文件对象,常用于文件切片(slice())、下载或上传(如 fetch 的 FormData)
优势:适合处理大文件,可直接用于网络请求。
使用场景:文件下载、图片预览。
<!DOCTYPE html>
<html>
<body>
<button onclick="downloadFile()">下载 Blob 文件</button>
<script>
function downloadFile() {
// 1. 创建 Blob 内容
const text = "Hello, Blob!";
const blob = new Blob([text], { type: 'text/plain' });
// 2. 生成临时 URL 并下载
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'example.txt';
a.click();
URL.revokeObjectURL(url);
}
</script>
</body>
</html>
5. File 和 FileReader
File
作用:
读取用户本地文件内容,继承自 Blob,表示用户本地文件(通过 <input type="file"> 获取),包含文件名、类型等元数据
FileReader
作用:用于异步读取 Blob
或 File
内容为 ArrayBuffer
、DataURL
或文本
优势:直接获取文件元数据,支持异步读取。
使用场景:文件上传、图片预览。
<!DOCTYPE html>
<html>
<body>
<input type="file" id="fileInput">
<div id="output"></div>
<script>
document.getElementById('fileInput').addEventListener('change', function(e) {
const file = e.target.files[0];
const reader = new FileReader();
// 1. 读取为文本
reader.readAsText(file);
reader.onload = () => {
document.getElementById('output').innerHTML = `
<p>文件名: ${file.name}</p>
<p>大小: ${file.size} 字节</p>
<p>内容: ${reader.result}</p>
`;
};
});
</script>
</body>
</html>
6. ImageData 和 Canvas
作用:操作 Canvas 的像素数据。
优势:直接修改图像像素,实现实时效果。
使用场景:图像滤镜、实时视频处理。
<!DOCTYPE html>
<html>
<body>
<canvas id="canvas" width="200" height="200"></canvas>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// 1. 绘制原始图形
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, 200, 200);
// 2. 获取像素数据并反色
const imageData = ctx.getImageData(0, 0, 200, 200);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i]; // R
data[i + 1] = 255 - data[i + 1]; // G
data[i + 2] = 255 - data[i + 2]; // B
}
// 3. 写回修改后的像素
ctx.putImageData(imageData, 0, 0);
</script>
</body>
</html>
7. Streams API
作用:分块处理大型数据流,避免内存溢出。
优势:高效处理大文件,实时数据传输。
使用场景:视频流、大文件上传/下载。
<!DOCTYPE html>
<html>
<body>
<script>
// 1. 创建一个简单的 ReadableStream
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array([72, 101, 108, 108, 111])); // "Hello"
controller.close();
}
});
// 2. 通过 Reader 读取数据块
const reader = stream.getReader();
reader.read().then(({ done, value }) => {
if (!done) {
console.log("流数据:", new TextDecoder().decode(value));
}
});
</script>
</body>
</html>
8. WebAssembly
作用:运行高性能编译型代码(如 C/C++)。
优势:接近原生性能,适合计算密集型任务。
使用场景:游戏、音视频编码、加密算法。
<!DOCTYPE html>
<html>
<body>
<script>
// 假设存在 add.wasm 文件,导出一个加法函数
fetch('add.wasm')
.then(response => response.arrayBuffer())
.then(buffer => WebAssembly.instantiate(buffer))
.then(({ instance }) => {
console.log("WASM 加法结果:", instance.exports.add(2, 3));
});
</script>
</body>
</html>
常用的二进制数据转换函数
1. 将 File
对象转换为 Blob
对象
/**
* 将 File 对象转换为 Blob 对象(File 继承自 Blob,可直接切片)
* @param {File} file - 用户上传的 File 对象
* @returns {Blob} - 转换后的 Blob 对象
*/
function fileToBlob(file) {
// File 本身是 Blob 的子类,直接使用 slice 方法切割完整文件
return file.slice(0, file.size, file.type);
}
// 示例用法
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
const blob = fileToBlob(file);
console.log('File -> Blob:', blob);
});
2. 将 Blob
对象转换为 File
对象
/**
* 将 Blob 转换为 File 对象(添加文件名和类型)
* @param {Blob} blob - 需要转换的 Blob 对象
* @param {string} fileName - 文件名(含扩展名,如 "image.jpg")
* @param {string} [fileType] - 文件类型(如 "image/jpeg",默认使用 Blob 的 type)
* @returns {File} - 转换后的 File 对象
*/
function blobToFile(blob, fileName, fileType) {
// 保留原始 Blob 的类型(若未指定 fileType)
const type = fileType || blob.type;
// 使用 File 构造函数创建对象(兼容性良好)
return new File([blob], fileName, { type });
}
// 示例用法
const blob = new Blob(['Hello, World!'], { type: 'text/plain' });
const file = blobToFile(blob, 'example.txt');
console.log('Blob -> File:', file);
3. 将 Blob
转换为 ArrayBuffer
/**
* 异步将 Blob 转换为 ArrayBuffer
* @param {Blob} blob - 需要转换的 Blob 对象
* @returns {Promise<ArrayBuffer>} - 返回 Promise 包含 ArrayBuffer
*/
function blobToArrayBuffer(blob) {
return new Promise((resolve, reject) => {
// 使用 FileReader 读取 Blob
const reader = new FileReader();
reader.onload = () => resolve(reader.result); // 读取完成返回 ArrayBuffer
reader.onerror = () => reject(reader.error); // 处理错误
reader.readAsArrayBuffer(blob); // 关键 API 调用
});
}
// 示例用法
const blob = new Blob(['Hello']);
blobToArrayBuffer(blob).then(buffer => {
console.log('Blob -> ArrayBuffer:', buffer);
});
4. 将 ArrayBuffer
转换为 Blob
/**
* 将 ArrayBuffer 转换为 Blob 对象
* @param {ArrayBuffer} buffer - 需要转换的 ArrayBuffer
* @param {string} [type] - Blob 的 MIME 类型(如 "image/png")
* @returns {Blob} - 转换后的 Blob
*/
function arrayBufferToBlob(buffer, type) {
// 直接通过 Blob 构造函数转换
return new Blob([buffer], { type: type || '' });
}
// 示例用法
const buffer = new Uint8Array([72, 101, 108, 108, 111]).buffer;
const blob = arrayBufferToBlob(buffer, 'text/plain');
console.log('ArrayBuffer -> Blob:', blob);
5. 将 Blob
转换为 Base64
字符串
/**
* 异步将 Blob 转换为 Base64 字符串
* @param {Blob} blob - 需要转换的 Blob
* @returns {Promise<string>} - 返回 Promise 包含 Base64 字符串
*/
function blobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
// 去除 DataURL 前缀(如 "data:text/plain;base64,")
const base64 = reader.result.split(',')[1];
resolve(base64);
};
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(blob); // 读取为 DataURL
});
}
// 示例用法
const blob = new Blob(['Hello']);
blobToBase64(blob).then(base64 => {
console.log('Blob -> Base64:', base64); // 输出 "SGVsbG8="
});
6. 将 Base64
字符串转换为 Blob
/**
* 将 Base64 字符串转换为 Blob 对象
* @param {string} base64 - Base64 字符串
* @param {string} [type] - Blob 的 MIME 类型(如 "image/png")
* @returns {Blob} - 转换后的 Blob
*/
function base64ToBlob(base64, type) {
// 将 Base64 转换为二进制字符串
const byteString = atob(base64);
// 创建 Uint8Array 视图
const buffer = new Uint8Array(byteString.length);
for (let i = 0; i < byteString.length; i++) {
buffer[i] = byteString.charCodeAt(i);
}
// 生成 Blob
return new Blob([buffer], { type: type || '' });
}
// 示例用法
const base64 = 'SGVsbG8='; // "Hello" 的 Base64
const blob = base64ToBlob(base64, 'text/plain');
console.log('Base64 -> Blob:', blob);
7. 将 Blob
转换为 DataURL
/**
* 异步将 Blob 转换为 DataURL(可直接用于 img.src)
* @param {Blob} blob - 需要转换的 Blob
* @returns {Promise<string>} - 返回 Promise 包含 DataURL
*/
function blobToDataURL(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result); // 直接返回完整 DataURL
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(blob);
});
}
// 示例用法(图片预览)
const blob = new Blob([new Uint8Array([0x89, 0x50, 0x4E, 0x47])], { type: 'image/png' });
blobToDataURL(blob).then(dataURL => {
console.log('Blob -> DataURL:', dataURL); // 输出 "data:image/png;base64,..."
});
8. 将 Blob
转换为 Object URL
/**
* 将 Blob 转换为 Object URL(需手动释放内存)
* @param {Blob} blob - 需要转换的 Blob
* @returns {string} - Object URL(如 "blob:http://...")
*/
function blobToObjectURL(blob) {
// 使用 URL.createObjectURL 生成临时链接
return URL.createObjectURL(blob);
}
// 示例用法
const blob = new Blob(['Hello']);
const url = blobToObjectURL(blob);
console.log('Blob -> Object URL:', url);
// 使用后需释放内存!
// URL.revokeObjectURL(url);
9. 将 Blob
转换为 Uint8Array
/**
* 异步将 Blob 转换为 Uint8Array
* @param {Blob} blob - 需要转换的 Blob
* @returns {Promise<Uint8Array>} - 返回 Promise 包含 Uint8Array
*/
function blobToUint8Array(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
// 通过 ArrayBuffer 创建 Uint8Array
const buffer = reader.result;
resolve(new Uint8Array(buffer));
};
reader.onerror = () => reject(reader.error);
reader.readAsArrayBuffer(blob);
});
}
// 示例用法
const blob = new Blob(['Hello']);
blobToUint8Array(blob).then(uint8Array => {
console.log('Blob -> Uint8Array:', uint8Array); // 输出 [72, 101, 108, 108, 111]
});
10. 将 Blob
转换为文本
/**
* 异步将 Blob 转换为文本内容
* @param {Blob} blob - 需要转换的 Blob
* @param {string} [encoding] - 文本编码(默认 "utf-8")
* @returns {Promise<string>} - 返回 Promise 包含文本内容
*/
function blobToText(blob, encoding = 'utf-8') {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsText(blob, encoding); // 指定编码读取
});
}
// 示例用法
const blob = new Blob(['你好']);
blobToText(blob).then(text => {
console.log('Blob -> Text:', text); // 输出 "你好"
});
11.使用总结
-
通用逻辑:大部分转换依赖
FileReader
和Blob
构造函数。 -
关键点:
-
FileReader
的readAsXXX
方法决定读取格式。 -
Blob
的type
属性影响生成文件的 MIME 类型。 -
Object URL
使用后需调用URL.revokeObjectURL()
释放内存。
-
-
错误处理:所有异步函数均返回 Promise,建议用
try/catch
包裹。
完整的图片上传案例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片上传与居中全屏预览</title>
<style>
/* 基础样式重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* 页面主体样式 */
body {
font-family: Arial, sans-serif;
min-height: 100vh;
background: #f5f5f5;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
/* 上传容器样式 */
.upload-container {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-bottom: 20px;
text-align: center;
}
/* 自定义上传按钮 */
.custom-upload-btn {
background: #2196F3;
color: white;
padding: 12px 24px;
border-radius: 5px;
cursor: pointer;
transition: background 0.3s;
display: inline-block;
}
.custom-upload-btn:hover {
background: #1976D2;
}
/* 隐藏原生文件输入 */
#fileInput {
display: none;
}
/* 错误提示样式 */
.error-message {
color: #ff4444;
margin-top: 10px;
display: none;
}
/* 预览容器样式 */
#previewContainer {
max-width: 90%;
margin: 20px 0;
cursor: zoom-in;
position: relative;
display: none; /* 默认隐藏 */
}
/* 预览图片样式 */
#previewImage {
max-width: 100%;
max-height: 70vh;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
transition: transform 0.3s;
}
/* 全屏模式样式 */
#previewContainer.fullscreen {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.9);
z-index: 9999;
cursor: zoom-out;
display: flex !important; /* 强制覆盖默认样式 */
align-items: center; /* 垂直居中 */
justify-content: center; /* 水平居中 */
}
/* 全屏时图片样式 */
#previewContainer.fullscreen #previewImage {
max-width: 90vw;
max-height: 90vh;
object-fit: contain; /* 保持比例填充 */
}
</style>
</head>
<body>
<!-- 上传区域 -->
<div class="upload-container">
<label class="custom-upload-btn" for="fileInput">
📷 点击上传图片
</label>
<input type="file" id="fileInput" accept="image/*">
<div class="error-message" id="errorMessage">只能上传图片文件 (JPEG/PNG/GIF)</div>
</div>
<!-- 图片预览区域 -->
<div id="previewContainer">
<img id="previewImage" alt="图片预览">
</div>
<script>
// =====================
// 元素获取
// =====================
const fileInput = document.getElementById('fileInput');
const previewImage = document.getElementById('previewImage');
const previewContainer = document.getElementById('previewContainer');
const errorMessage = document.getElementById('errorMessage');
// =====================
// 文件上传处理
// =====================
fileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
// 清除错误提示
errorMessage.style.display = 'none';
if (!file) return;
// 验证文件类型
if (!file.type.startsWith('image/')) {
showError("文件类型错误,请选择图片");
this.value = ''; // 清空选择
return;
}
// 创建文件阅读器
const reader = new FileReader();
// 文件读取成功回调
reader.onload = function(e) {
previewImage.src = e.target.result;
previewContainer.style.display = 'block'; // 显示预览容器
};
// 错误处理
reader.onerror = function() {
showError("文件读取失败");
previewContainer.style.display = 'none';
};
// 开始读取文件
reader.readAsDataURL(file);
});
// =====================
// 全屏功能
// =====================
previewContainer.addEventListener('click', toggleFullscreen);
// 全屏切换函数
function toggleFullscreen() {
if (document.fullscreenElement) {
exitFullscreen();
} else {
enterFullscreen(previewContainer);
}
}
// 进入全屏
function enterFullscreen(element) {
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.mozRequestFullScreen) { // Firefox
element.mozRequestFullScreen();
} else if (element.webkitRequestFullscreen) { // Chrome/Safari
element.webkitRequestFullscreen();
} else if (element.msRequestFullscreen) { // IE/Edge
element.msRequestFullscreen();
}
// 添加全屏类名
element.classList.add('fullscreen');
}
// 退出全屏
function exitFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) { // Firefox
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) { // Chrome/Safari
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) { // IE/Edge
document.msExitFullscreen();
}
// 移除全屏类名
previewContainer.classList.remove('fullscreen');
}
// 监听全屏状态变化
document.addEventListener('fullscreenchange', () => {
previewContainer.classList.toggle('fullscreen', !!document.fullscreenElement);
});
// =====================
// 辅助函数
// =====================
function showError(message) {
errorMessage.textContent = message;
errorMessage.style.display = 'block';
previewContainer.style.display = 'none';
}
</script>
</body>
</html>