小程序-云开发

小程序-云开发

小程序-云开发

小程序·云开发

小程序·云开发是微信团队联合腾讯云推出的专业的小程序开发服务。

开发者可以使用云开发快速开发小程序、小游戏、公众号网页等,并且原生打通微信开放能力。

开发者无需搭建服务器,可免鉴权直接使用平台提供的 API 进行业务开发。

什么是小程序云开发

  • 传统的小程序开发:前端+后端(各种后端,运维的问题)

    image.png

  • 云开发模式:小程序端+云开发(充当后台)

    image.png

云开发优势

  • 快速上线
  • 专注核心业务
  • 独立开发一个完整的微信小程序
  • 学习成本低,不需要学习新技术
  • 无需关注系统运维
  • 数据安全
image.png

能力概览

  • 云函数
  • 云数据库:json 数据库
  • 云存储
  • 云调用:云调用是云开发提供的基于云函数使用小程序开放接口的能力
  • HTTP API:服务端可以访问云资源,实现与云开发互通
image.png

配置环境开发

准备工作

  1. 下载并安装微信开发者工具
  2. 注册微信小程序,获取小程序的 AppID;
第 1 步:创建项目

打开并登录微信开发者工具,新建小程序项目,填入 AppID,后端服务选择“小程序·云开发”并勾选同意"云开发服务条款":

image.png

点击创建后,即可得到一个展示云开发基础能力的示例小程序:

image.png

第 2 步:开通云开发

在使用云开发能力之前,需要先开通云开发。

在开发者工具的工具栏左侧,点击 “云开发” 按钮即可打开云控制台,根据提示开通云开发,并且创建一个新的云开发环境。

image.png

  • 点击云开发,会打开云控制台
  • 每个appid,能够开发两个免费的云环境,每个环境都有独立的云函数、云数据库等
  • 默认已经开通了一个名为 cloud1 的环境
  • 每个环境相互隔离,拥有唯一的环境 ID,包含独立的数据库实例、存储空间、云函数配置等资源;
  • 初始创建的环境自动成为默认环境
  • 默认配额下可以创建两个环境。
第 3 步:开始开发

开通创建环境后,即可以开始在模拟器上操作小程序体验云开发提供的部分基础能力演示。

第三方快速注册的小程序

通过第三方快速注册的小程序也支持开通并使用云开发,可通过接口开通以及控制台操作开通;

详细流程请参考以下文档:

第三方快速注册的小程序支持云开发

第三方快速注册小程序支持云开发

第三方快速创建的小程序支持使用云开发目前有三种方式:接口开通、开发者工具开通、小程序后台系统开通;

方式一:接口开通

1.第三方通过接口查询小程序是否已经绑定了手机号,如未绑定可推送模板消息给小程序管理员微信收集手机号;

2.如已绑定或小程序管理员绑定好手机号后,调用接口开通云开发即可开通;

查询是否绑定手机号

通过本接口可以查询小程序是否绑定了手机号,并支持推送模版消息给小程序管理员收集手机号。使用过程中如遇到问题,可在开放平台服务商专区发帖交流。

请求地址
POST https://api.weixin.qq.com/tcb/checkmobile?access_token=ACCESS_TOKEN
请求参数
属性类型默认值必填说明
access_tokenString第三方平台接口调用令牌authorizer_access_token
push_tmplBoolfalse是否在小程序未绑定手机号时推送模版消息给管理员收集手机号
返回值

返回的 JSON 数据包

属性类型说明
errcodenumber错误码
errmsgstring错误信息
has_mobileBool是否绑定了手机号
errcode 的合法值
说明最低版本
0请求成功
-1系统错误
-1000系统错误
40014AccessToken 不合法
40101缺少必填参数
41001缺少AccessToken
42001AccessToken过期
43002HTTP METHOD 错误
其他错误码云开发错误码
POST 数据示例
{
    "push_tmpl": true
}
返回数据示例
{
    "errcode": 0,
    "errmsg": "ok",
    "has_mobile": true
}
方式二:开发者开通

1.第三方登录微信开发者工具中,选择该快速创建的小程序项目进入到开发者界面,点击“云开发”;

2.如该快速注册的小程序未绑定手机号,则弹窗提示发送模板消息给小程序管理员以收集手机号开通云开发;

3.点击发送则小程序管理员将收到一条模板消息,小程序管理员绑定手机号后,再次点击“云开发”,即可进入云开发开通界面。

方式三:小程序后台系统开通

1.小程序管理员进入小程序后台管理系统,进入”成员管理-所有成员-管理员-修改-添加手机号“,添加好手机号后提交; img

2.进入”开发-云开发-小程序云开发开通“,填写环境名称后确定即可开通云开发。 img

创建测试环境

一般我们开发时,一个环境用于测试,一个环境用于项目正式上线

cloud1用于我们正式上线,再创建一个环境用于测试

现在我们创建一个名为 test 新的环境,由于我以前已经创建了,所以它已经存在了

image.png

image.png

image.png

提交订单后,开始初始化环境

选择云环境

云环境创建后,可以在微信开发者工具中选择云环境

云环境创建好后,建议先重启 微信开发者工具,就可以看到环境了

image.png

选择 test 作为开发时的测试环境

目录结构

云开发项目目录

[小程序代码构成](小程序代码构成 | 微信开放文档 (qq.com))

image.png

没有使用云开发的项目目录

image.png

很明显,未使用云开发的项目目录结构比云开发项目要简单一些

设置云环境
资源环境

一个环境对应一整套独立的云开发资源,包括数据库、存储空间、云函数等资源。各个环境是相互独立的,用户开通云开发后即创建了一个环境,默认可拥有最多两个环境。在实际开发中,建议每一个正式环境都搭配一个测试环境,所有功能先在测试环境测试完毕后再上到正式环境。以初始可创建的两个环境为例,建议一个创建为 test 测试环境,一个创建为 release 正式环境。

为了方便开发者调试,从开发者工具 1.02.1905302 及基础库 2.7.1 起,在 wx.cloud.init 后会在调试器中输出 SDK 中所使用的默认环境:

devtools-network-cloud-init

同时,在 Network 面板中会输出各个云开发操作的请求详情,其中包括该调用所请求的环境 ID:

devtools-network-env

修改项目环境

在前面只是为云函数选择了云环境,就是在本地编写好云函数后,云函数将会上传到哪个云环境

这里要设置的是开发时或者程序运行时使用的云环境,如果选择错误,则可能找不到前面创建的云函数

打开 app.js

image.png

如何为用户开发项目

为自己开发小程序项目很简单,因为我们本身就懂技术

如何为客户开发呢?

  • 小程序应该是客户申请的小程序
  • 我们登录微信开发者工具时,使用自己的微信进行扫码登录,所以微信开发者工具是与开发者挂钩的
  • 主要在 app.json 中将 appid 修改为客户的小程序的 appid,那么我们就会使用客户的云环境、代 码也会上传到客户的小程序后台

基础概念

云开发能力

数据库

云开发提供了一个 JSON 数据库,顾名思义,数据库中的每条记录都是一个 JSON 格式的对象。一个数据库可以有多个集合(相当于关系型数据中的表),集合可看做一个 JSON 数组,数组中的每个对象就是一条记录,记录的格式是 JSON 对象。

关系型数据库和 JSON 数据库的概念对应关系如下表:

关系型文档型
数据库 database数据库 database
表 table集合 collection
行 row记录 record / doc
列 column字段 field

数据库 API 分为小程序端和服务端两部分,小程序端 API 拥有严格的调用权限控制,开发者可在小程序内直接调用 API 进行非敏感数据的操作。对于有更高安全要求的数据,可在云函数内通过服务端 API 进行操作。云函数的环境是与客户端完全隔离的,在云函数上可以私密且安全的操作数据库。

数据库 API 包含增删改查的能力,使用 API 操作数据库只需三步:获取数据库引用、构造查询/更新条件、发出请求。以下是一个在小程序中查询数据库的发表于美国的图书记录的例子:

// 1. 获取数据库引用
const db = wx.cloud.database()
// 2. 构造查询语句
// collection 方法获取一个集合的引用
// where 方法传入一个对象,数据库返回集合中字段等于指定值的 JSON 文档。API 也支持高级的查询条件(比如大于、小于、in 等),具体见文档查看支持列表
// get 方法会触发网络请求,往数据库取数据
db.collection('books').where({
  publishInfo: {
    country: 'United States'
  }
}).get({
  success: function(res) {
  // 输出 [{ "title": "The Catcher in the Rye", ... }]
  console.log(res)
 }
})

更多的数据库的 API 的使用和数据库管理,可以参考数据库指引章节。

存储

云开发提供了一块存储空间,提供了上传文件到云端、带权限管理的云端下载能力,开发者可以在小程序端和云函数端通过 API 使用云存储功能。

在小程序端可以分别调用 wx.cloud.uploadFilewx.cloud.downloadFile 完成上传和下载云文件操作。下面简单的几行代码,即可实现在小程序内让用户选择一张图片,然后上传到云端管理的功能:

// 让用户选择一张图片
wx.chooseImage({
  success: chooseResult => {
    // 将图片上传至云存储空间
    wx.cloud.uploadFile({
      // 指定上传到的云路径
      cloudPath: 'my-photo.png',
      // 指定要上传的文件的小程序临时文件路径
      filePath: chooseResult.tempFilePaths[0],
      // 成功回调
      success: res => {
        console.log('上传成功', res)
      },
    })
  },
})

上传完成后可在控制台中看到刚上传的图片。

更多的存储 API 和管理,可以参考存储指引章节。

云函数

云函数是一段运行在云端的代码,无需管理服务器,在开发工具内编写、一键上传部署即可运行后端代码。

小程序内提供了专门用于云函数调用的 API。开发者可以在云函数内使用 wx-server-sdk 提供的 getWXContext 方法获取到每次调用的上下文(appidopenid 等),无需维护复杂的鉴权机制,即可获取天然可信任的用户登录态(openid)。

