nodejs批量生成pdf, 导出zip压缩包 你是怎么实现的?
背景
之前我们生成pdf是一个一个。 详情看这里 juejin.cn/post/713904…
后来提了个需求,要批量导出PDF。
所以就做了这个功能
分析逻辑
1、循环生成PDF, 把文件Buffer存在一个数组里
2、循环文件Buffer列表, 生产压缩包
3、推到腾讯云对象存储 COS
说干就干
写接口 控制器层
const pdfPatchCreateRule = {
htmlData: {
type: 'array', required: true, itemType: 'object', rule: {
pathName: 'string',
url: 'string',
}
},
taskId: { type: 'number', required: true },
};
/**
* 批量创建PDF
* @param taskId {number} 任务id
* @param htmlData {htmlData[]} html数据
* @return Promise<void>
*/
@Post('/batchCreate')
public async batchCreate(): Promise<void> {
try {
const { ctx } = this;
/**
* 获取POST参数
*/
const body = ctx.request.body;
/**
* 校验参数
*/
const err = ctx.app.validator.validate(pdfPatchCreateRule, body);
if (err) {
let errFiled: ValidateError;
[ errFiled ] = err;
const errMsg: string = `${errFiled.field} ${errFiled.message}`;
return this.fail(0, errMsg);
}
const { htmlData, taskId } = body;
// 异步批量创建
this.ctx.service.pdf.patchCreate(htmlData, taskId);
this.success([], '操作成功');
} catch (e: any) {
this.fail(0, e.message || '服务器错误');
}
}
异步执行批量Html转PDF service层
1、循环生成PDF, 把文件Buffer存在一个数组里
const pdfBufferList: fileItem[] = [];
// const list: any = await Promise.all(urls.map(url => runWorker({ url })));
for (const singleHtml of htmlData) {
const index: number = htmlData.indexOf(singleHtml);
// 生成PDF
const pdfBuffer: Buffer = await this.buildPdf(singleHtml.url);
pdfBufferList.push({
file: pdfBuffer,
fileName: singleHtml.pathName
});
// 进度
const progress: string = ((index) / htmlData.length * 100).toFixed(2);
// 通知进度
this.notifyProgress({
progress,
taskId
});
}
// 生成buffer
const zipBuffer: Buffer = this.generateZip(pdfBufferList);
2、循环文件Buffer列表, 生产压缩包
const AdmZip = require('adm-zip');
interface fileItem {
file: Buffer;
fileName: string;
}
/**
* 生成zip
* @param bufferList {fileItem[]} 文件流列表
* @return Buffer
*/
private generateZip(bufferList: fileItem[] = []): Buffer {
const zip = new AdmZip();
bufferList.forEach((content: fileItem) => {
zip.addFile(`${content.fileName}.pdf`, content.file, '');
});
const zipBuffer = zip.toBuffer();
return zipBuffer;
}
3、推到腾讯云对象存储 COS
// 生成文件名
const fileName: string = this.service.oss.createFileName();
// 上传到OSS
const options = {
meta: {
author: 'zhangbo',
putTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
}
};
/**
* 上传压缩包
*/
const ossResult = await this.service.oss.putFile(zipBuffer, `${fileName}.zip`, options);
全部代码
const puppeteer = require('puppeteer');
import { Service } from 'egg';
const dayjs = require('dayjs');
const FormStream = require('formstream');
const AdmZip = require('adm-zip');
// const { Worker } = require('worker_threads');
interface htmlItem {
pathName: string;
url: string;
}
interface fileItem {
file: Buffer;
fileName: string;
}
export default class PDFService extends Service {
// @ts-ignore
readonly config = {
headless: true,
args: [
'--disable-gpu',
'--disable-dev-shm-usage',
'--disable-setuid-sandbox',
'--no-first-run',
'--no-sandbox',
'--no-zygote',
],
};
/**
* 回调地址
*/
// readonly callBackUrl: string = this.app.config.callBackUrl;
readonly callBackUrl: string = 'xxx'
/**
* dinging通知1地址
*/
readonly web_hook: string = this.app.config.web_hook;
/**
* 生成PDF
*/
public async buildPdf(url: string): Promise<Buffer> {
// 启动无头浏览器
const browser = await puppeteer.launch(this.config);
try {
// new一个Tab
const page = await browser.newPage();
// 设置窗口大小
await page.setViewport({
width: 1920,
height: 1080
});
// 跳转页面
await page.goto(url, {
waitUntil: 'networkidle0',
timeout: 0
});
// 返回PDF Buffer
const pdfBuffer = await page.pdf({
// headerTemplate,
// footerTemplate,
margin: {
top: 50,
bottom: 50,
left: 0,
right: 0
},
displayHeaderFooter: false,
printBackground: true,
});
this.ctx.logger.info('pdfBuffer');
return pdfBuffer;
} catch (e) {
throw e;
} finally {
browser.close();
}
}
/**
* 异步执行Html转PDF
* @param url 页面链接
* @param taskId 任务Id
*/
public async createPdf(url: string, taskId: number): Promise<void> {
// 通知的参数
const params = {
success: true,
errorMessage: '',
taskId,
ossUrl: '',
};
try {
// 生成PDF
const pdf: Buffer = await this.buildPdf(url);
// 生成文件名
const fileName: string = await this.service.oss.createFileName();
// 上传到OSS
const options = {
meta: {
taskId,
author: 'zhangbo',
htmlUrl: url,
putTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
}
};
const ossResult = await this.service.oss.putFile(pdf, `${fileName}.pdf`, options);
if (!ossResult.url) {
params.errorMessage = '上传oss失败 taskId';
params.success = false;
this.ctx.logger.error('上传oss失败 taskId: ', taskId, ossResult);
} else {
params.ossUrl = ossResult.url;
}
// 成功了通知Java那边
const result: boolean = await this.notify(params);
if (!result) {
await this.ddBot('通知回调失败 taskId: ' + taskId, params);
return;
}
this.ctx.logger.info('通知成功taskId: ', taskId);
} catch (e: any) {
await this.ddBot('生成失败taskId: ' + taskId, e.message);
this.ctx.logger.error('生成失败taskId: ', taskId, e.message || e);
params.errorMessage = '生成失败taskId' + e.message;
params.success = false;
// 失败了通知java那边
await this.notify(params);
}
}
/**
* 异步执行批量Html转PDF
* @param htmlData {htmlItem[]} htmlData
* @param taskId {number} 任务Id
*/
public async patchCreate(htmlData: htmlItem[] = [], taskId: number): Promise<void> {
// 通知的参数
const params = {
success: true,
errorMessage: '',
taskId,
ossUrl: '',
progress: 0.00
};
try {
const pdfBufferList: fileItem[] = [];
// const list: any = await Promise.all(urls.map(url => runWorker({ url })));
for (const singleHtml of htmlData) {
const index: number = htmlData.indexOf(singleHtml);
// 生成PDF
const pdfBuffer: Buffer = await this.buildPdf(singleHtml.url);
pdfBufferList.push({
file: pdfBuffer,
fileName: singleHtml.pathName
});
// 进度
const progress: string = ((index) / htmlData.length * 100).toFixed(2);
// 通知进度
this.notifyProgress({
progress,
taskId
});
}
// 生成buffer
const zipBuffer: Buffer = this.generateZip(pdfBufferList);
// 生成文件名
const fileName: string = this.service.oss.createFileName();
// 上传到OSS
const options = {
meta: {
author: 'zhangbo',
putTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
}
};
/**
* 上传压缩包
*/
const ossResult = await this.service.oss.putFile(zipBuffer, `${fileName}.zip`, options);
if (!ossResult.url) {
params.errorMessage = '上传oss失败 taskId';
params.success = false;
this.ctx.logger.error('上传oss失败 taskId: ', taskId, ossResult);
} else {
params.ossUrl = ossResult.url;
params.progress = 100;
}
// 成功了通知Java那边
const result: boolean = await this.notify(params);
if (!result) {
await this.ddBot('通知回调失败 taskId: ' + taskId, params);
return;
}
this.ctx.logger.info('通知成功taskId: ', taskId);
} catch (e: any) {
await this.ddBot('生成失败taskId: ' + taskId, e.message);
this.ctx.logger.error('生成失败taskId: ', taskId, e.message || e);
params.errorMessage = '生成失败taskId' + e.message;
params.success = false;
// 失败了通知java那边
await this.notify(params);
}
}
/**
* 生成zip
* @param bufferList {fileItem[]} 文件流列表
* @return Buffer
*/
private generateZip(bufferList: fileItem[] = []): Buffer {
const zip = new AdmZip();
bufferList.forEach((content: fileItem) => {
zip.addFile(`${content.fileName}.pdf`, content.file, '');
});
const zipBuffer = zip.toBuffer();
return zipBuffer;
}
// @ts-ignore
private sleep(time: number): Promise {
return new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
},time);
});
}
}
测试接口及生成
参数
{
"htmlData": [{
"pathName": "价值观测评(新)-海芋-杭州公司",
"url": "http://daily-eval.sunmeta.top/admin/#/pdf/XD-04/01c0ebc0a54340a2975cb7d8783fd1f5Recruit=true&appId=2_1907306751"
}],
"taskId": 179
}
返回
PDF压缩包链接 spf-material-input.oss-cn-shanghai.aliyuncs.com/eval_pdf_da…
总结
你学费了吗
转载自:https://juejin.cn/post/7208817904547987512