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