页面生成图片或PDF node-egg

没有特别的幸运,那么就特别的努力!!!

中间件:页面生成图片 node-egg

涉及到技术

node + egg + Puppeteer

官方网址:

node:https://nodejs.org/dist/v16.17.0/

egg: https://www.eggjs.org/zh-CN/

Puppeteer: https://zhaoqize.github.io/puppeteer-api-zh_CN/#/

本次使用node版本:16.17.0

解决文书智能生成多样化

场景1:

比如全国有34个省份,每个省份文书模板不一样

场景2:

条件不一样,文书生成格式布局不一样

先看效果

以百度地址为例: --- https://www.baidu.com

js 复制代码
1. 启动项目后
http://192.168.XX.XX:7400/  (浏览器访问  端开以你本机为准)

2. 通过postman测试

接口地址:http://192.168.XX.XX:7400/canvas/getImage
post请求 + 参数
{
    "url": "https://www.baidu.com"
}

效果图:

环境准备

操作系统:支持 macOS,Linux,Windows

运行环境:建议选择 LTS 版本,最低要求 8.x。

初始化项目

js 复制代码
$ mkdir node-egg
$ cd node-egg
$ npm init
$ npm i egg --save
$ npm i egg-bin --save-dev
$ npm i @sentry/node events generic-pool puppeteer -D

添加 npm scripts 到 package.json:

js 复制代码
{
  "name": "pdf",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "dev": "egg-bin dev"
  },
  "author": "hammer1010",
  "license": "ISC",
  "dependencies": {
    "@sentry/node": "^7.60.1",
    "egg": "^3.17.3",
    "events": "^3.3.0"
  },
  "devDependencies": {
    "egg-bin": "^6.4.1",
    "generic-pool": "^3.9.0",
    "puppeteer": "^20.9.0"
  }
}

目录结构

js 复制代码
egg-example
├── app
│   ├── controller
│   │   └── home.js
│   │   └── XX.js
│   ├── plugins
│   │   └── puppeteer-pool.js
│   ├── service
│   │   └── canvas.js
│   └── router.js
├── config
│   └── config.default.js
└── package.json

主要就是一个puppeteer-pool 线程池,新建一个puppeteer-pool.js文件

js 复制代码
'use strict';
const puppeteer = require('puppeteer');
const genericPool = require('generic-pool');

/**
 * 初始化一个 Puppeteer 池
 * @param {Object} [options={}] 创建池的配置配置
 * @param {Number} [options.max=10] 最多产生多少个 puppeteer 实例 。如果你设置它,请确保 在引用关闭时调用清理池。 pool.drain().then(()=>pool.clear())
 * @param {Number} [options.min=1] 保证池中最少有多少个实例存活
 * @param {Number} [options.maxUses=2048] 每一个 实例 最大可重用次数,超过后将重启实例。0表示不检验
 * @param {Number} [options.testOnBorrow=2048] 在将 实例 提供给用户之前,池应该验证这些实例。
 * @param {Boolean} [options.autostart=false] 是不是需要在 池 初始化时 初始化 实例
 * @param {Number} [options.idleTimeoutMillis=3600000] 如果一个实例 60分钟 都没访问就关掉他
 * @param {Number} [options.evictionRunIntervalMillis=180000] 每 3分钟 检查一次 实例的访问状态
 * @param {Object} [options.puppeteerArgs={}] puppeteer.launch 启动的参数
 * @param {Function} [options.validator=(instance)=>Promise.resolve(true))] 用户自定义校验 参数是 取到的一个实例
 * @param {Object} [options.otherConfig={}] 剩余的其他参数 // For all opts, see opts at https://github.com/coopernurse/node-pool#createpool
 * @return {Object} pool
 */
const initPuppeteerPool = (options = {}) => {
  const {
    max = 10,
    min = 2,
    maxUses = 2048,
    testOnBorrow = true,
    autostart = false,
    idleTimeoutMillis = 3600000,
    evictionRunIntervalMillis = 180000,
    puppeteerArgs = {},
    validator = () => Promise.resolve(true),
    ...otherConfig
  } = options;

  const factory = {
    create: () =>
      puppeteer.launch(puppeteerArgs).then(instance => {
        // 创建一个 puppeteer 实例 ,并且初始化使用次数为 0
        instance.useCount = 0;
        return instance;
      }),
    destroy: instance => {
      instance.close();
    },
    validate: instance => {
      // 执行一次自定义校验,并且校验校验 实例已使用次数。 当 返回 reject 时 表示实例不可用
      return validator(instance).then(valid => Promise.resolve(valid && (maxUses <= 0 || instance.useCount < maxUses)));
    },
  };
  const config = {
    max,
    min,
    testOnBorrow,
    autostart,
    idleTimeoutMillis,
    evictionRunIntervalMillis,
    ...otherConfig,
  };
  const pool = genericPool.createPool(factory, config);
  const genericAcquire = pool.acquire.bind(pool);
  // 重写了原有池的消费实例的方法。添加一个实例使用次数的增加
  pool.acquire = () =>
    genericAcquire().then(instance => {
      instance.useCount += 1;
      return instance;
    });
  pool.use = fn => {
    let resource;
    return pool
      .acquire()
      .then(r => {
        resource = r;
        return resource;
      })
      .then(fn)
      .then(
        result => {
          // 不管业务方使用实例成功与否都表示一下实例消费完成
          pool.release(resource);
          return result;
        },
        err => {
          pool.release(resource);
          throw err;
        }
      );
  };
  return pool;
};

