1. 为什么uniapp处理安卓非图片文件这么难?
最近在做一个uniapp项目,需要让用户选择安卓手机上的Excel文件,然后读取内容并上传。本以为很简单的事情,结果踩了一堆坑。uniapp选择图片和视频确实很方便,官方API直接支持,但一到Word、Excel这些办公文件,就完全不是那么回事了。
官方压根没提供直接支持非图片文件选择的API,这就很尴尬了。你可能会想,那就用插件吧?但市场上的插件要么绑定云服务,要么收费,免费的适配性又差。安卓的存储权限机制这几年改来改去,不同版本系统获取文件路径的方式都不一样,现在很多目录根本拿不到绝对路径,官方推荐用URI,但怎么转换又是个问题。
我试过几个插件,比如DCloud插件市场上的"安卓iOS双端APP文件选择",确实能拿到安卓的File对象,但问题是怎么转成uniapp能识别的File或Blob。安卓返回的路径可能是/mnt/开头的,直接强转又怕没有通用性,不同厂商设备可能表现不一样。我也尝试在安卓原生代码那边转成byte数组传过来,结果uniapp这边不识别基本类型byte,导包又遇到问题,最后只能放弃。
2. 传统方案的死胡同:Webview的局限性
被插件方案折磨够呛后,我开始考虑Webview方案。思路很简单:在uniapp里内嵌一个Webview,用Webview的input选择文件,然后再把数据传回给uniapp。
实际操作起来发现两个大问题:首先是多了一层UI,用户体验很割裂。本来期望的是原生应用那种无缝体验,结果变成了网页套壳的感觉。其次是样式问题,改成弹窗Webview后,样式调整非常麻烦,怎么都调不出想要的效果。
我还尝试过把Webview的HTML隐藏起来,然后模拟点击调用文件选择,结果系统提示选文件是敏感操作,不允许模拟点击。这条路也走不通了。
这时候我就在想,有没有既能绕过安卓适配问题,又不需要Webview的方案?既要原生体验,又要跨端兼容,还真让我找到了最终解决方案——RenderJs。
3. RenderJs:打通文件选择的任督二脉
RenderJs是uniapp提供的一个运行在视图层的JS引擎,它可以直接操作视图层的DOM和BOM。这意味着我们可以在视图层直接使用浏览器的File API来选择文件,完全绕过原生层的限制。
具体实现很简单,在vue文件里增加一个script标签,设置lang="renderjs"和module="excelHandler":
<script module="excelHandler" lang="renderjs">
export default {
methods: {
chooseExcel() {
const input = document.createElement('input')
input.type = 'file'
input.accept = '.xlsx, .xls'
input.onchange = (e) => {
const file = e.target.files[0]
this.readExcelFile(file)
}
input.click()
},
async readExcelFile(file) {
// 这里读取Excel文件的逻辑
}
}
}
</script>
在模板里绑定这个方法:
<button @click="excelHandler.chooseExcel">选择Excel文件</button>
这样就能无感调起安卓的文件选择器,而且返回的是标准的File对象,uniapp直接能用。不需要任何插件,不需要处理安卓版本兼容问题,简单粗暴但有效。
4. 跨层通信的坑:如何避免数据阻塞
本以为问题就这样解决了,结果发现官方文档里有一行小字:"但也有一个副作用是在造成了两层之间通信阻塞"。RenderJs运行在视图层,而我们的业务逻辑在逻辑层,数据怎么传回去成了新问题。
我开始搜资料,看到很多博客都建议通过绑定事件的方式通信,比如:
// 在RenderJs中
this.$ownerInstance.callMethod('receiveRenderData', {
type: "success",
errMsg: "解析成功",
data: jsonObj
})
然后在逻辑层定义对应的方法:
methods: {
receiveRenderData(data) {
// 处理从RenderJs传回来的数据
console.log('接收到数据:', data)
}
}
这种方式确实能通信,但如果传输的数据量很大,比如解析完的Excel数据有几十MB,就会出现明显的延迟甚至阻塞。我一开始就栽在这个坑里,页面直接卡死了。
解决方案是分片传输和数据压缩。对于大数据量,不要一次性传完,而是分成多个小包:
// 在RenderJs中分片传输
function sendLargeData(data, chunkSize = 100000) {
const totalChunks = Math.ceil(data.length / chunkSize)
for (let i = 0; i < totalChunks; i++) {
const chunk = data.slice(i * chunkSize, (i + 1) * chunkSize)
this.$ownerInstance.callMethod('receiveDataChunk', {
chunk,
index: i,
total: totalChunks
})
}
}
逻辑层接收到所有分片后再重组数据,这样就能避免通信阻塞的问题。
5. Excel解析实战:完整代码示例
现在来看看完整的Excel文件选择和解析流程。首先在RenderJs层处理文件选择和初步解析:
<script module="excelHandler" lang="renderjs">
import * as XLSX from 'xlsx'
export default {
methods: {
chooseExcel() {
const input = document.createElement('input')
input.type = 'file'
input.accept = '.xlsx, .xls, .csv'
input.onchange = (e) => {
const file = e.target.files[0]
if (file) {
this.readExcelFile(file)
}
}
input.click()
},
async readExcelFile(file) {
try {
const arrayBuffer = await file.arrayBuffer()
const workbook = XLSX.read(arrayBuffer, { type: 'array' })
const firstSheetName = workbook.SheetNames[0]
const worksheet = workbook.Sheets[firstSheetName]
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 })
// 分片传输大数据
this.sendDataInChunks(jsonData)
} catch (error) {
this.$ownerInstance.callMethod('receiveRenderData', {
type: "error",
errMsg: error.message
})
}
},
sendDataInChunks(data, chunkSize = 50000) {
const jsonString = JSON.stringify(data)
const totalChunks = Math.ceil(jsonString.length / chunkSize)
for (let i = 0; i < totalChunks; i++) {
const chunk = jsonString.slice(i * chunkSize, (i + 1) * chunkSize)
this.$ownerInstance.callMethod('receiveDataChunk', {
chunk,
index: i,
total: totalChunks,
isFirst: i === 0,
isLast: i === totalChunks - 1
})
}
}
}
}
</script>
在逻辑层接收并重组数据:
export default {
data() {
return {
excelData: null,
dataChunks: [],
receivingData: false
}
},
methods: {
receiveDataChunk(params) {
if (params.isFirst) {
this.receivingData = true
this.dataChunks = []
}
this.dataChunks[params.index] = params.chunk
if (params.isLast) {
this.receivingData = false
const completeData = JSON.parse(this.dataChunks.join(''))
this.excelData = completeData
this.processExcelData(completeData)
}
},
processExcelData(data) {
// 在这里处理Excel数据
console.log('处理Excel数据:', data)
// 上传到服务器或其他操作
},
receiveRenderData(data) {
if (data.type === 'error') {
uni.showToast({
title: data.errMsg,
icon: 'none'
})
}
}
}
}
6. 性能优化与内存管理
处理大文件时,性能优化很重要。我遇到过解析100MB+的Excel文件导致内存溢出的情况,后来总结出几个优化技巧:
流式读取:对于特大文件,不要一次性读取整个文件,而是分块读取:
async readLargeExcelFile(file) {
const chunkSize = 1024 * 1024 // 每次读取1MB
const totalChunks = Math.ceil(file.size / chunkSize)
let offset = 0
let allData = []
for (let i = 0; i < totalChunks; i++) {
const chunk = file.slice(offset, offset + chunkSize)
const arrayBuffer = await chunk.arrayBuffer()
// 处理当前分片的数据
const workbook = XLSX.read(arrayBuffer, { type: 'array' })
// 只提取需要的数据,避免内存堆积
const sheetData = this.extractNeededData(workbook)
allData = allData.concat(sheetData)
offset += chunkSize
// 定期垃圾回收
if (i % 10 === 0) {
await this.forceGarbageCollection()
}
}
return allData
}
数据压缩:在传输前压缩数据:
async compressData(data) {
const jsonString = JSON.stringify(data)
const encoder = new TextEncoder()
const dataUint8 = encoder.encode(jsonString)
// 使用Compression Streams API(需要检查兼容性)
if (window.CompressionStream) {
const compressedStream = new Blob([dataUint8]).stream()
.pipeThrough(new CompressionStream('gzip'))
const compressedBlob = await new Response(compressedStream).blob()
return await compressedBlob.arrayBuffer()
} else {
// 回退方案
return dataUint8
}
}
内存监控:实时监控内存使用,避免溢出:
function monitorMemoryUsage() {
if (window.performance && window.performance.memory) {
const memory = window.performance.memory
const usedJSHeapSize = memory.usedJSHeapSize / 1048576
const totalJSHeapSize = memory.totalJSHeapSize / 1048576
if (usedJSHeapSize > totalJSHeapSize * 0.8) {
console.warn('内存使用过高,建议释放资源')
this.forceGarbageCollection()
}
}
}
function forceGarbageCollection() {
if (window.gc) {
window.gc()
} else {
// 通过创建和释放大对象来"鼓励"GC
try {
new ArrayBuffer(1024 * 1024 * 100) // 100MB
} catch (e) {}
}
}
7. 兼容性处理与降级方案
虽然RenderJs方案很强大,但还是要考虑兼容性问题。有些老版本安卓机或特殊ROM可能支持不完全,需要准备降级方案。
特性检测:在使用前先检测相关API是否可用:
function checkRenderJsSupport() {
return typeof window !== 'undefined' &&
typeof document !== 'undefined' &&
typeof File !== 'undefined' &&
typeof Blob !== 'undefined'
}
function checkFileApiSupport() {
return 'showOpenFilePicker' in window ||
(typeof HTMLInputElement !== 'undefined' &&
HTMLInputElement.prototype.hasOwnProperty('webkitdirectory'))
}
降级方案:当RenderJs不可用时,回退到原生插件方案:
async chooseExcel() {
if (this.renderJsSupported) {
await this.excelHandler.chooseExcel()
} else {
// 使用uni.chooseMessageFile等原生API
const res = await uni.chooseMessageFile({
count: 1,
type: 'file',
extension: ['xlsx', 'xls']
})
this.processFile(res.tempFiles[0])
}
}
错误边界:做好错误处理和用户提示:
async readExcelFile(file) {
try {
// 主要逻辑
} catch (error) {
console.error('文件读取失败:', error)
if (error.name === 'SecurityError') {
uni.showModal({
title: '安全限制',
content: '请检查文件权限设置',
showCancel: false
})
} else if (error.name === 'QuotaExceededError') {
uni.showModal({
title: '内存不足',
content: '文件太大,请尝试 smaller 文件',
showCancel: false
})
} else {
uni.showToast({
title: '文件读取失败',
icon: 'none'
})
}
this.$ownerInstance.callMethod('receiveRenderData', {
type: "error",
errMsg: error.message
})
}
}
8. 实际项目中的最佳实践
经过多个项目的实践,我总结出一些最佳实践:
组件化封装:把文件选择功能封装成通用组件:
<!-- excel-reader.vue -->
<template>
<view>
<button @click="handleChooseExcel">选择Excel文件</button>
<script module="excelHandler" lang="renderjs">
<!-- RenderJs逻辑 -->
</script>
</view>
</template>
<script>
export default {
props: {
maxSize: {
type: Number,
default: 10 * 1024 * 1024 // 10MB
},
allowedTypes: {
type: Array,
default: () => ['xlsx', 'xls']
}
},
methods: {
handleChooseExcel() {
if (this.excelHandler) {
this.excelHandler.chooseExcel()
}
},
receiveDataChunk(params) {
// 处理数据分片
this.$emit('data-chunk', params)
},
receiveRenderData(data) {
if (data.type === 'error') {
this.$emit('error', data.errMsg)
}
}
}
}
</script>
类型校验:对文件类型和大小做严格校验:
validateFile(file) {
// 校验文件大小
if (file.size > this.maxSize) {
throw new Error(`文件大小不能超过 ${this.maxSize / 1024 / 1024}MB`)
}
// 校验文件类型
const fileExtension = file.name.split('.').pop().toLowerCase()
if (!this.allowedTypes.includes(fileExtension)) {
throw new Error(`仅支持 ${this.allowedTypes.join(', ')} 格式`)
}
// 校验MIME类型(如果可用)
if (file.type && !file.type.includes('sheet')) {
throw new Error('文件格式不正确')
}
return true
}
用户体验优化:添加加载状态和进度提示:
async readExcelFile(file) {
try {
this.$ownerInstance.callMethod('receiveRenderData', {
type: "progress",
progress: 0,
message: "开始读取文件"
})
await this.validateFile(file)
this.$ownerInstance.callMethod('receiveRenderData', {
type: "progress",
progress: 30,
message: "文件校验通过"
})
const arrayBuffer = await file.arrayBuffer()
this.$ownerInstance.callMethod('receiveRenderData', {
type: "progress",
progress: 60,
message: "解析Excel数据"
})
const workbook = XLSX.read(arrayBuffer, { type: 'array' })
const jsonData = this.convertWorkbookToJson(workbook)
this.$ownerInstance.callMethod('receiveRenderData', {
type: "progress",
progress: 100,
message: "解析完成"
})
this.sendDataInChunks(jsonData)
} catch (error) {
this.handleError(error)
}
}
这些实践在我们公司的多个项目中都得到了验证,能够稳定处理各种大小的Excel文件,用户体验也很流畅。
339



被折叠的 条评论
为什么被折叠?



