笔者之前实现了一个 PC 端的个人网盘功能,包含断点续传、文件秒传的文件管理系统,具体请参考文章**《断点续传与个人网盘系统的前后端设计》**
于是构思想将个人网盘功能扩展到小程序,沿用之前的后端(接口基本不需要改动),只需要完成移动端的 UI 实现即可。又由于 PC 端是基于 Vue 实现的,为了方便开发所以想直接采用跨平台框架,将 vue 代码转成小程序,而且这样还可以考虑后续转出 H5 或原生手机客户端等。
基于功能的原因,该小程序原则上不可能通过审核,所以只会作为个人练手项目。
关于跨平台框架的选型,对比了当前主要用的 3 个框架
对比了下,最终还是觉得选用 Taro-next 进行开发,虽然当前还是 beta 版,但是文档已经完善了,基本不影响使用。
Taro 安装参考官方文档,先全局安装 Taro 脚手架,再初始化一个项目,安装依赖。
使用npm run dev:weapp
启动开发环境,然后使用微信开发者工具
导入项目即可进行预览(最好申请一个小程序 Id,以便后续可以真机预览等功能)。
可以直接下载 vant-weapp 的生成包,直接放在目录下。然后就可以在页面的配置下引入自定义组件,Taro 会自动将这些同步到打包后的文件夹。微信小程序打包的时候也会自动去除没用到的包。
// index.config.js
export default {
usingComponents: {
"van-icon": "../../components/vant/icon/index",
"van-action-sheet": "../../components/vant/action-sheet/index",
"van-notify": "../../components/vant/notify/index",
"van-dialog": "../../components/vant/dialog/index",
"van-field": "../../components/vant/field/index",
"van-checkbox": "../../components/vant/checkbox/index",
"van-checkbox-group": "../../components/vant/checkbox-group/index",
"van-progress": "../../components/vant/progress/index",
"van-toast": "../../components/vant/toast/index",
},
};
具体请参考:
另外,一些 Vant 的全局组件可以挂载到 Vue 的实例上,方便使用。
// app.js
import Vue from "vue";
// ...
import Notify from "./components/vant/notify/notify";
import Dialog from "./components/vant/dialog/dialog";
import Toast from "./components/vant/toast/toast";
// ...
Vue.prototype.$notify = Notify;
Vue.prototype.$dialog = Dialog;
Vue.prototype.$toast = Toast;
// ...
const App = new Vue({
store,
onShow(options) {},
render(h) {
return h("block", this.$slots.default);
},
});
export default App;
使用时需注意,vant 该 3 个组件是要先定义默认节点的,要在一个全局的 vue 中加入以下节点代码
<van-notify id="van-notify" />
<van-dialog id="van-dialog" />
<van-toast id="van-toast" />
然后就可以在 vue 中使用
// Notify
this.$notify({ type: "danger", message: e || "登录失败", duration: 1000 });
// Dialog
this.$dialog
.confirm({
message:
"此操作会将文件移动到回收站,你可在一个月内进行恢复操作,一个月后将永久删除",
})
.then(() => {
// do something
})
.catch(() => {
// on cancel
});
// Toast
this.$toast.loading({
mask: true,
message: "上传中...",
});
可以为请求加入请求拦截与响应拦截,基于Taro.request
content-type: application/json
baseURL
,将请求前缀提取出来配置,方便修改请求拦截器
:将 sessionId 注入到请求头响应拦截器
:只给 errCode 为 200 的请求通过,其余请求为错误请求,直接使用 Notify 组件自动弹出错误信息参考代码如下:
// fetch.js
import Taro from "@tarojs/taro";
import Notify from "../components/vant/notify/notify";
const interceptor = function (chain) {
const requestParams = chain.requestParams;
const sessionId = Taro.getStorageSync("sessionId");
if (sessionId) requestParams.header.sessionId = sessionId;
return chain.proceed(requestParams).then(
(res) => {
const data = res.data;
if (data.errCode === 200) {
return Promise.resolve(data.data);
} else {
Notify({
type: "danger",
selector: "#van-notify",
message: data.errMsg,
duration: 1000,
});
return Promise.reject(data.errMsg);
}
},
(err) => {
Notify({
type: "danger",
selector: "#van-notify",
message: "服务端异常",
duration: 1000,
});
return err.toString();
}
);
};
Taro.addInterceptor(interceptor);
export const baseURL = "http://localhost:5001/storage";
export const instance = (method, url, data, options) => {
return Taro.request({
dataType: "json",
header: {
"content-type": "application/json",
},
method,
url: baseURL + url,
data,
...options,
});
};
将封装的实例像全局组件那样挂载到 Vue 的实例上,方便使用。
// app.js
import Vue from "vue";
// ...
import { instance, baseURL } from "./utils/fetch";
// ...
Vue.prototype.$get = (url, data, options) =>
instance("get", url, data, options);
Vue.prototype.$post = (url, data, options) =>
instance("post", url, data, options);
Vue.prototype.$baseURL = baseURL;
// ...
const App = new Vue({
store,
onShow(options) {},
render(h) {
return h("block", this.$slots.default);
},
});
export default App;
然后就在 vue 中使用:
// get
this.$get("/getFileList", {
currentPath: this.currentPathParams,
}).then((data) => {
this.fileList = data;
});
//post
this.$post("/delete", {
deleteList,
}).then((data) => {
this.$notify({ type: "success", message: "操作成功", duration: 1000 });
this.$emit("onNeedRefresh");
});
微信的文件上传并不能拿到文件实例,查了好多资料,并无找到能够进行文件分片的功能,所有不能实现断点续传了。这次直接使用了简单的单文件上传,后端添加了一个 simpleUpload 接口接收,其余操作在这就不细说了。
微信并无提供直接调用系统文件管理器的 API(可能也没权限),这次采用了微信提供的以下几种方式取代
这几个接口都会返回一个含有选取路径的成功回调,能拿到文件路径进行上传
上传逻辑
handleUploadFile (type = 1) {
const callback = (res) => {
this.$emit('update:actionVisible', false)
this.$toast.loading({
mask: true,
message: '上传中...'
})
const filePaths = type === 3 ? res.tempFiles.map(item => item.path) : res.tempFilePaths
Promise.all(
filePaths.map(item => {
return Taro.uploadFile({
url: this.$baseURL + '/simpleUpload',
filePath: item,
name: 'file',
formData: {
targetPath: this.currentPathArr.join('/')
},
header: {
sessionid: Taro.getStorageSync('sessionId')
}
}).then(data => {
try {
const res = JSON.parse(data.data)
if (res.errCode === 200) {
const { fileName } = res.data
this.$notify({ type: 'success', message: `上传成功,文件保存为${fileName}`, duration: 2000 })
this.$emit('onNeedRefresh')
} else {
this.$notify({ type: 'success', message: `上传失败,${res.errMsg}`, duration: 2000 })
}
} catch (e) {
this.$notify({ type: 'success', message: `上传失败,服务端错误`, duration: 2000 })
}
})
})
).then(() => {
this.$toast.clear()
})
}
if (type === 1) {
wx.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success (res) {
callback(res)
}
})
} else if (type === 2) {
wx.chooseVideo({
sourceType: ['album', 'camera'],
maxDuration: 60,
camera: 'back',
success (res) {
callback(res)
}
})
} else if (type === 3) {
wx.chooseMessageFile({
count: 1,
success (res) {
callback(res)
}
})
}
}
由于微信小程序文件存储最大为 10M,所以基本不可能实现下载功能了。于是把下载功能改成了在线预览功能,当前支持以下几种文件类型
Taro.openDocument
API 实现预览操作image
组件显示video
组件中显示以上,除了视频外,其余都是要先通过wx.downloadFile
将文件下载到本地生成临时路径再执行相应预览操作,这时候可以添加下载进度条优化用户体验。而视频不会直接进行下载,需要后端将视频资源设为流视频(Accept-Range
),这样视频可以一边下载一边播放。
进度条使用微信原生请求事件onProgressUpdate
实现
主要代码
handleActionPreview (el) {
const target = this.actionFileInfo
const targetPath = this.currentPathArr.join('/') + '/' + target.fileName
const realPath = targetPath.replace('$Root', this.$baseURL)
const sessionId = Taro.getStorageSync('sessionId')
if (videoSuffixArr.includes(target.suffix)) {
// 视频直接展示
this.handleActionCancel()
this.mediaPreviewVisible = 2
this.videoPreviewURL = realPath + '?sessionid=' + Taro.getStorageSync('sessionId')
} else if (this.previewArr.includes(target.suffix)) {
// 其他类型先下载
this.downloadTask = wx.downloadFile({
url: realPath,
header: {
'sessionid': sessionId
},
success: (data) => {
const { tempFilePath } = data
if (imgSuffixArr.includes(target.suffix)) {
this.mediaPreviewVisible = 1
this.imgPreviewURL = tempFilePath
} else if (documentSuffixArr.includes(target.suffix)) {
Taro.openDocument({
filePath: tempFilePath
})
}
this.handleActionCancel()
},
fail: () => {
this.$notify({ type: 'danger', message: `下载失败`, duration: 2000 })
}
})
this.downloadTask.onProgressUpdate((res) => {
this.isDownloading = true
const { progress, totalBytesWritten, totalBytesExpectedToWrite } = res
this.downloadingInfo = { progress, totalBytesWritten, totalBytesExpectedToWrite }
})
}
}
PS:后端是 Koa 服务,使用了koa-range
为静态资源下的视频实现分段请求功能,视频就可以一边下载一边播放,在 chrome 中视频可以进行拖动进度条,若不使用这个,在 chrome 中视频是无法拖动进度条的。
使用了 van-checkgroup 实现,直接更换当前文件列表,但尽量保持节点位置没发生改变,可防止界面回流影响性能。
该功能对应 PC 端上的移动,可以选择文件仅移动或者是复制,PC 端是使用一个树形组件进行选择文件夹。但发现 Vant 等并没有相关树形组件,可能需要自己封装一个,然后直接采用了用户点击移动操作后,记录选择的文件信息,然后用户需要进入到相应目录下进行粘贴操作。
执行命令npm run build:weapp
后,其余操作与正常小程序一样。