CSS 打印属性
介绍 CSS 打印相关的属性。
print 媒体查询
通过媒体查询在打印模式下应用不同的 CSS,有两种方式:
<style>
@media print {
/* 在打印模式下生效的 CSS */
}
</style>
<!-- 打印模式下加载应用此 css 文件 -->
<link href="/path/to/print.css" media="print" rel="stylesheet" />
@page
@page 规则,可以控制打印页面的各项属性,包括每页大小、边距、页面方向等:
@page {
size: 10mm 10mm; /* 控制所有页面宽高为 10 毫米 */
margin-top: 10pt;
}
@page :left {
size: 15in; /* 控制所有偶数页宽高为 15 英寸 */
}
@page :rigth {
size: a4 landscape; /* 控制所有奇数页的页面大小与方向 */
}
@page :first {
/* 应用于第一页 */
}
@page :blank {
/* 应用于空白页 */
}
.simple-page {
page: selector; /* 声明一个名为 selector 的 page */
}
@page selector {
/* 选中一个名为 selector 的 page */
}
@page 规则受支持的属性有限,完整的支持列表看 MDN#@page。
@page 支持 4 个伪类参数:
@page :left
控制偶数页的页面属性@page :right
控制奇数页的页面属性@page :first
控制第一页的页面属性@page :blank
控制空白页的页面属性
注意 @page 规则不能内嵌 CSS 选择器,比如下列 CSS 本意是隐藏第一页的页脚元素,但语法不受支持:
@page :first {
/* 不被支持的语法 */
.footer {
display: none;
}
}
page
page 属性用于声明一个指定名称的页面:
.simple-page {
page: main;
}
break-*
CSS 提供了三个用于控制分页逻辑的属性:
- break-before
- break-after
- break-inside
他们的属性值大部分是通用的,有两个常用的属性值:
- always:始终在遇到指定元素前、后插入分页符
- avoid:避免在指定元素前、后、内部插入分页符
假设我们有多个标签页,需要将这些标签页生成为一个 PDF,但每个标签页应该是一页而不能粘连,就可以使用分页属性来实现:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Label</title>
<style>
@media print {
/* 选中名为 main 的 page */
@page main {
margin: 10px;
/* 页面大小为 10x15 */
size: 100mm 150mm;
font-size: 12px;
line-height: 2;
color: #333;
font-weight: 500;
}
.container-box {
/* 声明一个 main 的 page */
page: main;
/* 每次遇到 container-box 后进行分页 */
break-after: always;
/* container-box 内部的元素是紧凑的,不允许分割 */
break-inside: avoid;
}
.container {
width: 358px;
height: 540px;
border: 1px solid #000;
}
.header {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 120px;
border-bottom: 2px solid #000;
}
.content {
padding: 10px;
height: calc(540px - 124px);
}
.title {
padding: 10px;
margin-top: 12px;
font-size: 16px;
letter-spacing: 2px;
}
}
</style>
</head>
<body>
<!-- 一页标签 -->
<div class="container-box">
<main class="container">
<header class="header">
<h1 class="title">TITLE</h1>
</header>
<section class="content">
<p>Content</p>
</section>
</main>
</div>
<!-- 一页标签 -->
<div class="container-box">
<main class="container">
<header class="header">
<h1 class="title">TITLE</h1>
</header>
<section class="content">
<p>Content</p>
</section>
</main>
</div>
<!-- 一页标签 -->
<div class="container-box">
<main class="container">
<header class="header">
<h1 class="title">TITLE</h1>
</header>
<section class="content">
<p>Content</p>
</section>
</main>
</div>
<!-- 一页标签 -->
<div class="container-box">
<main class="container">
<header class="header">
<h1 class="title">TITLE</h1>
</header>
<section class="content">
<p>Content</p>
</section>
</main>
</div>
</body>
</html>
效果如下:
上诉 HTML 在遇到包含类名 container-box
的元素时认为需要进行分页,由 break-after: always
完成;而 break-inside: avoid
保证 container-box
元素是高度聚合的,不允许内部元素跨页。
我们试试去掉这两个分页控制元素,得到的效果如下:
去掉了分页控制元素后得到的效果很明显不尽人意。
页面边距区域
打印分页除了内容区外,还有 16 个围绕在内容区的边距区域,如下图:
我们可以通过 @ + 指定方位来控制对应区域的内容:
@page :first {
@top-center {
content: 'My Book';
}
}
上述代码在第一页的顶部中间区域添加了一个标题,内容为 “My Book”。
注意,目前浏览器中只有 Chrome 131 版本实现了页面边距区域的特性。
除了页面边距区域外,未来的 CSS 分页模块还会添加:设置字符传、脚注、交叉引用等功能。
内置的页面计数器
CSS 打印内置了两个分页相关的计数器,分别为 page
和 pages
,表示当前页与总页数,我们可以这样使用他:
@media print {
@page :left {
@bottom-left {
content: 'Page ' counter(page) ' of ' counter(pages);
}
}
@page :right {
@bottom-right {
content: 'Page ' counter(page) ' of ' counter(pages);
}
}
}
上述 CSS 在每页底部添加了类似 Page 1 of 13
的内容,表示当前第几页以及总共有几页,对于偶数页而言添加的内容在左下,对于奇数页而言在右下。
PDF 生成
使用 Puppeteer 控制无头浏览器生成一些简单的 PDF:
- 编写 HTML 模板,通过 CSS 控制页面大小与分页
- 模板渲染工具动态生成 HTML,进行数据的填充
- 调用 Puppeteer 的 API,生成 PDF
这种方式有几个显而易见的好处:
- HTML + CSS 对于内容的排版和样式是很友好的
- 缩进开发周期,HTML 基本可以做到所见即所得
- 易于调试,只要一个浏览器就能实现大部分功能
- PDF 内容清晰,且每块内容是各自独立可选中的(如果由前端使用 HTML2Canvas 之类的库生成图片并添加至 PDF,可能会出现内容模糊的情况,且图片作为一个整体,无法进行内容的修改)
我们以 Node 中的 Puppeteer 看一下如何实现打印功能(Puppeteer 实现了多语言的版本,可以自行查找):
import puppeteer from 'puppeteer';
(async () => {
// 启动浏览器
const browser = await puppeteer.launch();
// 打开一个新的页面
const page = await browser.newPage();
// 获取动态渲染的页面内容
const html = /**/;
// 将页面内容替换为指定内容
await page.setContent(html);
// 生成并获取 PDF 内容
const uint8Array = await page.pdf();
console.log('PDF byte data: ' uint8Array);
await browser.close();
})();
上述代码中,pdf 方法支持传递一个选项参数,基本与浏览器打印中选项能够对应:
// 只展示部分字段...
interface PDFOptions {
// 是否展示页面页脚,大部分时候不需要
displayHeaderFooter: boolean;
// 页脚内容
footerTemplate: string;
// 一个枚举值,理解为纸张大小或分页大小,优先于宽度与高度设置
format: Enum;
// 页眉内容
headerTemplate: string;
// 分页宽度,可以是带单位的字符串
width: string | number;
// 分页高度,可以是带单位的字符串
height: string | number;
// 是否横版内容
landscape: boolean;
// 页面边距
margin: {
bottom: string | number;
left: string | number;
right: string | number;
top: string | number;
};
// 是否以 html 中 @page 定义的 size 为准设置分页的尺寸
preferCSSPageSize: boolean;
}
这里要注意 margin
和 preferCSSPageSize
属性:
- 如果没有指定
margin
参数,则默认以 CSS 中定义的边距值为准,这在大部分时候是符合我们预期的 - 当你的页面大小是自定义尺寸时,一定要设置
preferCSSPageSize: true
,这样生成的 PDF 每页尺寸才是符合预期的
最后说说一些需要注意的地方:
- 打印模式下
position: fixed
的行为是相对于所有分页去定位,即一个 fixed 元素在视觉上会出现在所有分页中。 - 想在多页打印中将一个元素固定在最后一页的底部目前没有什么很好的方法,原因如上所述(这也是我认为这种模式只能生成简单 PDF 的原因)
参考内容
- Add content to the margins of web pages when printed using CSS
- Designing For Print With CSS
- CSS paged media
-- end