boycot 搜索
avatar

boycot

使用Taro-next将个人网盘功能扩展到微信小程序

前言

笔者之前实现了一个 PC 端的个人网盘功能,包含断点续传、文件秒传的文件管理系统,具体请参考文章**《断点续传与个人网盘系统的前后端设计》**

于是构思想将个人网盘功能扩展到小程序,沿用之前的后端(接口基本不需要改动),只需要完成移动端的 UI 实现即可。又由于 PC 端是基于 Vue 实现的,为了方便开发所以想直接采用跨平台框架,将 vue 代码转成小程序,而且这样还可以考虑后续转出 H5 或原生手机客户端等。

基于功能的原因,该小程序原则上不可能通过审核,所以只会作为个人练手项目。

跨平台框架选型

关于跨平台框架的选型,对比了当前主要用的 3 个框架

  • mpvue: 美团开源的使用 Vue.js 开发小程序的前端框架,但当前好像好久没人维护了
  • uniapp: uni-app 是一个使用 Vue.js 开发小程序、H5、App 的统一前端框架
  • Taro-next: 京东开源的多端开发框架,2.X 版本是需使用 React 的,当前 beta3.X 版本对 vue 加入了支持。

对比了下,最终还是觉得选用 Taro-next 进行开发,虽然当前还是 beta 版,但是文档已经完善了,基本不影响使用。

安装与启动

Taro 安装参考官方文档,先全局安装 Taro 脚手架,再初始化一个项目,安装依赖。

使用npm run dev:weapp启动开发环境,然后使用微信开发者工具导入项目即可进行预览(最好申请一个小程序 Id,以便后续可以真机预览等功能)。

引入 Vant-weapp UI 框架

可以直接下载 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(可能也没权限),这次采用了微信提供的以下几种方式取代

  • wx.chooseImage: 从本地相册选择图片或使用相机拍照
  • wx.chooseVideo: 拍摄视频或从手机相册中选视频
  • wx.chooseMessageFile: 从客户端会话选择文件

这几个接口都会返回一个含有选取路径的成功回调,能拿到文件路径进行上传

upload-mode.jpg
upload-success.jpg


上传逻辑

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,所以基本不可能实现下载功能了。于是把下载功能改成了在线预览功能,当前支持以下几种文件类型

  • 办公文档(doc、docx、xls、xlsx、ppt、pptx、pdf):这几种文档都可以通过Taro.openDocumentAPI 实现预览操作
  • 图片(jpg、png、svg、gif):拿到图片临时地址后,使用浮层和image组件显示
  • 视频(mp4、mov、m4v、3gp、avi、m3u8):直接将视频地址放到浮层的video组件中显示

以上,除了视频外,其余都是要先通过wx.downloadFile将文件下载到本地生成临时路径再执行相应预览操作,这时候可以添加下载进度条优化用户体验。而视频不会直接进行下载,需要后端将视频资源设为流视频(Accept-Range),这样视频可以一边下载一边播放。

文件预览.gif

进度条使用微信原生请求事件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 实现,直接更换当前文件列表,但尽量保持节点位置没发生改变,可防止界面回流影响性能。

批量操作.gif

移动文件

该功能对应 PC 端上的移动,可以选择文件仅移动或者是复制,PC 端是使用一个树形组件进行选择文件夹。但发现 Vant 等并没有相关树形组件,可能需要自己封装一个,然后直接采用了用户点击移动操作后,记录选择的文件信息,然后用户需要进入到相应目录下进行粘贴操作。

移动文件.gif

关于打包发布

执行命令npm run build:weapp后,其余操作与正常小程序一样。

