簡易商城小程式全棧開發(mpvue+koa+mongodb)

mosa發表於2018-07-26

前言

接觸小程式有一段時間後並且多多少少做了一些專案之後,又開始了vue的旅程,受其核心思想的影響,對資料/狀態管理、元件化、跨平臺等都有較高的追求,mpvue 是一個使用 Vue.js開發小程式的前端框架,由此開始了mpvue踩坑之旅,想在提高程式碼可讀性的同時,也增加一點vue.js的開發體驗。

技術棧

前端: 微信小程式、mpvue
後端:koa
資料庫:mongodb 資料庫視覺化工具:Robo3T

商城小程式開跑

一個基本的商城小程式,包含了前端的首頁、分類、購物車、我的(訂單)四個tab頁,後端的資料定義、分類、和存取。各有其色,我在下面就相應介紹一些主要功能、對比原生小程式和vue.js所踩到的坑還有後端資料庫的功能應用。 想了解或者有何問題都可以去作品原始碼中瞭解哦。

成果分享

一、前臺頁面及功能展示

首頁:

簡易商城小程式全棧開發(mpvue+koa+mongodb)
加入購物車:

簡易商城小程式全棧開發(mpvue+koa+mongodb)
購物車全選結算:

簡易商城小程式全棧開發(mpvue+koa+mongodb)
地址管理:

簡易商城小程式全棧開發(mpvue+koa+mongodb)
簡易商城小程式全棧開發(mpvue+koa+mongodb)

1. 談元件封裝

舉個栗子說,首頁由三部分組成:頭部輪播推薦+中間橫向滑動推薦+縱向滾動商品list。這三部分,幾乎是所有商城類app必需的功能了。頭部的輪播推薦、中間的橫向滑動式推薦的封裝,我們都知道,諸如此類的功能元件,在各app上基本都少不了,最初學vue最先有所體會的,便是元件程式碼複用性高的特點,在進行一些元件複用遷移至別的元件或頁面時,可能都不需要改動程式碼或者改動少量程式碼就可以直接使用,可以說是相當方便了,對於mpvue元件內仍然支援原生小程式的swiper與scroll,兩者相容後,對於熟知小程式和vue的開發者,這項功能可以很高效率地完成。
最後主頁面檔案就是由一個個元件組成,可讀性很強了,對於初學者來說,模組封裝的思想是首先就得具備的了。

<template>
  <div class="container" @click="clickHandle('test click', $event)">
    <div class="swiperList">
      <swiper :text="motto" :swiperList="swiperlist"></swiper>
    </div>
    <div class="navTab">
      <div class="recTab">
        <text>  ——  為你推薦  ——</text>
    </div>
    </div>
    <scroll></scroll>
    <div class="hot">
      <span> —— 熱門商品 ——</span>
    </div>
    <hot :v-text="motto"></hot>
    <div class="fixed-img">
      <img :src="fixImg" alt="" class="fix-img">
    </div>
  </div>
</template>
複製程式碼

不過關於元件封裝與組合的問題,由於最近有研究vue效能優化和使用者體驗的一些知識點,考慮了一個比較嚴肅的問題:
先看一下常見的vue寫法:在html裡放一個app元件,app元件裡又引用了其他的子元件,形成一棵以app為根節點的元件樹:

<body>
    <app></app> 
</body>
複製程式碼

而這種做法就引發了效能問題,要初始化一個父元件,必然需要先初始化它的子元件,而子元件又有它自己的子元件。那麼要初始化根標籤,就需要從底層開始冒泡,將頁面所有元件都初始化完。所以我們的頁面會在所有元件都初始化完才開始顯示。
這個結果顯然不是我們要的,使用者每次點開頁面,還要面對一陣子的空白和響應,因為頁面啟動後不止要響應初始化頁面的元件,還有包含在app裡的其他元件,這樣嚴重拖慢了頁面開啟的速度。
更好的結果是頁面可以從上到下按順序流式渲染,這樣可能總體時間增長了,但首屏時間縮減,在使用者看來,頁面開啟速度就更快了。網上一些辦法大同小異,各有優缺點,所以...本人也在瘋狂試驗中,靜待好訊息。

**2.Class、Style的繫結 **

在不同父元件中引用同一子元件時,但是各自需要接收繫結的動態樣式去呈現不同的樣式,在繫結css style樣式這一關上,踩了個大坑:mpvue居然不支援用object的形式傳style,起先處於樣式一直上不去的抓狂當中,網上對於mpvue這方面的細節也少之又少,後來查詢了許多地方,發現class和style的繫結都是不支援classObj和styleObj形式,就嘗試用了字串,果然...改程式碼改到懷疑人生,結果你告訴我人生起步就是錯誤,怎能不心痛?...

