當(dāng)3D走進(jìn)義務(wù)購(Vue3+Pinia+Koa+Three.js 全棧項(xiàng)目)
前幾天一個(gè)朋友去義烏旅游,帶回來很多小商品,就是一整個(gè)物美價(jià)廉,但是為什么線下購物和網(wǎng)購有的時(shí)候差別這么大(網(wǎng)購經(jīng)常要退換貨啊??????),為此我萌生了一個(gè)想法,3D是不是就可以實(shí)現(xiàn)在線看商品的細(xì)節(jié)了,退換貨這么麻煩是不是可以省省了??
一、項(xiàng)目概述這個(gè)項(xiàng)目是對義務(wù)購app的一個(gè)模仿,相對于其官方app,我新增的亮點(diǎn)如下:
- 商品排列布局使用瀑布流布局
- 實(shí)現(xiàn)3D看商品功能
- 實(shí)現(xiàn)3D看義烏商貿(mào)城
同時(shí),基礎(chǔ)功能如下:
- 使用 MySQL 實(shí)現(xiàn)登錄注冊的功能
- 使用 MySQL 實(shí)現(xiàn)商品搜索功能
- 使用 MySQL 實(shí)現(xiàn)對用戶的購物車及收貨地址的增刪改查功能
技術(shù)棧:Vue3 + Pinia + Three.js + Koa
- 首頁
主頁.gif- 商品展示
商品展示.gif- 圈子
圈子.gif- 商品搜索
搜索.gif- 購物車+地址管理
加購.gif
三、項(xiàng)目思路
-
登錄采用sessionStorage做數(shù)據(jù)持久化,保存當(dāng)前賬號的登錄狀態(tài),在登錄的時(shí)候向后端發(fā)起接口請求,將當(dāng)前賬號的數(shù)據(jù)返回給前端。 -
商品搜索歷史采用localStorage做數(shù)據(jù)持久化,保存當(dāng)前賬號的搜索歷史,在搜索的時(shí)候向后端發(fā)起接口請求,將當(dāng)前賬號的數(shù)據(jù)返回給前端。 -
對于官方的
商品展示以及商城的內(nèi)部,增加一個(gè)3D 預(yù)覽模塊。 -
將
不同的頁面封裝成一個(gè)組件,然后通過Vue-router對路由進(jìn)行集中管理,實(shí)現(xiàn)不同商品頁面展示不同商品。 -
借助
Pinia保存橫向?qū)Ш綑冢ㄉ唐贩N類)的 id ,購物車數(shù)量角標(biāo),glb文件的路徑。
markdown
復(fù)制代碼
|-- client // 客戶端目錄結(jié)構(gòu) |-- public //商品3D模型 |-- draco |-- model |-- src |-- api //自己封裝的axios用于響應(yīng)攔截 |-- assets //圖片及基本css的初始化 |-- components //組件 |-- router //路由配置 |-- store //倉庫 |-- views //頁面 |-- server // 服務(wù)端目錄結(jié)構(gòu) |-- config //mysql配置文件 |-- controllers //控制器 |-- data //商品數(shù)據(jù) |-- routes //路由
-
UI組件庫:
Vant -
移動(dòng)端適配:
lib-flexible -
CSS預(yù)處理器:
less -
滾動(dòng):
BetterScroll
1. 組件
眾所周知,組件可以省去很多代碼的編寫,這個(gè)項(xiàng)目中我將頭部導(dǎo)航欄,底部導(dǎo)航欄,商品瀑布流布局做成組件便于引用。這里我主要介紹下頭部導(dǎo)航欄及商品瀑布流布局的實(shí)現(xiàn)。
(1) 頭部導(dǎo)航欄(對不同類別的商品的展示)
實(shí)現(xiàn)過程:后端數(shù)據(jù)中每個(gè)類別的商品數(shù)據(jù)都包含id這個(gè)字段,我將導(dǎo)航欄的每個(gè)類別的id和后端給的id對應(yīng)起來,并將這個(gè)id存儲在pinia倉庫中,這樣只要在頁面用watch監(jiān)聽倉庫id的變化去向后端請求相應(yīng)類別的數(shù)據(jù)即可。
image.png(2) 商品瀑布流布局(提供更好的用戶體驗(yàn))
實(shí)現(xiàn)過程:利用flex布局,它可以實(shí)現(xiàn)兩欄以上的瀑布流布局,我這里是兩欄瀑布流布局,故將父容器設(shè)置為彈性容器,子容器為兩個(gè)彈性容器,將這兩個(gè)子容器的排列方向設(shè)置為垂直排列,并用flex:1;兩列平分區(qū)域占滿整個(gè)視窗。
image.png2. 倉庫
倉庫的出現(xiàn)讓我們可以在不同的頁面進(jìn)行數(shù)據(jù)共享,簡直不要太爽,再也不用擔(dān)心跨組件通信了!
這里簡單介紹一下購物車角標(biāo)的實(shí)現(xiàn):
因?yàn)樘砑踊騽h除商品,購物車角標(biāo)將立即更新,不管是在主頁還是購物車頁面還是商品詳情頁面,角標(biāo)都得實(shí)時(shí)更新它的數(shù)值,我們將變量值、更新角標(biāo)重新獲取購物車數(shù)據(jù)的方法定義在倉庫中,這樣在頁面就可以直接引入并使用就好啦~
import?{defineStore}?from?'pinia'
import?axios?from?'axios'
const?useCartStore=defineStore('cart',{
??state:()=>{??
????return{
??????badge:0???//響應(yīng)式數(shù)據(jù)badge
????}
??},
??actions:{
????async?changeBadge(){
??????const?res?=await?axios.post('/cartList',?{??//獲取購物車數(shù)據(jù)
????????username:?JSON.parse(sessionStorage.getItem('userInfo')).username
??????})
??????this.badge=res.data.length
????}
??}
})
export?default?useCartStore
3. 搜索模塊
實(shí)現(xiàn)過程:利用localStorage對搜索的詞進(jìn)行數(shù)據(jù)持久化,這樣就能方便的從localStorage中拿到搜索的歷史詞段,并將其傳給后端使用mysql檢索相應(yīng)的數(shù)據(jù),并可以對其進(jìn)行刪除(也就是清除歷史記錄)
??PS: 后面發(fā)現(xiàn)的一個(gè)小優(yōu)化: 逛淘寶發(fā)現(xiàn)我啥都不輸入點(diǎn)擊搜索可以搜索默認(rèn)的字段,那還不簡單?這只需要發(fā)一次接口請求將默認(rèn)字段傳給后端即可啦~
4. 3D商品預(yù)覽
使用?Three.js?將引入的商品模型放入頁面中,項(xiàng)目中模型來源于此:sketchfab.com[1]
由于模型的展示是通過點(diǎn)擊商品圖片后,以 遮罩層 + 動(dòng)畫 的形式呈現(xiàn)出來,不同商品展示不同模型,我們將其做成一個(gè)組件便于引用。部分代碼如下:
import?*?as?THREE?from?'three';
import?{?OrbitControls?}?from?'three/addons/controls/OrbitControls.js';
import?{?ref,?onMounted?}?from?'vue';
import?{?GLTFLoader?}?from?'three/examples/jsm/loaders/GLTFLoader'
import?{?DRACOLoader?}?from?'three/examples/jsm/loaders/DRACOLoader';
import?{?useRoute?}?from?'vue-router';
const?route?=?useRoute()
const?{?id?}?=?route.params
const?canvasDom?=?ref(null)
//場景
const?scene?=?new?THREE.Scene()
//渲染器
const?renderer?=?new?THREE.WebGLRenderer({?antialias:?true,?setAlpha:?true?})??//setAlpha讓其可設(shè)置透明度
renderer.setSize(window.innerWidth,?window.innerHeight)
//鏡頭
const?camera?=?new?THREE.PerspectiveCamera(75,?window.innerWidth?/?window.innerHeight,?0.1,?1000)
camera.position.set(10,?10,?10)
camera.lookAt(0,?0,?0)
const?controls?=?new?OrbitControls(camera,?renderer.domElement)
//?渲染函數(shù)
const?render?=?()?=>?{
??renderer.render(scene,?camera)
??controls.update()
??requestAnimationFrame(render)
}
onMounted(()?=>?{
??//渲染
??canvasDom.value.appendChild(renderer.domElement)
??//?設(shè)置背景顏色并啟用透明度
??renderer.setClearColor(0x000000,?0.2);
??render()
??//網(wǎng)格地面
??const?gridHelper?=?new?THREE.GridHelper(80)
??gridHelper.material.transparent?=?true
??gridHelper.material.opacity?=?0
??scene.add(gridHelper)
??//加載gltf模型
??const?loader?=?new?GLTFLoader()
??const?dracoLoader?=?new?DRACOLoader()
??dracoLoader.setDecoderPath('../../public/draco/gltf/')
??loader.setDRACOLoader(dracoLoader)
??loader.load(`../../public/model/${id}.glb`,?(gltf)?=>?{??//傳id讓其點(diǎn)擊不同商品展示不同模型?id對應(yīng)商品的id
????//?console.log(gltf.scene);
????const?bmw?=?gltf.scene
????bmw.scale.set(0.2,?0.2,?0.2);?//模型縮放
????scene.add(bmw)?//將整個(gè)模型組添加到場景中
??})
});
//灑滿燈光
const?light?=?new?THREE.DirectionalLight(0xffffff,?1)
light.position.set(0,?0,?10)
scene.add(light)
const?light2?=?new?THREE.DirectionalLight(0xffffff,?1);
light2.position.set(0,?0,?-10);
scene.add(light2);
const?light3?=?new?THREE.DirectionalLight(0xffffff,?1);
light3.position.set(10,?0,?0);
scene.add(light3);
const?light4?=?new?THREE.DirectionalLight(0xffffff,?1);
light4.position.set(-10,?0,?0);
scene.add(light4);
const?light5?=?new?THREE.DirectionalLight(0xffffff,?1);
light5.position.set(0,?10,?0);
scene.add(light5);
const?light6?=?new?THREE.DirectionalLight(0xffffff,?0.3);
light6.position.set(5,?10,?0);
scene.add(light6);
const?light7?=?new?THREE.DirectionalLight(0xffffff,?0.3);
light7.position.set(0,?10,?5);
scene.add(light7);
const?light8?=?new?THREE.DirectionalLight(0xffffff,?0.3);
light8.position.set(0,?10,?-5);
scene.add(light8);
const?light9?=?new?THREE.DirectionalLight(0xffffff,?0.3);
light9.position.set(-5,?10,?0);
scene.add(light9);
5. 商貿(mào)城3D預(yù)覽
實(shí)現(xiàn)過程與3D商品預(yù)覽類似,我們只需用這段代碼將全景圖作為場景的背景圖即可(全景圖的資源路徑存在倉庫中):
cubeTextureLoader.load(store.loadUrl,?(texture)?=>?{
????const?crt?=?new?THREE.WebGLCubeRenderTarget(texture.image.height)
????crt.fromEquirectangularTexture(renderer,?texture)??//把全景圖轉(zhuǎn)換為紋理格式
????scene.background?=?crt.texture
??})
六、后端實(shí)現(xiàn)
使用 Koa 框架搭建后端開發(fā)環(huán)境,后端分為四塊:
- 配置文件:對mysql的配置
- 路由:定義接口請求路徑及響應(yīng)體
- 控制器:當(dāng)接口被請求時(shí),需要向前端響應(yīng)的操作,即數(shù)據(jù)庫的增刪改查
- 數(shù)據(jù):后端提供給前端的數(shù)據(jù)
數(shù)據(jù)庫中創(chuàng)建了三個(gè)表:
- users表:存儲用戶賬號密碼
- cart表: 存儲用戶的購物車信息
- address表: 存儲用戶的地址信息
這里以登錄注冊模塊為例,路由代碼如下:
const?router?=?require('koa-router')()
//引入拋出的對象里的方法
const?userService?=?require('../controllers/mySqlController.js')
router.prefix('/users')
//登錄接口
router.post('/login',?async?(ctx,?next)?=>?{
??console.log(ctx.request.body);
??const?{?username,?password?}?=?ctx.request.body
??//去讀取數(shù)據(jù)庫中的users表,判斷讀取到的值和前端傳過來的值是否匹配
??try?{
????const?result?=?await?userService.userLogin(username,?password)
????console.log(result);
????if?(result.length)?{
??????let?data?=?{
????????id:?result[0].id,
????????username:?result[0].username
??????}
??????ctx.body?=?{
????????code:?'80000',
????????data:?data,
????????msg:?'登陸成功'
??????}
????}?else?{
??????ctx.body?=?{
????????code:?'80004',
????????data:?'error',
????????msg:?'賬號或密碼錯(cuò)誤'
??????}
????}
??}?catch?(error)?{
????ctx.body?=?{
??????code:?'80002',
??????data:?error,
??????msg:?'服務(wù)器異常'
????}
??}
})
//注冊接口
router.post('/register',?async?(ctx,?next)?=>?{
??const?{?username,?password?}?=?ctx.request.body
??//判斷賬號或密碼是否為空
??if?(!username?||?!password)?{
????ctx.body?=?{
??????code:?'80001',
??????msg:?'賬號或密碼不能為空'
????}
????return
??}
??//判斷該賬號是否在數(shù)據(jù)庫中存在
??try?{
????let?findres?=?await?userService.userfind(username)
????if?(findres.length)?{??//如找到數(shù)據(jù)則向前端報(bào)錯(cuò)
??????ctx.body?=?{
????????code:?'80003',
????????data:?'error',
????????msg:?'用戶名已存在!'
??????}
????}?else?{??//如沒找到則注冊成功,往數(shù)據(jù)庫添加這條數(shù)據(jù)
??????await?userService.userRegister([username,?password])
????????.then(res?=>?{
??????????//?console.log(res);
??????????if?(res.affectedRows?!==?0)?{
????????????ctx.body?=?{
??????????????code:?'80000',
??????????????data:?'success',
??????????????msg:?'注冊成功!'
????????????}
??????????}?else?{
????????????ctx.body?=?{
??????????????code:?'80004',
??????????????data:?'error',
??????????????msg:?'注冊失??!'
????????????}
??????????}
????????})
????}
??}?catch?(error)?{
????ctx.body?=?{
??????code:?'80002',
??????data:?error,
??????msg:?'服務(wù)器異常'
????}
??}
})
module.exports?=?router
七、總結(jié)
這個(gè)項(xiàng)目讓我對Vue3這個(gè)框架的使用更熟練了,整個(gè)過程中也遇到了很多bug及問題,以前我是一個(gè)很怕代碼出bug的小白,一遇到問題就問別人哈哈哈,這個(gè)項(xiàng)目讓我學(xué)會了怎樣一步一步尋找錯(cuò)誤并分析原因,現(xiàn)在自己能夠解決大多數(shù)的bug,也回顧了很多基礎(chǔ)的js知識,體驗(yàn)了一把理論聯(lián)系實(shí)踐了。不過,這個(gè)項(xiàng)目還是有需要改進(jìn)完善的地方,后續(xù)再見!
附項(xiàng)目地址 [2]
參考資料
[1]https://link.juejin.cn/?target=https%3A%2F%2Fsketchfab.com
[2]https://gitee.com/zt18507085081/Yiwu_Shopping
關(guān)于本文作者:zt_ever https://juejin.cn/post/7251101434023624760
最后
最后不要忘了點(diǎn)贊呦!
