目录

48.创建个人相册

7.6 创建个人相册

当我们要开始创建一个实际的小程序项目时,除了要考虑原型设计(产品经理扮演的角色)与页面的重构(设计前端开发的工作)以外,首先需要思考的是各个功能所对应的页面、组件等的交互逻辑(最终转化为项目的文件结构、各种函数、模块、component以及一个个API)以及这些功能的背后所依赖的各项数据(最终转化为数据库的结构设计、数据字典等)。接下来我们就以创建一个个人相册的项目为例,来具体进行讲解。

7.6.1 数据库的设计与结构

和Excel表、关系型数据库(如MySQL)以行和列、多表关系来设计表结构不同的是,云开发的数据库是基于文档的。我们可以在一个记录里嵌套多层数组和对象,把每个文档所需要的数据都嵌入到一个文档里,而不是分散到多个不同的集合。

比如我们想做一个网盘小程序,用来记录用户信息,以及创建的相册、文件夹,这里相册和文件夹因为可以创建很多个,所以它是一个数组;而每一个相册对象和文件夹对象里都可以存储一个照片列表和文件列表,我们发现在云开发数据库里一个元素的值是数组,数组里又嵌套对象,对象里又有元素是数组是非常常见的事情。以下是网盘小程序的数据库设计,包含了一个用户的信息,上传的所有文件和照片等信息:

{
  "_id": "",   //自动生成的ID,也可以自定义,云数据库无法像MySQL一样ID自增,自定义时要注意_id的唯一性
  "_openid": "oUL-m5FuRmuVmxvbYOGuXbuEDsn8",  //用户在当前小程序的openid
  "nickName": "李东bbsky",  //用户的昵称,可以将微信的昵称存储在这个字段
  "avatarUrl": "https://thirdwx.qlogo.cn/mmopen/vi_32/77Vejpiabfr62gPczlm4SicRqQxE1RBCs29NrdPqZZMtRu1uO47VichN1mrguRaNict5urtyyS3mFlkfGicH3bicoibMQ/132", //用户的头像链接,同样可以来自微信的userInfo信息
  "albums": [      //相册,用来存储照片
    {
      "albumName": "风景",  //相册名称
      "coverURL": "",//相册封面地址,可以从相册里随机取一张照片或取第一张照片
      "photos": [    //照片的数组,存储一个相册里N张照片
        {
          "comments": "云开发线下活动合影",//照片的备注或描述
          "fileID": "" //照片的地址,可以是云存储的fileID或https链接
        }
      ]
    }
  ],
  "folders": [   //文件夹,用来存储各种文档
    {
      "folderName": "工作周报",//文件夹名称
      "files": [     //文件的数组,存储文件夹里的N个文件
        {
          "name": "25周工作总结",  //文件名称
          "fileID": "cloud://xly-xrlur.786c-xly-xrlur-1300446086/1571902980622-737.xls", //文件的地址,可以是云存储的fileID或https链接
          "comments": "第25周工作的内容,主要包括小程序的宣传运营"//文件备注、描述
        }
      ]
    }
  ]
}

在我们开始开发一个项目之前,就先规划好类似于上面的数据库结构设计,这样开发的时候就能清楚有哪些字段,字段用的是什么数据类型,字段与字段之间有什么联系。有了这些规划,我们再来思考这些数据怎么获取,是通过什么函数、什么API、什么交互来获取。比如用户的信息可以来自wx.getUserInfo()的userInfo,也可以根据情况在用户登录之后自定义填写一些信息,比如职业等数据。

如果是用关系型数据库,就会建user表来存储用户信息,albums表存储相册信息,folders表存储文件夹信息,photos表存储照片信息,files表存储文件信息,相信大家可以通过这个案例对云数据库是面向文档的数据库有一个大致的了解。

当然云开发的数据库也是可以把数据分散到不同集合的,需要视不同的情况而定,在后面章节我们会介绍。这种将每个文档所需的数据都嵌入到一个文档内部的做法,我们称之为反范式化(denormalization),将数据分散到多个不同的集合,不同集合之间相互引用称之为范式化(normalization),也就是说反范式化文档里包含子文档,而范式化呢,文档的子文档则是存储在另一个集合之中。

7.6.2 UI与文件结构

1、文件结构

