小程序 网易云课堂云开发初体验

前不久,小程序推出了云开发的功能,使开发者们无需搭建服务器,用云端能力直接迈入全栈开发。正巧用着网易云课堂充电,界面精致、细节到位,于是决定用云开发来仿一仿网易云课堂。

来,先看一波效果图

购物车

直接购买



工具

在项目开发中选用好的工具使得工作事半功倍

  1. 微信开发者工具   (云开发)
  2. vscode  vs中如emmet之类的各种插件能让coding爽的不行,我是在vs中coding 在开发者工具里调试  。
  3. easymock 快速的模拟出数据请求,(主页的数据请求量较多且只是可读,就直接用easymock了)
  4. 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属性(bool)。

当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☆☆☆!





你可能感兴趣的:(小程序 网易云课堂云开发初体验)