Vue3自定義打印實現(xiàn)原理詳解
前言
最近接觸到了一個 Vue3 的打印需求,我發(fā)現(xiàn)自己雖然從事前端開發(fā)已有多年,但對如何實現(xiàn)自定義打印還沒有深入研究,一般都是找現(xiàn)成的庫來解決問題,借這次的機會研究了一下如何實現(xiàn)自定義打印。
在現(xiàn)在的前端開發(fā)中,打印是一個比較常見的功能,無論是在生成報表、下載發(fā)票還是其他用途上都會用到。 最基本的打印方式是直接打印整個頁面內(nèi)容,但在實際項目中,我們常常需要更靈活的打印功能,例如選擇性打印某個特定部分、提供打印預覽功能等。這就產(chǎn)生了許多 JavaScript 打印庫,如 print-js、vue-print-nb 等,這些庫不僅簡化了打印流程,還提供了豐富的自定義打印配置,滿足各種不同的打印需求。
由于當前的項目基于 Vue3 進行開發(fā),我簡單調(diào)研后發(fā)現(xiàn) vue-print-nb 是 Vue 中常用的打印庫,同時其提供了適用于 Vue3 的 vue3-print-nb 庫。然而,在實際使用過程中,我發(fā)現(xiàn)該庫由于發(fā)布較早,且后期未繼續(xù)維護,因此在支持 Vue3 的一些特性方面有所欠缺,所以我在此基礎(chǔ)上進行了優(yōu)化和改造,來更好地滿足需求。
本文將詳細探討如何使用原生 JavaScript 實現(xiàn)自定義打印功能,同時講解對 vue3-print-nb 的改造和優(yōu)化。
實現(xiàn)自定義打印
實現(xiàn)自定義打印的核心思想是通過將要打印的內(nèi)容放入一個 iframe 或者新窗口中,然后調(diào)用 window.print() 方法進行打印。然而,需要考慮的問題遠不止這些,例如樣式還原、表單展示、以及canvas 的打印等。
實現(xiàn)思路
1. 確定打印區(qū)域
首先,需要確定用戶希望打印的頁面區(qū)域, 這通常通過用戶傳遞的一個選擇器或直接傳入的 DOM 元素來實現(xiàn):
- 選擇器:通過傳遞一個 CSS 選擇器來標識需要打印的元素。
- DOM 元素:直接傳遞所需打印的 DOM 對象。
2. 克隆打印內(nèi)容
為了保證打印內(nèi)容與頁面其他部分隔離開來,通常會將需要打印的內(nèi)容深度克隆。這確保了打印時不會受到動態(tài)內(nèi)容變化的干擾,并且可以對其進行獨立處理:
/**
* 拷貝需要打印的節(jié)點
*/
function cloneContent(selector) {
const originalContent = document
.querySelector(selector)
.cloneNode(true);
// 在克隆內(nèi)容中處理樣式和事件,比如刪除不需要的節(jié)點
originalContent
.querySelectorAll('.no-print')
.forEach((el) => el.remove());
return originalContent.outerHTML;
}
3. 創(chuàng)建新的上下文
為了確保打印內(nèi)容不受當前頁面的影響,通常會創(chuàng)建一個新的窗口或 iframe 并復制打印內(nèi)容到新的上下文中。兩種常見的實現(xiàn)方式是:
- 新窗口:打開一個新的瀏覽器窗口或標簽頁,將克隆內(nèi)容放置其內(nèi),然后觸發(fā)打印。
- Iframe:在當前頁面中創(chuàng)建一個隱藏的
iframe,將打印內(nèi)容放入iframe后,再從iframe中觸發(fā)打印。
/**
* 創(chuàng)建一個新的上下文(打印窗口)
*/
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è)計打印庫時,首先需要解決的問題就是如何在新的上下文中還原樣式,包括頁面原有樣式和用戶自定義樣式,以滿足復雜業(yè)務(wù)需求。
- 獨立樣式:插入到新的打印文檔或上下文中,可以是內(nèi)聯(lián)樣式或者外部樣式表。
- 打印媒體查詢:使用
@media print媒體查詢,以定義僅在打印時生效的特殊樣式,使得打印和屏幕顯示效果獨立。
/**
* 獲取 head
*/
function getHead() {
// 獲取網(wǎng)頁原有的 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('');
// 獲取頁面中的 style 標簽
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ā)打印
準備好打印內(nèi)容和樣式后,打印庫通過調(diào)用 window.print 方法來觸發(fā)打印。打印操作通常包含以下步驟:
- 打印對話框:調(diào)用
window.print來觸發(fā)系統(tǒng)打印對話框。 - 清除上下文:打印完成后,關(guān)閉或銷毀用于打印的臨時窗口或
iframe,以避免內(nèi)存泄漏和資源占用。
/**
* 觸發(fā)打印操作
*/
function triggerPrint(printWindow) {
const iframeWin = printWindow?.contentWindow;
iframeWin.addEventListener('load', () => {
iframeWin.focus();
iframeWin.print();
iframe.remove();
})
}
通過上述步驟,我們就可以靈活地控制打印內(nèi)容和樣式,并能提供良好的打印體驗和可定制化的功能。
綜合示例
通過以上步驟,我們可以實現(xià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">風吹雨打都不怕 </p>
<p class="gc">啦啦啦啦</p>
<p class="gc">叮當當咚咚當當 葫蘆娃</p>
<p class="gc">叮當當咚咚當當 本領(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)頁原有的 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('');
// 獲取頁面中的 style 標簽
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)建一個新的上下文(打印窗口)
*/
function createPrintIframe() {
const iframe = document.createElement('iframe');
iframe.src = new Date().getTime().toString();
iframe.style.display = 'none';
document.body.appendChild(iframe);
return iframe;
}
/**
* 拷貝需要打印的節(jié)點
*/
function cloneContent(selector) {
const originalContent = document
.querySelector(selector)
.cloneNode(true);
// 在克隆內(nèi)容中處理樣式和事件,比如刪除不需要的節(jié)點
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>
實現(xiàn)效果如下:

通過上述示例,可以實現(xiàn)一個簡易的自定義打印功能。在實際業(yè)務(wù)中,我們可能需要支持更多復雜的需求,如外部 CSS 的引入,打印 canvas、表單等。
vue3-print-nb
vue3-print-nb 是一個用于 Vue3 的打印插件,提供了相對豐富的打印功能。但在實際使用過程中,我發(fā)現(xiàn)了一些不足之處,例如對 Vue3 setup 函數(shù)的支持不夠友好,以及不支持手動調(diào)用等。
優(yōu)化和改造
為了更好地支持 Vue3 setup 函數(shù),我對 vue3-print-nb 進行了以下改造:
- 使用 TypeScript 重寫了整個庫,使得調(diào)用傳參時體驗更佳。
- 增加了對
VuePrintNext類的導出,允許手動調(diào)用打印方法,不再局限于指令調(diào)用。 - 增強 Vue3 setup 支持,允許通過直接導入
vPrint指令進行局部導入使用。 - 支持設(shè)置指定的 CSS 選擇器來忽略打印部分內(nèi)容。
- 支持通過 CSS 選擇器或手動傳入 DOM 節(jié)點進行局部打印。
此外,還修復了一些已知的 bug:
- 背景色打印失效問題。
- 通過 URL 打印時,有時無法觸發(fā)打印行為的問題。
- 其他一些小問題。
新打印插件
我已將上述優(yōu)化和改造封裝成一個新的插件:vue-print-next,并已上傳至 GitHub:https://github.com/Alessandro-Pang/vue-print-next。你可以通過 npm 進行安裝和使用。
npm install vue-print-next
結(jié)語
通過本文,我們對自定義打印的實現(xiàn)原理有了比較全面的了解。盡管實現(xiàn)打印功能仍然依賴于 window.print API,但通過 window.open 或者 iframe 巧妙的將所需打印內(nèi)容進行隔離處理,可以實現(xiàn)靈活的打印功能。
以上就是Vue3自定義打印實現(xiàn)原理詳解的詳細內(nèi)容,更多關(guān)于Vue3自定義打印的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue實現(xiàn)的網(wǎng)易云音樂在線播放和下載功能案例
這篇文章主要介紹了vue實現(xiàn)的網(wǎng)易云音樂在線播放和下載功能,結(jié)合具體實例形式分析了網(wǎng)易云音樂相關(guān)接口調(diào)用與操作技巧,需要的朋友可以參考下2019-02-02
使用element-ui設(shè)置table組件寬度(width)為百分比
這篇文章主要介紹了使用element-ui設(shè)置table組件寬度(width)為百分比方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-04-04
Vue3通過hooks方式封裝節(jié)流和防抖的代碼詳解
vue3 中的 hooks 就是函數(shù)的一種寫法,就是將文件的一些單獨功能的js代碼進行抽離出來,放到單獨的js文件中,或者說是一些可以復用的公共方法/功能,本文給大家介紹了Vue3通過hooks方式封裝節(jié)流和防抖,需要的朋友可以參考下2024-10-10
vscode 使用Prettier插件格式化配置使用代碼詳解
這篇文章主要介紹了vscode 使用Prettier插件格式化配置使用,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-08-08
Vue開發(fā)配置tsconfig.json文件的實現(xiàn)
tsconfig.json文件中指定了用來編譯這個項目的根文件和編譯選項,本文就來介紹一下Vue開發(fā)配置tsconfig.json文件的實現(xiàn),感興趣的可以了解一下2023-08-08

