小程序云开发实战

1. 前言

小程序云开发(Tencent Cloud Base),为开发者提供完整的原生云端支持和微信服务支持。无需搭建服务器,通过它提供的 API 就能无需搭建服务器方便地实现云函数、云数据库、云存储和云调用,并且同开发者已经使用的云服务相互兼容。

2. 总览

编号 能力 作用 说明
1 云函数 无需自建服务器 在云端运行的代码,微信私有协议天然鉴权,开发者只需编写自身业务逻辑代码
2 云数据库 无需自建数据库 一个既可在小程序前端操作,也能在云函数中读写的 JSON 数据库
3 云存储 无需自建存储和 CDN 在小程序前端直接上传/下载云端文件,在云开发控制台可视化管理
4 云调用 原生微信服务集成 基于云函数免鉴权使用小程序开放接口的能力,包括服务端调用、获取开放数据等能力

2.1 云函数

云函数即在云端(服务器端)运行的函数。

  1. 在物理设计上,一个云函数可由多个文件组成,占用一定量的 CPU 内存等计算资源。各云函数完全独立。
  2. 一个云函数的写法与一个在本地定义的 JavaScript 方法无异,代码运行在云端 Node.js 中。当云函数被小程序端调用时,定义的代码会被放在 Node.js 运行环境中执行。
  3. 云开发的云函数的独特优势在于与微信登录鉴权的无缝整合。当小程序端调用云函数时,云函数的传入参数中会被注入小程序端用户的 openid,开发者无需校验 openid 的正确性因为微信已经完成了这部分鉴权,开发者可以直接使用该 openid。

2.2 云数据库

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

2.3 云存储

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

2.4 云调用

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

小程序云开发官方文档很清晰,我们就不过多讲解理论,这里我们实际开发一个工单处理的 demo 来进行技术分享。

3. 初始化云开发

3.1 新建云开发项目

  1. 使用微信开发者工具新建项目,后端服务选择小程序云开发,注意必须输入可用的 appid。
image
  1. 打开我们新建的云开发项目,miniprogram 目录存放我们小程序前端项目,cloudfunctions 目录存放我们的云函数。点击微信开发者工具左上角云开发按钮,根据提示开通云开发创建云环境,等待 10 分钟左右就可以使用云环境啦。
image
  1. 开通成功后打开云控制台,我们能看到腾讯云免费为我们提供的基本配置,可在设置中进行升级购买。控制台的其他部分我们在下面再讲解,现在我们就可以正式开始云开发了。
image

3.2 初始化云环境

这里微信提供一个包含几个主要功能的 demo 大家有空可以看看。这里有一个坑,这里我们调用云函数会报云函数调用失败的错误,是因为我们还没有初始化,我们要在 app.js 中调用wx.cloud.init进行初始化,如下

image

注意: 这里的 env 在云控制台的设置中能看到,注意是取环境 ID,不是环境名称。

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

4. 云数据库

4.1 前端操作数据库-新增记录

  1. 我们在前端目录新增一个添加工单(serverDetail)页面如下 (页面样式以及部分前端逻辑我们就先跳过)
image
  1. 新增数据前我们要在控制台数据库里面新增该集合,不然会报错,这里我们新增一个名为server_order的集合。
image
  1. 新增一条工单记录
// 初始化数据库
const db = wx.cloud.database();
// serverDetail.js
// 这里我们输入一些数据,然后点击提交审核按钮调用如下方法
async addServer(e) {
    try {
        // 图片上传逻辑在后文进行讲解
        const choosePic = await this.doUpload();
        const formData = e.detail.value;
        const serverData = {
            serverType: this.serverType,
            choosePic: choosePic,
            status: 0,
            ...formData
        };
        const formData = e.detail.value;
        const res = await db.collection('server_order').add({
            data: serverData
        });
        if (res._id) {
            // 成功处理
        } else {
            // 失败处理
        }
    } catch (err) {
        console.error('[addServer]', err);
    }
}

注意: 前端操作数据库支持回调风格和 Promise 风格调用,这里我们使用 await 进行异步开发。

  1. 然后我们就能在数据库就能看到这条数据了,我们会发现集合里的每条记录会有一个自动生成的索引_id和当前用户的_openid
