小白变大神,8月做个todolist 送自己

2024-08-02 11:19:33 浏览数 (1)

通过搭建一个todolist应用,掌握使用WxMpCloudBooster代码库中的工具函数来实现数据库的增删改查功能,并介绍utils中的工具函数。

如果你属于以下类型的读者,本文提供的教程指引跟代码库会帮助你更好上手及深入学习

  1. 拥有一定的编程经验,并对JavaScript语言有所了解和熟悉。
  2. 已经掌握云开发基础原理。
  3. 对云开发还不算完全熟悉,希望快速上手。

前言

阅读本篇文章之前,建议可在文末查看上一篇文章:《小白变大神 | 初识云开发数据库》,创建一个空页面 Todolist,并创建对应的数据库表。在本篇文章中,将继续完善 Todolist 的功能,学习基础的数据库读写工具函数,后续可直接把文中的代码库应用到自己的项目中,提高开发效率。

1. Todolist 的写入与刷新

1.使用微信默认样式

本次讲解云开发环境的 JavaScript 代码,页面UI上使用微信默认方案 WeUI,请向 app.json 文件添加下面的配置,这样文章中的 wxml 代码复制到自己的项目中时,就能得到和文章中一样的页面UI效果。

代码语言:javascript复制
// 向app.json文件添加下面的配置,让你的页面UI效果和我的一样
{
  "useExtendedLib": {
    "weui": true
  },
  "style": "v2",
}

假设已经了解了小程序的 WeUI 和 WXML。

2.获取代码库 WxMpCloudBooster

新建一个项目,需要先获取 WxMpCloudBooster 库中的代码。可以在github代码库:sdjl/WxMpCloudBooster下载,或者使用如下的命令:

代码语言:javascript复制
# 获取项目
git clone https://github.com/sdjl/WxMpCloudBooster.git

# 切换到本篇文章(文章二)对应的代码库
cd WxMpCloudBooster
git checkout article2

注意:建议 checkout 到 article2,否则拿到的代码可能和本文中不一致

3.添加 Todolist 页面UI

获取 WxMpCloudBooster 项目代码并切换到 article2 分支后,请复制miniprogram/pages/todo/todo.wxml 文件到项目中。刷新页面获得如图所示效果:

提示:为了减少文章篇幅,将不在文章中放置完整的 wxml 代码,可以点击这里跳转查看 article2 的代码

4.数据绑定函数:_inputChange

查看代码库中的 miniprogram/pages/todo/todo.js 文件(这里的代码库指WxMpCloudBooster,以下同),会看到如下代码:

代码语言:javascript复制
'use strict'

import utils from '../../utils/utils'

Page({

  behaviors: utils.behaviors(),

  data: {
    todo_list: [], // 未完成事项列表
    done_list: [], // 已完成事项列表
    new_title: '', // 新增待办事项的内容,与输入框绑定
  },

  // ...

})

首先,在 data 中定义了一个 new_title 变量,用于存放 <textarea> 输入框中新建 todo 的内容。但是,当在 <textarea> 中输入文字时,系统并不会自动更新 data.new_title 的值,需要在 wxml 中使用 _inputChange 函数来实现数据绑定。代码如下:

代码语言:javascript复制
<textarea
  class="weui-textarea"
  value="{{new_title}}"
  bind:input="_inputChange"
  data-field="new_title"
  placeholder="这里是一个<textarea>输入框"
  placeholder-class="weui-input__placeholder"
/>

_inputChange 函数可以在代码库的 miniprogram/utils/page_behaviors.js 文件中找到:

代码语言:javascript复制
module.exports = Behavior({

  methods: {

      // page_behaviors.js 中的函数都有下划线 _
      _inputChange(e) {
        const { field } = e.currentTarget.dataset
        this.setData({
          [`${field}`]: e.detail.value
        })
      },

  },

})

假设已经了解了小程序的事件机制和Behavior

为了在 todo.wxml 中使用 _inputChange 函数,需要在 todo.js 中引入 behaviors :

代码语言:javascript复制
Page({

  behaviors: utils.behaviors(),

})

这里的 behaviors 是 Page 的一个内置属性,可在 Page文档中查看。

在 utils.js 中可查看 behaviors 函数:

代码语言:javascript复制
const PAGE_BEHAVIORS = require('page_behaviors')

const utils = {

  behaviors(){
    return [ PAGE_BEHAVIORS, ]
  }

}

提醒:当修改 Behavior 文件时,需要重新编译小程序

这样,在 <teatarea> 中输入文字时,data.new_title 的值会自动更新,实现数据绑定。

随着本教程的深入,page_behaviors.js 文件的功能会越来越丰富。

5.写入数据函数:utils.addDoc

