微信小程序

使用小程序·云开发构建多媒体小程序

2018-11-03  本文已影响175人  wch853

小程序·云开发

什么是小程序的云开发?一句话就是能够使开发者省去搭建服务器、申请域名的成本,从开发到运维提供整套解决方案的小程序开发方式。
官方定义是,小程序·云开发为开发者提供完整的云端支持,弱化后端和运维概念,无需搭建服务器,使用平台提供的 API 进行核心业务开发,即可实现快速上线和迭代。
相对于传统小程序开发,云开发新提供了三大基础支持:

由此开发者只需在小程序端调用API即可搭建简单的后端服务,无需考虑搭建服务器、申请合法域名带来的成本。云开发环境免费版对资源有一定限制,但在用户量不大的情况下可以提供稳定的服务,相对于真正的后端服务,云开发环境还集成了控制台,提供了运维的有效手段。

云开发·多媒体服务

初始化小程序·云开发

注册小程序

登录微信公众平台,使用邮箱注册并激活小程序。进入小程序管理后台首页填写小程序信息,并添加开发者,详情见官方文档

创建小程序项目

建立云开发环境

  wx.cloud.init({
    traceUser: true,
    env: 'test-xxx'
  })

云开发环境

云开发提供了一整套云服务及简单、易用的 API 和管理界面,以尽可能降低后端开发成本,让开发者能够专注于核心业务逻辑的开发、尽可能轻松的完成后端的操作和管理。这套云环境包括数据库、存储和云函数。

云开发控制台

云开发控制台提供了云开发的管理界面和运维工具,开发者可以通过操作控制台来查看环境使用情况、操作数据库、存储、管理云函数等。


云开发控制台

数据库

云开发环境提供一个文档数据库,其API和功能类似于MongoDB。进入控制台的数据库选项,新建一个集合。集合中每条数据都是JSON格式的。

数据库管理
// 云db对象
const db = wx.cloud.database()

module.exports = {

  /**
   * 新增记录
   *
   * @param data 数据
   * @param collection 集合
   * @return {"_id": String, "errMsg": String}
   */
  add: function(data, collection) {
    return db.collection(collection).add({
      data: data
    })
  },

  /**
   * 查询记录
   * 
   * @param collection 集合
   * @param where 查询条件
   * @param skip 查询起始位置
   * @param limit 查询数量
   * @return {"data": Array, "errMsg": String}
   */
  query: function(collection, where, skip, limit) {
    where = where || {}
    skip = skip || 0
    limit = limit || 10
    return db.collection(collection)
      .where(where).orderBy('time', 'desc')
      .skip(skip).limit(limit).get()
  },

  /**
   * 查询记录数量
   *
   * @param collection 集合
   * @param where 查询条件
   * @return {"total": Number, "errMsg": String}
   */
  count: function(collection, where) {
    where = where || {}
    return db.collection(collection).where(where).count()
  },

  /**
   * 新增/全部更新文档
   *
   * @param collection 集合
   * @param doc 文档_id
   * @param data 数据
   * @return {"_id": String, "errMsg": String}
   */
  addDoc: function(collection, doc, data) {
    collection = collection || lovc
    return db.collection(collection).doc(doc).set({
      data: data
    })
  },

  /**
   * 查询文档
   *
   * @param collection 集合
   * @param doc 文档_id
   * @return {"data": Object, "errMsg": String}
   */
  getDoc: function(collection, doc) {
    collection = collection || lovc
    return db.collection(collection).doc(doc).get()
  },

  /**
   * 部分更新文档
   * 
   * @param collection 集合
   * @param doc 文档_id   
   * @param data 数据
   * @return {"stats": Object, "errMsg": String}
   */
  update: function(collection, doc, data) {
    collection = collection || lovc
    return db.collection(collection).doc(doc).update({
      data: data
    })
  },

  /**
   * 删除文档
   *
   * @param collection 集合
   * @param doc 文档_id
   * @return {"stats": Object, "errMsg": String}
   */
  remove: function(collection, doc) {
    collection = collection || lovc
    return db.collection(collection).doc(doc).remove()
  }
}

存储

云环境提供了免费的5G云存储空间,文件上传后每个文件会生成一个 fileID 和一个 https 下载地址,在小程序中,srcposter 等属性中都可以直接使用。

