前言
目前在业务碰到一个需求,之前是一个网页的应用,现在要求把这个应用内嵌到Electron里面打包成一个安装包,由于之前应用内涉及到大量的打印功能,且采用的都是window.print()
的方式来实现的,这种方式在谷歌等现代浏览器上本身就能够实现预览,虽然内嵌到Electron后还是能成功打印,但是无法进行预览,只会出现如下图的系统页面

所以希望在不调整之前系统的打印逻辑前提下,能够实现在Electron中进行打印预览并成功打印,经过摸索,得出了以下的解决方案:
1. 创建Electron应用,内嵌网页
首先创建一个electron窗口,直接通过mainWindow.loadURL()
方法嵌入线上应用,这里便于演示,我采用加载本地静态html的方式来实现
JavaScript
// main.js
const { app, BrowserWindow, ipcMain, screen } = require('electron');
let mainWindow; // 主窗口
let previewWindow; // 预览窗口
function createWindow() {
// 获取主屏幕的尺寸
const primaryDisplay = screen.getPrimaryDisplay()
const { width, height } = primaryDisplay.workAreaSize
mainWindow = new BrowserWindow({
width: width,
height: height,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
// 作为演示,采用本地静态文件
mainWindow.loadFile('index.html')
// 可通过以下方式内嵌网页
//mainWindow.loadURL(`http://www.example.com`)
// 打开开发者工具
mainWindow.webContents.openDevTools();
}
app.whenReady().then(() => {
createWindow();
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
// 退出
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit();
});
2. 拦截系统打印
这里通过重写window.print()
方法,拦截系统自带的打印功能,自己来写一个在electron内用于实现打印预览的页面,需要获取到需要打印的dom节点,同时为了保证样式生效,我们这里同时把当前页面的所有style标签和link样式文件都获取到,一并传给预览页面
html
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>打印预览示例</title>
<style>
.print-button {
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
</head>
<body>
<h1>打印预览示例</h1>
<button class="print-button" onclick="showPreview()">打印预览</button>
<div id="printTable">
打印内容
</div>
<script>
const { ipcRenderer } = require('electron');
// 拦截默认打印行为
const originalPrint = window.print;
window.print = function () {
console.log('渲染进程拦截到打印请求');
try {
// 获取要打印的内容
const printContent = document.getElementById("printTable").innerHTML
// 获取所有<style>标签
const styleEles = Array.from(document.querySelectorAll('style'));
let styleTags = [];
styleEles.forEach((style, index) => {
styleTags.push({
id: style.id || `style-${index}`,
content: style.textContent,
});
});
// 获取所有<link>标签中的样式表
const linkEles = Array.from(document.querySelectorAll('link[rel="stylesheet"]'));
let linkTags = [];
linkEles.forEach((link, index) => {
linkTags.push({
id: link.id || `stylesheet-${index}`,
href: link.href,
});
});
// 调用渲染进程中的预览方法,并把打印内容和样式一起传过去
ipcRenderer.invoke('show-preview', {
printContent: printContent,
styleTags: styleTags,
linkTags: linkTags
});
} catch (error) {
console.error('打印错误:', error);
}
};
function showPreview() {
window.print();
}
</script>
</body>
</html>
3. 加载打印预览界面
获取到要打印的内容后,单独开启一个无边框和菜单栏的窗口,窗口内加载一个用于展示要打印内容的静态文件preview.html
,在这个静态文件里面处理需要展示的内容和打印的逻辑
JavaScript
// main.js
// 创建预览窗口
function createPreviewWindow(printData) {
// 获取主屏幕的尺寸
const primaryDisplay = screen.getPrimaryDisplay()
const { width, height } = primaryDisplay.workAreaSize
previewWindow = new BrowserWindow({
width: parseInt(width * 0.7),
height: parseInt(height * 0.9),
frame: false,
autoHideMenuBar: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
// 加载预览页面
previewWindow.loadFile('preview.html');
// 等待页面加载完成后发送打印数据
previewWindow.webContents.on('did-finish-load', () => {
previewWindow.webContents.send('print-data', printData);
});
previewWindow.webContents.openDevTools()
}
// 处理打印预览请求
ipcMain.handle('show-preview', async (event, printData) => {
try {
// 调用创建预览窗口
createPreviewWindow(printData);
return { success: true };
} catch (error) {
console.error('预览错误:', error);
return { success: false, error: error.message };
}
});
4. 预览页面的实现
预览页面可以自由编写布局和样式,主要逻辑是把需要打印的内容和样式加入到页面中,同时加载本地可用的打印机和其他打印配置
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>打印预览</title>
<style>
body {
margin: 0;
padding: 0;
display: flex;
height: 100vh;
overflow: hidden;
background-color: #f0f0f0;
}
.main-container {
display: flex;
width: 100%;
height: 100%;
}
.print-document {
flex-grow: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
overflow-y: auto;
}
/* 这里不能缺少,用于打印的时候不把侧边栏也打印出来 */
@media print {
.print-sidebar {
display: none;
}
}
</style>
</head>
<body>
<div class="main-container">
<div id="print-container" class="print-document">
</div>
<div class="print-sidebar">
<div class="sidebar-header">
<h2>打印</h2>
</div>
<div class="print-section">
<label>目标打印机</label>
<div class="select-wrapper">
<select id="printer"></select>
</div>
</div>
<div class="print-section">
<label>页面</label>
<div class="select-wrapper">
<select id="pageSize">
<option value="A4">全部</option>
<option value="A3">A3</option>
<option value="B5">B5</option>
<option value="letter">Letter</option>
</select>
</div>
</div>
<!-- 其它你需要的配置项 -->
<div class="sidebar-actions">
<button class="print-button" onclick="print()">打印</button>
<button class="cancel-button" onclick="closePreview()">取消</button>
</div>
</div>
</div>
<script>
const { ipcRenderer } = require('electron');
// 接收打印数据
ipcRenderer.on('print-data', (event, data) => {
if (data.printContent) {
// 把要打印的内容放到页面上
const printEle = document.getElementById('print-container')
printEle.innerHTML = data.printContent
}
const head = document.head || document.getElementsByTagName('head')[0];
// 如果有style标签,把style标签放到页面中
if (data.styleTags && data.styleTags.length > 0) {
data.styleTags.forEach((style, index) => {
const styleElement = document.createElement('style');
styleElement.textContent = style.content;
head.appendChild(styleElement);
});
}
// 如果有link标签,添加link标签
if (data.linkTags && data.linkTags.length > 0) {
data.linkTags.forEach((link, index) => {
const linkElement = document.createElement('link');
linkElement.rel = 'stylesheet';
linkElement.type = 'text/css';
linkElement.href = link.href;
head.appendChild(linkElement);
});
}
// 加载本机可用的打印机列表
loadPrinters();
});
// 加载打印机列表
async function loadPrinters() {
try {
const printers = await ipcRenderer.invoke('get-printers');
const printerSelect = document.getElementById('printer');
printerSelect.innerHTML = '';
printers.forEach(printer => {
const option = document.createElement('option');
option.value = printer.name;
option.textContent = printer.name;
printerSelect.appendChild(option);
});
} catch (error) {
console.error('加载打印机列表失败:', error);
}
}
// 打印功能
async function print() {
try {
const options = {
deviceName: document.getElementById('printer').value,
pageSize: document.getElementById('pageSize').value,
landscape: document.getElementById('orientation').value === 'landscape',
color: document.getElementById('color').value === 'true',
printBackground: true,
marginType: 'printableArea'
};
// 调用主线程的打印功能
await ipcRenderer.invoke('do-print', options);
} catch (error) {
alert('打印错误:' + error.message);
}
}
// 关闭预览窗口
function closePreview() {
ipcRenderer.invoke('close-preview');
}
// 初始加载打印机
document.addEventListener('DOMContentLoaded', () => {
loadPrinters();
});
</script>
</body>
</html>
以下是获取打印机列表和执行打印的逻辑
js
// main.js
// 获取打印机列表
ipcMain.handle('get-printers', async () => {
try {
const printers = await mainWindow.webContents.getPrintersAsync();
return printers;
} catch (error) {
console.error('获取打印机列表失败:', error);
return [];
}
});
// 处理实际打印请求
ipcMain.handle('do-print', async (event, options) => {
try {
const printOptions = {
silent: true, //静默打印
printBackground: true,
deviceName: options.deviceName || '',
copies: options.copies || 1,
pageSize: options.pageSize || 'A4',
margins: {
marginType: options.marginType || 'printableArea'
},
landscape: options.landscape || false,
scale: options.scale || 1.0,
color: options.color || true,
headerFooter: options.headerFooter || false,
pageRanges: options.pageRanges || '',
collate: options.collate || true,
duplex: options.duplex || 'none'
};
await previewWindow.webContents.print(printOptions);
} catch (error) {
console.error('打印错误:', error);
}
});
通过以上方式就实现了不改变原来应用调用window.print()
的方式,实现打印预览和打印的逻辑,具体业务中,可以把重写window.print()
这一段逻辑单独写成一个js文件,全局加载,如果希望保留原有的打印,可以判断一下是否在electron的环境中,如果在electron的环境才进行方法覆盖,这样在就能保证无论是通过浏览器打开或者在electron打开,都可以完成对应的预览功能。