云调用

云调用是云开发提供的基于云函数使用小程序开放接口的能力,支持在云函数调用服务端开放接口,如发送模板消息、获取小程序码等操作都可以在云函数中完成,详情可见具体开发指引

HTTP API

云开发资源也可以通过 HTTP 接口访问,即在小程序外访问,接口见HTTP API 文档

通过这个章节,我们已经了解了云开发是什么,提供了哪些能力,能做什么,接下来跟着我们一起进入开发指引的章节,看看如何上手开发吧!

基础能力

数据库

创建第一个集合

打开控制台,选择 “数据库” 标签页,通过 “添加集合” 入口创建一个集合。假设我们要创建一个待办事项小程序,我们创建一个名为 todos 的集合。创建成功后,可以看到 todos 集合管理界面,界面中我们可以添加记录、查找记录、管理索引和管理权限。

image.png

创建第一条记录

控制台提供了可视化添加数据的交互界面,点击 “添加记录” 添加我们的第一条待办事项:

{
  // 描述,String 类型
  "description": "learn mini-program cloud service",
  // 截止时间,Date 类型
  "due": Date("2018-09-01"),
  // 标签,Array 类型
  "tags": [
    "tech",
    "mini-program",
    "cloud"
  ],
  // 个性化样式,Object 类型
  "style": {
    "color": "red"
  },
  // 是否已完成,Boolean 类型
  "done": false
}

添加完成后可在控制台中查看到刚添加的数据。

数据类型

云开发数据库提供以下几种数据类型:

  • String:字符串
  • Number:数字
  • Object:对象
  • Array:数组
  • Bool:布尔值
  • Date:时间
  • Geo:多种地理位置类型,详见下
  • Null

下面对几个需要额外说明的字段做下补充说明。

Date

Date 类型用于表示时间,精确到毫秒,在小程序端可用 JavaScript 内置 Date 对象创建。需要特别注意的是,在小程序端创建的时间是客户端时间,不是服务端时间,这意味着在小程序端的时间与服务端时间不一定吻合,如果需要使用服务端时间,应该用 API 中提供的 serverDate 对象来创建一个服务端当前时间的标记,当使用了 serverDate 对象的请求抵达服务端处理时,该字段会被转换成服务端当前的时间,更棒的是,我们在构造 serverDate 对象时还可通过传入一个有 offset 字段的对象来标记一个与当前服务端时间偏移 offset 毫秒的时间,这样我们就可以达到比如如下效果:指定一个字段为服务端时间往后一个小时。

那么当我们需要使用客户端时间时,存放 Date 对象和存放毫秒数是否是一样的效果呢?不是的,我们的数据库有针对日期类型的优化,建议大家使用时都用 DateserverDate 构造时间对象。

地理位置

db.Geo.Point 外,需小程序基础库版本 2.6.3 或以上。控制台需开发者工具版本 1.02.1903251 或以上。

云开发数据库提供了多种地理位置数据类型的增删查改支持,支持的地理位置数据类型有以下几种(API 文档):

字段说明最低基础库版本
Point2.2.3
LineString线段2.6.3
Polygon多边形2.6.3
MultiPoint点集合2.6.3
MultiLineString线段集合2.6.3
MultiPolygon多边形集合2.6.3

要使用地理位置查询功能时,必须建立地理位置索引,建议用于存储地理位置数据的字段均建立地理位置索引。地理位置索引可在云控制台建立索引的入口中选择地理位置索引(2dsphere)。

具体的使用方法可参见 API 文档

Null

null 相当于一个占位符,表示一个字段存在但是值为空。

权限控制

数据库的权限分为小程序端和管理端,管理端包括云函数端和控制台。小程序端运行在小程序中,读写数据库受权限控制限制,管理端运行在云函数上,拥有所有读写数据库的权限。云控制台的权限同管理端,拥有所有权限。小程序端操作数据库应有严格的安全规则限制。

我们提供了两种权限控制方案,第一种是初期提供的基础的四种简易权限设置,第二种是灵活的、可自定义的权限控制,即数据库安全规则。

  • 安全规则

  • 简易权限配置权限控制

    数据库的权限分为小程序端和管理端,管理端包括云函数端和控制台。小程序端运行在小程序中,读写数据库受权限控制限制,管理端运行在云函数上,拥有所有读写数据库的权限。云控制台的权限同管理端,拥有所有权限。小程序端操作数据库应有严格的安全规则限制。

    我们提供了两种权限控制方案,第一种是初期提供的基础的四种简易权限设置,第二种是灵活的、可自定义的权限控制,即数据库安全规则。

数据库增删改查 (SDK)
方法作用
const db = wx.cloud.database()初始化
add插入数据
get()查询数据
db.collection(‘表名’).doc(‘唯一字段名’).get()获取一个记录的数据
db.collection(‘表名’).where()获取多个记录的数据
db.collection(‘表名’).get()获取一个集合的数据
update局部更新一个或多个记录
set替换更新一个记录
remove删除数据
初始化

在开始使用数据库 API 进行增删改查操作之前,需要先获取数据库的引用。以下调用获取默认环境的数据库的引用:

const db = wx.cloud.database()

如需获取其他环境的数据库引用,可以在调用时传入一个对象参数,在其中通过 env 字段指定要使用的环境。此时方法会返回一个对测试环境数据库的引用。

示例:假设有一个环境名为 test,用做测试环境,那么可以如下获取测试环境数据库:

const testDB = wx.cloud.database({
  env: 'test'
})

要操作一个集合,需先获取它的引用。在获取了数据库的引用后,就可以通过数据库引用上的 collection 方法获取一个集合的引用了,比如获取待办事项清单集合:

const todos = db.collection('todos')

获取集合的引用并不会发起网络请求去拉取它的数据,我们可以通过此引用在该集合上进行增删查改的操作,除此之外,还可以通过集合上的 doc 方法来获取集合中一个指定 ID 的记录的引用。同理,记录的引用可以用于对特定记录进行更新和删除操作。

假设我们有一个待办事项的 ID 为 todo-identifiant-aleatoire,那么我们可以通过 doc 方法获取它的引用:

const todo = db.collection('todos').doc('todo-identifiant-aleatoire')
插入数据

可以通过在集合对象上调用 add 方法往集合中插入一条记录。还是用待办事项清单的例子,比如我们想新增一个待办事项:

db.collection('todos').add({
  // data 字段表示需新增的 JSON 数据
  data: {
    // _id: 'todo-identifiant-aleatoire', // 可选自定义 _id,在此处场景下用数据库自动分配的就可以了
    description: "learn cloud database",
    due: new Date("2018-09-01"),
    tags: [
      "cloud",
      "database"
    ],
    // 为待办事项添加一个地理位置(113°E,23°N)
    location: new db.Geo.Point(113, 23),
    done: false
  },
  success: function(res) {
    // res 是一个对象,其中有 _id 字段标记刚创建的记录的 id
    console.log(res)
  }
})

当然,Promise 风格也是支持的,只要传入对象中没有 success, failcomplete,那么 add 方法就会返回一个 Promise:

db.collection('todos').add({
  // data 字段表示需新增的 JSON 数据
  data: {
    description: "learn cloud database",
    due: new Date("2018-09-01"),
    tags: [
      "cloud",
      "database"
    ],
    location: new db.Geo.Point(113, 23),
    done: false
  }
})
.then(res => {
  console.log(res)
})

数据库的增删查改 API 都同时支持回调风格和 Promise 风格调用。

在创建成功之后,我们可以在控制台中查看到刚新增的数据。

可以在 add API 文档中查阅完整的 API 定义。

查询数据

在记录和集合上都有提供 get 方法用于获取单个记录或集合中多个记录的数据。

假设我们已有一个集合 todos,其中包含以下格式记录:

[
  {
    _id: 'todo-identifiant-aleatoire',
    _openid: 'user-open-id', // 假设用户的 openid 为 user-open-id
    description: "learn cloud database",
    due: Date("2018-09-01"),
    progress: 20,
    tags: [
      "cloud",
      "database"
    ],
    style: {
      color: 'white',
      size: 'large'
    },
    location: Point(113.33, 23.33), // 113.33°E,23.33°N
    done: false
  },
  {
    _id: 'todo-identifiant-aleatoire-2',
    _openid: 'user-open-id', // 假设用户的 openid 为 user-open-id
    description: "write a novel",
    due: Date("2018-12-25"),
    progress: 50,
    tags: [
      "writing"
    ],
    style: {
      color: 'yellow',
      size: 'normal'
    },
    location: Point(113.22, 23.22), // 113.22°E,23.22°N
    done: false
  }
  // more...
]
获取一个记录的数据

我们先来看看如何获取一个记录的数据,假设我们已有一个 ID 为 todo-identifiant-aleatoire 的在集合 todos 上的记录,那么我们可以通过在该记录的引用调用 get 方法获取这个待办事项的数据:

db.collection('todos').doc('todo-identifiant-aleatoire').get({
  success: function(res) {
    // res.data 包含该记录的数据
    console.log(res.data)
  }
})

也可以用 Promise 风格调用:

db.collection('todos').doc('todo-identifiant-aleatoire').get().then(res => {
  // res.data 包含该记录的数据
  console.log(res.data)
})
获取多个记录的数据

我们也可以一次性获取多条记录。通过调用集合上的 where 方法可以指定查询条件,再调用 get 方法即可只返回满足指定查询条件的记录,比如获取用户的所有未完成的待办事项:

db.collection('todos').where({
  _openid: 'user-open-id',
  done: false
})
.get({
  success: function(res) {
    // res.data 是包含以上定义的两条记录的数组
    console.log(res.data)
  }
})

where 方法接收一个对象参数,该对象中每个字段和它的值构成一个需满足的匹配条件,各个字段间的关系是 “与” 的关系,即需同时满足这些匹配条件,在这个例子中,就是查询出 todos 集合中 _openid 等于 user-open-iddone 等于 false 的记录。在查询条件中我们也可以指定匹配一个嵌套字段的值,比如找出自己的标为黄色的待办事项:

