JS自定義打印及靜默打印的實(shí)現(xiàn)方法
在瀏覽器上打印應(yīng)該一個(gè)比較常見(jiàn)的操作。 最簡(jiǎn)單的打印方式就是直接點(diǎn)擊瀏覽器右上角,找到“打印”按鈕或者調(diào)用window.print()。 通過(guò)此方式,就能將當(dāng)前頁(yè)面整個(gè)打印出來(lái)了。

然而,實(shí)際情況下大多數(shù)需求都不會(huì)如此簡(jiǎn)單。 更多的可能是需要打印頁(yè)面中的某一段“特定”內(nèi)容或者自定義內(nèi)容。 這就需要用到自定義打印了。
一、自定義打印兩個(gè)方法
實(shí)現(xiàn)自定義打印的方法網(wǎng)上能找到很多不同的實(shí)現(xiàn)方案和 js 庫(kù)。 其中有兩個(gè)用的最多的方法:
1)直接調(diào)用window.print()。 在調(diào)用此方法之前將不需要被打印的元素先通過(guò)display='none'隱藏掉,打印執(zhí)行完畢后再通過(guò)display='block'還原頁(yè)面顯示。
2)創(chuàng)建臨時(shí) Iframe 進(jìn)行打印。 臨時(shí)的 Iframe 標(biāo)簽創(chuàng)建完成后將需要打印的內(nèi)容拼接成 html 字符串渲染到 Iframe 里面,再執(zhí)行iframe.contentWindow.print()。
方法 1 操作起來(lái)方便快捷,適合簡(jiǎn)單的頁(yè)面,對(duì)于稍微復(fù)雜一點(diǎn)的頁(yè)面就很不方便了。 方法 2 適合復(fù)雜的打印需求,幾乎可以滿足所有的打印需求,本人在后文中設(shè)計(jì)的自定義打印方案就是基于此方法實(shí)現(xiàn)。
1.iframe 打印基本使用
Iframe 打印和直接頁(yè)面打印其實(shí)是一樣的,最終也是調(diào)用 window.print()。 只不過(guò)我們是將打印的內(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) {
// 新建一個(gè)隱藏起來(lái)的iframe,并將其添加到當(dāng)前頁(yè)面的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字符串寫(xiě)入iframe
doc.write(printHtml);
doc.close();
iframe.contentWindow.focus();
setTimeout(function () {
// 對(duì)iframe執(zhí)行打印操作
//延遲50ms是為了解決第一次樣式不生效的問(wèn)題
iframe.contentWindow.print();
}, 50);
// 網(wǎng)上有人加了這一段代碼,應(yīng)該是為了兼容ie,這個(gè)看個(gè)人需求添加上。
if (navigator.userAgent.indexOf('MSIE') > 0) {
document.body.removeChild(iframe);
}
};
html 字符串拼接方法實(shí)現(xiàn)如下:
/**
* 生成 Iframe 內(nèi)嵌頁(yè)面字符串并執(zhí)行打印
* 為了將業(yè)務(wù)和打印功能分開(kāi),這里將打印的 html 頁(yè)面做成了一個(gè) html 模板,并上傳至 cdn。
* 后分別拉取 html 模板、接口數(shù)據(jù)、然后通過(guò)第三方庫(kù) mustache 來(lái)組裝生成 html 字符串。
* 最后將其傳入前面的打印方法進(jìn)行打印
*/
// 從cdn上獲取html字符串
const htmlStr = await fetchRemoteData('這里填寫(xiě)html模板字符串的cdn地址');
// 從服務(wù)端獲取數(shù)據(jù)
const data = await fetchRemoteData('這里獲取接口數(shù)據(jù),用于打印文件的數(shù)據(jù)');
// 使用mustache模板語(yǔ)法進(jìn)行渲染(需要和html模板字符串模板一致,可以使用其他模板如 handlebars)
const printHtml = mustache.render(htmlStr, data);
// 執(zhí)行打印
handlePrintByLocalIframe(printHtml);
至此,一個(gè)基本的打印功能就完成了,針對(duì)單頁(yè)打印、普通文本的打印場(chǎng)景已經(jīng)足夠了。
只是,這就結(jié)束了嗎?
當(dāng)然不會(huì),實(shí)際需求中還有很多復(fù)雜的打印場(chǎng)景,比如報(bào)表打印。 打印報(bào)表的時(shí)候往往會(huì)涉及到分頁(yè)、頁(yè)頭、頁(yè)眉、頁(yè)腳等比較復(fù)雜的場(chǎng)景。
比如:
<!-- 文末有示例 --> 首頁(yè)頁(yè)需要頁(yè)頭其它也不需要 首頁(yè)都需要表頭,末頁(yè)需要簽名其它也不需要 ...
很顯然,面對(duì)這些“有理”要求,僅靠上面這個(gè)方案還做不到。
二、定制化的自定義打印
上文實(shí)現(xiàn)的打印,其實(shí)現(xiàn)原理就是拼接 html 字符串,然后將字符串傳入 iframe,然后進(jìn)行打印。 而作為一名前端開(kāi)發(fā),操作 html 就像呼吸一樣簡(jiǎn)單,想要在網(wǎng)頁(yè)上畫(huà)出來(lái)分頁(yè)、表頭、頁(yè)眉、頁(yè)腳這些根本沒(méi)什么難度可言。 因此,理論上只需要在原方案基礎(chǔ)上做“億點(diǎn)優(yōu)化”就可以解決了。
下面介紹一下本人的設(shè)計(jì)實(shí)現(xiàn)方案,其核心在于自定義分頁(yè)。
具體打印方案
首先從接口拿到數(shù)據(jù)并將其轉(zhuǎn)換成下面的數(shù)據(jù)結(jié)構(gòu)。 其核心在于 pageList,這個(gè) pageList 保存的就是打印的時(shí)候各個(gè)打印頁(yè)需要用到的數(shù)據(jù)和配置。 我們?yōu)槊恳豁?yè)定制當(dāng)頁(yè)渲染所需要的特定的數(shù)據(jù)和特定配置。
1)約定的數(shù)據(jù)格式示例
const data = {
pageTitle: '多頁(yè)模板的數(shù)據(jù)',
pageList: [
{
// 只有第一頁(yè)有head,后面的頁(yè)沒(méi)有
pageHead: true,
pageNum: 1, // 當(dāng)前頁(yè)屬于第1頁(yè)
list: [
{
dataId: 1,
dataName: 'dataName1',
dataNum: 8,
},
//...第一頁(yè)的其他數(shù)據(jù) 28 條
],
},
{
pageHead: false, // 除了第1頁(yè)其他頁(yè)面都不需要標(biāo)題信息。
pageNum: 2, // 當(dāng)前頁(yè)屬于第2頁(yè)
list: [
{
dataId: 2,
dataName: 'dataName2',
dataNum: 6,
},
//...第2頁(yè)的其他數(shù)據(jù) 28 + 2 條,多了pageHead 的空間所以多兩條
],
},
],
};
這份數(shù)據(jù)屬于是定制化數(shù)據(jù),具體數(shù)據(jù)格式與需要打印的 html 模板文件有關(guān)。 每一頁(yè)的數(shù)據(jù)都是通過(guò)手動(dòng)計(jì)算出來(lái)的,計(jì)算方法示例如下:
/**
* serverDataList 為接口返回的原始數(shù)組數(shù)據(jù)
* 此方法將原始的數(shù)據(jù)轉(zhuǎn)換成每一頁(yè)單獨(dú)需要的特定數(shù)據(jù)格式
* 這里僅是一個(gè)示例,具體復(fù)雜度跟其打印的業(yè)務(wù)和模板文件有關(guān)
* 理論上可以實(shí)現(xiàn)任何打印
*/
const calculatePageNum = (serverDataList) => {
// 這里的數(shù)值需要手動(dòng)測(cè)量,畢竟每一行的高度都不一樣,需要根據(jù)實(shí)際情況測(cè)試出來(lái)
const firstPageMaxNum = 36;
const otherPageMaxNum = 40;
const pageList = [];
let currentPage = 0; // 當(dāng)前遍歷到第幾頁(yè)
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;
};
上述方法最終輸出的是一個(gè)大的 pageList, 內(nèi)部有一個(gè)小的 list。 pageList 包含的是各個(gè)頁(yè)面的數(shù)據(jù),而 list 包含的是某一頁(yè)的列表數(shù)據(jù)。 除此之外,還有當(dāng)前頁(yè)面的頁(yè)碼,是否應(yīng)該包含頭部信息等。
可以看出,這份數(shù)據(jù)就是為分頁(yè)服務(wù)的,有了這份數(shù)據(jù),我們只需要同步設(shè)計(jì)出相應(yīng)的 html 模板. 然后將對(duì)應(yīng)的數(shù)據(jù)傳入模板進(jìn)行渲染就能得到相應(yīng)的分頁(yè) html 字符串了。
2)對(duì)應(yīng)的 html 模板
html模板可以是任何模板語(yǔ)法,這里我們采用的最簡(jiǎn)單的mustache語(yǔ)法
<body class="a4-body">
<!-- pageList的數(shù)組長(zhǎng)度就是當(dāng)前頁(yè)數(shù),這里是一個(gè)遍歷循環(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)前頁(yè)面的數(shù)據(jù),每一頁(yè)的長(zhǎng)度可以不一樣,如果有header這里就少幾行 -->
{{#list}}
<tr>
<td>{{dataId}}</td>
<td>{{dataName}}</td>
<td>{{dataNum}}</td>
</tr>
{{/list}}
</table>
</section>
<ul class="a4-footer">
<li>第{{pageNum}}頁(yè) 總{{pageList.length}}頁(yè)</li>
</ul>
{{/pageList}}
</body>
不難看出,當(dāng)我們將前面格式化出來(lái)的 pageList 數(shù)據(jù)渲染到如上模板就能得到多個(gè) pageList。 每個(gè) pageList 又包含多個(gè)數(shù)據(jù)行l(wèi)ist,最終輸出的就是一個(gè)完整的分頁(yè) html 字符串結(jié)構(gòu)了。
當(dāng)然,僅僅有對(duì)應(yīng)的結(jié)構(gòu)是不夠的,作為 html 頁(yè)面,還需要配合對(duì)應(yīng)的 css 樣式。
所以,我們還需要用 css 來(lái)做一些布局來(lái)保證 pageList 里面的一個(gè) item 的總高度為 A4 的高度。 只要保證這個(gè)高度,其內(nèi)部樣式如何變化都沒(méi)關(guān)系,多一個(gè) header、或者某個(gè)特殊頁(yè)面多一個(gè)特殊元素都無(wú)所謂。 無(wú)非是在計(jì)算 pageList 的時(shí)候?qū)?shù)據(jù)進(jìn)行增減即可。
因此,根據(jù)上文的 html 模板,對(duì)模板里面的元素設(shè)置其 a4-body 容器和 a4-page 容器的高度,使其每一頁(yè)高度固定。 這樣我們打印出來(lái)的內(nèi)容就是我們最終期望的分頁(yè)數(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 頁(yè)面內(nèi)容了。 后面不論需要打印的內(nèi)容如何變化,只需要處理好這幾部分之間的關(guān)系,我們就能得到對(duì)應(yīng)的 html 頁(yè)面,將其塞入 iframe 就能打印任意內(nèi)容。
三、靜默打印
前面我們都是調(diào)用的瀏覽器自帶的打印能力,即 window.print()方法觸發(fā)的瀏覽器預(yù)覽打印。 這種方式非常簡(jiǎn)單,接入也不麻煩。
然而,它有一個(gè)不容疏忽的缺點(diǎn)(也不算缺點(diǎn),畢竟瀏覽器并不是專業(yè)打印設(shè)備,需要考慮到安全性和通用性),那就是打印觸發(fā)之前它一定會(huì)彈出一個(gè)“預(yù)覽”大彈窗。
而有時(shí)候我們的需求是點(diǎn)擊按鈕就實(shí)現(xiàn)打印,直接給打印機(jī)發(fā)出打印指令,不要彈出打印“預(yù)覽”彈窗。
通過(guò)各種途徑了解到,這是無(wú)法實(shí)現(xiàn)的,至少純“瀏覽器前端”,通過(guò)瀏覽器端的 js 無(wú)法實(shí)現(xiàn)。
那就沒(méi)有辦法了嗎?
當(dāng)然有,那就是自己開(kāi)發(fā)一個(gè)打印App。
瀏覽器本身其實(shí)也可以看做是一個(gè)特殊的“打印App”。 瀏覽器能調(diào)用打印機(jī),自定義打印App當(dāng)然可以。
1、如何設(shè)計(jì)打印App的功能
打印App就一個(gè)PC端的應(yīng)用,用 Electron 就能很輕松的做出來(lái)。 其需要實(shí)現(xiàn)兩個(gè)核心功能:
1.連接和管理電腦設(shè)備上的打印機(jī)
2.能夠與瀏覽器進(jìn)行通信。
連接和管理電腦設(shè)備上的打印機(jī)這個(gè)這里不做詳細(xì)介紹,網(wǎng)上有成熟實(shí)現(xiàn)方案,使用 electron 自帶的 API 即可實(shí)現(xiàn)。
至于如何與瀏覽器進(jìn)行通信,也不是麻煩,這里簡(jiǎn)單介紹下實(shí)現(xiàn)思路。 其實(shí)也很簡(jiǎn)單,無(wú)非就是一個(gè) Socket 通信。
我們只需要在此應(yīng)用上啟用一個(gè) Socket Server 服務(wù),此服務(wù)監(jiān)聽(tīng)一個(gè)端口,比如:18877。 這個(gè) Socket 服務(wù)和我們服務(wù)器上啟動(dòng)的服務(wù)是一樣的,只不過(guò)此服務(wù)是直接部署到我們用戶的本地機(jī)器上的,只給當(dāng)前用戶使用的。
之后我們只需要在瀏覽器端啟動(dòng)一個(gè) Websocket 本地客戶端,然后建立與 ws://127.0.0.1:18877 的連接即可。
當(dāng)我們需要打印的時(shí)候,只需發(fā)送 Socket 信息給打印 App,將打印事件、打印文本及其他相關(guān)打印信息發(fā)送給打印控件服務(wù)。 打印控件接收到請(qǐng)求之后再調(diào)用電腦的打印功能,調(diào)用打印機(jī)即可。
至此,一套最基本的打印控件打印方案就算完成了。
最終實(shí)現(xiàn)整體架構(gòu)圖

打印結(jié)果示例

以上就是JS分頁(yè)打印及靜默打印的方法實(shí)現(xiàn)的詳細(xì)內(nèi)容,更多關(guān)于JS分頁(yè)打印及靜默打印的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
mpvue微信小程序的接口請(qǐng)求fly全局?jǐn)r截代碼實(shí)例
這篇文章主要介紹了mpvue微信小程序的接口請(qǐng)求fly全局?jǐn)r截代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-11-11
JS獲取鼠標(biāo)坐標(biāo)并且根據(jù)鼠標(biāo)位置不同彈出不同內(nèi)容
這篇文章主要介紹了js獲取鼠標(biāo)坐標(biāo)并且根據(jù)鼠標(biāo)位置不同彈出不同內(nèi)容的實(shí)例代碼,需要的朋友可以參考下2017-06-06
JavaScript前后端數(shù)據(jù)交互工具ajax使用教程
Ajax(Asynchronous?Javascript?And?XML),即是異步的JavaScript和XML,Ajax其實(shí)就是瀏覽器與服務(wù)器之間的一種異步通信方式2022-10-10
固定背景實(shí)現(xiàn)的背景滾動(dòng)特效示例分享
固定背景滾動(dòng)特效,使用background-attachment: fixed和導(dǎo)航菜單,頁(yè)面會(huì)非常平滑的滾動(dòng),感興趣的朋友可以參考下哈希望對(duì)你有所幫助2013-05-05
利用canvas實(shí)現(xiàn)的加載動(dòng)畫(huà)效果實(shí)例代碼
之前看到一個(gè)Android的加載效果不錯(cuò),一直想自己動(dòng)手做一個(gè),正好這段時(shí)間重溫了一個(gè)Canvas,所以就嘗試了一下。下面這篇文章主要給大家介紹了關(guān)于利用canvas實(shí)現(xiàn)加載效果的相關(guān)資料,需要的朋友可以參考下。2017-07-07
NestJS裝飾器實(shí)現(xiàn)GET請(qǐng)求
本文介紹了如何通過(guò)裝飾器實(shí)現(xiàn)GET請(qǐng)求,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-10-10
微信小程序?qū)崿F(xiàn)點(diǎn)擊導(dǎo)航標(biāo)簽滾動(dòng)定位到對(duì)應(yīng)位置
這篇文章主要為大家詳細(xì)介紹了微信小程序?qū)崿F(xiàn)點(diǎn)擊導(dǎo)航標(biāo)簽滾動(dòng)定位到對(duì)應(yīng)位置,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-11-11
html向js方法傳遞參數(shù)具體實(shí)現(xiàn)
html如何向js方法傳遞參數(shù),在本文將為大家詳細(xì)介紹下html注冊(cè)事件向引用方法中的傳參問(wèn)題,感興趣的朋友可以參考下,希望對(duì)大家有所幫助2013-08-08

