欢迎光临
我们一直在努力

前端老哥必看:window.print只打半截?一招搞定HTML实际高度打印不踩坑

在这里插入图片描述

前端老哥必看:window.print只打半截?一招搞定HTML实际高度打印不踩坑

  • 前端老哥必看:window.print只打半截?一招搞定HTML实际高度打印不踩坑
    • 别整那些虚的,咱们直接开唠
    • 这玩意儿到底是个啥妖魔鬼怪
      • 浏览器打印机制那点不为人知的秘密
      • CSS里的print媒体查询,是救星还是坑货?
    • 深挖底层逻辑,把打印机按在地上摩擦
      • height: auto失效?布局塌陷的锅谁来背
      • 强制分页符的正确打开方式
      • 动态内容高度计算,别让JS骗了打印机
      • 隐藏的overflow: hidden和fixed定位
    • 这招好用是好用,但也有翻车的时候
      • 优点当然是爽啊
      • 缺点也得认,有些坑真的躲不掉
    • 实战场景大乱斗
      • 电商后台订单详情打印
      • 财务报表长表格打印
      • 简历生成器实战
      • 电子发票和物流面单
    • 遇到报错别慌,老司机的排查套路
      • 打印出来是空白?
      • 内容被截断一半?
      • 样式错乱像裸奔?
      • 控制台没报错但就是打不全?
      • 图片加载慢导致高度计算不准
    • 几个私藏的开发小技巧
      • 虚拟滚动长列表的打印 hack
      • Promise包装器等资源加载
      • 打印预览模拟A4效果
      • 打印按钮防抖
    • 最后啰嗦两句,别嫌我烦

前端老哥必看:window.print只打半截?一招搞定HTML实际高度打印不踩坑

别整那些虚的,咱们直接开唠

兄弟,你是不是也遇到过这种鬼故事?页面上明明有十几屏的内容,一点打印按钮,出来的PDF就只有当前屏幕那一截,后面的东西全没了,跟被狗啃了似的。

我跟你讲,这事儿我熟得很。去年做电商后台,产品经理甩过来一个需求:“做个订单打印功能,要能把订单详情、商品列表、物流信息一次性打出来”。我当时心想,这不就window.print()一行代码的事儿吗,分分钟搞定。结果上线第二天,产品经理举着一张只打了半截的A4纸杀到我工位,那眼神跟要吃了我似的。

“后面的商品呢?我的二十几个商品怎么只显示了前三个?”

我当场尬住。打开浏览器一看,果然,打印预览里只有可视区域那一块,下面全是白的。当时我就纳闷了,这浏览器是不是脑子有坑?我HTML明明那么长,你凭什么只给我打一半?

后来一查资料,差点没把我气死。原来浏览器的打印机制就是这么"懒"——它默认只打印当前视口(viewport)里的内容。换句话说,你没滚动到的地方,它觉得你不重要,直接给你忽略了。这逻辑简直感人,就像你去饭店点菜,服务员只给你上你眼前能看到的那几盘,后面的菜因为你还没看到所以就不做了。

今天咱们不聊那些虚头八脑的大道理,就聊聊怎么让window.print这个祖宗乖乖听话,把整个HTML的身体都给你吐出来。这篇文章我会把我踩过的坑、试过的错、以及最后总结出来的野路子全盘托出。保证你看完以后,再遇到打印问题能跟产品经理拍着胸脯说:“这需求我接了,稳得很。”

这玩意儿到底是个啥妖魔鬼怪

浏览器打印机制那点不为人知的秘密

要搞定打印问题,咱们得先搞清楚浏览器到底是怎么想的。你以为点下打印按钮,浏览器就会乖乖把整个DOM树都渲染一遍?太天真了。

浏览器在接收到打印指令时,会干这么几件事:

  • 克隆当前文档:它会先复制一份当前的DOM结构
  • 应用打印样式:把@media print里的CSS规则套上去
  • 重新布局计算:根据纸张尺寸(通常是A4)重新计算布局
  • 分页处理:把内容切成一页一页的
  • 生成打印预览:最后呈现给你看
  • 问题就出在第3步。浏览器在重新布局的时候,对于那些height: auto或者没有明确高度的容器,它有时候会犯迷糊。特别是当你的内容是通过JavaScript动态加载的,或者图片是懒加载的,浏览器算高度的时候可能还没加载完,结果就是算出来的高度比实际小,后面的内容就被"截断"了。

    更坑的是,有些浏览器(我就不点名了,IE说的就是你)还会受限于内存或者性能考虑,故意只渲染可视区域附近的内容。这就像是老师改卷子,只看第一页,后面的随便打个分就完事了。

    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吧

    所以MDN文档上那些理论,在实际项目里真的不够用。文档会说"浏览器应该正确处理打印媒体查询",但它不会告诉你"Safari在打印iframe里的内容时会有10%的概率丢失样式"这种细节。

    深挖底层逻辑,把打印机按在地上摩擦

    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 */
    }

    这时候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布局的 */
    .flex-container {
    display: block !important; /* 打印时flex有时候会有奇怪的问题 */
    }
    }

    看到那些!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; /* 保证标题下面至少跟3行内容 */
    widows: 3; /* 保证段落末尾至少留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); // 假设这是React的setState
    window.print(); // 立即打印
    });
    }

    你以为setState之后页面就更新了?Too young too simple。React的setState是异步的,你这时候打印,可能DOM还没更新完呢。结果就是打印出来的还是老数据,或者只有半截。

    正确的姿势是要等渲染完成。在React里可以用setTimeout或者requestAnimationFrame:

    function handlePrint() {
    fetchData().then(data => {
    setState(data);

    // 等React更新完DOM
    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() {
    // 1. 展开所有懒加载内容
    const lazyElements = document.querySelectorAll('[data-lazy]');
    lazyElements.forEach(el => {
    el.src = el.dataset.src;
    el.removeAttribute('data-lazy');
    });

    // 2. 等待字体加载
    if (document.fonts) {
    await document.fonts.ready;
    }

    // 3. 等待图片加载(包括刚才替换的)
    await waitForImages();

    // 4. 强制重排,确保浏览器拿到最新高度
    document.body.style.zoom = '1.001';
    void document.body.offsetHeight; // 强制重排
    document.body.style.zoom = '1';

    // 5. 给浏览器一点喘息时间
    await new Promise(resolve => setTimeout(resolve, 300));
    }

    // 使用
    async function handlePrint() {
    await prepareForPrint();
    window.print();
    }

    看到那个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 {
    /* 隐藏所有fixed定位的元素 */
    .modal, .drawer, .toast, .loading-mask {
    display: none !important;
    }

    /* 把absolute定位的也检查一下 */
    [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; /* A4宽度 */
    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-print-color-adjust: exact; /* Chrome/Safari */
    print-color-adjust: exact; /* 标准语法 */
    }

    .product-table th,
    .product-table td
    {
    border: 1px solid #ddd;
    padding: 8px;
    text-align: left;
    }

    /* 图片控制大小 */
    .product-img {
    max-width: 60px;
    max-height: 60px;
    object-fit: cover;
    }

    /* 分页控制 */
    .page-break {
    page-break-after: always;
    }
    }

    /* 屏幕预览样式 */
    @media screen {
    .print-container {
    box-shadow: 0 0 10px rgba(0,0,0,0.1);
    background: white;
    margin: 20px auto;
    min-height: 297mm; /* A4高度 */
    }

    .print-btn {
    position: fixed;
    top: 20px;
    right: 20px;
    padding: 10px 20px;
    background: #1890ff;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    z-index: 9999;
    }
    }
    </style>
    </head>
    <body>
    <button class="print-btn no-print" onclick="handlePrint()">打印订单</button>

    <div class="print-container" id="printArea">
    <!– 订单头部 –>
    <div class="section order-header">
    <h1>订单详情 #202402240001</h1>
    <p>下单时间:2024-02-24 14:30:00 | 订单状态:已发货</p>
    </div>

    <!– 商品列表 –>
    <div class="section product-section">
    <h2>商品信息</h2>
    <table class="product-table">
    <thead>
    <tr>
    <th>商品图片</th>
    <th>商品名称</th>
    <th>SKU</th>
    <th>单价</th>
    <th>数量</th>
    <th>小计</th>
    </tr>
    </thead>
    <tbody id="productList">
    <!– 动态插入商品行 –>
    </tbody>
    <tfoot>
    <tr>
    <td colspan="5" style="text-align: right;"><strong>合计:</strong></td>
    <td><strong>¥<span id="totalAmount">0.00</span></strong></td>
    </tr>
    </tfoot>
    </table>
    </div>

    <!– 收货信息 –>
    <div class="section address-section">
    <h2>收货信息</h2>
    <p>收货人:张三</p>
    <p>手机号:138****8888</p>
    <p>地址:北京市朝阳区某某街道某某小区12号楼3单元501室</p>
    </div>

    <!– 物流信息 –>
    <div class="section logistics-section">
    <h2>物流轨迹</h2>
    <table class="product-table">
    <thead>
    <tr>
    <th>时间</th>
    <th>状态</th>
    <th>详情</th>
    </tr>
    </thead>
    <tbody id="logisticsList">
    <!– 动态插入物流信息 –>
    </tbody>
    </table>
    </div>
    </div>

    <script>
    // 模拟大量商品数据
    const products = Array.from({length: 50}, (_, i) => ({
    id: i + 1,
    name: `测试商品${i + 1}号 超长名称测试打印效果是否会自动换行处理`,
    sku: `SKU${String(i + 1).padStart(6, '0')}`,
    price: (Math.random() * 1000).toFixed(2),
    quantity: Math.floor(Math.random() * 5) + 1,
    image: `https://via.placeholder.com/60×60?text=${i + 1}`
    }));

    // 模拟物流数据
    const logistics = [
    { time: '2024-02-24 16:30:00', status: '已签收', detail: '您的订单已由本人签收,感谢使用' },
    { time: '2024-02-24 09:15:00', status: '派送中', detail: '快递员正在为您派件,电话:13800138000' },
    { time: '2024-02-24 07:30:00', status: '运输中', detail: '快件已到达北京朝阳营业点' },
    { time: '2024-02-23 22:00:00', status: '运输中', detail: '快件已发往北京转运中心' },
    { time: '2024-02-23 18:00:00', status: '已发货', detail: '商家已发货,快递公司:顺丰速运,运单号:SF1234567890' }
    ];

    // 渲染商品列表
    function renderProducts() {
    const tbody = document.getElementById('productList');
    let total = 0;

    tbody.innerHTML = products.map(p => {
    const subtotal = parseFloat(p.price) * p.quantity;
    total += subtotal;
    return `
    <tr>
    <td><img src="
    ${p.image}" class="product-img" alt="${p.name}" crossorigin="anonymous"></td>
    <td>
    ${p.name}</td>
    <td>
    ${p.sku}</td>
    <td>¥
    ${p.price}</td>
    <td>
    ${p.quantity}</td>
    <td>¥
    ${subtotal.toFixed(2)}</td>
    </tr>
    `
    ;
    }).join('');

    document.getElementById('totalAmount').textContent = total.toFixed(2);
    }

    // 渲染物流信息
    function renderLogistics() {
    const tbody = document.getElementById('logisticsList');
    tbody.innerHTML = logistics.map(l => `
    <tr>
    <td>
    ${l.time}</td>
    <td>
    ${l.status}</td>
    <td>
    ${l.detail}</td>
    </tr>
    `
    ).join('');
    }

    // 等待所有资源加载
    function waitForResources() {
    return new Promise((resolve) => {
    // 等待图片
    const images = document.querySelectorAll('img');
    let loaded = 0;
    const total = images.length;

    if (total === 0) {
    resolve();
    return;
    }

    images.forEach(img => {
    if (img.complete) {
    loaded++;
    if (loaded === total) resolve();
    } else {
    img.onload = img.onerror = () => {
    loaded++;
    if (loaded === total) resolve();
    };
    }
    });

    // 超时保护,最多等5秒
    setTimeout(resolve, 5000);
    });
    }

    // 打印处理函数
    async function handlePrint() {
    const btn = document.querySelector('.print-btn');
    btn.textContent = '准备中…';
    btn.disabled = true;

    try {
    // 先渲染数据(如果是异步获取的话)
    renderProducts();
    renderLogistics();

    // 等待浏览器渲染
    await new Promise(r => setTimeout(r, 100));

    // 等待图片等资源
    await waitForResources();

    // 强制重排确保高度正确
    document.body.style.zoom = '1.001';
    void document.body.offsetHeight;
    document.body.style.zoom = '1';

    // 再等等
    await new Promise(r => setTimeout(r, 300));

    // 调起打印
    window.print();
    } catch (e) {
    console.error('打印准备失败:', e);
    alert('打印准备失败,请重试');
    } finally {
    btn.textContent = '打印订单';
    btn.disabled = false;
    }
    }

    // 初始化
    renderProducts();
    renderLogistics();
    </script>
    </body>
    </html>

    这段代码里几个关键点:

  • @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: 1px solid #B4C7E7;
    word-wrap: break-word; /* 长内容自动换行 */
    }

    /* 斑马纹,提升可读性 */
    .financial-table tbody tr:nth-child(even) {
    background: #D9E2F3 !important;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
    }

    /* 汇总行固定在最后一页底部?不,每页底部都要 */
    tfoot {
    display: table-footer-group;
    font-weight: bold;
    background: #FFF2CC !important;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
    }

    /* 数字右对齐,方便阅读 */
    .num {
    text-align: right;
    font-family: "Courier New", monospace; /* 等宽字体对齐数字 */
    }

    /* 防止行被截断 */
    tr {
    page-break-inside: avoid;
    }

    /* 大分组之间强制分页 */
    .group-header {
    page-break-before: always;
    background: #E7E6E6 !important;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
    font-weight: bold;
    }

    .group-header:first-of-type {
    page-break-before: auto; /* 第一个分组不需要分页 */
    }
    }

    @media screen {
    body {
    padding: 20px;
    background: #f0f2f5;
    }

    .container {
    max-width: 1200px;
    margin: 0 auto;
    background: white;
    padding: 20px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.1);
    }
    }
    </style>
    </head>
    <body>
    <div class="container">
    <h1>2024年度财务报表</h1>
    <p class="no-print">生成时间:2024-02-24</p>

    <table class="financial-table">
    <thead>
    <tr>
    <th style="width: 8%">科目编码</th>
    <th style="width: 20%">科目名称</th>
    <th style="width: 12%">期初余额</th>
    <th style="width: 12%">本期借方</th>
    <th style="width: 12%">本期贷方</th>
    <th style="width: 12%">期末余额</th>
    <th style="width: 12%">备注</th>
    </tr>
    </thead>
    <tbody id="dataBody">
    <!– 动态生成大量数据 –>
    </tbody>
    <tfoot>
    <tr>
    <td colspan="2" style="text-align: center;">合计</td>
    <td class="num" id="sumBegin">0.00</td>
    <td class="num" id="sumDebit">0.00</td>
    <td class="num" id="sumCredit">0.00</td>
    <td class="num" id="sumEnd">0.00</td>
    <td>–</td>
    </tr>
    </tfoot>
    </table>
    </div>

    <script>
    // 生成模拟财务数据
    function generateData() {
    const body = document.getElementById('dataBody');
    let html = '';
    let sumBegin = 0, sumDebit = 0, sumCredit = 0, sumEnd = 0;

    // 生成几个大分组
    const groups = ['资产类', '负债类', '权益类', '损益类'];
    const groupCodes = ['1', '2', '3', '4'];

    groups.forEach((group, gIndex) => {
    // 分组标题行
    html += `
    <tr class="group-header">
    <td colspan="7">
    ${groupCodes[gIndex]}000 ${group}</td>
    </tr>
    `
    ;

    // 每个分组生成30条明细
    for (let i = 1; i <= 30; i++) {
    const code = `${groupCodes[gIndex]}${String(i).padStart(3, '0')}`;
    const begin = (Math.random() * 1000000).toFixed(2);
    const debit = (Math.random() * 500000).toFixed(2);
    const credit = (Math.random() * 500000).toFixed(2);
    const end = (parseFloat(begin) + parseFloat(debit) – parseFloat(credit)).toFixed(2);

    sumBegin += parseFloat(begin);
    sumDebit += parseFloat(debit);
    sumCredit += parseFloat(credit);
    sumEnd += parseFloat(end);

    html += `
    <tr>
    <td>
    ${code}</td>
    <td>
    ${group}科目-${i}</td>
    <td class="num">
    ${parseFloat(begin).toLocaleString('zh-CN', {minimumFractionDigits: 2})}</td>
    <td class="num">
    ${parseFloat(debit).toLocaleString('zh-CN', {minimumFractionDigits: 2})}</td>
    <td class="num">
    ${parseFloat(credit).toLocaleString('zh-CN', {minimumFractionDigits: 2})}</td>
    <td class="num">
    ${parseFloat(end).toLocaleString('zh-CN', {minimumFractionDigits: 2})}</td>
    <td>
    ${i % 5 === 0 ? '需关注' : ''}</td>
    </tr>
    `
    ;
    }
    });

    body.innerHTML = html;

    // 更新合计
    document.getElementById('sumBegin').textContent = sumBegin.toLocaleString('zh-CN', {minimumFractionDigits: 2});
    document.getElementById('sumDebit').textContent = sumDebit.toLocaleString('zh-CN', {minimumFractionDigits: 2});
    document.getElementById('sumCredit').textContent = sumCredit.toLocaleString('zh-CN', {minimumFractionDigits: 2});
    document.getElementById('sumEnd').textContent = sumEnd.toLocaleString('zh-CN', {minimumFractionDigits: 2});
    }

    generateData();
    </script>
    </body>
    </html>

    这里的关键技巧:

  • 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;
    }

    /* A4尺寸精确控制:210mm x 297mm */
    .resume-page {
    width: 210mm;
    min-height: 297mm;
    max-height: 297mm; /* 严格限制高度,超出就分页 */
    padding: 15mm;
    margin: 0 auto;
    background: white;
    position: relative;
    overflow: hidden; /* 防止内容溢出 */
    }

    /* 多页简历时,每页都是A4 */
    .resume-page + .resume-page {
    page-break-before: always;
    margin-top: 0;
    }

    @media print {
    @page {
    size: A4;
    margin: 0; /* 我们在容器里控制边距,这里设0 */
    }

    body {
    background: white;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
    }

    .resume-page {
    box-shadow: none;
    margin: 0;
    width: 100%; /* 打印时撑满纸张 */
    page-break-after: always;
    }

    /* 最后一页不需要分页 */
    .resume-page:last-of-type {
    page-break-after: auto;
    }
    }

    @media screen {
    body {
    background: #e8e8e8;
    padding: 20px;
    }

    .resume-page {
    box-shadow: 0 0 10px rgba(0,0,0,0.2);
    margin-bottom: 20px;
    }
    }

    /* 简历内容样式 */
    .header {
    text-align: center;
    border-bottom: 2px solid #333;
    padding-bottom: 15px;
    margin-bottom: 20px;
    }

    .name {
    font-size: 28px;
    font-weight: bold;
    margin-bottom: 8px;
    }

    .contact {
    color: #666;
    font-size: 14px;
    }

    .section {
    margin-bottom: 20px;
    }

    .section-title {
    font-size: 16px;
    font-weight: bold;
    border-left: 4px solid #1890ff;
    padding-left: 8px;
    margin-bottom: 10px;
    color: #1890ff;
    }

    .item {
    margin-bottom: 12px;
    page-break-inside: avoid; /* 经历块不跨页 */
    }

    .item-header {
    display: flex;
    justify-content: space-between;
    font-weight: bold;
    margin-bottom: 4px;
    }

    .item-content {
    color: #555;
    font-size: 14px;
    line-height: 1.6;
    }

    /* 技能条 */
    .skill-bar {
    display: flex;
    align-items: center;
    margin-bottom: 8px;
    }

    .skill-name {
    width: 100px;
    font-size: 14px;
    }

    .skill-progress {
    flex: 1;
    height: 8px;
    background: #e8e8e8;
    border-radius: 4px;
    overflow: hidden;
    }

    .skill-fill {
    height: 100%;
    background: #1890ff;
    border-radius: 4px;
    }
    </style>
    </head>
    <body>
    <!– 第一页 –>
    <div class="resume-page">
    <div class="header">
    <div class="name">张三</div>
    <div class="contact">
    电话:138-0000-0000 | 邮箱:zhangsan@example.com | 微信:zhangsan_dev
    </div>
    </div>

    <div class="section">
    <div class="section-title">教育背景</div>
    <div class="item">
    <div class="item-header">
    <span>某某大学 – 计算机科学与技术(本科)</span>
    <span>2015.09 – 2019.06</span>
    </div>
    <div class="item-content">
    主修课程:数据结构、算法设计、操作系统、计算机网络、数据库原理<br>
    在校荣誉:优秀毕业生、国家奖学金、ACM程序设计竞赛省级银奖
    </div>
    </div>
    </div>

    <div class="section">
    <div class="section-title">工作经历</div>

    <div class="item">
    <div class="item-header">
    <span>某某科技有限公司 – 高级前端工程师</span>
    <span>2021.07 – 至今</span>
    </div>
    <div class="item-content">
    负责公司核心电商平台前端架构设计与开发,主导技术选型从Vue2迁移至Vue3<br>
    搭建前端监控体系,错误率降低80%,首屏加载时间优化至1.2s以内<br>
    带领5人前端团队,制定代码规范与Code Review机制,提升团队整体代码质量
    </div>
    </div>

    <div class="item">
    <div class="item-header">
    <span>某某互联网公司 – 前端开发工程师</span>
    <span>2019.07 – 2021.06</span>
    </div>
    <div class="item-content">
    参与公司ToB管理后台开发,负责权限管理、数据可视化等模块<br>
    封装通用组件库15+个,提升团队开发效率30%<br>
    优化老旧项目构建流程,打包体积减少40%,构建速度提升2倍
    </div>
    </div>
    </div>

    <div class="section">
    <div class="section-title">专业技能</div>
    <div class="skill-bar">
    <span class="skill-name">Vue/React</span>
    <div class="skill-progress">
    <div class="skill-fill" style="width: 90%"></div>
    </div>
    </div>
    <div class="skill-bar">
    <span class="skill-name">TypeScript</span>
    <div class="skill-progress">
    <div class="skill-fill" style="width: 85%"></div>
    </div>
    </div>
    <div class="skill-bar">
    <span class="skill-name">Node.js</span>
    <div class="skill-progress">
    <div class="skill-fill" style="width: 75%"></div>
    </div>
    </div>
    <div class="skill-bar">
    <span class="skill-name">工程化/性能优化</span>
    <div class="skill-progress">
    <div class="skill-fill" style="width: 80%"></div>
    </div>
    </div>
    </div>
    </div>

    <!– 第二页(项目经历) –>
    <div class="resume-page">
    <div class="section">
    <div class="section-title">项目经历</div>

    <div class="item">
    <div class="item-header">
    <span>企业级低代码平台</span>
    <span>2023.01 – 2023.12</span>
    </div>
    <div class="item-content">
    <strong>项目描述:</strong>面向企业内部使用的低代码应用搭建平台,支持表单、流程、报表可视化配置<br>
    <strong>技术栈:</strong>Vue3 + TypeScript + Vite + Pinia + Element Plus<br>
    <strong>主要职责:</strong><br>
    1. 设计并实现可视化编辑器核心引擎,支持组件拖拽、属性配置、实时预览<br>
    2. 开发DSL解析器,将可视化配置转换为可执行的Vue代码<br>
    3. 实现自定义组件注册机制,支持第三方组件动态加载<br>
    4. 优化大数据量场景下的渲染性能,采用虚拟滚动+懒加载策略,支持万级数据流畅编辑
    </div>
    </div>

    <div class="item">
    <div class="item-header">
    <span>跨境电商ERP系统</span>
    <span>2022.03 – 2022.12</span>
    </div>
    <div class="item-content">
    <strong>项目描述:</strong>整合多平台(亚马逊、eBay、Shopify)订单、库存、物流管理的ERP系统<br>
    <strong>技术栈:</strong>React + Redux + Ant Design + ECharts<br>
    <strong>主要职责:</strong><br>
    1. 负责订单管理、库存同步、物流追踪等核心模块开发<br>
    2. 设计并实现通用表格组件,支持动态列配置、批量操作、数据导出,复用至20+页面<br>
    3. 封装WebSocket实时通知组件,实现订单状态变更实时推送<br>
    4. 开发数据看板,使用ECharts实现多维度销售数据可视化,支持钻取分析
    </div>
    </div>

    <div class="item">
    <div class="item-header">
    <span>微信小程序生态建设</span>
    <span>2021.07 – 2022.02</span>
    </div>
    <div class="item-content">
    <strong>项目描述:</strong>公司品牌矩阵小程序开发,包含商城、会员、营销等多个小程序<br>
    <strong>技术栈:</strong>Taro3 + Vue3 + 微信原生API<br>
    <strong>主要职责:</strong><br>
    1. 搭建Taro跨端开发框架,实现一套代码编译至微信小程序和H5<br>
    2. 开发通用支付、分享、登录SDK,统一处理各平台差异<br>
    3. 优化小程序性能,包体积从3MB压缩至1.2MB,启动时间减少50%<br>
    4. 设计灰度发布机制,支持按用户、地域、版本号维度渐进式发布
    </div>
    </div>
    </div>

    <div class="section">
    <div class="section-title">自我评价</div>
    <div class="item-content">
    5年前端开发经验,精通Vue/React生态,具备完整的大型项目架构经验。对前端工程化、性能优化有深入实践,
    擅长组件化设计和开发效率提升。具备良好的团队协作能力和技术领导力,能够带领团队攻克技术难点。
    持续关注前端技术发展趋势,热爱开源社区,GitHub个人项目累计Star 2000+。
    </div>
    </div>
    </div>
    </body>
    </html>

    简历打印的核心要点:

  • 严格尺寸控制:210mm x 297mm,不多不少
  • page-break-before: always:多页简历时,每页都是独立的A4
  • page-break-inside: avoid:经历块、项目块不能跨页断开,否则阅读体验很差
  • 打印边距处理:@page的margin设为0,在容器里用padding控制边距,这样不同浏览器的兼容性更好
  • 电子发票和物流面单

    这类场景对尺寸精度要求极高,差1毫米可能就贴不上或者塞不进快递袋。

    <!DOCTYPE html>
    <html>
    <head>
    <style>
    /* 电子发票:通常240mm x 140mm */
    .invoice {
    width: 240mm;
    height: 140mm;
    padding: 10mm;
    margin: 0 auto;
    background: white;
    border: 1px solid #ddd;
    position: relative;
    font-family: "SimSun", serif; /* 发票通常用宋体 */
    overflow: hidden;
    }

    /* 物流面单:通常100mm x 150mm(热敏纸) */
    .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; /* 发票 */
    /* size: 100mm 150mm; 面单 */
    margin: 0;
    }

    body {
    margin: 0;
    padding: 0;
    }

    .invoice, .waybill {
    border: none;
    width: 100%;
    height: 100%;
    page-break-after: always;
    }
    }

    /* 发票样式细节 */
    .invoice-header {
    text-align: center;
    border-bottom: 2px solid #f00;
    padding-bottom: 5mm;
    margin-bottom: 5mm;
    }

    .invoice-title {
    font-size: 24pt;
    color: #f00;
    font-weight: bold;
    letter-spacing: 10px;
    }

    .invoice-info {
    display: flex;
    justify-content: space-between;
    font-size: 10pt;
    margin-top: 5mm;
    }

    .invoice-table {
    width: 100%;
    border-collapse: collapse;
    margin: 5mm 0;
    font-size: 9pt;
    }

    .invoice-table th,
    .invoice-table td
    {
    border: 1px solid #999;
    padding: 2mm;
    text-align: center;
    }

    .invoice-amount {
    text-align: right;
    font-size: 12pt;
    font-weight: bold;
    margin-top: 5mm;
    }

    .invoice-qrcode {
    position: absolute;
    right: 10mm;
    bottom: 10mm;
    width: 25mm;
    height: 25mm;
    border: 1px solid #000;
    text-align: center;
    line-height: 25mm;
    font-size: 8pt;
    }

    /* 面单样式细节 */
    .waybill-barcode {
    height: 20mm;
    text-align: center;
    border-bottom: 2px solid #000;
    margin-bottom: 3mm;
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: "Libre Barcode 39", monospace;
    font-size: 36pt;
    }

    .waybill-route {
    display: flex;
    justify-content: space-between;
    border-bottom: 1px dashed #000;
    padding-bottom: 2mm;
    margin-bottom: 2mm;
    }

    .waybill-info {
    line-height: 1.8;
    }

    .waybill-label {
    display: inline-block;
    width: 15mm;
    text-align: right;
    margin-right: 2mm;
    }
    </style>
    </head>
    <body>
    <!– 电子发票示例 –>
    <div class="invoice">
    <div class="invoice-header">
    <div class="invoice-title">增值税电子普通发票</div>
    <div class="invoice-info">
    <span>发票代码:011001900211</span>
    <span>发票号码:12345678</span>
    <span>开票日期:2024年02月24日</span>
    </div>
    </div>

    <table class="invoice-table">
    <tr>
    <th style="width: 30%">购买方信息</th>
    <td style="width: 70%; text-align: left;">
    名称:某某科技有限公司<br>
    纳税人识别号:91110108MA00XXXXXX<br>
    地址、电话:北京市海淀区某某路XX号 010-12345678<br>
    开户行及账号:中国工商银行北京分行 0200XXXXXX
    </td>
    </tr>
    <tr>
    <th colspan="2">
    <table style="width: 100%; border: none;">
    <tr style="border: none;">
    <td style="border: none; width: 40%">货物或应税劳务、服务名称</td>
    <td style="border: none; width: 15%">规格型号</td>
    <td style="border: none; width: 10%">单位</td>
    <td style="border: none; width: 10%">数量</td>
    <td style="border: none; width: 10%">单价</td>
    <td style="border: none; width: 15%">金额</td>
    </tr>
    <tr style="border: none;">
    <td style="border: none;">*软件*技术服务费</td>
    <td style="border: none;">–</td>
    <td style="border: none;">次</td>
    <td style="border: none;">1</td>
    <td style="border: none;">10000.00</td>
    <td style="border: none;">10000.00</td>
    </tr>
    </table>
    </th>
    </tr>
    <tr>
    <th>销售方信息</th>
    <td style="text-align: left;">
    名称:北京某某软件服务有限公司<br>
    纳税人识别号:91110108MA00YYYYYY<br>
    地址、电话:北京市朝阳区某某路XX号 010-87654321<br>
    开户行及账号:中国建设银行北京分行 1100YYYYYY
    </td>
    </tr>
    </table>

    <div class="invoice-amount">
    价税合计(大写):壹万零壹佰元整<br>
    <span style="color: #f00;">(小写):¥10100.00</span>
    </div>

    <div class="invoice-qrcode">
    二维码区域<br>
    (发票信息)
    </div>
    </div>

    <!– 物流面单示例 –>
    <div class="waybill">
    <div class="waybill-barcode">
    *SF1234567890*
    </div>

    <div class="waybill-route">
    <div style="font-size: 14pt; font-weight: bold;">北京→上海</div>
    <div>标快<br>陆运</div>
    </div>

    <div class="waybill-info">
    <div>
    <span class="waybill-label">收:</span>
    <strong>张三</strong> 138****8888<br>
    <span style="margin-left: 17mm;">上海市浦东新区某某街道某某小区12号楼</span>
    </div>
    <div style="margin-top: 2mm;">
    <span class="waybill-label">寄:</span>
    李四 139****9999<br>
    <span style="margin-left: 17mm;">北京市朝阳区某某大厦8层</span>
    </div>
    </div>

    <div style="border-top: 1px solid #000; margin-top: 3mm; padding-top: 2mm;">
    <strong>物品信息:</strong>电子产品(已验视)<br>
    <strong>重量:</strong>2.5kg &nbsp;&nbsp; <strong>保价:</strong>¥5000
    </div>

    <div style="position: absolute; bottom: 5mm; left: 5mm; right: 5mm; font-size: 8pt; color: #666;">
    订单号:2024022414300001 &nbsp;&nbsp; 打印时间:2024-02-24 14:30:00
    </div>
    </div>
    </body>
    </html>

    精准打印的关键:

  • 毫米级尺寸控制:用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;
    }

    // 使用
    checkVisibility(document.getElementById('printContent'));

    还有就是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() {
    // 1. 等DOM更新(React/Vue)
    await new Promise(resolve => setTimeout(resolve, 0));

    // 2. 等字体
    if (document.fonts) {
    await document.fonts.ready;
    }

    // 3. 等图片(包括背景图)
    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 = src;
    return img;
    })];

    await Promise.all(allImages.map(img =>
    img.complete ? Promise.resolve() : new Promise((resolve) => {
    img.onload = img.onerror = resolve;
    setTimeout(resolve, 3000); // 单张图片最多等3秒
    })
    ));

    // 4. 等iframe(如果有的话)
    const iframes = Array.from(document.querySelectorAll('iframe'));
    await Promise.all(iframes.map(iframe =>
    new Promise((resolve) => {
    if (iframe.contentDocument?.readyState === 'complete') {
    resolve();
    } else {
    iframe.onload = resolve;
    setTimeout(resolve, 5000);
    }
    })
    ));

    // 5. 最后缓冲
    await new Promise(resolve => setTimeout(resolve, 500));
    }

    // 使用
    async function handlePrint() {
    await waitForEverything();
    window.print();
    }

    图片加载慢导致高度计算不准

    如果图片很多很大,即使做了等待,也可能因为网络问题导致超时。这时候可以考虑预先把图片转成base64:

    // 预加载并转换图片为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等),打印的时候只有可视区的几条数据。这时候你需要在打印前"展开"所有数据。

    // 假设你用的是react-window
    function handlePrint() {
    const listRef = useRef();

    // 1. 先滚动到底部,强制加载所有数据
    listRef.current.scrollToItem(999999, 'end');

    // 2. 等渲染完成
    setTimeout(() => {
    // 3. 临时禁用虚拟滚动,渲染全部内容
    setIsVirtual(false); // 你的状态控制

    // 4. 等DOM更新
    setTimeout(() => {
    window.print();

    // 5. 打印完恢复虚拟滚动
    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';

    // 触发所有数据的渲染(这取决于你用的具体库)
    // 可能需要手动触发scroll事件或者调用库提供的API

    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;

    // 可以扩展检查其他资源,比如video、audio等

    if (incompleteImages.length === 0 && fontsReady) {
    resolve({ success: true, time: Date.now() – startTime });
    return;
    }

    if (Date.now() – startTime > timeout) {
    console.warn('资源等待超时,未完成:', {
    images: incompleteImages.length,
    fonts: !fontsReady
    });
    resolve({ success: false, reason: 'timeout' });
    return;
    }

    requestAnimationFrame(check);
    };

    check();
    });
    }

    // 使用
    async function handlePrint() {
    console.log('等待资源加载…');
    const result = await createResourceWaiter(5000);
    console.log('资源状态:', result);

    window.print();
    }

    打印预览模拟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: calc(-297mm * (1 – 100vw / 230mm));
    }
    }

    这样在小屏幕上会自动缩放,保证能看到完整的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); // 3秒内只能点一次

    或者加个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
  • 代码写得再漂亮,打印出来是一坨屎也没人夸你。前端工程师的价值就体现在这些细节里。一个能把打印功能做得丝般顺滑的前端,绝对是团队里的宝藏。

    赶紧去试试吧。要是还搞不定,回来继续吐槽,我随时在线接招。毕竟咱们前端佬就是在填坑中成长的,对吧?

    欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。

    推荐:DTcode7的博客首页。 一个做过前端开发的产品经理,经历过睿智产品的折磨导致脱发之后,励志要翻身农奴把歌唱,一边打入敌人内部一边持续提升自己,为我们广大开发同胞谋福祉,坚决抵制睿智产品折磨我们码农兄弟!


    专栏系列(点击解锁)学习路线(点击解锁)知识定位
    《微信小程序相关博客》 持续更新中~ 结合微信官方原生框架、uniapp等小程序框架,记录请求、封装、tabbar、UI组件的学习记录和使用技巧等
    《AIGC相关博客》 持续更新中~ AIGC、AI生产力工具的介绍,例如stable diffusion这种的AI绘画工具安装、使用、技巧等总结
    《HTML网站开发相关》 《前端基础入门三大核心之html相关博客》 前端基础入门三大核心之html板块的内容,入坑前端或者辅助学习的必看知识
    《前端基础入门三大核心之JS相关博客》 前端JS是JavaScript语言在网页开发中的应用,负责实现交互效果和动态内容。它与HTML和CSS并称前端三剑客,共同构建用户界面。通过操作DOM元素、响应事件、发起网络请求等,JS使页面能够响应用户行为,实现数据动态展示和页面流畅跳转,是现代Web开发的核心
    《前端基础入门三大核心之CSS相关博客》 介绍前端开发中遇到的CSS疑问和各种奇妙的CSS语法,同时收集精美的CSS效果代码,用来丰富你的web网页
    《canvas绘图相关博客》 Canvas是HTML5中用于绘制图形的元素,通过JavaScript及其提供的绘图API,开发者可以在网页上绘制出各种复杂的图形、动画和图像效果。Canvas提供了高度的灵活性和控制力,使得前端绘图技术更加丰富和多样化
    《Vue实战相关博客》 持续更新中~ 详细总结了常用UI库elementUI的使用技巧以及Vue的学习之旅
    《python相关博客》 持续更新中~ Python,简洁易学的编程语言,强大到足以应对各种应用场景,是编程新手的理想选择,也是专业人士的得力工具
    《sql数据库相关博客》 持续更新中~ SQL数据库:高效管理数据的利器,学会SQL,轻松驾驭结构化数据,解锁数据分析与挖掘的无限可能
    《算法系列相关博客》 持续更新中~ 算法与数据结构学习总结,通过JS来编写处理复杂有趣的算法问题,提升你的技术思维
    《IT信息技术相关博客》 持续更新中~ 作为信息化人员所需要掌握的底层技术,涉及软件开发、网络建设、系统维护等领域的知识
    《信息化人员基础技能知识相关博客》 无论你是开发、产品、实施、经理,只要是从事信息化相关行业的人员,都应该掌握这些信息化的基础知识,可以不精通但是一定要了解,避免日常工作中贻笑大方
    《信息化技能面试宝典相关博客》 涉及信息化相关工作基础知识和面试技巧,提升自我能力与面试通过率,扩展知识面
    《前端开发习惯与小技巧相关博客》 持续更新中~ 罗列常用的开发工具使用技巧,如 Vscode快捷键操作、Git、CMD、游览器控制台等
    《photoshop相关博客》 持续更新中~ 基础的PS学习记录,含括PPI与DPI、物理像素dp、逻辑像素dip、矢量图和位图以及帧动画等的学习总结
    日常开发&办公&生产【实用工具】分享相关博客》 持续更新中~ 分享介绍各种开发中、工作中、个人生产以及学习上的工具,丰富阅历,给大家提供处理事情的更多角度,学习了解更多的便利工具,如Fiddler抓包、办公快捷键、虚拟机VMware等工具

    吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤 非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!

    在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » 前端老哥必看:window.print只打半截?一招搞定HTML实际高度打印不踩坑
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址