module.exports = { initPuppeteerPool };

核心代码

js 复制代码
'use strict';

const Service = require('egg').Service;
const path = require('path');

const { mkdirSyncGuard, __Time, generateGuid } = require('../../util');

// const regx = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\*\+,;=.]+$/; // 校验URL合法性
const regx = /(^(http|https):\/\/([\w\-]+\.)+[\w\-]+(\/[\w\u4e00-\u9fa5\-\.\/?\@\%\!\&=\{\}\\"\[\]\+\~\:\#\;\,]*)?)/;


class CanvasService extends Service {

  // 截图
  async generateImage({ url, width, height, isMobile, deviceScaleFactor }) {
    const { app } = this;

    const fileDir = __Time(new Date()).substr(0, 10);
    // path.resolve() 拼接路径 ==> __dirname 获取文件所在的绝对路径
    const fileTempPath = path.resolve(__dirname, '../public/canvas', fileDir); // 文件临时路径
    mkdirSyncGuard(fileTempPath); // 递归检查目录
    if (!regx.test(url)) return null;

    try {
      const FileName = `${generateGuid()}.png`;
      const ImagePath = path.join(fileTempPath, FileName);

      await app.pool.use(async puppeteerInstance => {
        const page = await puppeteerInstance.newPage();
        width && height && await page.setViewport({ width, height, isMobile, deviceScaleFactor });
        await page.goto(url, { waitUntil: 'networkidle2' });
        await page.screenshot({ path: ImagePath, type: 'png', fullPage: true, width: width || 768 });
        await page.close();
      });
      return `canvas/${fileDir}/${FileName}`;
    } catch (e) {
      console.log(e);
    }
  }

  /**
   * 生成PDF
   * @param {object} param 参数
   */
  async generatePDF({ url, width = undefined, height = undefined, isMobile = undefined, deviceScaleFactor = 1, format = undefined, margin = undefined, printBackground = true }) {
    const { app } = this;

    const fileDir = __Time(new Date()).substr(0, 10);
    const fileTempPath = path.resolve(__dirname, '../public/canvas', fileDir); // 文件临时路径
    mkdirSyncGuard(fileTempPath); // 递归检查目录

    if (!regx.test(url)) return null;

    try {
      const FileName = `${generateGuid()}.pdf`;
      const FilePath = path.join(fileTempPath, FileName);

      await app.pool.use(async puppeteerInstance => {
        const page = await puppeteerInstance.newPage();
        width && height && await page.setViewport({ width, height, isMobile, deviceScaleFactor });
        await page.goto(url, {
          waitUntil: 'networkidle2',
        });
        // I dont't no Why
        format && await page.pdf({ path: FilePath, omitBackground: true, displayHeaderFooter: true, printBackground: Boolean(printBackground), width: width || 768, height: height || 1400, format: format || 'letter', margin: margin || 0 });
        !format && await page.pdf({ path: FilePath, omitBackground: true, displayHeaderFooter: true, printBackground: Boolean(printBackground), width: width || 768, height: height || 1400, margin: margin || 0 });
        await page.close();
      });
      return `canvas/${fileDir}/${FileName}`;
    } catch (e) {
      console.log(e);
    }
  }
}

module.exports = CanvasService;

完整代码

https://gitee.com/hammer1010_admin/node-egg

希望能帮助到大家,同时祝愿大家在开发旅途中愉快!!!

拿着 不谢 请叫我"锤" !!!

相关推荐
belldeep5 小时前
python:reportlab 将多个图片合并成一个PDF文件
python·pdf·reportlab
墨染辉10 小时前
pdf处理2
pdf
墨染辉21 小时前
10.2 如何解决从复杂 PDF 文件中提取数据的问题?
pdf
shandianchengzi1 天前
【记录】Excel|Excel 打印成 PDF 页数太多怎么办
pdf·excel
bianshaopeng1 天前
android 原生加载pdf
android·pdf
卢卡斯2331 天前
在线PDF怎么转换成JPG图片?分享14种转换操作!
pdf
J不A秃V头A2 天前
iTextPDF中,要实现表格中的内容在数据长度超过边框时自动换行
java·pdf
嘻嘻仙人2 天前
【杂谈一之概率论】CDF、PDF、PMF和PPF概念解释与分析
pdf·概率论·pmf·cdf
资深前端之路3 天前
vue2 将页面生成pdf下载
前端·vue.js·pdf
Eiceblue3 天前
Python 复制PDF中的页面
vscode·python·pdf