微信小程序之图片选择、预览与上传

2018-08-21 10:24:00 浏览数 (2)

所谓:一图胜千言。这话说明了图片描述事物的能力是非常强大的(怪不得我们可以用表情包聊一整天),尤其现在的手机拍照功能那么方便,用户对使用拍照和相册的需求日益上升。因此,在我们的移动应用中,可能经常会碰到这样的功能需求,需要为用户提供在相册中选择照片或者拍照片并上传的功能。

例如下图所示的应用界面,这是一个比较典型的创建帖子或问答等内容的表单,用户可以填写标题和正文,并从自己的手机相册中选择3张图片(或直接通过摄像头拍摄),且当点击缩略图时,可以全屏预览查看这些图片:

像这样一个带图片上传和预览功能的表单,在移动app中是比较常见的。那么在微信小程序中该如何来实现呢?且看我们一步步来构建这样的功能。

标题和正文输入框

对于这个表单,我们首先来创建上部的2个输入区域:标题和正文输入区。我们使用了一个单行输入框组件<input>来接收标题的输入,而使用一个多行输入组件<textarea>来接收正文的输入,并且为它们分别设置了maxlength属性来作最大输入字符数的限制。然后,为了更加直观,我们还为这2个输入区域分别放置了一个展示当前已输入字符数统计状态的标签。

界面的WXML代码大致如下所示:

代码语言:javascript复制
<view class="question-input-area">

  <!-- 问题标题区域  -->
  <view class="question-title-wrap">
    <!-- 标题输入框 -->
    <input class="question-title" placeholder="请输入标题" maxlength="40" placeholder-style="color:#b3b3b3;font-size:18px;" bindinput="handleTitleInput"></input>
    <!-- 标题输入字数统计 -->
    <view class="title-input-counter">{{titleCount}}/40</view>
  </view>

  <!-- 问题正文区域  -->
  <view class="weui-cells weui-cells_after-title">
    <view class="weui-cell">
      <view class="weui-cell__bd">
        <!-- 多行输入框 -->
        <textarea class="weui-textarea" placeholder="请输入问题的正文内容。" maxlength="500" placeholder-style="color:#b3b3b3;font-size:14px;" style="height: 12rem" bindinput="handleContentInput" />
        <!-- 正文输入字数统计 -->
        <view class="weui-textarea-counter">{{contentCount}}/500</view>
      </view>
    </view>
  </view>

</view>

而与之对应的Page代码如下:

代码语言:javascript复制
import { $init, $digest } from '../../utils/common.util'