image

注意: 有个小坑,新建集合默认仅创建者可读写,我们需要如下修改一下权限

image

4.2 前端操作数据库-分页查询

  1. 我们在前端目录新增一个工单列表(serverList)页面,如下
image
// serverList.js
// 这里我们把分页查询的逻辑整个贴出来
const db = wx.cloud.database();
let currentPage = 0;
const pageSize = 5;

Page({
  data: {
    currentServer: {},
    serverList: [],
    loadMore: false, //"上拉加载"的变量,默认false,隐藏
    loadAll: false //“没有数据”的变量,默认false,隐藏
  },
  onLoad(options) {
    this.app = getApp();
  },
  onShow() {
    this.getServerList();
  },
  onReachBottom() {
    if (!this.data.loadMore) {
      this.setData({
        loadMore: true, //加载中
        loadAll: false //是否加载完所有数据
      });
      setTimeout(() => {
        this.getServerList();
      }, 2000);
    }
  },
  /**
   * 数据库-获取工单列表
   */
  async getServerList() {
    try {
      //第一次加载数据
      if (currentPage == 1) {
        this.setData({
          loadMore: true, //把"上拉加载"的变量设为true,显示
          loadAll: false //把“没有数据”设为false,隐藏
        });
      }

      //云数据的请求
      const { data } = await db
        .collection('server_order')
        .where({
          _openid: this.app.globalData.openid
        })
        .skip(currentPage * pageSize) //从第几个数据开始
        .limit(pageSize)
        .get();

      if (data && data.length > 0) {
        currentPage++;
        //把新请求到的数据添加到serverList里
        let list = this.data.serverList.concat(data);
        this.setData({
          serverList: list, //获取数据数组
          loadMore: false //把"上拉加载"的变量设为false,显示
        });
        if (data.length < pageSize) {
          this.setData({
            loadMore: false, //隐藏加载中。。
            loadAll: true //所有数据都加载完了
          });
        }
      } else {
        this.setData({
          loadAll: true, //把“没有数据”设为true,显示
          loadMore: false //把"上拉加载"的变量设为false,隐藏
        });
      }
    } catch (err) {
      console.error('[getServerList]', err);
    }
  }
});

可以看到这里我们是通过_openid进行条件搜索,可以去掉根据业务场景搜索全部或者进行工单状态等的其他条件搜索。这里的 openid 我们是从 globalData 里面取的,openid 的获取是通过云函数处理,这个在下文进行介绍。

4.3 前端操作数据库-删除集合

  1. 在工单列表点击某一工单,会带参数工单 id 跳转工单详情,通过 id 查询该工单详细数据。
image
  1. 删除该工单
// serverDetail.js
// 点击删除按钮调用如下方法
async deleteServer(e) {
    try {
      const res = await db
        .collection('server_order')
        .doc(this.data.serverId)
        .remove();
        if (res.result.stats.removed===1) {
            // 成功处理
        } else {
            // 失败处理
        }
    } catch (err) {
        console.error('[deleteServer]', err);
    }
}

4.4 前端操作数据库-更新集合

  1. 我们希望用户有不同的角色,管理员可以审核工单,管理员的处理在云函数那一块,这里我们假设已经是管理员,进行审核操作。

  2. 审核该工单

// 点击审核调用如下方法
async updateServer(e) {
    try {
      const res = await db
        .collection('server_order')
        .doc(this.data.serverId)
        .update({
          data: {
            status
          }
        });
        if (res.result.stats.updated===1) {
            // 成功处理
        } else {
            // 失败处理
        }
    } catch (err) {
        console.error('[updateServer]', err);
    }
}

注意:数据库批量操作以及集合的删、改操作只能在云函数里面进行。

5. 云函数

5.1 login 云函数

  1. 在微信开发者工具-调试器-项目目录cloudfunctions右键选择新增 Node.js 云函数,新建login云函数,用来处理用户openid等信息
