uni-app 安卓端纯前端预览 DOCX 的实现思路
在 uni-app 项目中,docx 文件预览看起来像是一个简单需求,但在安卓 App 环境下会遇到不少坑。
本文结合当前项目的实现,说明为什么不能直接在页面中渲染 docx,以及如何通过 docx-preview + 本地 web-view 实现纯前端预览。
一、DOCX 为什么可以纯前端预览
docx 本质上不是传统二进制文件,而是一个压缩包。
它内部主要包含:
- XML 文档内容
- 样式文件
- 图片资源
- 字体、编号、页眉页脚等配置
所以前端可以通过 JS 解压并解析这些 XML,再渲染成 HTML。
常见纯前端库有:
docx-previewmammoth.js
其中:
docx-preview更偏向"还原 Word 页面效果"mammoth.js更偏向"提取正文内容"
如果目标是"预览文档",更推荐使用 docx-preview。
二、为什么 DOC 不能一样处理
doc 是老版本 Word 二进制格式,不是 zip + XML 结构。
浏览器端没有成熟稳定的纯 JS 方案可以完整解析 .doc 文件。
所以:
text
docx -> 可以尝试纯前端预览
doc -> 不建议纯前端预览
对于 .doc,更推荐:
- 后端转 PDF
- 调用系统应用打开
- 使用文档服务或原生插件
三、为什么不能直接在 uni-app 页面里渲染
一开始最容易想到的写法是:
js
import { renderAsync } from 'docx-preview';
const renderDocx = async () => {
const bodyEl = document.getElementById('docxBodyBox');
await renderAsync(buffer, bodyEl);
};
在 H5 里这通常没问题。
但在 uni-app 安卓 App 端,普通页面里的 <script setup> 并不是稳定的浏览器 DOM 执行环境。
常见问题包括:
document不存在document.getElementById()找不到容器- DOM 已渲染但原生层还没准备好
- 文件读取能力和浏览器 H5 不一致
因此,直接在 filePreview.vue 里使用 docx-preview,在安卓 App 端并不可靠。
四、最终方案:本地 HTML + web-view
更稳的方案是:
text
uni-app 页面
-> web-view 打开本地 viewer.html
-> viewer.html 加载 docx-preview
-> viewer.html 下载或读取 docx 文件
-> docx-preview 渲染 HTML
也就是说,把 docx-preview 放到真正的 WebView 页面里执行。
项目结构示例:
text
static/docx-preview/
├── viewer.html
├── docx-preview.min.js
└── jszip.min.js
其中:
viewer.html是本地预览页面docx-preview.min.js是预览库jszip.min.js是 docx 解压依赖
五、App 页面如何打开 DOCX 预览
在 filePreview.vue 中,docx 文件走单独分支:
html
<view v-else-if="isDocxFile" class="docx-preview-container">
<!-- #ifdef APP-PLUS -->
<view v-if="docxRendering" class="preview-loading">
<view class="loading-spinner"></view>
<text class="loading-text">文档下载中...</text>
</view>
<view v-else-if="docxRenderError" class="preview-error">
<text class="error-text">{{ docxRenderError }}</text>
<view class="retry-btn" @click="prepareAppDocxPreview">重新加载</view>
<view class="retry-btn secondary" @click="previewWithSystemViewer">
系统应用打开
</view>
</view>
<view v-else class="app-native-preview">
<web-view
:src="getDocxViewerUrl"
class="docx-webview"
:fullscreen="false"
style="width: 100%; height: 100%; border: 0"
:webview-styles="{
width: '100%',
height: '100%',
progress: { color: '#16a5fb' }
}"
></web-view>
</view>
<!-- #endif -->
</view>
预览地址:
js
const getDocxViewerUrl = computed(() => {
const previewUrl = docxLocalPath.value || fileInfo.value.url;
if (!previewUrl) return '';
return `/static/docx-preview/viewer.html?file=${encodeURIComponent(previewUrl)}`;
});
六、为什么要先用 uni.downloadFile 下载
一开始可以让 viewer.html 自己下载远程 docx:
js
fetch(fileUrl)
或:
js
plus.downloader.createDownload(fileUrl)
但在真实项目中经常会失败:
- 文件地址需要登录态
- 下载接口需要
token - 本地 HTML 不一定能拿到业务请求头
- 远程文件可能有跨域限制
plus.downloader在 web-view 内部回调不稳定
所以更稳的方式是:
text
filePreview.vue 用 uni.downloadFile 下载
-> 带上 token
-> 拿到 tempFilePath
-> 传给 viewer.html
代码示例:
js
const prepareAppDocxPreview = () => {
docxLocalPath.value = '';
docxRenderError.value = '';
docxRendering.value = true;
uni.downloadFile({
url: fileInfo.value.url,
header: {
token: uni.getStorageSync('token') || ''
},
success: (res) => {
docxRendering.value = false;
if (res.statusCode !== 200 || !res.tempFilePath) {
docxRenderError.value = `文档下载失败: ${res.statusCode || '未知错误'}`;
return;
}
try {
docxLocalPath.value =
plus.io.convertLocalFileSystemURL(res.tempFilePath) ||
res.tempFilePath;
} catch (error) {
docxLocalPath.value = res.tempFilePath;
}
},
fail: () => {
docxRendering.value = false;
docxRenderError.value = '文档下载失败,请检查文件地址或权限';
}
});
};
七、viewer.html 如何渲染 DOCX
viewer.html 需要先加载依赖:
html
<script src="./jszip.min.js"></script>
<script src="./docx-preview.min.js"></script>
然后读取文件并渲染:
js
await window.docx.renderAsync(
buffer,
document.getElementById('docxBodyBox'),
document.getElementById('docxStyleBox'),
{
className: 'docx-rendered',
inWrapper: true,
ignoreWidth: false,
ignoreHeight: false,
ignoreFonts: false,
breakPages: true,
experimental: false,
useBase64URL: true
}
);
八、本地文件读取的坑
安卓 App 端下载后的路径可能是:
text
_doc/uniapp_temp_xxx/download/xxx.docx
也可能被转换成:
text
/storage/emulated/0/Android/data/xxx/xxx.docx
这些路径不是标准浏览器 URL。
常见坑包括:
1. FileReader 不能读取 plus.io 的 File 对象
错误示例:
text
Failed to execute 'readAsArrayBuffer' on 'FileReader':
parameter 1 is not of type 'Blob'
原因是 Android WebView 里的 plus.io File 对象不一定是标准 Blob。
2. XHR 不能直接读取 _doc/...
错误示例:
text
read local file failed
原因是 _doc/... 是 5+ 私有路径,不是普通浏览器可直接访问路径。
3. 需要多路径兜底
所以读取本地文件时,需要尝试多种路径:
entry.nativeURLentry.toLocalURL()entry.toURL()_doc/.../storage/...file:///storage/...
示例逻辑:
js
function readLocalByPlus(fileUrl) {
return new Promise(function(resolve, reject) {
plus.io.resolveLocalFileSystemURL(fileUrl, function(entry) {
const paths = [];
if (entry.nativeURL) paths.push(entry.nativeURL);
if (typeof entry.toLocalURL === 'function') paths.push(entry.toLocalURL());
if (typeof entry.toURL === 'function') paths.push(entry.toURL());
paths.push(fileUrl);
readFirstAvailablePath(paths).then(resolve).catch(reject);
}, reject);
});
}
九、web-view 全屏问题
在安卓 App 端,web-view 是原生子窗口,不是普通 DOM 节点。
所以仅写:
css
width: 100%;
height: 100%;
不一定能限制它的真实大小。
需要通过当前页面的原生子 WebView 设置尺寸:
js
const childWebview = currentWebview.children()[0];
childWebview.setStyle({
top: Math.round(rect.top),
left: Math.round(rect.left),
width: Math.round(rect.width),
height: Math.round(rect.height)
});
这样才能避免 docx 预览的 web-view 在安卓端覆盖全屏。
十、最终链路总结
当前推荐链路是:
text
用户打开 docx 文件
-> filePreview.vue 判断是 docx
-> uni.downloadFile 带 token 下载文件
-> 得到本地 tempFilePath
-> 转换本地路径
-> web-view 打开 static/docx-preview/viewer.html?file=本地路径
-> viewer.html 读取本地 docx
-> docx-preview 渲染 HTML
这套方案的特点:
- 不依赖后端转 PDF
- 不依赖 Office Online
- 不依赖 X5 插件
- 属于纯前端预览
- 只支持
.docx .doc仍然需要系统应用或后端转换
十一、局限性
docx-preview 不是 Word 官方内核,因此存在一些限制:
- 复杂页眉页脚可能不完全一致
- 批注、修订痕迹支持有限
- 特殊字体可能不一致
- 复杂表格可能变形
- 大文件渲染慢
- 加密文档不支持
.doc不支持
所以它适合"在线预览",不适合对版式要求极高的正式排版校验。
十二、结论
在 uni-app 安卓 App 端,docx 纯前端预览不能简单地把 docx-preview 写在页面脚本里。
更稳的方式是:
text
docx-preview 放到本地 HTML
通过 web-view 执行
App 页面负责下载和传递本地文件路径
viewer 页面负责解析和渲染
一句话总结:
DOCX 可以纯前端预览,但在 App 端必须把渲染放到真正的 WebView 环境里处理。