Page({

  data: {
    titleCount: 0, //标题字数
    contentCount: 0, //正文字数
    title: '', //标题内容
    content: '' //正文内容
  },

  onLoad(options) {
    $init(this)
  },

  handleTitleInput(e) {
    const value = e.detail.value
    this.data.title = value
    this.data.titleCount = value.length  //计算已输入的标题字数
    $digest(this)
  },

  handleContentInput(e) {
    const value = e.detail.value
    this.data.content = value
    this.data.contentCount = value.length  //计算已输入的正文字数
    $digest(this)
  }
}

【注意】有人可能会对这里的一些代码觉得奇怪,这段JavaScript代码中出现的$init和$digest是什么?其实它是一个通过对象深层比较,将Page的data对象中的数据进行批量、按需更新到视图层WXML中的一个功能。对初学者来说,你暂且可以认为是在每个调用$digest(this)的地方调用了一次this.setData()的操作吧,方便理解。

通过上面的两段代码,我们就已经把表单的输入框部分创建出来了。下面,我们要进入本文的关键功能部分。

选择和预览图片、以及上传图片

微信小程序提供的众多API中,wx.chooseImage函数就是用来访问手机相册或摄像头的。调用该函数后,界面下方会呼出一个菜单,可以分别选择进入相册挑选已有照片或是打开摄像头进行拍照:

二话不说继续上代码!我们往WXML里新添一个按钮,点击该按钮就会触发wx.chooseImage的调用:

代码语言:javascript复制
<button
    type="default" size="mini" bindtap="chooseImage" 
    wx:if="{{images.length < 3}}"
>添加图片</button>
代码语言:javascript复制
import { $init, $digest } from '../../utils/common.util'

Page({

  data: {
    images: []
  },

  onLoad(options) {
    $init(this)
  },

  chooseImage(e) {
    wx.chooseImage({
      sizeType: ['original', 'compressed'],  //可选择原图或压缩后的图片
      sourceType: ['album', 'camera'], //可选择性开放访问相册、相机
      success: res => {
        const images = this.data.images.concat(res.tempFilePaths)
        // 限制最多只能留下3张照片
        this.data.images = images.length <= 3 ? images : images.slice(0, 3) 
        $digest(this)
      }
    })
  }
}

通过以上代码,我们就可以开始把玩起手机相册和摄像头了。但是目前选择了照片或拍了照之后,在表单界面上并不能看到。下面我们就要继续做选择图片后的展示工作。

我们通过wx:for语法,将我们之前存在images数组中的照片展示到界面上来:

代码语言:javascript复制
<view class="question-images">
  <block wx:for="{{images}}" wx:key="*this">
    <view class="q-image-wrap">
      <!-- 图片缩略图  -->
      <image class="q-image" src="{{item}}" mode="aspectFill" data-idx="{{index}}" bindtap="handleImagePreview"></image>
      <!-- 移除图片的按钮  -->
      <view class="q-image-remover" data-idx="{{index}}" bindtap="removeImage">删除</view>
    </view>
  </block>
</view>

我们在每个缩略图元素上绑定了一个点击事件,当点击缩略图的时候,会调用微信小程序提供的预览图片的方法wx.previewImage进行全屏预览,用户可以左右滑动查看选中图片列表中的大图。另外,在每个缩略图的下方,还有一个删除按钮,用于移除所选的图片,方便重新选图。下面是对应的JS代码:

代码语言:javascript复制
import { $init, $digest } from '../../utils/common.util'

Page({

  data: {
    images: []
  },

  onLoad(options) {
    $init(this)
  },

  removeImage(e) {
    const idx = e.target.dataset.idx
    this.data.images.splice(idx, 1)
    $digest(this)
  },

  handleImagePreview(e) {
    const idx = e.target.dataset.idx
    const images = this.data.images
    wx.previewImage({
      current: images[idx],  //当前预览的图片
      urls: images,  //所有要预览的图片
    })
  }
}

终于,只剩下最后一件事,就是提交表单数据及上传图片到后端,将的这些数据组成一个完整的问题,保存进数据库。

对于我们的WXML,还缺最后这个提交按钮呢!立马补上吧:

代码语言:javascript复制
<!-- 提交表单按钮  -->
<button class="weui-btn" type="primary" bindtap="submitForm">提交</button>

然后就是这Page中的集大成者(大杂烩吧,哈哈)submitForm函数:

代码语言:javascript复制
import { $init, $digest } from '../../utils/common.util'

Page({

  data: {
    images: []
  },

  onLoad(options) {
    $init(this)
  },

  submitForm(e) {
    const title = this.data.title
    const content = this.data.content

    if (title && content) {
      const arr = []

      //将选择的图片组成一个Promise数组,准备进行并行上传
      for (let path of this.data.images) {
        arr.push(wxUploadFile({
          url: config.urls.question   '/image/upload',
          filePath: path,
          name: 'qimg',
        }))
      }

      wx.showLoading({
        title: '正在创建...',
        mask: true
      })

      // 开始并行上传图片
      Promise.all(arr).then(res => {
        // 上传成功,获取这些图片在服务器上的地址,组成一个数组
        return res.map(item => JSON.parse(item.data).url)
      }).catch(err => {
        console.log(">>>> upload images error:", err)
      }).then(urls => {
        // 调用保存问题的后端接口
        return createQuestion({
          title: title,
          content: content,
          images: urls
        })
      }).then(res => {
        // 保存问题成功,返回上一页(通常是一个问题列表页)
        const pages = getCurrentPages();
        const currPage = pages[pages.length - 1];
        const prevPage = pages[pages.length - 2];

        // 将新创建的问题,添加到前一页(问题列表页)第一行
        prevPage.data.questions.unshift(res)
        $digest(prevPage)

        wx.navigateBack()
      }).catch(err => {
        console.log(">>>> create question error:", err)
      }).then(() => {
        wx.hideLoading()
      })
    }
  }
}

这个提交保存函数的主要流程是:

  1. 将图片分别通过文件上传APIwx.uploadFile进行上传,并返回上传后的图片地址备用;
  2. 接着将标题、正文、以及刚才的图片地址一并通过调用后端创建问题的API,保存到数据库中。
  3. 保存完毕,返回问题列表页

在我的这个实现代码中,是将上传文件和创建问题分别通过2个后端API来进行的,其实wx.uploadFile除了上传文件,同时也可以携带其他表单数据,这样一来,就可以用单一API来实现。具体选择哪一种方式,主要看你们实际的后端API的设计了。

最后,附上比较完整的源代码供大家参考吧。

  • 点击下载本文完整示例代码
  • 点击下载本文出现的$init, $digest等工具函数

0 人点赞