likes
comments
collection
share

nodejs批量生成pdf, 导出zip压缩包 你是怎么实现的?

作者站长头像
站长
· 阅读数 15

背景

之前我们生成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…

nodejs批量生成pdf, 导出zip压缩包  你是怎么实现的?

总结

你学费了吗