解決:

<template>
<div class="swiper-list">
    <d-swiper :swiperList="swiperlist" :styleObject="styleobject"></d-swiper>
</div>
</template>
<script>
    data() {
        return {
            styleobject:'width:100%;height:750rpx;position:absolute;top:0;z-index:3'
        }
    }
</script>
複製程式碼

3. “v-for巢狀”陷阱

在做vue專案的時候難免會用到迴圈,需要用到index索引值,但是v-for在巢狀時index沒辦法重複用,內迴圈與外迴圈不能共用一個index。

<swiper-item v-for="(items,index) in swiperList" :key="index">
    <div v-for="item in items" class="swiper-info" :key="item.id" @click="choose" >
        <image :src="item.url"  class="swiper-image" :style="styleObject"/>
    </div>
</swiper-item>
複製程式碼

以上程式碼就會報錯:

簡易商城小程式全棧開發(mpvue+koa+mongodb)
而給內迴圈再加上另一個索引,便沒有報錯:

<swiper-item v-for="(items,index) in swiperList" :key="index">
    <div v-for="(item,i) in items" class="swiper-info" :key="i" @click="choose" >
        <image :src="item.url"  class="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, name: '全部'},
      {id: 1, name: 'JAVA'},
      {id: 2, name: 'C++'},
      {id: 3, name: 'PHP'},
      {id: 4, name: 'VUE'},
      {id: 5, name: 'CSS'},
      {id: 6, name: 'HTML'},
      {id: 7, name: 'JavaScript'}
    ]
    this.categories = category
    this.getGoodsList(0)
  },
methods: {
    getGoodsList(categoryId){
      console.log(categoryId);
      if(categoryId == 0){
        categoryId = ''
      }
      wx.request({
        url: 'http://localhost:3030/shop/goods/list',
        data: {
          categoryId: categoryId
        },
        method: 'POST', 
        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: 'POST',
      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.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()。

const Koa = require('koa');//引入koa框架
const app = new Koa();
app.listen(3000);
這裡的app.listen()方法只是以下方法的語法糖:

const http = require('http');
const Koa = require('koa');
const app = 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驅動,簡化操作

簡易商城小程式全棧開發(mpvue+koa+mongodb)

const mongoose = require('mongoose')

const db = mongoose.createConnection('mongodb://localhost/shop') //建立與shop資料庫的連線(shop是我本地資料庫名)
複製程式碼

本地資料庫shop中建了分別“地址管理”、“商品詳情”、“訂單詳情”、“商品列表”、“使用者列表”五個資料表:

簡易商城小程式全棧開發(mpvue+koa+mongodb)

Schema介面定義資料模型:
Schema用於定義資料庫的結構。類似建立表時的資料定義(不僅僅可以定義文件的結構和屬性,還可以定義文件的例項方法、靜態模型方法、複合索引等),每個Schema會對映到mongodb中的一個collection,但是Schema不具備運算元據庫的能力。
資料表跟物件的對映,同時具有檢查效果,檢查每組資料是否滿足模型中定義的條件 同時,每個物件對映成一個資料包表,就可用該物件進行儲存操作,等同運算元據表,而非mysql命令列般繁瑣的操作

以“商品列表”資料表為例:

// 模型通過Schema介面定義。
var Schema = mongoose.Schema;

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

複製程式碼

定義了資料表中需要的資料項的型別,資料表傳入資料後會一一對應:

簡易商城小程式全棧開發(mpvue+koa+mongodb)

4.koa-router“路由庫”

const Router = require('koa-router')()//引入koa-router
const router = new Router();// 建立 router 例項物件
//註冊路由
router.post('/shop/goods/list', 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: 0,
          errMsg: 'success',
          data: docs
        })
      })
    } else {
      Goods.find((err, docs) => {
        if (err) {
          reject(err)
        }
        resolve({
          code: 0,
          errMsg: 'success',
          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,
      this.totalPrice = total,
      this.allSelect = allSelect,
      this.noSelect = noSelect,
      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",
        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(), this.totalPrice(), !currentAllSelect, this.noSelect(), list);
    },
複製程式碼

結語:

寫這個專案抓狂了很多次,因為很多vue能用的但在mpvue裡實現不了,導致走了很多彎路,踩了很多坑,但是程式猿成長不就是在一個個坑裡掉下去又爬起來的過程中嗎?作文不易,夥伴們能打賞點就打賞點吧... 順便附上我的專案地址:“mpvue-demo” ,不過還有很多需要完善的地方,漫漫長路一起走啊!

相關文章