db.collection('todos').where({
  _openid: 'user-open-id',
  style: {
    color: 'yellow'
  }
})
.get({
  success: function(res) {
    console.log(res.data)
  }
})

也可以用 “点表示法” 表示嵌套字段:

db.collection('todos').where({
  _openid: 'user-open-id',
  'style.color': 'yellow'
})
.get({
  success: function(res) {
    console.log(res.data)
  }
})
获取一个集合的数据

如果要获取一个集合的数据,比如获取 todos 集合上的所有记录,可以在集合上调用 get 方法获取,但通常不建议这么使用,在小程序中我们需要尽量避免一次性获取过量的数据,只应获取必要的数据。为了防止误操作以及保护小程序体验,小程序端在获取集合数据时服务器一次默认并且最多返回 20 条记录,云函数端这个数字则是 100。开发者可以通过 limit 方法指定需要获取的记录数量,但小程序端不能超过 20 条,云函数端不能超过 100 条。

db.collection('todos').get({
  success: function(res) {
    // res.data 是一个包含集合中有权限访问的所有记录的数据,不超过 20 条
    console.log(res.data)
  }
})

也可以用 Promise 风格调用:

db.collection('todos').get().then(res => {
  // res.data 是一个包含集合中有权限访问的所有记录的数据,不超过 20 条
  console.log(res.data)
})

下面是在云函数端获取一个集合所有记录的例子,因为有最多一次取 100 条的限制,因此很可能一个请求无法取出所有数据,需要分批次取:

const cloud = require('wx-server-sdk')
cloud.init()
const db = cloud.database()
const MAX_LIMIT = 100
exports.main = async (event, context) => {
  // 先取出集合记录总数
  const countResult = await db.collection('todos').count()
  const total = countResult.total
  // 计算需分几次取
  const batchTimes = Math.ceil(total / 100)
  // 承载所有读操作的 promise 的数组
  const tasks = []
  for (let i = 0; i < batchTimes; i++) {
    const promise = db.collection('todos').skip(i * MAX_LIMIT).limit(MAX_LIMIT).get()
    tasks.push(promise)
  }
  // 等待所有
  return (await Promise.all(tasks)).reduce((acc, cur) => {
    return {
      data: acc.data.concat(cur.data),
      errMsg: acc.errMsg,
    }
  })
}
指令

使用数据库 API 提供的 where 方法我们可以构造复杂的查询条件完成复杂的查询任务。在本节中我们还是使用上一章节中使用的示例数据。

查询指令

假设我们需要查询进度大于 30% 的待办事项,那么传入对象表示全等匹配的方式就无法满足了,这时就需要用到查询指令。数据库 API 提供了大于、小于等多种查询指令,这些指令都暴露在 db.command 对象上。比如查询进度大于 30% 的待办事项:

const _ = db.command
db.collection('todos').where({
  // gt 方法用于指定一个 "大于" 条件,此处 _.gt(30) 是一个 "大于 30" 的条件
  progress: _.gt(30)
})
.get({
  success: function(res) {
    console.log(res.data)
  }
})

API 提供了以下查询指令:

查询指令说明
eq等于
neq不等于
lt小于
lte小于或等于
gt大于
gte大于或等于
in字段值在给定数组中
nin字段值不在给定数组中

更多具体的查询指令 API 文档可参考数据库 API 文档

逻辑指令

除了指定一个字段满足一个条件之外,我们还可以通过指定一个字段需同时满足多个条件,比如用 and 逻辑指令查询进度在 30% 和 70% 之间的待办事项:

const _ = db.command
db.collection('todos').where({
  // and 方法用于指定一个 "与" 条件,此处表示需同时满足 _.gt(30) 和 _.lt(70) 两个条件
  progress: _.gt(30).and(_.lt(70))
})
.get({
  success: function(res) {
    console.log(res.data)
  }
})

既然有 and,当然也有 or 了,比如查询进度为 0 或 100 的待办事项:

const _ = db.command
db.collection('todos').where({
  // or 方法用于指定一个 "或" 条件,此处表示需满足 _.eq(0) 或 _.eq(100)
  progress: _.eq(0).or(_.eq(100))
})
.get({
  success: function(res) {
    console.log(res.data)
  }
})

如果我们需要跨字段进行 “或” 操作,可以做到吗?答案是肯定的,or 指令还可以用来接受多个(可以多于两个)查询条件,表示需满足多个查询条件中的任意一个,比如我们查询进度小于或等于 50% 或颜色为白色或黄色的待办事项:

const _ = db.command
db.collection('todos').where(_.or([
  {
    progress: _.lte(50)
  },
  {
    style: {
      color: _.in(['white', 'yellow'])
    }
  }
]))
.get({
  success: function(res) {
    console.log(res.data)
  }
})

具体的逻辑查询指令 API 文档可参考数据库 Command API 文档

更新数据

在这章节我们一起看看如何使用数据库 API 完成数据更新,在本节中我们还是沿用读取数据章节中使用的数据。

更新数据主要有两个方法:

API说明
update局部更新一个或多个记录
set替换更新一个记录
局部更新

使用 update 方法可以局部更新一个记录或一个集合中的记录,局部更新意味着只有指定的字段会得到更新,其他字段不受影响。

比如我们可以用以下代码将一个待办事项置为已完成:

db.collection('todos').doc('todo-identifiant-aleatoire').update({
  // data 传入需要局部更新的数据
  data: {
    // 表示将 done 字段置为 true
    done: true
  },
  success: function(res) {
    console.log(res.data)
  }
})

除了用指定值更新字段外,数据库 API 还提供了一系列的更新指令用于执行更复杂的更新操作,更新指令可以通过 db.command 取得:

更新指令说明
set设置字段为指定值
remove删除字段
inc原子自增字段值
mul原子自乘字段值
push如字段值为数组,往数组尾部增加指定值
pop如字段值为数组,从数组尾部删除一个元素
shift如字段值为数组,从数组头部删除一个元素
unshift如字段值为数组,往数组头部增加指定值

比如我们可以将一个待办事项的进度 +10%:

const _ = db.command
db.collection('todos').doc('todo-identifiant-aleatoire').update({
  data: {
    // 表示指示数据库将字段自增 10
    progress: _.inc(10)
  },
  success: function(res) {
    console.log(res.data)
  }
})

inc 指令而不是取出值、加 10 再写进去的好处在于这个写操作是个原子操作,不会受到并发写的影响,比如同时有两名用户 A 和 B 取了同一个字段值,然后分别加上 10 和 20 再写进数据库,那么这个字段最终结果会是加了 20 而不是 30。如果使用 inc 指令则不会有这个问题。

如果字段是个数组,那么我们可以使用 pushpopshiftunshift 对数组进行原子更新操作,比如给一条待办事项加多一个标签:

const _ = db.command
db.collection('todos').doc('todo-identifiant-aleatoire').update({
  data: {
    tags: _.push('mini-program')
  },
  success: function(res) {
    console.log(res.data)
  }
})

可能读者已经注意到我们提供了 set 指令,这个指令有什么用呢?这个指令的用处在于更新一个字段值为另一个对象。比如如下语句是更新 style.color 字段为 ‘blue’ 而不是把 style 字段更新为 { color: 'blue' } 对象:

const _ = db.command
db.collection('todos').doc('todo-identifiant-aleatoire').update({
  data: {
    style: {
      color: 'blue'
    }
  },
  success: function(res) {
    console.log(res.data)
  }
})

如果需要将这个 style 字段更新为另一个对象,可以使用 set 指令:

const _ = db.command
db.collection('todos').doc('todo-identifiant-aleatoire').update({
  data: {
    style: _.set({
      color: 'blue'
    })
  },
  success: function(res) {
    console.log(res.data)
  }
})

如果需要更新多个数据,需在 Server 端进行操作(云函数),在 where 语句后同样的调用 update 方法即可,比如将所有未完待办事项的进度加 10%:

// 使用了 async await 语法
const cloud = require('wx-server-sdk')
const db = cloud.database()
const _ = db.command

exports.main = async (event, context) => {
  try {
    return await db.collection('todos').where({
      done: false
    })
    .update({
      data: {
        progress: _.inc(10)
      },
    })
  } catch(e) {
    console.error(e)
  }
}

更完整详细的更新指令可以参考数据库 Command API 文档

替换更新

如果需要替换更新一条记录,可以在记录上使用 set 方法,替换更新意味着用传入的对象替换指定的记录:

const _ = db.command
db.collection('todos').doc('todo-identifiant-aleatoire').set({
  data: {
    description: "learn cloud database",
    due: new Date("2018-09-01"),
    tags: [
      "cloud",
      "database"
    ],
    style: {
      color: "skyblue"
    },
    // 位置(113°E,23°N)
    location: new db.Geo.Point(113, 23),
    done: false
  },
  success: function(res) {
    console.log(res.data)
  }
})

如果指定 ID 的记录不存在,则会自动创建该记录,该记录将拥有指定的 ID。

删除数据

在这章节我们一起看看如何使用数据库 API 完成数据删除,在本节中我们还是沿用读取数据章节中使用的数据。

删除一条记录

对记录使用 remove 方法可以删除该条记录,比如:

db.collection('todos').doc('todo-identifiant-aleatoire').remove({
  success: function(res) {
    console.log(res.data)
  }
})
删除多条记录

如果需要更新多个数据,需在 Server 端进行操作(云函数)。可通过 where 语句选取多条记录执行删除,只有有权限删除的记录会被删除。比如删除所有已完成的待办事项:

// 使用了 async await 语法
const cloud = require('wx-server-sdk')
const db = cloud.database()
const _ = db.command

exports.main = async (event, context) => {
  try {
    return await db.collection('todos').where({
      done: true
    }).remove()
  } catch(e) {
    console.error(e)
  }
}

