简易商城小程序全栈开发(mpvue+koa+mongodb)

接触小程序有一段时间后并且多多少少做了一些项目之后,又开始了vue的旅程,受其核心思想的影响,对数据/状态管理、组件化、跨平台等都有较高的追求,mpvue 是一个使用 Vue.js开发小程序的前端框架,由此开始了mpvue踩坑之旅,想在提高代码可读性的同时,也增加一点vue.js的开发体验。

技术栈

前端: 微信小程序、mpvue

后端:koa

数据库:mongodb 数据库可视化工具:Robo3T

商城小程序开跑

一个基本的商城小程序,包含了前端的首页分类、购物车、我的(订单)四个tab页,后端的数据定义、分类、和存取。各有其色,我在下面就相应介绍一些主要功能、对比原生小程序和vue.js所踩到的坑还有后端数据库功能应用。 想了解或者有何问题都可以去 作品源码 中了解哦。

成果分享

一、前台页面功能展示

首页


加入购物车:

购物车全选结算:

地址管理:

1. 谈组件封装

举个栗子说,首页由三部分组成:头部轮播推荐+中间横向滑动推荐+纵向滚动商品list。这三部分,几乎是所有商城类app必需的功能了。头部的轮播推荐、中间的横向滑动式推荐的封装,我们都知道,诸如此类的功能组件,在各app上基本都少不了,最初学vue最先有所体会的,便是组件代码复用性高的特点,在进行一些组件复用迁移至别的组件或页面时,可能都不需要改动代码或者改动少量代码就可以直接使用,可以说是相当方便了,对于mpvue组件内仍然支持原生小程序的swiper与scroll,两者兼容后,对于熟知小程序和vue的开发者,这项功能可以很高效率地完成。

最后主页面文件就是由一个个组件组成,可读性很强了,对于初学者来说,模块封装的思想是首先就得具备的了。

<template>
  <div class="container" @click="clickHandle('test click',$event)">
    <"swiperList">
      <swiper :text="motto" :swiperList="swiperlist"></swiper>
    </div>
    <"navTab">
      <"recTab">
        <text>  ——  为你推荐  ——</text>
    </div>
    </scroll></scroll>
    <"hot">
      <span> —— 热门商品 ——</span>
    </hot :v-text="motto"></hot>
    <"fixed-img">
      <img :src="fixImg" alt="" class="fix-img">
    </div>
  </div>
</template>
复制代码

不过关于组件封装与组合的问题,由于最近有研究vue性能优化和用户体验的一些知识点,考虑了一个比较严肃的问题:

先看一下常见的vue写法:在html里放一个app组件,app组件里又引用了其他的子组件,形成一棵以app为根节点的组件树:

而这种做法就引发了性能问题,要初始化一个父组件,必然需要先初始化它的子组件,而子组件又有它自己的子组件。那么要初始化根标签,就需要从底层开始冒泡,将页面所有组件都初始化完。所以我们的页面会在所有组件都初始化完才开始显示

这个结果显然不是我们要的,用户每次点开页面,还要面对一阵子的空白和响应,因为页面启动后不止要响应初始化页面的组件,还有包含在app里的其他组件,这样严重拖慢了页面打开的速度。

更好的结果是页面可以从上到下按顺序流式渲染,这样可能总体时间增长了,但首屏时间缩减,在用户看来,页面打开速度就更快了。网上一些办法大同小异,各有优缺点,所以...本人也在疯狂试验中,静待好消息。

**2.Class、Style的绑定 **

在不同父组件中引用同一子组件时,但是各自需要接收绑定的动态样式去呈现不同的样式,在绑定css style样式这一关上,踩了个大坑:mpvue居然不支持用object的形式传style,起先处于样式一直上不去的抓狂当中,网上对于mpvue这方面的细节也少之又少,后来查找了许多地方,发现class和style的绑定都是不支持classObj和styleObj形式,就尝试用了字符串,果然...改代码改到怀疑人生,结果你告诉我人生起步就是错误,怎能不心痛?...