存储管理
module.exports = {

  /**
   * 上传文件
   * 
   * @param fileName 文件名
   * @param filePath 文件路径
   * @return {"errMsg": String, "fileID": String, "statusCode": Number}
   */
  upload: function(fileName, filePath) {
    return wx.cloud.uploadFile({
      cloudPath: fileName,
      filePath: filePath
    })
  },

  /**
   * 下载文件
   * 
   * @param fileID 文件名
   * @return {"tempFilePath": String, "statusCode": Number}
   */
  download: function(fileID) {
    return wx.cloud.downloadFile({
      fileID: fileID
    })
  }
}

云函数

云函数即在云端(服务器端)运行的函数。通过使用云函数,开发者无需购买、搭建服务器,只需编写函数代码并部署到云端即可在小程序端调用,同时云函数之间也可互相调用。云函数本身是一个本地定义的JS方法,上传并部署到指定云环境中,运行在云端的 NodeJS 中。云函数调用时用户的 openid 会作为请求参数进行调用,从而能够与微信鉴权无缝结合。

创建云函数
// 云函数入口文件
const cloud = require('wx-server-sdk')
const rp = require('request-promise')

cloud.init()

// 云函数入口函数
exports.main = async(event, context) => {
  if (event.method === 'GET') {
    return get(event.url, event.data)
  } else if (event.method === 'POST') {
    return post(event.url, event.data)
  }
}

/**
 * GET请求
 * 
 * @param url REST服务地址
 * @param data 请求参数
 */
function get(url, data) {
  if (undefined !== data && '' !== data) {
    let queryString = '?'
    for (let key in data) {
      if (!queryString.endsWith('?')) {
        queryString = queryString.concat('&')
      }
      queryString = queryString.concat(key).concat('=').concat(data[key])
    }
    url = url.concat(queryString)
  }
  return rp(url)
}

/**
 * POST请求
 * 
 * @param url REST服务地址
 * @param data 请求参数
 */
function post(url, data) {
  return rp({
    uri: url,
    method: 'POST',
    body: data,
    json: true
  })
}
// 云函数入口文件
const cloud = require('wx-server-sdk')
const TcbRouter = require('tcb-router')

cloud.init()

// 云函数入口函数
exports.main = async(event, context) => {
  const app = new TcbRouter({
    event
  });

  // app.use 表示该中间件会适用于所有的路由
  app.use(async(ctx, next) => {
    // 创建返回data对象
    ctx.data = {}
    // 执行下一中间件
    await next()
  })

  // 路由为数组表示,该中间件适用于多个路由
  // app.router(['x', 'y'], async (ctx, next) => {
  //   ctx.data.from = 'cloud';
  //   await next();
  // });

  app.router('router', async(ctx, next) => {
    ctx.data.openId = event.userInfo.openId
    await next();
  }, async(ctx) => {
    ctx.data.other = event.other;
    // ctx.body 返回数据到小程序端
    ctx.body = {
      code: 0,
      data: ctx.data
    }
  })

  return app.serve()
}
module.exports = {

  /**
   * 通过云函数访问服务
   * 
   * @param name 云函数名
   * @param params 参数
   * @return {"errMsg": String, "result": Object, "requestID": String}
   */
  call: function(name, params) {
    return wx.cloud.callFunction({
      name: name,
      data: params
    })
  },

  /**
   * 通过tcb-router访问服务
   *
   * @param url router路径
   * @param params 参数
   * @return {"errMsg": String, "result": String, "requestID": String}
   */
  tcbRouter: function(url, params) {
    params.$url = url
    return this.call('router', params)
  },

  /**
   * 云函数-REST访问服务
   *
   * @param url REST服务地址
   * @param 请求方法
   * @param params 请求参数
   * @return {"errMsg": String, "result": String, "requestID": String}
   */
  rest: function(url, method, params) {
    return this.call('rest', {
      url: url,
      method: method,
      data: params
    })
  }
}

开发多媒体服务

引入iView Webapp

引入iView Webapp作为小程序的前端框架。

# clone iView Weapp
git clone https://github.com/TalkingData/iview-weapp.git
cd iview-weapp
# 安装依赖
npm install
# 编译组件,便于打开到模拟器查看
npm run dev

封装媒体API

与云开发原生支持 Promise 不同,原生小程序API只能通过回调获取返回值,对于项目中使用的相关小程序的媒体API进行封装,避免多层回调。

