uni-app + unibest + Vue3 + Vite 开发微信小程序避坑指南(实战总结)
这不是一篇入门教程,而是基于真实项目踩坑后的工程化经验总结,重点解决:
- 文件上传 / 选择
- 图片压缩
- 文件预览(Blob → 小程序)
- 接口兼容问题
- 常见运行时报错
适用于:
-
unibest + Vue3 + Vite 技术栈
-
微信小程序端
一、环境与基础认知
1. unibest + Vite 的特点
-
构建快
-
TS 友好
-
但部分 uni API 类型不稳定
常见问题:
Property 'forEach' does not exist on type ...
解决:
const tempFiles = Array.isArray(res.tempFiles)
? res.tempFiles
: [res.tempFiles]
二、文件选择(核心坑)
坑1:uni.chooseFile 在小程序不可用
你遇到的报错:
TypeError: chooseFile is not a function
正确方案(微信小程序专用)
wx.chooseMessageFile({
count: 5,
type: 'file',
extension: ['pdf', 'doc', 'docx', 'xls', 'xlsx'],
})
关键点:
-
uni.chooseFile❌(很多版本不可用) -
必须用
wx.chooseMessageFile
推荐封装(一步完成:选择 + 校验 + 上传)
核心思路:
选择 → 校验 → 并发上传 → 返回结果
三、上传文件核心问题
坑2:uploadFile 报错
你遇到的错误:
e.url.startsWith is not a function
原因
你传了错误参数:
uploadSingle(file) // file 是对象
而正确应该是:
uploadSingle(file.path)
小程序文件结构:
{
path: "wxfile://xxx",
name: "xxx.pdf",
size: 12345
}
正确上传写法
uni.uploadFile({
url,
filePath: file.path,
name: 'file',
})
四、图片压缩(必须做)
问题
-
图片动不动 5MB+
-
上传慢 / 超时
推荐策略
const shouldCompress = (size) => size / 1024 / 1024 > 1
if (shouldCompress(file.size)) {
await uni.compressImage({
src: filePath,
quality: 75
})
}
注意
-
只压缩图片
-
PDF / Word 不可压缩
五、文件预览(最容易踩坑)
坑3:PC 用 Blob,小程序不支持
PC代码:
responseType: 'blob'
小程序 不支持 Blob
小程序正确方案
流程必须这样:
请求 arraybuffer → 写入本地 → 打开文件
标准实现
// 1. 请求文件流
uni.request({
url,
responseType: 'arraybuffer'
})
// 2. 写文件
wx.getFileSystemManager().writeFile()
// 3. 打开
uni.openDocument()
坑4:接口返回 JSON(错误)而不是文件流
必须判断:
if (contentType.includes('application/json')) {
// 说明是错误返回
}
六、文件预览封装建议
推荐工具方法
previewFileById({
fileId,
fileUpType
})
必须处理的异常
-
HTTP 状态码 ≠ 200
-
content-type 是 JSON
-
文件写入失败
-
openDocument 失败
七、图片加载失败处理
坑5:image error 无限触发
你的写法:
item.url = defaultImg
如果 defaultImg 也加载失败 → 死循环
正确写法
function imageError(e, item, index) {
if (item.url === defaultImg)
return
item.url = defaultImg
}
八、复制文本体验优化
问题
user-select
需要长按 ,不友好
推荐方案
uni.setClipboardData({
data: value
})
UI 加一个复制图标即可
九、附件URL获取(并发问题)
问题
Promise.allSettled
-
写法复杂
-
不好维护
推荐简化
const tasks = files.map(async file => {
try {
const res = await getAttachmentUrl()
return success
} catch {
return default
}
})
await Promise.all(tasks)
十、性能与稳定性建议
1. 并发控制(重要)
const concurrency = 3
避免:
-
接口打爆
-
小程序限制
2. 日志体系
建议统一:
const log = (...args) => console.log('[upload]', ...args)
const logError = (...args) => console.error('[upload.error]', ...args)
3. 全链路兜底
每个步骤必须考虑失败:
-
选择失败
-
校验失败
-
上传失败
-
解析失败
十一、总结(核心经验)
常见错误
-
用 uni.chooseFile ❌
-
上传传 file 对象 ❌
-
直接用 Blob ❌
-
不判断 content-type ❌
正确心智模型
文件上传
选择 → 校验 →(图片压缩)→ 上传 → 结果汇总
文件预览
请求流 → 写文件 → 打开
十二、建议封装(强烈推荐)
建议抽成3个核心模块:
1、useUploadFile(图片上传)
-
支持压缩
-
支持并发
2、useChooseAnyFile(文件上传)
-
支持文件类型限制
-
支持批量上传
3、previewFileById(文件预览)
-
统一处理流
-
自动识别类型
例如:
封装的useUploadFile.ts
import { ref } from 'vue'
import { getEnvBaseUrl } from '@/utils'
const baseUrl = getEnvBaseUrl()
// const baseUrl = 'http://10.xxx:9101'
/* ================== 日志 ================== */
const LOG_ENABLE = true
const log = (...args: any[]) => LOG_ENABLE && console.log('[upload]', ...args)
const logError = (...args: any[]) => LOG_ENABLE && console.error('[upload.error]', ...args)
/* ================== URL ================== */
export const uploadFileUrl = {
CERT: `${baseUrl}/api/xxx/file/upLoad`,
AI_CERT: `${baseUrl}/api/xxx/rapidVinRecognition`,
PREVIEW_CERT: `${baseUrl}/api/xxx/file/preview-url`,
}
/* ================== 类型定义 ================== */
export interface UploadOptions {
count?: number
sizeType?: Array<'original' | 'compressed'>
sourceType?: Array<'album' | 'camera'>
maxSize?: number
onProgress?: (progress: number) => void
onSuccess?: (res: Record<string, any>) => void
onError?: (err: any) => void
onComplete?: (summary: {
data: any[]
sizeLimitCount: number
uploadFailCount: number
}) => void
onLoading?: (loading: boolean) => void
}
/* ================== 核心上传 ================== */
export function useUploadFile<T = any>(
url: string,
formData: Record<string, any> = {},
options: UploadOptions = {},
directFilePath?: string,
) {
const loading = ref(false)
const error = ref(false)
const data = ref<T>()
const progress = ref(0)
const {
count = 4,
sizeType = ['original', 'compressed'],
sourceType = ['album', 'camera'],
maxSize = 10,
onProgress,
onSuccess,
onError,
onComplete,
onLoading,
} = options
const setLoading = (val: boolean) => {
loading.value = val
onLoading?.(val)
log('loading:', val)
}
const checkFileSize = (size: number) => {
const fileSize = size / 1024 / 1024
log('checkFileSize, maxSize=', fileSize, maxSize, 'MB')
const ok = fileSize <= maxSize
log('checkFileSize:', fileSize, '=>', ok)
return ok
}
/* ================== 压缩 ================== */
const compressImage = (filePath: string, quality = 65) => {
return new Promise<string>((resolve) => {
uni.compressImage({
src: filePath,
quality,
success: (res) => {
log('[已压缩], res:', res)
return resolve(res.tempFilePath)
},
fail: () => resolve(filePath), // 兜底
})
})
}
const shouldCompress = (file?: any) => {
// if (url === uploadFileUrl.AI_CERT)
// return false
if (!file)
return true
return file.size / 1024 / 1024 > 1
}
/* ================== 上传 ================== */
const uploadSingle = (filePath: string) => {
log('开始上传单文件:', filePath, formData)
return new Promise<{ ok: boolean, data?: any }>((resolve) => {
const task = uni.uploadFile({
url,
filePath,
name: 'file',
formData,
header: { 'Content-Type': 'multipart/form-data' },
success: (res) => {
log('upload success raw:', res)
try {
const r = JSON.parse(res.data)
log('upload parsed:', r)
if (r?.result === '1') {
const response = {
...r.row,
url: filePath,
fileId: r.row?.docFileInfoDTO?.attachmentId,
fileUpType: r.row?.docFileInfoDTO?.storageSystem,
}
onSuccess?.(response)
resolve({ ok: true, data: response })
}
else {
logError('业务失败:', r)
onError?.(r)
resolve({ ok: false })
}
}
catch (e) {
logError('解析失败:', e)
onError?.(e)
resolve({ ok: false })
}
},
fail: (err) => {
logError('upload fail:', err)
onError?.(err)
resolve({ ok: false })
},
})
task.onProgressUpdate((res) => {
progress.value = res.progress
onProgress?.(res.progress)
})
})
}
/* ================== 主流程 ================== */
const run = async () => {
log('run start', { directFilePath })
if (directFilePath) {
setLoading(true)
let finalPath = directFilePath
if (shouldCompress()) {
finalPath = await compressImage(directFilePath)
}
const r = await uploadSingle(finalPath)
data.value = r.ok ? r.data : undefined
onComplete?.({
data: r.ok && r.data ? [r.data] : [],
sizeLimitCount: 0,
uploadFailCount: r.ok ? 0 : 1,
})
setLoading(false)
return
}
uni.chooseMedia({
count,
mediaType: ['image'],
sourceType,
sizeType,
success: async (res) => {
log('chooseMedia success:', res)
const valid: string[] = []
let sizeLimitCount = 0
res.tempFiles.forEach((f) => {
if (checkFileSize(f.size))
valid.push(f.tempFilePath)
else sizeLimitCount++
})
log('valid files:', valid)
if (!valid.length) {
onComplete?.({ data: [], sizeLimitCount, uploadFailCount: 0 })
return
}
setLoading(true)
const results = await Promise.all(
valid.map(async (filePath, index) => {
const raw = res.tempFiles[index]
let finalPath = filePath
if (shouldCompress(raw)) {
finalPath = await compressImage(filePath)
}
return uploadSingle(finalPath)
}),
)
log('upload results:', results)
const successList = results.filter(r => r.ok).map(r => r.data)
const failCount = results.filter(r => !r.ok).length
data.value = successList as any
onComplete?.({
data: successList,
sizeLimitCount,
uploadFailCount: failCount,
})
setLoading(false)
},
fail: (err) => {
logError('chooseMedia fail:', err)
error.value = true
onError?.(err)
},
})
}
return { loading, error, data, progress, run }
}
/* ================== 选择文件 ================== */
export function useChooseFile(options: UploadOptions = {}) {
const {
count = 4,
sizeType = ['original', 'compressed'],
sourceType = ['album', 'camera'],
maxSize = 10,
} = options
const files = ref<string[]>([])
const rawFiles = ref<any[]>([])
const run = () => {
log('chooseFile start')
return new Promise((resolve, reject) => {
uni.chooseMedia({
count,
mediaType: ['image'],
sizeType,
sourceType,
success: (res) => {
log('chooseFile success:', res)
const valid: string[] = []
res.tempFiles.forEach((f) => {
if (f.size / 1024 / 1024 <= maxSize)
valid.push(f.tempFilePath)
})
files.value = valid
rawFiles.value = res.tempFiles
log('chooseFile valid:', valid)
resolve({ files: valid, rawFiles: res.tempFiles })
},
fail: (err) => {
logError('chooseFile fail:', err)
reject(err)
},
})
})
}
return { files, rawFiles, run }
}
/* ================== AI上传 ================== */
export function useUploadAIFile<T = any>(options?: {
url?: string
formData?: Record<string, any>
}) {
const loading = ref(false)
const data = ref<T | null>(null)
const error = ref<any>(null)
const finalUrl = options?.url ?? uploadFileUrl.AI_CERT
const formData = options?.formData || {}
const run = (filePath: string) => {
log('AI upload start:', finalUrl, formData, filePath)
loading.value = true
return new Promise<{ ok: boolean, data?: T }>((resolve) => {
const task = uni.uploadFile({
url: finalUrl,
filePath,
name: 'file',
formData,
header: { 'Content-Type': 'multipart/form-data' },
success: (res) => {
try {
const r = JSON.parse(res.data)
log('AI upload 结果:', r)
if (r?.result === '1') {
data.value = r
resolve({ ok: true, data: r.row })
}
else {
error.value = r
resolve({ ok: false })
}
}
catch (e) {
error.value = e
resolve({ ok: false })
}
},
fail: (err) => {
error.value = err
resolve({ ok: false })
},
})
setTimeout(() => {
if (loading.value) {
task.abort()
error.value = new Error('AI识别超时')
resolve({ ok: false })
}
}, 30000)
}).finally(() => {
loading.value = false
})
}
return { loading, data, error, run }
}
/* ================== 批量上传 ================== */
export async function uploadBatchFile<T = any>(
url: string,
filePaths: string[],
formData: Record<string, any> = {},
) {
log('batch upload start:', filePaths)
const successList: T[] = []
let failCount = 0
for (const path of filePaths) {
log('batch uploading:', path)
const { data, run } = useUploadFile<T>(url, formData, {}, path)
await run()
if (data.value) {
successList.push(data.value)
}
else {
failCount++
logError('batch fail:', path)
}
}
log('batch result:', { successList, failCount })
return { successList, failCount }
}
最后一句
小程序开发不是“能跑就行”,而是:
接口、文件、设备能力三者兼容的工程问题
把上传 + 预览这套打通,你的项目稳定性会提升一个量级。


浙公网安备 33010602011771号