解决

        }
    }
</script>
复制代码

3. “v-for嵌套”陷阱

在做vue项目的时候难免会用到循环,需要用到index索引值,但是v-for在嵌套时index没办法重复用,内循环与外循环不能共用一个index。

<swiper-item v-for="(items,index) in swiperList" :key="index">
    <div v-"item in items" class="swiper-info" :key="item.id" @click="choose" >
        <image :src="item.url"  "swiper-image" :style="styleObject"/>
    </div>
</swiper-item>
复制代码

以上代码就会报错:

而给内循环再加上一个索引,便没有报错:

4.this指向问题与箭头函数的应用

这是vue文档里的原话:All lifecycle hooks are called with their 'this' context pointing to the Vue instance invoking it.

意思是:在Vue所有的生命周期钩子方法(如created,mounted, updated以及destroyed)里使用this,this指向调用它的Vue实例,即(new Vue)。 mpvue里同理。 我们都知道,生命周期函数中的this都是指向Vue实例的,因此我们就可以访问数据,对属性方法进行运算。

props:{
    goods:Array
},mounted: function(options){
    let category = [
      {id: 0,0);">name: '全部'},{1,0);">'JAVA'},0);">2,0);">'C++'},0);">3,0);">'PHP'},0);">4,0);">'VUE'},0);">5,0);">'CSS'},0);">6,0);">'HTML'},0);">7,0);">'JavaScript'}
    ]
    this.categories = category
    this.getGoodsList(0)
  },0);">methods: {
    getGoodsList(categoryId){
      console.log(categoryId);
      if(categoryId == 0){
        categoryId = ''
      }
      wx.request({
        url: 'http://localhost:3030/shop/goods/list',        data: {
          categoryId: categoryId
        },96);">        method: 'POST',96);">        success: res => {
          console.log(res);
          this.goods = res.data.data;
        }
      })
    },}
复制代码

普通函数this指向这个函数运行的上下文环境,也就是调用它的上下文,所以在这里,对于生命周期函数用普通函数还是箭头函数其实并没有影响,因为它的定义环境与运行环境是同一个,所以同样能取到vue实例中数据、属性方法。 箭头函数中,this指向的是定义它的最外层代码块,()=>{} 等价于 function(){}.bind(this);所以this当然指向的是vue实例。起初并没有考虑到this指向的问题,在wx.request({})中success用了普通函数,结果一直报错“goods is not defined”,用了箭头函数解决,起初普通函数的this指向 getGoodsList()的上下文环境,所以一直没办法取到值。

5.onLoad与onShow

在进行首页点击商品跳转到详情页时,onLoad()无法获取更新数据。

首先虽然onLoad: function (options) 这个是可以接受到值的,但是这个只是加载一次,不是我想要的效果,我需要在本页面(不关闭的情况下)到另外一个页面跳转进来,接收到对应商品的数据。

所以需要将代码放在onshow内部, 在每次页面加载的时候都会进行当前状态的查询查询对应数据的子对象,更新渲染到详情页上。

onShow: function(options){
    // console.log(this.styleobject)
      // console.log(options)
    wx.getStorage({
      key: 'shopCarInfo',success: (res) =>{
        // success
        console.log(`initshopCarInfo:${res.data}`)
        this.shopCarInfo = res.data;
        this.shopNum = res.data.shopNum
      }
    })
    wx.request({
      url: 'http://localhost:3030/shop/goods/detail',//请求detail数据表的数据
      method: data: {
        id: options.id
      },success: res =>{
        // console.log(res);
        const dataInfo = res.data.data.basicInfo;
        this.saveShopCar = dataInfo;
        this.goodsDetail.name = dataInfo.name;
        this.goodsDetail.minPrice = dataInfo.minPrice;
        this.goodsDetail.goodsDescribe = dataInfo.characteristic;

        let goodsLabel = this.goodsLabel
        goodsLabel = res.data;
        // console.log(goodsLabel);
        this.selectSizePrice = dataInfo.minPrice;
        this.goodsLabel.pic = dataInfo.pic;
        this.goodsLabel.name = dataInfo.name;
        this.buyNumMax = dataInfo.stores;
        this.buyNumMin = (dataInfo.stores > 0) ? 1 : 0;
      }
    })
  }