乳白
杏仁黄
茉莉黄
麦秆黄
油菜花黄
佛手黄
篾黄
葵扇黄
柠檬黄
金瓜黄
藤黄
酪黄
香水玫瑰黄
淡密黄
大豆黄
素馨黄
向日葵黄
雅梨黄
黄连黄
金盏黄
蛋壳黄
肉色
鹅掌黄
鸡蛋黄
鼬黄
榴萼黄
淡橘橙
枇杷黄
橙皮黄
北瓜黄
杏黄
雄黄
万寿菊黄
菊蕾白
秋葵黄
硫华黄
柚黄
芒果黄
蒿黄
姜黄
香蕉黄
草黄
新禾绿
月灰
淡灰绿
草灰绿
苔绿
碧螺春绿
燕羽灰
蟹壳灰
潭水绿
橄榄绿
蚌肉白
豆汁黄
淡茧黄
乳鸭黄
荔肉白
象牙黄
炒米黄
鹦鹉冠黄
木瓜黄
浅烙黄
莲子白
谷黄
栀子黄
芥黄
银鼠灰
尘灰
枯绿
鲛青
粽叶绿
灰绿
鹤灰
淡松烟
暗海水绿
棕榈绿
米色
淡肉色
麦芽糖黄
琥珀黄
甘草黄
初熟杏黄
浅驼色
沙石黄
虎皮黄
土黄
百灵鸟灰
山鸡黄
龟背黄
苍黄
莱阳梨黄
蜴蜊绿
松鼠灰
橄榄灰
蟹壳绿
古铜绿
焦茶绿
粉白
落英淡粉
瓜瓤粉
蜜黄
金叶黄
金莺黄
鹿角棕
凋叶棕
玳瑁黄
软木黄
风帆黄
桂皮淡棕
猴毛灰
山鸡褐
驼色
茶褐
古铜褐
荷花白
玫瑰粉
橘橙
美人焦橙
润红
淡桃红
海螺橙
桃红
颊红
淡罂粟红
晨曦红
蟹壳红
金莲花橙
草莓红
龙睛鱼红
蜻蜓红
大红
柿红
榴花红
银朱
朱红
鲑鱼红
金黄
鹿皮褐
醉瓜肉
麂棕
淡银灰
淡赭
槟榔综
银灰
海鸥灰
淡咖啡
岩石棕
芒果棕
石板灰
珠母灰
丁香棕
咖啡
筍皮棕
燕颔红
玉粉红
金驼
铁棕
蛛网灰
淡可可棕
中红灰
淡土黄
淡豆沙
椰壳棕
淡铁灰
中灰驼
淡栗棕
可可棕
柞叶棕
野蔷薇红
菠萝红
藕荷
陶瓷红
晓灰
余烬红
火砖红
火泥棕
绀红
橡树棕
海报灰
玫瑰灰
火山棕
豆沙
淡米粉
初桃粉红
介壳淡粉红
淡藏花红
瓜瓤红
芙蓉红
莓酱红
法螺红
落霞红
淡玫瑰灰
蟹蝥红
火岩棕
赭石
暗驼棕
酱棕
栗棕
洋水仙红
谷鞘红
苹果红
铁水红
桂红
极光红
粉红
舌红
曲红
红汞红
淡绯
无花果红
榴子红
胭脂红
合欢红
春梅红
香叶红
珊瑚红
萝卜红
淡茜红
艳红
淡菽红
鱼鳃红
樱桃红
淡蕊香红
石竹红
草茉莉红
茶花红
枸枢红
秋海棠红
丽春红
夕阳红
鹤顶红
鹅血石红
覆盆子红
貂紫
暗玉紫
栗紫
葡萄酱紫
牡丹粉红
山茶红
海棠红
玉红
高粱红
满江红
枣红
葡萄紫
酱紫
淡曙红
唐菖蒲红
鹅冠红
莓红
枫叶红
苋菜红
烟红
暗紫苑红
殷红
猪肝紫
金鱼紫
草珠红
淡绛红
品红
凤仙花红
粉团花红
夹竹桃红
榲桲红
姜红
莲瓣红
水红
报春红
月季红
豇豆红
霞光红
松叶牡丹红
喜蛋红
鼠鼻红
尖晶玉红
山黎豆红
锦葵红
鼠背灰
甘蔗紫
石竹紫
苍蝇灰
卵石紫
李紫
茄皮紫
吊钟花红
兔眼红
紫荆红
菜头紫
鹞冠紫
葡萄酒红
磨石紫
檀紫
火鹅紫
墨紫
晶红
扁豆花红
白芨红
嫩菱红
菠根红
酢酱草红
洋葱紫
海象紫
绀紫
古铜紫
石蕊红
芍药耕红
藏花红
初荷红
马鞭草紫
丁香淡紫
丹紫红
玫瑰红
淡牵牛紫
凤信紫
萝兰紫
玫瑰紫
藤萝紫
槿紫
蕈紫
桔梗紫
魏紫
芝兰紫
菱锰红
龙须红
蓟粉红
电气石红
樱草紫
芦穗灰
隐红灰
苋菜紫
芦灰
暮云灰
斑鸠灰
淡藤萝紫
淡青紫
青蛤壳紫
豆蔻紫
扁豆紫
芥花紫
青莲
芓紫
葛巾紫
牵牛紫
紫灰
龙睛鱼紫
荸荠紫
古鼎灰
乌梅紫
深牵牛紫
银白
芡食白
远山紫
淡蓝紫
山梗紫
螺甸紫
玛瑙灰
野菊紫
满天星紫
锌灰
野葡萄紫
剑锋紫
龙葵紫
暗龙胆紫
晶石紫
暗蓝紫
景泰蓝
尼罗蓝
远天蓝
星蓝
羽扇豆蓝
花青
睛蓝
虹蓝
湖水蓝
秋波蓝
涧石蓝
潮蓝
群青
霁青
碧青
宝石蓝
天蓝
柏林蓝
海青
钴蓝
鸢尾蓝
牵牛花蓝
飞燕草蓝
品蓝
银鱼白
安安蓝
鱼尾灰
鲸鱼灰
海参灰
沙鱼灰
钢蓝
云水蓝
晴山蓝
靛青
大理石灰
海涛蓝
蝶翅蓝
海军蓝
水牛灰
牛角灰
燕颔蓝
云峰白
井天蓝
云山蓝
釉蓝
鸥蓝
搪磁蓝
月影白
星灰
淡蓝灰
鷃蓝
嫩灰
战舰灰
瓦罐灰
青灰
鸽蓝
钢青
暗蓝
月白
海天蓝
清水蓝
瀑布蓝
蔚蓝
孔雀蓝
甸子蓝
石绿
竹篁绿
粉绿
美蝶绿
毛绿
蔻梢绿
麦苗绿
蛙绿
铜绿
竹绿
蓝绿
穹灰
翠蓝
胆矾蓝
樫鸟蓝
闪蓝
冰山蓝
虾壳青
晚波蓝
蜻蜓蓝
玉鈫蓝
垩灰
夏云灰
苍蓝
黄昏灰
灰蓝
深灰蓝
玉簪绿
青矾绿
草原远绿
梧枝绿
浪花绿
海王绿
亚丁绿
镍灰
明灰
淡绿灰
飞泉绿
狼烟灰
绿灰
苍绿
深海绿
长石灰
苷蓝绿
莽丛绿
淡翠绿
明绿
田园绿
翠绿
淡绿
葱绿
孔雀绿
艾绿
蟾绿
宫殿绿
松霜绿
蛋白石绿
薄荷绿
瓦松绿
荷叶绿
田螺绿
白屈菜绿
河豚灰
蒽油绿
槲寄生绿
云杉绿
嫩菊绿
艾背绿
嘉陵水绿
玉髓绿
鲜绿
宝石绿
海沬绿
姚黄
橄榄石绿
水绿
芦苇绿
槐花黄绿
苹果绿
芽绿
蝶黄
橄榄黄绿
鹦鹉绿
油绿
象牙白
汉白玉
雪白
鱼肚白
珍珠灰
浅灰
铅灰
中灰
瓦灰
夜灰
雁灰
深灰