/**
 * login云函数,将小程序用户 openid 返回给小程序端
 *
 * event 参数包含
 * - 小程序端调用传入的 data
 * - 经过微信鉴权直接可信的用户唯一标识 openid
 *
 */
exports.main = (event, context) => {
  console.log(event, context);
  // console.log 的内容可以在云开发云函数调用日志查看
  return {
    openid: event.userInfo.openId
  };
};

注意: 云函数开发完成后需要上传,例如本次就需要在微信开发者工具cloudfunctions/login文件夹右键选择上次并部署:云端安装依赖

这里我们还能进行云函数本地调试、终端调用和云函数管理,这里就不多做介绍。

  1. 前端调用云函数
// app.js
getOpenId() {
    try {
      const openid = wx.getStorageSync('openid');
      if (!openid) {
        wx.cloud
          .callFunction({
            name: 'login',
            data: {}
          })
          .then(res => {
            // console.log('[getOpenId]', res);
            const openid = res.result.openid;
            if (openid) {
              this.globalData.openid = openid;
              wx.setStorageSync('openid', openid);
            }
          });
      } else {
        this.globalData.openid = openid;
      }
    } catch (err) {
      console.log('[getOpenId]', err);
    }
}
  1. 调试云函数

在云开发控制台-云函数,我们可以看到云函数的列表和日志,在这里我们能进行配置和云端调试以及查看对应的日志。现在我们就完成了上文中用户openid的获取存储啦。

image

6. 云存储

  1. 调用微信 api 选择图片
// serverDetail.js
/**
 * 选择图片(最多三张)
 */
choosePhoto() {
    wx.chooseImage({
      count: 3,
      success: res => {
        //每一张图片的地址
        let tempFilePaths = res.tempFilePaths;
        //放进存储的数组
        for (let index in tempFilePaths) {
          if (this.tempPic.length < 4) {
            this.tempPic.push(tempFilePaths[index]);
          }
          if (this.tempPic.length > 3) {
            this.tempPic = this.tempPic.splice(0, 3);
          }
        }
        this.setData({
          choosePic: this.tempPic
        });
      }
    });
},
/**
 * 删除图片
 */
picpDelete(e) {
    let index = e.currentTarget.dataset.index;
    this.tempPic.splice(index, 1);
    this.setData({
      choosePic: this.tempPic
    });
}
  1. 封装图片上传 service
/*
 * @Descripttion: 处理图片上传/下载service
 * @Author: majun
 * @Date: 2019-08-01 23:33:48
 * @LastEditors: majun
 * @LastEditTime: 2019-08-06 14:32:57
 */

export class UpDownService {
  upload(filePaths) {
    return new Promise((resolve, reject) => {
      if (typeof filePaths === 'string') {
        // 单张图片上传
        this.uploadSingle(filePaths).then(url => {
          resolve(url);
        });
      } else if (
        Object.prototype.toString.call(filePaths) === '[object Array]'
      ) {
        // 多张图片上传
        const promises = filePaths.map(path => {
          return this.uploadSingle(path);
        });
        Promise.all(promises).then(urls => {
          resolve(urls);
        });
      } else {
        reject('传入的数据格式不正确');
      }
    });
  }

  uploadSingle(filePath) {
    return new Promise(resolve => {
      if (!filePath || typeof filePath !== 'string') {
        // 直接返回,不使用reject是因为防止影响多张上传中断,可以通过返回的数据判断是否成功
        resolve();
        return;
      }
      // 调用云存储
      wx.cloud.uploadFile({
        cloudPath: 'my-image' + filePath.match(/\.[^.]+?$/)[0],
        filePath,
        success: res => {
          if (res.statusCode === 200) {
            resolve(res);
          } else {
            resolve();
          }
        },
        fail: () => {
          resolve();
        }
      });
    });
  }
}
  1. 点击新增工单的时候先上传图片
// serverDetail.js
/**
 * 上传图片
 */
doUpload() {
    return new Promise(async (resolve, reject) => {
      try {
        const resp = await this.upDownService.upload(this.data.choosePic);
        if (resp) {
          resolve(resp);
        } else {
          reject('上传失败');
        }
      } catch (err) {
        reject(err);
      }
    });
},
image

