普通视图

发现新文章,点击刷新页面。
昨天 — 2026年2月27日首页

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

作者 JunjunZ
2026年2月27日 11:24

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

核心需求分析

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

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

核心接口设计

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

/** 文件预览选项接口 */
export interface PreviewFileOptions {
  /** 文件数据,可以是URL地址、Blob对象或ArrayBuffer */
  file: string | Blob | ArrayBuffer
  /** 文件名(可选,用于识别文件类型) */
  fileName?: string
  /** 文件类型(可选,如:'pdf', 'doc', 'jpg'等) */
  fileType?: string
  /** 是否显示加载提示 */
  showLoading?: boolean
}

基础工具函数

1. 获取文件扩展名

文件类型判断的基础,从文件名 / URL 中提取扩展名并统一为小写

/** 根据文件名或URL获取文件扩展名 */
function getFileExtension(fileName: string = ''): string {
  const match = fileName.match(/\.([^.]+)$/)
  return match ? match[1].toLowerCase() : ''
}

2. 文件类型判断

区分图片和可预览的文档类型,便于后续分发处理逻辑:

/** 判断是否为图片文件 */
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 和小程序双端:

/**
 * 将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是整个方案的核心,整合了数据源处理、类型判断、双端适配逻辑:

/**
 * 预览文件(支持文件流和文件地址)
 * @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. 多张图片预览

封装专门的图片批量预览函数,简化调用:

/**
 * 预览多张图片
 * @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. 接口下载 + 预览

整合 “接口请求文件流 + 预览” 流程,简化业务调用:

/**
 * 从接口下载并预览文件
 * @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. 预览网络图片

previewFile({ file: 'https://example.com/image.jpg' })

2. 预览接口返回的 PDF 文件流

// 方式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. 预览多张图片

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

昨天以前首页

uniapp实现图片压缩并上传

作者 JunjunZ
2026年2月26日 10:51

最近在使用uniapp开发时,有个功能既要支持H5和小程序双平台,又要实现图片自动压缩,还要处理好接口响应的各种异常情况。最终封装了这个 useUploadMethod 自定义上传方法,今天分享给大家。

痛点分析

先看看我们平时会遇到哪些问题:

// 痛点1:图片太大,上传慢
uni.uploadFile({
  filePath: 'big-image.jpg'  // 5MB的图片直接上传
  // 用户等得花儿都谢了
})

// 痛点2:登录态过期
uni.uploadFile({
  success: (res) => {
    // {"code":405,"msg":"未登录"}
    // 啥也没发生,用户继续操作,然后报错
  }
})

// 痛点3:H5和小程序API不统一
// H5用 File/Blob
// 小程序用 tempFilePath
// 代码里到处都是 #ifdef
技术方案
1. 整体架构

整个上传方法分为三个核心层:

  • 预处理层:图片压缩、参数组装
  • 上传层:跨平台上传、进度监听
  • 响应层:状态码处理、登录态管理
2. 图片压缩模块

跨平台压缩策略

async function compressImage(file: UploadFileItem, options: any): Promise<File | string> {
  // 未启用压缩,直接返回
  if (!options?.enabled) return file.url

  // H5平台:使用 compressorjs
  // #ifdef H5
  return compressImageH5(file, options)
  // #endif

  // 小程序平台:使用 uni.compressImage
  // #ifndef H5
  return new Promise((resolve) => {
    uni.compressImage({
      src: file.url,
      quality: options.quality || 80,
      width: options.maxWidth,
      height: options.maxHeight,
      success: (res) => resolve(res.tempFilePath),
      fail: () => resolve(file.url) // 压缩失败回退原图
    })
  })
  // #endif
}

设计亮点

  • 条件编译处理平台差异
  • 压缩失败自动降级使用原图
  • 统一返回类型,上层无感知

H5平台深度优化(compressorjs)

async function compressImageH5(file: UploadFileItem, options?: CompressOptions): Promise<File | string> {
  let { name: fileName, url: filePath } = file
  
  return new Promise((resolve) => {
    // 从blob URL获取文件
    fetch(filePath)
      .then(res => res.blob())
      .then((blob) => {
        // compressorjs压缩配置
        new Compressor(blob, {
          quality: (options?.quality || 80) / 100, // 转换为0-1范围
          maxWidth: options?.maxWidth,
          maxHeight: options?.maxHeight,
          mimeType: blob.type,
          success: (compressedBlob) => {
            // 生成标准File对象
            const fileName = `file-${Date.now()}.${blob.type.split('/')[1]}`
            const file = new File([compressedBlob], fileName, { type: blob.type })
            resolve(file)
          },
          error: () => resolve(filePath) // 压缩失败回退
        })
      })
      .catch(() => resolve(filePath))
  })
}

关键点

  • fetch + blob() 获取原始文件数据
  • compressorjs 提供高质量的图片压缩
  • 返回 File 对象,H5上传更标准
3. 核心上传方法
export function useUploadMethod(httpOptions: HttpOptions) {
  const { url, name, formData: data, header, timeout, onStart, onFinish, onSuccess, compress } = httpOptions

  const uploadMethod: UploadMethod = async (file, formData, options) => {
    // 1. 上传开始钩子
    onStart?.()

    // 2. 图片压缩(如果启用)
    let filePath = file.url
    try {
      filePath = await compressImage(file, compress)
    } catch {
      filePath = file.url // 异常降级
    }

    // 3. 创建上传任务
    const uploadTask = uni.uploadFile({
      url: options.action || url,
      header: { ...header, ...options.header },
      name: options.name || name,
      formData: { ...data, ...formData },
      timeout: timeout || 60000,
      
      // 4. 跨平台文件参数处理
      ...(typeof File !== 'undefined' && filePath instanceof File 
          ? { file: filePath }   // H5: File对象
          : { filePath }),       // 小程序: 路径字符串

      // 5. 响应处理
      success: (res) => handleSuccess(res, file, options),
      fail: (err) => handleError(err, file, options)
    })

    // 6. 进度监听
    uploadTask.onProgressUpdate((res) => {
      options.onProgress(res, file)
    })
  }

  return { uploadMethod }
}
4. 智能响应处理器
// 上传成功处理
function handleSuccess(res: any, file: UploadFileItem, options: any) {
  try {
    // 解析响应数据
    const resData = JSON.parse(res.data) as ResData<any>
    
    // 状态码检查
    if (res.statusCode >= 200 && res.statusCode < 300) {
      const { code, msg: errMsg = '上传失败' } = resData
      
      if (+code === 200) {
        // 上传成功
        options.onSuccess(res, file, resData)
        onSuccess?.(res, file, resData)
        return
      }
      
      // 登录态过期处理
      if (+code === 405 || errMsg.includes('未登录')) {
        toast.show(errMsg || '登录态失效')
        logout()
        login() // 自动跳转登录页
        return
      }
      
      // 其他业务错误
      toast.show(errMsg)
      options.onError({ ...res, errMsg }, file, resData)
      return
    }
    
    // HTTP 401处理
    if (res.statusCode === 401) {
      toast.show('登录态失效')
      logout()
      login()
      return
    }
    
    // 其他HTTP错误
    toast.show(resData.msg || `服务出错:${res.statusCode}`)
    options.onError({ ...res, errMsg: '服务开小差了' }, file)
    
  } finally {
    onFinish?.() // 无论成功失败都调用
  }
}

// 上传失败处理
function handleError(err: any, file: UploadFileItem, options: any) {
  try {
    toast.show('网络错误,请稍后再试')
    // 设置上传失败
    options.onError(err, file, formData)
  } finally {
    // 文件上传完成时调用
    onFinish?.()
  }
} as any)
基础用法
<template>
  <wd-upload
    :upload-method="uploadMethod"
    v-model:file-list="fileList"
    @change="handleChange"
  />
</template>

<script setup>
import { useUploadMethod } from './upload-method'

// 配置上传方法
const { uploadMethod } = useUploadMethod({
  url: '/api/upload',
  name: 'file',
  header: {
    'Authorization': 'Bearer ' + getToken()
  },
  // 图片压缩配置
  compress: {
    enabled: true,
    quality: 80,
    maxWidth: 1920,
    maxHeight: 1080
  },
  // 钩子函数
  onStart: () => console.log('开始上传'),
  onSuccess: (res, file) => console.log('上传成功', file),
  onFinish: () => console.log('上传完成')
})
</script>
❌
❌