原生小程序上传初探

994 阅读3分钟

背景

上传作为一种常见的前端交互场景,在小程序端也不例外。这里总结了一些原生微信小程序中使用vanweap组件进行上传业务开发的场景。

技术栈:原生微信小程序 + ant/weapp(1.10.23)

Uploader组件介绍

用于将本地的图片或文件上传至服务器,并在上传过程中展示预览图和上传进度。目前 Uploader 组件不包含将文件上传至服务器的接口逻辑(该步骤需要自行实现)。

引入

{
  "component": true,
  "usingComponents": {
    "van-cell-group": "@vant/weapp/cell-group/index",
    "van-uploader": "@vant/weapp/uploader/index",
	"van-loading": "@vant/weapp/loading/index"
  }
}

api文档参考

自定义一个组件

尽管vant的样式已经很好看了,但是还是不能满足UI的需求。假设UI的需求如下

image-20241013205333679

我们可以看到,上传前的样式还好处理,但是上传中和上传后,就不太符合要求了。在这里使用自定义上传标志位枚区分上传状态。而且通过max-count属性可以限制上传文件的数量,上传数量达到限制后,会自动隐藏上传区域,所以不需要关注全部上传完成之后如何处理上传组件。

isUploadSucc:{
 success: 1,
 uploading: 0
}

部分关键代码如下:

wxml

<!-- 上传组 -->
<van-cell-group custom-class="upload-cell-group no-margin-left">
    <view wx:for="{{ fileList }}" wx:key="keyId">
        <!-- 上传成功 -->
        <view wx:if="{{item.isUploadSucc === 1}}" class="upload-img-wrapper {{item.isDelted ? 'del__out' : ''}}">
            <image src="{{ item.ossUrl }}" mode="aspectFit" class="pic_choose" mode="aspectFit" data-index="{{ index }}" bind:tap="previewImage" />
            <view class="del-btn-wrapper ">
                 <!-- 删除按钮 -->
                <image src="/asserts/images/del-btn.png" class="del-btn {{item.isDelted ? 'del__show' : ''}}" data-index="{{ index }}" catchtap="delImgBefore" />
            </view>
        </view>
        <!-- 上传中 -->
        <view wx:elif="{{item.isUploadSucc === 0}}" class="upload-img-wrapper spinner-warpper">
            <van-loading type="spinner" size="20" vertical >加载中</van-loading>
        </view>
    </view>
    <van-uploader 
      max-count="5" 
      multiple 
      accept="image" 
      file-list="{{ fileList }}" 
      bind:after-read="afterRead" 
      preview-image="{{false}}" 
      custom-style="margin-left: 20rpx;" 
      deletable="{{ false }}"
      use-before-read
      bind:before-read="beforeRead" 
    >
        <view class="pic_choose">
            <image src="/asserts/images/pic_choose.png" ></image>
            <view class="upload-placeholder">上传图片</view>
        </view>
        <text slot="error">加载失败</text>
        
    </van-uploader>
</van-cell-group>

js

注意:这里有一个坑,wx会把gif格式的文件也当成图片,如果业务不允许上传动图的话,需要使用beforeRead钩子函数进行二次过滤。

// components/imgUploadGroup/imgUploadGroup.js
import { apiPost } from "../../apis/address";