假设已经根据上一篇文章创建了 todo 和 p_todo 表,且两个表的数据库权限均选择了“自定义安全规则”,并使用了如下的安全配置:

代码语言:javascript复制
{
  "read": "doc._openid == auth.openid",
  "write": "doc._openid == auth.openid"
}

为了向 todo 表中写入数据,需使用 utils.js 中的 addDoc 函数:

代码语言:javascript复制
addDoc(c, d) {const_=thisreturn new Promise((resolve, reject) => {
    _.coll(c).add({ data: d })
      .then(res => {
        resolve(res._id)
      })
      .catch(e => {
        reject(e)
      })
  })
}

addDoc 中的 coll 函数会自动根据环境判断使用 todo 还是 p_todo 表。当在微信开发者工具中运行时,addDoc 会向 todo 表中写入数据,而在生产环境或真机预览时,addDoc 会向 p_todo 表中写入数据,并且以后提供的所有数据库操作函数都会自动判断

然后在 todo.js 中使用 addDoc 函数:

代码语言:javascript复制
async addTodo (e) {
  const _ = this
  const { new_title } = _.data

  utils.addDoc('todo', {title: new_title, status: '未完成'})
    .then(new_todo_id => {
      _.updateTodoList()
      _.setData({
        new_title: '' // 清空输入框
      })
    })
}

这样就完成了点击“添加 todo”按钮时创建新 todo 的功能。

6.数据读取函数:utils.docs 与 utils.myDocs

新建 todo 后,需要在 updateTodoList 函数中重新读取 todo_list 列表,并重新渲染页面,需要一个读取数据的函数 utils.docs。

前文提到,当使用“自定义安全规则”且规则中有 auth.openid == doc._openid 时,需要在查询语句中添加 _openid: ‘{openid}’ 条件(否则会抛出没有权限的异常)。

因此,代码库中还提供了 utils.myDocs 函数,此函数专门用于读取上述权限设置的数据。

utils.js 文件中这两个函数的定义如下(完整的代码请查看代码库):

代码语言:javascript复制
docs ({
  c,
  w = {},
  page_num = 0,
  page_size = 20,
  only = '',
  except = '',
  created = false,
  order_by = {},
  mine = false,
} = {}) {
  // 代码请查看代码库
},

myDocs (args) {
  args.mine = true
  return this.docs(args)
}

有了这两个工具函数后,就可以在 updateTodoList 函数中重新读取 todo_list,并重新渲染页面。

代码语言:javascript复制
async updateTodoList () {
  const _ = this
  utils.myDocs({c: 'todo', w: {status: '未完成'} })
    .then(docs => {
      _.setData({
        todo_list: docs
      })
    })
}

7.utils.docs 的参数详解

utils.docs 函数支持多个参数,下面来详细解释这些参数的用法:

c 参数

c 参数是唯一一个必传的,表示 collection 集合名称。当运行在生产环境时会自动添加 p_ 前缀,因此请勿在这里输入p_前缀,其他数据库操作函数也一样

w 参数

w 参数表示查询条件 where,如 w: {status: ‘未完成’}。

还可以在 w 中使用“点表示法”,如:

代码语言:javascript复制
utils.docs(
  c: 'xxx',
  w: { 'people[0].name': '张三' }
)

这里的查询条件规则与官方文档中的规则一致。

8.page_num 和 page_size 参数

page_num 和 page_size 参数用于分页读取数据,page_num 从0开始,page_size 最大为20(微信限制每次最多读取20条数据)。

微信限制前端每次最多读取20条数据主要是为了避免加载时间过长,从而保障用户体验(毕竟有许多小白什么代码都敢写,可查看 get函数文档)。

在下篇文章中将会提供 utils.allDocs 函数,可实现仅消耗一次调用次数就能读取所有数据。

9.only 和 except 参数

only 和 except 参数用于控制返回的字段,当仅需要返回 _id 和 _openid 时,可以这样写:

代码语言:javascript复制
utils.docs(
  c: 'xxx',
  only: '_id, _openid'
)

同样的,当不需要返回 _openid 和 created 字段,其他字段都返回时,可以这样写:

代码语言:javascript复制
utils.docs(
  c: 'xxx',
  except: '_openid, created'
)

但是,当使用 only 时,无论 only 中是否写了 _id,_id 都会返回。除非同时使用 except 显性排除了 _id,如:

代码语言:javascript复制
utils.docs(
  c: 'xxx',
  only: 'title, content',
  except: '_id'
)

这是因为通常使用 only 时,实际上需要 _id 字段,但是每次都写 _id 会很麻烦。

10.created 参数

created 参数用于控制是否给返回的数据添加创建时间字段,共有4个字段:created、created_str、yymmdd、hhmmss。

