实现微信小程序内嵌H5中的下载功能
一、项目场景:
微信小程序的开发框架是uniapp,使用uniapp脚手架搭建,其中有页面是展示另一个小程序,在这个页面主体内容使用了标签将H5的页面内容展示,H5中有页面存放了下载的路径。点击下载按钮下载文件,或者预览文件让用户手动保存。
难点
- 如果是pc端,下载用一个
<a>
标签就很容易,但是在小程序里的标签中是行不通的。 - 此外,小程序里的
<web-view>
与小程序的通行方式主要用postMessage
,但是触发条件非常苛刻,参照微信的官方文档,只在小程序后退,组件销毁,还有分享时才会触发postMessage并一次性把值全部带出来,使用起来非常不便,对于小程序实际内容主体是<web-view>
内嵌的spa的H5
页面的情况下,销毁组件会带来很多麻烦,因此最后放弃了这个方案。
解决方案:
1、H5微信小程序:
a、首先必不可少的是安装jweixin-module模块:
npm i jweixin-module
b、在main.js中将依赖绑定:
import wx from "jweixin-module"
Vue.prototype.$wx = wx
c、H5对应页面点击下载时代码为:
this.$wx.miniProgram.navigateTo({
url:"/pages/study/downLoading?url="+encodeURIComponent(fileInfo.content_url)
})
2、uni-app的小程序
a、对应的/pages/study/downLoading页面
<template> <div>下载中。。。</div> </template>
<script lang="ts" setup>
import { onLoad } from '@dcloudio/uni-app';
onLoad((option: any) => {
console.log('gfdjhwghjghjkjghjklghjk', option.url);
handleDown(decodeURIComponent(option.url));
});
const handleDown = (url: string) => {
uni.showLoading({
title: '下载中', //提示文字
mask: true, //是否显示透明蒙层,防止触摸穿透,默认:false
});
console.log(url);
let imageType = ['gif', 'png', 'jpg', 'tif', 'bmp', 'webp', 'jpeg', 'JPG'];
const downloadTask = uni.downloadFile({
url: url,
success: res => {
console.log(res, 'resresresres');
uni.hideLoading();
if (res.statusCode === 200) {
console.log(res.tempFilePath, '1111111');
let originFileNameSplit = res.tempFilePath.split('.');
let fileNameType = originFileNameSplit[originFileNameSplit.length - 1];
uni.getFileSystemManager().saveFile({
//微信保存文件,这个存储有点复杂
// 临时存储路径,先有临时存储路径方可使用wx官方的存储本地路径( uni.env.USER_DATA_PATH )
tempFilePath: res.tempFilePath,
//定义本地的存储路径及名称
success(res) {
const savedFilePath = res.savedFilePath;
uni.setStorageSync('uploadData', savedFilePath); //保存传入的参数
uni.setStorageSync('isImageType', imageType.includes(fileNameType)); //保存传入的参数
uni.hideLoading();
uni.navigateBack({
delta: 1,
});
},
fail(err) {
uni.showToast({
title: '预览失败',
icon: 'error',
duration: 1500,
});
setTimeout(() => {
uni.navigateBack({
delta: 1,
});
}, 3000);
},
});
}
},
fail: err => {
uni.hideLoading();
uni.showToast({
title: '下载失败',
icon: 'error',
duration: 1500,
});
console.log(err);
setTimeout(() => {
uni.navigateBack({
delta: 1,
});
}, 3000);
},
});
downloadTask.onProgressUpdate(res => {
// console.log('下载进度', res.progress);
// console.log('已经下载的数据长度', res.totalBytesWritten);
// console.log('预期需要下载的数据总长度', res.totalBytesExpectedToWrite);
});
};
</script>
b、可以预览的时候返回到web-view页面:
<template>
<view class="study">
<web-view class="webviewOut" :src="你的url地址"></web-view>
</view>
</template>
<script setup lang="ts">
onShow(() => {
const url = uni.getStorageSync('uploadData');
console.log('1111111111111111111111111111111111', url);
if (url) {
uni.hideHomeButton();
handleDown(url); //具体的微信下载文件的方法
//每次onShow执行完,还有上面的下载方法执行完后要把这个标记重置为false,这样不同情况触发的onShow才能区分是否是下载文件页面回来的。可能写的重复但是多写几次比较放心
}
});
const handleDown = (savedFilePath: string) => {
const isImageType = uni.getStorageSync('isImageType');
if (isImageType) {
uni.previewImage({
urls: [savedFilePath],
success: function (res) {
uni.removeStorageSync('uploadData');
uni.removeStorageSync('isImageType');
},
fail: function (err) {
console.log(res);
uni.showToast({
title: '预览失败',
icon: 'error',
duration: 1500,
});
uni.removeStorageSync('uploadData');
uni.removeStorageSync('isImageType');
},
});
} else {
uni.openDocument({
//微信打开文件
filePath: savedFilePath,
showMenu: true,
success: function (res) {
uni.removeStorageSync('uploadData');
uni.removeStorageSync('isImageType');
},
fail: function (err) {
console.log(res);
uni.showToast({
title: '预览失败',
icon: 'error',
duration: 1500,
});
uni.removeStorageSync('uploadData');
uni.removeStorageSync('isImageType');
},
});
}
};
</script>
<style scoped lang="scss">
.study {
height: 100vh;
box-sizing: border-box;
padding-bottom: 98rpx;
}
</style>
二、注意(下载大文件)
uni.downloadFile
主要包含两个参数:url
和 filePath
,其中 url 代表下载文件的路径,filePath 代表下载文件保存的路径。
uni.downloadFile 可以用来下载较小的文件,但是对于大文件的下载需要通过 分片下载方式
。具体来说,可以使用 uni.downloadFile 和 uni.uploadFile
结合使用实现分片下载。同时,需要注意为下载文件分配足够的存储空间,以便能够完整保存整个文件。
具体步骤如下:
1.获取文件的总大小。
uni.request({
url: 'http://example.com/fileUrl',
method: 'HEAD',
success: function (res) {
let contentLength = res.header['Content-Length'];
// 计算出下载总块数
let blockCount = Math.ceil(contentLength / BLOCK_SIZE);
// 创建一个和块数相同的数组
let blockList = new Array(blockCount).fill(0);
// 进行分块下载
downloadFileByBlock(url, savePath, blockList);
}
});
2.将文件分成若干个块(BLOCK_SIZE 为每个块的大小),以便分批下载。
3.循环下载每一块。
function downloadFileByBlock(url, savePath, blockList) {
blockList.forEach(function (v, i) {
let rangeStart = i * BLOCK_SIZE;
let rangeEnd = (i + 1) * BLOCK_SIZE - 1;
// 利用 Range 请求头指定下载区间
uni.downloadFile({
url: url,
filePath: savePath,
header: {
'Range': 'bytes=' + rangeStart + '-' + rangeEnd
},
success: function (res) {
console.log(res);
},
fail: function (err) {
console.log(err);
}
});
});
}
大文件上传也可以采用类似的方式, 即将文件分成若干块,每次上传一个块,直到上传完整个文件。
完整代码(自己改了改)
这里给出针对微信小程序 web-view 内嵌 H5 中大文件切片下载的完整代码实现:
// download.js
const downloadUrl = 'http://example.com/files/file.zip' // 下载地址
const fileName = 'file.zip' // 下载的文件名
const fileSize = 1024 * 1024 * 100 // 文件大小,100MB
const chunkSize = 1024 * 1024 * 2 // 分片大小,2MB
const tempFilePath = wx.env.USER_DATA_PATH + '/downloads/' // 临时文件存储路径
Page({
data: {
progress: 0, // 下载进度
isDownloading: false, // 是否正在下载
downloadTask: null // 下载任务句柄
},
// 开始下载
startDownload() {
if (this.data.isDownloading) {
return
}
this.setData({ isDownloading: true })
this.downloadChunks()
},
// 取消下载
cancelDownload() {
if (this.data.downloadTask) {
this.data.downloadTask.abort()
}
this.resetState()
},
// 处理分片下载任务队列,完成后进行合并操作
downloadChunks() {
const chunkNum = Math.ceil(fileSize / chunkSize)
const chunks = []
for (let i = 0; i < chunkNum; i++) {
const start = i * chunkSize
const end = Math.min(start + chunkSize, fileSize)
chunks.push({
index: i,
start,
end,
downloaded: false,
filePath: `${tempFilePath}${i}`
})
}
// 进度状态:已下载的字节数
let loadedBytes = 0
// 下载分片
const downloadChunk = (chunk) => {
return new Promise((resolve, reject) => {
const downloadTask = wx.downloadFile({
url: downloadUrl,
header: {},
filePath: chunk.filePath,
success: (res) => {
if (res.statusCode === 200) {
loadedBytes += chunk.end - chunk.start + 1
chunk.downloaded = true
resolve()
} else {
reject(new Error('Download chunk error.'))
}
},
fail: () => {
reject(new Error('Download chunk fail.'))
}
})
downloadTask.onProgressUpdate((res) => {
const progress = Math.floor((loadedBytes + res.totalBytesWritten) / fileSize * 100)
console.log('分片下载进度:', progress)
this.setData({ progress })
})
this.setData({ downloadTask })
})
}
Promise.all(chunks.map(downloadChunk))
.then(() => {
this.mergeFiles()
})
.catch((err) => {
console.log(err)
this.resetState()
})
},
// 合并已下载的分片文件
mergeFiles() {
const fs = wx.getFileSystemManager()
const stream = fs.createWriteStream(`${wx.env.USER_DATA_PATH}/downloads/${fileName}`)
const write = (index) => {
const chunk = chunks[index]
if (!chunk.downloaded) {
console.log(`Chunk ${chunk.index} not downloaded.`)
return
}
fs.readFile(chunk.filePath, 'binary', (err, data) => {
if (err) {
console.log(err)
return
}
stream.write(data, 'binary')
if (index < chunks.length - 1) {
write(index + 1)
} else {
// 文件合并完成
stream.end()
this.resetState()
}
})
}
write(0)
},
// 重置下载状态
resetState() {
this.setData({
progress: 0,
isDownloading: false,
downloadTask: null
})
}
})
在 H5 页面中可以引入此文件作为下载操作的逻辑处理。同时还需要在 wxml 文件中添加下载按钮及进度条,代码如下:
<!-- download.wxml -->
<view class="download-container">
<button class="download-btn" bindtap="startDownload">下载</button>
<progress percent="{{ progress }}%" />
</view>
需要在小程序的 app.json 文件中添加下载临时路径配置,代码如下:
抱歉,似乎代码显示出现了截断,请继续查看完整的 app.json 中的配置:
{
"pages": [
"pages/index/index",
"pages/download/download"
],
"permission": {
"scope.userLocation": {
"desc": "小程序将获取您的定位信息"
}
},
"downloadFile": {
"domain": [ "example.com" ],
"timeout": 10000
},
"subpackages": []
}
其中,downloadFile 字段下对应了下载文件的配置,包括限定下载域名、超时时间等。文件下载的域名必须在此处进行配置,否则会被微信限制,无法正常下载。
总结:
大致过程就是这样,网上有很多解决方案都是在uni-app小程序的loading页面直接去缓存一下h5发过来的下载地址之后,直接返回到web-view页面去进行下载预览,他的 缺点
是点击下载时页面会一闪而过空白的微信页面再回来,我把下载直接放到了downLoading页面就是为了可以告诉用户我已经在下载了,可以给用户一点好的体验。
uni.navigateBack({delta: 1});
返回到web-view的目的是为了你在预览界面点击左上角返回时可以回到web-view页面,直接下载预览都在downLoading页面的话,你在预览页面点击返回就会回到downLoading页面,体验不是很好。