Component({
  options: {
    styleIsolation: "shared",
  },
  properties: {},

  data: {
    fileList: [],
  },
  behaviors: ["wx://component-export"],
  export() {
    return { fileList: this.data.fileList };
  },
  methods: {
    // 前置钩子:校验上传文件类型 => 限制为图片
    beforeRead(event) {
      const { file, callback } = event.detail;
      const regex = /\.gif$/i;
      for (let i = 0; i < file.length; i++) {
        if (regex.test(file[i].url)) {
          callback(false);
          wx.showToast({
            title: "不支持上传动图类型的图片,请重新上传",
            icon: "none",
            duration: 2000,
          });
          return ;
        }
      }
      callback(true);
      console.log(file, callback);
    },
    // 上传钩子
    async afterRead(event) {
      const { file } = event.detail; // 是个数组
      // 当设置 mutiple 为 true 时, file 为数组格式,否则为对象格式
      // loading
      const _fileList = [...this.data.fileList];
      file.forEach((item) => {
        item.keyId = Math.random() * 10;
        item.isDelted = false;
        item.isUploadSucc = 0; // 开始上传
        _fileList.push(item);
      });
      this.setData({ fileList: _fileList }); // 视图更新为loading状态

      // 上传
      const files = file.map((item) => item.tempFilePath);
      const upLoadURLs = await this.multiUploadHandler(files);

      // 上传成功后,再次更新视图
      let uploadFileIndex = 0;
      _fileList.forEach((item, index) => {
        if (!item.ossUrl) {
          item.ossUrl = upLoadURLs[uploadFileIndex].value; // type: uploadType, value: imgUrl // 使用上传后的阿里云oss地址
          item.isUploadSucc = 1; // 更新标志位
          uploadFileIndex += 1;
        }
      });
      this.setData({ fileList: [..._fileList] });
    },
      
    uploadSingleFile(fileTmpUrl) {
      return new Promise((resolve, reject) => {
        wx.uploadFile({
          url: `${apiPost}/api-faq/auth/file/upload`,
          filePath: fileTmpUrl,
          name: "file",
          success(res) {
            const data = JSON.parse(res.data);
            if (data.code === 0) {
              resolve(data.data);
              return;
            }
            reject(new Error(data.msg));
          },
          fail(err) {
            reject(err);
          },
        });
      });
    },
	
    // 多文件上传,使用promise数组处理,这里不必等待所有成功,如果业务需要,可以在fileList中添加status为failed标识上传失败状态。
    multiUploadHandler(fileList) {
      return Promise.allSettled(
        fileList.map((fileUrl) => this.uploadSingleFile(fileUrl))
      );
    },

    // 删除图片
    delImgHandle(e) {
      const { index } = e.currentTarget.dataset;
      const _fileList = [...this.data.fileList];
      _fileList[index].isDelted = !_fileList[index].isDelted;
      this.setData({ fileList: _fileList });
      // 等待动画
      setTimeout(() => {
        const _fileList = [...this.data.fileList];
        _fileList.splice(index, 1);
        this.setData({ fileList: _fileList });
      }, 900);
    },

    // 删除前确认
    delImgBefore(e) {
      wx.showModal({
        title: "删除提示",
        content: "请确认是否删除该图片",
        success: (res) => {
          this.delImgHandle(e);
        },
      });
    },

    // 预览上传的图片
    previewImage(e) {
      const { index } = e.currentTarget.dataset;
      wx.previewImage({
        current: this.data.fileList[index].url, // 当前显示图片的http链接
        urls: this.data.fileList.map((item) => item.url), // 需要预览的图片http链接列表
      });
    },
  },
});

less

这里只记录删除动画

// 删除按钮
.del-btn-wrapper {
  width: 45rpx;
  height: 45rpx;
  position: absolute;
  top: 0;
  right: 0;
  transform: translate(50%, -50%);
  image {
    .img-fill()
  }
}
// 删除按钮的动效
.del-btn.del__show {
  animation: del__show__rotate 0.7s forwards ease-in-out;
}
@keyframes del__show__rotate {
  0% {
    transform-origin: 50% 50%;
    transform: rotate(0deg);
  }
  100% {
    transform-origin: 50% 50%;
    transform: rotate(180deg);
  }
}
// 上传的预览图隐藏
.del__out {
  transition: opacity 0.8s ease-in-out;
  opacity: 0;
}

操作展示

upload.gif

其他思考

微信小程序中,对于图片的操作,除了上传,还有就是在接入im功能的时候需要发送图片。微信自带的sdk很强大,可以很方便的对图片进行操作。

以下是在im功能(即时通讯)场景下对于图片的操作

sendImageMessage(sourceType) {
    const maxSize = 20480000
    wx.chooseMedia({
      mediaType: ['image'],
      sourceType: album,
      count: 1,
      success: (res) => {
        console.log(res)
        const size = res.tempFiles[0].size
        if (size > maxSize) {
          wx.showToast({
            title: '大于20M图片不支持发送',
            icon: 'none',
          })
          return
        }
        // 消息实例
        const message = wx.$TUIKit.createImageMessage({
          to: this.getToAccount(),
          conversationType: this.data.conversation.type,
          payload: {
            file: res,
          },
          onProgress: (percent) => {
            message.percent = percent
          },
        })
        this.$sendTIMMessage(message)
      },
      fail: (err) => {
        console.error(err)
      },
    })
  },
  
$sendTIMMessage(message) {
    // im 发送图片类型消息
    wx.$TUIKit
      .sendMessage(message, {
        offlinePushInfo: {
          disablePush: true,
        },
      })
      .then((res) => {
       // do something
      })
      .catch((error) => {
        // do something
      })
  },

最后

最近在搞im,第一次做这种类型的业务,做的是甲方(pc)乙方(小程序)和一个机器人做智能客服的群聊形式,之后有时间记录一下其中的坑。

参考资料

Vant-Weapp