复制代码

了解小程序onLoad与onShow生命周期函数

onLoad:生命周期函数–监听小程序初始化,当小程序初始化完成时,会触发 onLoadh(全局只触发一次)。

onShow:生命周期函数–监听小程序显示,当小程序启动,或从后台进入前台显示,会触发 onShow。

二、后台数据库及数据存取

1.架设 HTTP 服务

在全局配置文件中: 1).引入koa并实例化

const Koa = require('koa');
const app = new Koa()
复制代码

2).app.listen(端口号):创建并返回 HTTP 服务器,将给定的参数传递给Server#listen()。

);//引入koa框架
new Koa();
app.listen(3000);
这里的app.listen()方法只是以下方法的语法糖:

const http = 'http');
'koa');
new Koa();
http.createServer(app.callback()).listen(3000);
复制代码

这样基本的配置完毕,我们就可以用“http://localhost3030+请求地址参数”获取数据库的值了。

2.Koa-router路由中间件

koa-router 是常用的 koa 的路由库。

如果依靠ctx.request.url去手动处理路由,将会写很多处理代码,这时候就需要对应的路由的中间件对路由进行控制,这里介绍一个比较好用的路由中间件koa-router。

以路由切换催动界面切换,”数据化”界面。

3.建立对象模型

在构建函数库之前,先来聊聊对象的建模。

Mongoose是在node.js异步环境下对mongodb进行便捷操作的对象模型工具。该npm包封装了操作mongodb的方法

Mongoose有两个特点:

1、通过关系型数据库的思想来设计非关系型数

2、基于mongodb驱动,简化操作

)

const db = mongoose.createConnection('mongodb://localhost/shop') //建立与shop数据库的连接(shop是我本地数据库名)
复制代码

本地数据库shop中建了分别“地址管理”、“商品详情”、“订单详情”、“商品列表”、“用户列表”五个数据表:

Schema界面定义数据模型:

Schema用于定义数据库的结构。类似创建表时的数据定义(不仅仅可以定义文档的结构和属性,还可以定义文档的实例方法、静态模型方法、复合索引等),每个Schema会映射到mongodb中的一个collection,但是Schema不具备操作数据库的能力。

数据表跟对象的映射,同时具有检查效果,检查每组数据是否满足模型中定义的条件 同时,每个对象映射成一个数据报表,就可用该对象进行保存操作,等同操作数据表,而非MySQL命令行般繁琐的操作

以“商品列表”数据表为例:

// 模型通过Schema界面定义。
var Schema = mongoose.Schema;

const listSchema = new Schema({
  barCode: String,categoryId: Number,characteristic: commission: commissionType: dateAdd: dateStart: id: Schema.Types.ObjectId,logisticsId: minPrice: minscore: name: numberFav: numberGoodReputation: numberOrders: originalPrice: paixu: pic: pingtuan: Boolean,pingtuanPrice: propertyIds: recommendStatus: recommendStatusstr: shopId: status: statusstr: stores: userId: videoId: views: weight: 代码

定义了数据表中需要的数据项的类型,数据表传入数据后会一一对应:

4.koa-router“路由库”

)()//引入koa-router
const router = new Router();// 创建 router 实例对象
//注册路由
router.post('/shop/goods/list',51); font-weight: 700;">async (ctx,next) => {
  const params = ctx.request.body
  //以‘listSchema’的模型去取到Goods的数据
  const Goods = db.db.model('Goods',db.listSchema) //第一个‘db’是require来的自定义的,第二个‘db’是取到连接到mongodb的数据库,model代指实体数据(根据schema获取该字段下的数据,然后传给Goods))
  ctx.body = await new Promise((resolve,reject) => {//ctx.body是ctx.response.body的缩写,代指响应数据
    //异步,等到获取到数据之后再将body发出去
    if (params.categoryId) {
      Goods.find({categoryId: params.categoryId},(err,docs) => {
        if (err) {
          reject(err)
        }
        resolve({
          code: errMsg: 'success',data: docs
        })
      })
    } else {
      Goods.find((err,docs) => {
        data: docs
        })
      })
    }
  })
})
复制代码

