JS自定義打印及靜默打印的實(shí)現(xiàn)方法
在瀏覽器上打印應(yīng)該一個比較常見的操作。 最簡單的打印方式就是直接點(diǎn)擊瀏覽器右上角,找到“打印”按鈕或者調(diào)用window.print()
。 通過此方式,就能將當(dāng)前頁面整個打印出來了。
然而,實(shí)際情況下大多數(shù)需求都不會如此簡單。 更多的可能是需要打印頁面中的某一段“特定”內(nèi)容或者自定義內(nèi)容。 這就需要用到自定義打印了。
一、自定義打印兩個方法
實(shí)現(xiàn)自定義打印的方法網(wǎng)上能找到很多不同的實(shí)現(xiàn)方案和 js 庫。 其中有兩個用的最多的方法:
1)直接調(diào)用window.print()
。 在調(diào)用此方法之前將不需要被打印的元素先通過display='none'
隱藏掉,打印執(zhí)行完畢后再通過display='block'
還原頁面顯示。
2)創(chuàng)建臨時 Iframe 進(jìn)行打印。 臨時的 Iframe 標(biāo)簽創(chuàng)建完成后將需要打印的內(nèi)容拼接成 html 字符串渲染到 Iframe 里面,再執(zhí)行iframe.contentWindow.print()
。
方法 1 操作起來方便快捷,適合簡單的頁面,對于稍微復(fù)雜一點(diǎn)的頁面就很不方便了。 方法 2 適合復(fù)雜的打印需求,幾乎可以滿足所有的打印需求,本人在后文中設(shè)計(jì)的自定義打印方案就是基于此方法實(shí)現(xiàn)。
1.iframe 打印基本使用
Iframe 打印和直接頁面打印其實(shí)是一樣的,最終也是調(diào)用 window.print()
。 只不過我們是將打印的內(nèi)容渲染在 iframe 內(nèi)部,后在 iframe 內(nèi)部調(diào)用而已。
打印方法實(shí)現(xiàn)如下:
/** * 打印方法實(shí)現(xiàn) */ const handlePrintByLocalIframe = ({ printHtml }) => { // 判斷是否已經(jīng)存在該iframe let iframe: any = document.getElementById('J_printIframe'); if (!iframe) { // 新建一個隱藏起來的iframe,并將其添加到當(dāng)前頁面的dom里面 iframe = document.createElement('IFRAME'); iframe.setAttribute('id', 'J_printIframe'); iframe.setAttribute('style', 'position: absolute; width: 0px; height: 0px;left:-5000px;top:-5000px;'); document.body.appendChild(iframe); } const doc = iframe.contentWindow.document; // 將需要打印的html字符串寫入iframe doc.write(printHtml); doc.close(); iframe.contentWindow.focus(); setTimeout(function () { // 對iframe執(zhí)行打印操作 //延遲50ms是為了解決第一次樣式不生效的問題 iframe.contentWindow.print(); }, 50); // 網(wǎng)上有人加了這一段代碼,應(yīng)該是為了兼容ie,這個看個人需求添加上。 if (navigator.userAgent.indexOf('MSIE') > 0) { document.body.removeChild(iframe); } };
html 字符串拼接方法實(shí)現(xiàn)如下:
/** * 生成 Iframe 內(nèi)嵌頁面字符串并執(zhí)行打印 * 為了將業(yè)務(wù)和打印功能分開,這里將打印的 html 頁面做成了一個 html 模板,并上傳至 cdn。 * 后分別拉取 html 模板、接口數(shù)據(jù)、然后通過第三方庫 mustache 來組裝生成 html 字符串。 * 最后將其傳入前面的打印方法進(jìn)行打印 */ // 從cdn上獲取html字符串 const htmlStr = await fetchRemoteData('這里填寫html模板字符串的cdn地址'); // 從服務(wù)端獲取數(shù)據(jù) const data = await fetchRemoteData('這里獲取接口數(shù)據(jù),用于打印文件的數(shù)據(jù)'); // 使用mustache模板語法進(jìn)行渲染(需要和html模板字符串模板一致,可以使用其他模板如 handlebars) const printHtml = mustache.render(htmlStr, data); // 執(zhí)行打印 handlePrintByLocalIframe(printHtml);
至此,一個基本的打印功能就完成了,針對單頁打印、普通文本的打印場景已經(jīng)足夠了。
只是,這就結(jié)束了嗎?
當(dāng)然不會,實(shí)際需求中還有很多復(fù)雜的打印場景,比如報表打印。 打印報表的時候往往會涉及到分頁、頁頭、頁眉、頁腳等比較復(fù)雜的場景。
比如:
<!-- 文末有示例 --> 首頁頁需要頁頭其它也不需要 首頁都需要表頭,末頁需要簽名其它也不需要 ...
很顯然,面對這些“有理”要求,僅靠上面這個方案還做不到。
二、定制化的自定義打印
上文實(shí)現(xiàn)的打印,其實(shí)現(xiàn)原理就是拼接 html 字符串,然后將字符串傳入 iframe,然后進(jìn)行打印。 而作為一名前端開發(fā),操作 html 就像呼吸一樣簡單,想要在網(wǎng)頁上畫出來分頁、表頭、頁眉、頁腳這些根本沒什么難度可言。 因此,理論上只需要在原方案基礎(chǔ)上做“億點(diǎn)優(yōu)化”就可以解決了。
下面介紹一下本人的設(shè)計(jì)實(shí)現(xiàn)方案,其核心在于自定義分頁
。
具體打印方案
首先從接口拿到數(shù)據(jù)并將其轉(zhuǎn)換成下面的數(shù)據(jù)結(jié)構(gòu)。 其核心在于 pageList
,這個 pageList 保存的就是打印的時候各個打印頁需要用到的數(shù)據(jù)和配置。 我們?yōu)槊恳豁摱ㄖ飘?dāng)頁渲染所需要的特定的數(shù)據(jù)和特定配置。
1)約定的數(shù)據(jù)格式示例
const data = { pageTitle: '多頁模板的數(shù)據(jù)', pageList: [ { // 只有第一頁有head,后面的頁沒有 pageHead: true, pageNum: 1, // 當(dāng)前頁屬于第1頁 list: [ { dataId: 1, dataName: 'dataName1', dataNum: 8, }, //...第一頁的其他數(shù)據(jù) 28 條 ], }, { pageHead: false, // 除了第1頁其他頁面都不需要標(biāo)題信息。 pageNum: 2, // 當(dāng)前頁屬于第2頁 list: [ { dataId: 2, dataName: 'dataName2', dataNum: 6, }, //...第2頁的其他數(shù)據(jù) 28 + 2 條,多了pageHead 的空間所以多兩條 ], }, ], };
這份數(shù)據(jù)屬于是定制化數(shù)據(jù),具體數(shù)據(jù)格式與需要打印的 html 模板文件有關(guān)。 每一頁的數(shù)據(jù)都是通過手動計(jì)算出來的,計(jì)算方法示例如下:
/** * serverDataList 為接口返回的原始數(shù)組數(shù)據(jù) * 此方法將原始的數(shù)據(jù)轉(zhuǎn)換成每一頁單獨(dú)需要的特定數(shù)據(jù)格式 * 這里僅是一個示例,具體復(fù)雜度跟其打印的業(yè)務(wù)和模板文件有關(guān) * 理論上可以實(shí)現(xiàn)任何打印 */ const calculatePageNum = (serverDataList) => { // 這里的數(shù)值需要手動測量,畢竟每一行的高度都不一樣,需要根據(jù)實(shí)際情況測試出來 const firstPageMaxNum = 36; const otherPageMaxNum = 40; const pageList = []; let currentPage = 0; // 當(dāng)前遍歷到第幾頁 serverDataList.forEach((item, index) => { const { dataId, dataName, dataNum } = item; currentPage = index < firstPageMaxNum ? 1 : 1 + Math.ceil((index + 1 - firstPageMaxNum) / otherPageMaxNum); if (!pageList[currentPage - 1]) { pageList[currentPage - 1] = { pageHead: currentPage === 1, pageNum: currentPage, list: [item], }; } else { pageList[currentPage - 1].list.push(item); } }); return pageList; };
上述方法最終輸出的是一個大的 pageList, 內(nèi)部有一個小的 list。 pageList 包含的是各個頁面的數(shù)據(jù),而 list 包含的是某一頁的列表數(shù)據(jù)。 除此之外,還有當(dāng)前頁面的頁碼,是否應(yīng)該包含頭部信息等。
可以看出,這份數(shù)據(jù)就是為分頁服務(wù)的,有了這份數(shù)據(jù),我們只需要同步設(shè)計(jì)出相應(yīng)的 html 模板. 然后將對應(yīng)的數(shù)據(jù)傳入模板進(jìn)行渲染就能得到相應(yīng)的分頁 html 字符串了。
2)對應(yīng)的 html 模板
html模板可以是任何模板語法,這里我們采用的最簡單的mustache
語法
<body class="a4-body"> <!-- pageList的數(shù)組長度就是當(dāng)前頁數(shù),這里是一個遍歷循環(huán) --> {{#pageList}} <section class="a4-page"> {{#pageHead}} <header class="head"> <h2>{{pageTitle}}</h2> </header> {{/pageHead}} <table class="a4-table"> <tr> <th>數(shù)據(jù)ID</th> <th>數(shù)據(jù)名稱</th> <th>數(shù)據(jù)數(shù)量</th> </tr> <!-- 這里list就是當(dāng)前頁面的數(shù)據(jù),每一頁的長度可以不一樣,如果有header這里就少幾行 --> {{#list}} <tr> <td>{{dataId}}</td> <td>{{dataName}}</td> <td>{{dataNum}}</td> </tr> {{/list}} </table> </section> <ul class="a4-footer"> <li>第{{pageNum}}頁 總{{pageList.length}}頁</li> </ul> {{/pageList}} </body>
不難看出,當(dāng)我們將前面格式化出來的 pageList 數(shù)據(jù)渲染到如上模板就能得到多個 pageList。 每個 pageList 又包含多個數(shù)據(jù)行l(wèi)ist,最終輸出的就是一個完整的分頁 html 字符串結(jié)構(gòu)了。
當(dāng)然,僅僅有對應(yīng)的結(jié)構(gòu)是不夠的,作為 html 頁面,還需要配合對應(yīng)的 css 樣式。
所以,我們還需要用 css 來做一些布局來保證 pageList 里面的一個 item 的總高度為 A4 的高度
。 只要保證這個高度,其內(nèi)部樣式如何變化都沒關(guān)系,多一個 header、或者某個特殊頁面多一個特殊元素都無所謂。 無非是在計(jì)算 pageList 的時候?qū)?shù)據(jù)進(jìn)行增減即可。
因此,根據(jù)上文的 html 模板,對模板里面的元素設(shè)置其 a4-body 容器和 a4-page 容器的高度,使其每一頁高度固定。 這樣我們打印出來的內(nèi)容就是我們最終期望的分頁數(shù)據(jù)了。(這里主要介紹A4紙張,其它的原理類似)。
CSS 核心代碼如下:
/* css全部使用mm作為單位 */ .a4-body { width: 208mm; /** 這里的寬度就是A4紙的寬度 */ margin: 0 auto; text-align: center; } .a4-page { width: 100%; padding: 6mm; /** 這里高度 + a4-footer 的高度就是整張A4紙的高度(297mm) */ height: 288mm; margin: 0 auto; box-sizing: border-box; } .a4-footer { line-height: 9mm; }
至此,有了 html 模板和 css 負(fù)責(zé)處理 ui 和布局,傳入分割好的數(shù)據(jù),最終就能渲染出固定樣式的 html 頁面內(nèi)容了。 后面不論需要打印的內(nèi)容如何變化,只需要處理好這幾部分之間的關(guān)系,我們就能得到對應(yīng)的 html 頁面,將其塞入 iframe 就能打印任意內(nèi)容。
三、靜默打印
前面我們都是調(diào)用的瀏覽器自帶的打印能力,即 window.print()
方法觸發(fā)的瀏覽器預(yù)覽打印。 這種方式非常簡單,接入也不麻煩。
然而,它有一個不容疏忽的缺點(diǎn)(也不算缺點(diǎn),畢竟瀏覽器并不是專業(yè)打印設(shè)備,需要考慮到安全性和通用性),那就是打印觸發(fā)之前它一定會彈出一個“預(yù)覽”大彈窗。
而有時候我們的需求是點(diǎn)擊按鈕就實(shí)現(xiàn)打印,直接給打印機(jī)發(fā)出打印指令,不要彈出打印“預(yù)覽”彈窗。
通過各種途徑了解到,這是無法實(shí)現(xiàn)的,至少純“瀏覽器前端”,通過瀏覽器端的 js 無法實(shí)現(xiàn)。
那就沒有辦法了嗎?
當(dāng)然有,那就是自己開發(fā)一個打印App
。
瀏覽器本身其實(shí)也可以看做是一個特殊的“打印App”。 瀏覽器能調(diào)用打印機(jī),自定義打印App當(dāng)然可以。
1、如何設(shè)計(jì)打印App的功能
打印App就一個PC端的應(yīng)用,用 Electron 就能很輕松的做出來。 其需要實(shí)現(xiàn)兩個核心功能:
1.連接和管理電腦設(shè)備上的打印機(jī)
2.能夠與瀏覽器進(jìn)行通信。
連接和管理電腦設(shè)備上的打印機(jī)這個這里不做詳細(xì)介紹,網(wǎng)上有成熟實(shí)現(xiàn)方案,使用 electron 自帶的 API 即可實(shí)現(xiàn)。
至于如何與瀏覽器進(jìn)行通信,也不是麻煩,這里簡單介紹下實(shí)現(xiàn)思路。 其實(shí)也很簡單,無非就是一個 Socket 通信。
我們只需要在此應(yīng)用上啟用一個 Socket Server 服務(wù),此服務(wù)監(jiān)聽一個端口,比如:18877。 這個 Socket 服務(wù)和我們服務(wù)器上啟動的服務(wù)是一樣的,只不過此服務(wù)是直接部署到我們用戶的本地機(jī)器上的,只給當(dāng)前用戶使用的。
之后我們只需要在瀏覽器端啟動一個 Websocket 本地客戶端,然后建立與 ws://127.0.0.1:18877
的連接即可。
當(dāng)我們需要打印的時候,只需發(fā)送 Socket 信息給打印 App,將打印事件、打印文本及其他相關(guān)打印信息發(fā)送給打印控件服務(wù)。 打印控件接收到請求之后再調(diào)用電腦的打印功能,調(diào)用打印機(jī)即可。
至此,一套最基本的打印控件打印方案就算完成了。
最終實(shí)現(xiàn)整體架構(gòu)圖
打印結(jié)果示例
以上就是JS分頁打印及靜默打印的方法實(shí)現(xiàn)的詳細(xì)內(nèi)容,更多關(guān)于JS分頁打印及靜默打印的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
mpvue微信小程序的接口請求fly全局?jǐn)r截代碼實(shí)例
這篇文章主要介紹了mpvue微信小程序的接口請求fly全局?jǐn)r截代碼實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-11-11JS獲取鼠標(biāo)坐標(biāo)并且根據(jù)鼠標(biāo)位置不同彈出不同內(nèi)容
這篇文章主要介紹了js獲取鼠標(biāo)坐標(biāo)并且根據(jù)鼠標(biāo)位置不同彈出不同內(nèi)容的實(shí)例代碼,需要的朋友可以參考下2017-06-06JavaScript前后端數(shù)據(jù)交互工具ajax使用教程
Ajax(Asynchronous?Javascript?And?XML),即是異步的JavaScript和XML,Ajax其實(shí)就是瀏覽器與服務(wù)器之間的一種異步通信方式2022-10-10利用canvas實(shí)現(xiàn)的加載動畫效果實(shí)例代碼
之前看到一個Android的加載效果不錯,一直想自己動手做一個,正好這段時間重溫了一個Canvas,所以就嘗試了一下。下面這篇文章主要給大家介紹了關(guān)于利用canvas實(shí)現(xiàn)加載效果的相關(guān)資料,需要的朋友可以參考下。2017-07-07微信小程序?qū)崿F(xiàn)點(diǎn)擊導(dǎo)航標(biāo)簽滾動定位到對應(yīng)位置
這篇文章主要為大家詳細(xì)介紹了微信小程序?qū)崿F(xiàn)點(diǎn)擊導(dǎo)航標(biāo)簽滾動定位到對應(yīng)位置,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-11-11html向js方法傳遞參數(shù)具體實(shí)現(xiàn)
html如何向js方法傳遞參數(shù),在本文將為大家詳細(xì)介紹下html注冊事件向引用方法中的傳參問題,感興趣的朋友可以參考下,希望對大家有所幫助2013-08-08