在大多数情况下,我们希望用户只能操作自己的数据(自己的代表事项),不能操作其他人的数据(其他人的待办事项),这就需要引入权限控制了。

查询、更新数组/嵌套对象

我们可以对对象、对象中的元素、数组、数组中的元素进行匹配查询,甚至还可以对数组和对象相互嵌套的字段进行匹配查询/更新,下面我们从普通匹配开始讲讲如何进行匹配。

普通匹配

传入的对象的每个 <key, value> 构成一个筛选条件,有多个 <key, value> 则表示需同时满足这些条件,是的关系,如果需要关系,可使用 [command.or]((Command.or))

比如找出未完成的进度 50 的待办事项:

db.collection('todos').where({
  done: false,
  progress: 50
}).get()
匹配记录中的嵌套字段

假设在集合中有如下一个记录:

{
  "style": {
    "color": "red"
  }
}

如果我们想要找出集合中 style.colorred 的记录,那么可以传入相同结构的对象做查询条件或使用 ”点表示法“ 查询:

// 方式一
db.collection('todos').where({
  style: {
    color: 'red'
  }
}).get()

// 方式二
db.collection('todos').where({
  'style.color': 'red'
}).get()
匹配数组
匹配数组

假设在集合中有如下一个记录:

{
  "numbers": [10, 20, 30]
}

可以传入一个完全相同的数组来筛选出这条记录:

db.collection('todos').where({
  numbers: [10, 20, 30]
}).get()
匹配数组中的元素

如果想找出数组字段中数组值包含某个值的记录,那可以在匹配数组字段时传入想要匹配的值。如对上面的例子,可传入一个数组中存在的元素来筛选出所有 numbers 字段的值包含 20 的记录:

db.collection('todos').where({
  numbers: 20
}).get()
匹配数组第 n 项元素

如果想找出数组字段中数组的第 n 个元素等于某个值的记录,那在 <key, value> 匹配中可以以 字段.下标key,目标值为 value 来做匹配。如对上面的例子,如果想找出 number 字段第二项的值为 20 的记录,可以如下查询(注意:数组下标从 0 开始):

db.collection('todos').where({
  'numbers.1': 20
}).get()

更新也是类似,比如我们要更新 _idtest 的记录的 numbers 字段的第二项元素至 30:

db.collection('todos').doc('test').update({
  data: {
    'numbers.1': 30
  },
})
结合查询指令进行匹配

在对数组字段进行匹配时,也可以使用如 lt, gt 等指令,来筛选出字段数组中存在满足给定比较条件的记录。如对上面的例子,可查找出所有 numbers 字段的数组值中存在包含大于 25 的值的记录:

const _ = db.command
db.collection('todos').where({
  numbers: _.gt(25)
}).get()

查询指令也可以通过逻辑指令组合条件,比如找出所有 numbers 数组中存在包含大于 25 的值、同时也存在小于 15 的值的记录:

const _ = db.command
db.collection('todos').where({
  numbers: _.gt(25).and(_.lt(15))
}).get()
匹配并更新数组中的元素

如果想要匹配并更新数组中的元素,而不是替换整个数组,除了指定数组下标外,还可以:

1. 更新数组中第一个匹配到的元素

更新数组字段的时候可以用 字段路径.$ 的表示法来更新数组字段的第一个满足查询匹配条件的元素。注意使用这种更新时,查询条件必须包含该数组字段。

假如有如下记录:

{
  "_id": "doc1",
  "scores": [10, 20, 30]
}
{
  "_id": "doc2",
  "scores": [20, 20, 40]
}

让所有 scores 中的第一个 20 的元素更新为 25:

// 注意:批量更新需在云函数中进行
const _ = db.command
db.collection('todos').where({
  scores: 20
}).update({
  data: {
    'scores.$': 25
  }
})

如果记录是对象数组的话也可以做到,路径如 字段路径.$.字段路径

注意事项:

  • 不支持用在数组嵌套数组
  • 如果用 unset 更新操作符,不会从数组中去除该元素,而是置为 null
  • 如果数组元素不是对象、且查询条件用了 neqnotnin,则不能使用 $

2. 更新数组中所有匹配的元素

更新数组字段的时候可以用 字段路径.$[] 的表示法来更新数组字段的所有元素。

假如有如下记录:

{
  "_id": "doc1",
  "scores": {
    "math": [10, 20, 30]
  }
}

比如让 scores.math 字段所有数字加 10:

const _ = db.command
db.collection('todos').doc('doc1').update({
  data: {
    'scores.math.$[]': _.inc(10)
  }
})

更新后 scores.math 数组从 [10, 20, 30] 变为 [20, 30, 40]

如果数组是对象数组也是可以的,假如有如下记录:

{
  "_id": "doc1",
  "scores": {
    "math": [
      { "examId": 1, "score": 10 },
      { "examId": 2, "score": 20 },
      { "examId": 3, "score": 30 }
    ]
  }
}

可以更新 scores.math 下各个元素的 score 原子自增 10:

const _ = db.command
db.collection('todos').doc('doc1').update({
  data: {
    'scores.math.$[].score': _.inc(10)
  }
})
匹配多重嵌套的数组和对象

上面所讲述的所有规则都可以嵌套使用的,假设我们在集合中有如下一个记录:

{
  "root": {
    "objects": [
      {
        "numbers": [10, 20, 30]
      },
      {
        "numbers": [50, 60, 70]
      }
    ]
  }
}

我们可以如下找出集合中所有的满足 root.objects 字段数组的第二项的 numbers 字段的第三项等于 70 的记录:

db.collection('todos').where({
  'root.objects.1.numbers.2': 70
}).get()

注意,指定下标不是必须的,比如可以如下找出集合中所有的满足 root.objects 字段数组中任意一项的 numbers 字段包含 30 的记录:

db.collection('todos').where({
  'root.objects.numbers': 30
}).get()

更新操作也是类似,比如我们要更新 _idtestroot.objects 字段数组的第二项的 numbers 字段的第三项 为 80:

db.collection('todos').doc('test').update({
  data: {
    'root.objects.1.numbers.2': 80
  },
})
联表查询

版本要求:wx-server-sdk 1.3.0 或以上 不支持在小程序端使用

使用聚合查询的 lookup 聚合阶段可以进行联表查询,相关文档如下:

增删改查 (管理端)
控制台数据库高级操作

从开发者工具 1.02.1906202 开始,在云控制台数据库管理页中可以编写和执行数据库脚本,脚本可对数据库进行增删查改 & 聚合操作,语法同 SDK 数据库语法,具体见数据库脚本语法说明

数据库脚本

数据库脚本是用来执行数据库 CRUD & 聚合 操作的脚本,语法同云开发 SDK 数据库语法,目前可应用数据库脚本在以下场景:

  1. 控制台中可以使用数据库脚本进行高级数据库 CRUD & 聚合 管理操作 文档
  2. HTTP API 中的数据库接口 文档
数据库脚本语法

数据库脚本语法同 SDK 数据库语法,是 JavaScript 的真子集,出于安全考虑在语法上带有一定的限制性,以下是一个查询的脚本示例:

db.collection('test')
  .where({
    price: _.gt(10)
  })
  .field({
    name: true,
    price: true,
  })
  .orderBy('price', 'desc')
  .skip(1)
  .limit(100)
  .get()
全局变量

在脚本中提供以下全局变量:

变量名说明
db等于 wx.cloud.database() 的结果 (不区分环境)
_等于 db.command
语法规则

以下列举以 JavaScript 语法表达式出发解释主要的语法异同点(规则和限制):

表达式支持性示例
获取属性支持获取对象的合法属性,对象如 db_,合法属性如 dbcollection 属性db.collection
函数调用支持db.collection()
new支持new db.Geo.Point(113, 23)
变量声明支持变量声明,同时支持对象解构器的声明方式const Geo = db.Geo const { Point } = db.Geo
对象声明支持const obj = { age: _.gt(10) }
常量声明支持const max = 10
负数支持const min = -5
注释支持// comment /* comment */
其他不支持

不支持表达式简要一览:

  • 函数声明
  • 类声明
  • 变量赋值(不能声明后重新赋值)
  • 算术运算(+, -…)
  • 三值表达式(a ? b : c
  • 条件表达式(if, else
  • switch 表达式
  • 遍历表达式(for...in, for...of, …)
  • 数组解构器
  • try catch
报错提示

如果语法不正确,在错误信息中会给出错误原因出错的行列号,示例:

syntax errro

云函数

使用 npm

在云函数中我们可以引入第三方依赖来帮助我们更快的开发。云函数的运行环境是 Node.js,因此我们可以使用 npm 安装第三方依赖。比如除了使用 Node.js 提供的原生 http 接口在云函数中发起网络请求,我们还可以使用一个流行的 Node.js 网络请求库 request 来更便捷的发起网络请求。

注意,在 IDE 中选择上传云函数时,可以选择云端安装依赖(不上传 node_modules 文件夹)或全量上传(同时上传 node_modules 文件夹)。

在云函数中使用 wx-server-sdk

云函数属于管理端,在云函数中运行的代码拥有不受限的数据库读写权限和云文件读写权限。需特别注意,云函数运行环境即是管理端,与云函数中的传入的 openId 对应的微信用户是否是小程序的管理员 / 开发者无关。

云函数中使用 wx-server-sdk 需在对应云函数目录下安装 wx-server-sdk 依赖,在创建云函数时会在云函数目录下默认新建一个 package.json 并提示用户是否立即本地安装依赖。请注意云函数的运行环境是 Node.js,因此在本地安装依赖时务必保证已安装 Node.js,同时 nodenpm 都在环境变量中。如不本地安装依赖,可以用命令行在该目录下运行:

npm install --save wx-server-sdk@latest

在云函数中调用其他 API 前,同小程序端一样,也需要执行一次初始化方法:

const cloud = require('wx-server-sdk')
// 给定字符串环境 ID:接下来的 API 调用都将请求到环境 some-env-id
cloud.init({
  env: 'some-env-id'
})

或:

const cloud = require('wx-server-sdk')
// 给定 DYNAMIC_CURRENT_ENV 常量:接下来的 API 调用都将请求到与该云函数当前所在环境相同的环境
// 请安装 wx-server-sdk v1.1.0 或以上以使用该常量
cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV
})

