没有特别的幸运,那么就特别的努力!!!
中间件:页面生成图片 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
希望能帮助到大家,同时祝愿大家在开发旅途中愉快!!!
拿着 不谢 请叫我“锤” !!!