前端老哥必看:window.print 只打半截?一招搞定 HTML 实际高度打印不踩坑
兄弟,你是不是也遇到过这种鬼故事?页面上明明有十几屏的内容,一点打印按钮,出来的 PDF 就只有当前屏幕那一截,后面的东西全没了。
我跟你讲,这事儿我熟得很。去年做电商后台,产品经理甩过来一个需求:'做个订单打印功能,要能把订单详情、商品列表、物流信息一次性打出来'。我当时心想,这不就 window.print() 一行代码的事儿吗,分分钟搞定。结果上线第二天,产品经理举着一张只打了半截的 A4 纸杀到我工位。
'后面的商品呢?我的二十几个商品怎么只显示了前三个?'
打开浏览器一看,果然,打印预览里只有可视区域那一块,下面全是白的。后来一查资料,原来浏览器的打印机制就是这么'懒'——它默认只打印当前视口(viewport)里的内容。换句话说,你没滚动到的地方,它觉得你不重要,直接给你忽略了。
今天咱们聊聊怎么让 window.print 这个祖宗乖乖听话,把整个 HTML 的身体都给你吐出来。
这玩意儿到底是个啥妖魔鬼怪
浏览器打印机制那点不为人知的秘密
浏览器在接收到打印指令时,会干这么几件事:
- 克隆当前文档:它会先复制一份当前的 DOM 结构
- 应用打印样式:把
@media print 里的 CSS 规则套上去
- 重新布局计算:根据纸张尺寸(通常是 A4)重新计算布局
- 分页处理:把内容切成一页一页的
- 生成打印预览:最后呈现给你看
问题就出在第 3 步。浏览器在重新布局的时候,对于那些 height: auto 或者没有明确高度的容器,它有时候会犯迷糊。特别是当你的内容是通过 JavaScript 动态加载的,或者图片是懒加载的,浏览器算高度的时候可能还没加载完,结果就是算出来的高度比实际小,后面的内容就被'截断'了。
CSS 里的 print 媒体查询,是救星还是坑货?
很多人一遇到打印问题,第一反应就是加 @media print 媒体查询。这思路没错,但用得不好就是给自己挖坑。
最常见的写法是这样的:
@media print {
body {
height: auto;
overflow: visible;
}
.no-print {
display: none;
}
}
但实际上,height: auto 在打印上下文里有时候并不听话。因为浏览器在打印模式下,会创建一个叫'打印上下文'(print context)的东西,在这个上下文里,视口概念被纸张尺寸取代了。如果你的 body 或者某个父容器在屏幕模式下被设置了固定高度或者 overflow: hidden,到了打印模式下这些属性可能还残留着,导致内容被截断。
我实测过几个浏览器的差异:
- Chrome:相对听话,只要给 body 加上
height: auto 和 overflow: visible 基本能解决大部分问题
- Firefox:有点轴,有时候需要显式地给 html 也加上同样的样式
- Safari:最矫情,经常需要手动触发重绘才能拿到正确的高度
- Edge(老版):建议直接放弃治疗,让用户导出 PDF 吧
深挖底层逻辑,把打印机按在地上摩擦
height: auto 失效?布局塌陷的锅谁来背
咱们来深入看看为什么 height: auto 在打印时会失效。这得从 CSS 的包含块(containing block)机制说起。
在正常屏幕显示时,body 的包含块是视口(viewport),所以 height: 100% 就是视口高度。但在打印模式下,包含块变成了页面框(page box),也就是一张纸的大小。这时候如果某个父元素还是按照屏幕逻辑来设置高度,就会出现问题。
举个例子,假设你有这么个结构:
<div class="wrapper">
<div class="content"></div>
</div>
屏幕模式下 CSS 可能是这样的:
.wrapper {
height: 100vh;
overflow-y: auto;
}
.content {
height: auto;
}
到了打印模式下,如果你只给 body 加样式,忘了处理 wrapper:
@media print {
body {
height: auto;
overflow: visible;
}
}
这时候 wrapper 还是那个 100vh 的高度,overflow 虽然默认是 visible,但有些浏览器会保留滚动容器的特性,结果 content 的高度被 wrapper 限制住了,超出部分就被截断了。
正确的做法是要把所有可能限制高度的容器都'松绑':
@media print {
html, body {
height: auto !important;
overflow: visible !important;
margin: 0;
padding: 0;
}
.wrapper,
.container,
.main,
#app {
height: auto !important;
max-height: none !important;
overflow: visible !important;
}
.flex-container {
display: block !important;
}
}
看到那些 !important 了吗?别嫌粗暴,打印的时候就得下猛药。因为你不知道哪个第三方库的 CSS 会突然冒出来给你一刀。
强制分页符的正确打开方式
长内容打印肯定要分页,但分页的时机很重要。你不能让表格在一行中间断开,也不能让标题孤零零地留在页尾。
CSS 提供了几个控制分页的属性:
page-break-before: 在元素前分页
page-break-after: 在元素后分页
page-break-inside: 控制元素内部是否允许分页(最常用的是 avoid)
但是!这里有个大坑。很多人这样写:
<div style="page-break-after: always;"></div>
插一个空 div 来强制分页。这办法能用,但太丑了,而且会破坏你的 HTML 结构。更优雅的做法是用伪元素:
.page-break {
page-break-after: always;
position: relative;
}
h2 {
page-break-before: auto;
page-break-after: avoid;
orphans: 3;
widows: 3;
}
对于表格,防止行被截断的终极方案:
@media print {
table {
page-break-inside: auto;
border-collapse: collapse;
}
tr {
page-break-inside: avoid;
page-break-after: auto;
}
thead {
display: table-header-group;
}
tfoot {
display: table-footer-group;
}
}
这里要重点说说 orphans 和 widows 这两个属性,知道的人不多,但超级有用。它们控制的是段落被分页时,留在当前页和带到下一页的最小行数。比如设置 orphans: 3,意思就是如果分页后当前页剩下的行数少于 3 行,那就把整个段落都带到下一页去。这样就不会出现页尾孤零零的一两行文字,看着特别丑。
动态内容高度计算,别让 JS 骗了打印机
现在的前端项目,哪个不是满屏的 JavaScript 动态渲染?React、Vue、Angular,数据都是异步来的。这时候打印就会遇到一个经典问题:你触发 window.print() 的时候,数据还没渲染完呢。
比如这样的代码:
function handlePrint() {
fetchData().then(data => {
setState(data);
window.print();
});
}
你以为 setState 之后页面就更新了?Too young too simple。React 的 setState 是异步的,你这时候打印,可能 DOM 还没更新完呢。结果就是打印出来的还是老数据,或者只有半截。
正确的姿势是要等渲染完成。在 React 里可以用 setTimeout 或者 requestAnimationFrame:
function handlePrint() {
fetchData().then(data => {
setState(data);
setTimeout(() => {
waitForImages().then(() => {
window.print();
});
}, 100);
});
}
function waitForImages() {
const images = document.querySelectorAll('img');
const promises = Array.from(images).map(img => {
if (img.complete) return Promise.resolve();
return new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = resolve;
});
});
return Promise.all(promises);
}
但这还不够保险。有些图片是从 CDN 懒加载的,有些字体是 webfont 还没下载完,这些都会影响最终的高度计算。最保险的做法是,在打印前先把所有资源都'固化':
async function prepareForPrint() {
const lazyElements = document.querySelectorAll('[data-lazy]');
lazyElements.forEach(el => {
el.src = el.dataset.src;
el.removeAttribute('data-lazy');
});
if (document.fonts) {
await document.fonts.ready;
}
await waitForImages();
document.body.style.zoom = '1.001';
void document.body.offsetHeight;
document.body.style.zoom = '1';
await new Promise(resolve => setTimeout(resolve, 300));
}
() {
();
.();
}
看到那个 zoom 的骚操作了吗?这是强制浏览器重排(reflow)的一个小技巧。改变 zoom 值会让浏览器重新计算所有元素的布局,确保我们拿到的是最终的高度。
隐藏的 overflow: hidden 和 fixed 定位
有些元素在屏幕模式下看着人畜无害,一到打印模式就变身内容杀手。最常见的就是 overflow: hidden 和 position: fixed。
overflow: hidden 很好理解,超出部分直接截断。但 position: fixed 为什么也会出问题?因为浏览器在打印时,会把 fixed 定位的元素当成'页眉页脚'来处理,有时候会把它们钉在每一页的同一个位置,导致内容重叠或者被遮挡。
更隐蔽的是一些 UI 框架的 modal、drawer、toast 这些组件,它们往往会有这样的样式:
.modal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
overflow: auto;
background: rgba(0, 0, 0, 0.5);
}
打印的时候,这个 modal 如果还开着,它就会覆盖整个页面,你只能打出一个黑框。所以打印前一定要清理这些层:
@media print {
.modal,
.drawer,
.toast,
.loading-mask {
display: none !important;
}
[style*="position: fixed"],
[style*="position:fixed"] {
position: static !important;
}
}
还有那种无限滚动的列表,屏幕上是虚拟滚动只渲染可视区,打印的时候你得把它变成全部渲染。这个后面实战部分会详细讲。
这招好用是好用,但也有翻车的时候
优点当然是爽啊
说实话,当你把这些技巧都用上以后,打印体验是真的爽。一次性把几十页内容都输出,不用让用户手动翻页,PDF 生成也是完整的,老板看了确实会点头。
而且做好打印适配以后,你会发现顺带解决了很多其他问题。比如导出 PDF 的时候,用浏览器的'打印到 PDF'功能,比你用 jsPDF、html2canvas 这些库去折腾要省事多了,出来的效果还更标准。毕竟浏览器自己最懂怎么渲染 HTML。
还有就是响应式的问题。你搞定了打印媒体查询,其实也就理清了页面在不同'视口'下的表现逻辑,这对理解 CSS 布局很有帮助。
缺点也得认,有些坑真的躲不掉
但是!凡事都有但是。这套方案在老旧浏览器面前就是渣渣。IE11 及以下版本,打印机制跟现代浏览器完全是两个物种。你在这边调得好好的分页符,到 IE 那边可能直接无视;你算得精准的高度,IE 可能根本不买账。
还有移动端浏览器的打印,那简直是灾难。iOS Safari 的打印预览经常抽风,有时候内容显示不全,有时候比例失调。Android 的各种浏览器内核更是五花八门,你根本测不过来。
图片加载的问题也很头大。如果你的页面有很多大图,即使做了 waitForImages,也可能因为网络问题导致超时。而且图片加载完到浏览器实际渲染出来,中间还有解码的时间,这个很难精确控制。结果就是打印出来图片位置是个空白框,或者只显示了一半。
还有一个很隐蔽的问题:内存。如果你的页面真的有几万行内容,全部展开渲染,低端设备可能会直接卡死。特别是用 Electron 打包的桌面应用,或者老旧的办公电脑,内存就 4G8G 的,你一下子塞太多 DOM 节点进去,浏览器直接罢工。
所以这套方案更适合后台管理系统、数据报表这种相对可控的场景。如果是面向 C 端用户的公开页面,建议还是提供'导出 PDF'的备用方案,别死磕 window.print。
实战场景大乱斗
电商后台订单详情打印
这是我最常遇到的场景。一个订单可能有几十个商品,每个商品有图片、名称、SKU、价格、数量,下面还有收货信息、物流轨迹、支付记录、操作日志。加起来十几页是常态。
这种页面的特点是:内容高度不确定,图片多,表格多。
直接上代码,看怎么组织:
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
line-height: 1.6;
color: #333;
}
.print-container {
max-width: 210mm;
margin: 0 auto;
padding: 10mm;
}
@media print {
@page {
size: A4;
margin: 10mm;
}
body {
background: white;
}
.no-print {
display: none !important;
}
.section {
page-break-inside: avoid;
margin-bottom: 20px;
}
.product-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.product-table th {
background: #f5f5f5 !important;
-webkit-: exact;
: exact;
}
,
{
: solid ;
: ;
: left;
}
{
: ;
: ;
: cover;
}
{
: always;
}
}
screen {
{
: (, , , );
: white;
: auto;
: ;
}
{
: fixed;
: ;
: ;
: ;
: ;
: white;
: none;
: ;
: pointer;
: ;
}
}
打印订单
订单详情 #202402240001
下单时间:2024-02-24 14:30:00 | 订单状态:已发货
商品信息
商品图片
商品名称
SKU
单价
数量
小计
合计:
¥0.00
收货信息
收货人:张三
手机号:138****8888
地址:北京市朝阳区某某街道某某小区 12 号楼 3 单元 501 室
物流轨迹
时间
状态
详情
这段代码里几个关键点:
@page 规则:控制纸张大小和边距,这是 CSS3 的规范,现代浏览器都支持
-webkit-print-color-adjust: exact:默认情况下浏览器打印会忽略背景色以节省墨水,这个属性强制保留背景色,对于表格表头很重要
crossorigin="anonymous":图片加这个属性是为了避免跨域问题导致 canvas 处理失败(虽然这里没用到 canvas,但养成习惯好)
- tfoot 的用法:把合计行放在 tfoot 里,这样如果表格跨页,每页底部都会显示合计,非常实用
财务报表长表格打印
财务报表的特点是列多、数据密、需要精确对齐。而且通常要求每页都显示表头,底部要有汇总。
<!DOCTYPE html>
<html>
<head>
<style>
@media print {
@page {
size: A4 landscape;
margin: 15mm;
}
body {
font-size: 10pt;
line-height: 1.4;
}
.financial-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
thead {
display: table-header-group;
}
.financial-table th {
background: #4472C4 !important;
color: white !important;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
padding: 8px;
border: 1px solid #2F5597;
font-weight: bold;
}
.financial-table td {
padding: 6px 8px;
border: solid ;
: break-word;
}
(even) {
: ;
-webkit-: exact;
: exact;
}
{
: table-footer-group;
: bold;
: ;
-webkit-: exact;
: exact;
}
{
: right;
: , monospace;
}
{
: avoid;
}
{
: always;
: ;
-webkit-: exact;
: exact;
: bold;
}
{
: auto;
}
}
screen {
{
: ;
: ;
}
{
: ;
: auto;
: white;
: ;
: (, , , );
}
}
2024 年度财务报表
生成时间:2024-02-24
科目编码
科目名称
期初余额
本期借方
本期贷方
期末余额
备注
合计
0.00
0.00
0.00
0.00
-
这里的关键技巧:
table-layout: fixed:配合列宽百分比,确保表格不会乱动
display: table-header-group 和 table-footer-group:这是 CSS 2.1 就有的规范,让 thead 和 tfoot 在打印时每页都重复
page-break-before: always:大分组之间强制分页,让报表结构更清晰
- 等宽字体对齐数字:用 Courier New 这类等宽字体显示金额,小数点能对齐,看着舒服
简历生成器实战
简历打印的核心诉求是:不管用户用什么屏幕看的,打印出来必须是完美的 A4 比例,不能多一寸不能少一寸。
<!DOCTYPE html>
<html>
<head>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.resume-page {
width: 210mm;
min-height: 297mm;
max-height: 297mm;
padding: 15mm;
margin: 0 auto;
background: white;
position: relative;
overflow: hidden;
}
.resume-page + .resume-page {
page-break-before: always;
margin-top: 0;
}
@media print {
@page {
size: A4;
margin: 0;
}
body {
background: white;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.resume-page {
box-shadow: none;
margin: 0;
width: 100%;
: always;
}
{
: auto;
}
}
screen {
{
: ;
: ;
}
{
: (, , , );
: ;
}
}
{
: center;
: solid ;
: ;
: ;
}
{
: ;
: bold;
: ;
}
{
: ;
: ;
}
{
: ;
}
{
: ;
: bold;
: solid ;
: ;
: ;
: ;
}
{
: ;
: avoid;
}
{
: flex;
: space-between;
: bold;
: ;
}
{
: ;
: ;
: ;
}
{
: flex;
: center;
: ;
}
{
: ;
: ;
}
{
: ;
: ;
: ;
: ;
: hidden;
}
{
: ;
: ;
: ;
}
张三
电话:138-0000-0000 | 邮箱:[email protected] | 微信:zhangsan_dev
教育背景
某某大学 - 计算机科学与技术(本科)
2015.09 - 2019.06
主修课程:数据结构、算法设计、操作系统、计算机网络、数据库原理 在校荣誉:优秀毕业生、国家奖学金、ACM 程序设计竞赛省级银奖
工作经历
某某科技有限公司 - 高级前端工程师
2021.07 - 至今
负责公司核心电商平台前端架构设计与开发,主导技术选型从 Vue2 迁移至 Vue3 搭建前端监控体系,错误率降低 80%,首屏加载时间优化至 1.2s 以内 带领 5 人前端团队,制定代码规范与 Code Review 机制,提升团队整体代码质量
某某互联网公司 - 前端开发工程师
2019.07 - 2021.06
参与公司 ToB 管理后台开发,负责权限管理、数据可视化等模块 封装通用组件库 15+ 个,提升团队开发效率 30% 优化老旧项目构建流程,打包体积减少 40%,构建速度提升 2 倍
专业技能
Vue/React
TypeScript
Node.js
工程化/性能优化
项目经历
企业级低代码平台
2023.01 - 2023.12
项目描述:面向企业内部使用的低代码应用搭建平台,支持表单、流程、报表可视化配置技术栈:Vue3 + TypeScript + Vite + Pinia + Element Plus主要职责: 1. 设计并实现可视化编辑器核心引擎,支持组件拖拽、属性配置、实时预览 2. 开发 DSL 解析器,将可视化配置转换为可执行的 Vue 代码 3. 实现自定义组件注册机制,支持第三方组件动态加载 4. 优化大数据量场景下的渲染性能,采用虚拟滚动 + 懒加载策略,支持万级数据流畅编辑
跨境电商 ERP 系统
2022.03 - 2022.12
项目描述:整合多平台(亚马逊、eBay、Shopify)订单、库存、物流管理的 ERP 系统技术栈:React + Redux + Ant Design + ECharts主要职责: 1. 负责订单管理、库存同步、物流追踪等核心模块开发 2. 设计并实现通用表格组件,支持动态列配置、批量操作、数据导出,复用至 20+ 页面 3. 封装 WebSocket 实时通知组件,实现订单状态变更实时推送 4. 开发数据看板,使用 ECharts 实现多维度销售数据可视化,支持钻取分析
微信小程序生态建设
2021.07 - 2022.02
项目描述:公司品牌矩阵小程序开发,包含商城、会员、营销等多个小程序技术栈:Taro3 + Vue3 + 微信原生 API主要职责: 1. 搭建 Taro 跨端开发框架,实现一套代码编译至微信小程序和 H5 2. 开发通用支付、分享、登录 SDK,统一处理各平台差异 3. 优化小程序性能,包体积从 3MB 压缩至 1.2MB,启动时间减少 50% 4. 设计灰度发布机制,支持按用户、地域、版本号维度渐进式发布
自我评价
5 年前端开发经验,精通 Vue/React 生态,具备完整的大型项目架构经验。对前端工程化、性能优化有深入实践, 擅长组件化设计和开发效率提升。具备良好的团队协作能力和技术领导力,能够带领团队攻克技术难点。 持续关注前端技术发展趋势,热爱开源社区,GitHub 个人项目累计 Star 2000+。
简历打印的核心要点:
- 严格尺寸控制:
210mm x 297mm,不多不少
page-break-before: always:多页简历时,每页都是独立的 A4
page-break-inside: avoid:经历块、项目块不能跨页断开,否则阅读体验很差
- 打印边距处理:
@page 的 margin 设为 0,在容器里用 padding 控制边距,这样不同浏览器的兼容性更好
电子发票和物流面单
这类场景对尺寸精度要求极高,差 1 毫米可能就贴不上或者塞不进快递袋。
<!DOCTYPE html>
<html>
<head>
<style>
.invoice {
width: 240mm;
height: 140mm;
padding: 10mm;
margin: 0 auto;
background: white;
border: 1px solid #ddd;
position: relative;
font-family: "SimSun", serif;
overflow: hidden;
}
.waybill {
width: 100mm;
height: 150mm;
padding: 5mm;
margin: 0 auto;
background: white;
border: 1px solid #000;
position: relative;
font-family: "Microsoft YaHei", sans-serif;
font-size: 10pt;
overflow: hidden;
}
@media print {
@page {
size: 240mm 140mm;
: ;
}
{
: ;
: ;
}
,
{
: none;
: ;
: ;
: always;
}
}
{
: center;
: solid ;
: ;
: ;
}
{
: ;
: ;
: bold;
: ;
}
{
: flex;
: space-between;
: ;
: ;
}
{
: ;
: collapse;
: ;
: ;
}
,
{
: solid ;
: ;
: center;
}
{
: right;
: ;
: bold;
: ;
}
{
: absolute;
: ;
: ;
: ;
: ;
: solid ;
: center;
: ;
: ;
}
{
: ;
: center;
: solid ;
: ;
: flex;
: center;
: center;
: , monospace;
: ;
}
{
: flex;
: space-between;
: dashed ;
: ;
: ;
}
{
: ;
}
{
: inline-block;
: ;
: right;
: ;
}
增值税电子普通发票
发票代码:011001900211
发票号码:12345678
开票日期:2024 年 02 月 24 日
购买方信息
名称:某某科技有限公司 纳税人识别号:91110108MA00XXXXXX 地址、电话:北京市海淀区某某路 XX 号 010-12345678 开户行及账号:中国工商银行北京分行 0200XXXXXX
货物或应税劳务、服务名称
规格型号
单位
数量
单价
金额
*软件*技术服务费
-
次
1
10000.00
10000.00
销售方信息
名称:北京某某软件服务有限公司 纳税人识别号:91110108MA00YYYYYY 地址、电话:北京市朝阳区某某路 XX 号 010-87654321 开户行及账号:中国建设银行北京分行 1100YYYYYY
价税合计(大写):壹万零壹佰元整(小写):¥10100.00
二维码区域 (发票信息)
*SF1234567890*
北京→上海
标快陆运
收:张三 138****8888
上海市浦东新区某某街道某某小区 12 号楼
寄: 李四 139****9999
北京市朝阳区某某大厦 8 层
物品信息:电子产品(已验视)
重量:2.5kg 保价:¥5000
订单号:2024022414300001 打印时间:2024-02-24 14:30:00
精准打印的关键:
- 毫米级尺寸控制:用
mm 单位,不要用 px 或者 pt,因为不同设备 DPI 不同,毫米是物理尺寸,最准确
@page size 自定义:可以指定任意尺寸,不只是 A4、A5 这种标准尺寸
overflow: hidden:严格防止内容溢出,因为发票和面单通常有固定的打印区域,超出就废了
- 字体选择:发票用宋体显得正式,面单用黑体或微软雅黑保证清晰可读
遇到报错别慌,老司机的排查套路
打印出来是空白?
先检查是不是有全局的 display: none 或者 visibility: hidden。特别是用了某些 UI 框架,打印的时候 modal 关闭了,但内容其实在 modal 里,跟着一起被隐藏了。
排查代码:
function checkVisibility(element) {
let current = element;
while (current) {
const style = window.getComputedStyle(current);
if (style.display === 'none') {
console.warn('隐藏元素:', current);
return false;
}
if (style.visibility === 'hidden') {
console.warn('不可见元素:', current);
return false;
}
current = current.parentElement;
}
return true;
}
还有就是 iframe 打印的问题。如果你把内容放在 iframe 里打印,要确保 iframe 已经正确加载,而且跨域策略不会阻止访问。
内容被截断一半?
这通常是父容器的 max-height 或者 overflow 属性在作祟。打印模式下这些属性有时候不会被自动重置。
快速修复:
@media print {
* {
max-height: none !important;
overflow: visible !important;
}
}
虽然粗暴,但管用。你可以先加上这个看看问题是否解决,然后再慢慢缩小范围找到具体是哪个元素在搞鬼。
样式错乱像裸奔?
检查 print 媒体查询是否被其他 CSS 覆盖了。特别是用了 CSS 框架(Bootstrap、Ant Design 等),它们可能也有 print 相关的样式,跟你写的冲突了。
用浏览器的打印预览功能(Ctrl+P 或者 Cmd+P),在预览界面检查元素,看看实际应用的样式是什么。Chrome 的打印预览也支持 DevTools,按 F12 能调出来。
控制台没报错但就是打不全?
这种情况多半是异步内容的问题。图片没加载完、字体没渲染完、React/Vue 还没更新完 DOM,你就触发打印了。
试试这个终极等待函数:
async function waitForEverything() {
await new Promise(resolve => setTimeout(resolve, 0));
if (document.fonts) {
await document.fonts.ready;
}
const images = Array.from(document.querySelectorAll('img'));
const bgImages = Array.from(document.querySelectorAll('*')).map(el => getComputedStyle(el).backgroundImage).filter(url => url !== 'none').map(url => url.replace(/url\(["']?([^"']*)["']?\)/, '$1'));
const allImages = [...images, ...bgImages.map(src => {
const img = new Image();
img. = src;
img;
})];
.(allImages.( img. ? .() : ( {
img. = img. = resolve;
(resolve, );
})));
iframes = .(.());
.(iframes.( ( {
(iframe.?. === ) {
();
} {
iframe. = resolve;
(resolve, );
}
})));
( (resolve, ));
}
() {
();
.();
}
图片加载慢导致高度计算不准
如果图片很多很大,即使做了等待,也可能因为网络问题导致超时。这时候可以考虑预先把图片转成 base64:
async function preloadImagesAsBase64() {
const images = document.querySelectorAll('img[data-src]');
for (let img of images) {
try {
const response = await fetch(img.dataset.src);
const blob = await response.blob();
const reader = new FileReader();
await new Promise((resolve, reject) => {
reader.onloadend = () => {
img.src = reader.result;
resolve();
};
reader.onerror = reject;
reader.readAsDataURL(blob);
});
} catch (e) {
console.warn('图片加载失败:', img.dataset.src);
img.src = img.dataset.src;
}
}
}
这样图片就变成了内联的 data URL,不需要网络请求,加载速度极快。缺点就是会增加 HTML 体积,适合打印内容不多的场景。
几个私藏的开发小技巧
虚拟滚动长列表的打印 hack
如果你的列表用了虚拟滚动(react-window、vue-virtual-scroller 等),打印的时候只有可视区的几条数据。这时候你需要在打印前'展开'所有数据。
function handlePrint() {
const listRef = useRef();
listRef.current.scrollToItem(999999, 'end');
setTimeout(() => {
setIsVirtual(false);
setTimeout(() => {
window.print();
setIsVirtual(true);
}, 100);
}, 500);
}
更通用的做法,如果你控制不了组件内部逻辑,可以直接操作 DOM:
function expandVirtualList() {
const container = document.querySelector('.virtual-list-container');
if (!container) return;
const originalHeight = container.style.height;
const originalOverflow = container.style.overflow;
container.style.height = 'auto';
container.style.overflow = 'visible';
container.style.maxHeight = 'none';
return () => {
container.style.height = originalHeight;
container.style.overflow = originalOverflow;
};
}
Promise 包装器等资源加载
前面其实已经写过类似的,这里再给个更健壮的版本,支持超时和错误处理:
function createResourceWaiter(timeout = 10000) {
return new Promise((resolve) => {
const startTime = Date.now();
const check = () => {
const images = Array.from(document.images);
const incompleteImages = images.filter(img => !img.complete);
const fontsReady = document.fonts ? document.fonts.status === 'loaded' : true;
if (incompleteImages.length === 0 && fontsReady) {
resolve({ success: true, time: Date.now() - startTime });
return;
}
if (Date.now() - startTime > timeout) {
console.warn('资源等待超时,未完成:', { images: incompleteImages.length, : !fontsReady });
({ : , : });
;
}
(check);
};
();
});
}
() {
.();
result = ();
.(, result);
.();
}
打印预览模拟 A4 效果
在屏幕上预览打印效果,避免反复试打印浪费纸:
<div class="print-preview">
<div class="a4-page">
</div>
</div>
<style>
.print-preview {
background: #e8e8e8;
padding: 20px;
min-height: 100vh;
}
.a4-page {
width: 210mm;
min-height: 297mm;
background: white;
margin: 0 auto;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
transform-origin: top center;
}
@media screen and (max-width: 230mm) {
.print-preview {
padding: 10px;
overflow-x: auto;
}
.a4-page {
transform: scale(calc(100vw / 230mm));
margin-bottom: (- * ( - / ));
}
}
这样在小屏幕上会自动缩放,保证能看到完整的 A4 页面。
打印按钮防抖
用户手贱连点打印按钮,可能会把浏览器搞崩,特别是内容多的时候:
function debounce(fn, delay) {
let timer = null;
return function (...args) {
if (timer) {
console.log('别急,正在准备...');
return;
}
fn.apply(this, args);
timer = setTimeout(() => {
timer = null;
}, delay);
};
}
const handlePrint = debounce(async () => {
await prepareForPrint();
window.print();
}, 3000);
或者加个 loading 状态更友好:
async function handlePrint() {
const btn = document.getElementById('printBtn');
if (btn.disabled) return;
btn.disabled = true;
btn.textContent = '准备中...';
try {
await prepareForPrint();
window.print();
} catch (e) {
alert('打印准备失败:' + e.message);
} finally {
btn.disabled = false;
btn.textContent = '打印';
}
}
最后啰嗦两句,别嫌我烦
看到这里,你应该对 window.print 这个'祖宗'有了全新的认识。它不是什么高深技术,但坑是真的多。下次再遇到打印问题,别只会重启浏览器或者让用户'试试别的浏览器',试试上面这些野路子。
记住几个核心要点:
- 高度问题:
height: auto + overflow: visible,给所有可能限制高度的容器都加上
- 异步问题:数据渲染完、图片加载完、字体准备好,再触发打印
- 分页问题:
page-break-inside: avoid 防止截断,伪元素比分页 div 优雅
- 样式问题:
!important 在打印媒体查询里是你的好朋友
- 尺寸问题:用
mm 做单位,不要用 px
代码写得再漂亮,打印出来是一坨屎也没人夸你。前端工程师的价值就体现在这些细节里。一个能把打印功能做得丝般顺滑的前端,绝对是团队里的宝藏。
赶紧去试试吧。要是还搞不定,回来继续吐槽,我随时在线接招。毕竟咱们前端佬就是在填坑中成长的,对吧?