wx-server-sdk 与小程序端的云 API 以同样的风格提供了数据库、存储和云函数的 API。下面提供几个简单的操作数据库、存储和云函数的示例:

云函数中调用数据库

假设在数据库中已有一个 todos 集合,我们可以如下方式取得 todos 集合的数据:

const cloud = require('wx-server-sdk')

cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV
})

const db = cloud.database()
exports.main = async (event, context) => {
  // collection 上的 get 方法会返回一个 Promise,因此云函数会在数据库异步取完数据后返回结果
  return db.collection('todos').get()
}
云函数中调用存储

假设我们要上传在云函数目录中包含的一个图片文件(demo.jpg):

const cloud = require('wx-server-sdk')
const fs = require('fs')
const path = require('path')

cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV
})

exports.main = async (event, context) => {
  const fileStream = fs.createReadStream(path.join(__dirname, 'demo.jpg'))
  return await cloud.uploadFile({
    cloudPath: 'demo.jpg',
    fileContent: fileStream,
  })
}

在云函数中,__dirname 的值是云端云函数代码所在目录

云函数中调用其他云函数

假设我们要在云函数中调用另一个云函数 sum 并返回 sum 所返回的结果:

const cloud = require('wx-server-sdk')

cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV
})

exports.main = async (event, context) => {
  return await cloud.callFunction({
    name: 'sum',
    data: {
      x: 1,
      y: 2,
    }
  })
}

更多的文档可参见API 文档

云函数本地调试功能

