在 uniapp 开发中,文件预览是一个高频且易踩坑的需求场景 ------ 既要兼容 H5 和小程序双端,又要处理网络 URL、文件流(Blob/ArrayBuffer)等不同来源的文件,还要适配图片、PDF、Office 文档等多种格式。本文基于实际项目代码,拆解一套通用、健壮的 uniapp 文件预览方案。
核心需求分析
一个完善的文件预览功能需要解决这些核心问题:
- 兼容文件地址(URL)和文件流(Blob/ArrayBuffer)两种数据源
- 区分图片、PDF、Word/Excel/PPT 等不同文件类型,提供对应预览方式
- 适配 H5 和小程序双端差异(API 不同、文件处理方式不同)
- 友好的加载状态提示和错误处理
- 支持多张图片预览、接口下载后预览等常见场景
核心接口设计
首先定义统一的预览配置接口,规范入参格式:
ts
/** 文件预览选项接口 */
export interface PreviewFileOptions {
/** 文件数据,可以是URL地址、Blob对象或ArrayBuffer */
file: string | Blob | ArrayBuffer
/** 文件名(可选,用于识别文件类型) */
fileName?: string
/** 文件类型(可选,如:'pdf', 'doc', 'jpg'等) */
fileType?: string
/** 是否显示加载提示 */
showLoading?: boolean
}
基础工具函数
1. 获取文件扩展名
文件类型判断的基础,从文件名 / URL 中提取扩展名并统一为小写
ts
/** 根据文件名或URL获取文件扩展名 */
function getFileExtension(fileName: string = ''): string {
const match = fileName.match(/\.([^.]+)$/)
return match ? match[1].toLowerCase() : ''
}
2. 文件类型判断
区分图片和可预览的文档类型,便于后续分发处理逻辑:
ts
/** 判断是否为图片文件 */
function isImageFile(ext: string): boolean {
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']
return imageExts.includes(ext.toLowerCase())
}
/** 判断是否为可用 openDocument 打开的文档 */
function isDocumentFile(ext: string): boolean {
const docExts = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx']
return docExts.includes(ext.toLowerCase())
}
3. 文件流转临时文件
这是处理文件流的核心函数,兼容 H5 和小程序双端:
ts
/**
* 将Blob或ArrayBuffer转换为临时文件
* @param data Blob或ArrayBuffer数据
* @param fileName 文件名
*/
async function blobToTempFile(data: Blob | ArrayBuffer, fileName: string = 'temp_file'): Promise<string> {
// #ifdef H5
// H5环境:创建临时URL
if (data instanceof Blob) {
return URL.createObjectURL(data)
}
else {
const blob = new Blob([data])
return URL.createObjectURL(blob)
}
// #endif
// #ifndef H5
// 小程序环境:使用文件系统保存临时文件
const fs = uni.getFileSystemManager()
const wxWriteFile = $uni.promisify(fs.writeFile)
// 确保有文件扩展名
const ext = getFileExtension(fileName) || 'tmp'
const filePath = `${(uni as any).env.USER_DATA_PATH}/preview_${Date.now()}.${ext}`
// 转换数据
let fileData: ArrayBuffer
if (data instanceof Blob) {
// Blob转ArrayBuffer
fileData = await data.arrayBuffer()
}
else {
// ArrayBuffer类型
fileData = data
}
await wxWriteFile({
filePath,
data: fileData,
encoding: 'binary',
})
return filePath
// #endif
}
- H5 端:利用
URL.createObjectURL生成临时 URL,直接用于预览 / 下载 - 小程序端:通过文件系统
writeFile将二进制数据写入本地临时文件,返回文件路径
核心预览函数实现
previewFile是整个方案的核心,整合了数据源处理、类型判断、双端适配逻辑:
ts
/**
* 预览文件(支持文件流和文件地址)
* @param options 预览选项
*/
export async function previewFile(options: PreviewFileOptions) {
const { file, fileName = '', fileType, showLoading = true } = options
try {
if (showLoading) {
$toast.loading('正在加载文件...')
}
let filePath: string
let ext: string
// 判断文件来源类型
if (typeof file === 'string') {
// 文件地址
filePath = file
ext = fileType || getFileExtension(file) || getFileExtension(fileName)
}
else {
// 文件流(Blob或ArrayBuffer)
ext = fileType || getFileExtension(fileName)
filePath = await blobToTempFile(file, fileName)
}
if (showLoading) {
$toast.loaded()
}
// 根据文件类型选择预览方式
if (isImageFile(ext)) {
// 图片预览
uni.previewImage({
current: filePath,
urls: [filePath],
fail: (err) => {
console.error('图片预览失败:', err)
$toast.show('图片预览失败')
},
})
}
else if (isDocumentFile(ext)) {
// #ifdef H5
// H5环境:PDF文件直接在新窗口打开,其他文档尝试下载
if (ext === 'pdf') {
window.open(filePath, '_blank')
}
else {
// 其他文档类型在H5中触发下载
const link = document.createElement('a')
link.href = filePath
link.download = fileName || `document.${ext}`
link.click()
$toast.show('文件已开始下载')
}
// #endif
// #ifndef H5
// 小程序环境:使用 openDocument
let docPath = filePath
// 如果是网络地址,需要先下载
if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
if (showLoading) {
$toast.loading('正在下载文件...')
}
const downloadRes: any = await $uni.downloadOnlineFile(filePath)
if (showLoading) {
$toast.loaded()
}
if (downloadRes.statusCode === 200 && downloadRes.tempFilePath) {
docPath = downloadRes.tempFilePath
}
else {
throw new Error('文件下载失败')
}
}
// 打开文档
const result = await $uni.openFile(docPath)
if (result === 'fail') {
$toast.show('文件打开失败,可能不支持该文件格式')
}
// #endif
}
else {
// 其他文件类型
// #ifdef H5
// H5环境:触发下载
const link = document.createElement('a')
link.href = filePath
link.download = fileName || `file.${ext}`
link.click()
// 下载完成提示
$toast.show('文件已开始下载')
// #endif
// #ifndef H5
// 小程序环境:尝试使用 openDocument 打开
let docPath = filePath
// 如果是网络地址,需要先下载
if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
if (showLoading) {
// 加载提示
$toast.loading('正在下载文件...')
}
const downloadRes: any = await $uni.downloadOnlineFile(filePath)
if (showLoading) {
// 加载完成
$toast.loaded()
}
if (downloadRes.statusCode === 200 && downloadRes.tempFilePath) {
docPath = downloadRes.tempFilePath
}
else {
throw new Error('文件下载失败')
}
}
const result = await $uni.openFile(docPath)
if (result === 'fail') {
$toast.show('无法预览该文件类型')
}
// #endif
}
}
catch (error) {
console.error('文件预览失败:', error)
// 加载完成
$toast.loaded()
$toast.show('文件预览失败')
}
}
关键逻辑拆解
- 数据源处理:区分 URL 字符串和文件流,文件流需先转为临时文件
- 双端适配 :
- H5 端:PDF 新窗口打开、其他文件触发下载
- 小程序端:网络文档先下载到本地,再用
openDocument打开
- 错误处理:全程 try-catch,加载状态统一管理,失败友好提示
扩展功能
1. 多张图片预览
封装专门的图片批量预览函数,简化调用:
ts
/**
* 预览多张图片
* @param urls 图片地址数组
* @param current 当前显示图片的索引,默认为0
*/
export function previewImages(urls: string[], current: number = 0) {
if (!urls || urls.length === 0) {
$toast.show('没有可预览的图片')
return
}
uni.previewImage({
current: urls[current],
urls,
fail: (err) => {
console.error('图片预览失败:', err)
$toast.show('图片预览失败')
},
})
}
2. 接口下载 + 预览
整合 "接口请求文件流 + 预览" 流程,简化业务调用:
ts
/**
* 从接口下载并预览文件
* @param url 接口地址
* @param fileName 文件名(用于判断文件类型)
* @param options 其他请求选项
*/
export async function downloadAndPreview(url: string, fileName: string, options: any = {}) {
try {
$toast.loading('正在下载文件...')
// 发起请求获取文件流
const response: any = await new Promise((resolve, reject) => {
uni.request({
url,
method: options.method || 'GET',
data: options.data || {},
header: options.header || {},
responseType: 'arraybuffer', // 获取二进制数据
success: resolve,
fail: reject,
})
})
// 加载完成
$toast.loaded()
if (response.statusCode === 200) {
// 预览文件
await previewFile({
file: response.data,
fileName,
showLoading: true,
})
}
else {
$toast.show('文件下载失败')
}
}
catch (error) {
console.error('下载并预览文件失败:', error)
$toast.loaded()
$toast.show('文件下载失败')
}
}
实用示例
1. 预览网络图片
ts
previewFile({ file: 'https://example.com/image.jpg' })
2. 预览接口返回的 PDF 文件流
ts
// 方式1:手动处理文件流
const blob = await fetch('/api/file').then(res => res.blob())
previewFile({ file: blob, fileName: 'document.pdf' })
// 方式2:使用封装的downloadAndPreview
downloadAndPreview('/api/download/file', 'document.pdf')
3. 预览多张图片
ts
previewImages(['https://example.com/1.jpg', 'https://example.com/2.jpg'], 0)
避坑指南
- 小程序文件权限 :小程序中临时文件需放在
USER_DATA_PATH目录,避免路径权限问题 - H5 临时 URL 释放 :如果频繁处理 Blob,记得在合适时机调用
URL.revokeObjectURL释放内存(本文示例未实现,可根据需求补充) - 文件类型判断 :优先使用传入的
fileType,其次从文件名 / URL 提取,避免扩展名判断错误 - 小程序下载限制:小程序下载文件需配置 download 域名白名单,否则会下载失败
- 错误处理:所有异步操作(下载、写入文件、预览)都要加错误捕获,避免页面卡死
总结
这套文件预览方案基于 uniapp 跨端特性,实现了 "多数据源 + 多文件类型 + 双端适配" 的全场景覆盖,封装的函数可直接集成到项目中。核心思路是:统一入参格式 → 标准化文件处理 → 按类型 / 端分发预览逻辑 → 完善的状态和错误处理。