├── imp.jpg ├── images ├── ww.png └── imp.jpg ├── public ├── .DS_Store └── vue-plugin-hiprint │ └── index.html ├── .dockerignore ├── docker-compose.yaml ├── plugins ├── sensible.js ├── static.js ├── cacheman.js └── cors.js ├── package.json ├── app.js ├── LICENSE ├── Dockerfile ├── README.md ├── .gitignore ├── routes └── root.js └── lib └── puppeteer-html-export.js /imp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CcSimple/node-hiprint-pdf/HEAD/imp.jpg -------------------------------------------------------------------------------- /images/ww.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CcSimple/node-hiprint-pdf/HEAD/images/ww.png -------------------------------------------------------------------------------- /images/imp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CcSimple/node-hiprint-pdf/HEAD/images/imp.jpg -------------------------------------------------------------------------------- /public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CcSimple/node-hiprint-pdf/HEAD/public/.DS_Store -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | *.md 2 | !README.md 3 | *.zip 4 | *.rar 5 | *.tgz 6 | *.tar 7 | *.log 8 | *.tmp 9 | *.bak 10 | node_modules/ 11 | pnpm-debug.log 12 | pnpm-lock.yaml 13 | .git/ 14 | .DS_Store 15 | .vscode/ 16 | .dockerignore 17 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | node-hiprint-pdf: 4 | image: node-hiprint-pdf:1.0.0 5 | restart: always 6 | container_name: node-hiprint-pdf 7 | build: 8 | context: ./ 9 | dockerfile: Dockerfile 10 | ports: 11 | - "3000:3000" 12 | -------------------------------------------------------------------------------- /plugins/sensible.js: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin' 2 | import sensible from '@fastify/sensible' 3 | 4 | /** 5 | * This plugins adds some utilities to handle http errors 6 | * 7 | * @see https://github.com/fastify/fastify-sensible 8 | */ 9 | export default fp(async (fastify) => { 10 | fastify.register(sensible) 11 | }) 12 | -------------------------------------------------------------------------------- /plugins/static.js: -------------------------------------------------------------------------------- 1 | import fp from "fastify-plugin"; 2 | import fastifyStatic from "@fastify/static"; 3 | import path from "path"; 4 | import { fileURLToPath } from "url"; 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = path.dirname(__filename); 7 | 8 | export default fp(async (fastify) => { 9 | const root = path.join(__dirname, "..", "public"); 10 | fastify.register(fastifyStatic, { 11 | root: root, 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /plugins/cacheman.js: -------------------------------------------------------------------------------- 1 | import fp from "fastify-plugin"; 2 | import fastifyCacheman from "fastify-cacheman"; 3 | 4 | export default fp(async (fastify) => { 5 | fastify.register(fastifyCacheman, { 6 | // 使用文件缓存 7 | // engine: "file", 8 | 9 | // 使用内存缓存 10 | engine: "memory", 11 | 12 | // 使用 redis 缓存 13 | // engine: 'redis', 14 | // port: 6379, 15 | // host: '127.0.0.1', 16 | // password: 'my-p@ssw0rd', 17 | // database: 1, 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /plugins/cors.js: -------------------------------------------------------------------------------- 1 | import fp from "fastify-plugin"; 2 | import cors from "@fastify/cors"; 3 | 4 | // export default fp(async (fastify) => { 5 | // fastify.register(cors, {}); 6 | // }); 7 | export default fp(async (fastify) => { 8 | fastify.register(cors, { 9 | origin: true, // 允许所有来源,也可以指定具体的域名,例如 "http://example.com" 10 | methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], 11 | // allowedHeaders: ["Content-Type", "Authorization"], 12 | // credentials: true, // 如果需要支持 cookie 等凭证信息 13 | }); 14 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "name": "node-hiprint-pdf", 4 | "description": "This project was bootstrapped with Fastify-CLI.", 5 | "version": "1.0.0", 6 | "main": "app.js", 7 | "scripts": { 8 | "start": "fastify start -l info -p 3000 app.js", 9 | "dev": "fastify start -w -l info -P app.js" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "dependencies": { 15 | "@fastify/autoload": "^5.0.0", 16 | "@fastify/cors": "^9.0.1", 17 | "@fastify/sensible": "^5.0.0", 18 | "@fastify/static": "^7.0.4", 19 | "fastify": "^4.26.1", 20 | "fastify-cacheman": "^3.1.0", 21 | "fastify-cli": "^6.3.0", 22 | "fastify-plugin": "^4.0.0", 23 | "puppeteer": "^23.1.1" 24 | }, 25 | "devDependencies": { 26 | "c8": "^10.1.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import AutoLoad from '@fastify/autoload' 3 | import { fileURLToPath } from 'url' 4 | 5 | const __filename = fileURLToPath(import.meta.url) 6 | const __dirname = path.dirname(__filename) 7 | 8 | // Pass --options via CLI arguments in command to enable these options. 9 | export const options = {} 10 | 11 | export default async function (fastify, opts) { 12 | // Place here your custom code! 13 | 14 | // Do not touch the following lines 15 | 16 | // This loads all plugins defined in plugins 17 | // those should be support plugins that are reused 18 | // through your application 19 | fastify.register(AutoLoad, { 20 | dir: path.join(__dirname, 'plugins'), 21 | options: Object.assign({}, opts) 22 | }) 23 | 24 | // This loads all plugins defined in routes 25 | // define your routes in one of these 26 | fastify.register(AutoLoad, { 27 | dir: path.join(__dirname, 'routes'), 28 | options: Object.assign({}, opts) 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 [CcSimple] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 使用官方Node.js基础镜像(这里用了镜像:国内网络环境问题...) 2 | FROM hub.rat.dev/library/node:20-alpine 3 | 4 | # 设置工作目录 5 | WORKDIR /app 6 | 7 | # 复制package.json文件和package-lock.json文件(如果存在) 8 | COPY package*.json . 9 | 10 | # 设置环境变量 11 | ENV NODE_ENV prd 12 | 13 | # 安装项目依赖 14 | RUN npm config set registry https://registry.npmmirror.com/ 15 | 16 | RUN npm install --production --force 17 | 18 | # 阿里云镜像 19 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories 20 | 21 | # 安装必要的依赖 22 | RUN apk add --no-cache \ 23 | chromium \ 24 | nss \ 25 | freetype \ 26 | harfbuzz \ 27 | ca-certificates \ 28 | ttf-freefont \ 29 | nodejs \ 30 | yarn \ 31 | # 中文字体乱码问题 32 | ttf-dejavu \ 33 | font-droid-nonlatin \ 34 | msttcorefonts-installer fontconfig && \ 35 | update-ms-fonts && \ 36 | fc-cache -f 37 | 38 | # 安装puppeteer依赖 39 | ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser 40 | 41 | # 设置时区为上海 42 | ENV TZ=Asia/Shanghai 43 | 44 | # 设置语言为中文 45 | ENV LANG=zh_CN.UTF-8 46 | 47 | # 安装 puppeteer 48 | RUN yarn config set registry https://registry.npmmirror.com 49 | RUN yarn add puppeteer@13.5.0 50 | 51 | # 复制项目文件到工作目录 52 | COPY . . 53 | 54 | # 暴露容器端口 55 | EXPOSE 3000 56 | 57 | # 运行Node.js应用 58 | CMD ["npm", "run", "start"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node hiprint pdf/image server 2 | 3 | Node server for generating [vue-plugin-hiprint](https://github.com/ccsimple/vue-plugin-hiprint) print templates in HTML, PDF, image . . 4 | 5 | ## framework 6 | 7 | - [node](https://nodejs.cn/) ^18.x 8 | - [puppeteer](https://pptr.nodejs.cn/) ^23.x 9 | - [fastify](https://fastify.dev/) ^4.x 10 | - [vue-plugin-hiprint](https://github.com/CcSimple/vue-plugin-hiprint) 11 | 12 | ## run 13 | 14 | ```bash 15 | # 1 16 | git clone https://github.com/CcSimple/node-hiprint-pdf.git 17 | # 2 18 | cd node-hiprint-pdf 19 | # 3 20 | npm i --registry https://registry.npmmirror.com 21 | # 4 22 | npm run start 23 | ``` 24 | 25 | ## docker 26 | 27 | ```bash 28 | docker compose up -d 29 | ``` 30 | 31 | ## api 32 | 33 | | type | api | desc | options | 34 | | ---- | ----- | ----- | ----------------------------------------------------------------------------------------- | 35 | | POST | /img | image | {template:{}, printData:{}, options:{}, url:'', noFile: false} | 36 | | POST | /pdf | pdf | {template:{}, printData:{}, options:{}, url:'', noFile: false} | 37 | | POST | /html | html | {template:{}, printData:{}, options:{}, url:'', domId: '#hiprintTemplate', noFile: false} | 38 | 39 | ## options 40 | 41 | - template: [vue-plugin-hiprint](https://github.com/CcSimple/vue-plugin-hiprint) 模板 json 42 | - printData: 打印数据 json 43 | - options: api 对应 puppeteer 配置 44 | - /img [https://pptr.dev/api/puppeteer.screenshotoptions](https://pptr.dev/api/puppeteer.screenshotoptions) 45 | - /pdf [https://pptr.dev/api/puppeteer.pdfoptions](https://pptr.dev/api/puppeteer.pdfoptions) 46 | - /html [nothing]() 47 | - url: 自定义渲染页面(替换内置的 public\vue-plugin-hiprint\index.html) 48 | - domId: 获取 html 时 指定的 节点. 默认: '#hiprintTemplate' 49 | - noFile: 是否不生成文件(img,pdf,html),true:不生成,false:生成. 默认 false 50 | 51 | ## example 52 | 53 | ```js 54 | import axios from "axios"; 55 | import template from "./template"; 56 | import printData from "./printData"; 57 | 58 | // pdf 59 | axios.post("/pdf", { 60 | template, 61 | printData, 62 | }); 63 | 64 | // img 65 | axios.post("/img", { 66 | template, 67 | printData, 68 | }); 69 | 70 | // html 71 | axios.post("/html", { 72 | template, 73 | printData, 74 | domId: "#hiprintTemplate", // 获取指定节点的 html 75 | }); 76 | ``` 77 | 78 | ## 在线体验 79 | 80 | > 2C4G 服务器,性能较弱.若无法正常访问,可以查看文章,获取 demo 源码. 81 | 82 | [https://mp.weixin.qq.com/s/a5JnxKnA7a4QVAyeCmBKbA](https://mp.weixin.qq.com/s/a5JnxKnA7a4QVAyeCmBKbA) 83 | 84 |  85 | 86 | #### 都看到这里了,点个 star 吧! 87 | 88 | ## di~ 89 | 90 |  91 | -------------------------------------------------------------------------------- /public/vue-plugin-hiprint/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 64 | 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !public/vue-plugin-hiprint/ 2 | public/files/ 3 | package-lock.json 4 | 5 | # File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig 6 | # Created by https://www.toptal.com/developers/gitignore/api/node 7 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 8 | 9 | ### Node ### 10 | # Logs 11 | logs 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | lerna-debug.log* 17 | .pnpm-debug.log* 18 | 19 | # Diagnostic reports (https://nodejs.org/api/report.html) 20 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 21 | 22 | # Runtime data 23 | pids 24 | *.pid 25 | *.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | lib-cov 30 | 31 | # Coverage directory used by tools like istanbul 32 | coverage 33 | *.lcov 34 | 35 | # nyc test coverage 36 | .nyc_output 37 | 38 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 39 | .grunt 40 | 41 | # Bower dependency directory (https://bower.io/) 42 | bower_components 43 | 44 | # node-waf configuration 45 | .lock-wscript 46 | 47 | # Compiled binary addons (https://nodejs.org/api/addons.html) 48 | build/Release 49 | 50 | # Dependency directories 51 | node_modules/ 52 | jspm_packages/ 53 | 54 | # Snowpack dependency directory (https://snowpack.dev/) 55 | web_modules/ 56 | 57 | # TypeScript cache 58 | *.tsbuildinfo 59 | 60 | # Optional npm cache directory 61 | .npm 62 | 63 | # Optional eslint cache 64 | .eslintcache 65 | 66 | # Optional stylelint cache 67 | .stylelintcache 68 | 69 | # Microbundle cache 70 | .rpt2_cache/ 71 | .rts2_cache_cjs/ 72 | .rts2_cache_es/ 73 | .rts2_cache_umd/ 74 | 75 | # Optional REPL history 76 | .node_repl_history 77 | 78 | # Output of 'npm pack' 79 | *.tgz 80 | 81 | # Yarn Integrity file 82 | .yarn-integrity 83 | 84 | # dotenv environment variable files 85 | .env 86 | .env.development.local 87 | .env.test.local 88 | .env.production.local 89 | .env.local 90 | 91 | # parcel-bundler cache (https://parceljs.org/) 92 | .cache 93 | .parcel-cache 94 | 95 | # Next.js build output 96 | .next 97 | out 98 | 99 | # Nuxt.js build / generate output 100 | .nuxt 101 | dist 102 | 103 | # Gatsby files 104 | .cache/ 105 | # Comment in the public line in if your project uses Gatsby and not Next.js 106 | # https://nextjs.org/blog/next-9-1#public-directory-support 107 | # public 108 | 109 | # vuepress build output 110 | .vuepress/dist 111 | 112 | # vuepress v2.x temp and cache directory 113 | .temp 114 | 115 | # Docusaurus cache and generated files 116 | .docusaurus 117 | 118 | # Serverless directories 119 | .serverless/ 120 | 121 | # FuseBox cache 122 | .fusebox/ 123 | 124 | # DynamoDB Local files 125 | .dynamodb/ 126 | 127 | # TernJS port file 128 | .tern-port 129 | 130 | # Stores VSCode versions used for testing VSCode extensions 131 | .vscode-test 132 | 133 | # yarn v2 134 | .yarn/cache 135 | .yarn/unplugged 136 | .yarn/build-state.yml 137 | .yarn/install-state.gz 138 | .pnp.* 139 | 140 | ### Node Patch ### 141 | # Serverless Webpack directories 142 | .webpack/ 143 | 144 | # Optional stylelint cache 145 | 146 | # SvelteKit build / generate output 147 | .svelte-kit 148 | 149 | # End of https://www.toptal.com/developers/gitignore/api/node 150 | 151 | # Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) 152 | -------------------------------------------------------------------------------- /routes/root.js: -------------------------------------------------------------------------------- 1 | import PuppeteerHtmlExport from "../lib/puppeteer-html-export.js"; 2 | import path from "path"; 3 | import fs from "fs"; 4 | import { fileURLToPath } from "url"; 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = path.dirname(__filename); 7 | const publicPath = path.join(__dirname, "..", "public"); 8 | 9 | const todayDir = () => { 10 | // yyyy-mm-dd 11 | const date = new Date(); 12 | const year = date.getFullYear(); 13 | const month = (date.getMonth() + 1).toString().padStart(2, "0"); 14 | const day = date.getDate().toString().padStart(2, "0"); 15 | const today = `${year}-${month}-${day}`; 16 | const dir = path.join(publicPath, "files", today); 17 | console.log(dir); 18 | if (!fs.existsSync(dir)) { 19 | fs.mkdirSync(dir, { recursive: true }); 20 | } 21 | return `files/${today}`; 22 | }; 23 | const randomId = () => { 24 | return Date.now() + "-" + Math.random().toString(36).slice(2); 25 | }; 26 | 27 | export default async function (fastify, opts) { 28 | fastify.get("/", async function (request, reply) { 29 | return "微信公众号: 不简说" + todayDir(); 30 | }); 31 | fastify.get("/template", async function (request, reply) { 32 | const cacheId = request.query.id; 33 | const value = await new Promise((resolve, reject) => { 34 | fastify.cacheman.get(cacheId, (err, value) => { 35 | if (err) throw err; 36 | resolve(value); 37 | }); 38 | }); 39 | if (value) { 40 | reply.send({ code: 1, msg: "ok", data: value }); 41 | } else { 42 | reply.send({ code: 0, msg: "no cache", data: null }); 43 | } 44 | }); 45 | fastify.post("/img", async function (request, reply) { 46 | try { 47 | const cacheId = randomId(); 48 | const cacheTime = request.query.time || 1 * 60; // 60s 49 | const data = request.body; 50 | console.log("cacheId", cacheId); 51 | console.log("query", request.query); 52 | console.log("data", data); 53 | const host = `${request.protocol}://${request.hostname}`; 54 | const url = data.url || `${host}/vue-plugin-hiprint/index.html`; 55 | fastify.cacheman.set(cacheId, data, cacheTime, (err, value) => { 56 | if (err) throw err; 57 | }); 58 | // 更多参数见: https://pptr.dev/api/puppeteer.screenshotoptions 59 | const name = `/${todayDir()}/${cacheId}.png`; 60 | const options = { 61 | fullPage: true, 62 | path: `${publicPath}${name}`, // 保存路径, 没有则不会保存文件, 保存文件 存在 io 异步问题 63 | ...data.options, 64 | }; 65 | if (data.noFile) { 66 | delete options.path; 67 | } 68 | console.log(options); 69 | const phe = new PuppeteerHtmlExport(); 70 | const base64 = await phe.screenshot(`${url}?id=${cacheId}`, options); 71 | const res = { 72 | code: 1, 73 | msg: "success", 74 | data: `${host}${name}`, // 返回保存的路径 75 | // data: `data:image/png;base64,${base64}`, 76 | }; 77 | if (data.noFile) { 78 | res.data = `data:image/png;base64,${base64}`; 79 | } 80 | reply.send(res); 81 | } catch (error) { 82 | console.log("createImage error", error); 83 | reply.send({ 84 | code: 0, 85 | msg: error, 86 | data: null, 87 | }); 88 | } 89 | return; 90 | }); 91 | fastify.post("/pdf", async function (request, reply) { 92 | try { 93 | const cacheId = randomId(); 94 | const cacheTime = request.query.time || 1 * 60; // 60s 95 | const data = request.body; 96 | console.log("cacheId", cacheId); 97 | console.log("query", request.query); 98 | console.log("data", data); 99 | const host = `${request.protocol}://${request.hostname}`; 100 | const url = data.url || `${host}/vue-plugin-hiprint/index.html`; 101 | fastify.cacheman.set(cacheId, data, cacheTime, (err, value) => { 102 | if (err) throw err; 103 | }); 104 | // 更多参数见: https://pptr.dev/api/puppeteer.pdfoptions 105 | const name = `/${todayDir()}/${cacheId}.pdf`; 106 | const options = { 107 | // width: "240mm", 108 | // height: "140mm", 109 | path: `${publicPath}${name}`, // 保存路径, 没有则不会保存文件, 保存文件 存在 io 异步问题 110 | ...data.options, 111 | }; 112 | if (data.noFile) { 113 | delete options.path; 114 | } 115 | console.log(options); 116 | const phe = new PuppeteerHtmlExport(); 117 | const buffer = await phe.createPdf(`${url}?id=${cacheId}`, options); 118 | const res = { 119 | code: 1, 120 | msg: "success", 121 | data: `${host}${name}`, // 保存文件的路径 122 | // data: buffer, // 返回 buffer 123 | }; 124 | if (data.noFile) { 125 | res.data = buffer; 126 | } 127 | reply.send(res); 128 | } catch (error) { 129 | console.log("createPdf error", error); 130 | reply.send({ 131 | code: 0, 132 | msg: error, 133 | data: null, 134 | }); 135 | } 136 | }); 137 | fastify.post("/html", async function (request, reply) { 138 | try { 139 | const cacheId = randomId(); 140 | const cacheTime = request.query.time || 1 * 60; // 60s 141 | const data = request.body; 142 | console.log("cacheId", cacheId); 143 | console.log("query", request.query); 144 | console.log("data", data); 145 | const host = `${request.protocol}://${request.hostname}`; 146 | const url = data.url || `${host}/vue-plugin-hiprint/index.html`; 147 | fastify.cacheman.set(cacheId, data, cacheTime, (err, value) => { 148 | if (err) throw err; 149 | }); 150 | // 更多参数见: https://pptr.dev/api/puppeteer.pdfoptions 151 | const name = `/${todayDir()}/${cacheId}.html`; 152 | const options = { 153 | ...data.options, 154 | }; 155 | console.log(options); 156 | const phe = new PuppeteerHtmlExport(); 157 | const htmlContent = await phe.htmlContent(`${url}?id=${cacheId}`, options); 158 | if (!data.noFile) { 159 | fs.writeFileSync(`${publicPath}${name}`, htmlContent); 160 | } 161 | const res = { 162 | code: 1, 163 | msg: "success", 164 | data: `${htmlContent}`, 165 | }; 166 | if (data.noFile) { 167 | res.data = buffer; 168 | } 169 | reply.send(res); 170 | } catch (error) { 171 | console.log("createHtml error", error); 172 | reply.send({ 173 | code: 0, 174 | msg: error, 175 | data: null, 176 | }); 177 | } 178 | }); 179 | } 180 | -------------------------------------------------------------------------------- /lib/puppeteer-html-export.js: -------------------------------------------------------------------------------- 1 | import puppeteer from "puppeteer"; 2 | // 往对象中添加键 3 | const omit = (obj, keys) => { 4 | const result = { ...obj }; 5 | keys.forEach((key) => { 6 | result[key]; 7 | }); 8 | return result; 9 | }; 10 | const isUrl = (url) => { 11 | if (!url) return false; 12 | if (url.startsWith("http://") || url.startsWith("https://")) return true; 13 | return false; 14 | }; 15 | 16 | export default class PuppeteerHtmlExport { 17 | constructor() { 18 | this.args = ["--no-sandbox", "--disable-setuid-sandbox"]; 19 | this.browser = null; 20 | this.options = {}; 21 | this.browserPromise = null; 22 | this.autoCloseBrowser = true; 23 | } 24 | 25 | async setOptions(options) { 26 | this.options = options; 27 | this.browserPromise = await this.initializeBrowser(); 28 | } 29 | 30 | async getPage() { 31 | await this.browserPromise; 32 | if (!this.browser) { 33 | throw new Error("Browser not initialized"); 34 | } 35 | const page = await this.browser.newPage(); 36 | this.setPageHeaders(page); 37 | return page; 38 | } 39 | 40 | /** 41 | * 前往url/html 等等,等待dom加载完成 42 | * @param {*} page 43 | * @param {*} content url/html 44 | */ 45 | async waitGoToDomContentLoaded(page, content) { 46 | const timeout = this.options.timeout ? { timeout: this.options.timeout } : {}; 47 | if (isUrl(content)) { 48 | await page.goto(content, { waitUntil: ["domcontentloaded", "networkidle0"], ...timeout }); 49 | } else { 50 | await page.setContent(content, { waitUntil: "networkidle0", ...timeout }); 51 | } 52 | } 53 | 54 | /** 55 | * 等等模板加载完成 56 | * @param {*} page 57 | * @param {*} loadImage 是否等待加载图片 58 | */ 59 | async waitTemplateLoaded(page, loadImage = true) { 60 | // 等待 hiprint 打印模板加载完成 61 | await page.waitForSelector(".hiprint-printTemplate", { visible: true, timeout: 60 * 1000 }); 62 | // 等待 图片 加载完成 63 | if (loadImage) { 64 | await page.evaluate(() => { 65 | var images = document.querySelectorAll("img"); 66 | function preLoad() { 67 | var promises = []; 68 | function loadImage(img) { 69 | return new Promise(function (resolve, reject) { 70 | if (img.complete) { 71 | resolve(img); 72 | } 73 | img.onload = function () { 74 | resolve(img); 75 | }; 76 | img.onerror = function (e) { 77 | resolve(img); 78 | }; 79 | }); 80 | } 81 | for (var i = 0; i < images.length; i++) { 82 | promises.push(loadImage(images[i])); 83 | } 84 | return Promise.all(promises); 85 | } 86 | return preLoad(); 87 | }); 88 | } 89 | } 90 | 91 | async createPdf(content, options = {}) { 92 | await this.setOptions(options); 93 | const page = await this.getPage(); 94 | await this.waitGoToDomContentLoaded(page, content); 95 | // 等待 hiprint 打印模板加载完成 96 | await this.waitTemplateLoaded(page); 97 | // 生成 PDF 98 | const pdfBuffer = await this.generatePDF(page); 99 | await this.closeBrowserIfNeeded(); 100 | return pdfBuffer; 101 | } 102 | 103 | async screenshot(content, options = {}) { 104 | await this.setOptions(options); 105 | const page = await this.getPage(); 106 | await this.waitGoToDomContentLoaded(page, content); 107 | // 等待 hiprint 打印模板加载完成 108 | await this.waitTemplateLoaded(page); 109 | // 生成截图 110 | const base64 = await this.generateScreenshot(page); 111 | await this.closeBrowserIfNeeded(); 112 | return base64; 113 | } 114 | 115 | async htmlContent(content, options = {}) { 116 | await this.setOptions(options); 117 | const page = await this.getPage(); 118 | await this.waitGoToDomContentLoaded(page, content); 119 | // 等待 hiprint 打印模板加载完成 120 | await this.waitTemplateLoaded(page, false); 121 | //// 获取整个html内容 122 | // const html = await page.content(); 123 | //// 获取页面body的 HTML 内容 124 | // const html = await page.$eval("body", (body) => body.innerHTML); 125 | // 获取指定元素的 HTML 内容 126 | const html = await page.$eval(options.domId || "#hiprintTemplate", (element) => element.innerHTML); 127 | await this.closeBrowserIfNeeded(); 128 | return html; 129 | } 130 | 131 | setAutoCloseBrowser(flag) { 132 | this.autoCloseBrowser = flag; 133 | } 134 | 135 | async closeBrowserIfNeeded() { 136 | if (this.browser && this.autoCloseBrowser) { 137 | await this.browser.close(); 138 | this.browser = null; 139 | } 140 | } 141 | 142 | async initializeBrowser() { 143 | if (this.browser) { 144 | return; 145 | } 146 | 147 | try { 148 | if (this.options?.args) { 149 | this.args = this.options.args; 150 | } 151 | const headless = this.options?.headless !== undefined ? this.options.headless : "new"; 152 | 153 | const launchOptions = { 154 | args: this.args, 155 | headless, 156 | }; 157 | 158 | if (this.options?.executablePath) { 159 | launchOptions.executablePath = this.options.executablePath; 160 | } 161 | 162 | this.browser = await puppeteer.launch(launchOptions); 163 | 164 | this.browser.on("disconnected", () => { 165 | this.browser = null; 166 | }); 167 | 168 | this.browser.on("error", (error) => { 169 | console.error("Browser error:", error); 170 | }); 171 | } catch (error) { 172 | throw new Error(`Failed to connect to browser: ${error.message}`); 173 | } 174 | } 175 | 176 | setPageHeaders(page) { 177 | const headers = { 178 | ...(this.options?.authorization ? { Authorization: this.options.authorization } : {}), 179 | ...(this.options?.headers || {}), 180 | }; 181 | 182 | if (Object.keys(headers).length > 0) { 183 | page.setExtraHTTPHeaders(headers); 184 | } 185 | } 186 | 187 | async generatePDF(page) { 188 | const data = await page.pdf({ 189 | ...omit(this.options, ["authorization", "executablePath", "args", "headless", "headers"]), 190 | printBackground: this.options.printBackground ?? true, 191 | }); 192 | return Buffer.from(data); 193 | } 194 | 195 | async generateScreenshot(page) { 196 | const havePath = this.options.path; 197 | const data = await page.screenshot({ 198 | ...omit(this.options, ["authorization", "executablePath", "args", "headless", "headers"]), 199 | encoding: havePath ? "binary" : "base64", 200 | }); 201 | // return Buffer.from(data); 202 | return data; 203 | } 204 | 205 | async closeBrowser() { 206 | if (this.browser) { 207 | await this.browser.close(); 208 | this.browser = null; 209 | } 210 | } 211 | 212 | async closeBrowserTabs() { 213 | const pages = await this.browser.pages(); 214 | for (let i = 1; i < pages.length; i++) { 215 | await pages[i].close(); 216 | } 217 | } 218 | } 219 | --------------------------------------------------------------------------------