module.exports = {

  /**
   * 选择照片
   * 
   * @return {"errMsg": String, "tempFilePaths": Array(String), "tempFiles": Array(Obejct)}
   */
  chooseImage: function() {
    return new Promise(function(resolve) {
      wx.chooseImage({
        count: 9,
        sizeType: ['original', 'compressed'],
        sourceType: ['album'],
        success: res => {
          resolve(res)
        }
      })
    })
  },

  /**
   * 全屏预览照片
   * 
   * @param current 当前显示图片的链接
   * @param urls 需要预览的图片链接列表(云文件ID @since 2.2.3)
   */
  previewImage: function(current, urls) {
    return new Promise(function(resolve, reject) {
      wx.previewImage({
        current: current,
        urls: urls,
        success: res => {
          resolve(res)
        },
        fail: err => {
          reject(err)
        }
      })
    })
  },

  /**
   * 选择视频
   * 
   * @return {"errMsg": String, "tempFilePath": String, "thumbTempFilePath": String, "duration": Number, "width": Number, "height": Number, "size": Number}
   */
  chooseVideo: function() {
    return new Promise(function(resolve, reject) {
      wx.chooseVideo({
        sourceType: ['album'],
        compressed: true,
        maxDuration: 60,
        success: res => {
          resolve(res)
        },
        fail: err => {
          reject(err)
        }
      })
    })
  },

  /**
   * 保存video到本地
   * 
   * @param filePath 文件路径
   * @return {"errMsg": String}
   */
  saveVideo: function(filePath) {
    return new Promise(function(resolve, reject) {
      wx.saveVideoToPhotosAlbum({
        filePath: filePath,
        success: res => {
          resolve(res)
        },
        fail: err => {
          reject(err)
        }
      })
    })
  },
  
  /**
   * 构造媒体存储标记
   * 
   * @param index 文件类型 0:视频,1:声音,2:照片
   * @param fileID 云文件ID
   * @param author 上传者
   * @return Object
   */
  mark: function(index, fileID, author) {
    return {
      index: index,
      fileID: fileID,
      author: author
    }
  }
}

页面布局

底部标签栏

新建一个 Page,在json配置文件中引入TabBar组件:

"usingComponents": {
    "i-tab-bar": "../../dist/tab-bar/index",
    "i-tab-bar-item": "../../dist/tab-bar-item/index"
}

新建4个 tab 页和一个增加按钮区域,指定每个 tab-itemkey,绑定 TabBarbindchange 事件来监听点击标签页切换事件,指定current 属性可以切换各标签的 icon。这4个标签实际是写在同一个 Page 中的,当切换标签时,通过 wx:if 控制各个区域是否显示。

<i-tab-bar i-class="bar-high" fixed="true" current="{{ bar }}" bindchange="clickTabBar">
  <i-tab-bar-item key="video" icon="live" current-icon="live_fill" title="视频"></i-tab-bar-item>
  <i-tab-bar-item key="audio" icon="play" current-icon="play_fill" title="声音"></i-tab-bar-item>
  <i-tab-bar-item key="add" icon="add" current-icon="add" color="#ffff00"></i-tab-bar-item>
  <i-tab-bar-item key="notice" icon="remind" current-icon="remind_fill" title="通知"></i-tab-bar-item>
  <i-tab-bar-item key="mine" icon="mine" current-icon="mine_fill" title="我的"></i-tab-bar-item>
</i-tab-bar>
底部标签栏
TabBar 组件的高度为 50px,内容区域可以设置 padding-bottom: 50px; 来避免底部标签页遮挡内容。
用户登录

button 组件的 open-type (微信开放能力)提供了用户主动登录的方式。

  <button open-type="getUserInfo" bindgetuserinfo="onGetUserInfo" class="user-avatar" style="background-image: url({{avatarUrl}})"></button>

处理用户登录事件,获取用户openid用于判断是否登录,查询时作为条件等:

  /**
   * 用户登录获取用户信息
   */
  onGetUserInfo: function(e) {
    let userInfo = e.detail.userInfo
    // 项目模板中默认提供的云函数,可用于获取用户openid
    cloud.call('login', {}).then(res => {
      let openid = res.result.openid
      userInfo.openid = openid
      // 将用户信息写入本地缓存
      wx.setStorage({
        key: 'userInfo',
        data: userInfo,
      })
      // 设置openid
      this.setData({
        openid: openid
      })
    }).catch(err => {
      console.log(err)
    })
    // 设置登录头像
    this.setData({
      avatarUrl: userInfo.avatarUrl
    })
  }
遮罩层组件

上传文件等场景中,如果不希望用户在当前操作完成前有其它动作,可以弹出遮罩层保护用户界面。