云开发提供了云函数本地调试功能,在本地提供了一套与线上一致的 Node.js 云函数运行环境,让开发者可以在本地对云函数调试,使用本地调试可以提高开发、调试效率:

  • 单步调试/断点调试:比起通过云开发控制台中查看线上打的日志的方法进行调试,使用本地调试后可以对云函数 Node.js 实例进行单步调试/断点调试
  • 集成小程序测试:在模拟器中对小程序发起的交互点击等操作如果触发了开启了本地调试的云函数,会请求到本地实例而不是云端
  • 优化开发流程、提高开发效率:调试阶段不需上传部署云函数,在调试云函数时,相对于不使用本地调试时的调试流程(“本地修改代码 -> 上传部署云函数 -> 调用")的调试流程,省去了上传等待的步骤,改成只需 “本地修改 -> 调用” 的流程,大大提高开发调试效率
注意:

在使用本地调试功能时,如果使用npm安装了插件,在我们使用云函数的本地调试功能时不会自动安装,会报错

image.png

这个使用需要我们在项目中手动安装一下 wx-server-sdk :

npm install --save wx-server-sdk@latest

定时触发器

该功能需开发者工具 1.02.1811270 及以上版本方可使用 从开发者工具 1.02.1910182 开始,新上传的定时触发器内支持使用云调用

如果云函数需要定时 / 定期执行,也就是定时触发,我们可以使用云函数定时触发器。配置了定时触发器的云函数,会在相应时间点被自动触发,函数的返回结果不会返回给调用方。

在需要添加触发器的云函数目录下新建文件 config.json,格式如下:

{
  // triggers 字段是触发器数组,目前仅支持一个触发器,即数组只能填写一个,不可添加多个
  "triggers": [
    {
      // name: 触发器的名字,规则见下方说明
      "name": "myTrigger",
      // type: 触发器类型,目前仅支持 timer (即 定时触发器)
      "type": "timer",
      // config: 触发器配置,在定时触发器下,config 格式为 cron 表达式,规则见下方说明
      "config": "0 0 2 1 * * *"
    }
  ]
}

字段规则:

  • 定时触发器名称 (name):最大支持 60 个字符,支持 a-z, A-Z, 0-9, -_。必须以字母开头,且一个函数下不支持同名的多个定时触发器。
  • 定时触发器触发周期 (config):指定的函数触发时间。填写自定义标准的 cron 表达式来决定何时触发函数。有关 cron 表达式的更多信息,请参考下面的内容。
Cron 表达式

Cron 表达式有七个必需字段,按空格分隔。

第一位第二位第三位第四位第五位第六位第七位
分钟小时星期

其中,每个字段都有相应的取值范围:

字段通配符
0-59 的整数, - * /
分钟0-59 的整数, - * /
小时0-23 的整数, - * /
1-31 的整数(需要考虑月的天数), - * /
1-12 的整数 或 JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC, - * /
星期0-6 的整数 或 SUN,MON,TUE,WED,THU,FRI,SAT;其中 0 指周日,1 指周二,依次类推,6 指周六, - * /
1970~2099 的整数, - * /

通配符

通配符含义
, (逗号)代表取用逗号隔开的字符的并集。例如:在“小时”字段中 1,2,3 表示1点、2点和3点
- (破折号)包含指定范围的所有值。例如:在“日”字段中,1-15 包含指定月份的 1 号到 15 号
* (星号)表示所有值。在“小时”字段中,* 表示每个小时
/ (正斜杠)指定增量。在“分钟”字段中,输入 1/10 以指定从第一分钟开始的每隔十分钟重复。例如,第 11 分钟、第 21 分钟和第 31 分钟,依此类推

注意事项

  • 在 Cron 表达式中的“日”和“星期”字段同时指定值时,两者为“或”关系,即两者的条件分别均生效。
  • 触发器规则的时区为 UTC+8

示例

下面展示了一些 Cron 表达式和相关含义的示例:

  • */5 * * * * * * 表示每5秒触发一次
  • 0 0 2 1 * * * 表示在每月的1日的凌晨2点触发
  • 0 15 10 * * MON-FRI * 表示在周一到周五每天上午10:15触发
  • 0 0 10,14,16 * * * * 表示在每天上午10点,下午2点,4点触发
  • 0 */30 9-17 * * * * 表示在每天上午9点到下午5点内每半小时触发
  • 0 0 12 * * WED * 表示在每个星期三中午12点触发

tcb-ruoter

使用原因:

  1. 微信小程序云开发云函数有个数限制
  2. 代码层级结构清晰,底层数据库操作函数可复用
  3. 还原真实前后端交互过程

安装

npm install --save tcb-router

引入

const TcbRouter = require('tcb-router');

创建实例

exports.main = async (event, context) => {
  const app = new TcbRouter({ event })
  }

云函数使用

// 云函数入口函数
exports.main = async (event, context) => {
//创建实例
  const app = new TcbRouter({ event })
  // 创建playlist路由
  app.router('playlist', async (ctx,next)=>{
    let res =await cloud.database().collection('playList').get()
    //ctx.body为返回给小程序端的数据
    ctx.body=res
  })
  //通过return app.serve()方式返回
 return app.serve();
}

小程序中使用

onReachBottom:async function () {
    wx.showLoading({
      title: '数据加载中',
    })
    let res= await wx.cloud.callFunction({
      name:'music',
      data:{
        $url:'playlist',
      }
    })
  },

一个小项目

实现底部导航

很多小程序都有一个底部导航

主要目录和文件
  • app.json:小程序跟目录下有一个 app.json ,这个是全局配置文件
  • pages目录:还有一个 pages 目录,里面是我们自己编写的一个个页面,其实就是页面级组件,跟 咱们 vue 项目中的 views 是一个意思
  • components目录:跟咱们 vue 项目一样,用于编写一些非页面级组件,也就是局部组件
app.json

这是当前项目的 app.json 配置,是一个对象

包含 pages、window、sitemapLocation、style 选项,完整的配置选项参考 [全局配置](全局配置 | 微信开放文档 (qq.com))

{
  "pages": [
    "pages/index/index",
    "pages/userConsole/userConsole",
    "pages/storageConsole/storageConsole",
    "pages/databaseGuide/databaseGuide",
    "pages/addFunction/addFunction",
    "pages/deployFunctions/deployFunctions",
    "pages/chooseLib/chooseLib",
    "pages/openapi/openapi",
    "pages/openapi/serverapi/serverapi",
    "pages/openapi/callback/callback",
    "pages/openapi/cloudid/cloudid",
    "pages/im/im",
    "pages/im/room/room"
  ],
  "window": {
    "backgroundColor": "#F6F6F6",
    "backgroundTextStyle": "light",
    "navigationBarBackgroundColor": "#F6F6F6",
    "navigationBarTitleText": "云开发 QuickStart",
    "navigationBarTextStyle": "black"
  },
  "sitemapLocation": "sitemap.json",
  "style": "v2"
}

这里有两个选项大家需要现在就搞明白

  • pages:
    • 用于指定小程序由哪些页面组成,每一项都对应一个页面的 路径(含文件名) 信息。文件名不需要写文件后缀,框架会自动去寻找对应位置的 .json , .js , .wxml , .wxss 四个文件进行处理
    • 未指定 entryPagePath 时,数组的第一项代表小程序的初始页面(首页)
    • 也就是说,我们在 index 中创建的所有页面都必须在这里注册
  • tabBar:
    • 如果小程序是一个多 tab 应用(客户端窗口的底部或顶部有 tab 栏可以切换页面),可以通过 tabBar 配置项指定 tab 栏的表现,以及 tab 切换时显示的对应页面。完整配置
实现底部导航
删除无关页面

因为这是创建的一个示例项目,所以先删除 pages 中无关的文件,全部删除即可

删除之后,将 app.json 中 pages 数组的内容清空

创建新的page

开发者工具中创建新的 page 方法很多

一种比较简单但不稳定的方式就是在 pages 中直接编写 page 路径,然后会自动在 pages 目录下生成

 "pages": [
    "pages/music/music",
    "pages/blog/blog",
    "pages/profile/profile",
    "pages/test/test",
    "pages/musiclist/musiclist",
    "pages/player/player"
  ],

每个页面由四个文件组成

  • .wxml:相当于我们以前学习的 html,只不过可以进行数据绑定,列表渲染等,其实就与我们学习 的 vue 组件中的模板一样
  • .wxss:WXSS 具有 CSS 大部分特性。同时为了更适合开发微信小程序,WXSS 对 CSS 进行了扩充 以及修改,就相当于 vue 组件中的样式
  • .js:编写业务逻辑
  • .json:页面级的配置文件

总结:

我们 vue 项目中,每个组件中包含模板、样式和js业务代码

小程序的 page 相当于将一个组件拆分成几个部分,每个文件编写不同的代码

配置底部导航

通过 tabBar 配置底部导航栏

tabBar 的值是一个对象

对象中有三个属性比较重要

  • list 属性,配置底部导航显示的页面
  • color:tab 上的文字默认颜色,仅支持十六进制
  • selectedColor:tab 上的文字选中时的颜色,仅支持十六进制
"tabBar": {
    "color": "#474747",
    "selectedColor": "#d43c43",
    "list": [
      {
        "pagePath": "pages/music/music",
        "text": "音乐",
        "iconPath": "images/icon/音乐_music170.png",
        "selectedIconPath": "images/icon/音乐_music170-选中.png"
      },
      {
        "pagePath": "pages/blog/blog",
        "text": "发现",
        "iconPath": "images/icon/发现.png",
        "selectedIconPath": "images/icon/发现-选中.png"
      },
      {
        "pagePath": "pages/profile/profile",
        "text": "我的",
        "iconPath": "images/icon/我的.png",
        "selectedIconPath": "images/icon/我的-选中.png"
      }
    ]
  }

图片可以从 iconfont 中下载,放到 images 目录中

image.png

修改顶部导航
"window": {
    "backgroundColor": "#FFC0CB",
    "backgroundTextStyle": "light",
    "navigationBarBackgroundColor": "#FFC0CB",
    "navigationBarTitleText": "AC 辰橙音乐",
    "navigationBarTextStyle": "black"
  },

image.png

音乐页

轮播图
使用小程序内置组件

music.wxml:

<swiper indicator-dots="{{indicatorDots}}" autoplay="{{autoplay}}" interval="{{interval}}" duration="{{duration}}">
  <block wx:for="{{musicImgs}}" wx:key="id">
    <swiper-item>
      <image src="{{item.url}}" mode="aspectFill" class="swiper-image"></image>
    </swiper-item>
  </block>
</swiper>

music.wxss:

.swiper-image{
  width: 100%;
  height: 100%;
}

music.js

musicImgs: [
      { id: 1, url:
      'http://5b0988e595225.cdn.sohucs.com/images/20171014/58439640cba1419c9a5cbf5c30c6f693.jpeg' },
      { id: 2, url:
      'https://p4.music.126.net/TckmIUgzalFCP4Y4aHJaIg==/109951163420280200.jpg?param=640y300' },
      { id: 3, url:
      'https://p3.music.126.net/1Pni3ofqAjqc-LPO8bP9Tw==/109951162805373493.jpg?param=640y300' },
      {id:4,url:'https://p3.music.126.net/14ZX4HQ-x0EQIqNPTa9ZXw==/109951163445849595.jpg?param=640y300'}
      ],
      indicatorDots: true,
      vertical: false,
      autoplay: true,
      interval: 2000,
      duration: 500,

总结:

  • swiper-item 的默认宽度与父元素一样,会随着屏幕的变化而变化,但是 swiper-item 中的元素并不是,所以设置图片宽度和高度为 100%
  • block 组件是干嘛的?
    • block只是一个标签: <block></block>
    • 并且 block 标签是不会被渲染出来的
    • block只接收控制属性: wx:if || wx:for
    • 那么它的作用到底是什么?
      • 它的作用就是充当一个容器,类似于 <view></view>
      • 举个例子: 当我们使用 一个 判断条件 决定 是否显示或者隐藏 多个标签时,通常我们会在外部包裹一个容器,这样方便于我们进行判断,但是在外层的容器<view>标签就只是一个单纯的容器,没有其他作用,并且它会被渲染出来,消耗性能
      • 而 block 则可以起到同样的作用,并且不会被渲染出来
组件

组件是什么?

  • 组件时一种面向用户的、独立的、可复用额交互元素的封装

  • 小程序中的组件与 vue 中组件概念是一样的,不过组织形式不一样

  • vue 中组件是一个 vue 文件,结构、样式、业务逻辑都写在这个文件中

  • 小程序中组件被分成了四个部分:结构文件、样式文件、js文件和配置文件

组件化开发的意义

  • 复用

  • 为了解耦:把复杂系统拆分成多个组件,分离组件边界和责任,便于独立升级和维护。

  • 更有效的代码组合 有利于单元测试 对重构友好

  • 设计原则 高内聚 低耦合 职责单一 避免过多参数

如何在微信中自定义组件,请参考 [自定义组件](自定义组件 | 微信开放文档 (qq.com))

开发 playlist 组件
组件关系

在 components 目录下新建文件夹 playlist,在playlist文件夹中创建 playlist 组件

此组件是我们的个歌单组件,开发完成后,在 pages/music文件中的json文件中引入并在页面中使用,所以他们之间是父子关系

歌单组件的数据应该来自引用此组件的页面,这样才能做到组件复用

所以首先在 pages/music页面的 data 中加入如下数据,然后再传递给 components/playlist 组件

开发初期,先使用这些静态数据,后期会调用云函数,从云数据库中获取

将下列代码写在pages/music的 js 文件的 data 中

playlist: [
{ "_id": "08560c9e5d042a5c0174f1ca26f1d7b2", "copywrier": "热门推荐",
"playCount": 1.4641238e+06, "highQuality": false, "type": 0.0, "canDislike":
true, "name": "天气转热了,适合听点凉爽的歌。", "alg": "cityLevel_unknow",
"createTime": { "$date": "2019-06-14T23:14:36.746Z" }, "id": 2.780381322e+09,
"picUrl":
"https://p2.music.126.net/Biky7TE4CtW6NjGuqoUKZg==/109951164041827987.jpg",
"trackCount": 53.0 },
{ "_id": "08560c9e5d042a5c0174f1da7aa357aa", "highQuality": false,
"copywriter": "热门推荐", "canDislike": true, "playCount": 622822.6, "id":
2.740107647e+09, "name": "「时空潜行」囿于昼夜的空想主义者", "type": 0.0, "alg":
"cityLevel_unknow", "createTime": { "$date": "2019-06-14T23:14:36.955Z" },
"picUrl":
"https://p2.music.126.net/Q0eS0avwGK04LufWM7qJug==/109951164116217181.jpg",
"trackCount": 20.0 },
{ "_id": "08560c9e5d042a5c0174f1de21c7e79e", "id": 2.828842343e+09, "type":
0.0, "name": "粤语情诗:与你听风声,观赏过夜星", "picUrl":
"https://p2.music.126.net/K9IcG8cU6v4_SwuQ_x2xMA==/109951164124604652.jpg",
"highQuality": false, "alg": "cityLevel_unknow", "playCount": 1.785097e+06,
"trackCount": 52.0, "copywriter": "热门推荐", "canDislike": true, "createTime": {
"$date": "2019-06-14T23:14:36.982Z" } },
{ "_id": "08560c9e5d042a5d0174f1e67d1bb16f", "playCount": 7.719329e+06,
"highQuality": false, "trackCount": 950.0, "alg": "cityLevel_unknow", "id":
9.17794768e+08, "type": 0.0, "name": "翻唱简史:日本四百首", "canDislike": true,
"createTime": { "$date": "2019-06-14T23:14:37.037Z" }, "copywriter": "热门推荐",
"picUrl":
"https://p2.music.126.net/NczCuurE5eVvObUjssoGjQ==/109951163788653124.jpg" },
{ "_id": "08560c9e5d042a5d0174f1ea32c4c288", "type": 0.0, "copywriter": "热
门推荐", "highQuality": false, "createTime": { "$date": "2019-06-14T23:14:37.097Z"
}, "id": 2.201879658e+09, "alg": "cityLevel_unknow", "playCount": 1.06749088e+08,
"name": "你的青春里有没有属于你的一首歌?", "picUrl":
"https://p2.music.126.net/wpahk9cQCDtdzJPE52EzJQ==/109951163271025942.jpg",
"canDislike": true, "trackCount": 169.0 },
{ "_id": "08560c9e5d0829820362a79f4b049d2d", "alg": "cityLevel_unknow",
"name": "「乐队的夏天」参赛歌曲合集丨EP04更新", "highQuality": false, "picUrl":
"http://p2.music.126.net/2WE5C2EypEwLJd2qXFd4cw==/109951164086686815.jpg",
"trackCount": 158.0, "createTime": { "$date": "2019-06-18T00:00:02.553Z" },
"copywriter": "热门推荐", "playCount": 1.5742008e+06, "canDislike": true, "id":
2.79477263e+09, "type": 0.0 }
]
传递数据给子组件

小程序中父子组件传值与 vue 基本一致

组件中的 properties 属性与 vue 组件中的 props 作用一致,具体用法 请参考 [自定义组件](自定义组件 | 微信开放文档 (qq.com))

playlist.js 中加入代码

properties: {
playlist: Object
},
编写模板和样式

playlist.wxml

<view class="playlist-container" bind:tap="goToMusiclist">
    <image src="{{playlist.picUrl}}" class="playlist-img"></image>
	<text class="playlist-playcount">{{_count}}</text>
	<view class="playlist-name">{{playlist.name}}</view>
</view>

playlist.wxss

/* components/playList/playList.wxss */
.playlist-container {
  width: 220rpx;
  position: relative;
  padding-bottom: 20rpx;
}

.playlist-img {
  width: 100%;
  height: 220rpx;
  border-radius: 6rpx;
}

