页面生成图片或PDF node-egg

news2025/1/22 15:58:54

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

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

  • 涉及到技术
    • node + egg + Puppeteer
  • 解决文书智能生成多样化
  • 先看效果
  • 环境准备
    • 初始化项目
  • 目录结构
    • 核心代码
  • 完整代码
    • https://gitee.com/hammer1010_admin/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

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。

初始化项目

$ 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:

{
  "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"
  }
}

目录结构

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文件

'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 };

核心代码

'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

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

拿着 不谢 请叫我“锤” !!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/812740.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

web自动化测试,定位不到元素的原因及解决方案

1.动态id定位不到元素 分析原因&#xff1a;每次打开页面&#xff0c;ID都会变化。用ID去找元素&#xff0c;每次刷新页面ID都会发生变化。 解决方案&#xff1a;推荐使用xpath的相对路径方法或者cssSelector查找到该元素。        2.iframe原因定位不到元素 分析原因…

SQL注入之布尔盲注

SQL注入之布尔盲注 一、布尔盲注介绍二、布尔盲注的特性三、布尔盲注流程3.1、确定注入点3.2、判断数据库的版本3.3、判断数据库的长度3.4、猜解当前数据库名称&#xff08;本步骤需要重复&#xff09;3.5、猜解数据表的数量3.6、猜解第一个数据表名称的长度3.7、猜解第一个数据…

【React】关于组件之间的通讯

&#x1f31f;组件化&#xff1a;把一个项目拆成一个一个的组件&#xff0c;为了便与开发与维护 组件之间互相独立且封闭&#xff0c;一般而言&#xff0c;每个组件只能使用自己的数据&#xff08;组件状态私有&#xff09;。 如果组件之间相互传参怎么办&#xff1f; 那么就要…

Python 算法交易实验65 算法交易二三事

说明 对算法交易的一些内容做一些回顾和反思吧。 老规矩&#xff0c;先chat一下 道理说的都对&#xff0c;如果要补充就推荐再看一本书量化交易:如何建立自己的算法交易事业,我觉得这样就比较完整了。 简单来说&#xff0c;把量化当成事业&#xff0c;而不是一种投机&#…

一起学算法(计算排序篇)

概念&#xff1a; 计数排序&#xff08;Counting sort&#xff09;是一个非基于比较稳定的线性时间的排序算法 非基于比较&#xff1a;之前学的排序都是通过比较数据的大小来实现有序的&#xff0c;比如希尔排序等&#xff0c;而计数排序不需要比较数据的大小而进行排序&…

数据结构:谈快速排序的多种优化和非递归展开,以及排序思想归纳

文章目录 写在前面快速排序的基本体系快速排序的优化快速排序的非递归实现排序分类总结插入排序选择排序交换排序归并排序 写在前面 快速排序作为效率相当高的排序算法&#xff0c;除了对于特殊数据有其一定的局限性&#xff0c;在大多数应用场景中都有它特有的优势和应用&…

PHP8的数据类型转换-PHP8知识详解

什么是数据类型转换&#xff1f; 答&#xff1a;数据从一个类型转换成另外一个类型&#xff0c;就是数据类型转换。 在PHP8中&#xff0c;变量的类型就是由赋值决定的&#xff0c;也就是说&#xff0c;如果 string 赋值给 $var&#xff0c;然后 $var 的类型就是 string。之后…

Python:给MySQL创建1000张表和创建1张有50个字段的表

1、创建1000张表 import pymysqldbhost "10.1.1.143" dbuser "root" dbpassword "123456" dbname "demo_cg1000" dbport 3306 dbconn pymysql.connect(hostdbhost, userdbuser, passworddbpassword, dbdbname, portdbport)mycu…

前端学习--vue2--2--vue指令基础

写在前面&#xff1a; 前置内容 - vue配置 文章目录 插值表达式v-html条件渲染v-show和v-ifv-ifv-if的扩展标签复用组件 v-show v-on /事件v-bind /&#xff1a;属性v-modelv-for 循环元素v-slotv-prev-cloak vue指令只的是带有v-前缀的特殊标签属性 插值表达式 插值表达式{…

