小程序·云开发实战 - 迷你微博


0. 前言

本文将手把手教你如何写出迷你版微博的一行行代码,迷你版微博包含以下功能:

  • Feed 流:关注动态、所有动态
  • 发送图文动态
  • 搜索用户
  • 关注系统
  • 点赞动态
  • 个人主页

使用到的云开发能力:

  • 云数据库
  • 云存储
  • 云函数
  • 云调用

没错,几乎是所有的云开发能力。也就是说,读完这篇实战,你就相当于完全入门了云开发!

咳咳,当然,实际上这里只是介绍核心逻辑和重点代码片段,完整代码建议下载查看。

1. 取得授权

作为一个社交平台,首先要做的肯定是经过用户授权,获取用户信息,小程序提供了很方便的接口:


这个 button 有个 open-type 属性,这个属性是专门用来使用小程序的开放能力的,而 getUserInfo 则表示 获取用户信息,可以从bindgetuserinfo回调中获取到用户信息

于是我们可以在 wxml 里放入这个 button 后,在相应的 js 里写如下代码:

Page({
  ...

  getUserInfo: function(e) {
    wx.navigateTo({
      url: "/pages/circle/circle"
    })
  },

  ...
})

这样在成功获取到用户信息后,我们就能跳转到迷你微博页面了。

需要注意,不能使用 wx.authorize({scope: "scope.userInfo"}) 来获取读取用户信息的权限,因为它不会跳出授权弹窗。目前只能使用上面所述的方式实现。

2. 主页设计

社交平台的主页大同小异,主要由三个部分组成:

  • Feed 流
  • 消息
  • 个人信息

那么很容易就能想到这样的布局(注意新建一个 Page 哦,路径:pages/circle/circle.wxml):


  
  

  
  

  
  

  
    
      
    
    
      
    
    
      
    
  

很好理解,画面主要被分为上下两个部分:上面的部分是主要内容,下面的部分是三个 Tab 组成的 Footer。重点 WXSS 实现(完整的 WXSS 可以下载源码查看):

.footer {
  box-shadow: 0 0 15rpx #ccc;
  display: flex;
  position: fixed;
  height: 120rpx;
  bottom: 0;
  width: 100%;
  flex-direction: row;
  justify-content: center;
  z-index: 100;
  background: #fff;
}

.footer-item {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  width: 33.33%;
  color: #333;
}

.footer-item:nth-child(2) {
  border-left: 3rpx solid #aaa;
  border-right: 3rpx solid #aaa;
  flex-grow: 1;
}

.footer-btn {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  border-radius: 0;
  font-size: 30rpx;
}

核心逻辑是通过 position: fixed 来让 Footer 一直在下方。

读者会发现有一个 currentPage 的 data ,这个 data 的作用其实很直观:通过判断它的值是 main/msg/me 中的哪一个来决定主要内容。同时,为了让首次使用的用户知道自己在哪个 Tab,Footer 中相应的 button 也会从白底黑字黑底白字,与另外两个 Tab 形成对比。

现在我们来看看 main 部分的代码(在上面代码的基础上扩充):

...

  
    
      
    
  
  
    
  


  
    
      
    
    无数据
  
  

...

这里用到了 列表渲染 和 条件渲染,还不清楚的可以点击进去学习一下。

可以看到,相比之前的代码,我添加一个 header,同时 main-area 的内部也新增了一个 scroll-view(用于展示 Feed 流) 和一个 button(用于编辑新迷你微博)。header 的功能很简单:左侧区域是一个 picker,可以选择查看的动态类型(目前有 关注动态所有动态 两种);右侧区域是一个按钮,点击后可以跳转到搜索页面,这两个功能我们先放一下,先继续看 main-area 的新增内容。

main-area 里的 scroll-view 是一个可监听滚动事件的列表,其中监听事件的实现:

