前不久,小程序推出了云开发的功能,使开发者们无需搭建服务器,用云端能力直接迈入全栈开发。正巧用着网易云课堂充电,界面精致、细节到位,于是决定用云开发来仿一仿网易云课堂。
来,先看一波效果图
购物车
直接购买
工具
在项目开发中选用好的工具使得工作事半功倍
- 微信开发者工具 (云开发)
- vscode vs中如emmet之类的各种插件能让coding爽的不行,我是在vs中coding 在开发者工具里调试 。
- easymock 快速的模拟出数据请求,(主页的数据请求量较多且只是可读,就直接用easymock了)
- Mark man 让你成为快乐切图仔
项目结构
|-study163 项目名
|-cloudfunctions 云函数
|-getMyCourse 获取我的课程
|-getCourseInfo 获取课程信息
|-getCart 获取购物车
|-miniprogram 项目模块
|-components 自定义组件
|-box-module 盒子
|-myCourse-module 我的课程
|-special-module 专题
|-utils 工具
|-indexMock 获取主页数据
|-viewContent 文本处理
|-pages 页面
|-account 账号
|-cart 购物车
|-confirm 确认订单
|-courseInfo 课程信息
|-myStudy 我的学习
|-index 首页
|-vant-weapp 有赞vant框架组件库
|-···
app.js 全局js
app.json 全局json配置
app.wxss 全局wxss
复制代码
云开发
在新版的开发者工具中,具有了云开发的功能,且在启动模板中添加了云开发快速启动模板
云开发的三个核心
- 数据库 文档(JSON)数据库 类似MongoDB
- 存储管理 存储空间,类似百度云盘
- 云函数 独立的node项目
数据库
课程的数据项是多出了我的预料的,这里我只定义了我所使用了的部分。每个页面显示不同的课程。这里要注意的一点是,数据滚动栏里的、课程盒子里的、课程信息里的图片虽然看起来相似,但并不是同一张,所以储存的链接不同。小tips:设计好数据库后记得设置相对应的权限管理,否则可能出现读取不到数据的情况。
_id是数据库自动生成的属性(唯一),而我定义了一个id属性,串联个个数据表(collection),类似传统sql数据库中的主键。
course_info
course_cart
my_course
存储管理
当文件储存在此后,会生成一个下载地址,可以直接拿出来用。
当然也可以用相应的API直接进行上传或下载
云函数
云函数就是部署在云端的独立运行的node后端项目,根据自身的业务具体实现。在生成云函数后,首先记得安装依赖,在终端中yarn。写完后不光得保存还得记得上传并部署。
云函数(小demo)的门槛不高,可以轻松助你实现全栈开发。
我的云函数业务其实很简单,就是简单的查询
// 云函数入口文件
const cloud = require('wx-server-sdk')
cloud.init()const db = cloud.database();const course_cart = db.collection('course_cart');
// 云函数入口函数
exports.main = async (event, context) => {
let info = course_cart.where({ id: event.id }).get();
return info
}复制代码
众所周知js是个单线程语言,而数据交互是个异步过程。这一段没有处理好的话,可能出现即使获取到了数据也没显示出来的问题。所以啊,异步问题还是得交给promise处理!
const promiseArr = cart_coursesId.map(course => new Promise((resolve, reject) =>{
wx.cloud.callFunction({
name: 'getCart',
data: { id: course} })
.then(res =>{
resolve(res.result.data[0]);
})
}))
Promise
.all(promiseArr)
.then(data => {
console.log(data);
wx.hideLoading()
this.setData({
courses: data
})
}) 复制代码
部分页面解析
首页
关于主页的课程盒子,我想用一个模板兼容所有的表现效果,所以这也是坑我最多的地方。
当isBig为false时,图片宽度为48.7%,margin-right为 2.6%;,并通过父类的ntd-child(even)将偶数子类(处于右边)的margin-right改为0%,形成 (48.7%+2.6%)+48.7%的结构分割。但当isBig为true,第一行就只有一张大图,所以从第二行开始偶数子元素出现在左边,结构变成48.7%+(48.7%+2.6%)本来在中间的间隙到了右边,两张图片贴在了一起,很不美观。所以我在每处理到一张大图的同时,再生成一个display为none的小盒子。这样第一行依然只显示一张大图,但有了两个子元素。所以下面的行列,依然是奇数子类在左边,偶数子类在右边。
效果实现了,但是当我想把它封装成组件时,天坑出现了 component不支持ntd-child选择器,并且组件的slot也并不只是直白的整段插入。不过日后还是会重新封装~
"courses">
for="{{mainPage.sellWell.content}}" wx:key="index" >
if="{{item.isBig}}">
"course_big" >
"../courseInfo/courseInfo?id={{item.id}}" >
"course-image_big " src="{{item.image}}" />
"course-title">{{item.title}}
"course-price" wx:if="{{item.price > 0}}">¥{{item.price}}
"display:none">
else>
"course" >
"../courseInfo/courseInfo?id={{item.id}}" >
"course-image " src="{{item.image}}" />
"course-title">{{item.title}}
"course-price" wx:if="{{item.price > 0}}">¥{{item.price}}
复制代码
我的学习
虽然简单,但这是典型的数据驱动界面效果,数据绑定,也就是MVVM。页面通过对数据status的判断来决定显示对应的部分。
"status">
-
"{{status == '1' ? 'active':''}}" bindtap="showStatus" data-status="1">我的课程
-
"{{status == '2' ? 'active':''}}" bindtap="showStatus" data-status="2">我的微专业
if="{{status == '1'}}">
else>
复制代码
给相应的标签设置对应的data-status,再将修改的函数绑定到bindtap上,一个最简单的MVVM例子就实现了。
showStatus: function(e){
let status = e.currentTarget.dataset.status;
this.setData({
status:status
})
}复制代码
关于课程的获取就是云函数的用武之地了
onShow:function(){
wx.cloud.callFunction({
name: 'getMyCourse',
}).then(res=>{
this.setData({
my_courses: res.result.data
})
})
}复制代码
在你需要使用云函数的地方调用wx.cloud.callFunction(),把要调用的云函数的函数名作为参数传入,如此处{name: 'getMyCourse'}。接着在wxml内通过一个wx:for把数据输出即可。此处myCourse-module为封装好了的自定义组件。
for="{{my_courses}}" wx:key="index">
"{{item.image}}" title="{{item.title}}" cid="{{item.id}}">
复制代码
购物车
相关数据项有
totalPrice:0,
selectedId:[],
selectAllStatus: false复制代码
通过对列表中数据的isSelected属性判断,来计算总价
getTotalPrice: function(){
let courses = this.data.courses;
let total = 0;
let selectedId = [];
for(let i=0;iif(courses[i].isSelected){
total += courses[i].price;
selectedId.push(courses[i].id)
}
}
this.setData({
totalPrice:total,
selectedId
})
console.log('[total]',total);
console.log('[selected]',selectedId);
return total;
}复制代码
选中课程,还是MVVM数据绑定,且选中后重新计算总价
selectedList:function(e){
const index = e.currentTarget.dataset.index;
const courses = this.data.courses;
courses[index].isSelected = !courses[index].isSelected;
this.setData({
courses
})
this.getTotalPrice();
}复制代码
全选,每次进行selectAll操作,先将selectAllStatus改为!selectAllStatus,(全选=>全不选 || 全不选=>全选 ),之后将所有数据的isSelected属性统一为selectAllStatus的当前状态。
selectAll:function(e){
let courses = this.data.courses;
let selectAllStatus = this.data.selectAllStatus;
selectAllStatus = !selectAllStatus;
courses.forEach((item, index) => {
item.isSelected = selectAllStatus
})
this.setData({
courses,
selectAllStatus
})
this.getTotalPrice();
}复制代码
数据传递到下一个界面 将选中的数组selectedId作为对象通过navigator传入下一个页面。
if="{{totalPrice > 0}}">
"../confirm/confirm?ids={{selectedId}}">
"pay-btn active" bindtap="confirm">去结算
复制代码
订单
先获取购物车传递的数据
let orderIds = options.ids.split(",");
console.log(orderIds)
this.setData({
userInfo : app.globalData.userInfo,
orderIds: orderIds
})
复制代码
完成订单后,将购买的课程存入数据库内
wx.cloud.init()
const db = wx.cloud.database();
const my_courses = db.collection('my_courses');复制代码
addMyCourse(){
const orders = this.data.orders;
for(let order of orders){
let myCourse = {
title:order.title,
image:order.image,
id:order.id
}
console.log(myCourse);
my_courses.add({data:myCourse})
}
}复制代码
提交订单之后 my_courses更新,相应的,打开被购买的课程页面也会更新,查询当前课程是否在购买的课程中,在则将isPaid改为true。
wx.cloud.callFunction({
name: 'getMyCourse',
}).then(res=>{
for(const my_course of res.result.data){
if(options.id === my_course.id){
let isPaid = true;
this.setData({
isPaid
})
return ;
}
}
})复制代码
if="{{isPaid}}">
"foot">打开网易云课堂APP学习 支持倍速播放"arrow">
else>
复制代码
两个小工具
- indexMock 在进行数据请求时,不要将请求写在页面的js里,将其封装出去,在需要用的页面import即可。
let indexMock = function(url){
return new Promise((resolve,reject)=>{
wx.request({
url:url,
success(res){
resolve(res.data)
},
fail(err){
reject(err)
}
})
})
}
module.exports = { indexMock: indexMock}复制代码
import { indexMock } from '../../utils/indexMock.js';
//先引入indexMock
Page({
data: {
mainPage:{},
url:"https://www.easy-mock.com/mock/5bda9d1a58caf84108172bab/study163/mainPage",
},
onLoad() {
const url = this.data.url;
indexMock(url) //indexMock返回的是一个Promise 后面用then()处理就
.then(res => {
console.log(res)
this.setData({
mainPage:res
})
})
.then(res=>{
console.log("mainpage",this.data.mainPage)
})
} //其他函数....
});
复制代码
- viewContent 在显示课程介绍时,遇到了一个头疼的问题,原来介绍是html格式的,有各种
之类的标签,而小程序内是wxml文件。最初我想到的是用wxparse来解析。但此处我只需要解析轻量的文章,有点杀鸡焉用牛刀的感觉。其实呢,wxparse的实现就是通过生成node数组来转换。所以我干脆直接用正则写一个转换的数组工具。
const viewContent = (data) => {
if(data && (typeof data === "string")){
return data.replace(/<\/?span>/g, "").replace(/<\/?p>/g, "").replace(/ /g, " ").split("
");
}
else{
return
}
}
module.exports = { viewContent: viewContent}复制代码
数据库中存的是html文档
这里要注意的一点是要给txt-item设置一个高度,实现空行的效果。
结语
写项目时总是,思考不全总是遇到各种各样的坑坑坑,算是意识到结构健壮、逻辑一致的重要性。wxml是结构,首先得保证结构的稳定性,至于样式就是体力活了,至于数据那就是MVVM大显身手的地方了。路漫漫其修远兮,吾将上下而求索。
感谢阅读,文中有错误的地方或者有建议欢迎提出!
项目地址:https://github.com/MarchYuanx/study163
欢迎 ☆☆☆star☆☆☆!