通常并不需要在创建数据时写入当前时间字段,因为可以从 _id 中分析出创建这个数据的时间。除非需要根据此字段进行排序或其他查询操作。

这4个时间字段的格式如下:

  • created:javascript 的 Date 对象
  • created_str:完整时间字符串,如:‘2024-07-26 12:02:00’
  • yymmdd:日期,如:‘2024-07-26’
  • hhmmss:时间,如:‘12:02:00’

注意:如果数据是在云函数中创建的,需要把云函数的时区设置为 UTC 8(即在云函数中添加 TZ=Asia/Shanghai 配置)。

样例代码如下:

代码语言:javascript复制
const docs = await utils.docs({
  c: 'xxx',
  created: true,
})
console.log(docs[0].created) // Date 时间对象
console.log(docs[0].yymmdd)  // '2024-07-26'
11.order_by 参数

order_by 参数用于控制返回数据的排序,当仅需根据一个字段升序排序时,可以直接写字段名,如:

代码语言:javascript复制
utils.docs({
  c: 'xxx',
  order_by: 'rank',
})

当需要使用降序或多字段排序时,需传入一个对象,如:

代码语言:javascript复制
utils.docs({
  c: 'xxx',
  order_by: {school: true, grade: 'desc', 'math.score': 0}
})

以上查询先按 school 升序,再按 grade 降序,最后按 math.score 降序。

在 order_by 中,‘asc’、1 或 true 均表示升序,‘desc’、0 或 false 均表示降序。

12.mine 参数与 myDocs 函数

由于使用了“自定义安全规则”且读取规则为 auth.openid == doc._openid(以后简称“自己的数据”),此时系统要求在查询数据时必须在 where 中添加 _openid: ‘{openid}’ 条件,否则会抛出没有权限的异常,如图所示:

当 mine=true 时,docs 函数会自动添加 _openid: ‘{openid}’ 条件。

但这样在阅读代码时语义不直观,因此建议用 myDocs 代替,myDocs 的参数和功能与 docs 一致,只是 mine 参数默认为 true。

以后还会有许多类似的函数对,如 getDoc、getMyDoc 等。记住一个简单的原则即可:使用 utils 库时,当操作“自己的数据”时,请使用对应的 my 函数。

13.其他依赖工具函数

utils.docs 依赖了其他 utils 中的函数,部分被依赖的函数如下(具体实现请在代码库中查看):

代码语言:javascript复制
const utils = {

  // 判断值是否为undefined或null
  isNone (i) {},

  // 判断值是否为空对象{}、空数组[]、空字符串''、空内容串'  '、undefined、null
  isEmpty (i) {},

  // 判断值是否为数组
  isArray(i) {},

  // 判断值是object但不是数组、null、undefined
  isObject(i) {},

  // 判断值是否为字符串
  isString(i) {},

  // 拆分字符串,返回数组
  // 函数会过滤掉空字符串,并去除两边的空白
  split (s, char = ' ') {},

  // 判断某个元素是否在数组或对象中
  in (item, arr) {},

  // 从数据_id中获取时间,返回Date对象
  getTimeFromId (id) {},

  // 返回时间的年月日字符串,如:'2023-07-01'
  yymmdd (t) {},

  // 返回时间的时分秒字符串,如:'01:02:03'
  hhmmss (t) {},

  // 返回时间的完整字符串,如:'2023-07-01 01:02:03'
  dateToString (t) {},

}

随着本系列教程的深入,utils.js 会提供更多的工具函数,希望帮助提高开发效率。

14.Todolist 列表显示效果

目前已经完成了 todo 的添加功能,当在输入框中输入文字并点击“添加 todo”按钮时,会在页面中显示新的 todo。效果如图所示:

2. Todolist 的完成与删除

1.更新数据函数:utils.updateDoc 与 utils.updateMyDoc

utils.js 中提供了数据更新函数,用于更新一个文档,代码如下:

代码语言:javascript复制
updateDoc (c, id, {_openid, _id, ...d}, {mine = false} = {}) {
  const _ = this
  const w = {_id: id}
  return new Promise((resolve, reject) => {
    _.coll(c)
      .where({...w, ...(mine ? {_openid: '{openid}'} : {})})
      .limit(1)
      .update({data: d})
      .then(res => {
        if(res.stats.updated > 0){
          resolve(true)
        } else {
          resolve(false)
        }
      })
      .catch(reject)
  })
},

updateMyDoc (c, id, d) {
  return this.updateDoc(c, id, d, {mine: true})
}

同样的,如果要更新“自己的数据”,请使用 updateMyDoc。这两个函数的前三个参数分别表示:

  • c:集合名称
  • id:文档的 _id
  • d:要更新的数据

