基于Vue的 web端和桌面端文件下载和预览总结。项目使用的vue版本为 2.6.10,vue-cli版本为 3.12.1,node版本为 v14.17.5,electron 版本 12.0.0。
注意:本文方法都是基于后端提供pdf资源的情形!
※注:本文代码区域每行开头的“+”表示新增,“-”表示删除,“M”表示修改;代码中的“...”表示省略。
思路:
一般来说后端会返回二进制的 pdf 文件,这里以原生的ajax请求为例,指定xhr.responseType = 'blob',接收到Blob 对象。将 Blob 对象 通过 window.URL.createObjectURL(blob) 转化为 URL,再将 URL 赋值给 a 标签的 link属性,调用 a.click() 即可下载。
方法和属性解释:
代码示例:
javascript
复制代码
``// url:接口地址,
// query:请求参数, query格式: { testId: xxx, formatId: aaa}
function download(url, query) {
let xhr = new XMLHttpRequest()
xhr.open('GET', ${axios.defaults.baseURL}${url}?${QS.stringify(query)}
, true)
// AJAX 请求时,如果指定responseType属性为blob,下载下来的就是一个 Blob 对象
xhr.responseType = 'blob'
xhr.setRequestHeader('Access-Token', storage.session.get('token'))
xhr.onload = function() {
if (this.status == 200) {
let blob = this.response
//生成URL
let href = window.URL.createObjectURL(blob)
let link = document.createElement('a')
link.download = '波形.pdf'
link.href = href
link.click()
window.URL.revokeObjectURL(href)
}
}
xhr.send(null)
}``
如果后端返回的是 base64 的数据可以通过下面的方法先转化为 URL :
javascript
复制代码
``/**
*/
base64ToURL({ b64data = '', contentType = '', sliceSize = 512 } = {}) {
return new Promise((resolve, reject) => {
// 使用 atob() 方法将数据解码
let byteCharacters = atob(b64data)
let byteArrays = []
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
let slice = byteCharacters.slice(offset, offset + sliceSize)
let byteNumbers = []
for (let i = 0; i < slice.length; i++) {
byteNumbers.push(slice.charCodeAt(i))
}
// 8 位无符号整数值的类型化数组。内容将初始化为 0。
// 如果无法分配请求数目的字节,则将引发异常。
byteArrays.push(new Uint8Array(byteNumbers))
}
let result = new Blob(byteArrays, {
type: contentType,
})
result = Object.assign(result, {
// 这里一定要处理一下 URL.createObjectURL
// 该方法生成的 URL格式: blob:http://localhost/c745ef73-ece9-46da-8f66-ebes574789b1
preview: window.URL.createObjectURL(result),
name: XXX.png
,
})
resolve(result)
})
}``
这里选用 pdf.js 插件来进行预览,
官网地址:mozilla.github.io/pdf.js/gett…
下载 Prebuilt (for older browsers) 对应的稳定版本,解压后有一个build和web文件夹,这里主要用到 web 文件夹。
pdf 插件所处的目录结构
javascript
复制代码
`(略)
|- /public
|- pdf
|- build
|- web
...
|- viewer.html
...
|- /src
(略)`
这里以 Vue 为例来说明,其他框架用法类似。
xxx.vue 组件中用法:
将下载完后的 blob 或 base64 格式的文件 通过 window.URL.createObjectURL() 转化为 URL 赋值 给 下方的 pdfURL 即可。
javascript
复制代码
``...
<iframe
v-if="pdfURL"
width="100%"
height="100%"
scrolling="no"
:src="/pdf/web/viewer.html?file=${encodeURIComponent(pdfUrl)}
"
class="pdf_preview"
</iframe>
...``
此方法点击pdf.js 插件头部的打印图标即可实现打印
原理:将下载后的 Blob
对象通过 window.URL.createObjectURL(blob)
转化为 URL
,将URL 赋值给 iframe.src,然后通过 iframe.contentWindow.print() 实现打印。
完整代码如下:
javascript
复制代码
``// url:接口地址,query:查询参数
function print(url, query) {
let xhr = new XMLHttpRequest()
xhr.open('GET', ${axios.defaults.baseURL}${url}?${QS.stringify(query)}
, true)
xhr.responseType = 'blob'
xhr.setRequestHeader('Access-Token', storage.session.get('token'))
xhr.onload = function() {
if (this.status == 200) {
// let blob = new Blob([this.response], {
// type: 'application/pdf;charset=utf-8',
// })
let blob = this.response
//生成URL
let href = window.URL.createObjectURL(blob)
// console.log(href)
Vue.nextTick(() => {
let EleIframe = document.querySelector('.iframe_print')
if (EleIframe) {
EleIframe.remove()
}
let iframe = document.createElement('iframe')
iframe.src = href
iframe.classList.add('iframe_print')
iframe.style.display = 'none'
document.body.append(iframe)
// iframe.contentWindow.focus()
iframe.contentWindow.print()
window.URL.revokeObjectURL(href)
})
}
}
xhr.send()
}``
在 web 端实现静默打印需要借助 控件,原理就是点击打印时 用 http 或 websocket 和打印控件进行通信来实现静默打印。此方法我暂未尝试过,有兴趣的小伙伴可以查询资料后尝试一下。
思路:
一般来说后端会返回二进制的 pdf 文件,这里以原生的ajax请求为例,指定xhr.responseType = 'blob',接收到Blob 对象。然后通过 FileReader 将blob对象转化为 base64,再通过 Buffer.from(data, 'base64') 将base64 转化为Buffer,最后通过 dialog.showSaveDialog 和 fs.writeFile 下载到指定位置即可。
方法和属性解释:
代码示例:
javascript
复制代码
``const { remote } = require('electron')
const dialog = remote.dialog
const fs = require('fs')
// 将浏览器中的 blob 转化为 nodejs 中的 buffer 对象
blobToBuffer(blob) {
return new Promise((resolve, reject) => {
const fileReader = new FileReader()
fileReader.onload = () => {
resolve(Buffer.from(fileReader.result))
}
fileReader.onerror = reject
fileReader.readAsArrayBuffer(blob)
})
}
...
// url:接口地址,
// query:请求参数, query格式: { testId: xxx, formatId: aaa}
function download(url, query) {
var xhr = new XMLHttpRequest()
xhr.open('GET', ${axios.defaults.baseURL}${url}?${QS.stringify(query)}
, true)
// AJAX 请求时,如果指定responseType属性为blob,下载下来的就是一个 Blob 对象
xhr.responseType = 'blob'
xhr.onload = async function () {
if (this.status == 200) {
let buffer
try {
buffer = await blobToBuffer(this.response)
} catch (error) {
console.log(error)
}
dialog
.showSaveDialog({
properties: ['openFile', 'openDirectory'],
title: '保存文件',
defaultPath: ${+new Date()}.pdf
,
filters: [{ name: '.pdf', extensions: ['pdf'] }],
})
.then((res) => {
if (res.filePath) {
// 将文件写入到手动选择的路径下
fs.writeFile(res.filePath, buffer, 'binary', (err) => {
if (err) {
// 向渲染进程发送消息通知失败
e.sender.send('reply', err)
} else {
// 向渲染进程发送消息通知成功
e.sender.send('reply', res.filePath)
}
})
}
})
}
}
xhr.send(null)
}
...``
这里选用 pdf.js 插件来进行预览,
官网地址:mozilla.github.io/pdf.js/gett…
下载 Prebuilt (for older browsers) 对应的稳定版本,解压后有一个build和web文件夹,这里主要用到 web 文件夹。
pdf 插件所处的目录结构
javascript
复制代码
`(略)
|- /public
|- pdf
|- build
|- web
...
|- viewer.html
...
|- /src
(略)`
首先将 pdf 文件下载到指定的目录下, 以写入到 /public/data
目录下为例
writeFile.js 文件:
javascript
复制代码
``const { remote } = require('electron')
const app = remote.app
const fs = require('fs')
const path = require('path')
...
// 将浏览器中的 blob 转化为 nodejs 中的 buffer 对象
blobToBuffer(blob) {
return new Promise((resolve, reject) => {
const fileReader = new FileReader()
fileReader.onload = () => {
resolve(Buffer.from(fileReader.result))
}
fileReader.onerror = reject
fileReader.readAsArrayBuffer(blob)
})
}
// 预览pdf时调用
// url:接口地址,
// query:请求参数, query格式: { testId: xxx, formatId: aaa}
function downloadPreview(url, query) {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest()
xhr.open('GET', ${axios.defaults.baseURL}${url}?${QS.stringify(query)}
, true)
xhr.responseType = 'blob'
xhr.setRequestHeader('Access-Token', sessionStorage.getItem('token'))
xhr.onload = async function () {
if (this.status == 200) {
let buffer
try {
buffer = await blobToBuffer(this.response)
} catch (error) {
console.log(error)
}
let filePath
// 开发环境
if (process.env.NODE_ENV === 'development') {
filePath = path.join(__static, 'data/preview.pdf')
// 生产环境
} else {
filePath = path.join(app.getAppPath(), 'data/preview.pdf')
}
fs.stat(filePath, (err, stats) => {
if (!err) {
// console.log(stats);
// console.log(stats.isFile());
// 文件存在时先删除再写入
if (stats.isFile()) {
let res = fs.unlinkSync(filePath)
if (!res) {
fs.writeFile(filePath, buffer, 'binary', (err) => {
if (err) {
// console.log(err)
reject(err)
} else {
// console.log('保存成功')
resolve('../../data/preview.pdf')
}
})
}
}
} else {
// 文件不存在时直接写入
fs.writeFile(filePath, buffer, 'binary', (err) => {
if (err) {
// console.log(err)
reject(err)
} else {
// console.log('保存成功')
resolve('../../data/preview.pdf')
}
})
}
})
}
}
xhr.send(null)
})
}
...
export {downloadPreview}``
__static(nsis 中 asar 的配置为false)
开发环境下的值:C:\Users\DELL\Desktop\项目名\public
生产环境下的值(electron 11):C:\Program Files (x86)\项目名\resources
生产环境下的值(electron 12):C:\Program Files (x86)\项目名\resources\app.asar
app.getAppPath()
开发环境下的值:C:\Users\DELL\Desktop\项目名\dist_electron
生产环境下的值:C:\Program Files (x86)\项目名\resources\app
因为 __static
在生产环境下有坑,所以在生产环境中获取路径建议还是用 app.getAppPath()
,
请参考:
javascript
复制代码
`let filePath
// 开发环境
if (process.env.NODE_ENV === 'development') {
filePath = path.join(__static, 'data/preview.pdf')
// 生产环境
} else {
filePath = path.join(app.getAppPath(), 'data/preview.pdf')
}`
这里以 Vue 为例来说明,其他框架用法类似。
xxx.vue 组件中用法:
javascript
复制代码
``...
<iframe
v-if="pdfUrl"
width="100%"
height="100%"
scrolling="no"
:src="/pdf/web/viewer.html?file=${encodeURIComponent(pdfUrl)}
"
class="pdf_preview"
</iframe>
...
import { downloadPreview } from '@/xxx/writeFile.js'
...
// 将pdf 文件对应的路径赋值给 pdfUrl后,就能预览下载完成后的 pdf 文件了
downloadPreview('xxx/xxx', {id: xxx}).then(res => {this.pdfUrl = res})
...``
此方法点击pdf.js 插件头部的打印图标即可实现打印
参照 1.3.2节 方法
原理:将下载后的 Blob
对象转化为 nodejs 中的 buffer,再通过 fs.writeFile 写入到指定的 filePath,然后 通过nodejs 的 childProcess 模块用命令方式执行SumatraPDF 插件来打印,打印完成通过 fs.unlink 删除下载的pdf临时文件。
Vue 组件中的 js 代码如下:
this.$api.exportReport 为一个http请求,blob 为返回的 pdf 对应的 blob对象,ipc.send('printPDF', filePath) 为渲染进程和主进程通信的方法。
javascript
复制代码
``const fs = require('fs')
const path = require('path')
import { Buffer } from 'buffer'
...
// 将浏览器中的 blob 转化为 nodejs 中的 buffer 对象
blobToBuffer(blob) {
return new Promise((resolve, reject) => {
const fileReader = new FileReader()
fileReader.onload = () => {
resolve(Buffer.from(fileReader.result))
}
fileReader.onerror = reject
fileReader.readAsArrayBuffer(blob)
})
}
async printPdf() {
// 首先请求到 pdf 的blob资源, getPdfBlob 方法我这里为写出来,根据业务去请求即可。
const blob = await getPdfBlob()
// blob 转化为 buffer
const buffer = await blobToBuffer(blob)
// 写入目录
const dirPath = this.$tools.getDownloadsPath()
// 写入文件名
const fileName = print.pdf
// 写入路径
const filePath = ${dirPath}\\${fileName}
// 写入成功后 通知主进程打印
this.$tools
.downloadPdfToPrint(buffer, dirPath, filePath)
.then((res) => {
ipcRenderer.send('printPDF', res.filePath)
})
}
// 打印 pdf 文件
printPdf()
...``
主进程中的 printPDF 事件如下(这里是采用默认打印机打印,关于打印机的更多需求请读者自行研究,我自己也是浅尝辄止):
javascript
复制代码
``const fs = require('fs')
const path = require('path')
const childProcess = require('child_process')
import process from 'process'
const { ipcMain, app, BrowserWindow} = require('electron')
...
ipcMain.on('printPDF', (event, filePath) => {
switch (process.platform) {
case 'win32':
// child_process.exec(command, options)
// command <string>: 要运行的命令,参数以空格分隔。
// options.cwd <string> | <URL> 子进程的当前工作目录。 默认值: process.cwd()。
// options.windowsHide <boolean> 隐藏通常在 Windows 系统上创建的子进程控制台窗口。 默认值: false。
// saveLog(当前打印pdf文件的路径: '${filePath}\n
)
childProcess.exec(
SumatraPDF.exe -print-to-default -print-settings "paper=A4" "${filePath}"
,
{
windowsHide: true,
cwd: path.join(__static, 'SumatraPDF'),
},
(e) => {
if (e) {
event.sender.send(
'main-to-renderer',
'打印失败',
path.join(__static, 'SumatraPDF')
)
throw e
}
event.sender.send(
'main-to-renderer',
'打印成功',
path.join(__static, 'SumatraPDF')
)
/ 打印完成后删除创建的临时文件 /
fs.unlink(filePath, (e) => {})
}
)
break
case 'darwin':
case 'linux':
childProcess.exec(
SumatraPDF.exe -print-to-default "${filePath}"
,
{
windowsHide: true,
cwd: path.join(__static, 'SumatraPDF'),
},
(e) => {
if (e) {
throw e
}
/ 打印完成后删除创建的临时文件 /
fs.unlink(filePath, (e) => {})
}
)
break
default:
throw new Error('Platform not supported.')
}
})``
this.$tools 工具函数如下:
js
复制代码
`const fs = require('fs')
const path = require('path')
const { app } = require('electron').remote
/**
*/
downloadPdfToPrint(buffer, dirPath, filePath) {
const writeFile = (filePath, buffer, resolve, reject) => {
fs.writeFile(filePath, buffer, 'binary', (err) => {
if (err) {
// console.log(err)
reject(err)
} else {
// console.log('保存成功')
resolve({ filePath })
}
})
}
return new Promise(async (resolve, reject) => {
fs.stat(dirPath, (err, stats) => {
if (!err) {
// 走到这里, 表示目录存在
if (fs.existsSync(filePath)) {
// console.log('electrophysiology.pdf 存在');
fs.unlinkSync(filePath)
// 先删除pdf再写入
writeFile(filePath, buffer, resolve, reject)
} else {
// console.log('electrophysiology.pdf 不存在');
// 直接写入
writeFile(filePath, buffer, resolve, reject)
}
} else {
// 只要目录不存在,就会走到这里, 先创建目录, 再写pdf
fs.mkdir(dirPath, { recursive: true }, (err) => {
if (err) {
throw err
} else {
writeFile(filePath, buffer, resolve, reject)
}
})
}
})
})
},
getDownloadsPath() {
let downloadsPath
// 开发环境
if (process.env.NODE_ENV === 'development') {
downloadsPath = path.join(__static, 'downloads')
// 生产环境
} else {
downloadsPath = path.join(app.getAppPath(), 'downloads')
}
return downloadsPath
}`
最近在项目中碰到个很奇葩的问题,我项目中打印都是采用A4纸大小,但是有某些后端获取的pdf文件的尺寸小于A4纸大小时(我项目中pdf文档属性中页面大小为:279.4 × 215.9 毫米,此属性在浏览器中可查看到。A4纸大小为297mm × 210mm),静默打印会失败,打印机闪红灯,按一下打印机的ok键就能打印出来了。且非静默打印都是可以直接打印成功的。经多种尝试后,终于定位到问题,在命令行中加入打印设置纸张设为A4大小即可。一开始看文档时找开发者文档去了,没想到在用户文档部分,浪费了很多时间。。。
SumatraPDF 命令行部分文档地址:www.sumatrapdfreader.org/docs/Comman…
解决方法:将 SumatraPDF.exe -print-to-default "${filePath}"
改为 SumatraPDF.exe -print-to-default -print-settings "paper=A4" "${filePath}"
即可正常打印了
由于本人水平有限,文中如有错误,欢迎在评论区指正。若文章对您有些许帮助,欢迎点赞和关注!
原网址: 访问
创建于: 2023-08-18 15:04:02
目录: default
标签: 无
未标明原创文章均为采集,版权归作者所有,转载无需和我联系,请注明原出处,南摩阿彌陀佛,知识,不只知道,要得到
最新评论