6

Web打印探秘

 3 years ago
source link: https://zhuanlan.zhihu.com/p/59753593
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Web打印探秘

笔者最近做了一个在 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用于打印预览。如图所示,展示了谷歌首页的打印预览:

和 CSS 盒子模型一样,页面盒子模型由外边距 (margin)、边框 (border)、内边距 (padding) 和 内容区域 (content area) 构成:

有以下两点可以注意:

  • 打印页面时,只打印出页面的内容区域
  • 页面默认有页眉页脚信息,展现到页面外边距范围

默认情况下,页面是从左到右、从上到下展示,如果需要更改打印设备的方向,可以通过设置根元素的 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 :

页面默认有页眉页脚信息,展现到页面外边距范围,通过去除 页面模型 的外边距,使得内容不会延伸到页面的边缘,再通过设置 body 元素的 margin 来保证 A4 纸打印出来的页面带有外边距:

@media print {
	@page {
		margin: 0;
	}
	body {
		margin: 2cm;
	}
}

现在打印出来的页面不再具有默认的页底:

构建自定义的页眉页底

通过将对应的页眉、页底元素的 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 模板为例进行了一个简单的实践。

最后,笔者近期建立了一个技术交流群,欢迎大家在群里讨论技术,另外,帮团队打个广告,医疗健康事业部还在招聘前端、后台、数据工程师,有想加入腾讯医疗健康的朋友可以加群或者直接发送简历到 [email protected]




About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK