戰(zhàn)指南)
打印預(yù)覽里表格被攔腰截?cái)?、卡片從中間裂開、列表項(xiàng)跨頁斷行——這些幾乎每個(gè)做過打印需求的前端都踩過。你翻遍整個(gè)樣式表最后往往發(fā)現(xiàn)解決問題的關(guān)鍵就藏在一個(gè)叫page-break-inside的 CSS 屬性里。這個(gè)屬性屬于 CSS 分頁控制體系的一員它解決的核心問題很單純控制一個(gè)元素內(nèi)部是否允許被瀏覽器拆分成兩頁。不過單純不等于簡單真正把它用好需要理解瀏覽器碎片的機(jī)制、屬性值的邊界、與其他分頁屬性的配合以及一堆瀏覽器的兼容性坑。這篇文章就圍繞page-break-inside展開從原理到實(shí)戰(zhàn)把我在報(bào)表打印、訂單打印、在線簡歷導(dǎo)出這些場(chǎng)景里積累的經(jīng)驗(yàn)完整梳理一遍適合做后臺(tái)管理系統(tǒng)、B 端工具、電商打印功能的前端同學(xué)參考。1. 先弄明白瀏覽器在打印時(shí)到底怎么拆頁1.1 分頁機(jī)制與三大分頁屬性HTML 文檔在屏幕上是一股連續(xù)的流但打印到紙上時(shí)瀏覽器必須把它拆成一個(gè)個(gè)物理頁面。這個(gè)拆的動(dòng)作專業(yè)術(shù)語叫分頁pagination而瀏覽器決定在哪拆、怎么拆的規(guī)則就是 CSS 分頁控制。瀏覽器默認(rèn)的分頁規(guī)則是自動(dòng)找合理斷點(diǎn)優(yōu)先在兩塊元素之間斷開盡量避免在文本行中間斷開盡量避免在標(biāo)題后緊跟空白的尷尬位置。這套默認(rèn)行為在絕大多數(shù)簡單文檔里夠用可一旦遇到復(fù)雜的表格、卡片、長列表默認(rèn)規(guī)則就會(huì)露出破綻——它根本不知道你的表格行是一個(gè)不可分割的整體。CSS 為此提供了三個(gè)核心分頁屬性分別控制三個(gè)方向?qū)傩钥刂频姆猪摲较虻湫陀猛綽reak-before/page-break-before元素之前是否強(qiáng)制或禁止分頁每個(gè)新章節(jié)另起一頁、標(biāo)題前不分頁break-after/page-break-after元素之后是否強(qiáng)制或禁止分頁表格后強(qiáng)制分頁、避免尾部孤立標(biāo)題break-inside/page-break-inside元素內(nèi)部是否允許分頁表格行、卡片、列表項(xiàng)不被切斷這三個(gè)方向其實(shí)對(duì)應(yīng)了我們?cè)诜猪摽刂浦械娜啃枨笄懊嬖趺磾?、后面怎么斷、中間怎么斷。page-break-inside管的就是最后一類——元素中間??梢赃@樣理解前兩個(gè)屬性決定頁面之間的接縫位置而page-break-inside決定一個(gè)元素要不要被這股力量從中間撕開。1.2 auto 與 avoid 究竟改寫了什么page-break-inside的屬性值以auto為默認(rèn)值意思是不設(shè)置任何限制瀏覽器按默認(rèn)規(guī)則自由斷頁。另一個(gè)核心值是avoid含義是盡可能不要把元素內(nèi)部拆開。.card { page-break-inside: avoid; }當(dāng)你給一個(gè)卡片設(shè)置了avoid瀏覽器在計(jì)算分頁位置時(shí)會(huì)把這個(gè)卡片當(dāng)作一個(gè)不可分割的整體來處理。如果當(dāng)前頁剩余空間不夠放整個(gè)卡片它就會(huì)把整個(gè)卡片推到下一頁而不是在卡片中間斷行。這里有一個(gè)需要拆穿的關(guān)鍵認(rèn)知avoid是盡力避免不是絕對(duì)禁止。如果元素本身的高度超過了一整頁的可用空間瀏覽器無論如何都不可能把它完整放在一頁里這時(shí)候即使設(shè)置了avoid該切斷還是得切斷。因此不要把page-break-inside: avoid當(dāng)作保證元素永遠(yuǎn)完整的銀彈它是在元素高度小于頁面可用空間的前提下才成立。另外一個(gè)常見誤區(qū)是avoid不會(huì)改變?cè)氐牟季址绞讲粫?huì)讓元素變成新塊、不會(huì)觸發(fā) BFC、不影響屏幕顯示效果。它是一個(gè)純粹的打印建議只在分頁計(jì)算時(shí)生效。2. 從用法到本質(zhì)屬性取值與雙寫兼容策略2.1 核心屬性值與實(shí)際效果page-break-inside的合法取值比想象中少實(shí)際開發(fā)中你幾乎只會(huì)用到以下這些取值含義auto默認(rèn)值允許在元素內(nèi)部斷頁avoid盡可能避免在元素內(nèi)部斷頁initial重置為默認(rèn)值即 autoinherit繼承父元素的取值從規(guī)范角度說CSS 2.1 定義的page-break-inside只有auto和avoid兩個(gè)有效值initial和inherit是所有 CSS 屬性通用的全局關(guān)鍵字。但注意CSS Fragmentation 規(guī)范Level 3給出的新屬性break-inside則多了一些擴(kuò)展值比如avoid-page、avoid-column等這是老的page-break-inside沒有的能力。實(shí)際寫樣式時(shí)我一般這樣設(shè)置tr, li, .card { page-break-inside: avoid; }這個(gè)寫法兼容性極好2005 年之后的瀏覽器基本都認(rèn)識(shí)它。Chrome、Firefox、Safari、Edge 也都實(shí)現(xiàn)了avoid值的分頁避讓邏輯。不過要注意page-break-inside在規(guī)范中被標(biāo)記為繼承自舊屬性的兼容別名主流瀏覽器雖然支持但新項(xiàng)目里我更推薦另一種寫法見下一節(jié)。2.2 break-inside 與 page-break-inside 的關(guān)系CSS Fragmentation 規(guī)范引入了break-inside作為page-break-inside的正式替代品。它的作用不再是單純的分頁而是統(tǒng)一管理三種碎片化場(chǎng)景分頁page、分欄column、分區(qū)域region。兩者的關(guān)系可以概括為page-break-inside: avoid; break-inside: avoid;這兩行一起寫是業(yè)界標(biāo)準(zhǔn)的兼容寫法。新瀏覽器優(yōu)先讀取后者老瀏覽器忽略未知屬性繼續(xù)用前者。順序上先寫老屬性、再寫新屬性是慣例避免新瀏覽器被舊值覆蓋。但break-inside不只是語法糖它引入了更細(xì)分的值.avoid-page { break-inside: avoid-page; /* 只在分頁時(shí)避免斷開 */ } .avoid-column { break-inside: avoid-column; /* 只在多欄布局中避免斷開 */ }我用一個(gè)實(shí)際例子說明差異一個(gè)自適應(yīng)兩欄打印布局如果只想讓卡片在跨頁時(shí)不切斷但允許欄間斷開用break-inside: avoid-page就比單獨(dú)的avoid更精準(zhǔn)。而老寫法page-break-inside做不到這種細(xì)分。還有一點(diǎn)容易被忽略break-inside的子值avoid在實(shí)現(xiàn)上涵蓋avoid-page和avoid-column兩者的能力也就是分頁分欄都要避讓。所以日常寫一個(gè)break-inside: avoid就覆蓋了大部分需求。3. 實(shí)戰(zhàn)表格、卡片與列表在打印排版中的?;钐幚?.1 表格行的完整性保護(hù)表格是打印場(chǎng)景的重災(zāi)區(qū)。一行數(shù)據(jù)橫跨兩頁上半行在第一頁底部、下半行在第二頁頂部這種撕裂感對(duì)報(bào)表閱讀體驗(yàn)是毀滅性的。table tr { page-break-inside: avoid; break-inside: avoid; }只給tr設(shè)置避讓還不夠還要處理兩個(gè)連帶問題。第一給表格容器加上table-layout: fixed和適當(dāng)?shù)牧袑捈s束避免某些列內(nèi)容過長導(dǎo)致行高被撐得比頁面還高第二td內(nèi)部的文本如果是一個(gè)沒有空格的長串瀏覽器可能為了不破壞單詞而把整個(gè)字符塊換行這本身不影響分頁避讓但會(huì)導(dǎo)致行高異常。更細(xì)節(jié)的寫法是這樣的table { width: 100%; border-collapse: collapse; page-break-inside: auto; } thead { display: table-header-group; } tr { page-break-inside: avoid; } td, th { word-break: break-word; }這里thead { display: table-header-group; }是一個(gè)經(jīng)典技巧它讓表頭在每一頁都重復(fù)顯示。如果thead不是表格組顯示模式有些瀏覽器在表格跨頁時(shí)只會(huì)在第一頁顯示表頭后面幾頁光禿禿的讀者根本不知道列的含義。3.2 卡片、列表與標(biāo)題塊的連續(xù)性表格之外最常見的被切斷對(duì)象是卡片、列表項(xiàng)和標(biāo)題組合。商品卡片被切成上下兩半、dt/dd列表項(xiàng)跨頁、標(biāo)題留在頁尾而正文跑到下一頁——這些問題都可以用同一套方案解決。給一個(gè)通用工具類是最實(shí)用的做法.avoid-break { page-break-inside: avoid; break-inside: avoid; } .list-item { page-break-inside: avoid; } dt, dd { page-break-inside: avoid; }對(duì)于標(biāo)題單純保護(hù)標(biāo)題元素不夠還要避免標(biāo)題孤立在頁面底部的尷尬。這里要用到break-after或者page-break-afterh2, h3 { page-break-after: avoid; break-after: avoid; } h2, h3 { page-break-inside: avoid; }page-break-after: avoid的意思是此元素之后盡量避免分頁也就是標(biāo)題后面得跟著正文內(nèi)容。它和page-break-inside: avoid配合使用標(biāo)題既不會(huì)內(nèi)部折斷也不會(huì)孤零零地懸在頁尾。我在實(shí)際項(xiàng)目里會(huì)把這兩類規(guī)則合并成一個(gè)print.css的公共片段所有打印頁面統(tǒng)一引入省得每個(gè)業(yè)務(wù)模塊重復(fù)寫。3.3 配合頁面邊距與分頁控制的整體排版方案page-break-inside不是孤立存在的它只有和頁面尺寸、邊距、分頁位置配合在一起打印效果才可控。先設(shè)置頁面級(jí)別的基礎(chǔ)參數(shù)page { size: A4; margin: 12mm 14mm; }這里page規(guī)則定義了打印頁面的物理尺寸和邊距。page-break-inside: avoid的整頁放不下就推到下一頁邏輯參考的正是這個(gè)可用區(qū)域。邊距設(shè)得太大單個(gè)元素很容易超出當(dāng)前頁剩余空間觸發(fā)更多的整塊移動(dòng)導(dǎo)致頁面尾部留白增多邊距設(shè)得太小視覺上又顯得擁擠。我用下來上下 12mm、左右 14mm 是比較均衡的初始值。還有一個(gè)細(xì)節(jié)值得注意當(dāng)一個(gè)元素因avoid被推到下一頁當(dāng)前頁尾部可能出現(xiàn)一大塊空白。這時(shí)候可以結(jié)合h2 { break-before: page; }這類規(guī)則來強(qiáng)制規(guī)劃分頁點(diǎn)讓內(nèi)容區(qū)域更緊湊。但這個(gè)要謹(jǐn)慎用強(qiáng)制分頁會(huì)讓頁面數(shù)量不可控適合每一章/每個(gè)表格開頭的場(chǎng)景不適合所有元素。另外如果卡片內(nèi)部有邊框或背景色跨頁保護(hù)還有一個(gè)視覺補(bǔ)充方案.card { page-break-inside: avoid; box-decoration-break: clone; }box-decoration-break: clone能讓同一元素被切斷時(shí)每個(gè)碎片都單獨(dú)渲染邊框、背景和內(nèi)邊距避免出現(xiàn)只有一半邊框的難看效果。但要注意如果用了avoid元素大概率不會(huì)被切斷這個(gè)屬性是給那些高度超過整頁、必須被切斷的元素準(zhǔn)備的。4. 排錯(cuò)清單為什么設(shè)置了 avoid 還是被切斷4.1 常見失效原因與排查順序我見過太多人寫了page-break-inside: avoid卻毫無效果于是懷疑屬性是假的。實(shí)際上這個(gè)屬性在大多數(shù)瀏覽器里是可靠的失效通常是下面幾種情況造成的元素高度超過整頁可用空間。這是最容易被忽略的原因。avoid的本意是整體放不下就整體移到下頁但如果元素本身比一頁還高瀏覽器沒有任何辦法把它放在單頁內(nèi)只能在中間切斷。排查方法是檢查打印預(yù)覽中該元素在屏幕上占的高度是否超過了頁面內(nèi)容區(qū)的高度。display 顯示模式的影響。老版本的 Chrome 對(duì)inline-*顯示模式的元素page-break-inside支持不穩(wěn)定。如果你把tr設(shè)置成了display: inline-table或者某個(gè)容器用了display: inline-block分頁避讓可能不生效。優(yōu)先保證目標(biāo)元素是塊級(jí)或表格相關(guān)顯示模式。overflow 設(shè)置干擾。當(dāng)元素設(shè)置了overflow: hidden或overflow: auto它在打印時(shí)會(huì)被視為一塊獨(dú)立的滾動(dòng)容器分頁算法在容器內(nèi)部切頁的行為會(huì)變得不可預(yù)期。某些瀏覽器直接忽略內(nèi)部元素的分頁避讓直接按容器邊界切斷。排查時(shí)可以先臨時(shí)去掉overflow看看效果。父容器的約束。如果父元素設(shè)置了page-break-inside: auto子元素的avoid依然有效但如果父元素本身有一個(gè)明確的高度比如height: 500px子元素一旦超出這個(gè)高度就會(huì)觸發(fā)父元素的分頁切斷這時(shí)候子元素的avoid是約束不住外層容器的。換句話說avoid只對(duì)自己內(nèi)部生效管不了父級(jí)拆它。嵌套避讓的沖突。多層元素都設(shè)置avoid時(shí)瀏覽器會(huì)嘗試滿足所有約束。如果兩個(gè)相鄰的不可分割元素加起來超過一頁瀏覽器必須打破其中至少一個(gè)約束于是較內(nèi)層的保護(hù)可能失效。這是分頁算法在約束沖突時(shí)的妥協(xié)。我把排查順序整理成一個(gè)速查表排查項(xiàng)操作方法元素是否高于整頁打印預(yù)覽測(cè)量元素在頁面中的高度是否存在 inline 顯示模式檢查 display 屬性是否存在 overflow 設(shè)置臨時(shí)移除 overflow 驗(yàn)證父容器是否有固定高度檢查祖先元素 height是否多個(gè) avoid 元素沖突減少避讓元素?cái)?shù)量測(cè)試4.2 瀏覽器兼容差異與降級(jí)方案page-break-inside的瀏覽器兼容性整體不錯(cuò)但細(xì)節(jié)上有幾個(gè)歷史分歧。Chrome 自 52 版本起對(duì)break-inside的支持逐漸完善但很長一段時(shí)間break-inside: avoid在 flex 容器內(nèi)部的作用不穩(wěn)定。這是因?yàn)樵缙趯?shí)現(xiàn)只針對(duì)塊級(jí)格式化上下文做避讓計(jì)算flex item 的分頁行為直到后來才跟進(jìn)。如果打印布局里大量使用 flex建議對(duì)關(guān)鍵保護(hù)元素額外包一層塊級(jí)容器或者改用傳統(tǒng)的 block 布局來承載打印結(jié)構(gòu)。Firefox 在break-inside支持上一直比較干脆但老版本的 Firefox 對(duì)page-break-inside: avoid也存在已知 bug——當(dāng)元素是表格單元格且表格設(shè)置了邊框合并border-collapse時(shí)避讓邏輯偶爾失效。遇到這種場(chǎng)景可以給單元格內(nèi)部再套一層 div把避讓目標(biāo)從td轉(zhuǎn)移到內(nèi)層 div 上。老 Edge非 Chromium 內(nèi)核和 IE11 的情況最特殊。IE11 根本無視page-break-inside: avoid而老 Edge 對(duì)page-break-inside只支持auto和avoid兩個(gè)值。好在現(xiàn)在需要兼容 IE 的打印場(chǎng)景越來越少真遇到了就只能接受現(xiàn)實(shí)把不穩(wěn)定顯示模式的內(nèi)容結(jié)構(gòu)調(diào)整成塊級(jí)元素并期待用戶升級(jí)瀏覽器。我的建議是代碼里永遠(yuǎn)雙寫新老屬性.component { page-break-inside: avoid; /* 老瀏覽器兜底 */ break-inside: avoid; /* 新規(guī)范實(shí)現(xiàn) */ }這樣在不支持break-inside的老環(huán)境里page-break-inside能兜住一部分在支持新規(guī)范的瀏覽器里break-inside拿到精確的語義。5. 把這套知識(shí)沉淀成自己的打印腳手架5.1 通用分頁保護(hù)樣式模板實(shí)踐幾次之后我整理了一套通用的print.scss片段新項(xiàng)目直接拷貝刪掉不需要的部分就能用。以下是一份最小可用的打印?;钅0錷edia print { * { -webkit-print-color-adjust: exact; print-color-adjust: exact; } body { font-size: 11pt; line-height: 1.5; } .avoid-break { page-break-inside: avoid; break-inside: avoid; } table { page-break-inside: auto; } table tr { page-break-inside: avoid; break-inside: avoid; } thead { display: table-header-group; } h1, h2, h3, h4 { page-break-after: avoid; break-after: avoid; page-break-inside: avoid; } img { page-break-inside: avoid; max-width: 100%; } ul, ol, dl { page-break-inside: avoid; } a[href]::after { content: ( attr(href) ); font-size: 0.8em; color: #666; } page { size: A4; margin: 12mm 14mm; } }這里有幾個(gè)設(shè)計(jì)取舍值得展開說明。a[href]::after顯示鏈接地址是一份打印樣式里的貼心小細(xì)節(jié)。紙質(zhì)文檔上讀者沒法點(diǎn)擊鏈接把 URL 打印出來才方便人按圖索驥。如果視覺上太雜亂可以只給特定類名的鏈接啟用。-webkit-print-color-adjust: exact的作用是強(qiáng)制保留背景色和背景圖避免瀏覽器為了省墨默認(rèn)去掉打印背景。設(shè)置table { page-break-inside: auto; }是刻意的表格作為一個(gè)整體往往跨越多頁強(qiáng)行avoid反而會(huì)導(dǎo)致表格被整塊推來推去產(chǎn)生大量空白。正確的策略是表格允許跨頁但行不允許。ul, ol, dl加avoid要小心。如果列表項(xiàng)很多整體超過一頁avoid會(huì)被強(qiáng)制破壞這時(shí)候可以把避讓粒度降到li、dt、dd層面而不是整個(gè)列表。5.2 驗(yàn)收步驟怎么快速驗(yàn)證打印效果樣式寫完了驗(yàn)證環(huán)節(jié)不能只看屏幕。我的標(biāo)準(zhǔn)驗(yàn)收流程是這樣的第一步在瀏覽器里打開打印預(yù)覽Ctrl/Cmd P確認(rèn)預(yù)覽中的分頁位置符合預(yù)期。第二步逐頁檢查有沒有半截元素——重點(diǎn)看表格行、卡片、標(biāo)題和圖片。第三步調(diào)整page邊距參數(shù)把頁面數(shù)量控制在一個(gè)合理范圍同時(shí)避免某個(gè)元素恰好差幾毫米跨頁。第四步如果頁面里包含動(dòng)態(tài)插入的數(shù)據(jù)一定要用數(shù)據(jù)量最多的那條記錄去測(cè)試很多打印 bug 只在內(nèi)容撐滿一整頁時(shí)才暴露。還有一個(gè)終端經(jīng)驗(yàn)在 Chrome 的打印預(yù)覽里直接點(diǎn)擊每頁張數(shù)下拉框選2 張/頁能快速發(fā)現(xiàn)元素是否被橫切成兩半。這是個(gè)老技巧卻總能在演示前幫我抓住遺漏的分頁問題。在 flex、grid 布局大行其道的今天很多打印 bug 的根源其實(shí)是個(gè)現(xiàn)代 CSS 和傳統(tǒng)分頁算法的錯(cuò)位。我現(xiàn)在的做法是打印組件繼續(xù)保持獨(dú)立的塊級(jí)結(jié)構(gòu)不把彈性布局直接塞給打印環(huán)境。page-break-inside這個(gè)屬性本身不復(fù)雜但它背后是一整套內(nèi)容?;畹乃季S——print 不是把屏幕內(nèi)容拍扁到紙上而是重新排版一遍。最后分享一個(gè)印象最深的坑某次客戶反饋合同打印時(shí)一個(gè)簽名區(qū)域永遠(yuǎn)會(huì)被切斷怎么加avoid都沒用。排查到最后發(fā)現(xiàn)簽名區(qū)外層套了一個(gè)overflow: hidden的容器高度正好是 980px而頁面內(nèi)容區(qū)只有 960px。這個(gè)容器本身印不下瀏覽器只能硬切內(nèi)層的所有避讓都被繞過了。從那以后我在處理打印樣式時(shí)遇到overflow: hidden就格外敏感它像是打印世界里的規(guī)則破壞者總要再三確認(rèn)是否真的需要。