<view class='mask' hidden="{{mask}}"></view>

mask.wxss

.mask {
  width: 100%;
  height: 100%;
  position: fixed;
  background-color: #999;
  z-index: 999;
  top: 0;
  left: 0;
  opacity: 0.1;
}

mask.js

  /**
   * 组件的属性列表
   */
  properties: {
    hidden: {
      type: Boolean,
      value: true
    }
  }
  "usingComponents": {
    "mask": "../../components/mask/mask"
  }

编辑 Page 的wxml文件:

<!-- 遮罩对象 -->
<mask hidden="{{!mask}}"></mask>

在js文件中通过指定mask属性来打开/关闭遮罩层。

消息提示

在json配置文件中引入Toast组件。
引入 $Toast 对象:

const {
  $Toast
} = require('../../dist/base/index');
  /**
   * 弹出toast提示
   * 
   * @param content loading显示内容
   * @param type toast类型 default、success、warning、error、loading
   * @param modal 遮罩层是否打开
   * @param duration 持续时间,单位s,0为不自动关闭,需调用 $Toast.hide() 方法手动关闭
   * @param mask toast是否可关闭
   */
  popToast: function(content, type, modal, duration, mask) {
    duration = duration || 0
    mask = mask || false
    modal = modal || false
    $Toast({
      content: content,
      type: type,
      duration: duration,
      mask: mask
    })
    // 打开遮罩层
    this.setData({
      hidden: modal
    })
  },

  /**
   * 关闭toast提示
   */
  hideToast: function() {
    $Toast.hide()
    // 关闭遮罩层
    this.setData({
      hidden: true
    })
  }
处理多媒体上传

为了将上传文件与上传用户相关联、便于查找上传的文件,将数据库和存储结合使用。即将文件上传到存储后,获取返回的 fileID,与上传者信息、文件类型、文件描述等一起写入数据库。
在json配置文件中引入ActionSheet组件。

  <i-action-sheet visible="{{ showAdd }}" actions="{{ addActions }}" show-cancel bind:cancel="cancelAdd" bind:click="handleChooseMedia" />

处理 TabBar 的点击事件,当选择的是新增按钮时,弹出 ActionSheet 选项。

  /**
   * TabBar点击事件
   */
  clickTabBar({
    detail
  }) {
    let key = detail.key
    if ('add' === key) {
      // 点击添加按钮弹出上传选项
      this.setData({
        showAdd: true
      })
    } else {
      this.setData({
        bar: key
      })
    }
  }
弹出新增选项

ActionSheet 点击事件绑定了 handleChooseMedia 函数:

  handleChooseMedia({
    detail
  }) {
    // ActionSheet隐藏
    this.cancelAdd()
    // 登录提示
    if (this.data.openid === '') {
      this.popToast('请先登录~', 'warning', true, 3, true)
      return
    }
    // actions选项的索引,从0开始
    const index = detail.index;
    let that = this;
    if (0 === index) {
      // 视频
      media.chooseVideo().then(res => {
        that.popToast('上传中...', 'loading')
        // 文件
        let tempFilePath = res.tempFilePath
        let tempFilename = that.splitFileName(tempFilePath)
        // 缩略图文件(目前微信开发工具有这个字段,真机无)
        let thumbTempFilePath = res.thumbTempFilePath
        // 上传视频文件
        cloud.upload(tempFilename, tempFilePath).then(res => {
          let mark = media.mark(index, res.fileID, that.data.nickName)
          if (thumbTempFilePath) {
            // 缩略图文件
            let thumbTempFilename = that.splitFileName(thumbTempFilePath)
            // 上传缩略图文件
            cloud.upload(thumbTempFilename, thumbTempFilePath).then(res => {
              let thumb = res.fileID
              mark.thumb = thumb
              // 写入数据库
              cloud.add(mark).then(res => {
                let id = res._id
                // 跳转到编辑视频详情页面
                wx.navigateTo({
                  url: '../editVideo/editVideo'.concat('?thumb=').concat(thumb)
                    .concat('&id=').concat(id)
                })
                that.hideToast()
              }).catch(err => {
                console.log(err)
              })
            })
          } else {
            // 未生成缩略图,使用默认图片
            let thumb = 'cloud://test-d518bb.7465-test-d518bb/system/default-poster.jpg'
            mark.thumb = thumb
            // 写入数据库
            cloud.add(mark).then(res => {
              that.hideToast()
              let id = res._id
              wx.navigateTo({
                url: '../editVideo/editVideo'.concat('?thumb=').concat(thumb)
                  .concat('&id=').concat(id)
              })
            }).catch(err => {
              console.log(err)
            })
          }
        }).catch(err => {
          console.log(err)
        })
      }).catch(err => {
        console.log(err)
      })
    } else if (1 === index) {
      // 照片
      media.chooseImage().then(res => {
        res.tempFilePaths.forEach(function(filePath, i) {
          that.popToast('上传中...', 'loading')
          let filename = that.splitFileName(filePath)
          cloud.upload(filename, filePath).then(res => {
            let mark = media.mark(index, res.fileID, that.data.nickName)
            // 写入数据库
            cloud.add(mark).then(res => {
              that.loadAlbum()
              that.hideToast()
            }).catch(err => {
              console.log(err)
            })
          }).catch(err => {
            console.log(err)
          })
        })
      }).catch(err => {
        console.log(err)
      })
    }
  }
