微信小程序uploader上传文件并提交表单数据完整案例(接口框架WebAPI)

2023-10-27

写在前面的话

最近又自己在折腾微信小程序了。最新的一个功能中需要实现图片上传。幸运的是,微信小程序扩展能力中有现成的文件上传组件uploader可以使用,而不幸的是,这个组件坑实在太多了,而我又不是单纯的文件上传,还需要同步上传表单数据,因此各种坑,要么就是数据传不过去,要么就是后台取不到数据,折腾了我一天,各种尝试,终于搞定了。前后端完整用法记录一下,希望大家都能快速上手~

uploader介绍

uploader是微信小程序WeUI组件库中的一个图片上传的组件。大家可以在小程序开发文档中——扩展能力中找到相关用法。
在这里插入图片描述

这是一个集合了图片选择、上传、预览、删除的完整组件,属性定义也比较全面,可以自定义上传个数,有上传loading提醒和失败提醒,点击预览功能等,基本可以涵盖图片文件上传的所有功能要求。

用法也很简单,在json文件中加入引用后,在wxml文件中直接引入该组件就行,不需要跟自定义的那种文件上传一样,定义一堆标签和样式,方便多了。

官方文档有简单的使用案例:
1.在json中引入uploader组件
在这里插入图片描述2.在wxml中调用该组件,设置属性方法等

 <mp-uploader bindfail="uploadError" bindsuccess="uploadSuccess" select="{{selectFile}}" upload="{{uplaodFile}}" files="{{files}}" max-count="5" title="图片上传" tips="图片上传提示"></mp-uploader>

3.定义js中的上传方法

Page({
    data: {
        files: [{
            url: 'http://mmbiz.qpic.cn/mmbiz_png/VUIF3v9blLsicfV8ysC76e9fZzWgy8YJ2bQO58p43Lib8ncGXmuyibLY7O3hia8sWv25KCibQb7MbJW3Q7xibNzfRN7A/0',
        }, {
            loading: true
        }, {
            error: true
        }]
    },
    onLoad() {
        this.setData({
            selectFile: this.selectFile.bind(this),
            uplaodFile: this.uplaodFile.bind(this)
        })
    },

    selectFile(files) {
        console.log('files', files)
        // 返回false可以阻止某次文件上传
    },
    uplaodFile(files) {
        console.log('upload files', files)
        // 文件上传的函数,返回一个promise
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                reject('some error')
            }, 1000)
        })
    },
    uploadError(e) {
        console.log('upload error', e.detail)
    },
    uploadSuccess(e) {
        console.log('upload success', e.detail)
    }
});

这部分代码里面其实只需要补充uplaodFile上传方法调用后台上传图片的接口,上传功能就算完整了,这算是一个可用的完整Demo。但是实际使用起来,还是需要完善一下滴。废话不多说,直接上代码~

用法与代码

小程序前端

1.如前文所说,在json中引入组件,在页面调用

 <mp-uploader bindfail="uploadError" bindsuccess="uploadSuccess" select="{{selectFile}}" upload="{{uplaodFile}}" files="{{files}}" max-count="1" title=""></mp-uploader>

因为我只需要上传一张图片,因此设置max-count等于1。
在这里插入图片描述

2.在uplaodFile中,需要调用resolve({urls})方法设置上传成功状态,否则图片会如下图所示一直显示在加载中,体验很不友好,因此先“假装”已经上传成功,等之后提交表单时再真正上传到后台。
在这里插入图片描述
修改uplaodFile方法,调用resolve({urls})方法设置上传成功状态,保存临时文件目录tempFilePaths(后面会用到)

  uplaodFile(files) {
    console.log('upload files', files);
    var that = this;
    // 文件上传的函数,返回一个promise
    return new Promise((resolve, reject) => {
      const tempFilePaths = files.tempFilePaths;
      that.setData(
        {
          filesUrl: tempFilePaths
        }
      )
      var object = {};
      object['urls'] = tempFilePaths;
      resolve(object);
    })
  },

此时图片会正常显示:
在这里插入图片描述3.在表单提交方法中调用文件上传接口。
在微信小程序中,有一个wx.uploadFile的API方法,用来将本地资源上传到服务器,同时这个方法还能上传HTTP 请求中其他额外的 form data,刚好满足我的需求。
在通用的app.js文件中定义了uploadFile 方法,参数url为后台接口路径,filePath是本地图片路径,param则是需要上传的表单数据。

