|
接觸小程序有一段時間后并且多多少少做了一些項(xiàng)目之后,又開始了vue的旅程,受其核心思想的影響,對數(shù)據(jù)/狀態(tài)管理、組件化、跨平臺等都有較高的追求,mpvue 是一個使用 Vue.js開發(fā)小程序的前端框架,由此開始了mpvue踩坑之旅,想在提高代碼可讀性的同時,也增加一點(diǎn)vue.js的開發(fā)體驗(yàn)。 技術(shù)棧前端: 微信小程序、mpvue 后端:koa 數(shù)據(jù)庫:mongodb 數(shù)據(jù)庫可視化工具:Robo3T 商城小程序開跑一個基本的商城小程序,包含了前端的首頁、分類、購物車、我的(訂單)四個tab頁,后端的數(shù)據(jù)定義、分類、和存取。各有其色,我在下面就相應(yīng)介紹一些主要功能、對比原生小程序和vue.js所踩到的坑還有后端數(shù)據(jù)庫的功能應(yīng)用。 想了解或者有何問題都可以去 作品源碼 中了解哦。 成果分享一、前臺頁面及功能展示首頁:
加入購物車:
購物車全選結(jié)算:
地址管理:
1. 談組件封裝 舉個栗子說,首頁由三部分組成:頭部輪播推薦+中間橫向滑動推薦+縱向滾動商品list。這三部分,幾乎是所有商城類app必需的功能了。頭部的輪播推薦、中間的橫向滑動式推薦的封裝,我們都知道,諸如此類的功能組件,在各app上基本都少不了,最初學(xué)vue最先有所體會的,便是組件代碼復(fù)用性高的特點(diǎn),在進(jìn)行一些組件復(fù)用遷移至別的組件或頁面時,可能都不需要改動代碼或者改動少量代碼就可以直接使用,可以說是相當(dāng)方便了,對于mpvue組件內(nèi)仍然支持原生小程序的swiper與scroll,兩者兼容后,對于熟知小程序和vue的開發(fā)者,這項(xiàng)功能可以很高效率地完成。 最后主頁面文件就是由一個個組件組成,可讀性很強(qiáng)了,對于初學(xué)者來說,模塊封裝的思想是首先就得具備的了。
<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>
復(fù)制代碼
不過關(guān)于組件封裝與組合的問題,由于最近有研究vue性能優(yōu)化和用戶體驗(yàn)的一些知識點(diǎn),考慮了一個比較嚴(yán)肅的問題: 先看一下常見的vue寫法:在html里放一個app組件,app組件里又引用了其他的子組件,形成一棵以app為根節(jié)點(diǎn)的組件樹:
<body>
<app></app>
</body>
復(fù)制代碼
而這種做法就引發(fā)了性能問題,要初始化一個父組件,必然需要先初始化它的子組件,而子組件又有它自己的子組件。那么要初始化根標(biāo)簽,就需要從底層開始冒泡,將頁面所有組件都初始化完。所以我們的頁面會在所有組件都初始化完才開始顯示。 這個結(jié)果顯然不是我們要的,用戶每次點(diǎn)開頁面,還要面對一陣子的空白和響應(yīng),因?yàn)轫撁鎲雍蟛恢挂憫?yīng)初始化頁面的組件,還有包含在app里的其他組件,這樣嚴(yán)重拖慢了頁面打開的速度。 更好的結(jié)果是頁面可以從上到下按順序流式渲染,這樣可能總體時間增長了,但首屏?xí)r間縮減,在用戶看來,頁面打開速度就更快了。網(wǎng)上一些辦法大同小異,各有優(yōu)缺點(diǎn),所以...本人也在瘋狂試驗(yàn)中,靜待好消息。 **2.Class、Style的綁定 **在不同父組件中引用同一子組件時,但是各自需要接收綁定的動態(tài)樣式去呈現(xiàn)不同的樣式,在綁定css style樣式這一關(guān)上,踩了個大坑:mpvue居然不支持用object的形式傳style,起先處于樣式一直上不去的抓狂當(dāng)中,網(wǎng)上對于mpvue這方面的細(xì)節(jié)也少之又少,后來查找了許多地方,發(fā)現(xiàn)class和style的綁定都是不支持classObj和styleObj形式,就嘗試用了字符串,果然...改代碼改到懷疑人生,結(jié)果你告訴我人生起步就是錯誤,怎能不心痛?... 解決:
<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>
復(fù)制代碼
3. “v-for嵌套”陷阱在做vue項(xiàng)目的時候難免會用到循環(huán),需要用到index索引值,但是v-for在嵌套時index沒辦法重復(fù)用,內(nèi)循環(huán)與外循環(huán)不能共用一個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>
復(fù)制代碼
以上代碼就會報(bào)錯:
而給內(nèi)循環(huán)再加上另一個索引,便沒有報(bào)錯:
<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>
復(fù)制代碼
4.this指向問題與箭頭函數(shù)的應(yīng)用這是vue文檔里的原話:All lifecycle hooks are called with their 'this' context pointing to the Vue instance invoking it. 意思是:在Vue所有的生命周期鉤子方法(如created,mounted, updated以及destroyed)里使用this,this指向調(diào)用它的Vue實(shí)例,即(new Vue)。 mpvue里同理。 我們都知道,生命周期函數(shù)中的this都是指向Vue實(shí)例的,因此我們就可以訪問數(shù)據(jù),對屬性和方法進(jìn)行運(yùn)算。
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;
}
})
},
}
復(fù)制代碼
普通函數(shù)this指向這個函數(shù)運(yùn)行的上下文環(huán)境,也就是調(diào)用它的上下文,所以在這里,對于生命周期函數(shù)用普通函數(shù)還是箭頭函數(shù)其實(shí)并沒有影響,因?yàn)樗亩x環(huán)境與運(yùn)行環(huán)境是同一個,所以同樣能取到vue實(shí)例中數(shù)據(jù)、屬性和方法。 箭頭函數(shù)中,this指向的是定義它的最外層代碼塊,()=>{} 等價于 function(){}.bind(this);所以this當(dāng)然指向的是vue實(shí)例。起初并沒有考慮到this指向的問題,在wx.request({})中success用了普通函數(shù),結(jié)果一直報(bào)錯“goods is not defined”,用了箭頭函數(shù)才解決,起初普通函數(shù)的this指向 getGoodsList()的上下文環(huán)境,所以一直沒辦法取到值。 5.onLoad與onShow在進(jìn)行首頁點(diǎn)擊商品跳轉(zhuǎn)到詳情頁時,onLoad()無法獲取更新數(shù)據(jù)。 首先雖然onLoad: function (options) 這個是可以接受到值的,但是這個只是加載一次,不是我想要的效果,我需要在本頁面(不關(guān)閉的情況下)到另外一個頁面在跳轉(zhuǎn)進(jìn)來,接收到對應(yīng)商品的數(shù)據(jù)。 所以需要將代碼放在onshow內(nèi)部, 在每次頁面加載的時候都會進(jìn)行當(dāng)前狀態(tài)的查詢,查詢對應(yīng)數(shù)據(jù)的子對象,更新渲染到詳情頁上。
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數(shù)據(jù)表的數(shù)據(jù)
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;
}
})
}
復(fù)制代碼
了解小程序onLoad與onShow生命周期函數(shù): onLoad:生命周期函數(shù)–監(jiān)聽小程序初始化,當(dāng)小程序初始化完成時,會觸發(fā) onLoadh(全局只觸發(fā)一次)。 onShow:生命周期函數(shù)–監(jiān)聽小程序顯示,當(dāng)小程序啟動,或從后臺進(jìn)入前臺顯示,會觸發(fā) onShow。 二、后臺數(shù)據(jù)庫及數(shù)據(jù)存取1.架設(shè) HTTP 服務(wù)在全局配置文件中: 1).引入koa并實(shí)例化
const Koa = require('koa');
const app = new Koa()
復(fù)制代碼
2).app.listen(端口號):創(chuàng)建并返回 HTTP 服務(wù)器,將給定的參數(shù)傳遞給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);
復(fù)制代碼
這樣基本的配置完畢,我們就可以用“http://localhost3030+請求地址參數(shù)”獲取到數(shù)據(jù)庫的值了。 2.Koa-router路由中間件koa-router 是常用的 koa 的路由庫。 如果依靠ctx.request.url去手動處理路由,將會寫很多處理代碼,這時候就需要對應(yīng)的路由的中間件對路由進(jìn)行控制,這里介紹一個比較好用的路由中間件koa-router。 以路由切換催動界面切換,”數(shù)據(jù)化”界面。 3.建立對象模型在構(gòu)建函數(shù)庫之前,先來聊聊對象的建模。 Mongoose是在node.js異步環(huán)境下對mongodb進(jìn)行便捷操作的對象模型工具。該npm包封裝了操作mongodb的方法。 Mongoose有兩個特點(diǎn): 1、通過關(guān)系型數(shù)據(jù)庫的思想來設(shè)計(jì)非關(guān)系型數(shù) 2、基于mongodb驅(qū)動,簡化操作
const mongoose = require('mongoose')
const db = mongoose.createConnection('mongodb://localhost/shop') //建立與shop數(shù)據(jù)庫的連接(shop是我本地?cái)?shù)據(jù)庫名)
復(fù)制代碼
本地?cái)?shù)據(jù)庫shop中建了分別“地址管理”、“商品詳情”、“訂單詳情”、“商品列表”、“用戶列表”五個數(shù)據(jù)表:
Schema界面定義數(shù)據(jù)模型: Schema用于定義數(shù)據(jù)庫的結(jié)構(gòu)。類似創(chuàng)建表時的數(shù)據(jù)定義(不僅僅可以定義文檔的結(jié)構(gòu)和屬性,還可以定義文檔的實(shí)例方法、靜態(tài)模型方法、復(fù)合索引等),每個Schema會映射到mongodb中的一個collection,但是Schema不具備操作數(shù)據(jù)庫的能力。 數(shù)據(jù)表跟對象的映射,同時具有檢查效果,檢查每組數(shù)據(jù)是否滿足模型中定義的條件 同時,每個對象映射成一個數(shù)據(jù)報(bào)表,就可用該對象進(jìn)行保存操作,等同操作數(shù)據(jù)表,而非mysql命令行般繁瑣的操作 以“商品列表”數(shù)據(jù)表為例:
// 模型通過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,
})
復(fù)制代碼
定義了數(shù)據(jù)表中需要的數(shù)據(jù)項(xiàng)的類型,數(shù)據(jù)表傳入數(shù)據(jù)后會一一對應(yīng):
4.koa-router“路由庫”
const Router = require('koa-router')()//引入koa-router
const router = new Router();// 創(chuàng)建 router 實(shí)例對象
//注冊路由
router.post('/shop/goods/list', async (ctx, next) => {
const params = ctx.request.body
//以‘listSchema’的模型去取到Goods的數(shù)據(jù)
const Goods = db.db.model('Goods', db.listSchema) //第一個‘db’是require來的自定義的,第二個‘db’是取到連接到mongodb的數(shù)據(jù)庫,model代指實(shí)體數(shù)據(jù)(根據(jù)schema獲取該字段下的數(shù)據(jù),然后傳給Goods))
ctx.body = await new Promise((resolve, reject) => {//ctx.body是ctx.response.body的縮寫,代指響應(yīng)數(shù)據(jù)
//異步,等到獲取到數(shù)據(jù)之后再將body發(fā)出去
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
})
})
}
})
})
復(fù)制代碼
所有的數(shù)據(jù)庫操作都是異步的操作,所以需要封裝promise來實(shí)現(xiàn),由此通過POST “http://localhost3030/shop/goods/list”便可訪問本地shop數(shù)據(jù)庫了。 這里順便提一下“ctx”的使用,ctx(context)上下文,我們都知道有node.js 中有request(請求)對象和respones(響應(yīng))對象。Koa把這兩個對象封裝在ctx對象中。 參數(shù)ctx是由koa傳入的封裝了request和response的變量,我們可以通過它訪問request和response (前端通過ajax請求http獲取數(shù)據(jù)) 我們可以通過ctx請求or獲取數(shù)據(jù)庫中的數(shù)據(jù)。 Ctx.body 屬性就是發(fā)送給用戶的內(nèi)容 body是http協(xié)議中的響應(yīng)體,header是指響應(yīng)頭 ctx.body = ctx.res.body = ctx.response.body 5.數(shù)據(jù)緩存之模型層設(shè)置1).為什么要做數(shù)據(jù)緩存? 在這里不得不提一句數(shù)據(jù)緩存的重要性,雖然我是從本地?cái)?shù)據(jù)庫獲取的數(shù)據(jù),但是由于需要的數(shù)據(jù)量較多,再者前面說的性能優(yōu)化還未完成,每次還是有一定的請求時間,沒必要每次打開都去請求一遍后端,渲染頁面較慢,所以需要將需要經(jīng)常用到的數(shù)據(jù)做本地緩存,這樣能大大提高頁面渲染速度。 2).設(shè)置模型層
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
})
},
復(fù)制代碼
將需要做本地存儲數(shù)據(jù)的方法封裝成一個方法模型,當(dāng)需要做本地存儲時,直接做引用,如今vue、react中多用到的架構(gòu)思想,都對模型層封裝有一定的要求。
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);
}
|