uniapp 文件预览:从文件流到多格式预览的完整实现

在 uniapp 开发中,文件预览是一个高频且易踩坑的需求场景 ------ 既要兼容 H5 和小程序双端,又要处理网络 URL、文件流(Blob/ArrayBuffer)等不同来源的文件,还要适配图片、PDF、Office 文档等多种格式。本文基于实际项目代码,拆解一套通用、健壮的 uniapp 文件预览方案。

核心需求分析

一个完善的文件预览功能需要解决这些核心问题:

  1. 兼容文件地址(URL)和文件流(Blob/ArrayBuffer)两种数据源
  2. 区分图片、PDF、Word/Excel/PPT 等不同文件类型,提供对应预览方式
  3. 适配 H5 和小程序双端差异(API 不同、文件处理方式不同)
  4. 友好的加载状态提示和错误处理
  5. 支持多张图片预览、接口下载后预览等常见场景

核心接口设计

首先定义统一的预览配置接口,规范入参格式:

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('文件预览失败')
  }
}

关键逻辑拆解

  1. 数据源处理:区分 URL 字符串和文件流,文件流需先转为临时文件
  2. 双端适配
    • H5 端:PDF 新窗口打开、其他文件触发下载
    • 小程序端:网络文档先下载到本地,再用openDocument打开
  3. 错误处理:全程 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)

避坑指南

  1. 小程序文件权限 :小程序中临时文件需放在USER_DATA_PATH目录,避免路径权限问题
  2. H5 临时 URL 释放 :如果频繁处理 Blob,记得在合适时机调用URL.revokeObjectURL释放内存(本文示例未实现,可根据需求补充)
  3. 文件类型判断 :优先使用传入的fileType,其次从文件名 / URL 提取,避免扩展名判断错误
  4. 小程序下载限制:小程序下载文件需配置 download 域名白名单,否则会下载失败
  5. 错误处理:所有异步操作(下载、写入文件、预览)都要加错误捕获,避免页面卡死

总结

这套文件预览方案基于 uniapp 跨端特性,实现了 "多数据源 + 多文件类型 + 双端适配" 的全场景覆盖,封装的函数可直接集成到项目中。核心思路是:统一入参格式 → 标准化文件处理 → 按类型 / 端分发预览逻辑 → 完善的状态和错误处理

相关推荐
_Eleven2 小时前
React 19 深度解析:Actions 与 use API 源码揭秘
前端
_AaronWong2 小时前
Vue3+Element Plus 通用表格组件封装与使用实践
前端·javascript·vue.js
前端西瓜哥2 小时前
图形编辑器开发:文字排版如何实现自动换行?
前端
全栈老石2 小时前
手写一个无限画布 #3:如何在Canvas 层上建立事件体系
前端·javascript·canvas
晴殇i3 小时前
BroadcastChannel:浏览器原生跨标签页通信
前端·面试
DeathGhost3 小时前
分享URL地址到微信朋友圈没有缩略图?
前端·html
MrBread3 小时前
微任务链式派生阻塞渲染
前端·debug
wuhen_n3 小时前
patch算法:新旧节点的比对与更新
前端·javascript·vue.js
小岛前端3 小时前
Cloudflare 掀桌子了,Next.js 迎来重大变化,尤雨溪都说酷!
前端·vite·next.js