信首發于微信公眾號「GitHub精選」,歡迎大家關注。
大家好,我是章魚貓。
今天給大家推薦的這個開源項目是來自于讀者的投稿,我超級喜歡這個開源項目。尤其是做小程序開發的時候,經常遇到將內容生成圖片分享到朋友圈。這個開源項目就能夠解決你的問題,可以將 html 轉為圖片,還可以轉為 PDF ,還支持加水印。
這個開源項目就是:Doctron,它是基于 Docker、無狀態、簡單、快速、高質量的文檔轉換服務。目前支持將 html 轉為 pdf、圖片 (使用 chrome (Chromium) 瀏覽器內核,保證轉換質量)。支持 PDF 添加水印。
作者認為目前開源界沒有較好的服務器端 HTML 轉 PDF、圖片的工具,像 wkhtmltopdf、dompdf、mpdf 等這些比較出名的轉換工具,對一些簡單 CSS 樣式的 HTML 轉換能做到不失真,對一些有復雜 CSS 樣式的 HTML 不能做到所見即所得。Doctron 使用 chrome 內核恰巧彌補了這些缺點。
開源項目作者還提供了體驗網站:
您可以打開下面的鏈接在線體驗轉換質量,由于服務器配置較低,以及網絡原因,轉換可能會慢一點,實際部署到服務器速度會不一樣。
項目體驗地址:http://doctron.lampnick.com/
開源項目特性如下:
安裝和使用步驟如下:
開源項目地址:https://github.com/lampnick/doctron
開源項目作者:lampnick
公眾號:「GitHub 精選」,值得你關注,每天都分享開源項目,挖掘開源的價值。
文概述:如何使用 nodejs 在服務端將 html 批量轉成 pdf 并客戶端下載。
目標一:使用 node 在服務端實現 html 批量轉成 pdf
分為兩步:
自己轉(存疑)?或者找個中間件。自己轉的話。。。算了,找插件。
網上搜到了 html-pdf,看了下周下載量接近 7W,版本迭代 21 個,最近更新時間 latest,不錯,整體滿足心里預期。
api使用如圖:
提供了三種接口:文件、stream 流、buffer。不同返回值類型,使用 toFile()需要 2 個參數:文件地址(末尾要有文件名)、回調函數。
toFile()結果會在指定文件夾生成 pdf 文件;
toStream()、toBuffer()可以在回調里拿到pdf文件數據流
目標二:將 PDF 傳到客戶端
怎么傳輸?
經過分析歸納,可以濃縮為 3 個點:
以下分析可以略過直接到結果:
分析:測試用的是 express 框架,那么可以排除原生 res.end()(存疑),為啥排除它?有 send 我干嘛還用 end;將 pdf 傳向客戶端又不是下載或寫入文件,排除 createWriteStream()方法;PDF 文件可以自己跑過去嗎?肯定不行!如果啥都不動能不能成功下載文件?也可以。鏈接下載,好像挺萬能。base64 能用嗎?。。。好像是圖片在用!不知道對文件好使不好使(存疑)。
篩選后:
傳輸方法:res.send()、管道 pipe();
傳輸內容:stream 流、buffer、下載鏈接;
使用下載鏈接:
經過分析了,下載鏈接肯定能實現這個功能!為什么呢?只要把 pdf 的文件路徑用變量存下來,然后返回給前端,拿著絕對路徑地址模擬點擊下載應該就能實現!但這種方法并沒有在真正意義上跟要傳輸的內容打交道,所以還值得繼續探索。
最終決定使用,管道pipe()發送stream流輸出PDF文件流到客戶端
目標三:客戶端可以成功接收并自動下載
網上搜索有:Windows.open()方法,iframe 等。感覺都有點偏。
本文使用a標簽實現,其他方法另行嘗試。
ajax + a標簽
$.ajax({
url:'url',
responseType:'blob',
success (res) {
console.log(res.toString('utf-8'));
// 創建 blob 對象,解析流數據
// , {
// 如何后端沒返回下載文件類型,則需要手動設置:type: 'application/pdf;chartset=UTF-8' 表示下載文檔為 pdf,如果是 word 則設置為 msword,excel 為 excel
// type: 'application/pdf;chartset=UTF-8'
// }
const blob = new Blob([res]);
const a = document.createElement('a')
// // 兼容 webkix 瀏覽器,處理 webkit 瀏覽器中 href 自動添加 blob 前綴,默認在瀏覽器打開而不是下載
const URL = window.URL || window.webkitURL
// // 根據解析后的 blob 對象創建 URL 對象
const herf = URL.createObjectURL(blob)
// 下載鏈接
// a.href = 'url'
a.href = herf
// // 下載文件名,如果后端沒有返回,可以自己寫 a.download = '文件.pdf'
a.download = '文件.pdf'
// document.body.appendChild(a)
a.click()
// document.body.removeChild(a)
// 在內存中移除 URL 對象
window.URL.revokeObjectURL(herf)
}
})
傳統jQuery接收數據流后,需要使用new Blob([res])再處理才能繼續使用,
而,fetch也需要,不過簡便多了
fetch+ a標簽
//1.請求
let res = await fetch([地址]);
//2.解析
let data = res.blob()
//3.創建a標簽
let eleA = document.createElement('a')
//4.創建鼠標事件對象
let e = document.createEvent("MouseEvents")
//5.初始化事件對象
e.initEvent("click", false, false)
?
?
document.body.appendChild(eleA)
eleA.download = 'index.zip'
eleA.href = data
//給指定的元素,執行事件 click 事件
eleA.dispatchEvent(e)
document.body.removeChild(eleA)
報錯:
eleA.href直接使用 blob 數據作為下載鏈接是不行的,必須使用 window.URL 對象,果然無知者無畏,更改后的代碼如下:
//1.請求
let res=await fetch([地址]);
//2.解析
let data=await res.blob();
const a = document.createElement('a')
const URL = window.URL || window.webkitURL
const herf = URL.createObjectURL(data)
a.href = herf
a.download = '文件.zip'
a.click()
window.URL.revokeObjectURL(herf)
使用 oFile() 測試
server.get('/', async (req, res) => {
toPdf(res);
});
?
?
function toPdf (res) {
let archive = archiver('zip', {
zlib: { level: 9 } // 設置壓縮級別
});
archive.on('error', function(err){
throw err;
});
archive.pipe(res);
for (let i = 0; i < 100; i++) {
let html = `<div style="width: 100px;height:100px;background:#fff;color:red;font-size:16px;font-weight:bold">這是第 <span>${i}</span> 個 pdf</div>`;
pdf.create(html, options).toFile(`./static/${i}.pdf`, () => {
archive.glob('static/*')
archive.finalize()
})
}
}
看命令窗口偶爾還會報 Queue Closed 錯誤;
觀察發現程序一邊轉 pdf,一邊下載,而且是按照順序轉換下載有 0.1.2.3...,最后壓縮返回,這個過程循環很少時發現沒問題,但次數增加很多后如20,100次,當for循環到最后一次時,直接執行archiver.finalize(),完。。。結束了,所以造成Queue Closed原因是沒限制archiver.finalize()執行時機???
找證據:
官方文檔顯示:
// finalize the archive (ie we are done appending files but streams have to finish yet)
// 完成存檔
// 'close', 'end' or 'finish' may be fired right after calling this method so register to them beforehand
// 調用此方法后,可能會立即觸發“ close”,“ end”或“ finish”,因此請事先注冊
archive.finalize();
果然,,
添加結束條件:
pdf.create(html, options).toFile(`./static/${i}.pdf`, () => {
if (i === 99) {
archive.glob('static/*')
archive.finalize()
}
})
執行成功,Queue Closed沒出現,并且在瀏覽器自動下載了一個壓縮包,打開后
發現只有 91 個文件,缺少了最后幾個文件,
for 循環明明執行了 100 次。百思不得解,開始我以為跟網絡有關系,畢竟有傳言只要(網絡)夠快什么(隊列關閉)錯誤都追不上你。后來我發現用筆記本執行程序這個錯誤會頻繁出現,
查看了 html-pdf 源碼猜測會不會是同步在阻塞,導致循環結束后 PDF 生成文件還未完成導致 Queue Closed,怎么解決呢,閉包!具體原理未知(存疑),
經過修改
((html, i) => {
pdf.create(html).toFile(`./static/${i}.pdf`, () => {
if (i === 49) {
archive.glob('static/*')
archive.finalize();
}
})
})(html, i)
成功!!!打開壓縮文件查看文件總數,
一切正常,Good!
使用 toStream()測試
(function(html, i) {
pdf.create(html).toStream((err, stream) => {
archive.append(stream, { name: `${i}.pdf`});
if (i === 99) {
archive.finalize();
}
});
})(html, i)
我去,第一次跑果然有問題,反復執行多次總是缺少前幾個文件
第一次跑,少了第一個文件 1.pdf:
第二次,循環執行 50 次,結果只有 45 個成功,前 5 個失敗:
命令行打印也證明了這一點。。。每次失敗個數竟然還不同,但還好有個規律它們都是前幾個
。。。
繼續執行,竟然還有其他報錯類型。。。無語。。
大致意思是:輸入資源必須是 Stream 流或者 Buffer,可能是在使用 archiver.append 時塞入了 undefined 之類的,給它上個保險,如果值存在才執行
stream && archive.append(stream, { name: `${i}.pdf`});
綜合問題共有 2 個:1.并不是所有 html 片段都進行了轉換 PDF 的操作,可能會隨機出現遺漏,比如 45.46.突然就到 48;2.即使所有 html 都進行了轉換操作,還總是缺少前面幾個文件。
問題一解決:
html-pdf 源碼如下圖
研究發現 html-pdf 的 toStream 應該是一個異步方法,查看源碼后 stream.on('end')也證明了這一點。由于不是順序生成 stream 流,那么最后一個流生成并不代表所有都完成,所以當用 i===99 判斷結束就有問題,可能會跳過某一個不執行轉換PDF需要len=50來避免,然而添加過后,每個文件都進行了PDF轉換,但是stream顯示為undefined,進而PDF文件也總是少前面幾個,導致 i=99 出現時還有好多 toStream 沒有完成,SO 第一個問題就出現了;(注:不過異步操作也因此避免同步帶來方法一的問題:隊列關閉錯誤。)
原因找到,解決辦法就是添加變量手動強行控制進程:
如圖,len 初始 100,減為 0 時代表所有都已經轉換,可以結束
let len = 100;
然后,開始計數,不到最后一個完成 stream 流轉換不結束
pdf.create(html).toStream((err, stream) => {
len--;
stream && archive.append(stream, { name: `${i}.pdf`});
if (len === 0) {
archive.finalize();
}
});
命令行打印下錯誤信息看看怎么回事控制臺
問題二解決:
先打印下錯誤信息 console.log('err:', err)
暴露了,PDF generation timeout. 一個 timeout 已經夠了,能夠說明很多問題,html-pdf 提供了轉換超時限制,時間超出 timeout 自然無法成功轉換成 stream 流輸出,解決辦法更簡單:
官方文檔,給出了一個配置項 options:{}對象,其中就有 timeout 設置,我們可以視情況放大此參數,
// 一分鐘內轉 pdf 不成功,則視為失敗
let options = {
timeout: 60000
}
pdf.create(html, options).toStream((err, stream) => {
。。。。。。
});
繼續測試,終于完成。。。
結語:
1.本文走了許多彎路,踩了多多個坑;
2.文中標記存疑的地方依然有很多,都是等待去學習理解的地方;
3.對于名詞、問題的解釋描述不夠精準透徹,需要深度挖掘對知識點的認知;
4.對問題的解決方式不夠標準、熟練,這才是造成多走彎路的原因;
5.雖然解決問題才是我們的最終目的,但是仍需追求解決方式的多樣化,找到問題的根源 格物致知 才能給自己醍醐灌頂之感;
家好,很高興又見面了,我是"高級前端?進階?",由我帶著大家一起關注前端前沿、深入前端底層技術,大家一起進步,也歡迎大家關注、點贊、收藏、轉發!
react-print-pdf 使用適用于 PDF 和打印文檔的 React UI 套件構建和生成 PDF。 簡單、可重復使用的組件和模板可創建出色的發票、文檔、小冊子。 同時,允許開發者使用最喜歡的前端框架 React 構建。
react-print-pdf 使用一系列高質量、無樣式的組件,用于使用 React 和 TypeScript 創建漂亮的 PDF,可以完全替代 docx、latex 等令人痛苦和過時庫。 借助 react-print-pdf,采用一種由開發人員設計并為開發人員設計的 PDF 創建新方法。
與其他解決方案不同,react-print-pdf 使開發者可以完全控制文檔,允許設計具有腳注、標題、頁邊距等功能的復雜布局。 此外,react-print-pdf 還使開發者能夠跟蹤和分析文檔的特定部分,并使用數據庫中的數據構建和更新圖表。 同時,react-print-pdf 團隊和社區將繼續開發出色的功能來簡化 PDF 生成過程。
react-print-pdf 的典型特征包括:
目前 react-print-pdf 在 Github 通過 Apache-2.0 協議開源,是一個值得關注的前端開源項目。
首先需要安裝相應的依賴:
npm install @onedoc/react-print
// npm
yarn add @onedoc/react-print
// yarn
pnpm add @onedoc/react-print
// pnpm
接著從預構建組件列表中將需要的組件導入 PDF 模板:
import {PageTop, PageBottom, PageBreak} from "@onedoc/react-print";
接著集成組件:
export const document = ({props}) => {
return (
<div>
<PageTop>
<span>Hello #1</span>
</PageTop>
<div>Hello #2</div>
<PageBottom>
<div className="text-gray-400 text-sm">Hello #3</div>
</PageBottom>
<PageBreak />
<span>Hello #4, but on a new page ! </span>
</div>
);
};
react-print-pdf 允許將 CSS 添加到文檔,同時安全地解析和轉義。
但是值得注意的是,雖然可以使用 <style> 標簽添加常規 CSS,但建議使用 CSS 組件來確保正確轉義 CSS,尤其是在使用 URL 或其他潛在不安全內容時。
import {CSS} from "@onedoc/react-print";
<CSS>{`@page { size: a4 landscape; }`}</CSS>;
下面是 CSS 的內容:
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap");
html,
body {
font-size: 28px;
font-family: "Inter", sans-serif;
}
@page {
size: A4;
}
react-print-pd 還支持直接在 React 組件中渲染 LaTeX 公式,只需要導入相應的組件即可:
import {Latex} from "@onedoc/react-print";
<Latex>{String.raw`\frac{1}{2}`}</Latex>;
在模板中渲染 Markdown 只需要提供一個簡單的 markdown-to-jsx 包裝器。
Markdown 允許開發者輕松地將內容與布局分開,從而更輕松地維護和更新模板。 開發者可以從 CMS 或其他來源提取內容,并使用 Markdown 對其進行格式化。
開發者還可以使用自定義組件和變量使 Markdown 更加動態。 例如,可以用自己的組件替換 Markdown 組件,或者使用變量插入動態內容。
import {Markdown} from "@onedoc/react-print";
<Markdown>{`# Hello, world!
> This is a blockquote
---
This is a paragraph with a [link](https://google.com)`}</Markdown>;
值得一提的是,使用 react-print-print 設計的 PDF 可以由首選的文檔管理提供商生成、托管(等等),比如:
本文主要和大家介紹 react-print-pdf ,其使用一系列高質量、無樣式的組件,用于使用 React 和 TypeScript 創建漂亮的 PDF,可以完全替代 docx、latex 等令人痛苦和過時庫。因為篇幅問題,關于 react-print-pdf 只是做了一個簡短的介紹,但是文末的參考資料提供了大量優秀文檔以供學習,如果有興趣可以自行閱讀。如果大家有什么疑問歡迎在評論區留言。
https://github.com/OnedocLabs/react-print-pdf
https://react.onedoclabs.com/introduction
https://www.dhiwise.com/post/how-to-simplify-react-pdf-handling-with-react-pdf-renderer
*請認真填寫需求信息,我們會在24小時內與您取得聯系。