Web列印探祕

counterxing發表於2019-03-19

筆者最近做了一個在 Web 構建列印模板的需求,從中學習到一些有價值的東西,特地記錄一篇文章分享。

概述

本文首先描述筆者所處的專案組的 Web 列印專案的需求背景,然後描述筆者在實踐 Web 列印專案的過程中遇到了諸多問題,闡述 Web 列印的問題解決思路,最後給出了另外一種 Web 列印的需求解決方案,即使用Headless browser生成圖片並列印的方案。預計閱讀時間5 ~ 10分鐘。

本文主要分下面幾個方面:

  • Web 構建列印模板需求
  • 基本概念
    • 列印裝置介面
    • 頁面模型
    • 引入列印樣式
  • 處理 Web列印 分頁問題
  • 去除瀏覽器預設的頁首頁底
  • 構建自定義的頁首頁底
  • 使用 Headless browser 生成圖片的解決方案

Web 構建列印模板需求

產品經理小姐姐近期給筆者寫了這樣一個需求:

  1. 實現一個列印報告的模板頁面,瀏覽器或客戶端呼叫列印裝置的介面列印出對應的報告。
  2. 對應報告支援報告模板配置,模板分幾種,例如免費玩家用極簡版、低保玩家用基本版、充值玩家用高階版、土豪玩家用頂配版。沒錯,充值才能變得更強。
  3. 需要實現分頁功能,支援把對應的內容展示到對應的頁。例如:內容A為基本資訊,需要展示到第一頁,低保玩家享受內容B,展示到第二頁...土豪玩家享受所有的功能,展示到第n頁。
  4. 展示產品配置的對應的頁首、頁底。

於是,為了解決上述需求,筆者大概寫了這樣的一個模板,如下所示:

<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用於列印預覽。如圖所示,展示了谷歌首頁的列印預覽:

Web列印探祕

頁面模型

和 CSS 盒子模型一樣,頁面盒子模型由外邊距 (margin)、邊框 (border)、內邊距 (padding) 和 內容區域 (content area) 構成:

Web列印探祕

有以下兩點可以注意:

  • 列印頁面時,只列印出頁面的內容區域
  • 頁面預設有頁首頁尾資訊,展現到頁面外邊距範圍

預設情況下,頁面是從左到右、從上到下展示,如果需要更改列印裝置的方向,可以通過設定根元素的 directionwriting-mode 屬性來改變頁面方向。

引入列印樣式

可以通過三種方式引入列印樣式:

  1. 使用 @media print
@media print {
	body {
        background-color:#FFFFFF;
        margin: 0mm;  /* this affects the margin on the content before sending to printer */
    }
    // ...
}
複製程式碼
  1. 內聯樣式使用media屬性:
<style type="text/css" media="print">
</style>
複製程式碼
  1. 在 CSS 中使用 @import
@import url("print.css") print;
複製程式碼
  1. HTML 中使用的link標籤新增media屬性:
<link rel="stylesheet" media="print" href="print.css">
複製程式碼

處理 Web列印 分頁問題

專案需求中首先遇到的問題是需要處理 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 :

Web列印探祕

頁面預設有頁首頁尾資訊,展現到頁面外邊距範圍,通過去除 頁面模型 的外邊距,使得內容不會延伸到頁面的邊緣,再通過設定 body 元素的 margin 來保證 A4 紙列印出來的頁面帶有外邊距:

@media print {
	@page {
		margin: 0;
	}
	body {
		margin: 2cm;
	}
}
複製程式碼

現在列印出來的頁面不再具有預設的頁底:

Web列印探祕

構建自定義的頁首頁底

通過將對應的頁首、頁底元素的 position 設定為 fixed 可以固定對應節點到頁面的任意一部分,它們也將在每個列印頁面上重複。

.header {
	position: fixed;
	top: 0;
}
.footer {
	position: fixed;
	bottom: 0;
}
複製程式碼

使用 Headless browser 生成圖片的解決方案

上面說了那麼多,都是在前端實現的 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 。

Web列印探祕

相關文章