一. 前提条件
⏹之前在如下两遍文章中通过PowerShell
和Python
实现了该需求,现在使用JavaScript
来实现此功能
🤔优缺点
- 直接一个HTML文件就搞定,可以考虑把相关参数写到网页中,更加方便参数输入,更加灵活
- 但如果要生成的文件体积过大,会引起浏览器内存溢出,导致网页工具崩溃
二. 方式一
2.1 实现思路
⏹多线程
浏览器中的JavaScript
是运行在单一的主线程上的,同一个时间内只能做一件事情,没有像PowerShell
和Python
那样的多线程。
但是JavaScript
有web worker
,可以将生成csv这样的消耗资源的运算交给web worker
完成。
⏹JS形式
可以将web worker
相关的JS代码写在HTML
中,直接一个文件搞定。
⏹CSV文件生成
可以将生成的csv数据先转换为字节数组,然后转换为Blob
对象,然后再通过URL.createObjectURL
创建临时文件下载链接,实现下载。
2.2 脚本内容
web worker
相关的js代码按照规范而言,应该写到JS代码中,此案例为了实现一个单HTML文件的小工具,所以并没有进行拆分。web worker
所在的<script>标签
的type属性
必须是一个浏览器不认识的值,否则无法通过document.querySelector("#sub_worker").textContent
来去获取其JS文本。web worker
使用完毕之后,需要关闭,以节约资源。
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CSV 生成器</title>
</head>
<body>
<button onclick="generateCSV()">生成 CSV</button>
<a id="downloadLink" style="display:none">下载 CSV</a>
</body>
<script id="sub_worker" type="app/worker">
// 子worker监听主worker的message事件
self.addEventListener('message', function({ data: {startRow, endRow} }) {
const csvRows = [];
for (let i = startRow; i <= endRow; i++) {
// 随机年龄
const randomAge = Math.floor(Math.random() * (60 - 18 + 1)) + 18;
// 随机日期
const randomDate = new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000);
const dateString = randomDate.toISOString().slice(0, 19).replace("T", " ");
// csv的行数据
const csvRowData = `"${i}","Name_${i}",${randomAge},"你好${i}@example.com","${dateString}"`
csvRows.push(csvRowData);
}
/*
子worker将生成的数据发送给主worker, 最后一行要添加一个换行符,
目的是保证多个 csv数据块 进行拼接的时候, 拼接处的数据可以换行
*/
postMessage(csvRows.join("\n") + "\n");
// 子worker的任务执行完毕之后关闭,节约资源
self.close();
}, false);
</script>
<script>
// 生成500万行csv
const ROWS = 5_000_000;
// 线程数, 根据电脑的配置自行调整
const PROCESS_COUNT = 8;
// 每个 worker 处理的行数
const CHUNK_SIZE = Math.ceil(ROWS / PROCESS_COUNT);
// 读取 worker 的文本代码内容, 将其创建为Blob对象后, 转换为临时URL
const blobData = new Blob([document.querySelector("#sub_worker").textContent])
const tempUrl = URL.createObjectURL(blobData)
// 生成 CSV 数据
function generateCSV() {
console.time("CSV 生成耗时");
// 存储所有 worker 所做成的数据
let byteCsvArray = [];
let completedWorkers = 0;
// 开启多个 子worker 创建csv
for (let i = 0; i < PROCESS_COUNT; i++) {
const startRow = i * CHUNK_SIZE + 1;
const endRow = Math.min((i + 1) * CHUNK_SIZE, ROWS);
// 创建 Web Worker 对象, 指定Worker的名称
const worker = new Worker(tempUrl, {name: `csv_create_worker_${i}`});
// 主worker通知子worker工作
worker.postMessage({ startRow, endRow });
// 主worker监听子worker的postMessage事件
worker.addEventListener("message", ({data}) => {
// 通过 TextEncoder 将csv文本转换为 字节数组
byteCsvArray.push(new TextEncoder().encode(data));
completedWorkers++;
// 所有 worker 任务完成,下载csv
if (completedWorkers === PROCESS_COUNT) {
downloadCSV(byteCsvArray);
console.timeEnd("CSV 生成耗时");
}
// 关闭主worker,节约资源
worker.terminate()
});
}
}
// 下载 CSV
function downloadCSV(byteCsvArray) {
const blob = new Blob(byteCsvArray, { type: "text/csv;charset=utf-8" });
const link = document.getElementById("downloadLink");
link.href = URL.createObjectURL(blob);
link.download = "person_data.csv";
link.style.display = "block";
link.textContent = "点击下载 CSV";
}
</script>
</html>
三. 方式二
3.1 实现思路
- 整体实现思路和方式一基本相同
- 不同点在于文件下载的方式,文件下载使用了
window.showDirectoryPicker()
的api - 该api可以将大文件的数据以流的方式分批写入的本地文件,避免了一次性加载到浏览器的内存中。
- 由于代码较多,所以按照功能进行了拆分。
3.2 脚本内容
⏹CSS部分
<style>
@keyframes donut-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.donut-container {
display: flex;
justify-content: center;
align-items: center;
margin-top: 20%;
}
.donut {
display: inline-block;
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: #7983ff;
border-radius: 50%;
width: 30px;
height: 30px;
animation: donut-spin 1.2s linear infinite;
}
.hidden {
display: none;
}
</style>
⏹HTML部分
<body>
<button onclick="selectCsvFileSaveDir()">📂 选择保存CSV的文件夹</button>
<ul id="file-list"></ul>
<button onclick="generateCSV()">生成 CSV</button>
<!-- loading效果 -->
<div class="donut-container hidden" id="loading">
<div class="donut"></div>
</div>
</body>
⏹JavaScript部分 - 文件夹选择功能的代码
<script>
let csvDirHandle = null;
const csvDirFileNameList = [];
async function selectCsvFileSaveDir() {
try {
// 清空页面上的文件名展示区域
const fileList = document.getElementById("file-list");
fileList.innerHTML = "";
// 获取选中的文件夹对象
csvDirHandle = await window.showDirectoryPicker();
for await (const [name, handle] of csvDirHandle.entries()) {
if (handle.kind !== "file") {
continue;
}
// 创建li标签, 将文件夹下的所有文件展示在页面上
const liTag = document.createElement("li");
liTag.textContent = name;
fileList.appendChild(liTag);
// 将文件夹下的所有文件名缓存到list中
csvDirFileNameList.push(name);
}
} catch (err) {
console.error("选择文件夹时发生异常:", err);
}
}
</script>
⏹JavaScript部分 - 子worker生成csv数据的代码
<script id="sub_worker" type="app/worker">
// 子worker监听主worker的message事件
self.addEventListener('message', function({ data: {startRow, endRow} }) {
let csvRows = [];
for (let i = startRow; i <= endRow; i++) {
// 随机年龄
const randomAge = Math.floor(Math.random() * (60 - 18 + 1)) + 18;
// 随机日期
const randomDate = new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000);
const dateString = randomDate.toISOString().slice(0, 19).replace("T", " ");
// csv的行数据
const csvRowData = `"${i}","Name_${i}",${randomAge},"你好${i}@example.com","${dateString}"`
csvRows.push(csvRowData);
}
self.postMessage(csvRows.join("\n") + "\n");
csvRows.length = 0;
// 子worker的任务执行完毕之后关闭,节约资源
self.close();
}, false);
</script>
⏹JavaScript部分 - 调用,下载文件部分的代码
<script>
// 生成500万行csv
const ROWS = 5000000;
// 线程数
const PROCESS_COUNT = 8;
// 每个 worker 处理的行数
const CHUNK_SIZE = Math.ceil(ROWS / PROCESS_COUNT);
// 读取 worker 的文本代码内容, 将其创建为Blob对象后, 转换为临时URL
const blobData = new Blob([document.querySelector("#sub_worker").textContent])
const tempUrl = URL.createObjectURL(blobData)
// 生成 CSV 数据
async function generateCSV() {
if (!csvDirHandle) {
alert("请先选择文件夹之后, 再生成CSV文件...")
return
}
let completedWorkers = 0;
let fileWriterHelper = null;
console.time("CSV 生成耗时");
try {
const fileHandle = await csvDirHandle.getFileHandle("person_data.csv", { create: true });
fileWriterHelper = await fileHandle.createWritable();
} catch (error) {
if (error.name === "NotAllowedError") {
alert("用户拒绝了文件访问权限...");
} else {
console.error("发生错误:", error);
}
return;
}
// 显示loading效果
document.querySelector("#loading").classList.remove("hidden");
// 开启多个 子worker 创建csv
for (let i = 0; i < PROCESS_COUNT; i++) {
const startRow = i * CHUNK_SIZE + 1;
const endRow = Math.min((i + 1) * CHUNK_SIZE, ROWS);
// 创建 Web Worker 对象, 指定Worker的名称
const worker = new Worker(tempUrl, {name: `csv_create_worker_${i}`});
// 主worker通知子worker工作
worker.postMessage({ startRow, endRow });
// 主worker监听子worker的postMessage事件
worker.addEventListener("message", async ({data: csvStr}) => {
// 将csv字符串数据转换为字节数据后, 通过 fileWriterHelper 将内容写入本地文件中
await fileWriterHelper.write(new TextEncoder().encode(csvStr));
completedWorkers++;
// 判断所有 worker 任务完成
if (completedWorkers === PROCESS_COUNT) {
// 隐藏loading效果
document.querySelector("#loading").classList.add("hidden");
// 关闭fileWriterHelper写入对象, 将csv文件保存到本地
await fileWriterHelper.close();
alert("csv文件已保存成功...");
}
// 关闭主worker,节约资源
worker.terminate()
});
}
}
</script>