///上传单个文件
const uploadFile = (url, filePath,param) => {
  return new Promise((resolve, reject) => {
    wx.uploadFile({
      url: url, //仅为示例,非真实的接口地址
      filePath:filePath,
      name: 'file',
      formData: param,
      success (res){ //上传成功
        console.log(res)
        //成功调用接口
        resolve(JSON.parse(res.data));
      },
      fail(err){
        console.log(err)
        wx.showToast({ title: '请求失败,请刷新后重试', icon: 'none' });
        reject(err)
      }
    })
  })
}

在页面调用uploadFile方法

  submitForm: function () {
    this.selectComponent('#form').validate((valid, errors) => {
      if (!valid) {   //数据校验
        const firstError = Object.keys(errors)
        if (firstError.length) {
          this.setData({
            error: errors[firstError[0]].message
          })
        }
      } else {  //校验通过,保存
        var that = this;
        var url='/api/TestAPI/Add'; //后台接口地址
        var filePath=that.data.filesUrl[0];
        var formData={   //表单数据
          '_Name': that.data.formData._Name,
          '_Description': that.data.formData._Description,
          '_Type': that.data.formData._Type,
          '_IsVisible': that.data.formData._IsAllVisible.toString(), //Boolean类型
          '_Tips': JSON.stringify(that.data.formData._tips) //Array类型
        };
        api.uploadFile(url,filePath,formData).then((res) => {  //上图图片并保存表单
            if (res.Code == "Success") {
              wx.showToast({
                title: '添加成功'
              });
              wx.navigateBack({  //返回上一页
                delta: 1,
              })
            }
        })
        .catch((err) => {
          wx.showToast({
            title: '保存失败'
          })
            })
      }
    })
  },

大家看上面的代码,可以发现我将formData数据重新赋值了一次,并且做了一次类型转化。为什么要这么麻烦呢?直接一个formData扔过去不行吗?——答案是不行,采坑记录1,后面再细说。反正这么写之后图片和数据都可以传过去了,后台再接收就可以了。

后台接口 WebAPI

后台接口我采用的是WebAPI框架,可以自动生成基于RESTful标准的接口帮助文档,很方便使用。

WebApi的接口参数有两种形式,一种是基于url的[FromUri] ,另一种则是基于表单数据的[FromBody] 。在我之前不需要上传文件,直接通过post请求获取前端表单数据时,只要使用[FromBody]参数就可以直接将前端的Json对象转化成实体类,很简单易用。
在这里插入图片描述然而使用wx.uploadFile上传数据之后,不再能接收到[FromBody] 参数。因此接口方法也需要做些调整。代码如下:

        [HttpPost]
        [Route("Add")]
        public ApiResultModel Add()
        {
            ApiResultModel result = new ApiResultModel() { Code = APIReturnCode.Error.ToString(), Message = "添加失败" };
            var hole = new TreeHole();
            HttpContextBase context = (HttpContextBase)Request.Properties["MS_HttpContext"];//获取传统context
            HttpRequestBase request = context.Request;//定义传统request对象
            hole.Name = request.Form["_Name"];
            hole.Description = request.Form["_Description"];
            hole.Type = Guid.Parse(request.Form["_Type"]);
            hole.IsAllVisible = Boolean.Parse(request.Form["_IsAllVisible"]); //获得boolean类型数据
            var obk = request.Form["_tips"];   
            var tips = JsonConvert.DeserializeObject<JArray>(obk);//获得Array类型数据
            XTransaction tran = SessionManager.Instance.BeginTransaction();
            try
            {
                string url;
                bool isUploaded = uploadImage(out url);  //保存图片
                if (!isUploaded)
                {
                    result.Message = "上传图片失败";
                    return result;
                }
                hole.Id = Guid.NewGuid();
                hole.CreateTime = DateTime.Now;
                hole.IsDelete = false;
                hole.HeadImage = url;

                //保存代码(略)
                
                result.Message = "添加成功!";
                result.Code = APIReturnCode.Success.ToString();
                result.Token = hole.Id.ToString();
            }
            catch (Exception ex)
            {
                if (tran != null) tran.RollbackTransaction();
                result.Message = ex.Message;
            }
            finally
            {
                tran.Dispose();
            }
            return result;
        }