上传成功后拿到的是文件 ID,后续操作都基于文件 ID 而不是 URL。打开云开发控制台-存储,我们就能看到刚刚上传成功的文件,这里也可以进行权限设置。

7. 云调用

云开发提供的基于云函数使用小程序开放接口的能力

7.1 服务端调用

无需换取 access_token,只要是在从小程序端触发的云函数中发起的云调用都经过微信自动鉴权,可以在登记权限后直接调用如发送模板消息等开放接口

// 使用前需要配置云调用权限
// 在云函数目录下新建config.json文件,配置规则如下
{
  "permissions": {
    "openapi": [
      "templateMessage.send"
    ]
  }
}

permissions.openapi 是个字符串数组字段,值必须为所需调用的服务端接口名称。(服务端调用云函数列表)

7.1.1 发送模板消息

<form class="list" bindsubmit="submitTemplateMessageForm" report-submit>
  <button class="list-item" form-type="submit">
    <text>发送模板消息</text>
  </button>
</form>
  async submitTemplateMessageForm(e) {
    const res = await wx.cloud.callFunction({
      name: 'openapi',
      data: {
        action: 'sendTemplateMessage',
        formId: e.detail.formId
      }
    });
    if (res) {
      // 成功处理
    } else {
      // 失败处理
    }
  },
// openapi云函数入口文件
const cloud = require('wx-server-sdk');
cloud.init();

// 云函数入口函数
exports.main = async (event, context) => {
  // console.log(event)
  switch (event.action) {
    case 'sendTemplateMessage': {
      return sendTemplateMessage(event);
    }
    case 'getWXACode': {
      return getWXACode(event);
    }
    case 'getOpenData': {
      return getOpenData(event);
    }
    default: {
      return;
    }
  }
};

async function sendTemplateMessage(event) {
  const { OPENID } = cloud.getWXContext();
  // 接下来将新增模板、发送模板消息、然后删除模板
  // 注意:新增模板然后再删除并不是建议的做法,此处只是为了演示,模板 ID 应在添加后保存起来后续使用
  const addResult = await cloud.openapi.templateMessage.addTemplate({
    id: 'AT0002',
    keywordIdList: [3, 4, 5]
  });
  const templateId = addResult.templateId;
  const sendResult = await cloud.openapi.templateMessage.send({
    touser: OPENID,
    templateId,
    formId: event.formId,
    page: 'pages/index/index',
    data: {
      keyword1: {
        value: '比邻国际小学'
      },
      keyword2: {
        value: '2020 年 1 月 1 日'
      },
      keyword3: {
        value: '体验课'
      }
    }
  });
  await cloud.openapi.templateMessage.deleteTemplate({
    templateId
  });
  return sendResult;
}

7.1.2 获取小程序码

// 云函数
async function getWXACode(event) {
  // 此处将获取永久有效的小程序码,并将其保存在云文件存储中,最后返回云文件 ID 给前端使用
  const wxacodeResult = await cloud.openapi.wxacode.get({
    path: 'pages/index/index'
  });
  const fileExtensionMatches = wxacodeResult.contentType.match(/\/([^\/]+)/);
  const fileExtension =
    (fileExtensionMatches && fileExtensionMatches[1]) || 'jpg';
  const uploadResult = await cloud.uploadFile({
    // 云文件路径,此处为演示采用一个固定名称
    cloudPath: `wxacode_default_openapi_page.${fileExtension}`,
    // 要上传的文件内容可直接传入图片 Buffer
    fileContent: wxacodeResult.buffer
  });
  if (!uploadResult.fileID) {
    throw new Error(
      `upload failed with empty fileID and storage server status code ${uploadResult.statusCode}`
    );
  }
  return uploadResult.fileID;
}

7.2 开放数据调用

暂无时间调研,详情请看云调用直接获取开放数据,后期进行补充

7.3 消息推送

暂无时间调研,详情请看云函数接收消息推送,后期进行补充

8. 结语

8.1 参考资料

云开发官网

8.2 拓展

使用云开发进行支付

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容