浏览器自带 PDF 预览功能实现(网页端+Electron 客户端)

Posted by Harry on 2019-12-22
Words 1.1k and Reading Time 4 Minutes
Viewed Times

背景

因为业务需要,我们需要开发一个在线预览 PDF 文件的功能,在我们开发的第一版本当中采用的是后端转为 HTML 后传输到前端进行展示,但这种方式存在几点问题:

  1. 不够美观
  2. 用户体验不够友好
  3. 插入 HTML 存在 xss 注入风险
    因此需要利用浏览器自身能力来预览 PDF 文件,使用浏览器自身插件有以下优势:
  4. 用户体验更好,自带工具栏
  5. 美观度更好,百分百还原
  6. 避免代码注入

开发

开发环境:Chrome、Electron
PDF 来源:接口请求

流程如下

  1. 请求 blob 格式数据返回,并且后端 response 体中 Content-Type 需要配置为 application/pdf,否则下一步中生成的 blob 链接访问时会直接触发下载。具体原因请查看 MIME types
  2. 浏览器处理成 blob://..., 客户端则需要处理成 file://... 临时访问路径。
  3. 将生成的 url 添加到 iframe | 新窗口(Chrome)、webview | BrowserWindow(Electron)。

关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onreadystatechange = function onreadystatechange() {
if (xhr.readyState === this.DONE) {
if (xhr.status === 200) {
// 客户端模式:先下载文件到本地暂存为临时文件,再进行预览
// 原因:electron 不支持 blob://... 形式的链接访问 pdf 文件,详见 issue
// https://github.com/electron/electron/issues/13038
// 调用 fs 模块暂时到应用程序临时目录下,再以 file:// 的链接访问
if (window.isClientMode) {
const reader = new FileReader();
reader.onload = function () {
try {
const fs = window.electron.remote.require('fs');
const pdfViewerFilePath = window.process.env.pdfViewerFilePath;
fs.writeFileSync(pdfViewerFilePath, this.result, 'binary');
pdfViewerDom.src = `file://${pdfViewerFilePath}`;
} catch (error) {
console.error('write file error', error);
}
}
reader.readAsBinaryString(this.response);
} else {
// 浏览器模式
// 转为 blob://... 链接,并设置到 iframe 上
pdfViewerDom.src = URL.createObjectURL(this.response);
}
} else {
console.error('XHR failed', this);
}
}
};
// 接受 blob 类型的数据
xhr.responseType = 'blob';
xhr.send();
1
2
3
4
5
6
7
8
9
render() {
return window.isClientMode ? (
// Electron 中使用 webview 协会的 iframe, 并且需要设置 plugins 允许使用浏览器插件,默认禁用
// Electron 中也可以使用 new BrowserWindow({ webPreferences: { plugins: true }) 打开创建的 url
<webview plugins="true" id="js-pdf-viewer" />
) : (
<iframe id="js-pdf-viewer" />
);
}

问题回顾

浏览器打开 blob:// 链接直接下载问题是怎么回事

此问题是由于前期没有注意使用指定的 MIME types,通过原生浏览器预览 PDF 需要指定为 application/pdf 类型。

Electron 不能使用 iframe

Electron 中使用 webview 替换 iframe。

与 iframe 不同, webview 在与应用程序不同的进程中运行。它与您的网页没有相同的权限, 应用程序和嵌入内容之间的所有交互都将是异步的。 这将保证你的应用对于嵌入的内容的安全性。 注意: 从宿主页上调用 webview 的方法大多数都需要对主进程进行同步调用

Electron 默认禁用插件功能,无法调起 PDF 预览界面导致空白页面

  • 在 webview 中使用 plugins 开启。
  • 新窗口使用 new BrowserWindow({ webPreferences: { plugins: true })

    当有此属性时, webview中的访客页将有能力去使用浏览器的插件,Plugins 默认是禁用的。

Electron 不支持加载 blog:// 类型的文件预览 url

官方 bug: https://github.com/electron/electron/issues/13038
由于 Electron 不支持,所以这里使用替换方案:客户端模式下需先下载文件到本地暂存为临时文件,再进行预览。
具体步骤为:

  1. 调用 fs 模块暂时到应用程序临时目录下
  2. 再以 file:// 的链接的形式访问

关键代码

1
2
3
4
5
// set temp.pdf path
const pdfPreviewFilePath =
process.platform === 'darwin'
? `${homePath}/Library/Logs/${appName}/temp.pdf`
: `${homePath}\\AppData\\Roaming\\${appName}\\temp.pdf`;
1
2
3
4
5
6
7
8
9
10
11
12
// save as temp.pdf
const reader = new FileReader();
reader.onload = function() {
try {
const fs = window.electron.remote.require('fs');
fs.writeFileSync('${path}/temp.pdf', this.result, 'binary');
window.electron.ipcRenderer.send('pdf-viewer');
} catch (error) {
console.error('write file error', error);
}
}
reader.readAsBinaryString(blob);
1
2
3
4
5
6
7
8
9
10
// open pdf viewer
ipcMain.on('pdf-viewer', () => {
const pdfViewerConfig = {
webPreferences: {
plugins: true,
},
};
const pdfViewer = new BrowserWindow(pdfViewerConfig);
pdfViewer.loadURL(`file://${pdfPreviewFilePath}`); // the path of temp.pdf
});

Reference

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types
https://developer.mozilla.org/zh-CN/docs/Web/Security/Securing_your_site/Configuring_server_MIME_types
http://www.iana.org/assignments/media-types/index.html
https://electronjs.org/docs/api/webview-tag


This is copyright.