大多数公司和开发者在开发应用时和部署服务时,无论是选择公有云还是自建数据中心,都需要提前考虑服务器、存储和数据库等需求,并且需要花费时间精力在部署应用、依赖。那么是否有一种架构可以帮我们节省这部分的成本呢?这就是 Serverless(无服务器)架构。具体来说,Serverless 架构是指由第三方云计算供应商负责后端基础结构的维护,以服务的方式为开发者提供如数据库、消息、身份验证等所需功能。简言之, 这个架构的就是要让开发人员关注代码的运行而不需要管理任何的基础设施。 小程序 · 云开发就是一种 Serverless 架构的实现方式。
更直白一些:个人开发者在小程序访问量不大的阶段(DAU[日活]在0 ~ 500之间),可以 免费 使用小程序提供的云服务,对个人开发者很友好, 具体价格参考
更多内容请查阅, 云开发详细文档
由于小程序云开发中,一般不需要使用到 wx.request
来请求服务端数据了,以前通常把所有服务端api在统一文件中管理,在云开发中就没有必要了。但是在云开发中,也需要获取服务端数据,所以对数据获取进行封装还是有必要的,不然后期代码非常散落,以下介绍单词天天斗中的实践内容
在小程序前端代码目录中,新建 model
文件夹,其中文件和数据集合(数据表名)保持一致;在很多后端框架,比如thinkPHP、thinkJS、egg中都有类似实践,云开发中参考抽离出数据model层,把所有的数据库操作进行封装(包含云函数对相应的数据库的操作)
├── model | ├── base.js # 基类 | ├── book.js # 单词书数据表 | ├── index.js # 默认引用的文件 (文件中导入了其他所有数据集合基类) | ├── room.js # 房间数据表 | ├── sign.js # 签到数据表 | ├── user.js # 用户表 | ├── userWord.js # 生词表 | └── word.js # 单词表 复制代码
// base基类,所有其他数据集合都继承该类,用来做数据集合初始化 import $ from './../utils/Tool' const DB_PREFIX = 'pk_' // 数据集合前缀 export default class { constructor(collectionName) { const env = $.store.get('env') // 获取当前的云开发环境 const db = wx.cloud.database({ env }) // 初始化数据库操作 this.model = db.collection(`${DB_PREFIX}${collectionName}`) // 初始化数据集合 this._ = db.command // 对db.command的引用,可以做数据自加、自减等操作 this.db = db this.env = env } get date() { return wx.cloud.database({ env: this.env }).serverDate() // 获取服务端时间 } /** * 取服务器偏移量后的时间 * @param {Number} offset 时间偏移,单位为ms 可+可- */ serverDate(offset = 0) { return wx.cloud.database({ env: this.env }).serverDate({ offset }) } } 复制代码
以room集合部分函数做例子,其中包含了房间集合所有的数据操作
import Base from './base' // 引入基类 import $ from './../utils/Tool' // 全局工具对象 const collectionName = 'room' // 数据集合名称 export const ROOM_STATE = { IS_OK: 'OK', // 房间状态正常 IS_PK: 'PK', // 对战中 IS_READY: 'READY', // 非房主用户已经准备 IS_FINISH: 'FINISH', // 对战结束 IS_USER_LEAVE: 'LEAVE' // 对战中有用户离开 } /** * 权限: 所有用户可读写 */ class RoomModel extends Base { // 继承上述提到的base基类 constructor() { super(collectionName) } // 用户准备 userReady(roomId, isNPC = false, openid = $.store.get('openid')) { return this.model.where({ _id: roomId, 'right.openid': '', state: ROOM_STATE.IS_OK }).update({ data: { right: { openid }, state: ROOM_STATE.IS_READY, isNPC } }) } // 用户取消准备 userCancelReady(roomId) { return this.model.where({ _id: roomId, 'right.openid': this._.neq(''), state: ROOM_STATE.IS_READY }).update({ data: { right: { openid: '' }, state: ROOM_STATE.IS_OK } }) } // 开始PK startPK(roomId) { return this.model.where({ _id: roomId, 'right.openid': this._.neq(''), state: ROOM_STATE.IS_READY }).update({ data: { state: ROOM_STATE.IS_PK } }) } // 创建房间 async create(list, isFriend, bookDesc, bookName) { try { const { _id = '' } = await this.model.add({ data: { list, isFriend, createTime: this.date, bookDesc, bookName, left: { openid: '{openid}', gradeSum: 0, grades: {} }, right: { openid: '', gradeSum: 0, grades: {} }, state: ROOM_STATE.IS_OK, nextRoomId: '', // 再来一局的房间id isNPC: false // 是否为机器人对战局 } }) if (_id !== '') { return _id } throw new Error('roomId get fail') } catch (error) { log.error(error) throw error } } // 单词选择 selectOption(roomId, index, score, listIndex, isHouseOwner) { const position = isHouseOwner ? 'left' : 'right' return this.model.doc(roomId).update({ data: { [position]: { gradeSum: this._.inc(score), grades: { [listIndex]: { index, score } } } } }) } /** * 结束房间的对战 */ finish(roomId) { return this.model.where({ _id: roomId, state: ROOM_STATE.IS_PK }).update({ data: { state: ROOM_STATE.IS_FINISH } }) } leave(roomId) { return this.model.where({ _id: roomId, state: ROOM_STATE.IS_PK }).update({ data: { state: ROOM_STATE.IS_USER_LEAVE } }) } remove(roomId, state = ROOM_STATE.IS_OK) { return this.model.where({ _id: roomId, _openid: '{openid}', state }).remove() } /** * 搜索随机匹配房间 * 2mins之内创建的房间 */ searchRoom(bookDesc) { return this.model.where({ bookDesc, isFriend: false, 'right.openid': '', 'left.openid': this._.neq($.store.get('openid')), state: ROOM_STATE.IS_OK, createTime: this._.gt(this.serverDate(-2 * 60 * 1000)) // 创建时间要>2分钟之前 }).limit(1).field({ _id: true }).get() } /** * 再来一局 * @param {String} roomId 当前房间id * @param {String} nextRoomId 下一局房间的id */ updateNextRoomId(roomId, nextRoomId) { return this.model.where({ _id: roomId, state: ROOM_STATE.IS_FINISH, nextRoomId: '' }).update({ data: { nextRoomId } }) } } export default new RoomModel() 复制代码
单词书中,改变当前用户选择的单词书使用了云函数,所有做一个例子解析
import Base from './base' import $ from './../utils/Tool' const collectionName = 'book' /** * 权限: 所有用户可读 */ class BookModel extends Base { constructor() { super(collectionName) } async getInfo() { const { data } = await this.model.get() return data } async changeBook(bookId, oldBookId, bookName, bookDesc) { if (bookId !== oldBookId) { const { result: bookList } = await $.callCloud('model_book_changeBook', { bookId, oldBookId, bookName, bookDesc }) // 调用云函数的操作,也封装在对应的数据集合文件中 return bookList } } } export default new BookModel() 复制代码
这样导入又导出有一个最大的好处,之后引入集合文件,就只需要import index.js
就行
// index.js import userModel from './user' import bookModel from './book' import wordModel from './word' import roomModel from './room' import userWordModel from './userWord' import signModel from './sign' export { userModel, bookModel, wordModel, roomModel, userWordModel, signModel } 复制代码
// 引入model类 import { userModel, bookModel, wordModel, roomModel } from './../../model/index' // 切换用户当前选择的单词书 const bookList = await bookModel.changeBook(bookId, oldBookId, name, desc) 复制代码
登录获取openid,如果是新用户就先进行注册
// user.js 用户表 类,同上述单词书、房间集合类 import Base from './base' import $ from './../utils/Tool' const collectionName = 'user' /** * 权限: 所有用户可读,仅创建者可写 */ class UserModel extends Base { constructor() { super(collectionName) } register() { return this.model.add({ data: { ...doc, createTime: this.date } }) } /** * 获取自己的用户信息 */ async getOwnInfo() { const { result: userInfo } = await $.callCloud('model_user_getInfo') if (userInfo === null) { // 新用户 await this.register() return (await this.getOwnInfo()) // 注册后,递归 } $.store.set('openid', userInfo._openid) return userInfo } } export default new UserModel() 复制代码
model_user_getInfo
云函数如下:
const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() const userModel = db.collection('pk_user') exports.main = async () => { const { OPENID: openid } = cloud.getWXContext() const asBook = 'book' const { list } = await userModel .aggregate() .match({ _openid: openid }) .limit(1) .lookup({ from: 'pk_book', localField: 'bookId', foreignField: '_id', as: asBook }) .end() if (list.length === 0) { return null } const userInfo = { ...list[0], bookName: list[0][asBook][0].name, bookDesc: list[0][asBook][0].desc } delete userInfo[asBook] return userInfo } 复制代码
首页 home.js
中获取服务端数据,如下调用
async onLoad() { await this.getData() // 登录 + 获取用户数据 } /** * 获取页面服务端数据 */ async getData() { $.loading() const userInfo = await userModel.getOwnInfo() const bookList = await bookModel.getInfo() this.setData({ userInfo, bookList }) $.hideLoading() } 复制代码
为什么小程序可以直接操作数据库了,还需要云函数?
update
、 remove
操作,获取超过记录条数超过20条的 get
查询操作等 云开发的基本费用问题
基础版一,有一定的免费额度, 查看详细 ,但除了免费额度需要留意,需要注意的还有以下几点
如下:
小程序名称: 单词天天斗 AppID: wx51a5362ef159dbcd 环境ID: prod-words-pk 说明: 单词天天斗的环境prod-words-pk的资源包数据库并发连接数的使用量为56个>=20个。 可登录微信开发者工具-云开发控制台查看详细的资源使用数据或调整配额。 复制代码
本书是一个实战系列的分享,会持续更新、修正,感谢同学们来一起学习 ~
由于本项目参加了大学的比赛,所以暂时不做开源,比赛结束在微信公众号: Join-FE
进行开源(代码 + 设计图),关注不要错过哦 ~
同系列文章,可以到老包同学的掘金主页食用