前言
接觸小程式有一段時間後並且多多少少做了一些專案之後,又開始了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)">
<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>
複製程式碼
以上程式碼就會報錯:
而給內迴圈再加上另一個索引,便沒有報錯:<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驅動,簡化操作
const mongoose = require('mongoose')
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: 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,
})
複製程式碼
定義了資料表中需要的資料項的型別,資料表傳入資料後會一一對應:
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” ,不過還有很多需要完善的地方,漫漫長路一起走啊!