一般来说,制作一个小程序需要先梳理清楚你要做的这个项目的产品需求,比如大致的页面结构、包含哪些功能、页面设计大致是怎样的风格、页面间的跳转、组件的触发等交互是怎样进行的等等,会根据这些先做出一个原型图,然后根据原型图的要求做出设计稿,最后才轮到开发。如果是自己独立开发虽然不必这么复杂,但是小程序的草图也是要“成竹在胸”才行。

当我们拿到设计稿时,就可以规划UI使用什么框架,比如WeUI、Vant Weapp、Color UI、iView、Lin UI等,会封装哪些函数、模块、组件,规划会有哪些云函数及相应的功能、云存储的文件目录等,以及规划项目的文件结构。

project
├── cloudfunctions //云函数根目录
   └── folder //云函数
├── miniprogram //小程序目录
   └── pages   //存放页面,页面的创建方法可以参考第一部分的教程
       └── folder
       └── file
       └── album
       └── photo
       └── user
   └── images  //存放图片元素
   └── style   //存放封装好的样式
   └── utils   //存放封装的模块
   └── components //存放封装好的组件
   └── app.js 
   └── app.json
   └── app.wxss
└── project.config.json 

将设计稿的交互逻辑转化为函数的逻辑,也就是会调用哪些API,代码根据条件遵循怎样的流程,前后端、页面、组件、本地与数据库之间的参数、数据的处理等,是将交互设计稿转化为实际的代码最为核心的工作。

2、引入WeUI拓展

在前面我们了解过如何引入WeUI,作为官方出品的UI框架,WeUI支持更简单的引入方式以及有着比较丰富的实用组件。WeUI的使用方法在官方技术文档的拓展能力标签页有相对比较完善的介绍。

在app.json里添加useExtendedLib的配置,就能引入WeUI了,使用这种方式不会计入代码包大小,以及不需要使用import直接使用即可(当然你可以右键miniprogram打开终端输入npm install weui-miniprogram然后再引入)。

{
  "useExtendedLib": {
    "weui": true
  }
}

当需要在页面比如folder.wxml中使用组件时,只需要把需要用到的组件在相应页面的json文件(这里为foler.wxss)里引入即可(不使用的虽然引入了也没有关系,但是建议只引入用到的)

{
  "usingComponents": {
    "mp-dialog": "/miniprogram_npm/weui-miniprogram/dialog/dialog",
    "mp-toptips": "/miniprogram_npm/weui-miniprogram/toptips/toptips"
  },
  "navigationBarTitleText": "文件夹列表"
}

我们可以在folder.wxml里输入以下代码来验证模块是否引入成功(模拟器的小程序的前端展示可以看得到内容即可):

<mp-toptips msg="组件引入成功,提示5秒后消失" type="error" show="true" delay="5000"></mp-toptips>
<mp-form id="form" rules="{{rules}}" models="{{formData}}">
  <mp-cells title="单选列表项">
    <mp-checkbox-group prop="radio" multi="{{false}}" bindchange="radioChange">
      <mp-checkbox wx:for="{{radioItems}}" wx:key="value" label="{{item.name}}" value="{{item.value}}" checked="{{item.checked}}"></mp-checkbox>
    </mp-checkbox-group>
  </mp-cells>
</mp-form>
<mp-dialog title="WeUI引入成功" show="true" bindbuttontap="" buttons="{{[{text: '确定'}]}}">
  <view>看得到这个界面说明WeUI拓展引入成功</view>
</mp-dialog>

7.6.3 建立用户与数据的联系

打开云开发控制台的数据库标签,新建一个clouddisk的集合,并使用安全规则修改它的权限为“所有人可读,仅创建者可读写”,即{"read": true,"write": "doc._openid == auth.openid" }。使用开发者工具新建一个folder的页面(更加建议你在页面文件结构规划的时候就把这些页面给建好了),然后在folder.js的页面生命周期函数onLoad里输入以下代码:

this.checkUser()

然后再在Page()对象里输入以下代码,代码的意思是如果clouddisk里没有用户创建的数据,那就在clouddisk里新增一条记录;如果有数据,就返回数据:

无论你是用简易版的权限设置,还是使用安全规则,在这里我们都不需要事先获取用户的openid,因为如果你使用的是简易版权限,对数据库进行增删改查时都会自带where({_openid:'{openid}'})的条件;而如果使用时安全规则,你可以直接使用这个条件,它不需要我们知道用户的openid具体是什么。也就说,我们没有必要先用云函数返回用户的openid,省了这样一个操作,能提升小程序的加载速度以及节省云开发资源。