其中图片上传方法如下:

   /// <summary>
        /// 上传图片
        /// </summary>
        /// <param name="imageUrl"></param>
        /// <returns></returns>
    public bool uploadImage(out string picturePath)
        {
            picturePath = "";
            try
            {
                const string fileTypes = "gif,jpg,jpeg,png,bmp";//允许上传的图片文件格式
                var content = Request.Content;//获取或设置 HTTP 消息的内容(当需要获取HTTP信息是会使用到)
                const string tempUploadFiles = "/UploadFile/"; //保存路径
                var newFilePath = DateTime.Now.ToString("yyyy-MM-dd") + "/";
                var memoryStreamProvider = new MultipartMemoryStreamProvider();//获取文件流信息
                Task.Run(async () => await Request.Content.ReadAsMultipartAsync(memoryStreamProvider)).Wait(); //读取数据
                foreach (var item in memoryStreamProvider.Contents)
                {
                    if (item.Headers.ContentDisposition.FileName == null) continue; //判断数据类型
                    var filename = item.Headers.ContentDisposition.FileName.Replace("\"", "");//这里获取含有双引号'" ',需去掉
                    var file = new FileInfo(filename);
                    //upload(判断是否是运行上传的图片格式)
                    if (Array.IndexOf(fileTypes.Split(','), file.Extension.Substring(1).ToLower()) == -1)
                    {
                        return false;
                    }
                    //获取后缀
                    var extension = Path.GetExtension(filename);
                    var newFileName = Guid.NewGuid().ToString() + extension;//重命名
                    if (!Directory.Exists(HostingEnvironment.MapPath("/") + tempUploadFiles + newFilePath))
                    {
                        Directory.CreateDirectory(HostingEnvironment.MapPath("/") + tempUploadFiles + newFilePath);
                    }
                    var filePath = Path.Combine(HostingEnvironment.MapPath("/") + tempUploadFiles + newFilePath, newFileName);
                    picturePath = Path.Combine(tempUploadFiles + newFilePath, newFileName);//图片相对路径
                    var result = item.ReadAsStreamAsync().Result;
                    using (var br = new BinaryReader(result))
                    {
                        var data = br.ReadBytes((int)result.Length);
                        File.WriteAllBytes(filePath, data);//保存图片
                    }
                }
                //保存成功
                return true;
            }
            catch (Exception ex)
            {
                return false;
            }
        }

采坑记录

采坑记录一:使用wx.uploadFile中的formData传递表单数据时,Boolean类型、数组等无法传递到后台。
wx.uploadFile是客户端发起一个 HTTPS POST 请求,其中 content-type 为 multipart/form-data。按照之前的开发经验,直接将表单对象序列化,整个扔过去就行了。如下图所示:


然而小程序中没有serialize方法,直接传formData,在后台接收时会出现如下问题:
在这里插入图片描述

因此我只能采用最简单最原始的方法,将表单对象中无法传递的类型转成字符串的形式,后台接收到之后再转化一下。其中Boolean类型直接toString就好了,数组类型则需要使用JSON.stringify()转换:

'_IsVisible': that.data.formData._IsAllVisible.toString(),
'_Tips': JSON.stringify(that.data.formData._tips)

采坑记录二:WebAPI接口 MultipartFormDataStreamProvider.FormData内容为空
因为wx.uploadFile的 content-type 为 multipart/form-data类型为空,所以接口中需要通过MultipartMemoryStreamProvider来获取数据。我在网上寻找参考案例时,发现很多都是如下写法:

   string root = HttpContext.Current.Server.MapPath("~/upload/TreeHole");
            if (!System.IO.File.Exists(root))
            {
                Directory.CreateDirectory(root);
            }
                var provider = new MultipartFormDataStreamProvider(root);
   foreach (var key in provider.FormData.AllKeys)
   {
      foreach (var val in provider.FormData.GetValues(key))
      {
        str += string.Format("{0}: {1}", key, val);
      }
   }