.playlist-playcount {
  font-size: 24rpx;
  color: #fff;
  text-shadow: 1px 0 0 rgba(0, 0, 0, 0.15);
  position: absolute;
  right: 10rpx;
  top: 4rpx;
  padding-left: 26rpx;
  background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMiAyMCI+PGcgb3BhY2l0eT0iLjE1Ij48cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGZpbGw9IiMwNDAwMDAiIGQ9Im0yMiAxNi43NzdjMCAxLjIzMy0xLjEyMSAyLjIzMy0yLjUwNiAyLjIzMy0xLjM4NCAwLTIuNTA2LTEtMi41MDYtMi4yMzN2LTIuNTUzYzAtMS4yMzQgMS4xMjItMi4yMzMgMi41MDYtMi4yMzMuMTc0IDAgLjM0My4wMTcuNTA2LjA0NnYtMS4zN2gtLjAzM2MuMDE3LS4yMi4wMzMtLjQ0MS4wMzMtLjY2NiAwLTQuNDE4LTMuNTgyLTgtOC04LTQuNDE4IDAtOCAzLjU4Mi04IDggMCAuMjI1LjAxNi40NDYuMDM0LjY2NmgtLjAzNHYxLjM3Yy4xNjMtLjAyOS4zMzMtLjA0Ni41MDUtLjA0NiAxLjM4NCAwIDIuNTA2Ljk5OSAyLjUwNiAyLjIzM3YyLjU1M2MwIDEuMjMzLTEuMTIyIDIuMjMzLTIuNTA2IDIuMjMzcy0yLjUwNS0uOTk5LTIuNTA1LTIuMjMzdi0yLjU1M2MwLS4yNTguMDU5LS41MDEuMTQ4LS43My0uMDg1LS4xNDgtLjE0OC0uMzEtLjE0OC0uNDkzdi0yLjY2N2MwLS4wMjMuMDEyLS4wNDMuMDEzLS4wNjctLjAwNC0uMDg4LS4wMTMtLjE3Ni0uMDEzLS4yNjYgMC01LjUyMyA0LjQ3Ny0xMCAxMC0xMCA1LjUyMyAwIDEwIDQuNDc3IDEwIDEwIDAgLjA5LS4wMDkuMTc4LS4wMTQuMjY2LjAwMi4wMjQuMDE0LjA0NC4wMTQuMDY3djJjMCAuMzA2LS4xNDUuNTY5LS4zNi43NTMuMjI0LjMzNC4zNi43Mi4zNiAxLjEzOHYyLjU1MiIvPjwvZz48cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGZpbGw9IiNmZmYiIGQ9Im0yMCAxNi43NzdjMCAxLjIzMy0xLjEyMSAyLjIzMy0yLjUwNiAyLjIzMy0xLjM4NCAwLTIuNTA2LTEtMi41MDYtMi4yMzN2LTIuNTUzYzAtMS4yMzQgMS4xMjItMi4yMzMgMi41MDYtMi4yMzMuMTc0IDAgLjM0My4wMTcuNTA2LjA0NnYtMS4zN2gtLjAzM2MuMDE3LS4yMi4wMzMtLjQ0MS4wMzMtLjY2NiAwLTQuNDE4LTMuNTgyLTgtOC04LTQuNDE4IDAtOCAzLjU4Mi04IDggMCAuMjI1LjAxNi40NDYuMDM0LjY2NmgtLjAzNHYxLjM3Yy4xNjMtLjAyOS4zMzMtLjA0Ni41MDUtLjA0NiAxLjM4NCAwIDIuNTA2Ljk5OSAyLjUwNiAyLjIzM3YyLjU1M2MwIDEuMjMzLTEuMTIyIDIuMjMzLTIuNTA2IDIuMjMzcy0yLjUwNS0uOTk5LTIuNTA1LTIuMjMzdi0yLjU1M2MwLS4yNTguMDU5LS41MDEuMTQ4LS43My0uMDg1LS4xNDgtLjE0OC0uMzEtLjE0OC0uNDkzdi0yLjY2N2MwLS4wMjMuMDEyLS4wNDMuMDEzLS4wNjctLjAwNC0uMDg4LS4wMTMtLjE3Ni0uMDEzLS4yNjYgMC01LjUyMyA0LjQ3Ny0xMCAxMC0xMCA1LjUyMyAwIDEwIDQuNDc3IDEwIDEwIDAgLjA5LS4wMDkuMTc4LS4wMTQuMjY2LjAwMi4wMjQuMDE0LjA0NC4wMTQuMDY3djJjMCAuMzA2LS4xNDUuNTY5LS4zNi43NTMuMjI0LjMzNC4zNi43Mi4zNiAxLjEzOHYyLjU1MiIvPjwvc3ZnPg==) no-repeat 0 8rpx/22rpx 20rpx;
}

.playlist-name {
  font-size: 26rpx;
  line-height: 1.2;
  padding: 2px 0 0 6px;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  text-overflow: ellipsis;
}
引入组件

music 页面中引入组件

music.json 中加入如下代码

"usingComponents": {
    "my-playlist":"../../components/playList/playList"
  },
使用组件

music.wxml 中加入如下代码

<!-- 专辑列表 -->
<view class="playlist-container">
  <block wx:for="{{playlist}}" wx:key="index">
    <my-playlist playlist="{{item}}"></my-playlist>
  </block>
</view>

music.wxss 中补充如下样式

.playlist-container {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-around;
    margin-top: 10rpx;
    flex-direction: row;
}

image.png

播放量修饰

当前歌单中播放量的展示还不是很友好,所以我们要对其进行修饰

这里可以使用 [监听器](数据监听器 | 微信开放文档 (qq.com)) 解决这个问题

监听器非常类似于 vue 组件中的 侦听器

在 playList.js 中写入

observers: {
    'playlist.playCount': function (filed) {
        let res = this._transNumber(filed, 2)
        this.setData({
            _count: res
        })
    }
},

data 中定义 _count 属性

data: {
    _count:0
},

并且自定义方法

methods: {
    _transNumber(num, point) {
      let numStr = num.toString().split('.')[0]
      if (numStr.length < 6) {
        return numStr
      } else if (numStr.length >= 6 && numStr.length <= 8) {
        let decimal = numStr.substring(numStr.length - 4, numStr.length - 4 +
          point)
        return parseFloat(parseInt(num / 10000) + '.' + decimal) +
          '万'
      } else if (numStr.length > 8) {
        let decimal = numStr.substring(numStr.length - 8, numStr.length - 8 +
          point)
        return parseFloat(parseInt(num / 100000000) + '.' + decimal) + '亿'
      }
    }
  }

最后修改 playList.wxml 组件模板

<text class="playlist-playcount">{{_count}}</text>
动态获取数据

在前面的编写中,我们的歌单数据都是写死的,而事实上,我们的数据都应该来自后台,也就是来自我们的云数据库

当然,云数据库中的数据也应该是通过管理系统或者其他方式动态维护的,但是为了简单,这里我用的是老师导出的数据,后面可以使用云函数+触发器定时从他搭建的 api 中获取并导入最新的推荐歌单数据

地址如下:https://github.com/Binaryify/NeteaseCloudMusicApi

导入数据文件

云控制台中的数据库中新建集合 playlist,playlist.json 文件下载到本地并导入到云数据库中

image.png

编写云函数

编写云函数,查询数据库并返回数据

image.png

新建云函数 music,编写如下代码

// 云函数入口文件
const cloud = require('wx-server-sdk')
cloud.init({
    env:'test-9gw23ncj6465e410', 
    traceUser:true
})
// 云函数入口函数
exports.main = async (event, context) => {
    let res = await cloud.database().collection('playlist')
    .skip(event.start)
    .limit(event.count)
    .orderBy('createTime', 'desc')
    .get()
    return res
}

注意: cloud.init 方法需要配置参数,特别需要指明 env

否则测试时可能会一直提示 “database collection not exists”

老问题了,还是没解决

env在此查看

image.png

在此注意,我们的 根目录下的 app.js 中的 env 记得也要编写

image.png

然后执行云端测试

image.png

测试 结果

image.png

本地调用

music.js 中将 playlist的值设置为空数组

新增 pagesize

pagesize: 15

onload 中编写如下代码

/**
* 生命周期函数--监听页面加载
*/
onLoad: async function (options) {
    wx.showLoading({
        title: '加载中',
    })
    let res=wx.cloud.callFunction({
        name: 'music',
    	data: {
        	start: this.data.playlist.length,
        	count: this.data.pagesize
    	}
	})
	this.setData({
    	playlist:(await res).result.data
	})
	wx.hideLoading()
},
封装数据请求函数

将 onload 事件中代码封装成函数,方便在 后续的 事件中分别调用

在此我们以对上拉和下拉进行封装,只需调用即可

// 加载音乐数据
  async loadMusic(isPullDown){
    wx.showLoading({
      title: '数据加载中',
    })
    let res = await wx.cloud.callFunction({
      name:'music',
      data:{
        start:this.data.playlist.length,
        count:15,
        $url:'playlist'
      }
    })
    this.setData({
      // isPullDown 为 true是为刷新页面,相反则是加载
      playlist:isPullDown?res.result.data:[...this.data.playlist,...res.result.data]
    })
    wx.hideLoading()
  },

onload 函数改写为

 /**
   * 生命周期函数--监听页面加载
   */
  onLoad: async function (options) {
    this.loadMusic()
  },
上拉加载更多

在上拉函数中调用 loadMusic函数

/**
   * 页面上拉触底事件的处理函数
   */
  onReachBottom: async function () {
    this.loadMusic()
  },
下拉刷新

在下拉刷新中调用

/**
   * 页面相关事件处理函数--监听用户下拉动作
   */
  onPullDownRefresh: async function () {
    this.loadMusic(true)
    wx.stopPullDownRefresh()
  },