const app = getApp()
async checkUser() {
  //获取clouddisk是否有当前用户的数据,注意这里默认带了一个where({_openid:"当前用户的openid"})的条件
  const userData = (await db.collection('clouddisk').where({
    _openid:'{openid}' //一定要开启安全规则,不开安全规则{openid}会失效,没有开启安全规则,数据库查询自带openid的权限,就不需要写这个条件了
  }).get()).data
  console.log("当前用户的数据对象",userData)

  //如果当前用户的数据data数组的长度为0,说明数据库里没有当前用户的数据
  if(userData.length === 0){      
    //没有当前用户的数据,那就新建一个数据框架,其中_id和_openid会自动生成
    await db.collection('clouddisk').add({
      data:{
        //nickName和avatarUrl可以通过getUserInfo来获取,这里不多介绍
        "nickName": "", 
        "avatarUrl": "",
        "albums": [ ],
        "folders": [ ]
      }
    })
    wx.switchTab({   //如果数据库不存在用户,除了在数据库创建一条空记录以外,还跳转到user页面,尽管用户已经登录了,但是还需要借助button的open-type="getUserInfo"来获取用户的信息
      url: '/pages/user/user'
    })
  }else{ //如果有数据,就做两件事情,一是使用setData提供页面渲染数据,二是将数据存储到全局对象
    this.setData({
      userData
    })
    app.globalData.userData = userData
    console.log('用户数据',userData)
  }
},

根据数据库的设计,这里一个用户只能创建一条记录,用户创建的相册、文件夹,上传的照片、文件都会存储到这个记录里。当用户打开小程序的时候执行checkUser函数,如果是新用户就在数据库里创建一条记录;如果不是新用户,就会返回数据并将数据传递给Page对象的data里。

在创建记录时,我们有规划的事先就将一些字段给预填充了,也就是在add的data里添加了一些必要的字段,如nickName等,这方便我们后续可以直接使用update的更新指令给字段更新数据。

7.6.4 获取用户信息并存储到数据库

在前面我们了解到,由于云开发的免鉴权,当用户打开小程序时就有了一个独一无二的openid,也就是自动登录了。不过光有openid,在展示上没有用户的头像和昵称等信息交互体验会不太好,我们可以把用户的这些信息获取之后存储到数据库,以后用户的这些信息可以只来自于数据库,永远不再需要用户的授权了(不过微信用户会经常修改自己的昵称和头像,你需要用户可以手动同步更新时就需要用户授权),你还可以让用户自定义头像和昵称(增删改查云数据库)。

1、获取用户信息

使用开发者工具新建一个user页面(在规划页面文件结构就建好了),然后在user.wxml里输入以下代码,我们通过组件的方式来获取用户的信息:

<button open-type="getUserInfo" bindgetuserinfo="getUserInfo" lang="zh_CN">点击获取用户信息</button>
<image src="{{avatarUrl}}"></image>
<view>{{city}}</view>
<view>{{nickName}}</view>

在user.js的data里初始化avatarUrl、nickName以及city,没有获取到用户信息时,用一张默认图片代替,昵称显示用户未登录,city显示为未知:

data: {
  avatarUrl: '',  //可以预填充一个用户未登陆的灰色图片
  nickName:"用户未登陆", //预填充,提醒用户登录
  city:"未知",    //预填充
},

然后在user.js文件里输入以下代码,在事件处理函数getUserInfo我们可以打印event对象,open-type=”getUserInfo”的组件的event对象的detail里就有userInfo:

getUserInfo: function (event) {
  console.log('getUserInfo打印的事件对象', event)
  let { avatarUrl, city, nickName}= event.detail.userInfo
  this.setData({
    avatarUrl,city, nickName
  })
},

将获取的avatarUrl,city,nickName通过this.setData()赋值给data。编译之后点击点击获取用户信息按钮,首先会弹出授权弹窗,当用户确认之后,就会显示用户的信息。

2、获取用户高清头像

我们发现获取到的头像不是很清晰,这是因为默认的头像大小为132*132UserInfo用户头像说明),如果把avatarUrl链接后面的132修改为0就能获取到640*640大小的头像了:

getUserInfon: function (event) {
  let { avatarUrl, city, nickName}= event.detail.userInfo
  avatarUrl = avatarUrl.split("/")
  avatarUrl[avatarUrl.length - 1] = 0;
  avatarUrl = avatarUrl.join('/'); 
  this.setData({
    avatarUrl,city, nickName
  })
},

