1024剛過,也祝願各位碼友在今後生活中,身體健康,事事順心,再無Bug。
一、前言
之前寫過一篇文章關於上傳目錄文件:uni-app系統目錄文件上傳(非只圖片和視頻)解決方案,這次來解決文件預覽問題。
uni-app 是一個使用 Vue.js 開發所有前端應用的框架,開發者編寫一套代碼,可發佈到iOS、Android、H5、以及各種小程序(微信/支付寶/百度/頭條/QQ/釘釘)等多個平臺。在做業務系統時,不可避免會遇到文件在線預覽的需求。這裏的文件包括PDF、Word、Excel、PPT、圖片等。而在線預覽不是下載後再用本地軟件或瀏覽器打開預覽,而是直接通過文件流的形式查看。本方案主要解決在線預覽問題,以及在uni-app開發過程中遇到一系列問題。
如果有欠缺的地方,或者有更好的方案,還望各位碼友多提意見,多多交流,文章最後可以加我。
文件預覽,首先會想到pdf預覽,前端做pdf預覽,首先也會想到pdf.js,那我們就從pdf.js說起。
二、PDF預覽
pdf.js開源地址和在線例子
Github
Online Demo
2.1 使用方法一
- 下載插件包,下載地址
<img src="https://user-gold-cdn.xitu.io...;h=288&f=png&s=32729" width="240" align=center />
- 解壓,拷貝build和web目錄到項目hybrid->html目錄下,參考uni-app中web-view用法
<img src="https://user-gold-cdn.xitu.io...;h=262&f=png&s=17092" width="240" align=center />
-
新建vue組件file-preview.vue
- viewerUrl:前端本地viewer.html頁面地址
- fileUrl:文件流訪問地址,參考《三、文件流服務》
<template>
<view>
<web-view :src="allUrl"></web-view>
</view>
</template>
<script>
import globalConfig from '@/config'
export default {
data() {
return {
viewerUrl: '/hybrid/html/web/viewer.html',
// viewerUrl: globalConfig.baseUrl + '/pdf/web/viewer.html',
allUrl: ''
}
},
onLoad(options) {
let fileUrl = encodeURIComponent(
globalConfig.baseUrl + '/api/attachment?name=' + options.name + '&url=' + options.url)
this.allUrl = this.viewerUrl + '?file=' + fileUrl
}
}
</script>
-
效果
- h5端
顯示正常
- Android端
顯示模糊,並且中文顯示不全,其中模糊問題是模擬器原因;但是中文顯示問題是真,調試出現兩個警告。第二個警告pdf.js默認不顯示電子簽章(數字簽名)問題,查了很多資料也沒解決,各位碼友有遇到過並且解決了嗎?
<img src="https://user-gold-cdn.xitu.io...;h=812&f=png&s=253098" width="360" align=center />
- iOS端
出現跨域問題,並且調試出現無法訪問pdf.js國際化文件
<img src="https://user-gold-cdn.xitu.io...;h=452&f=png&s=95345" width="360" align=center />
- 解決
基於Android和iOS預覽出現的各種問題,最根本原因是viewer.html文件放到前端導致加載資源文件丟失問題。針對這個問題,我就在想能不能直接放在spring後端作爲靜態資源訪問文件呢?於是有了下面的方法。
2.2 使用方法二
- 在基於spring mvc的後端代碼中,將插件包的build和web文件夾放到webapp下面(新建pdf文件夾),spring boot架構的後端項目同理,放到靜態資源目錄
<img src="https://user-gold-cdn.xitu.io...;h=650&f=png&s=69156" width="400" align=center />
- 在xml文件中配置靜態文件訪問
- 修改前端組件file-preview.vue中的viewerUrl,其中globalConfig.baseUrl爲代理後端地址的baseUrl。如Vue中proxyTable或nginx代理
viewerUrl: globalConfig.baseUrl + '/pdf/web/viewer.html'
-
修改後效果
- iOS端
<img src="https://user-gold-cdn.xitu.io...;h=698&f=png&s=184270" width="360" align=center />
- Android端
模糊是模擬器原因,在真機上測試通過
<img src="https://user-gold-cdn.xitu.io...;h=634&f=png&s=140599" width="360" align=center />
三、文件流服務
3.1 方法一:tomcat配置
配置tomcat的config目錄下的server.xml,在最後的<server></server>中間添加如下:
port=8090 文件訪問服務端口
docBase="/root/" 文件存儲目錄
服務器上文件會存儲到/root/fileData/目錄下
文件訪問地址爲:http://ip地址:8090/fileData/...
<Service name="fileData">
<!--分配8089端口 -->
<!-- <Connector port="8090" protocol="HTTP/1.1" connectionTimeout="20000" URIEncoding="GBK" redirectPort="8443" /> -->
<Connector port="8090" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
<Engine name="fileData" defaultHost="localhost">
<!--name爲項目訪問地址 此配置的訪問爲http://localhost:8080 appBase配置tomcat下wabapps下的路徑-->
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true" xmlValidation="false" xmlNamespaceAware="false">
<!--資源地址-->
<Context path="" docBase="/root/" debug="0" reloadable="false"/>
</Host>
</Engine>
</Service>
3.2 方法二:寫代碼獲取服務器文件進行轉換
直接上代碼
讀取目錄文件,轉換爲二進制流
前端組件file-preview.vue中fileUrl爲 /api/attachment
核心代碼
ios = new FileInputStream(sourceFile);
os = response.getOutputStream();
int read = 0;
byte[] buffer = new byte[1024 * 1024];
while ((read = ios.read(buffer)) != -1) {
os.write(buffer, 0, read);
}
os.flush();
完整代碼
@RequestMapping(value = "/api/attachment", method = RequestMethod.GET)
public void getFileBytes(@RequestParam("name") String name, @RequestParam("url") String url, HttpServletRequest request, HttpServletResponse response) {
response.reset();
response.setContentType("application/octet-stream");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-Disposition", "attachment;filename=" + name);
AttachmentVO attachmentVO = new AttachmentVO();
FileInputStream ios = null;
OutputStream os = null;
try {
name = CharsetUtils.toUTF_8(name);
url = CharsetUtils.toUTF_8(url);
attachmentVO.setUrl(url);
attachmentVO.setName(name);
File sourceFile = getDictionaryFile(attachmentVO, request);
if (null == sourceFile) {
// throw new HttpResponseException(300, "附件不存在!");
return;
}
/**
* 判斷文件類型
*/
/* 獲得文件名後綴 */
String ext = "";
if (!"".equals(url) && url.contains(".")) {
ext = url.substring(url.lastIndexOf(".") + 1, url.length()).toUpperCase();
}
/* 根據文件類型不同進行預覽 */
/* 預覽pdf */
if ("PDF".equals(ext)) {
response.setContentType("application/pdf");
}
/**
* 將文件寫入輸出流,顯示在界面上,實現預覽效果
*/
ios = new FileInputStream(sourceFile);
os = response.getOutputStream();
int read = 0;
byte[] buffer = new byte[1024 * 1024];
while ((read = ios.read(buffer)) != -1) {
os.write(buffer, 0, read);
}
os.flush();
} catch (Exception e) {
e.printStackTrace();
try {
if (null != ios) {
ios.close();
}
if (null != os) {
os.close();
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
四、office文件(Word、Excel、PPT)預覽
原理:
搭建OpenOffice服務,將文件轉換爲pdf,在使用pdf.js預覽
4.1 搭建openOffice服務
- 下載Apache_OpenOffice
- 解壓
tar xzvfm Apache_OpenOffice_xxx.tar.gz
cd zh-CN/RPMS
rpm -ivh *rpm
- 運行
# 127.0.0.1只能本機使用該服務
/opt/openoffice4/program/soffice "-accept=socket,host=127.0.0.1,port=8100;urp;" -headless -nofirststartwizard &
# 0.0.0.0遠程ip能使用
/opt/openoffice4/program/soffice "-accept=socket,host=0.0.0.0,port=8100;urp;" -headless -nofirststartwizard &
4.2 集成java
- 在pom.xml添加jar包
<!-- openoffice start -->
<dependency>
<groupId>org.openoffice</groupId>
<artifactId>juh</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.openoffice</groupId>
<artifactId>jurt</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.openoffice</groupId>
<artifactId>ridl</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.openoffice</groupId>
<artifactId>unoil</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>com.artofsolving</groupId>
<artifactId>jodconverter</artifactId>
<version>2.2.2</version>
</dependency>
<!-- openoffice end -->
<span style="color:red">注意</span>:jodconverter需要單獨下載2.2.2版本,之前的版本都不行,而且maven中央倉庫沒有2.2.2版本。然後再單獨導入。下載地址:https://sourceforge.net/proje...
單獨導入
mvn install:install-file -Dfile="jodconverter-2.2.2.jar" -DgroupId=com.artofsolving -DartifactId=jodconverter -Dversion=2.2.2 -Dpackaging=jar
- 轉換代碼
核心代碼
connection = new SocketOpenOfficeConnection(openofficeHost, openofficePort);
connection.connect();
DocumentConverter converter = new StreamOpenOfficeDocumentConverter(connection);
converter.convert(sourceFile, pdfFile);
完整代碼
/* 利用openOffice將office文件轉換爲pdf格式, 然後預覽doc, docx, xls, xlsx, ppt, pptx */
if ("DOC".equals(ext) || "DOCX".equals(ext) || "XLS".equals(ext) || "XLSX".equals(ext) || "PPT".equals(ext) || "PPTX".equals(ext)) {
/* filePath在數據庫中是不帶文件後綴的, 由於jodConverter必須要識別後綴,所以將服務器中的文件重命名爲帶後綴的文件 */
// File docFileWithExt = new File(filePath + "." + ext.toLowerCase()); //帶後綴的文件
// docFile.renameTo(docFileWithExt);
/* 轉換之後的文件名 */
String filePath = sourceFile.getPath();
File pdfFile;
if (filePath.contains(".")) {
pdfFile = new File(filePath.substring(0, filePath.lastIndexOf(".")) + ".pdf");
} else {
pdfFile = new File(filePath + ".pdf");
}
/* 判斷即將要轉換的文件是否真實存在 */
if (sourceFile.exists()) {
/* 判斷該文件是否已經被轉換過,若已經轉換則直接預覽 */
if (!pdfFile.exists()) {
OpenOfficeConnection connection;
/* 打開OpenOffice連接 */
try {
connection = new SocketOpenOfficeConnection(openofficeHost, openofficePort);
connection.connect();
} catch (java.net.ConnectException e) {
log.warn("openOffice未連接,正在重新連接...");
// 啓動OpenOffice的服務
String command = openofficeInstallPath + "program/soffice -headless -accept=\"socket,host=127.0.0.1,port=8100;urp;\" -nofirststartwizard";
Runtime.getRuntime().exec(command);
Thread.sleep(1000);
connection = new SocketOpenOfficeConnection(8100);
connection.connect();
log.warn("openOffice重新連接成功!!!");
}
try {
// DocumentConverter converter = new OpenOfficeDocumentConverter(connection);
DocumentConverter converter = new StreamOpenOfficeDocumentConverter(connection);
converter.convert(sourceFile, pdfFile);
connection.disconnect();
// filePath = pdfFile.getPath(); // 文件轉換之後的路徑
sourceFile = pdfFile;
response.setContentType("application/pdf");
} catch (OpenOfficeException e) {
e.printStackTrace(); // 讀取轉換文件失敗
log.info("讀取轉換文件失敗!!!");
return;
} finally { // 發生exception時, connection不會自動切斷, 程序會一直掛着
try {
if (connection != null) {
connection.disconnect();
}
} catch (Exception e) {
e.printStackTrace();
}
}
} else {
// filePath = pdfFile.getPath(); // 文件已經轉換過
sourceFile = pdfFile;
response.setContentType("application/pdf");
}
} else {
log.info("需要預覽的文檔在服務器中不存在!!!");
// 文件不存在,直接返回
return;
}
}
五、圖片預覽
5.1 後端文件流
/* 預覽圖片 */
if ("PNG".equals(ext) || "JPEG".equals(ext) || "JPG".equals(ext)) {
response.setContentType("image/jpeg");
}
/* 預覽BMP格式的文件 */
if ("BMP".equals(ext)) {
response.setContentType("image/bmp");
}
/* 預覽GIF格式的文件 */
if ("GIF".equals(ext)) {
response.setContentType("image/gif");
}
5.2 前端預覽
採用uni-app的uni.previewImage接口
fileUrl:爲文件流訪問地址
// 預覽圖片
uni.previewImage({
urls: [fileUrl],
longPressActions: {
itemList: ['發送給朋友', '保存圖片', '收藏'],
success: function(data) {
console.log('選中了第' + (data.tapIndex + 1) + '個按鈕,第' + (data.index + 1) + '張圖片');
},
fail: function(err) {
console.log(err.errMsg);
}
}
})
附:完整文件流代碼
@RequestMapping(value = "/api/attachment", method = RequestMethod.GET)
public void getFileBytes(@RequestParam("name") String name, @RequestParam("url") String url, HttpServletRequest request, HttpServletResponse response) {
response.reset();
// 解決IFrame拒絕的問題,無效
// response.setHeader("X-Frame-Options", "SAMEORIGIN");
response.setContentType("application/octet-stream");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-Disposition", "attachment;filename=" + name);
AttachmentVO attachmentVO = new AttachmentVO();
FileInputStream ios = null;
OutputStream os = null;
try {
name = CharsetUtils.toUTF_8(name);
url = CharsetUtils.toUTF_8(url);
attachmentVO.setUrl(url);
attachmentVO.setName(name);
File sourceFile = getDictionaryFile(attachmentVO, request);
if (null == sourceFile) {
// throw new HttpResponseException(300, "附件不存在!");
return;
}
/**
* 判斷文件類型
*/
/* 獲得文件名後綴 */
String ext = "";
if (!"".equals(url) && url.contains(".")) {
ext = url.substring(url.lastIndexOf(".") + 1, url.length()).toUpperCase();
}
/* 根據文件類型不同進行預覽 */
/* 預覽圖片 */
if ("PNG".equals(ext) || "JPEG".equals(ext) || "JPG".equals(ext)) {
response.setContentType("image/jpeg");
}
/* 預覽BMP格式的文件 */
if ("BMP".equals(ext)) {
response.setContentType("image/bmp");
}
/* 預覽GIF格式的文件 */
if ("GIF".equals(ext)) {
response.setContentType("image/gif");
}
/* 預覽pdf */
if ("PDF".equals(ext)) {
response.setContentType("application/pdf");
}
/* 利用openOffice將office文件轉換爲pdf格式, 然後預覽doc, docx, xls, xlsx, ppt, pptx */
if ("DOC".equals(ext) || "DOCX".equals(ext) || "XLS".equals(ext) || "XLSX".equals(ext) || "PPT".equals(ext) || "PPTX".equals(ext)) {
/* filePath在數據庫中是不帶文件後綴的, 由於jodConverter必須要識別後綴,所以將服務器中的文件重命名爲帶後綴的文件 */
// File docFileWithExt = new File(filePath + "." + ext.toLowerCase()); //帶後綴的文件
// docFile.renameTo(docFileWithExt);
/* 轉換之後的文件名 */
String filePath = sourceFile.getPath();
File pdfFile;
if (filePath.contains(".")) {
pdfFile = new File(filePath.substring(0, filePath.lastIndexOf(".")) + ".pdf");
} else {
pdfFile = new File(filePath + ".pdf");
}
/* 判斷即將要轉換的文件是否真實存在 */
if (sourceFile.exists()) {
/* 判斷該文件是否已經被轉換過,若已經轉換則直接預覽 */
if (!pdfFile.exists()) {
OpenOfficeConnection connection;
/* 打開OpenOffice連接 */
try {
connection = new SocketOpenOfficeConnection(openofficeHost, openofficePort);
connection.connect();
} catch (java.net.ConnectException e) {
log.warn("openOffice未連接,正在重新連接...");
// 啓動OpenOffice的服務
String command = openofficeInstallPath + "program/soffice -headless -accept=\"socket,host=127.0.0.1,port=8100;urp;\" -nofirststartwizard";
Runtime.getRuntime().exec(command);
Thread.sleep(1000);
connection = new SocketOpenOfficeConnection(8100);
connection.connect();
log.warn("openOffice重新連接成功!!!");
}
try {
// DocumentConverter converter = new OpenOfficeDocumentConverter(connection);
DocumentConverter converter = new StreamOpenOfficeDocumentConverter(connection);
converter.convert(sourceFile, pdfFile);
connection.disconnect();
// filePath = pdfFile.getPath(); // 文件轉換之後的路徑
sourceFile = pdfFile;
response.setContentType("application/pdf");
} catch (OpenOfficeException e) {
e.printStackTrace(); // 讀取轉換文件失敗
log.info("讀取轉換文件失敗!!!");
return;
} finally { // 發生exception時, connection不會自動切斷, 程序會一直掛着
try {
if (connection != null) {
connection.disconnect();
}
} catch (Exception e) {
e.printStackTrace();
}
}
} else {
// filePath = pdfFile.getPath(); // 文件已經轉換過
sourceFile = pdfFile;
response.setContentType("application/pdf");
}
} else {
log.info("需要預覽的文檔在服務器中不存在!!!");
// 文件不存在,直接返回
return;
}
}
/**
* 將文件寫入輸出流,顯示在界面上,實現預覽效果
*/
ios = new FileInputStream(sourceFile);
os = response.getOutputStream();
int read = 0;
byte[] buffer = new byte[1024 * 1024];
while ((read = ios.read(buffer)) != -1) {
os.write(buffer, 0, read);
}
os.flush();
} catch (Exception e) {
e.printStackTrace();
try {
if (null != ios) {
ios.close();
}
if (null != os) {
os.close();
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
<center>贊助作者</center>
<center><img src="https://user-gold-cdn.xitu.io...;h=1080&f=png&s=342366" width="240" align=center /><center>
<center><img src="https://user-gold-cdn.xitu.io...;h=299&f=gif&s=20783" width="240" align=center /><center>