根据b站上coderwhy老师的视频做的Vue项目,在老师视频的基础上完善了项目.
b站视频地址:https://www.bilibili.com/video/BV17j411f74d
视频是2018年年末的,虽然有点老,但是作为入门是绝对够了的,讲的很好也很细.
完成后的项目我部署在了服务器,戳链接:http://39.105.119.76:8082/home
完整的项目地址 : https://github.com/gh2WHY/SuperMall
如果觉得有用,麻烦点个star,拜托了(╥╯^╰╥)。
项目的基本展示:
首页:
详情页:
分类页:
购物车页面:
个人中心页面:
做这个项目的过程中学习到了很多,对整个vue的知识都进行了一个相对比较完善的复习.
normalize.css
base.css
vue.config.js->为路径配置别名
.editorconfig->编码格式标准
TabBar
为我们的项目提供了一个基础的框架,封装完之后我们的项目就有了一个基础的框架,剩下的我们就可以在这个框架的基础上进行各个页面的开发.
关于TabBar
的封装,我在之前的一篇文章中已经介绍过,戳链接即可:
https://blog.csdn.net/lhrdlp/article/details/107850371
import axios from 'axios'
export function request(config) {
// 1.创建axios的实例
const instance = axios.create({
baseURL: '最新数据接口,添加老师微信coderwhy003获取',
timeout: 5000
})
// 2.axios的拦截器
// 2.1.请求拦截的作用
instance.interceptors.request.use(config => {
return config
}, err => {
// console.log(err);
})
// 2.2.响应拦截
instance.interceptors.response.use(res => {
return res.data
}, err => {
console.log(err);
})
// 3.发送真正的网络请求
return instance(config)
}
首页中有一个导航栏,观察首页的导航栏和详情页的导航栏,可以发现,我们的导航栏中不仅仅是中间部分有内容.
为了能在各个页面中使用这个组件,我们这个时候就需要用到插槽了,设置左-中-右三个插槽,在具体的页面可以根据不同的需求填充不同的内容.
核心的代码如下:(详细代码可在github下载源码查看)
<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>
在home.js
中先请求一下首页相关的轮播图数据和推荐数据
export function getHomeMultidata() {
return request({
url: '/home/multidata'
})
}
在home.vue
中获取数据,我们在created()
中获取数据,为了我们的代码更加规范,在 methods
中定义getHomeMultidata
方法,在created
中调用这个方法即可.
定义数据
//存储轮播图数据和推荐部分数据
banners: [],
recommends: [],
methods: {
getHomeMultidata() {
getHomeMultidata().then((res) => {
this.banners = res.data.banner.list;
this.recommends = res.data.recommend.list;
});
},
}
created() {
getHomeMultidata();
}
有关于轮播图的组件已经封装完成,在commpents - common - swiper
中已经封装完成.
在views - childcomps
文件中新建一个swiper.vue
,来封装我们的轮播图.咋首页请求的数据通过props
在子组件中接收.
我们需要将这个组件引入到swiper.vue
中,核心代码如下:
<template>
<swiper>
//banners是从父组件中传过来的轮播图相关的数据
<swiper-item v-for="(item,index) in banners" :key = 'index' >
<a :href="item.link">
<img :src="item.image" @load = 'swiperLoad'>
</a>
</swiper-item>
</swiper>
</template>
推荐部分有两个内容,一个是我们本地的图片,直接做个展示即可,还有一部分是我们从服务器请求过来的数据,我们需要封装对应的组件来对这部分数据进行一个相应的展示.
在views-home - childcomps
中新建HomeRecommend
组件来展示我们从组件请求过来的数据.
核心代码如下:
<template>
<div class="recommend">
<div v-for = '(item,index) in recommends' :key = 'index' class="recommend-item">
<a :href="item.link">
<img :src="item.image" alt="">
<div>{
{
item.title}}</div>
</a>
</div>
</div>
</template>
//具体的样式代码见源码
观察我们项目中的TabControl
,会发现我们不仅仅在首页中使用到了TabControl
,在分类页面中也有用到,所以我们在commponents - content - tabbar
中封装这个组件
核心代码如下:
<template>
//数据从父组件通过数组的形式中传过来
<div class="tab-control" >
<div class="tab-control-item"
v-for="(item,index) in titles" :key = 'index'
:class ="{active : index === currentIndex}"
@click = 'tabClick(index)'
><span>{
{
item}}</span>
</div>
</div>
</template>
<script>
export default {
name : 'TabControl',
props : {
titles : {
type : Array,
default() {
return [];
}
}
},
data() {
return {
currentIndex : 0,
}
},
methods : {
//点击哪个那个的颜色就发生变化
tabClick(index) {
this.currentIndex = index;
//发射的这个事件在后面会介绍到
this.$emit('tabClick',index);
},
}
}
</script>
<style>
.tab-control {
display: flex;
height: 40px;
background-color: #fff;
font-size: 15px;
line-height: 40px;
text-align : center;
}
.tab-control-item {
flex : 1;
}
.active {
color: var(--color-high-text);
}
.active > span {
padding : 5px;
border-bottom :3px solid var(--color-high-text);
}
</style>
在父组件中的使用如下:
<tab-control
class="tab-control"
:titles=" ['流行','新款','精选']"
@tabClick="tabClick"
ref="tabControl2"
></tab-control>
goods: {
pop: page1:/list[30]
new: page1/list[30]
sell: page1/list[30]
}
在network
下的home.js
中请求首页的商品数据,如下:
export function getHomeGoods(type,page) {
return request({
url: '/home/data',
//配置对应的参数,有三种类型pop.new sell,而且数据不止一页
params : {
type,
page,
}
})
}
在hone.vue
中获取响应的数据,和之前获取数据的方法一样,在methods
中封装方法,在created
中执行.
//data中初始化存储数据的变量
data() {
return {
goods: {
pop: {
page: 0, list: [] },
new: {
page: 0, list: [] },
sell: {
page: 0, list: [] },
},
}
}
getHomeGoods(type) {
//而且每次重新调用的时候我们的页面都需要+1,所以在data中我们初始化的page为0
let page = this.goods[type].page + 1;
getHomeGoods(type, page).then((res) => {
this.goods[type].list.push(...res.data.list);
this.goods[type].page += 1;
// 完成上拉加载更多
this.$refs.scroll.finishPullUp();
});
},
created() {
//获取详情数据
this.getHomeGoods("pop");
this.getHomeGoods("new");
this.getHomeGoods("sell");
},
在前面我们已经获取到了首页相关的商品数据,接下来我们要做的就是朵这些数据作业个展示.但是上面的数据有三种类型,分别是new
,pop
和sell
,先不管其他的功能,设计将 pop
相关的数据展示在页面中.
在这里我们封封装两个组件goods
和goodsItem
组件,路径如下图所示
goodslist
组件的核心代码如下:
//goods是从父组件传过来的值,然后把这里的每一项的值再传给goods的子组件,goodsItem
<template>
<div class="good-list">
<goods-list-item v-for = '(item,index) in goods' :key = 'index' :goods-item = 'item' ></goods-list-item>
</div>
</template>
goodsListItem
组件的核心代码如下
<template>
<div class="goods-list-item">
<img :src="showImg" @load = 'imageOnload' />
<div class="goods-info">
<p>{
{
goodsItem.title}}</p>
<span class="price">{
{
goodsItem.price}}</span>
<span class="collect">{
{
goodsItem.cfav}}</span>
</div>
</div>
</template>
//具体样式见源码
封装好这两个组件之后我们要做的就是在首页中展示这些数据:
<goods-list :goods="goods"></goods-list>
其实这个功能我自己觉得还是蛮难的,我们请求到的数据有三种类型,点击不同的按钮就要展示不同的内容,这个肯定能想到的就是tabcontrol
的点击得和我们下面的三种类型有某种绑定.
所以我们现在data中初始化一个变量:
//默认情况下为poo,即流行
currentType : 'pop'
既然我们要点击,那么肯定就要监听tabControl
的点击事件,通过点击,改变currentType
的值,从而给goods
组件传对应类型的数据.
之前在封装tabcontrol`的时候,就有提到他发射出去了一个事件,我们在父组件中接收这个事件
//tabcontrol中的代码,发射事件
methods : {
tabClick(index) {
this.currentIndex = index;
this.$emit('tabClick',index);
},
}
//在父组件中使用组件时接受这个事件
<tab-control
class="tab-control"
:titles=" ['流行','新款','精选']"
@tabClick="tabClick"
></tab-control>
已经接受到事件之后,我们考虑如何将点击和类型之间进行绑定??
在发射事件的时候我们已经传了一个参数,那就是它们的下标index
,那么就可以通过下标来进行绑定,当下标为0
的时候currentType
为 pop
,1
的时候为new
,以此类推,代码如下:
tabClick(index) {
switch (index) {
case 0:
this.currentType = "pop";
break;
case 1:
this.currentType = "new";
break;
case 2:
this.currentType = "sell";
}
},
然后在给goodslist
传递数据的时候就可以点击哪个就传递哪个类型的数据.
<goods-list :goods="goods[this.currentType].list"></goods-list>
但是呢,我们一般都不希望有太长的表达式,所以一般这种都是通过计算属性,如下:
<goods-list :goods="showGoods"></goods-list>
computed: {
showGoods() {
return this.goods[this.currentType].list;
},
},
封装scroll.vue
关于betterscroll的更多内容可以在github
上搜索查看,
https://github.com/ustbhuangyi/better-scroll/blob/master/README_zh-CN.md
更多关于betterscroll的内容,[戳链接]
npm install better-scroll --save
components - commom - Bscroll - Bscroll.vue
<template>
<div class="wrapper" ref="wrapper">
<div class="content">
<slot></slot>
</div>
</div>
</template>
<script>
import BScroll from "better-scroll";
export default {
name: "Scroll",
props: {
//监听滚动位置
//0,1都是不侦测实时位置
//2:只要在滚动过程中侦测,手指离开后的惯性滚动中不侦测
//3: 只要是滚都,都侦测
probeType: {
type: Number,
default: 0,
},
//这个配置用于做下拉刷新功能,默认为 false。当设置为 true 或者是一个 Object 的时候,可以开启下拉刷新,
pullUpLoad: {
type: Boolean,
default: false,
},
data: {
type: Array,
default: () => {
return [];
},
},
},
data() {
return {
scroll: null,
message: "哈哈哈",
};
},
mounted() {
// 1.创建BScroll对象
this.scroll = new BScroll(this.$refs.wrapper, {
click: true,
probeType: this.probeType,
pullUpLoad: this.pullUpLoad,
});
// 2.监听滚动的位置
this.scroll.on("scroll", (position) => {
this.$emit("scroll", position);
});
// 3.监听上拉事件
this.scroll.on("pullingUp", () => {
this.$emit("pullingUp");
});
console.log(this.scroll);
},
//这些方法在后面会介绍到他们具体的用途
methods: {
scrollTo(x, y, time = 300) {
this.scroll.scrollTo(x, y, time);
},
finishPullUp() {
this.scroll.finishPullUp();
},
refresh() {
this.scroll && this.scroll.refresh();
},
},
};
</script>
<style scoped>
</style>
在home.vue中使用better-scroll
//使用scroll将虽有要滚动的内容包裹起来.
<scroll
class="content"
:probo-type="3"
:pull-up-load="true"
@scroll="contentScroll"
@pullingUp="loadMore"
ref="scroll"
>
<home-swiper :banners="banners" @swiperImageLoad="swiperImageLoad" />
<home-recommend :recommends="recommends"></home-recommend>
<feature-view></feature-view>
<tab-control
class="tab-control"
:titles=" ['流行','新款','精选']"
@tabClick="tabClick"
ref="tabControl2"
></tab-control>
<goods-list :goods="showGoods"></goods-list>
</scroll>
通过阅读better-scroll
的挂房文档,我们了解到,better-scroll
的父元素必须有一个确定的高度,所以我们需要给content设置一个固定的高度.
也就是页面的这部分的内容需要是一个固定的高度,我们可以通过定位实现,具体的css
代码如下:
.content {
position: absolute;
top: 44px;
bottom: 49px;
left: 0;
right: 0;
overflow: hidden;
}
上图展示的红色的向上的箭头就是我们需要封装的backtop
组件,对象的图片在我们的img
文件夹中有.
在component-backTop
中封装BackTop
组件,详细如下:
<template>
<div class="back-top">
<img src="../../../assets/img/common/top.png" alt="">
</div>
</template>
<script>
export default {
name : 'BackTop'
}
</script>
<style scoped>
.back-top {
position: fixed;
right : 6px;
bottom : 50px;
}
.back-top img {
width: 43px;
height: 43px;
}
</style>
在父组件中直接使用即可.
直接给组件添加事件是不可行的,我们必须要加.native
修饰符,点击组件后需要返回到页面顶部,这个功能该怎么实现呢???
我们之前使用了better-scroll
对整个页面进行了重构,在better-scroll
中有scrollTo
方法,基本使用如下:
scrollTo(x, y, time, easing)
参数:
{Number} x 横轴坐标(单位 px)
{Number} y 纵轴坐标(单位 px)
{Number} time 滚动动画执行的时长(单位 ms)
{Object} easing 缓动函数,一般不建议修改,如果想修改,参考源码中的 ease.js 里的写法
返回值:无
在之前我们封装swiper.vue
的时候已经封装好了这个方法,在home.vue
中我们只需要直接调用这个方法即可,我们要返回的是页面顶部,所以x,y的值都为0.
<back-top @click.native="backTop"></back-top>
//methods
backTop() {
console.log(this.$refs.scroll);
this.$refs.scroll.scrollTo(0, 0, 500);
},
当页面滚动的库里大于1000px,组件显示,否则隐藏.
在better-scroll
中有这个方法可以监听滚动的位置,所以我们在父组件中就可以监听到滚动的距离,然后控制backtop
的显示与隐藏.
//监听滚动的位置
this.scroll.on("scroll", (position) => {
this.$emit("scroll", position);
});
在home.vue
中的相关代码如下:
<scroll
class="content"
:probo-type="3"
:pull-up-load="true"
@scroll="contentScroll"
@pullingUp="loadMore"
ref="scroll"
></scroll>
<back-top @click.native="backTop" v-show="isShowBackTop"></back-top>
data() {
return {
isShowBackTop: false,
}
}
methods : {
contentScroll(position) {
this.isShowBackTop = -position.y > 1000;
},
}
在昨晚上面的一系列工作后我们在首页中就可以使用better-scroll
对页面进行滚动,但是我们会发现一个bug
,当我们把页面滚动到一定的距离之后,再次滚动就不能进行滚动了,到底为什么会产生这个问题呢???
Better-Scroll在决定有多少区域可以滚动时, 是根据scrollerHeight属性决定
在bscroll.vue
打印下面的语句就可以找到scrollerHeight
console.log(this.scroll);
出现了bug
我们就要解决bug
,那么我们应该如何解决呢??
如何解决这个问题了?
所以分析之后我们现在要做的事情是监听GoodSListItem
中图片加载完毕的事件,当图片加载完成之后调用scroll.vue
中的refresh()
方法.画下面的一张图来表示他们之间的关系
我们需要在home.vue
中使用上面的两个方法,bscroll.vue
中的refresh
很好获得,通过在父组件中使用$refs.scroll.refresh
就可以拿到,但是监听图片加载完成的组件和home.vue
中不是父子组件的关系,那么如何才能在home.vue
中获取图片加载完成呢???
如何将GoodsListItem.vue中的事件传入到Home.vue中
具体的代码如下:
在mian.js
中定义
Vue.prototype.bus = new Vue();
在goodsListItem.vue
中将图片的加载事件发布到事件总线
//监听图片的加载
<img :src="showImg" @load = 'imageOnload' />
//正常发射事件
methods : {
imageOnload() {
this.bus.$emit('itemImageOnload')
},
}
在home.vue
设置当图片加载完成之后调用refresh
函数
created() {
this.bus.$on('itemImageOnload',this.$refs.scroll.refresh())
}
报错问题一: refresh找不到的问题
acroll.vue
中的代码 refresh() {
this.scroll && this.scroll.refresh();
},
home.vue
中的代码
mounted() {
this.bus.$on('itemImageOnload',this.$refs.scroll.refresh())
}
解决问题二: 对于refresh非常频繁的问题, 进行防抖操作
我们监听的是图片加载完成后就调用refresh
函数进行刷新,单数图片有很多张,加载一下就要进行一下刷新,所所以这个刷新是非常频繁的,我们需要对刷新进行防抖操作.
防抖函数起作用的过程:
//我把它封装在common-utlis.js中
debounce(func, delay) {
let timer = null
return function (...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, delay)
}
},
在home.vue
中对刷新函数进行防抖处理,之前mounte总的代码修改为如下代码:
mounted() {
const refresh = debounce(this.$refs.scroll.refresh, 500);
this.itemImgListener = ()=>{
refresh();
}
this.bus.$on("itemImageOnload",this.itemImgListener);
},
之前我们在获取首页商品数据的时候就已经有提到页码的问题,我们是已经获取到了后面页码的数据,但是此时此刻页面上还没有后面页的数据,我们要在页面拉到最底部的时候实现一个加载更多的功能(在很多app和网页上都有这个功能).
我们使用的是better-scroll
对页面进行的重构,那么在它的原生方法中就有一个,更多内容戳链接
在scroll.vue
中的设置如下:
mounted() {
// 3.监听上拉事件
this.scroll.on("pullingUp", () => {
this.$emit("pullingUp");
});
console.log(this.scroll);
},
methods: {
finishPullUp() {
this.scroll.finishPullUp();
},
},
};
在home.vue
中监听上拉加载更多
<scroll
class="content"
:probo-type="3"
:pull-up-load="true"
@scroll="contentScroll"
@pullingUp="loadMore"
ref="scroll"
></scroll>
在请求数据的时候调用加载更多的功能
getHomeGoods(type) {
let page = this.goods[type].page + 1;
getHomeGoods(type, page).then((res) => {
this.goods[type].list.push(...res.data.list);
this.goods[type].page += 1;
// 完成上拉加载更多
this.$refs.scroll.finishPullUp();
});
},
在loadMore
中完成加载更多
loadMore() {
this.getHomeGoods(this.currentType);
},
先来看一下我们要实现的效果:
因为TabControl
上面总共有三部分的内容,除了轮播图剩下的两张图片很小,当轮播图加载完毕的时候其他的应该也已经加载完成了,所以我们只需要监听轮播图的架子啊完成,然后加载完成之后获取tabcontrol
的高度.
监听HomeSwiper
中img
的加载完成.
<swiper>
<swiper-item v-for="(item,index) in banners" :key = 'index' >
<a :href="item.link">
<img :src="item.image" @load = 'swiperLoad'>
</a>
</swiper-item>
</swiper>
//可以使用isLoad的变量进行状态的记录.
data() {
return {
isLoad : false,
}
}
//发出事件
methods : {
swiperLoad() {
if(!this.isLoad) {
this.$emit('swiperImageLoad');
this.isLoad = true;
}
}
}
在`home.vue中接受事件
<home-swiper :banners="banners" @swiperImageLoad="swiperImageLoad" />
//加载完成后获取高度
swiperImageLoad() {
this.tabOffsetTop = this.$refs.tabControl2.$el.offsetTop;
},
问题:动态的改变tabControl的样式时, 会出现两个问题:
其他方案来解决停留问题.
如下:
//navbar的下面复制一份tabcontrol
<nav-bar class="home-nav">
<slot slot="center">购物街</slot>
</nav-bar>
<tab-control
class="tab-control"
:titles=" ['流行','新款','精选']"
@tabClick="tabClick"
ref="tabControl1"
v-show="isTabFixed"
></tab-control>
现在我们要控制的就是这个控件的显示与隐藏,之前的那个会随着内容的滚动一直滚上去,从而在视觉上给用户一种下面的tabcontrol
固定住了的效果.
通过变脸过来控制他显示与否
//默认情况下为false,不显示
isTabFixed : false
那么什么时候应该显示呢???之前已经获取到了offsetTop
,当滚动的距离大于等于之前获取的值时就应该显示.
如下:
//contentscroll函数在掐已经介绍过了
contentScroll(position) {
this.isTabFixed = -position.y >= this.tabOffsetTop;
},
当我们滑动一会首页的时候肯会切换到其他页面,我们希望再从其他页面切换回来的时候首页已经保持在我们离开时的那个位置.
设置home组件不随意销毁keep-alive
在app.vue中设置
<keep-alive >
<router-view></router-view>
</keep-alive>
//活跃的时候返回该位置
activated() {
//一进入组件就滚动到离开时保存的位置
this.$refs.scroll && this.$refs.scroll.scrollTo(0, this.saveY, 0);
//refresh():重新计算 better-scroll,
this.$refs.scroll && this.$refs.scroll.refresh();
},
//离开时保存一个位置
deactivated() {
//1. 保存离开时的位置
this.saveY = this.$refs.scroll.scroll.y;
},