超详细的基于Vue的移动端商城(仿蘑菇街)案例制作过程,本案例基于B站coderwhy老师(点进去即是项目的视频)的学习,从13个大部分讲了整个案例的制作过程,项目的准备工作、首页、详情页、购物车以及bug的解决和项目的继续优化,最后还讲了项目的部署,项目源码GitHub地址,如果觉得文章太长的话,可以点这里,拆解成了多个部分,也有自己基于该老师所记的Vue学习笔记,里面有配套的思维导图地址
Vue移动端商城案例
git仓库初始化
这里,有两种方案去git自己的项目,一个是在创建好项目文件夹后,连接到事先创建好的git仓库并push,另外一种是在创建好git仓库后,在本地文件夹下clone并创建自己的项目
这里说一下第一种方法
-
首先就是新建一个仓库了
-
然后就是用脚手架创建vue项目,接着在根目录
git bash
-
执行下面代码
git remote add origin git@github.com:Dong-666/vue-te.git git branch -M main git push -u origin main
ok,项目以及仓库都初始化完成
划分目录结构
src下的目录结构
- network (网络模块)→相关网络请求(axios)
- components (组件)
- common (公共模块)→可以多项目复用
- content (业务模块)→该项目可以复用
- views (路由页面模块)
- common (公共js文件–常量…)
- assets (静态资源)
- router (路由)→页面跳转
- store (vuex状态管理)
设置CSS初始化和全局样式
适应不同浏览器端对页面的适配以及设置好项目的主题颜色
css样式初始化:normalize.css
主题颜色以及css常量设置:base.css
路径别名以及代码风格设置
在项目根目录下创建一个vue.config.js文件,并填入相关配置
module.exports = {
configureWebpack: {
resolve: {
alias: {
'assets': '@/assets',
'common': '@/common',
'components': '@/components',
'network': '@/network',
'views': '@/views'
}
}
}
}
从项目代码规范方引入.editorconfig
文件,代码不摆了,每个项目有各自的风格
举例
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
tabbar引入以及模块划分(项目结构初始化)
将之前创建好的tabbar模块引入,方法同之前tabbar项目,并将首页等各view文件夹和页面html创建出来,路由表的创建等,注意路径
更换图标
在pubic文件夹下的index.html文件中有下列代码
其中,它利用了正则表达式去获取图标位置(知识待补充)
我们只需将同级目录下的favicon.ico文件替换成自己的图标即可
首页开发
首页导航栏的封装(navbar)
因为导航栏的通用性,在其它项目中可能可以用到,所以我们把它封装到components中的common里面去,命名为NavBar.vue
因为导航栏可能因为内容的改变而会有不同位置出现不同的内容,所以我们为其定义三个插槽,分别为左中右,同时定义样式
<template>
<div class="nav-bar">
<div class="left">
<slot name="left"></slot>
</div>
<div class="center">
<slot name="center"></slot>
</div>
<div class="right">
<slot name="right"></slot>
</div>
</div>
</template>
<script>
export default {
name: "NavBar"
}
</script>
<style scoped>
.nav-bar {
height: 44px;
display: flex;
text-align: center;
line-height: 44px;
box-shadow: 0 1px 1px rgba(100,100,100,.1)
}
.center {
flex: 1;
}
.right, .left {
width: 60px;
}
</style>
接着,当然就是在首页引用该组件啦,在首页导入该组件,同时在插槽处添加所需加的内容,然后就是样式的相关修改了
<template>
<div id="home">
<nav-bar class="home-nav">
<div slot="center">购物街</div>
</nav-bar>>
</div>
</template>
<script>
import NavBar from 'components/common/navbar/NavBar.vue'
export default {
name:'Home',
components: {
NavBar
},
data () {
return{
}
},
created() {
},
computed: {
}
}
</script>
<style scoped>
#home {
padding-top: 44px;
position: relative;
}
.home-nav {
position: fixed;
top: 0;
left: 0;
right: 0;
color: white;
z-index: 10;
background-color: var(--color-tint);
}
</style>
请求首页的多个数据
请求,就意味着我们需要书写网络请求相关代码了,安装axios插件
npm i axios --save
来到network下新建home.js文件,作为统一的网络请求
引入相关模块,并导出网络请求,使用函数getHomeMultidata封装该请求
import {
request} from './request.js'
export function getHomeMultidata() {
return request({
url: '/home/multidata'
})
}
}
接着,在Home组件中,在其被创建(create)时加入该网络请求
import {
getHomeMultidata, getHomeGoods} from 'network/home.js'
export default {
name:'Home',
created() {
getHomeMultidata().then(res => {
this.banners = res.data.banner.list
this.recommends = res.data.recommend.list
})
}
}
定义一个data数据用来存储请求返回得到的数据(熟悉垃圾回收)
data () {
return{
banners: [],
recommends: []
}
},
轮播图(swiper)
轮播图的制作后面要补上(vue实现)
这里根据设计图,将首页再次划分成几大模块
好吧,图有点丑,但不妨碍理解,这里,我们最终要呈现的是swiper这个大的组件,所以,为了让首页的逻辑更加清晰点,我们只定义上述图中的组件,而每个组件的内部实现,则由各自的组件去实现
所以,我们在home文件夹下新建childComps文件夹,新建HomeSwiper.vue,用来封装swiper相关组件
在HomeSwiper里,引入两个swiper相关模块并设置样式,好的,到这里,你就会发现父组件的数据咋办,是的,用props
HomeSwiper.vue
props: {
banners: {
type: Array,
default() {
return []
}
}
}
Home.vue
<home-swiper :banners='banners'></home-swiper>
ok,放出HomeSwiper全部代码
<template>
<swiper>
<swiper-item v-for="(item,index) in banners">
<a :href="item.link">
<img :src="item.image" alt="">
</a>
</swiper-item>
</swiper>
</template>
<script>
import { Swiper, SwiperItem } from 'components/common/swiper/index.js'
export default {
name: 'HomeSwiper',
props: {
banners: {
type: Array,
default() {
return []
}
}
},
components: {
Swiper,
SwiperItem
}
}
</script>
<style scoped>
</style>
推荐信息的展示
这里没啥可说的,主要是用到v-for去展示推荐信息
放上HomeRecommend代码
<template>
<div class="recommend-view">
<div v-for="item in recommends" class="recommend-item">
<a :href="item.link">
<img :src="item.image" alt="">
<div>{
{item.title}}</div>
</a>
</div>
</div>
</template>
<script>
export default {
name: 'HomeRecommendView',
props: {
recommends: {
type: Array,
default() {
return []
}
}
},
components: {
}
}
</script>
<style scoped>
.recommend-view {
display: flex;
width: 100%;
/* 水平居中 */
text-align: center;
font-size: 12px;
padding: 10px 0 20px;
border-bottom: 10px solid #eee;
}
.recommend-item {
flex: 1;
}
.recommend-item img {
width: 70px;
height: 70px;
margin-bottom: 10px;
}
</style>
FeatureView的实现
这个更简单,案例直接写死了,直接放代码
<template>
<div class="feature">
<a href="https://act.mogujie.com/zzlx67">
<img src="~assets/img/home/recommend_bg.jpg" alt="">
</a>
</div>
</template>
<script>
export default {
name: 'FeatureView',
props: {
},
components: {
}
}
</script>
<style scoped>
.feature img{
width: 100%;
}
</style>
TabControl
和推荐信息一个道理,没啥好说,最多就是样式上的修改
<template>
<div class="tab-control">
<div v-for="(item,index) in titles"
class="tab-control-item"
:class = "{active: index === currentIndex}">
<span>{
{item}}</span>
</div>
</div>
</template>
<script>
export default {
name:'TabControl',
components: {
},
data () {
return {
currentIndex: 0
}
},
props:{
titles: {
type: Array,
default() {
return []
}
}
},
methods: {
itemchange(index) {
this.currentIndex = index
}
}
}
</script>
<style scoped>
.tab-control {
display: flex;
height: 40px;
line-height: 40px;
text-align: center;
font-size: 15px;
background-color: #fff;
}
.tab-control-item {
flex: 1;
}
.active {
color: var(--color-high-text);
}
.active span {
border-bottom: 3px solid var(--color-tint);
}
</style>
在home.vue,为了实现tabControl吸顶效果,这里要使用到css一个属性position: sticky
.tab-control {
position: sticky;
top: 43px;
z-index: 10;
}
设计商品的数据结构
在这里,首页数据是根据TabControl切换而得到的,而每次上拉加载都刷新一定数量的数据,所以根据三个切换按钮在里面又定义了三个对象,而每次上拉加载都定义为加一页(默认第一页),所以定义page用来存储当前页数信息,用list来存储到当前页加载到的商品数量
goods: {
'pop': {
page:0, list:[]},
'new': {
page:0, list:[]},
'sell': {
page:0, list:[]},
}
首页数据的请求和封装
ntework里的home.js同第一个网络请求的函数一样,再次定义另外一个,获得商品数据
export function getHomeGoods(type,page) {
return request({
//接口已修改
url: '/api/h8/home/data',
params: {
type,
page
}
})
}
在home.vue调用该函数,并将其封装成方法,因为我们在网络请求后会做进一步的处理,所以我们用同名的函数将其封装起来,并在create()中调用这些网络请求以及具体的实现方法
created() {
this.getHomeMultidata()
this.getHomeGoods('pop')
this.getHomeGoods('new')
this.getHomeGoods('sell')
},
methods: {
//网络分析相关方法
getHomeMultidata() {
getHomeMultidata().then(res => {
this.banners = res.data.banner.list
this.recommends = res.data.recommend.list
})
},
getHomeGoods(type) {
const page = this.goods[type].page + 1
getHomeGoods(type, page).then(res => {
this.goods[type].list.push(...res.data.list)
this.goods[type].page ++
})
}
}
商品展示
因为涉及到该项目的业务功能(好几个页面会用到该组件),所以在components中的content中新建goods文件夹,新建两个vue文件GoodsList以及GoodsListItem,一个用来描述单个商品的详细样式,另外一个用来展示整体商品的样式
GoodsList.vue
<template>
<div class="goods-list">
<goods-list-item v-for="item in goods" :goods-item="item"></goods-list-item>
</div>
</template>
<script>
import GoodsListItem from './GoodsListItem.vue'
export default {
name: 'GoodsList',
props: {
goods: {
type: Array,
default() {
return []
}
}
},
components: {
GoodsListItem
}
}
</script>
<style scoped>
.goods-list {
display: flex;
padding: 2px;
/* 包裹 */
flex-wrap: wrap;
justify-content: space-around;
}
</style>
GoodsListItem.vue
<template>
<div class="goods-list-item">
<img :src="goodsItem.show.img" alt="">
<div class="goods-info">
<p>{
{goodsItem.title}}</p>
<span class="price">{
{goodsItem.price}}</span>
<span class="collect">{
{goodsItem.cfav}}</span>
</div>
</div>
</template>
<script>
export default {
name: 'GoodsListItem',
props: {
goodsItem: {
type: Object,
default() {
return {}
}
}
},
components: {
}
}
</script>
<style scoped>
.goods-list-item {
position: relative;
padding-bottom: 40px;
width: 48%;
}
.goods-list-item img {
width: 100%;
border-radius: 5px;
}
.goods-info {
position: absolute;
left: 0;
right: 0;
bottom: 5px;
overflow: hidden;
text-align: center;
font-size: 12px;
}
.goods-info p {
overflow: hidden;
/* 显示省略符号来代表被修剪的文本 */
text-overflow: ellipsis;
/* 文本不换行 */
white-space: nowrap;
margin-bottom: 3px;
}
.goods-info .price {
color: var(--color-high-text);
margin-right: 20px;
}
.goods-info .collect {
position: relative;
}
.goods-info .collect::before {
content: '';
position: absolute;
left: -15px;
top: -1px;
width: 14px;
height: 14px;
background: url("~assets/img/common/collect.svg") 0 0/14px 14px;
}
</style>
这里主要有几个css的新知识点(或者遗忘点)
/* 显示省略符号来代表被修剪的文本 */
text-overflow: ellipsis;
/* 文本不换行 */
white-space: nowrap;
/* 包裹 在子组件定义宽度后,可以根据宽度自动排列布局*/
flex-wrap: wrap;
/* 可以使子组件的间距均匀 */
justify-content: space-around;
tabcontrol切换商品类型
回到tabcontrol,我们需要通过点击事件来切换不同的商品展示,所以这里就要动态绑定点击事件,同时,为了触发父组件的goods信息改变,我们需要将该事件点击所对应的tab的索引传给父组件
动态绑定事件
<div v-for="(item,index) in titles"
class="tab-control-item"
:class = "{active: index === currentIndex}"
@click="itemchange(index)">
传出事件以及当前点击的索引
methods: {
itemchange(index) {
this.currentIndex = index
this.$emit('tabClick', index)
}
}
父组件接收点击事件
<tab-control :titles="['流行', '新款', '精选']" class="tab" @tabClick="tabClick"></tab-control>
用一个变量获得当前请求的数据索引(默认首页)
data () {
currentType: 'pop'
}
处理该事件
methods: {
//首页数据处理
tabClick(index) {
switch (index) {
case 0:
this.currentType = 'pop'
break
case 1:
this.currentType = 'new'
break
case 2:
this.currentType = 'sell'
break
}
}
}
再在computed中,动态改变当前请求的数据索引
computed: {
showGoods() {
return this.goods[this.currentType].list
}
}
在标签,动态绑定数据
<goods-list :goods="showGoods"></goods-list>
这样就OK啦
Better-scroll使用
BetterScroll 是一款重点解决移动端(已支持 PC)各种滚动场景需求的插件。它的核心是借鉴的 iscroll 的实现,它的 API 设计基本兼容 iscroll,在 iscroll 的基础上又扩展了一些 feature 以及做了一些性能优化。
BetterScroll 是使用纯 JavaScript 实现的,这意味着它是无依赖的。
因为网页在PC端上使用主要是用鼠标滚轮进行的,不会很卡,而如果你换到手机端,进行页面滚动的话就会发现明显的卡顿,这个时候就需要引入插件(或者自己写一个)。这里使用的是better-scroll,下面分两部分介绍它的基本使用(html页面和vue项目)
html使用
npm下载
npm i better-scroll --save
或者GitHub下载
然后找到dist文件夹,将该文件拖出来,你也可以直接js引用到该文件
<script src="./better-scroll.min.js"></script>
使用该插件之前需要了解一下知识
你需要将加入到滚动的标签统一放在一个div(其它单独的双标签元素也可)下,之后在此基础下再加上一个div标签进行包裹,如下图所示
页面结构代码演示如下(你可以加上更多’汉堡包’,拖动体验感更佳),使用wrapper包裹content,再加上你需要滚动的元素
<div class="wrapper">
<ul class="content">
<li>🍔</li>
<li>🍔</li>
<li>🍔</li>
<li>🍔</li>
<li>🍔</li>
<li>🍔</li>
<li>🍔</li>
<li>🍔</li>
<li>🍔</li>
<li>🍔</li>
<li>🍔</li>
<li>🍔</li>
<li>🍔</li>
<li>🍔</li>
</ul>
</div>
接下来就是具体的js逻辑实现了
首先我们要new一个better-scroll,我把它称为better-scroll初始化
这里,需要添加配置项
probeType
- 类型:
number
- 默认值:
0
- 可选值:
1|2|3
- 作用:有时候我们需要知道滚动的位置。当 probeType 为 1 的时候,会非实时(屏幕滑动超过一定时间后)派发scroll 事件;当 probeType 为 2 的时候,会在屏幕滑动的过程中实时的派发 scroll 事件;当 probeType 为 3 的时候,不仅在屏幕滑动的过程中,而且在 momentum 滚动动画运行过程中实时派发 scroll 事件。如果没有设置该值,其默认值为 0,即不派发 scroll 事件
click
- 类型:
boolean
- 默认值:
false
- 作用:BetterScroll 默认会阻止浏览器的原生 click 事件。当设置为 true,BetterScroll 会派发一个 click 事件,我们会给派发的 event 参数加一个私有属性
_constructed
,值为 true。
pullUpLoad
- 类型:
boolean
- **默认值:**false
- **作用:**动态监测你是否滚动到最底部(在最新版,你已经看不到该配置项了,需要通过引入插件去使用,当然你也可以通过下载完整版去使用该插件)
click
这里要补充一下,它对于本来就该具有的点击事件的元素是不会阻止的(button
),它会阻止div
、img
等标签的点击事件,其它配置项可以去官网查看并试试
let bs = BetterScroll.createBScroll(document.querySelector('.wrapper'), {
probeType: 1,
click: true,
pullUpLoad: true
})
然后你就可以试试滚动啦,你可以通过下列代码动态监测当前滚动的位置(position为当前所在坐标
),然后做出对应处理(前提是必须probeType
为2或3)
bs.on("scroll", (position) => {
console.log(position)
})
pullingUp
可以检测你是否到底了(前提pullUpLoad: true
),但只能检测一次,下次滚动到最低就没有了,你可以通过better-scroll
的finishPullUp()
方法多次检测
bs.on('pullingUp', () => {
console.log('你已经拉到底了')
bs.finishPullUp()
})
vue使用
这个用npm下载完使用会方便点(使用CLI4创建vue项目)
页面结构要求同html的使用方式
引入
//添加scroll插件
import BScroll from 'better-scroll'
在vue的生命周期函数mounted中去使用该插件,不能在created中使用,为什么呢,因为它刚初始化完,那些元素标签还没加载,直接用就会出现undefined或者null,同时,因为mounted的函数执行完就会boom的没了(函数栈还是内存的栈和堆的关系),所以你需要定义一个属性去接收你new出来的betterScroll对象
export default {
name:'Category',
data() {
return {
bs: null
}
},
mounted() {
this.bs = new BScroll(document.querySelector('.wrapper'), {
probeType: 3,
pullUpLoad: true //该属性添加后,probeType的值直接为3(修改成别的也没用)
})
this.bs.on('scroll', (position) => {
console.log(position)
})
this.bs.on('pullingUp', ()=> {
console.log('达到最低了')
this.bs.finishPullUp()
})
}
}
好好享用吧
封装better-scroll
B站老师有着疯狂的封装想法,哈哈哈开玩笑,其实是为了项目的后期更新和维护,我们使用插件前一般都是将其封装完后再去各个组件中使用,避免后面插件不维护更换插件引起的代码修改困难(可能要面临重构,很苦的)
根据上面所学的better-scroll,我们为其定义所需的基本结构,后面在使用的时候就直接往插槽添加标签即可
<template>
<div class="wrapper" ref="wrapper">
<div class="content">
<slot></slot>