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

然而,實(shí)際情況下大多數(shù)需求都不會(huì)如此簡(jiǎn)單。 更多的可能是需要打印頁面中的某一段“特定”內(nèi)容或者自定義內(nèi)容。 這就需要用到自定義打印了。
一、自定義打印兩個(gè)方法
實(shí)現(xiàn)自定義打印的方法網(wǎng)上能找到很多不同的實(shí)現(xiàn)方案和 js 庫。 其中有兩個(gè)用的最多的方法:
1)直接調(diào)用window.print()。 在調(diào)用此方法之前將不需要被打印的元素先通過display='none'隱藏掉,打印執(zhí)行完畢后再通過display='block'還原頁面顯示。
2)創(chuàng)建臨時(shí) Iframe 進(jìn)行打印。 臨時(shí)的 Iframe 標(biāo)簽創(chuàng)建完成后將需要打印的內(nèi)容拼接成 html 字符串渲染到 Iframe 里面,再執(zhí)行iframe.contentWindow.print()。
方法 1 操作起來方便快捷,適合簡(jiǎn)單的頁面,對(duì)于稍微復(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) {
// 新建一個(gè)隱藏起來的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 () {
// 對(duì)iframe執(zhí)行打印操作
//延遲50ms是為了解決第一次樣式不生效的問題
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)嵌頁面字符串并執(zhí)行打印
* 為了將業(yè)務(wù)和打印功能分開,這里將打印的 html 頁面做成了一個(gè) 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);
至此,一個(gè)基本的打印功能就完成了,針對(duì)單頁打印、普通文本的打印場(chǎng)景已經(jīng)足夠了。
只是,這就結(jié)束了嗎?
當(dāng)然不會(huì),實(shí)際需求中還有很多復(fù)雜的打印場(chǎng)景,比如報(bào)表打印。 打印報(bào)表的時(shí)候往往會(huì)涉及到分頁、頁頭、頁眉、頁腳等比較復(fù)雜的場(chǎng)景。
比如:
<!-- 文末有示例 --> 首頁頁需要頁頭其它也不需要 首頁都需要表頭,末頁需要簽名其它也不需要 ...
很顯然,面對(duì)這些“有理”要求,僅靠上面這個(gè)方案還做不到。
二、定制化的自定義打印
上文實(shí)現(xiàn)的打印,其實(shí)現(xiàn)原理就是拼接 html 字符串,然后將字符串傳入 iframe,然后進(jìn)行打印。 而作為一名前端開發(fā),操作 html 就像呼吸一樣簡(jiǎn)單,想要在網(wǎng)頁上畫出來分頁、表頭、頁眉、頁腳這些根本沒什么難度可言。 因此,理論上只需要在原方案基礎(chǔ)上做“億點(diǎn)優(yōu)化”就可以解決了。
下面介紹一下本人的設(shè)計(jì)實(shí)現(xiàn)方案,其核心在于自定義分頁。
具體打印方案
首先從接口拿到數(shù)據(jù)并將其轉(zhuǎn)換成下面的數(shù)據(jù)結(jié)構(gòu)。 其核心在于 pageList,這個(gè) pageList 保存的就是打印的時(shí)候各個(gè)打印頁需要用到的數(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ù)都是通過手動(dòng)計(jì)算出來的,計(jì)算方法示例如下:
/**
* serverDataList 為接口返回的原始數(shù)組數(shù)據(jù)
* 此方法將原始的數(shù)據(jù)轉(zhuǎn)換成每一頁單獨(dú)需要的特定數(shù)據(jù)格式
* 這里僅是一個(gè)示例,具體復(fù)雜度跟其打印的業(yè)務(wù)和模板文件有關(guān)
* 理論上可以實(shí)現(xiàn)任何打印
*/
const calculatePageNum = (serverDataList) => {
// 這里的數(shù)值需要手動(dòng)測(cè)量,畢竟每一行的高度都不一樣,需要根據(jù)實(shí)際情況測(cè)試出來
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;
};
上述方法最終輸出的是一個(gè)大的 pageList, 內(nèi)部有一個(gè)小的 list。 pageList 包含的是各個(gè)頁面的數(shù)據(jù),而 list 包含的是某一頁的列表數(shù)據(jù)。 除此之外,還有當(dāng)前頁面的頁碼,是否應(yīng)該包含頭部信息等。
可以看出,這份數(shù)據(jù)就是為分頁服務(wù)的,有了這份數(shù)據(jù),我們只需要同步設(shè)計(jì)出相應(yīng)的 html 模板. 然后將對(duì)應(yīng)的數(shù)據(jù)傳入模板進(jìn)行渲染就能得到相應(yīng)的分頁 html 字符串了。
2)對(duì)應(yīng)的 html 模板
html模板可以是任何模板語法,這里我們采用的最簡(jiǎn)單的mustache語法
<body class="a4-body">
<!-- pageList的數(shù)組長(zhǎng)度就是當(dāng)前頁數(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)前頁面的數(shù)據(jù),每一頁的長(zhǎng)度可以不一樣,如果有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ù)渲染到如上模板就能得到多個(gè) pageList。 每個(gè) pageList 又包含多個(gè)數(shù)據(jù)行l(wèi)ist,最終輸出的就是一個(gè)完整的分頁 html 字符串結(jié)構(gòu)了。
當(dāng)然,僅僅有對(duì)應(yīng)的結(jié)構(gòu)是不夠的,作為 html 頁面,還需要配合對(duì)應(yīng)的 css 樣式。
所以,我們還需要用 css 來做一些布局來保證 pageList 里面的一個(gè) item 的總高度為 A4 的高度。 只要保證這個(gè)高度,其內(nèi)部樣式如何變化都沒關(guān)系,多一個(gè) header、或者某個(gè)特殊頁面多一個(gè)特殊元素都無所謂。 無非是在計(jì)算 pageList 的時(shí)候?qū)?shù)據(jù)進(jìn)行增減即可。
因此,根據(jù)上文的 html 模板,對(duì)模板里面的元素設(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)系,我們就能得到對(duì)應(yīng)的 html 頁面,將其塞入 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ù)覽”彈窗。
通過各種途徑了解到,這是無法實(shí)現(xiàn)的,至少純“瀏覽器前端”,通過瀏覽器端的 js 無法實(shí)現(xiàn)。
那就沒有辦法了嗎?
當(dāng)然有,那就是自己開發(fā)一個(gè)打印App。
瀏覽器本身其實(shí)也可以看做是一個(gè)特殊的“打印App”。 瀏覽器能調(diào)用打印機(jī),自定義打印App當(dāng)然可以。
1、如何設(shè)計(jì)打印App的功能
打印App就一個(gè)PC端的應(yīng)用,用 Electron 就能很輕松的做出來。 其需要實(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)單,無非就是一個(gè) Socket 通信。
我們只需要在此應(yīng)用上啟用一個(gè) Socket Server 服務(wù),此服務(wù)監(jiān)聽一個(gè)端口,比如:18877。 這個(gè) Socket 服務(wù)和我們服務(wù)器上啟動(dòng)的服務(wù)是一樣的,只不過此服務(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分頁打印及靜默打印的方法實(shí)現(xiàn)的詳細(xì)內(nèi)容,更多關(guān)于JS分頁打印及靜默打印的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
mpvue微信小程序的接口請(qǐng)求fly全局?jǐn)r截代碼實(shí)例
這篇文章主要介紹了mpvue微信小程序的接口請(qǐng)求fly全局?jǐn)r截代碼實(shí)例,文中通過示例代碼介紹的非常詳細(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)航菜單,頁面會(huì)非常平滑的滾動(dòng),感興趣的朋友可以參考下哈希望對(duì)你有所幫助2013-05-05
利用canvas實(shí)現(xiàn)的加載動(dòng)畫效果實(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)求
本文介紹了如何通過裝飾器實(shí)現(xiàn)GET請(qǐng)求,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(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è)事件向引用方法中的傳參問題,感興趣的朋友可以參考下,希望對(duì)大家有所幫助2013-08-08

