Open xingbofeng opened 5 years ago
笔者最近做了一个在 Web 构建打印模板的需求,从中学习到一些有价值的东西,特地记录一篇文章分享。
本文首先描述笔者所处的项目组的 Web 打印项目的需求背景,然后描述笔者在实践 Web 打印项目的过程中遇到了诸多问题,阐述 Web 打印的问题解决思路,最后给出了另外一种 Web 打印的需求解决方案,即使用Headless browser生成图片并打印的方案。预计阅读时间5 ~ 10分钟。
Headless browser
本文主要分下面几个方面:
产品经理小姐姐近期给笔者写了这样一个需求:
于是,为了解决上述需求,笔者大概写了这样的一个模板,如下所示:
<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用于打印预览。如图所示,展示了谷歌首页的打印预览:
window.print()
document.execCommand('print')
pdf
和 CSS 盒子模型一样,页面盒子模型由外边距 (margin)、边框 (border)、内边距 (padding) 和 内容区域 (content area) 构成:
有以下两点可以注意:
默认情况下,页面是从左到右、从上到下展示,如果需要更改打印设备的方向,可以通过设置根元素的 direction 和 writing-mode 属性来改变页面方向。
可以通过三种方式引入打印样式:
@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
<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;实现在打印设备的分页:
break-after: page;
page-break-after: always;
.page1 { break-after: page; page-break-after: always; } // ...
实现分页的效果后,发现页面打印会在页底出现当前页面的 url :
页面默认有页眉页脚信息,展现到页面外边距范围,通过去除 页面模型 的外边距,使得内容不会延伸到页面的边缘,再通过设置 body 元素的 margin 来保证 A4 纸打印出来的页面带有外边距:
body
margin
@media print { @page { margin: 0; } body { margin: 2cm; } }
现在打印出来的页面不再具有默认的页底:
通过将对应的页眉、页底元素的 position 设置为 fixed 可以固定对应节点到页面的任意一部分,它们也将在每个打印页面上重复。
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字符串。
renderBase64
对应的,在服务端,可以通过读取预先写好的pug模板,传入对应数据生成对应页面预览图,再通过 Headless browser 生成截图保存到本地,即可实现 Web 打印在服务端的解决方案,如下代码所示,为服务端读取模板,并保存图片的部分代码:
pug
// 针对模板和数据的截图服务 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 打印项目的需求背景,然后描述笔者在实践 Web 打印项目的过程中遇到了诸多问题,阐述 Web 打印的问题解决思路,最后给出了另外一种 Web 打印的需求解决方案,即使用
Headless browser
生成图片并打印的方案。预计阅读时间5 ~ 10分钟。本文主要分下面几个方面:
Web 构建打印模板需求
产品经理小姐姐近期给笔者写了这样一个需求:
于是,为了解决上述需求,笔者大概写了这样的一个模板,如下所示:
基本概念
打印设备接口
浏览器打印是一个很成熟的应用,最简单的打印直接调用
window.print()
或者是调用document.execCommand('print')
。此时,浏览器会弹出打印预览的窗口,通过页面生成了pdf
用于打印预览。如图所示,展示了谷歌首页的打印预览:页面模型
和 CSS 盒子模型一样,页面盒子模型由外边距 (margin)、边框 (border)、内边距 (padding) 和 内容区域 (content area) 构成:
有以下两点可以注意:
默认情况下,页面是从左到右、从上到下展示,如果需要更改打印设备的方向,可以通过设置根元素的 direction 和 writing-mode 属性来改变页面方向。
引入打印样式
可以通过三种方式引入打印样式:
@media print
:media
属性:@import
:link
标签添加media
属性:处理 Web打印 分页问题
项目需求中首先遇到的问题是需要处理 Web打印 分页问题。即使该部分未占满一页纸的高度,也需要进行手动的分页。起初,我通过计算页面每个部分的高度,在对应页面部分的节点的高度下方预留一部分的外边距来实现,如下代码所示,通过查资料得知 A4纸的宽高比为 297 : 210 ,除去页面外边距(左右各 20mm )来算得每一部分需要预留的高度:
但是,其实 CSS 早就支持了打印设备里的分页问题了,可以通过设置
break-after: page;
或page-break-after: always;
实现在打印设备的分页:去除浏览器默认的页眉页底
实现分页的效果后,发现页面打印会在页底出现当前页面的 url :
页面默认有页眉页脚信息,展现到页面外边距范围,通过去除 页面模型 的外边距,使得内容不会延伸到页面的边缘,再通过设置
body
元素的margin
来保证 A4 纸打印出来的页面带有外边距:现在打印出来的页面不再具有默认的页底:
构建自定义的页眉页底
通过将对应的页眉、页底元素的
position
设置为fixed
可以固定对应节点到页面的任意一部分,它们也将在每个打印页面上重复。使用 Headless browser 生成图片的解决方案
上面说了那么多,都是在前端实现的 Web 打印的解决方案,但实际上,如果可以在后台直接通过 Web 页面,预先保存好的页面模板,通过拉取后台数据,并运行
Headless browser
生成一张截图,通过打印截图就可以解决这样的问题了,下面以 phantomjs 配合 pug为例,展示笔者使用Headless browser
生成图片的简单解决方案:如上代码所示,使用 Headless browser 打开一个链接,通过
renderBase64
将对应页面的预览图截图生成base64字符串。对应的,在服务端,可以通过读取预先写好的
pug
模板,传入对应数据生成对应页面预览图,再通过 Headless browser 生成截图保存到本地,即可实现 Web 打印在服务端的解决方案,如下代码所示,为服务端读取模板,并保存图片的部分代码:小结
本文为笔者在实践 Web 打印相关项目的项目总结,首先描述了 Web 打印项目一般需求,然后在打印设备下,页面模型的展现形式;然后描述了笔者在实践过程中遇到的一些常见问题,给出一些通用性的解决方案。最后,联想到 Headless browser 也可用于实现打印模板需求,笔者以 phantomjs 和 pug 模板为例进行了一个简单的实践。
最后,笔者近期建立了一个技术交流群,欢迎大家在群里讨论技术,另外,帮团队打个广告,医疗健康事业部还在招聘前端、后台、数据工程师,有想加入腾讯医疗健康的朋友可以加群或者直接发送简历到 counterxing@tencent.com 。