data: {
  ...
  addPosterBtnBottom: "190rpx",
  mainHeaderMaxHeight: "80rpx",
  mainAreaHeight: "calc(100vh - 200rpx)",
  mainAreaMarginTop: "80rpx",
},
onMainPageScroll: function(e) {
  if (e.detail.deltaY < 0) {
    this.setData({
      addPosterBtnBottom: "-190rpx",
      mainHeaderMaxHeight: "0",
      mainAreaHeight: "calc(100vh - 120rpx)",
      mainAreaMarginTop: "0rpx"
    })
  } else {
    this.setData({
      addPosterBtnBottom: "190rpx",
      mainHeaderMaxHeight: "80rpx",
      mainAreaHeight: "calc(100vh - 200rpx)",
      mainAreaMarginTop: "80rpx"
    })
  }
},
...

结合 wxml 可以知道,当页面向下滑动 (deltaY < 0) 时,header 和 button 会 “突然消失”,反之它们则会 “突然出现”。为了视觉上有更好地过渡,我们可以在 WXSS 中使用 transition

...
.main-area {
  position: relative;
  flex-grow: 1;
  overflow: auto;
  z-index: 1;
  transition: height 0.3s, margin-top 0.3s;
}
.main-header {
  position: fixed;
  width: 100%;
  height: 80rpx;
  background: #fff;
  top: 0;
  left: 0;
  display: flex;
  justify-content: space-around;
  align-items: center;
  z-index: 100;
  border-bottom: 3rpx solid #aaa;
  transition: max-height 0.3s;
  overflow: hidden;
}
.add-poster-btn {
  position: fixed;
  right: 60rpx;
  box-shadow: 5rpx 5rpx 10rpx #aaa;
  display: flex;
  justify-content: center;
  align-items: center;
  color: #333;
  padding-bottom: 10rpx;
  text-align: center;
  border-radius: 50%;
  font-size: 60rpx;
  width: 100rpx;
  height: 100rpx;
  transition: bottom 0.3s;
  background: #fff;
  z-index: 1;
}
...

3. Feed 流

3.1 post-item

前面提到,scroll-view 的内容是 Feed 流,那么首先就要想到使用 列表渲染。而且,为了方便在个人主页复用,列表渲染中的每一个 item 都要抽象出来。这时就要使用小程序中的 Custom-Component 功能了。

新建一个名为 post-itemComponent,其中 wxml 的实现(路径:pages/circle/component/post-item/post-item.js):


  
    {{data.author}}
    {{data.formatDate}}
  
  
    {{data.msg}}
  
  
    
  

可见,一个 poster-item 最主要有以下信息:

  • 作者名
  • 发送时间
  • 文本内容
  • 图片内容

其中,图片内容因为是可选的,所以使用了 条件渲染,这会在没有图片信息时不让图片显示区域占用屏幕空间。另外,图片内容主要是由 image-wrapper 组成,它也是一个 Custom-Component,主要功能是:

  • 强制长宽 1:1 裁剪显示图片
  • 点击查看大图
  • 未加载完成时显示 加载中

具体代码这里就不展示了,比较简单,读者可以在 component/image-wrapper 里找到。

回过头看 main-area 的其他新增部分,细心的读者会发现有这么一句:

无数据

这会在 Feed 流暂时没有获取到数据时给用户一个提示。

3.2 collections: poster、poster_users

展示 Feed 流的部分已经编写完毕,现在就差实际数据了。根据上一小节 poster-item 的主要信息,我们可以初步推断出一条迷你微博在 云数据库 的 collection poster 里是这样存储的:

{
  "username": "Tester",
  "date": "2019-07-22 12:00:00",
  "text": "Ceshiwenben",
  "photo": "xxx"
}

先来看 username。由于社交平台一般不会限制用户的昵称,所以如果每条迷你微博都存储昵称,那将来每次用户修改一次昵称,就要遍历数据库把所有迷你微博项都改一遍,相当耗费时间,所以我们不如存储一个 userId,并另外把 id 和 昵称 的对应关系存在另一个叫 poster_users 的 collection 里。

{
  "userId": "xxx",
  "name": "Tester",
  ...(其他用户信息)
}