所有的数据库操作都是异步的操作,所以需要封装promise来实现,由此通过POST “http://localhost3030/shop/goods/list”便可访问本地shop数据库了。 这里顺便提一下“ctx”的使用,ctx(context)上下文,我们都知道有node.js 中有request(请求)对象和respones(响应)对象。Koa把这两个对象封装在ctx对象中。 参数ctx是由koa传入的封装了request和response的变量,我们可以通过它访问request和response (前端通过ajax请求http获取数据) 我们可以通过ctx请求or获取数据库中的数据。

Ctx.body 属性就是发送给用户内容

body是http协议中的响应体,header是指响应头

ctx.body = ctx.res.body = ctx.response.body

5.数据缓存之模型层设置

1).为什么要做数据缓存?

在这里不得不提一句数据缓存的重要性,虽然我是从本地数据库获取的数据,但是由于需要的数据量较多,再者前面说的性能优化还未完成,每次还是有一定的请求时间,没必要每次打开都去请求一遍后端,渲染页面较慢,所以需要将需要经常用到的数据做本地缓存,这样能大大提高页面渲染速度。

2).设置模型层

setGoodsList: function (saveHidden,total,allSelect,noSelect,list) {
      this.saveHidden = saveHidden,51); font-weight: 700;">this.totalPrice = total,51); font-weight: 700;">this.allSelect = allSelect,51); font-weight: 700;">this.noSelect = noSelect,51); font-weight: 700;">this.goodsList = list
      var shopCarInfo = {};
      var tempNumber = 0;
      var list = [];
      shopCarInfo.shoplist = list;

      for (var i = 0; i < list.length; i++) {
        tempNumber = tempNumber + list[i].number
      }
      shopCarInfo.shopNum = tempNumber;
      wx.setStorage({
        key: "shopCarInfo",51); font-weight: 700;">data: shopCarInfo
      })
    },复制代码

将需要做本地存储数据的方法封装成一个方法模型,当需要做本地存储时,直接做引用,如今vue、react中多用到的架构思想,都对模型层封装有一定的要求。

bindAllSelect() {
      var list = this.goodsList;
      var currentAllSelect = this.allSelect
      if (currentAllSelect) {
        list.forEach((item) => {
          item.active = false
        })
      } else {
        list.forEach((item) => {
          item.active = true
        })
      }
      this.setGoodsList(this.getSaveHide(),51); font-weight: 700;">this.totalPrice(),!currentAllSelect,51); font-weight: 700;">this.noSelect(),list);
    },复制代码

结语:

写这个项目抓狂了很多次,因为很多vue能用的但在mpvue里实现不了,导致走了很多弯路,踩了很多坑,但是程序猿成长不就是在一个个坑里掉下去又爬起来的过程中吗?作文不易,伙伴们能打赏点就打赏点吧... 顺便附上我的项目地址: “mpvue-demo” ,不过还有很多需要完善的地方,漫漫长路一起走啊!


作者:mosa

原文:https://juejin.im/post/5b548ce8e51d45191d79f8a6?utm_source=tuicool&utm_medium=referral

相关文章

开发微信小程序的用户授权登录功能
小程序开发页面如何实现跳转?
浅谈小程序开发中蓝牙连接错误分析及解决方法
什么是小程序?它有哪些功能?
如何配置小程序开发项目结构?(教程)
怎么把自己的店加入小程序