Vue3+Pinia+Koa+Three.js 全栈电商项目总结复盘
作者:zt_ever
https://juejin.cn/post/7251101434023624760
前言
前几天一个朋友去义乌旅游,带回来很多小商品,就是一整个物美价廉,但是为什么线下购物和网购有的时候差别这么大(网购经常要退换货啊😭😭😭),为此我萌生了一个想法,3D是不是就可以实现在线看商品的细节了,退换货这么麻烦是不是可以省省了😏
一、项目概述
这个项目是对义务购app的一个模仿,相对于其官方app,我新增
的亮点如下:
商品排列布局使用瀑布流布局 实现3D看商品功能 实现3D看义乌商贸城
同时,基础
功能如下:
使用 MySQL 实现登录注册的功能 使用 MySQL 实现商品搜索功能 使用 MySQL 实现对用户的购物车及收货地址的增删改查功能
技术栈:Vue3 + Pinia + Three.js + Koa
二、项目展示
首页
商品展示
圈子
商品搜索
购物车+地址管理
三、项目思路
登录
采用sessionStorage
做数据持久化,保存
当前账号的登录状态
,在登录的时候向后端发起接口请求,将当前账号的数据返回给前端。商品搜索历史
采用localStorage
做数据持久化,保存
当前账号的搜索历史
,在搜索的时候向后端发起接口请求,将当前账号的数据返回给前端。对于官方的 商品展示
以及商城的内部
,增加一个3D 预览
模块。将 不同的页面
封装成一个组件
,然后通过Vue-router
对路由进行集中管理,实现不同商品页面展示不同商品。借助 Pinia
保存横向导航栏(商品种类)的 id ,购物车数量角标,glb文件的路径。
四、项目主体结构
|-- client // 客户端目录结构
|-- public //商品3D模型
|-- draco
|-- model
|-- src
|-- api //自己封装的axios用于响应拦截
|-- assets //图片及基本css的初始化
|-- components //组件
|-- router //路由配置
|-- store //仓库
|-- views //页面
|-- server // 服务端目录结构
|-- config //mysql配置文件
|-- controllers //控制器
|-- data //商品数据
|-- routes //路由
五、前端实现
UI组件库: Vant
移动端适配: lib-flexible
CSS预处理器: less
滚动: BetterScroll
1. 组件
众所周知,组件可以省去很多代码的编写,这个项目中我将头部导航栏
,底部导航栏
,商品瀑布流布局
做成组件便于引用。这里我主要介绍下头部导航栏及商品瀑布流布局的实现。
(1) 头部导航栏(对不同类别的商品的展示)
实现过程:后端数据中每个类别的商品数据都包含id
这个字段,我将导航栏的每个类别的id
和后端给的id对应起来,并将这个id存储在pinia仓库
中,这样只要在页面用watch监听
仓库id的变化去向后端请求
相应类别的数据即可。
(2) 商品瀑布流布局(提供更好的用户体验)
实现过程:利用flex
布局,它可以实现两栏以上
的瀑布流布局,我这里是两栏瀑布流布局,故将父容器设置为弹性
容器,子容器为两个
弹性容器,将这两个子容器的排列方向设置为垂直排列
,并用flex:1;
两列平分区域占满整个视窗。
2. 仓库
仓库的出现让我们可以在不同的页面进行数据共享
,简直不要太爽,再也不用担心跨组件通信了!
这里简单介绍一下购物车角标的实现:
因为添加或删除商品,购物车角标将立即更新
,不管是在主页还是购物车页面还是商品详情页面,角标都得实时更新
它的数值,我们将变量值
、更新角标重新获取购物车数据的方法
定义在仓库中,这样在页面就可以直接引入并使用就好啦~
import {defineStore} from 'pinia'
import axios from 'axios'
const useCartStore=defineStore('cart',{
state:()=>{
return{
badge:0 //响应式数据badge
}
},
actions:{
async changeBadge(){
const res =await axios.post('/cartList', { //获取购物车数据
username: JSON.parse(sessionStorage.getItem('userInfo')).username
})
this.badge=res.data.length
}
}
})
export default useCartStore
3. 搜索模块
实现过程:利用localStorage
对搜索的词进行数据持久化,这样就能方便的从localStorage中拿到搜索的历史词段
,并将其传给后端使用mysql
检索相应的数据,并可以对其进行删除
(也就是清除历史记录)
🚩PS: 后面发现的一个小优化: 逛淘宝发现我啥都不输入点击搜索可以搜索默认的字段,那还不简单?这只需要发一次接口请求将默认字段传给后端即可啦~
4. 3D商品预览
使用 Three.js
将引入的商品模型放入页面中,项目中模型来源于此: sketchfab.com
由于模型的展示是通过点击商品图片
后,以 遮罩层
+ 动画
的形式呈现出来,不同商品展示不同模型,我们将其做成一个组件
便于引用。部分代码如下:
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让其可设置透明度
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)
// 渲染函数
const render = () => {
renderer.render(scene, camera)
controls.update()
requestAnimationFrame(render)
}
onMounted(() => {
//渲染
canvasDom.value.appendChild(renderer.domElement)
// 设置背景颜色并启用透明度
renderer.setClearColor(0x000000, 0.2);
render()
//网格地面
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让其点击不同商品展示不同模型 id对应商品的id
// console.log(gltf.scene);
const bmw = gltf.scene
bmw.scale.set(0.2, 0.2, 0.2); //模型缩放
scene.add(bmw) //将整个模型组添加到场景中
})
});
//洒满灯光
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. 商贸城3D预览
实现过程与3D商品预览类似,我们只需用这段代码将全景图
作为场景的背景图
即可(全景图的资源路径存在仓库中):
cubeTextureLoader.load(store.loadUrl, (texture) => {
const crt = new THREE.WebGLCubeRenderTarget(texture.image.height)
crt.fromEquirectangularTexture(renderer, texture) //把全景图转换为纹理格式
scene.background = crt.texture
})
六、后端实现
使用 Koa 框架搭建后端开发环境,后端分为四块:
配置文件:对mysql的配置 路由:定义接口请求路径及响应体 控制器:当接口被请求时,需要向前端响应的操作,即数据库的增删改查 数据:后端提供给前端的数据
数据库中创建了三个表:
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
//去读取数据库中的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: '账号或密码错误'
}
}
} catch (error) {
ctx.body = {
code: '80002',
data: error,
msg: '服务器异常'
}
}
})
//注册接口
router.post('/register', async (ctx, next) => {
const { username, password } = ctx.request.body
//判断账号或密码是否为空
if (!username || !password) {
ctx.body = {
code: '80001',
msg: '账号或密码不能为空'
}
return
}
//判断该账号是否在数据库中存在
try {
let findres = await userService.userfind(username)
if (findres.length) { //如找到数据则向前端报错
ctx.body = {
code: '80003',
data: 'error',
msg: '用户名已存在!'
}
} else { //如没找到则注册成功,往数据库添加这条数据
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: '服务器异常'
}
}
})
module.exports = router
七、总结
这个项目让我对Vue3这个框架的使用更熟练了,整个过程中也遇到了很多bug及问题,以前我是一个很怕代码出bug的小白,一遇到问题就问别人哈哈哈,这个项目让我学会了怎样一步一步寻找错误并分析原因,现在自己能够解决大多数的bug,也回顾了很多基础的js知识,体验了一把理论联系实践了。
项目地址:https://gitee.com/zt18507085081/Yiwu_Shopping