【MATLAB第62期】基于MATLAB的PSO-NN、BBO-NN、前馈神经网络NN回归预测对比

【MATLAB第62期】基于MATLAB的PSO-NN、BBO-NN、前馈神经网络NN回归预测对比 一、数据设置 1、7输入1输出 2、103行样本 3、80个训练样本&#xff0c;23个测试样本 二、效果展示 NN训练集数据的R2为&#xff1a;0.73013 NN测试集数据的R2为&#xff1a;0.23848 NN训练集数据的…

【机器学习】Feature Engineering and Polynomial Regression

Feature Engineering and Polynomial Regression 1. 多项式特征2. 选择特征3. 缩放特征4. 复杂函数附录 首先&#xff0c;导入所需的库&#xff1a; import numpy as np import matplotlib.pyplot as plt from lab_utils_multi import zscore_normalize_features, run_gradien…

qt添加图标

1.添加资源 选择QtWidgetsApp.qrc文件打开 添加图标文件路径 添加图标文件 2.按钮添加图标 图标路径为:/res/res/swicth.jpg &#xff08;1&#xff09;代码设置图标 ui.pushButton_OPen->setIcon(QIcon(":/res/res/swicth.jpg")); &#xff08;2&#xff09;属…

设计模式:生成器模式

这个模式书上讲的比较简单&#xff0c;但是感觉精华应该是讲到了。 引用下其它博客的总结&#xff1a;生成器模式的核心在于分离构建算法和具体的构造实现&#xff0c;从而使得构建算法可以重用。 【设计模式】建造者模式_鼠晓的博客-CSDN博客

27 用linprog、fmincon求 解线性规划问题(matlab程序)

1.简述 ① linprog函数&#xff1a; 求解线性规划问题&#xff0c;求目标函数的最小值&#xff0c; [x,y] linprog(c,A,b,Aeq,beq,lb,ub) 求最大值时&#xff0c;c加上负号&#xff1a;-c ② intlinprog函数&#xff1a; 求解混合整数线性规划问题&#xff0c; [x,y] intl…

AI+低代码:开启普惠人工智能时代的新篇章

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

mac pd安装ubuntu并配置远程连接

背景 一个安静的下午&#xff0c;我又想去折腾点什么了。准备学习一下k8s的&#xff0c;但是没有服务器。把我给折腾的&#xff0c;在抱怨了&#xff1a;为什么M系列芯片的资源怎么这么少。 好在伙伴说&#xff0c;你可以尝试一下ubantu。于是&#xff0c;我只好在我的mac上安…

https协议 和 Charles 进行https抓包原理

1.对称加密 其变成复杂的加密密文发送出去。收信方收到密文后&#xff0c;若想解读原文&#xff0c;则需要使用加密用过的密钥及相同算法的逆算法对密文进行解密&#xff0c;才能使其恢复成可读明文。在对称加密算法中&#xff0c;使用的密钥只有一个&#xff0c;发收信双方都…

C++——继承(1)详解

目录 1.继承的含义 2.继承的定义&#xff1a; 3.继承方式 例子1&#xff1a;基类的访问限定符为public&#xff0c;两个派生类的继承方式分别为public、protected时&#xff1a; 例子2&#xff1a; 基类的访问限定符为protected&#xff0c;两个派生类的继承方式分别为pub…

细讲TCP三次握手四次挥手(三)

TCP/IP 协议族 在互联网使用的各种协议中最重要和最著名的就是 TCP/IP 两个协议。现在人们经常提到的 TCP/IP 并不一定是单指 TCP 和 IP 这两个具体的协议&#xff0c;而往往是表示互联网所使用的整个 TCP/IP 协议族。 互联网协议套件&#xff08;英语&#xff1a;Internet Pr…

【前端知识】React 基础巩固(四十一)——手动路由跳转、参数传递及路由配置

React 基础巩固(四十一)——手动路由跳转、参数传递及路由配置 一、实现手动跳转路由 利用 useNavigate 封装一个 withRouter&#xff08;hoc/with_router.js&#xff09; import { useNavigate } from "react-router-dom"; // 封装一个高阶组件 function withRou…