通过遍历MultipartFormDataStreamProvider中的FormData键值来获取数据。然后我在实际操作过程中,发现这样根本就去不到数据,FormData为Null! 我不知道是传值还是哪里出了问题,最后还是通过Request来获取值。

   HttpContextBase context = (HttpContextBase)Request.Properties["MS_HttpContext"];//获取传统context
   HttpRequestBase request = context.Request;//定义传统request对象
   hole.Name = request.Form["_Name"];
   hole.Description = request.Form["_Description"];
   hole.Type = Guid.Parse(request.Form["_Type"]);
   hole.IsAllVisible = Boolean.Parse(request.Form["_IsAllVisible"]); //获得boolean类型数据
   var obk = request.Form["_tips"];   
   var tips = JsonConvert.DeserializeObject<JArray>(obk);
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

微信小程序uploader上传文件并提交表单数据完整案例(接口框架WebAPI) 的相关文章

  • 如何在MVVM中管理多个窗口

    我知道有几个与此类似的问题 但我还没有找到明确的答案 我正在尝试深入研究 MVVM 并尽可能保持纯粹 但不确定如何在坚持模式的同时启动 关闭窗口 我最初的想法是向 ViewModel 发送数据绑定命令 触发代码来启动一个新视图 然后通过 X
  • 将复选框添加到 UniformGrid

    我正在尝试将复选框动态添加到 wpf 中的统一网格中 但看起来网格没有为它们分配足够的空间 所以它们都有点互相重叠 这就是我将它们添加到后面的代码中的方法 foreach string folder in subfolders PathCh
  • 检查两个数是否是彼此的排列?

    给定两个数字 a b 使得 1 例如 123 是 312 的有效排列 我也不想对数字中的数字进行排序 如果您指的是数字的字符 例如 1927 和 9721 则 至少 有几种方法 如果允许排序 一种方法是简单地sprintf将它们放入两个缓冲
  • 无法使用已与其底层 RCW 分离的 COM 对象。在 oledb 中

    我收到此错误 但我不知道我做错了什么 下面的代码在backrgroundworker中 将异常详细信息复制到剪贴板 System Runtime InteropServices InvalidComObjectException 未处理 通
  • 将数组向左或向右旋转一定数量的位置,复杂度为 o(n)

    我想编写一个程序 根据用户的输入 正 gt 负 include
  • 从父类调用子类方法

    a doStuff 方法是否可以在不编辑 A 类的情况下打印 B did stuff 如果是这样 我该怎么做 class Program static void Main string args A a new A B b new B a
  • linux perf:如何解释和查找热点

    我尝试了linux perf https perf wiki kernel org index php Main Page今天很实用 但在解释其结果时遇到了困难 我习惯了 valgrind 的 callgrind 这当然是与基于采样的 pe
  • 如何忽略“有符号和无符号整数表达式之间的比较”?

    谁能告诉我必须使用哪个标志才能使 gcc 忽略 有符号和无符号整数表达式之间的比较 警告消息 gcc Wno sign compare 但你确实应该修复它警告你的比较
  • 在 Visual Studio 2008 上设置预调试事件

    我想在 Visual Studio 中开始调试程序之前运行一个任务 我每次调试程序时都需要运行此任务 因此构建后事件还不够好 我查看了设置的 调试 选项卡 但没有这样的选项 有什么办法可以做到这一点吗 你唯一可以尝试的 IMO 就是尝试Co
  • C - 找到极限之间的所有友好数字

    首先是定义 一对友好的数字由两个不同的整数组成 其中 第一个整数的除数之和等于第二个整数 并且 第二个整数的除数之和等于第一个整数 完美数是等于其自身约数之和的数 我想做的是制作一个程序 询问用户一个下限和一个上限 然后向他 她提供这两个限
  • C#:如何防止主窗体过早显示

    在我的 main 方法中 我像往常一样启动主窗体 Application EnableVisualStyles Application SetCompatibleTextRenderingDefault false Application
  • 将目录压缩为单个文件的方法有哪些

    不知道怎么问 所以我会解释一下情况 我需要存储一些压缩文件 最初的想法是创建一个文件夹并存储所需数量的压缩文件 并创建一个文件来保存有关每个压缩文件的数据 但是 我不被允许创建许多文件 只能有一个 我决定创建一个压缩文件 其中包含有关进一步
  • Cython 和类的构造函数

    我对 Cython 使用默认构造函数有疑问 我的 C 类 Node 如下 Node h class Node public Node std cerr lt lt calling no arg constructor lt lt std e
  • vector 超出范围后不清除内存

    我遇到了以下问题 我不确定我是否错了或者它是一个非常奇怪的错误 我填充了一个巨大的字符串数组 并希望在某个点将其清除 这是一个最小的例子 include
  • Qt表格小部件,删除行的按钮

    我有一个 QTableWidget 对于所有行 我将一列的 setCellWidget 设置为按钮 我想将此按钮连接到删除该行的函数 我尝试了这段代码 它不起作用 因为如果我只是单击按钮 我不会将当前行设置为按钮的行 ui gt table
  • C++ 复制初始化和直接初始化,奇怪的情况

    在继续阅读本文之前 请阅读在 C 中 复制初始化和直接初始化之间有区别吗 https stackoverflow com questions 1051379 is there a difference in c between copy i
  • 插入记录后如何从SQL Server获取Identity值

    我在数据库中添加一条记录identity价值 我想在插入后获取身份值 我不想通过存储过程来做到这一点 这是我的代码 SQLString INSERT INTO myTable SQLString Cal1 Cal2 Cal3 Cal4 SQ
  • 需要哪个版本的 Visual C++ 运行时库?

    microsoft 的最新 vcredist 2010 版 是否包含以前的版本 2008 SP1 和 2005 SP1 还是我需要安装全部 3 个版本 谢谢 你需要所有这些
  • x86 上未对齐的指针

    有人可以提供一个示例 将指针从一种类型转换为另一种类型由于未对齐而失败吗 在评论中这个答案 https stackoverflow com questions 544928 reading integer size bytes from a
  • 防止索引超出范围错误

    我想编写对某些条件的检查 而不必使用 try catch 并且我想避免出现 Index Out of Range 错误的可能性 if array Element 0 Object Length gt 0 array Element 1 Ob

