Vue3自定義打印實(shí)現(xiàn)原理詳解
前言
最近接觸到了一個(gè) Vue3 的打印需求,我發(fā)現(xiàn)自己雖然從事前端開發(fā)已有多年,但對(duì)如何實(shí)現(xiàn)自定義打印還沒(méi)有深入研究,一般都是找現(xiàn)成的庫(kù)來(lái)解決問(wèn)題,借這次的機(jī)會(huì)研究了一下如何實(shí)現(xiàn)自定義打印。
在現(xiàn)在的前端開發(fā)中,打印是一個(gè)比較常見(jiàn)的功能,無(wú)論是在生成報(bào)表、下載發(fā)票還是其他用途上都會(huì)用到。 最基本的打印方式是直接打印整個(gè)頁(yè)面內(nèi)容,但在實(shí)際項(xiàng)目中,我們常常需要更靈活的打印功能,例如選擇性打印某個(gè)特定部分、提供打印預(yù)覽功能等。這就產(chǎn)生了許多 JavaScript 打印庫(kù),如 print-js
、vue-print-nb
等,這些庫(kù)不僅簡(jiǎn)化了打印流程,還提供了豐富的自定義打印配置,滿足各種不同的打印需求。
由于當(dāng)前的項(xiàng)目基于 Vue3 進(jìn)行開發(fā),我簡(jiǎn)單調(diào)研后發(fā)現(xiàn) vue-print-nb
是 Vue 中常用的打印庫(kù),同時(shí)其提供了適用于 Vue3 的 vue3-print-nb
庫(kù)。然而,在實(shí)際使用過(guò)程中,我發(fā)現(xiàn)該庫(kù)由于發(fā)布較早,且后期未繼續(xù)維護(hù),因此在支持 Vue3 的一些特性方面有所欠缺,所以我在此基礎(chǔ)上進(jìn)行了優(yōu)化和改造,來(lái)更好地滿足需求。
本文將詳細(xì)探討如何使用原生 JavaScript 實(shí)現(xiàn)自定義打印功能,同時(shí)講解對(duì) vue3-print-nb
的改造和優(yōu)化。
實(shí)現(xiàn)自定義打印
實(shí)現(xiàn)自定義打印的核心思想是通過(guò)將要打印的內(nèi)容放入一個(gè) iframe
或者新窗口中,然后調(diào)用 window.print()
方法進(jìn)行打印。然而,需要考慮的問(wèn)題遠(yuǎn)不止這些,例如樣式還原、表單展示、以及canvas 的打印等。
實(shí)現(xiàn)思路
1. 確定打印區(qū)域
首先,需要確定用戶希望打印的頁(yè)面區(qū)域, 這通常通過(guò)用戶傳遞的一個(gè)選擇器或直接傳入的 DOM 元素來(lái)實(shí)現(xiàn):
- 選擇器:通過(guò)傳遞一個(gè) CSS 選擇器來(lái)標(biāo)識(shí)需要打印的元素。
- DOM 元素:直接傳遞所需打印的 DOM 對(duì)象。
2. 克隆打印內(nèi)容
為了保證打印內(nèi)容與頁(yè)面其他部分隔離開來(lái),通常會(huì)將需要打印的內(nèi)容深度克隆。這確保了打印時(shí)不會(huì)受到動(dòng)態(tài)內(nèi)容變化的干擾,并且可以對(duì)其進(jìn)行獨(dú)立處理:
/** * 拷貝需要打印的節(jié)點(diǎn) */ function cloneContent(selector) { const originalContent = document .querySelector(selector) .cloneNode(true); // 在克隆內(nèi)容中處理樣式和事件,比如刪除不需要的節(jié)點(diǎn) originalContent .querySelectorAll('.no-print') .forEach((el) => el.remove()); return originalContent.outerHTML; }
3. 創(chuàng)建新的上下文
為了確保打印內(nèi)容不受當(dāng)前頁(yè)面的影響,通常會(huì)創(chuàng)建一個(gè)新的窗口或 iframe 并復(fù)制打印內(nèi)容到新的上下文中。兩種常見(jiàn)的實(shí)現(xiàn)方式是:
- 新窗口:打開一個(gè)新的瀏覽器窗口或標(biāo)簽頁(yè),將克隆內(nèi)容放置其內(nèi),然后觸發(fā)打印。
- Iframe:在當(dāng)前頁(yè)面中創(chuàng)建一個(gè)隱藏的
iframe
,將打印內(nèi)容放入iframe
后,再?gòu)?iframe
中觸發(fā)打印。
/** * 創(chuàng)建一個(gè)新的上下文(打印窗口) */ function createPrintIframe() { const iframe = document.createElement('iframe'); iframe.id = this.iframeId; iframe.src = new Date().getTime().toString(); iframe.style.display = 'none'; document.body.appendChild(iframe); return iframe; } /** * 將需要打印的內(nèi)容寫入新的上下文 */ function write(printWindow, writeContent) { const iframeDocument = printWindow.contentDocument; iframeDocument.open(); iframeDocument.write(`<!DOCTYPE html><html lang='zh'><head>${getHead()}</head><body>${writeContent}</body></html>`); iframeDocument.close(); }
4. 處理打印樣式
設(shè)計(jì)打印庫(kù)時(shí),首先需要解決的問(wèn)題就是如何在新的上下文中還原樣式,包括頁(yè)面原有樣式和用戶自定義樣式,以滿足復(fù)雜業(yè)務(wù)需求。
- 獨(dú)立樣式:插入到新的打印文檔或上下文中,可以是內(nèi)聯(lián)樣式或者外部樣式表。
- 打印媒體查詢:使用
@media print
媒體查詢,以定義僅在打印時(shí)生效的特殊樣式,使得打印和屏幕顯示效果獨(dú)立。
/** * 獲取 head */ function getHead() { // 獲取網(wǎng)頁(yè)原有的 css 鏈接 const links = Array.from(document.querySelectorAll('link')) .filter((item) => item.href.includes('.css')) .map( (item) => `<link type="text/css" rel="stylesheet" href='${item.href}'>` ) .join(''); // 獲取頁(yè)面中的 style 標(biāo)簽 const style = Array.from(document.styleSheets).reduce( (acc, styleSheet) => { const rules = styleSheet.cssRules || styleSheet.rules; if (!rules) return acc; acc += Array.from(rules).reduce( (innerAcc, rule) => innerAcc + rule.cssText, '' ); return acc; }, '' ); // ...省略處理用戶自定義引入的 css 樣式 // 返回 head return `<title>自定義打印示例</title>${links}<style type="text/css">${style}</style>`; }
5. 觸發(fā)打印
準(zhǔn)備好打印內(nèi)容和樣式后,打印庫(kù)通過(guò)調(diào)用 window.print
方法來(lái)觸發(fā)打印。打印操作通常包含以下步驟:
- 打印對(duì)話框:調(diào)用
window.print
來(lái)觸發(fā)系統(tǒng)打印對(duì)話框。 - 清除上下文:打印完成后,關(guān)閉或銷毀用于打印的臨時(shí)窗口或
iframe
,以避免內(nèi)存泄漏和資源占用。
/** * 觸發(fā)打印操作 */ function triggerPrint(printWindow) { const iframeWin = printWindow?.contentWindow; iframeWin.addEventListener('load', () => { iframeWin.focus(); iframeWin.print(); iframe.remove(); }) }
通過(guò)上述步驟,我們就可以靈活地控制打印內(nèi)容和樣式,并能提供良好的打印體驗(yàn)和可定制化的功能。
綜合示例
通過(guò)以上步驟,我們可以實(shí)現(xiàn)一個(gè)簡(jiǎn)易的自定義打印功能。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>自定義打印示例</title> <style> html, body { margin: 0; } body { padding: 20px } .printable { border: 1px solid #000; padding: 20px; } .gc { color: #0aff; font-size: 14px; } </style> </head> <body> <div class="printable" id="printableArea"> <h1>葫蘆娃</h1> <p class="gc">葫蘆娃 葫蘆娃</p> <p class="gc">一根藤上七朵花</p> <p class="gc">風(fēng)吹雨打都不怕 </p> <p class="gc">啦啦啦啦</p> <p class="gc">叮當(dāng)當(dāng)咚咚當(dāng)當(dāng) 葫蘆娃</p> <p class="gc">叮當(dāng)當(dāng)咚咚當(dāng)當(dāng) 本領(lǐng)大</p> <p class="gc">啦啦啦啦</p> <p class="no-print">我是不需要打印的內(nèi)容,啦啦啦啦啦</p> <p class="no-print">我是不需要打印的內(nèi)容,啦啦啦啦啦</p> <p class="no-print">我是不需要打印的內(nèi)容,啦啦啦啦啦</p> <p class="no-print">我是不需要打印的內(nèi)容,啦啦啦啦啦</p> </div> <div> <button id="printBtn" style="margin-top: 10px">打印內(nèi)容</button> </div> <script> /** * 獲取 head */ function getHead() { // 獲取網(wǎng)頁(yè)原有的 css 鏈接 const links = Array.from(document.querySelectorAll('link')) .filter((item) => item.href.includes('.css')) .map( (item) => `<link type="text/css" rel="stylesheet" href='${item.href}'>` ) .join(''); // 獲取頁(yè)面中的 style 標(biāo)簽 const style = Array.from(document.styleSheets).reduce( (acc, styleSheet) => { const rules = styleSheet.cssRules || styleSheet.rules; if (!rules) return acc; acc += Array.from(rules).reduce( (innerAcc, rule) => innerAcc + rule.cssText, '' ); return acc; }, '' ); // 返回 head return `<title>自定義打印示例</title>${links}<style type="text/css">${style}</style>`; } /** * 創(chuàng)建一個(gè)新的上下文(打印窗口) */ function createPrintIframe() { const iframe = document.createElement('iframe'); iframe.src = new Date().getTime().toString(); iframe.style.display = 'none'; document.body.appendChild(iframe); return iframe; } /** * 拷貝需要打印的節(jié)點(diǎn) */ function cloneContent(selector) { const originalContent = document .querySelector(selector) .cloneNode(true); // 在克隆內(nèi)容中處理樣式和事件,比如刪除不需要的節(jié)點(diǎn) originalContent .querySelectorAll('.no-print') .forEach((el) => el.remove()); return originalContent.outerHTML; } /** * 將需要打印的內(nèi)容寫入新的上下文 */ function write(printWindow, writeContent) { const iframeDocument = printWindow.contentDocument; iframeDocument.open(); iframeDocument.write(`<!DOCTYPE html><html lang='zh'><head>${getHead()}</head><body>${writeContent}</body></html>`); iframeDocument.close(); } /** * 觸發(fā)打印操作 */ function triggerPrint(printWindow) { const iframeWin = printWindow?.contentWindow; iframeWin.addEventListener('load', () => { iframeWin.focus(); iframeWin.print(); iframe.remove(); }) } /** * 打印元素 */ function printElement(selector) { const clonedContent = cloneContent(selector); const printWindow = createPrintIframe(); write(printWindow, clonedContent); triggerPrint(printWindow); } document.getElementById('printBtn').addEventListener('click', () => { printElement('#printableArea'); }); </script> </body> </html>
實(shí)現(xiàn)效果如下:
通過(guò)上述示例,可以實(shí)現(xiàn)一個(gè)簡(jiǎn)易的自定義打印功能。在實(shí)際業(yè)務(wù)中,我們可能需要支持更多復(fù)雜的需求,如外部 CSS 的引入,打印 canvas、表單等。
vue3-print-nb
vue3-print-nb
是一個(gè)用于 Vue3 的打印插件,提供了相對(duì)豐富的打印功能。但在實(shí)際使用過(guò)程中,我發(fā)現(xiàn)了一些不足之處,例如對(duì) Vue3 setup 函數(shù)的支持不夠友好,以及不支持手動(dòng)調(diào)用等。
優(yōu)化和改造
為了更好地支持 Vue3 setup 函數(shù),我對(duì) vue3-print-nb
進(jìn)行了以下改造:
- 使用 TypeScript 重寫了整個(gè)庫(kù),使得調(diào)用傳參時(shí)體驗(yàn)更佳。
- 增加了對(duì)
VuePrintNext
類的導(dǎo)出,允許手動(dòng)調(diào)用打印方法,不再局限于指令調(diào)用。 - 增強(qiáng) Vue3 setup 支持,允許通過(guò)直接導(dǎo)入
vPrint
指令進(jìn)行局部導(dǎo)入使用。 - 支持設(shè)置指定的 CSS 選擇器來(lái)忽略打印部分內(nèi)容。
- 支持通過(guò) CSS 選擇器或手動(dòng)傳入 DOM 節(jié)點(diǎn)進(jìn)行局部打印。
此外,還修復(fù)了一些已知的 bug:
- 背景色打印失效問(wèn)題。
- 通過(guò) URL 打印時(shí),有時(shí)無(wú)法觸發(fā)打印行為的問(wèn)題。
- 其他一些小問(wèn)題。
新打印插件
我已將上述優(yōu)化和改造封裝成一個(gè)新的插件:vue-print-next
,并已上傳至 GitHub:https://github.com/Alessandro-Pang/vue-print-next。你可以通過(guò) npm 進(jìn)行安裝和使用。
npm install vue-print-next
結(jié)語(yǔ)
通過(guò)本文,我們對(duì)自定義打印的實(shí)現(xiàn)原理有了比較全面的了解。盡管實(shí)現(xiàn)打印功能仍然依賴于 window.print
API,但通過(guò) window.open
或者 iframe
巧妙的將所需打印內(nèi)容進(jìn)行隔離處理,可以實(shí)現(xiàn)靈活的打印功能。
以上就是Vue3自定義打印實(shí)現(xiàn)原理詳解的詳細(xì)內(nèi)容,更多關(guān)于Vue3自定義打印的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue實(shí)現(xiàn)的網(wǎng)易云音樂(lè)在線播放和下載功能案例
這篇文章主要介紹了vue實(shí)現(xiàn)的網(wǎng)易云音樂(lè)在線播放和下載功能,結(jié)合具體實(shí)例形式分析了網(wǎng)易云音樂(lè)相關(guān)接口調(diào)用與操作技巧,需要的朋友可以參考下2019-02-02使用element-ui設(shè)置table組件寬度(width)為百分比
這篇文章主要介紹了使用element-ui設(shè)置table組件寬度(width)為百分比方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04Vue3通過(guò)hooks方式封裝節(jié)流和防抖的代碼詳解
vue3 中的 hooks 就是函數(shù)的一種寫法,就是將文件的一些單獨(dú)功能的js代碼進(jìn)行抽離出來(lái),放到單獨(dú)的js文件中,或者說(shuō)是一些可以復(fù)用的公共方法/功能,本文給大家介紹了Vue3通過(guò)hooks方式封裝節(jié)流和防抖,需要的朋友可以參考下2024-10-10vscode 使用Prettier插件格式化配置使用代碼詳解
這篇文章主要介紹了vscode 使用Prettier插件格式化配置使用,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-08-08Vue開發(fā)配置tsconfig.json文件的實(shí)現(xiàn)
tsconfig.json文件中指定了用來(lái)編譯這個(gè)項(xiàng)目的根文件和編譯選項(xiàng),本文就來(lái)介紹一下Vue開發(fā)配置tsconfig.json文件的實(shí)現(xiàn),感興趣的可以了解一下2023-08-08