Vue3自定義打印實(shí)現(xiàn)原理詳解
前言
最近接觸到了一個(gè) Vue3 的打印需求,我發(fā)現(xiàn)自己雖然從事前端開(kāi)發(fā)已有多年,但對(duì)如何實(shí)現(xiàn)自定義打印還沒(méi)有深入研究,一般都是找現(xiàn)成的庫(kù)來(lái)解決問(wèn)題,借這次的機(jī)會(huì)研究了一下如何實(shí)現(xiàn)自定義打印。
在現(xiàn)在的前端開(kāi)發(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)行開(kāi)發(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è)面其他部分隔離開(kāi)來(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)方式是:
- 新窗口:打開(kāi)一個(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-cli + sass 的正確打開(kāi)方式圖文詳解
本文通過(guò)圖文并茂的形式給大家介紹了vue-cli + sass 的正確打開(kāi)方式,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下吧2017-10-10
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-04
Vue3通過(guò)hooks方式封裝節(jié)流和防抖的代碼詳解
vue3 中的 hooks 就是函數(shù)的一種寫法,就是將文件的一些單獨(dú)功能的js代碼進(jìn)行抽離出來(lái),放到單獨(dú)的js文件中,或者說(shuō)是一些可以復(fù)用的公共方法/功能,本文給大家介紹了Vue3通過(guò)hooks方式封裝節(jié)流和防抖,需要的朋友可以參考下2024-10-10
vscode 使用Prettier插件格式化配置使用代碼詳解
這篇文章主要介紹了vscode 使用Prettier插件格式化配置使用,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-08-08
Vue開(kāi)發(fā)配置tsconfig.json文件的實(shí)現(xiàn)
tsconfig.json文件中指定了用來(lái)編譯這個(gè)項(xiàng)目的根文件和編譯選項(xiàng),本文就來(lái)介紹一下Vue開(kāi)發(fā)配置tsconfig.json文件的實(shí)現(xiàn),感興趣的可以了解一下2023-08-08