视频列表页

在小程序中,video 是原生组件,层级很高,作为列表元素时会遮挡底部标签栏,因此视频列表以缩略图列表的形式呈现,列表区域设置 padding-bottom: 50px; 来保证能够完全呈现。
点击缩略图,可以跳转到新页面观看视频和具体信息。

<view wx:for="{{videoFiles}}" wx:key="{{item.id}}" class='video-image-warpper'>
  <image class='video-image' mode='aspectFill' src='{{item.thumb}}' bindtap='playVideo' data-index="{{item.id}}"></image>
  <view class="video-like-icon">
    <i-icon type="like_fill" size="28" color="#ff5050" />
    <text>99k+</text>
  </view>
  <i-icon class="play-icon" type="play" size="28" color="#fff" />
</view>
</view>
  /**
   * 加载视频
   */
  loadVideo: function() {
    let that = this
    cloud.query({
      // 查询该用户上传的文件
      // _openid: that.data.openid,
      index: 0
    }).then(res => {
      let files = []
      res.data.forEach(function(e, i) {
        files.push({
          id: e._id,
          fileID: e.fileID,
          thumb: e.thumb,
          author: e.author,
          desc: e.desc
        })
      })
      that.setData({
        videoFiles: files
      })
    }).catch(err => {
      console.log(err)
    })
  }
视频详情页
playVideo: function(e) {
    let video
    files.forEach(function(element, i) {
      if (e.currentTarget.dataset['index'] === element.id) {
        video = element
        return
      }
    })
    if (undefined !== video) {
      wx.navigateTo({
        url: '../video/video?fileID='.concat(video.fileID)
          .concat('&thumb=').concat(video.thumb)
          .concat('&author=').concat(video.author)
          .concat('&desc=').concat(video.desc == undefined ? '' : video.desc)
      })
    }
  }
  <video id='video' src='{{fileID}}' poster='{{thumb}}' object-fit='cover' direction='0' bindended="onVideoEnd"></video>
  /**
   * 保存视频
   */
  saveVideo: function() {
    let that = this
    cloud.download(this.data.fileID).then(res => {
      // 临时文件路径
      let tempFilePath = res.tempFilePath
      media.saveVideo(tempFilePath).then(res => {}).catch(err => {
        console.log(err)
      })
    }).catch(err => {
      console.log(err)
    })
  }
相册
  <image class='album-image' mode='aspectFill' wx:for="{{albumFiles}}" wx:key="{{item.id}}" src='{{item.fileID}}' bindtap='imageToPreview' data-current="{{item.fileID}}" />
  /**
   * 初始化album
   * 
   * TODO 分页加载
   */
  loadAlbum: function() {
    let that = this
    cloud.query({
      index: 1
    }).then(res => {
      let files = []
      res.data.forEach(function(e, i) {
        files.push({
          id: e._id,
          fileID: e.fileID
        })
      })
      that.setData({
        albumFiles: files
      })
    }).catch(err => {
      console.log(err)
    })
  }
  /**
   * 相册图片点击全屏预览
   */
  imageToPreview: function(e) {
    // 被点击图片云文件ID
    let current = e.currentTarget.dataset['current']
    // 所有图片云文件ID
    let fileIDs = []
    this.data.albumFiles.forEach(function(e, i) {
      fileIDs.push(e.fileID)
    })
    media.previewImage(current, fileIDs)
  }
上一篇 下一篇

猜你喜欢

热点阅读