随机推荐

  • supervisor托管配置nginx

    前言 阅读本文档前 请先了解如何安装配置supervisor和nginx 以下是相关学习文档 超全面 CentOS7 安装及配置supervisor CentOS 安装及配置nginx 配置 1 创建supervisor托管配置文件 详细如
  • oracle导库报959,IMP-00003: 遇到 ORACLE 错误 959

    导入前先要建好表空间和用户 建议你导出的时候按用户导出 不要用sys全部导出来还有在导入的时候需要指定导入到哪个用中去 给个操作手顺吧 我今天刚弄完的 1 导出 exp user user dbname owner user file pa
  • opencv之初学

    浅浅地记录一下自己学习opencv的过程吧 我有想毕业之后从事图像处理方面的工作 所以就从现在学起 争取明年秋招时能拿到offer吧 1 下载opencv opencv有很多的版本 我大概在网上搜了一下它的下载过程 需要在Visual St
  • UE4中文本文件配置文件Json文件XML文件的读写

    虚幻引擎中提供了与平台无关的文件读写与访问接口 通过调用 可以完成一些文件的读写 比如文本文件 配置文件 json文件 xml文件等 完成文件读写 首先需要获取文件路径等相关信息 对调用这些操作 我们需要包含头文件PlatformFilem
  • stm32水质检测系统(TDS检测,水温检测,PH检测,wifi上传,上位机显示)

    一 硬件材料清单 1 STM32核心板 2 OLED显示屏 3 PH传感器 4 TDS传感器 5 DS18B02水温传感器 6 ESP8266 二 实现的功能 1 数据的实时检测 2 本地OLED数据实时刷新 3 远程终端上位机数据显示刷新
  • Flask系列 路由系统

    Flask路由系统细分 from flask import Flask app Flask name app route def index return ok if name main app run 从这个简单的代码入口 来剖析一下路由
  • SpringIOC和AOP概念原理

    springIOC概念和原理 控制反转 把对象创建和对象之间的调用过程 交给Spring进行管理 使用IOC目的 为了耦合度降低 IOC思想是基于IOC容器完成 IOC容器底层就是对象工厂 Spring提供了IOC容器2中实现方式 俩个接口
  • 利用强化学习进行股票操作实战(一)

    利用强化学习进行股票操作实战 今天开始利用强化学习实现股票操作 我在网上找了一个简单的强化学习进行股票操作的例子 并在此基础上进行了小改动 首先讲下建模的思路 当模型发出买入指令时 我们一次性全部买入 当模型发出卖出指令时则一次性全部卖出
  • Swift 中 10 个震惊小伙伴的单行代码

    作者 uraimo 原文链接 原文日期 2016 01 06译者 bestswifter 校对 numbbbbb 定稿 小锅 几年前 函数式编程的复兴正值巅峰 一篇介绍 Scala 中 10 个单行函数式代码的博文在网上走红 很快地 一系列
  • qnx 设备驱动开发_2021年起奥迪车将换装Linux系统 此前为QNX

    车东西5月21日消息 外媒Forbes报道 奥迪官方宣布到2021年 会对旗下多款车型的信息娱乐系统进行升级换代 此前 奥迪旗下车型的信息娱乐系统基于QNX研发而来 未来将更换为Linux系统 升级后的奥迪信息娱乐系统 最大的亮点在于增加的
  • xcode,ios单元测试网络请求 AFNetworking 无法引入

    单元测试引入AFNetworking 同需要在 Podfile 引入 platform ios 7 0 target MyDemoTests do pod AFNetworking gt 2 5 0 end 否则无法引入
  • C++11中enum class的使用

    枚举类型 enumeration 使我们可以将一组整型常量组织在一起 和类一样 每个枚举类型定义了一种新的类型 枚举属于字面值常量类型 C 包含两种枚举 限定作用域的和不限定作用域的 这里主要介绍限定作用域的 不限定作用域的使用可以参考 h
  • [亲测有效]QT生成项目时候,右下角显示红色构建进度条,但是不报错,且无法生成UI界面 的解决方法。

    最近用QT5 9时候 发现生成项目莫名的慢 即使是生成过的项目也要十几秒钟才能弹出UI界面 于是我就想换一个版本用一下 于是我选择了QT 5 6 1版本 但是我发现完成项目后 点击左下角的运行按钮 右下角显示红色进度条的构建过程 更为诡异的
  • OpenLayers绘制图形

    OpenLayers的显示构成由外向内为 ol Map 地图对象 ol layer Vector 图层对象layer Map含有多个layer 最终的显示效果是由多个layer叠加而成 ol source Vector和ol style S
  • FreeRTOS系列

    1 多任务系统 1 1 前后台系统 单片机裸机开发时 一般都是在main函数里面用while 1 做一个大循环来完成所有的处理 循环中调用相应的函数完成所需的处理 有时也需要在中断中完成一些处理 相对于多任务系统而言 这就是单人单任务系统也
  • 银河麒麟V10 + 飞腾D2000(ARM64) 安装Qt

    近期有个需求是在一个特定的硬件和系统组合下开发和发布软件 具体配置是 操作系统 银河麒麟V10桌面版 CPU 飞腾D2000 ARM64 折腾了很长时间 综合了多个网络资料 最终把Qt5装好了 记录如下 Qt版本选择5 9 9 一开始选择了
  • 推送服务本地通知频次及分类管控通知

    尊敬的华为开发者 为了给用户提供更好的消息通知体验 营造清朗网络空间 从2023年9月15日开始 华为推送服务将基于 华为消息分类标准 对本地通知进行灰度管控 主要包括对应用发送的本地通知进行分类管理 以及对资讯营销消息统一进行频次管控 注
  • kibana启动失败:server is not ready yet

    kibana启动失败 server is not ready yet 这篇文章主要是解决黑马项目 学成在线 的p106中的kibana无法正常启动 首先我们在虚拟机上查看kibana启动日志 docker logs f kibana 发现报
  • Java基础回顾 : Runtime类和System类

    1 Runtime 类的使用 Runtime 类的定义特点 Runtime类的介绍 Runtime 指的是运行时 当每一个JVM 进程启动的时候 都会存在有一个Runtime 类的实例化对象 它是随着JVM 的存在而存在的 通过查看APi可
  • 微信小程序uploader上传文件并提交表单数据完整案例(接口框架WebAPI)

    文章目录 写在前面的话 uploader介绍 用法与代码 小程序前端 后台接口 WebAPI 采坑记录 写在前面的话 最近又自己在折腾微信小程序了 最新的一个功能中需要实现图片上传 幸运的是 微信小程序扩展能力中有现成的文件上传组件uplo