userId 从哪里拿呢?当然是通过之前已经授权的获取用户信息接口拿到了,详细操作之后会说到。

接下来是 date,这里最好是服务器时间(因为客户端传过来的时间可能会有误差),而云开发文档里也有提供相应的接口:serverDate。这个数据可以直接被 new Date() 使用,可以理解为一个 UTC 时间。

text 即文本信息,直接存储即可。

photo 则表示附图数据,但是限于小程序 image 元素的实现,想要显示一张图片,要么提供该图片的 url,要么提供该图片在 云存储 的 id,所以这里最佳的实践是:先把图片上传到云存储里,然后把回调里的文件 id 作为数据存储。

综上所述,最后 poster 每一项的数据结构如下:

{
  "authorId": "xxx",
  "date": "utc-format-date",
  "text": "Ceshiwenben",
  "photoId": "yyy"
}

确定数据结构后,我们就可以开始往 collection 添加数据了。但是,在此之前,我们还缺少一个重要步骤。

3.3 用户信息录入 与 云数据库

没错,我们还没有在 poster_users 里添加一条新用户的信息。这个步骤一般在 pages/circle/circle 页面首次加载时判断即可:

getUserId: function(cb) {
  let that = this
  var value = this.data.userId || wx.getStorageSync("userId")
  if (value) {
    if (cb) {
      cb(value)
    }
    return value
  }
  wx.getSetting({
    success(res) {
      if (res.authSetting["scope.userInfo"]) {
        wx.getUserInfo({
          withCredentials: true,
          success: function(userData) {
            wx.setStorageSync("userId", userData.signature)
            that.setData({
              userId: userData.signature
            })
            db.collection("poster_users")
              .where({
                userId: userData.signature
              })
              .get()
              .then(searchResult => {
                if (searchResult.data.length === 0) {
                  wx.showToast({
                    title: "新用户录入中"
                  })
                  db.collection("poster_users")
                    .add({
                      data: {
                        userId: userData.signature,
                        date: db.serverDate(),
                        name: userData.userInfo.nickName,
                        gender: userData.userInfo.gender
                      }
                    })
                    .then(res => {
                      console.log(res)
                      if (res.errMsg === "collection.add:ok") {
                        wx.showToast({
                          title: "录入完成"
                        })
                        if (cb) cb()
                      }
                    })
                    .catch(err => {
                      wx.showToast({
                        title: "录入失败,请稍后重试",
                        image: "/images/error.png"
                      })
                      wx.navigateTo({
                        url: "/pages/index/index"
                      })
                    })
                } else {
                  if (cb) cb()
                }
              })
          }
        })
      } else {
        wx.showToast({
          title: "登陆失效,请重新授权登陆",
          image: "/images/error.png"
        })
        wx.navigateTo({
          url: "/pages/index/index"
        })
      }
    }
  })
}

代码实现比较复杂,整体思路是这样的:

  1. 判断是否已存储了 userId,如果有直接返回并调用回调函数,如果没有继续 2
  2. 通过 wx.getSetting 获取当前设置信息
  3. 如果返回里有 res.authSetting["scope.userInfo"] 说明已经授权读取用户信息,继续 3,没有授权的话就跳转回首页重新授权
  4. 调用 wx.getUserInfo 获取用户信息,成功后提取出 signature(这是每个微信用户的唯一签名),并调用 wx.setStorageSync 将其缓存
  5. 调用 db.collection().where().get() ,判断返回的数据是否是空数组,如果不是说明该用户已经录入(注意 where() 中的筛选条件),如果是说明该用户是新用户,继续 5
  6. 提示新用户录入中,同时调用 db.collection().add() 来添加用户信息,最后通过回调判断是否录入成功,并提示用户

不知不觉我们就使用了云开发中的 云数据库 功能,紧接着我们就要开始使用 云存储 和 云函数了!

3.4 addPoster 与 云存储

发送新的迷你微博,需要一个编辑新迷你微博的界面,路径我定为 pages/circle/add-poster/add-poster