3、页面加载时就显示用户信息

在获得了用户授权和用户信息的情况下,刷新页面或进行页面跳转,用户的个人信息还是不会显示,这是因为getUserInfo事件处理函数点击组件时才触发,我们需要在页面加载时也能触发获取用户信息才行。

我们可以在user.js的onLoad生命周期函数里输入以下代码,当用户授权之后来调用wx.getUserInfo() API:

wx.getSetting({
  success: res => {
    if (res.authSetting['scope.userInfo']) {
      wx.getUserInfo({
        success: res => {
          let { avatarUrl, city, nickName } =res.userInfo
          this.setData({
            avatarUrl, city, nickName
          })
        }
      })
    }
  }
});

这里为了方便,只是写了一些实现功能的基础代码,在实际的项目开发中,我们可以把函数封装起来,既可以像前面的将函数写到页面文件的js里,使用this.functionName()来调用;还可以把函数写到独立的模块文件里,比如utils/util.js里,使用module.exportsrequire引入。是否封装以及封装到哪个程度,在开发之间就可以做一个大致的规划。

4、将用户信息更新到云存储

我们可以在getUserInfo的函数里再调用uploadMsg函数将获取的用户信息更新保存到数据库里,这样小程序就不用再通过wx.getUserInfo来获取用户的信息,因为wx.getUserInfo这种方式会在一段时间和用户清空授权等情况下又需要用户授权同意才行,而将用户信息存到数据库里,该用户只需要同意授权一次就够了。

//往getUserInfo里添加调用函数的代码:
getUserInfo: function (event) {
  //可以添加到this.setData()的后面
  this.uploadMsg(avatarUrl,city, nickName)
},

async uploadMsg(avatarUrl,city,nickName){
  const result = await db.collection('clouddisk').where({
    _openid:'{openid}'
  }).update({
    data:{
      avatarUrl,city, nickName
    }
  })
  console.log("更新结果",result)
}

这里保存的是用户头像的链接,这个链接来自微信服务器,它不是永久有效的,会在用户更新头像的时候失效,你可以看功能的需要将用户的头像下载下来存储到云存储,数据库的avatarUrl字段为云存储的fileID或https链接就可以了。头像下载的方法可以参考【用云函数实现后端能力】的【HTTP处理】章节的内容。

7.6.4 获取相册文件夹数据

1、页面的数据获取流程图

当我们打开小程序或小程序的首页(folder页面)时,会先请求数据库,获取用户在该数据库的所有数据(由于这个数据库结构比较简单,所谓这里的“所有数据”只是一条记录)并存储到app.js的globalData,这样无论用户打开哪个页面,页面都会使用getApp().globalData来获取数据;当然其他页面的数据也是可以从数据库里获取数据,但是相比从globalData获取数据来说,会增加数据库的请求次数。结合前面的介绍,相册小程序的数据获取流程大致可以如下安排: https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/357104dea48e4728914606fbcbd5bff3~tplv-k3u1fbpfcp-zoom-1.image

不同的小程序,根据业务和功能的不同,数据的获取存储方式、页面的加载跳转逻辑、页面组件与用户的交互方式等都会也有所不同,但是再复杂的功能,都可以拆解成使用JavaScript(含API)对数据库、缓存、本地文件、云存储、全局变量等里的数据进行增删改查。

2、获取相册文件夹数据

在首页folder.js里,如果用户在数据库里有数据,我们可以把数据存储到全局对象globalData里,而用于渲染展示相册、文件夹信息的folder、album页面等更多页面,都可以从globalData里获取数据,比如我们可以在album.js里写如下代码:

const app = getApp()
Page({
  data: {
    userData:{}
  },

  onLoad: function (options) {
    this.setData({
      userData:app.globalData.userData
    })
    console.log(this.data.userData)
  },

})

由于个人相册只是一个功能比较简单的小项目,我们在设计数据库时,将单个用户的相册(照片)、文件夹(文件)的数据都存储到了一个记录里面(也就是反范式化设计)。如果你希望用户存储的照片、文件数量在几万以上或存储更多内容比如文章、评论,这可能导致一个记录的大小有好几M(记录上限不能超过16M),数据库的性能会受到影响。在后面的博客小程序,我们会介绍如何范式化设计数据库。