页面生成图片或PDF node-egg

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

中间件:页面生成图片 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 实例 ,并且初始化使用次数为 0instance.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 Whyformat && 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.mzph.cn/news/15469.shtml

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

相关文章

【python】可指定时间播放音频噪声

【诉求】 A是一段音频&#xff0c;它有个长度是9秒&#xff0c;A_2就是播放A段音频的前2s。传入播放的时间time&#xff0c;效果是播放对应时常的音频。抽象出来&#xff1a;time 输出音频的种类对应的长度 str audioList [A,B,C] Int audioLength [9,9,10] All_audio_Lengt…

node 菜单权限判断

对于那些在界面上没显示的菜单在后台路由中也是要检查下权限的&#xff0c;要不然像用户管理/userList这条路由虽然没有配给某个测试账号&#xff0c;但他可以直接在地址栏输入/userList进行访问。所以在Middleware中需要增加判断用户是否有此page_url的访问权限。 /** 需要用…

Linux - 添加普通用户为信任用户

1.添加用户 在Linux系统中&#xff0c;可以使用以下步骤添加用户&#xff1a; 打开终端并以root用户身份登录 输入以下命令以创建新用户&#xff08;请将username替换为您想要创建的用户名&#xff09;&#xff1a; adduser username 设置该用户的密码&#xff0c;使用以下命…

深蓝学院C++基础与深度解析笔记 第14章 元编程

1. 元编程的引入 ● 从泛型编程到元编程 – 泛型编程 使用一套代码处理不同类型 —— – 对于一些特殊的类型需要引入额外的处理逻辑 —— 引入操纵程序的程序 – 元编程与编译期计算&#xff08;优选编译期计算&#xff09; – std::vector<int>不同&#xff0c;因为…

在windows上安装minio

1、下载windows版的minio&#xff1a; https://dl.min.io/server/minio/release/windows-amd64/minio.exe 2、在指定位置创建一个名为minio文件夹&#xff0c;然后再把下载好的文件丢进去&#xff1a; 3、右键打开命令行窗口&#xff0c;然后执行如下命令&#xff1a;(在minio.…

在CentOS下安装docker

1&#xff09;在Cent OS安装docker先有一个Cent OS 7.6系统 这个很重要&#xff0c;不同版本按照的时候是不一样的。 2&#xff09;查看CentOS版本 cat /etc/redhat-releas 3&#xff09;用root账户登录进去配置国内yum源 wget -O /etc/yum.repos.d/CentOS-Base.repo http:…

C++中enum和enum class的区别

C中enum和enum class的区别 1.作用域不同2. 默认类型不同3.类型安全程度不同 在C中&#xff0c;enum和enum class都是用来定义枚举类型的关键字&#xff0c;但它们有几个区别。 1.作用域不同 enum的枚举成员的作用域在定义的枚举类型中是共享的&#xff0c;而enum class的每个枚…

浅谈进程调度

要想了解进程调度&#xff0c;我们首先得知道什么是进程&#xff0c;那么什么是进程呢&#xff1f;其实进程就是正在运行的程序&#xff0c;如此说来&#xff0c;是不是就认为一个程序只能有一个进程&#xff0c;其实不然&#xff0c;同一个程序运行多次可能会出现多个进程。如…

移动硬盘不显示怎么办?正确解决方式看这里!

移动硬盘为存储带来了很大的方便&#xff0c;在对数据存储时&#xff0c;可做到即插即用&#xff0c;且其体积小、容量大&#xff0c;且比较安全可靠。但在实际的使用中&#xff0c;也会出现各种问题。请看下面2个常见案例。 案例1&#xff1a;“各位朋友&#xff0c;我新买了一…

iTOP-RK3568开发板Windows 安装 RKTool 驱动

在烧写镜像之前首先需要安装 RKTool 驱动。 RKTool 驱动在网盘资料“iTOP-3568 开发板\01_【iTOP-RK3568 开发板】基础资料 \02_iTOP-RK3568 开发板烧写工具及驱动”路径下。 驱动如下图所示&#xff1a; 解压缩后&#xff0c;进入文件夹&#xff0c;如下图所示&#xff1a;…

Ant Design Pro 封装网络请求

可以直接在antdPro项目的app.tsx文件中对request进行运行时配置&#xff0c;并且该配置会直接透传到umi-request的全局配置。后续直接从umi中引入request或者useRequest直接使用&#xff0c;可以说是非常方便。文档可查看&#xff1a;umi.js 具体配置代码&#xff1a; import…

Android Calendar

1.字符串日期比较大小 public static boolean compareDate(String pre, String last) {SimpleDateFormat sdf new SimpleDateFormat("yyyy-MM-dd");try {Date lastDate sdf.parse(last);Calendar lastCal Calendar.getInstance();lastCal.setTime(lastDate);Date …

Python爬虫Scrapy(二)_入门案例

入门案例 学习目标 创建一个Scrapy项目定义提取的结构化数据(Item)编写爬取网站的Spider并提取出结构化数据(Item)编写Item Pipelines来存储提取到的Item(即结构化数据) 一、新建项目(scrapy startproject) 在开始爬取之前&#xff0c;必须创建一个新的Scrapy项目。进入自定…

Django系列之Channels

1. Channels 介绍 Django 中的 HTTP 请求是建立在请求和响应的简单概念之上的。浏览器发出请求&#xff0c;Django服务调用相应的视图函数&#xff0c;并返回响应内容给浏览器渲染。但是没有办法做到 服务器主动推送消息给浏览器。 因此&#xff0c;WebSocket 就应运而生了。…

深入理解设计模式之门面模式

深入理解设计模式之门面模式 什么是门面模式&#xff1f; 门面模式&#xff08;Facade Pattern&#xff09;是一种结构型设计模式&#xff0c;它提供了一个简单的接口&#xff0c;用于访问复杂子系统中的一组接口。门面模式通过封装子系统的复杂性&#xff0c;提供了一个更简…

809协议服务端程序解码程序

809协议服务端程序解码程序 目录概述需求&#xff1a; 设计思路实现思路分析1.服务端2.code: 拓展实现性能参数测试&#xff1a;1.功能测试 参考资料和推荐阅读 Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c;skip…

easyui主表子表维护页面

easyui主表子表维护页面 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><title>Title</title><!-- <#include "common.html"/> --><link rel"stylesheet" type&quo…

释放三年版本:Aspose.Total For NET [21.7/22.7/23.7]

请各位对号入座&#xff0c;选择自己需求范围&#xff0c;你懂的&#xff0c;你懂的&#xff0c;你懂的 Aspose.Total for .NET is the most complete package of all .NET File Format Automation APIs offered by Aspose. It empowers developers to create, edit, render, …

tinkerCAD案例:29. New: Cruising with Shapes 新功能:与形状一起Cruising

tinkerCAD案例&#xff1a;29. New: Cruising with Shapes 新功能&#xff1a;与形状一起Cruising 原文&#xff1a;有动图演示 Stacking and assembling shapes has never been easier with Tinkercad’s new Cruise tool! 使用Tinkercad的Cruising工具堆叠和组装形状从未如…

会议OA系统会议管理模块开发思路(layui搭建)

目录 一.为什么要进行开发 1.开发目的 2.项目流程 A.发起会议请求过程 1.首先实现我们的多选下拉框功能&#xff01; 2.时间组件功能&#xff0c;并且提交我们新增加的会议内容 3.在进行发起会议编码时遇到的问题&#xff0c;BUG 3.1.有点时候js访问不到路径 3.2在增加…