前言
我们日常开发中经常会遇到需要接受后端返回的二进制的格式的数据,那么我们应该如何将二进制的文件内容,转变成需求中要求的格式呢?这个时候我们必须要和 Blob File ArrayBuffer TypedArray
等打交道了。我们今天就以科普的目的,向大家介绍一下这部分的内容。
我们日常用到的相关库可以参考
m3u8-downloader
Blob.js
FileSaver
...
下面我们正式开始吧
Blob (Binary Large Object)对象
-
定义:Blob 对象表示一个不可变、原始数据的类文件对象。它可以看作是存储二进制数据的容器,数据可以是文本、图像、音频、视频等各种类型。Blob 对象的数据是按照字节序列存储的,并且具有一个指定数据类型(MIME 类型)的属性。
-
创建方式:
- 可以通过
Blob()
构造函数来创建。例如,要创建一个包含简单文本内容的 Blob 对象,可以使用以下代码:
- 可以通过
js
const blob = new Blob(['Hello, World!'], { type: 'text/plain' });
这里的第一个参数是一个包含数据的数组(可以是ArrayBuffer
、ArrayBufferView
、Blob
、DOMString
等类型的数组),第二个参数是一个对象,用于指定 Blob 的类型,在这个例子中是text/plain
,表示纯文本类型。
- 用途:
例子一
- 用于处理和传输二进制数据。例如,在文件上传时,可以将文件内容读取为 Blob 对象,然后通过
XMLHttpRequest
或者fetch
等方式将其发送到服务器。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<input type="file" id="fileInput" />
<button onclick="uploadFile()">上传文件</button>
<script>
function uploadFile() {
// 获取用户选择的文件输入元素
const fileInput = document.getElementById('fileInput');
// 获取用户选择的第一个文件(如果有选择的话)
const file = fileInput.files[0];
if (file) {
// 将文件内容读取为Blob对象
const reader = new FileReader();
reader.onload = function (e) {
const blob = new Blob([e.target.result], { type: file.type });
// 创建XMLHttpRequest对象用于发送请求
const xhr = new XMLHttpRequest();
xhr.open('POST', 'your_upload_server_url.php', true);
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
// 当请求完成时的回调函数
xhr.onload = function () {
if (xhr.status === 200) {
console.log('文件上传成功');
} else {
console.log('文件上传失败,状态码:', xhr.status);
}
};
// 发送Blob对象到服务器
xhr.send(blob);
};
reader.readAsArrayBuffer(file);
} else {
console.log('请先选择要上传的文件');
}
}
</script>
</body>
</html>
例子二
- 作为数据存储,比如在浏览器的本地存储(
localStorage
或sessionStorage
)中,由于它们只能存储字符串,所以可以将 Blob 对象转换为 Base64 编码的字符串进行存储,需要使用时再转换回来。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<script>
// 示例数据,这里创建一个简单的文本 Blob 对象
const blob = new Blob(['这是一段示例文本'], { type: 'text/plain' });
// 将 Blob 对象转换为 Base64 编码的字符串
const reader = new FileReader();
reader.onloadend = function () {
const base64String = reader.result;
// 将 Base64 编码的字符串存储到 localStorage
localStorage.setItem('myBlobData', base64String);
// 从 localStorage 中取出 Base64 编码的字符串并转换回 Blob 对象
const retrievedBase64String = localStorage.getItem('myBlobData');
const retrievedBlob = new Blob([window.atob(retrievedBase64String)], { type: 'text/plain' });
// 验证转换是否成功,这里简单输出 Blob 对象的内容
const reader2 = new FileReader();
reader2.onloadend = function () {
console.log(reader2.result);
};
reader2.readAsText(retrievedBlob);
};
reader.readAsDataURL(blob);
</script>
</body>
</html>
- 用于创建和处理媒体资源,如音频和视频。可以将音频或视频数据存储在 Blob 对象中,然后将其提供给
<audio>
或<video>
标签的src
属性来播放。
例子三
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<canvas id="myCanvas" width="200" height="200"></canvas>
<button onclick="saveCanvasAsImage()">保存图像</button>
<script>
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
// 在画布上绘制一个红色矩形
ctx.fillStyle = "red";
ctx.fillRect(0, 0, 100, 100);
function saveCanvasAsImage() {
canvas.toBlob((blob) => {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "canvas_image.png";
a.click();
URL.recycleObjectURL(url);
}, "image/png");
}
</script>
</body>
</html>
Blob 最佳的实践方式
使用 Blob 对象的流(stream)读取数据并处理
以下示例展示了如何使用 Blob
对象的流来逐块读取数据,并将数据内容输出到控制台。这种方式在处理大型Blob
对象(如大文件)时非常有用,可以避免一次性加载大量数据导致内存问题。之后可以出一篇关于大文件上传的帖子详细讲讲用法 关于 Stream
下面是 Blob对象 Stream 方法的例子
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<script>
// 创建一个Blob对象,这里包含一些示例文本
const blob = new Blob(['这是一段很长很长的文本内容,用来模拟大型数据的处理。'.repeat(10000)], { type: 'text/plain' });
const stream = blob.stream();
const reader = stream.getReader();
function read() {
reader.read().then(({ done, value }) => {
if (done) {
console.log('读取完成');
return;
}
// 将读取到的数据转换为字符串并输出
const textDecoder = new TextDecoder('utf-8');
const str = textDecoder.decode(value);
console.log(str);
read();
});
}
read();
</script>
</body>
</html>
- 首先,创建了一个包含大量重复文本的
Blob
对象,用于模拟大型数据。 - 然后,通过
blob.stream()
方法获取Blob
对象的流(ReadableStream
)。 - 接着,使用
stream.getReader()
获取流的读取器(ReadableStreamReader
)。 - 定义了一个
read
函数,在这个函数中,通过reader.read()
方法读取流中的数据块。当读取完成时,done
属性为true
,此时输出读取完成
并结束函数。如果done
为false
,则表示还有数据需要读取。 - 使用
TextDecoder
将读取到的数据块(value
是一个Uint8Array
类型的数据)转换为字符串,并输出到控制台。然后再次调用read
函数,继续读取下一个数据块,直到所有数据都被读取完。
- 使用 Blob 对象的流与
fetch
结合进行网络传输(模拟)
展示了如何将Blob
对象转换为流,然后使用类似于fetch
的方式将这个流发送到一个模拟的服务器端点(这里只是在本地模拟,没有真正的服务器通信)。这可以用于模拟文件上传或者其他需要以流的方式传输Blob
数据的场景。
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<script>
// 创建一个Blob对象,这里假设是一个文件内容
const blob = new Blob(['这是一个模拟的文件内容'], { type: 'text/plain' });
const stream = blob.stream();
const reader = stream.getReader();
const body = new ReadableStream({
async pull(controller) {
const { done, value } = await reader.read();
if (done) {
controller.close();
} else {
controller.enqueue(value);
}
}
});
const request = new Request('https://example.com/upload', {
method: 'POST',
body: body,
headers: {
'Content-Type': 'text/plain'
}
});
// 这里只是模拟fetch操作,没有真正发送网络请求
// 实际应用中可以使用真正的fetch API发送请求
const responsePromise = new Promise((resolve, reject) => {
// 模拟处理响应
resolve({ ok: true });
});
responsePromise.then((response) => {
if (response.ok) {
console.log('数据发送成功');
} else {
console.log('数据发送失败');
}
});
</script>
</body>
</html>
在这个例子中:
- 首先创建了一个
Blob
对象,代表模拟的文件内容。 - 获取
Blob
对象的流并得到读取器。 - 创建了一个新的
ReadableStream
对象作为请求的body
部分。在这个ReadableStream
的pull
方法中,通过读取Blob
对象的流读取器来获取数据块,并将数据块放入ReadableStream
中(通过controller.enqueue
),直到读取完成(done
为true
),此时关闭ReadableStream
(通过controller.close
)。 - 创建了一个
Request
对象,模拟一个POST
请求,将刚才创建的ReadableStream
作为请求体,设置了请求头的Content-Type
为text/plain
。 - 最后,创建了一个
Promise
来模拟fetch
请求的响应处理。在实际应用中,可以使用真正的fetch
API 来发送请求并处理响应。如果响应的ok
属性为true
,则表示数据发送成功,否则表示发送失败。
File 文件对象
-
定义 :File 对象是特殊类型的 Blob 对象,它继承了 Blob 对象的所有属性和方法。File 对象主要用于在浏览器中表示用户选择的文件,这些文件可以是通过
<input type="file">
元素或者拖放操作获取的。File 对象除了包含文件的二进制数据(像 Blob 对象一样),还包含了一些关于文件的元信息,如文件名、文件大小、文件的最后修改日期等。 -
获取方式:
- 最常见的方式是通过 HTML 的
<input type="file">
元素。当用户选择文件后,可以通过该元素的files
属性获取所选文件对应的 File 对象。例如:
- 最常见的方式是通过 HTML 的
js
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', function() {
const file = fileInput.files[0];
console.log('文件名:', file.name);
console.log('文件大小:', file.size);
console.log('文件类型:', file.type);
});
在这个例子中,当用户在文件选择框中选择一个文件并触发change
事件时,通过files[0]
获取第一个所选文件的 File 对象,然后可以访问它的name
(文件名)、size
(文件大小)和type
(文件类型)等属性。(这些属性其实在 Blob 中也有部分,例如 size
type
等)
js
const formData = new FormData();
const fileInput = document.querySelector('input[type="file"]');
const file = fileInput.files[0];
formData.append('file', file);
const xhr = new XMLHttpRequest();
xhr.open('POST', 'upload.php');
xhr.send(formData);
上面这些是基本的常规操作了,这里就没有过多叙述了。 在这个代码片段中,File
对象被添加到FormData
对象中,然后通过XMLHttpRequest
发送到服务器的upload.php
文件进行处理。
ArrayBuffer & TypedArray & DateView
ArrayBuffer
:
- 定义 :
ArrayBuffer
对象用来表示通用的、固定长度的原始二进制数据缓冲区。它就像是一个内存区域,用于存放二进制数据,但不能直接对其进行操作,需要通过视图(如TypedArray
&DataView
)来读写数据。
TypedArray
:
- 定义 :
TypedArray
是一组用于操作ArrayBuffer
中数据的类型化数组视图,比如Uint8Array
(无符号 8 位整数数组)、Int16Array
(有符号 16 位整数数组)等。不同类型的TypedArray
决定了以何种数据格式来解读和操作ArrayBuffer
里的数据。
js
const buffer = new ArrayBuffer(16);
const uint8View = new Uint8Array(buffer);
uint8View[0] = 25; // 设置第一个字节的值为25
console.log(uint8View[0]); // 输出25
在这里,先创建了ArrayBuffer
对象,然后通过Uint8Array
视图来操作它里面的数据,将第一个字节位置的数据设置为 25 并进行读取输出。
用法
1. 文件读取与处理
在浏览器中,当用户通过<input type="file">
选择一个文件后,可以将文件内容读取到ArrayBuffer
中进行后续处理,比如查看文件的字节数据、进行简单的数据修改或者分析文件格式等。下面是简单的一个例子
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<input type="file" id="fileInput" />
<button onclick="readFile()">读取文件</button>
<script>
function readFile() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function (e) {
const arrayBuffer = e.target.result;
// 创建Uint8Array视图来查看文件字节数据
const uint8View = new Uint8Array(arrayBuffer);
console.log(uint8View);
// 例如,可以统计文件字节长度
console.log('文件字节长度:', uint8View.byteLength);
// 或者查找特定字节值出现的次数(以下以查找字节值为0的次数为例)
let count = 0;
for (let i = 0; i < uint8View.byteLength; i++) {
if (uint8View[i] === 0) {
count++;
}
}
console.log('字节值为0的出现次数:', count);
};
reader.readAsArrayBuffer(file);
}
}
</script>
</body>
</html>
-
用户选择文件后点击按钮触发
readFile
函数。 -
通过
FileReader
的readAsArrayBuffer
方法将文件内容读取为ArrayBuffer
,在onload
回调函数中:- 首先创建了
Uint8Array
视图来查看文件字节数据情况,可以输出整个字节数组以及统计字节长度等基础信息。 - 进一步通过循环遍历字节数组,统计了特定字节值(这里是字节值为 0)出现的次数,展示了对文件字节数据进行简单分析的操作。
- 首先创建了
2. 图像处理
对于图像文件(如 PNG、JPEG 等),可以先将图像数据读取到ArrayBuffer
中,然后根据图像文件的格式规范来解析和处理图像相关信息,例如获取图像尺寸、修改像素颜色等(虽然实际中更常用专门的图像处理库,但理解底层原理可以借助ArrayBuffer
)。
以下是一个简单模拟读取图像文件数据到ArrayBuffer
并获取部分信息的示例(仅示意,实际完整解析很复杂):
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<input type="file" id="imageFileInput" />
<button onclick="readImageFile()">读取图像文件</button>
<script>
function readImageFile() {
const fileInput = document.getElementById('imageFileInput');
const file = fileInput.files[0];
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = function (e) {
const arrayBuffer = e.target.result;
// 假设简单查看前几个字节(不同图像格式开头有特定标识字节)
const uint8View = new Uint8Array(arrayBuffer);
console.log('图像文件开头部分字节:', uint8View.subarray(0, 10));
// 如果是PNG图像,其文件头通常是固定的字节序列,可以做简单判断
const pngHeader = [137, 80, 78, 71, 13, 10, 26, 10];
let isPng = true;
for (let i = 0; i < pngHeader.length; i++) {
if (uint8View[i]!== pngHeader[i]) {
isPng = false;
break;
}
}
if (isPng) {
console.log('该图像可能是PNG格式');
}
};
reader.readAsArrayBuffer(file);
}
}
</script>
</body>
</html>
在这个图像相关示例中:
-
限制用户只能选择图像文件(通过判断
file.type.startsWith('image/')
)。 -
同样利用
FileReader
读取文件内容为ArrayBuffer
后:- 创建
Uint8Array
视图查看文件开头部分字节数据,简单输出展示一下。 - 针对 PNG 图像格式有固定文件头字节序列的特点,通过循环对比判断所读取的文件是否可能是 PNG 格式,体现了对图像文件格式进行初步判断的操作思路。
- 创建
3. 网络数据传输与解析
在网络通信中,接收到的二进制数据(比如从服务器获取的特定格式的二进制文件、网络协议中的二进制数据包等)可以先存储在ArrayBuffer
中,再根据约定的数据格式进行解析和处理。
以下是一个模拟接收网络数据(简单用固定字节数组模拟)并解析的示例:
js
// 模拟从网络接收到的二进制数据(这里用固定字节数组示例)
const receivedData = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
const arrayBuffer = receivedData.buffer;
// 假设按照每2个字节一组解析为整数(只是简单示例,实际网络数据有复杂格式)
const int16View = new Int16Array(arrayBuffer);
for (let i = 0; i < int16View.length; i++) {
console.log('解析出的整数:', int16View[i]); // 解析出的整数: 513 1027 1541 2055
}
在这个示例中:
- 首先用
Uint8Array
创建了一个模拟从网络接收到的二进制数据数组。 - 获取其对应的
ArrayBuffer
,然后创建Int16Array
视图,按照每 2 个字节一组解析为整数,并循环输出解析出的整数,展示了一种简单的网络数据解析思路,实际中网络协议等的数据格式会复杂得多,需要依据具体规范来准确解析。
4. 与 WebGL 配合进行图形渲染
在 WebGL(用于在网页上绘制交互式 3D 和 2D 图形的 JavaScript API)中,ArrayBuffer
常用来存储顶点数据(如顶点坐标、颜色、纹理坐标等)以及索引数据等,为图形渲染提供数据源。
以下是一个简单的 WebGL 示例,展示用ArrayBuffer
存储顶点坐标数据并渲染一个简单三角形:
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<canvas id="myCanvas" width="300" height="300"></canvas>
<script>
const canvas = document.getElementById('myCanvas');
const gl = canvas.getContext('webgl');
if (gl) {
// 顶点坐标数据,这里是一个简单三角形的坐标
const vertices = new Float32Array([
0.0, 0.5, 0.0,
-0.5, -0.5, 0.0,
0.5, -0.5, 0.0
]);
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices.buffer, gl.STATIC_DRAW);
const vertexAttribLocation = gl.getAttribLocation(gl.createProgram(), 'a_position');
gl.vertexAttribPointer(vertexAttribLocation, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(vertexAttribLocation);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
}
</script>
</body>
</html>
在这个 WebGL 相关示例中:
- 先获取了
<canvas>
元素对应的 WebGL 上下文gl
。 - 创建了一个
Float32Array
数组存储三角形的顶点坐标数据,其底层对应的ArrayBuffer
就用来为 WebGL 提供顶点数据。 - 通过一系列 WebGL 的 API 调用,创建缓冲区、绑定数据、设置顶点属性指针以及启用顶点属性等操作,最终将数据传递给 WebGL 用于渲染出一个简单的三角形图形。
这些场景只是ArrayBuffer
众多应用场景中的一部分,它作为处理底层二进制数据的重要工具,在很多需要直接操作原始数据的地方都发挥着关键作用。这里其实不是使用的是这个 ArrayBuffer
对象 更多是举例它的作用
总结
以下是对Blob
、File
、ArrayBuffer
、TypedArray
这几个对象的共同点和不同点的总结:
共同点
- 都与二进制数据处理相关:它们在Web开发中都是用于处理二进制数据或者可以作为二进制数据的载体,以满足不同场景下对数据的操作需求,比如文件处理、网络传输、数据存储等涉及二进制数据的情况。
- 在数据流转中相互配合使用 :在实际应用中,这些对象常常会相互协作。例如,要上传一个文件(
File
对象),可以先将其读取为ArrayBuffer
或者Blob
对象来进行进一步的处理(如转换格式、切割数据等)后再发送给服务器;TypedArray
则常用来操作ArrayBuffer
里的数据,也可以基于Blob
对象获取的数据来构建视图进行查看和修改等操作。
不同点
- 定义和性质 :
- Blob(Binary Large Object):表示一个不可变、原始数据的类文件对象,是存储二进制数据的容器,数据类型可以是文本、图像、音频、视频等各种类型,并且带有指定的数据类型(MIME类型)属性,可看作是对二进制数据的一种高级封装,方便在不同场景下直接使用该数据集合。
- File :是特殊类型的
Blob
对象,继承了Blob
的所有属性和方法,主要用于表示用户选择的文件(通过<input type="file">
元素或者拖放操作获取),除了包含文件的二进制数据外,还额外包含了文件名、文件大小、文件的最后修改日期等关于文件的元信息,其重点在于对实际文件的描述和操作。 - ArrayBuffer :用来表示通用的、固定长度的原始二进制数据缓冲区,就像是开辟了一块内存区域用于存放二进制数据,但本身不能直接对其内部数据进行读写操作,只是作为一个数据的存储载体,需要借助视图(如
TypedArray
或DataView
)来访问和操作其中的数据。 - TypedArray :是一组用于操作
ArrayBuffer
中数据的类型化数组视图,如Uint8Array
(无符号8位整数数组)、Int16Array
(有符号16位整数数组)等,不同类型决定了以何种数据格式来解读和操作ArrayBuffer
里的数据,是一种方便、有类型规范的数据访问方式,使得对ArrayBuffer
中的数据操作更加直观和有针对性。
- 创建方式 :
- Blob :通过
Blob()
构造函数创建,例如new Blob(['Hello, World!'], { type: 'text/plain' });
,第一个参数是包含数据的数组(可由多种类型数据组成),第二个参数指定类型。 - File :通常由用户选择文件后通过
<input type="file">
元素的files
属性获取,如const file = document.querySelector('input[type="file"]').files[0];
,一般不是手动通过构造函数去创建(虽然本质上是特殊的Blob
,但来源主要基于用户操作)。 - ArrayBuffer :使用
new ArrayBuffer(size)
构造函数创建,参数size
指定要开辟的缓冲区字节大小,例如new ArrayBuffer(16);
就创建了一个长度为16字节的缓冲区。 - TypedArray :通过关联已有的
ArrayBuffer
来创建,例如const buffer = new ArrayBuffer(16); const uint8View = new Uint8Array(buffer);
,先创建ArrayBuffer
,再基于它创建对应的TypedArray
视图,当然也有其他构造方式可以同时指定长度等参数创建并关联ArrayBuffer
,如new Uint8Array(8);
会自动创建一个合适大小的ArrayBuffer
并建立关联。
- Blob :通过
- 属性特点 :
- Blob :主要属性有
size
(表示数据大小,单位为字节)、type
(表示数据的MIME类型,如text/plain
、image/png
等)以及slice
(用于切割数据,可获取原Blob
对象的一部分数据生成新的Blob
对象)等。 - File :除了继承
Blob
的属性(如size
、type
)外,还有自己特有的属性,像name
(文件名)、lastModified
(文件的最后修改日期)等,这些额外属性有助于更好地描述一个文件实体。 - ArrayBuffer :有
byteLength
属性,用于表示缓冲区的字节长度,也就是其存储二进制数据的容量大小,本身相对比较"纯粹",只关注内存区域大小和存储的数据本身。 - TypedArray :不同类型的
TypedArray
有对应反映其数据特性的属性,比如length
属性表示数组的长度(也就是包含元素的个数),并且由于其是基于ArrayBuffer
的视图,还可以通过buffer
属性获取关联的ArrayBuffer
对象,方便进行数据的整体管理和操作关联。
- Blob :主要属性有
- 使用场景 :
- Blob :常用于创建可下载文件(如将网页内容转换为
Blob
后提供下载链接)、在文件上传时将文件内容读取为Blob
对象再发送给服务器、处理和传输二进制数据,以及作为数据存储(可转换为Base64编码字符串存储在浏览器本地存储中,需要时再转换回来)等场景。 - File :主要用于文件相关操作,尤其是在处理用户选择的文件时,如文件上传(常和
FormData
对象配合将文件发送到服务器)、展示文件信息(文件名、大小等)以及在客户端对文件进行简单的读取分析等操作场景。 - ArrayBuffer :多用于底层的二进制数据处理,比如在读取文件或接收网络数据时先将数据存储到
ArrayBuffer
中,然后再依据具体需求通过视图进一步解析;也常用于和一些图形API(如WebGL)配合,存储顶点数据、索引数据等用于图形渲染等场景。 - TypedArray :常配合
ArrayBuffer
使用,在需要以特定的数据类型(如整数、浮点数等不同精度要求)来访问和操作ArrayBuffer
中的数据时发挥作用,例如在解析特定格式的二进制数据(按照固定字节数解析为不同类型的数据元素)、处理图像像素数据(可能按照字节或特定位深度来操作颜色信息)等场景中使用。
- Blob :常用于创建可下载文件(如将网页内容转换为
总体而言,它们在Web开发的二进制数据处理生态中各自扮演着不可或缺的角色,开发者根据具体的业务需求和操作阶段选择合适的对象来完成相应的数据处理任务。 如果文章最后可以帮到你的话,麻烦点个赞吧!!!这对我继续创作是莫大的帮助,最后谢谢大家的支持了。
参考文章