这里数据 d 可以使用“点表示法”,如:

代码语言:javascript复制
utils.updateDoc('todo', todo_id, { 'author.name': '张三' })
  .then(res => {
    if(res === true){
      console.log('更新成功')
    } else {
      console.log('更新失败')
    }
  })

注意:只有文档存在且数据有变化时,res 才会返回 true。

2.在 updateDoc 中排除 _openid 和 _id 字段

系统的 update 函数不允许传入 _openid 和 _id 字段,否则会抛出异常,如图所示:

但是,在实际开发中,经常会先读取一个 doc 文档,然后修改这个文档后使用当前 doc 去更新,如:

代码语言:javascript复制
const doc = await utils.getDoc('todo', todo_id)

doc.status = '已完成'

// ... 其他修改doc的代码

utils.updateDoc('todo', todo_id, doc) // 这里的 doc 中包含了 _openid 和 _id 字段

此时因为 doc 中包含了 _openid 和 _id 字段,所以会抛出上图所示异常。

为了解决这个问题,在 updateDoc 函数中使用了解构赋值 {_openid, _id, …d} 来排除 _openid 和 _id 字段。

现在就不需要担心向 updateDoc 和 updateMyDoc 函数传入 _openid 和 _id 了。

为什么不直接delete doc._openid?,原因一是可能需要使用 _id,原因二是可能会引起其他代码的 bug。

3.设置 todo 已完成

为了实现点击 todo 左边的圆圈时把 todo 的状态改为“已完成”,先在todo.wxml 绑定 completeTodo 事件(完整代码请看代码库):

代码语言:javascript复制
<label wx:for="{{todo_list}}">
  <view bind:tap="completeTodo" data-id="{{item._id}}">
    <!-- 这里是一个圆圈 -->
  </view>
</label>

然后在 todo.js 中实现 completeTodo 函数:

代码语言:javascript复制
async completeTodo (e) {
  const _ = this
  const { id } = e.currentTarget.dataset
  utils.updateMyDoc('todo', id, { status: '已完成' })
    .then(() => {
      _.updateTodoList()
    })
}

修改 updateTodoList 函数,把已完成的 todo 放到 data.done_list 中:

代码语言:javascript复制
async updateTodoList () {
  const _ = this

  _.setData({
    todo_list: await utils.myDocs({c: 'todo', w: {status: '未完成'} }),
    done_list: await utils.myDocs({c: 'todo', w: {status: '已完成'} }),
  })

},

点击圆圈后,todo 会从未完成移动到已完成中,效果如图所示:

提示:这里只是为了演示数据库查询功能,实际开发时不应该在 updateTodoList 中读取数据库

4.删除数据函数:utils.removeDoc 与 utils.removeMyDoc

删除数据的函数如下:

代码语言:javascript复制
removeDoc (c, id, {mine = false} = {}) {
  const _ = this
  const w = {_id: id}
  return new Promise((resolve, reject) => {
    _.coll(c)
      .where({...w, ...(mine ? {_openid: '{openid}'} : {})})
      .limit(1)
      .remove()
      .then(async res => {
        if(res.stats.removed > 0){
          resolve(true)
        } else {
          resolve(false)
        }
      })
      .catch(e => {
        resolve(false)
      })
  })
},

removeMyDoc (c, id) {
  return this.removeDoc(c, id, {mine: true})
}

同样删除“自己的数据”时要使用 removeMyDoc 函数。

5.删除 todo

为了简化本教程的操作,直接使用 longtap 事件来删除 todo,可以在未完成和已完成的 todo 中添加如下代码:

代码语言:javascript复制
<view class="weui-cell__bd"
    bind:longtap="deleteTodo" data-id="{{item._id}}" >
  <view>{{item.title}}</view>
</view>

然后在 todo.js 中实现 deleteTodo 函数后就可以长按 todo 来删除它了。

代码语言:javascript复制
async deleteTodo (e) {
  const _ = this
  const { id } = e.currentTarget.dataset
  utils.removeMyDoc('todo', id).then(_.updateTodoList)
}

3. 代码库

本系列教程搭配github代码库:sdjl/WxMpCloudBooster,可以使用 git checkout article n 来切换到第 n 篇文章对应的代码。

代码语言:javascript复制
# 获取项目
git clone https://github.com/sdjl/WxMpCloudBooster.git

# 切换到文章二对应的代码库
cd WxMpCloudBooster
git checkout article2

预告

在下篇文章中,会进一步介绍更多的数据库读写函数,以及提供云函数中操作数据库的版本,并探讨数据库在使用上有哪些限制。届时将结束数据库的基础教程,之后会开启其他内容的学习之旅。

原创为腾讯云开发布道师 刘永辉

0 人点赞