因为只在此页面进行下拉刷新,所以我们在 music.json 中匹配下拉刷新的样式和动作

在music.json 中加入以下代码

"enablePullDownRefresh": true,
"backgroundColor":"#000000",
"backgroundTextStyle":"light"
云函数路由

每个控件的云函数是有限制的,所以我们应该尽量利用每一个云函数,将业务相关的代码写道一个云函数中

所以这里的云函数与常规后台开发中的 api 不太一样

  • 每个 api 的功能应该尽量单一
  • 每个云函数应该尽量处理多的任务,当然这些任务是相关的,比如推荐歌单、热门歌单、音乐搜索 等

非要比较的话,云函数与我们以前学过的控制器更类似

比如学习 laravel 的时候有路由的概念,路由的作用就是将用户的请求分配到不同控制器中的方法处理, 这样虽然一个控制器中编写了很多方法,但通过路由就能够被正确的请求调用

云函数中也应该有相同的应对机制,将用户不同的请求分配到不同的处理中

laravel 等框架中都内置了路由组件,云函数中则没有,我们需要单独安装对应的基于 node 的路由组件

当然你也可以通过参数来区别,如果你不怕麻烦,可以这么玩

image.png

安装

在此注意我们需要安装到云函数端 的 music 中

npm install --save tcb-router
云函数中引入

music的index.js (playlist) 云函数中引入

const TcbRouter = require('tcb-router')
修改云函数代码
// 云函数入口文件
const cloud = require('wx-server-sdk')
const TcbRouter = require('tcb-router')
cloud.init({
    env: 'test-5gngkp7l028ba32b',
    traceUser: true,
})
// 云函数入口函数
exports.main = async (event, context) => {
    const app = new TcbRouter({ event });
    app.router('playlist', async (ctx, next) => {
        let res = await cloud.database().collection('playlist')
        .skip(event.start)
        .limit(event.count)
        .orderBy('createTime', 'desc')
        .get()
        ctx.body = res
        // await next(); // 执行下一中间件
    })
    return app.serve();
}

music 页面中调用云函数是,需要指明 url

image.png

歌单列表

点击歌单,跳转到个单列表

页面跳转

为 playlist 组件添加触摸事件

image.png

模板事件参见 [事件系统](事件 | 微信开放文档 (qq.com))

在methods 中新增 goToMusiclist 方法

goToMusiclist(){
      wx.navigateTo({
        url: '/pages/musiclist/musiclist?playlistId='+this.properties.playlist.id,
      })
    }

pages 下需要新建 musiclist 页面,在此我们使用最简单的方法,在app.json中编写

pages/musiclist/musiclist

直接创建

总结一下:

改写云函数

为了减少小程序的云函数,我们将详情页的数据和首页的歌曲列表放在同一个云函数中,为此我们要对其进行改造

跳转到歌单详情页后,应该根据歌单id获取歌单详细信息,包括歌曲列表,封面图片等

修改music云函数

云函数中,根据传递过来的歌单id,调用对应接口,查询歌单详细信息

这次使用 axios 进行接口请求

云函数中安装 axios

npm i axios

music 云函数中引入 axios

const axios =require('axios')

定义常量,存储请求的基准路径

const BASE_URL='http://api.daqitc.net'

修改入口函数,添加一个新的路由

app.router('musiclist', async (ctx, next) => {
    let res=await axios.get(BASE_URL+'/playlist/detail?id='+parseInt(event.playlistId))
    ctx.body=res.data
})

在我们进行一番改造之后,完整代码如下:

// 云函数入口文件
const cloud = require('wx-server-sdk')
const TcbRouter = require('tcb-router')
const axios = require('axios')

cloud.init({
  env:'test-9gw23ncj6465e410',
  traceUser:true
})

// 云函数入口函数
exports.main = async (event, context) => {
  const app = new TcbRouter({ event });
  app.router('playlist', async (ctx, next) => {
    let res = await cloud
      .database()
      .collection('playlist')
      .skip(event.start)
      .limit(event.count)
      .orderBy('createTime', 'desc')
      .get()
    ctx.body = res
  })
  app.router('musiclist', async (ctx, next) => {
    let res = await axios.get(`http://api.daqitc.net/playlist/detail?id=${event.playlistId}`)
    console.log();
    ctx.body = res.data
  })
  app.router('songurl',async (ctx,next)=>{
    let res=await axios.get(`http://api.daqitc.net/song/url?id=${event.musicid}`)
    ctx.body = res.data
  })
  return app.serve()
}
调用云函数

musiclist.js 中调用云函数

 /**
   * 生命周期函数--监听页面加载
   */
  onLoad: async function (options) {
    // console.log(options)
    wx.showLoading({
      title: '加载中',
    })
    // 调用云函数获取数据
    let res = await wx.cloud.callFunction({
      name: 'music',
      data: {
        playlistId: options.playlistId,
        $url: 'musiclist'
      }
    })
    const pl = res.result.playlist
    // 设置变量
    this.setData({
      musiclist: pl.tracks,
      listInfo: {
        coverImgUrl: pl.coverImgUrl,
        name: pl.name,
      }
    })
    // 将歌单详情缓存到本地
    wx.setStorageSync('musiclist', this.data.musiclist)
    wx.hideLoading()
  },

同时 data 中定义变量

data: {
    musiclist: [], // 存储歌曲列表
    listInfo: {}, // 存储歌单信息
},
编写musiclist页面
模板分析

歌单详情页的布局如下

image.png

页面被分成上下两个部分

  • 第一部分展示歌单信息

  • 第二部分展示歌单中歌曲列表信息

所以在 musiclist 页面模板布局时,分成两部分

  • 第一部分自己编写
  • 第二部分封装成一个组件
<view class='detail-container' style='background: url({{listInfo.coverImgUrl}}) no-repeat  top/cover'></view>
<view class='detail-mask'></view>
<view class='detail-info'>
  <image src="{{listInfo.coverImgUrl}}" class='detail-img'></image>
  <view class='detail'>
    <view class='detail-nm'>{{listInfo.name}}</view>
  </view>
</view>

<x-songlist musiclist="{{musiclist}}" />
模板样式
.detail-container {
  height: 320rpx;
  filter: blur(40rpx);
  opacity: 0.4;
}

.detail-mask {
  position: absolute;
  width: 100%;
  height: 320rpx;
  background-color: #333;
  top: 0;
  left: 0;
  z-index: -1;
}

.detail-img {
  width: 280rpx;
  height: 280rpx;
  margin-right: 24rpx;
  border-radius: 6rpx;
}

.detail-info {
  display: flex;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 320rpx;
  padding: 20rpx;
  box-sizing: border-box;
  align-items: center;
}

.detail {
  flex-grow: 1;
  line-height: 60rpx;
  width: 0;
}

.detail view {
  color: #fff;
  font-size: 24rpx;
}

.detail .detail-nm {
  font-size: 36rpx;
  font-weight: 400;
}
引用组件

musiclist.json 中加入代码,引入上面的组件

"usingComponents": {
    "x-songlist": "/components/songlist/songlist"
}
编写歌曲列表组件

components 目录下新建 songlist 组件

js代码

properties 中席间属性 musiclist 接受来自父组件的歌曲列表信息

/**
* 组件的属性列表
*/
properties: {
    musiclist: Array
},
模板

在模板中便利,并显示歌曲列表

<block wx:for="{{musiclist}}" wx:key="id">
  <view class="musiclist-container {{item.id===playerId?'playing':''}}" bind:tap="onSelect" data-musicid="{{item.id}}" data-index="{{index}}">
    <view class="musiclist-index">{{index+1}}</view>
    <view class="musiclist-info">
      <view class="musiclist-name">
        {{item.name}}
        <text class="musiclist-alia">{{item.alia.length==0?"":item.alia[0]}}</text>
      </view>
      <view class="musiclist-singer">{{item.ar[0].name}} - {{item.al.name}}</view>
    </view>
  </view>
</block>
样式
.musiclist-container {
  display: flex;
  padding: 14rpx 20rpx;
  align-items: center; /* 垂直居中 */
}

.musiclist-index {
  color: #888;
  font-size: 34rpx;
  width: 80rpx;
}

.musiclist-info {
  flex-grow: 1;
  width: 0;
}

.musiclist-name {
  font-size: 34rpx;
  color: #333;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  margin-bottom: 10rpx;
}

.musiclist-alia {
  color: #888;
}

.musiclist-singer {
  font-size: 24rpx;
  color: #888;
}

.playing view, .playing text {
  color: #d43c33;
}
选择歌曲

选择歌曲逻辑

  • 点击歌曲,歌曲高亮展示

  • 点击歌曲,跳转到歌曲播放页面,同时传递歌曲id

点击歌曲高亮展示

  • 为 musiclist-container 绑定 tap 事件
  • tap 事件中获取当前 tap 的歌曲 id
  • 设置 playerId 为 歌曲 id
  • 设置当前点击的 musiclist-container 为高亮

sonlist 组件中注册事件,并设置自定义属性值为歌曲 id

<view class="musiclist-container" bind:tap="onSelect" data-musicid="{{item.id}}">

onSelect 函数

 onSelect(event) {     
      const data = event.currentTarget.dataset
      const musicid = data.musicid    
      const index=data.index
      this.setData({
        playerId: musicid
      })

定义样式

.playing view, .playing text {
    color: #d43c33;
}

修改模板,根据当前点击的歌曲进行对应的高亮显示

<view class="musiclist-container {{item.id===playerId?'playing':''}}" bind:tap="onSelect" data-musicid="{{item.id}}">

跳转页面

加上如下代码

methods: {
    onSelect(event) {     
      const data = event.currentTarget.dataset
      const musicid = data.musicid    
      const index=data.index
      this.setData({
        playerId: musicid
      })
      // 新加的代码
      wx.navigateTo({
        url: `../../pages/player/player?musicId=${musicid}&index=${index}`,
      })
    }
  }

任务单.gif

未完待续。。。。。。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值