筆者最近作了一個在 Web 構建打印模板的需求,從中學習到一些有價值的東西,特意記錄一篇文章分享。
本文首先描述筆者所處的項目組的 Web 打印項目的需求背景,而後描述筆者在實踐 Web 打印項目的過程當中遇到了諸多問題,闡述 Web 打印的問題解決思路,最後給出了另一種 Web 打印的需求解決方案,即便用Headless browser
生成圖片並打印的方案。預計閱讀時間5 ~ 10分鐘。javascript
本文主要分下面幾個方面:css
基本概念html
產品經理小姐姐近期給筆者寫了這樣一個需求:前端
因而,爲了解決上述需求,筆者大概寫了這樣的一個模板,以下所示:java
<div class="page1"> <div>我是第一頁1</div> <div>我是第一頁2</div> <div>我是第一頁3</div> </div> <div class="page2"> <div>我是第二頁1</div> <div>我是第二頁2</div> <div>我是第二頁3</div> </div> <div class="page3"> <div>我是第三頁1</div> <div>我是第三頁2</div> <div>我是第三頁3</div> </div> <div class="page4"> <div>我是第四頁1</div> <div>我是第四頁2</div> <div>我是第四頁3</div> </div>
瀏覽器打印是一個很成熟的應用,最簡單的打印直接調用window.print()
或者是調用document.execCommand('print')
。此時,瀏覽器會彈出打印預覽的窗口,經過頁面生成了pdf
用於打印預覽。如圖所示,展現了谷歌首頁的打印預覽:git
和 CSS 盒子模型同樣,頁面盒子模型由外邊距 (margin)、邊框 (border)、內邊距 (padding) 和 內容區域 (content area) 構成:github
有如下兩點能夠注意:promise
默認狀況下,頁面是從左到右、從上到下展現,若是須要更改打印設備的方向,能夠經過設置根元素的 direction 和 writing-mode 屬性來改變頁面方向。瀏覽器
能夠經過三種方式引入打印樣式:less
@media print
:@media print { body { background-color:#FFFFFF; margin: 0mm; /* this affects the margin on the content before sending to printer */ } // ... }
media
屬性:<style type="text/css" media="print"> </style>
@import
:@import url("print.css") print;
link
標籤添加media
屬性:<link rel="stylesheet" media="print" href="print.css">
項目需求中首先遇到的問題是須要處理 Web打印 分頁問題。即便該部分未佔滿一頁紙的高度,也須要進行手動的分頁。起初,我經過計算頁面每一個部分的高度,在對應頁面部分的節點的高度下方預留一部分的外邊距來實現,以下代碼所示,經過查資料得知 A4紙的寬高比爲 297 : 210 ,除去頁面外邊距(左右各 20mm )來算得每一部分須要預留的高度:
const A4_HEIGHT_WIDTH_RATE = 297 / (210 - 2 * 13); // 打印區域長寬比:(A4紙高)比(A4紙寬減去左右側20mm的邊距) const PAGE_WIDTH = 680; // 頁面寬度(像素值) const PAGE_HEIGHT = PAGE_WIDTH * A4_HEIGHT_WIDTH_RATE; // 頁面高度 const $page1El = document.querySelector('.page1'); const page1Height = parseInt($page1El.clientHeight); // page1的高度是多少像素 const pageNum = Math.ceil(page1Height / PAGE_HEIGHT); // page1須要佔多少頁,超過1頁的高度,就須要佔2頁,所以向上取整 const marginBottom = pageNum * PAGE_HEIGHT - page1Height; // 須要預留多少外邊距 $page1El.style.marginBottom = `${marginBottom}px`;
可是,其實 CSS 早就支持了打印設備裏的分頁問題了,能夠經過設置break-after: page;
或 page-break-after: always;
實如今打印設備的分頁:
.page1 { break-after: page; page-break-after: always; } // ...
實現分頁的效果後,發現頁面打印會在頁底出現當前頁面的 url :
頁面默認有頁眉頁腳信息,展示到頁面外邊距範圍,經過去除 頁面模型 的外邊距,使得內容不會延伸到頁面的邊緣,再經過設置 body
元素的 margin
來保證 A4 紙打印出來的頁面帶有外邊距:
@media print { @page { margin: 0; } body { margin: 2cm; } }
如今打印出來的頁面再也不具備默認的頁底:
經過將對應的頁眉、頁底元素的 position
設置爲 fixed
能夠固定對應節點到頁面的任意一部分,它們也將在每一個打印頁面上重複。
.header { position: fixed; top: 0; } .footer { position: fixed; bottom: 0; }
上面說了那麼多,都是在前端實現的 Web 打印的解決方案,但實際上,若是能夠在後臺直接經過 Web 頁面,預先保存好的頁面模板,經過拉取後臺數據,並運行Headless browser
生成一張截圖,經過打印截圖就能夠解決這樣的問題了,下面以 phantomjs 配合 pug爲例,展現筆者使用Headless browser
生成圖片的簡單解決方案:
// 針對連接的截圖服務 // 返回phantom實例的promise對象,爲了獲取對應的base64編碼 function captureByUrl(url, data) { let instance; let page; const destroyInstance = () => { // 關閉頁面 page.close(); // 退出實例 instance.exit(); }; return phantom.create() // 首先,建立phantom實例 .then((_instance) => { instance = _instance; return instance.createPage(); }) .then((_page) => { page = _page; if (data.width && data.height) { // 設置phantom截圖頁面的寬高值 page.property('viewportSize', { width: data.width, height: data.height }); } page.setting('userAgent', 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36'); return page.open(url); }) .then(() => { return page.renderBase64('PNG'); // 渲染對應圖片,拿到base64字符串 }) .then((image) => { destroyInstance(); // 銷燬phantom實例 return image; }, (error) => { destroyInstance(); // 銷燬phantom實例 throw error; }); }
如上代碼所示,使用 Headless browser 打開一個連接,經過renderBase64
將對應頁面的預覽圖截圖生成base64字符串。
對應的,在服務端,能夠經過讀取預先寫好的pug
模板,傳入對應數據生成對應頁面預覽圖,再經過 Headless browser 生成截圖保存到本地,便可實現 Web 打印在服務端的解決方案,以下代碼所示,爲服務端讀取模板,並保存圖片的部分代碼:
// 針對模板和數據的截圖服務 function captureByTemplate(template, data) { const content = pug.compile(template)(Object.assign({ URL_PREFIX, }, data)); const contentInBase64 = new Buffer(content).toString('base64'); const url = `data:text/html;charset=utf8;base64,${contentInBase64}`; return captureByUrl(url, data); } captureByTemplate(fs.readFileSync('./print.pug', 'utf-8'), data) .then(base64Data => { fs.writeFile("out.png", base64Data, 'base64', function(err) { console.error(err); }); }) .catch(err => { console.error(err); });
本文爲筆者在實踐 Web 打印相關項目的項目總結,首先描述了 Web 打印項目通常需求,而後在打印設備下,頁面模型的展示形式;而後描述了筆者在實踐過程當中遇到的一些常見問題,給出一些通用性的解決方案。最後,聯想到 Headless browser 也可用於實現打印模板需求,筆者以 phantomjs 和 pug 模板爲例進行了一個簡單的實踐。
最後,筆者近期創建了一個技術交流羣,歡迎你們在羣裏討論技術,另外,幫團隊打個廣告,醫療健康事業部還在招聘前端、後臺、數據工程師,有想加入騰訊醫療健康的朋友能夠加羣或者直接發送簡歷到 counterxing@tencent.com 。