Vue商城项目的前提工作
用脚手架3创建项目
- vue create 项目名称
在GitHub上建一个仓库
将项目与github联系起来
- git init
- git add .
- git commit -m ‘项目名称’
- git remote add origin github地址
- git push -u origin master
更新文件到github
git add 文件名称或者 git add .
git commit -m “这是注释内容”
这一步从本地仓库或本地分支获取并集成(整合),输入指令:git pull origin master
如果过程中出现‘please enter a commit message…’,首先按下esc退出键然后输入 :wq即可
输入指令:git push -u origin master
划分目录结构
- src
- assets 放一些资源
- css
- img
- components 放一些公共的组件
- common 当前项目里面可以使用的组件 在下一个项目里面也可以使用的组件
- content 与业务相关的组件 只针对于当前项目来说是公共的
- views 对应视图的一些逻辑
- router 路由相关的东西
- store 公共状态的管理 vuex
- network 网络相关的
- common 公共的js文件
- const.js 公共常量
- utils.js 工具函数
- mixin.js 一些混入

- assets 放一些资源
引入css文件
一般开发的是一个前端项目的话,会对项目里面很多css进行初始化
- normalize.css 对浏览器上的很多标签进行统一 在github上搜索 文件链接
- base.css 这是属于自己的css
配置别名
在vue.config.js中配置别名
首页功能的实现



项目模块划分
在components/common封装tabbar
在components/content封装mainTabbar
npm install vue-router --save- 在router的index.js中配置路由映射关系
对组件进行懒加载 - 在main.js中挂载
请求首页的多个数据
npm install axios --save安装框架- 在network文件夹下新建request.js
import axios from 'axios'导入
注意:这里写的是export而不是export default,为什么?
因为export default只能暴露一个对象,而export 可以暴露多个对象,以后需要用到多个的话就可以- 在network文件夹下新建home.js获取首页所需数据的api
- 在Home组件中面向home.js进行开发,首页创建好之后就发送网络请求在create中
- 在data中保存数据
网络模块的封装
network->request.js
第一层是工具函数层,封装一个通用的axios,创建一个实例,包括请求的基本路径,请求拦截器,响应拦截器。
export function request(config) {
// 创建axios实例
const instance = axios.create({
// 默认是get请求
// 支持跨域 jsonp 在url后面写上callback
// 这个接口不支持post
baseURL: "http://123.207.32.32:8000",
timeout: 5000,
})
// 请求的拦截器
instance.interceptors.request.use(config => {
// 比如config中的一些信息不符合服务器的请求
// 比如每次发送网络请求时,都希望界面中显示一个请求的图标
// 某些网络请求(比如登录),需要携带一些信息
return config
}, err => {
console.log(err);
})
// 响应的拦截器
instance.interceptors.response.use(res => {
return res
}, err => {
console.log(err);
})
//发送真正的网络请求
return instance(config)
}
首页面向request发送网络请求
network->home.js
第二层封装,接口层,对首页所有数据的请求都在home.js中,统一管理。
export function getHomeMultidata(){
return request({
url:'/home/multidata'
})
}
第三层,调用层,Home.vue中,首页创建完就进行网络请求
getHomeMultidata(){
getHomeMultidata().then(res=>{
//console.log(res);
// result在组件中,会一直存在,保存在data中
//this.result=res;
this.banners=res.data.data.banner.list;// 轮播图
this.recommends=res.data.data.recommend.list; // 推荐信息
})
},
导航栏的封装和使用

- 在common中封装NavBar,使用具名插槽
- 在Home中引入封装好的组件
NavBar的封装
<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.vue中的使用
固定在顶部,不跟随滚动条滚动
<nav-bar class="home-nav">
<div slot="center">购物街</div>
</nav-bar>
轮播图的封装和使用

- 在componets/common下新建swiper文件下,封装Swiper和SwiperItem
- 在home/childComps下封装HomeSwiper组件,
- 在Home中引入封装好的组件
- 展示轮播图数据banners
推荐信息的展示

- 在home/childComps下封装HomeRecommendView组件
- 在Home中引入封装好的组件
- 展示推荐信息的数据recommend
<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>
FeatureView的封装

- 独立组件封装。在home/childComps下封装FeatureView组件
- 放的是一张图片 div>a>img
TabControl选项卡的封装

- 在componets/content下封装TabControl,只是文字不一样的时候,就没必要弄插槽了
- props->titles div->根据titles v-for遍历 div->span{{title}}
- flex布局,均等分
- 选中谁,谁就变红色
在data中弄一个变量currentIndex来记录当前谁被选中;
动态绑定class属性,:class="{active:index == currentIndex}",默认第一个选中变红色; - 点击谁的时候,谁就变,监听item的点击,绑定点击事件
@click="itemClick(index)"
itemClick(index){
this.currentIndex=index;
}
- 吸顶效果,监听滚动,当滚动到一定位置的时候,将position设置成fixed,向下滚动的时候把fixed属性删除,就可以随着一起滚动了。
position:sticky;必须设置top值,这个属性的作用是 在没有达到top值之前,position是sticky,达到之后改成fixed,但是很多浏览器不兼容这个属性。
商品列表数据的请求和展示

- 请求商品数据,既要展示流行数据,又要展示新款数据,还要展示精选数据,根据不同的点击,展示不同的数据
- 用变量保存数据,一次性把三个数据请求一下,在data中定义
要先考虑设计什么数据结构,然后再去使用
有自己对应的页码和数据,page用来记录当前加载到第几页了,list用来保存数据
goods:{
'pop':{page:0,list:[]},
'new':{page:0,list:[]},
'sell':{page:0,list:[]},
},
- 在network的
home.js中获取商品所需数据的api,getHomeGoods
export function getHomeGoods(type,page) {
return request({
url: '/home/data',
params:{
type,
page
}
})
}
- 在Home的
create中请求商品数据
默认情况下先加载第一页的数据,在以后的上拉加载更多的操作下再去请求更多的数据。
getHomeGoods(type){
const page=this.goods[type].page + 1;
getHomeGoods(type,page).then(res=>{
//对象中的扩展运算符(...)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中
this.goods[type].list.push(...res.data.data.list);
this.goods[type].page += 1;
})
},
- 在components/content下新建goods文件夹,在里面封装GoodsList.vue和GoodsListItem.vue,从某一个类型中取出商品的list传给GoodsList,然后遍历出小的item传给GoodsListItem,让GoodsListItem展示商品
- 在GoodsList.vue中,定义props:goods,商品一行显示两个,用flex布局,给子item设置固定的宽度
.goods{
display: flex;
flex-wrap: wrap;
justify-content: space-around;
}
- 在GoodsListItem.vue中,定义props:goodsItem,父组件向子组件传数据,用
props
TabControl点击切换商品
- 首先内部监听点击,将事件传到Home中去,之前点击的时候只是内部样式的改变,现在需要传递事件,子组件发生点击事件,将事件传给父组件,用自定义事件
$emit,
methods: {
itemClick(index){
this.currentIndex=index;
this.$emit('tabClick',index)
}
}
在父组件Home中监听事件,@tabClick="tabClick",根据监听决定goodslist显示商品的类型。
- 定义一个当前显示的商品类型,默认是pop,
currentType:'pop',根据点击切换显示类型
<goods-list :goods="goods[currentType].list"/>
tabClick(index){
switch (index) {
case 0:
this.currentType='pop'
break;
case 1:
this.currentType='new'
break;
case 2:
this.currentType='sell'
break;
}
}
:goods="goods[currentType].list"这一串太长了,弄一个计算属性computed,来展示商品
<goods-list :goods="showGoods"/>
computed: {
showGoods(){
return this.goods[this.currentType].list
}
},
吸顶效果
- 必须知道滚动到多少时,开始有吸顶效果,获取到
tabcontrol的offsetTop,在data中保存tabOffsetTop:0,为了能够拿到,绑定一个属性ref="tabControl",拿到组件,组件没有offsetTop属性,而是要拿到组件对应的元素,所有的组件都有一个属性$el,用来获取组件中的元素,mounted中DOM加载完毕,但是图片不一定加载完了,所以要等图片加载完了之后计算出一个最终的offsetTop, - 监听轮播图是否加载完成,只需要发出一次事件,弄一个变量记录状态,和之前的防抖的区别
在HomeSwiper.vue中
<img :src="item.image" @load="imageLoad">
data(){
return{
isLoad:false; // 只需要发出一次事件
}
},
imageLoad(){
if(!this.isLoad){
this.$emit('swiperImageLoad');
this.isLoad=true;
}
}
在Home.vue中
<home-swiper :banners="banners" @swiperImageLoad="swiperImageLoad"/>
swiperImageLoad(){
this.tabOffsetTop=this.$refs.tabControl.$el.offsetTop;
},
- 监听滚动,动态改变tabControl的样式,弄一个变量,
isTabFixed:false,默认情况下不吸顶,当滚动到一定位置的时候,改变状态,动态绑定class
<tab-control :titles="['流行','新款','精选']" @tabClick="tabClick"
ref="tabControl" :class="{fixed:isTabFixed}"/>
contentScroll(position){
//1.判断回到顶部是否显示
this.isShowBackTop = (-position.y) > 1000
//2.判断tabControl是否吸顶
this.isTabFixed = (-position.y) > this.tabOffsetTop
},
.fixed{
position: fixed;
left: 0;
right: 0;
top: 44px;
}
问题:这种方法下面的商品内容会一下子往上移,虽然tabControl设置了fixed,但是也随着better-scroll一起滚出去了,因为tabControl会脱标。better-scroll滚动是根据translate属性移动的,设置了fixed还是能跟着一起滚动。
可以把tabControl再复制一份,来实现停留效果。

但是选项卡会被标题导航给盖住。
标题购物车可以不定位,这样就不会脱标,使用定位是在用浏览器原生滚动的时候。复制的tabControl放到navbar的下面,此时在轮播图的后面,再设置相对定位给一个层级关系,相对定位是因为还在原来的位置。默认它是不显示的,v-show="isTabFixed",当滚动到一定位置的时候再显示,当滚动没有达到一定位置时,隐藏。
swiperImageLoad(){
this.tabOffsetTop=this.$refs.tabControl2.$el.offsetTop;
},
问题:现在两个tabControl并没有保持一致。
tabClick(index){
switch (index) {
case 0:
this.currentType='pop'
break;
case 1:
this.currentType='new'
break;
case 2:
this.currentType='sell'
break;
}
this.$refs.tabControl1.currentIndex=index;
this.$refs.tabControl2.currentIndex=index;
},
我们并不能保证用户点击的是哪一个,所以两个都要赋值。
当把项目部署到服务器之后,用手机端去请求网页的时候,样式各方面没什么问题,但是滚动的时候,没有一个滚动的时候的滑动效果,滑动的时候也很卡顿,此时用的是原生的滚动,什么是原生的滚动呢?当里面的内容超过了当前的窗口的时候,自动就可以滚动了。但是用在移动端,会非常卡顿。以前用
iScroll来适配移动端的滚动,但是这个框架现在作者不更新了。better-scroll在iscroll基础上,还加入了一些c3的属性。可以实现移动端的顺滑滚动,还在顶部和底部都增加了弹簧效果。
原生的滚动
.content{
height:150px;
overflow-y:scroll;
}
Better-Scroll的安装和使用
npm install better-scroll --save
在外层弄一个wrapper,wrapper需要固定的高度,在 wrapper里面放内容content,只能是一个标签组成的content,它是父元素的第一个子元素,滚动的部分是content元素。
使用
import BScroll from 'better-scroll'
new BScroll(el,options)el挂载一个要滚动的元素
new BScroll(document.querySelector('.wrapper'))
也可以直接传一个类,根据类型查找标签
new BScroll('.wrapper',{})实时监听当前滚动到什么位置了
(默认情况下,BScroll不能实时监听滚动的位置)
需要设置一个属性probeType
probeType值为0,1 不侦测实时的位置
2 在手指滚动的过程中侦测,手指离开后的惯性滚动过程中不侦测
3 只要是在滚动,都侦测上拉加载更多 设置
pullUpLoad为true,要调用finishPullUp方法才能实现下一次的上拉加载更多
要监听什么时候滚动到最底部了滚动区域包裹的元素需要监听点击 ,设置
click属性是true,better-scroll会阻止浏览器的原生 click 事件(现在好像可以了)
BScroll的基本使用
on方法
scroll事件 滚动的实时坐标
bscroll.on("scroll",(position)=>{
console.log(position);
})
pullingUp 上拉加载更多
bscroll.on("pullingUp",()=>{
// 发送网络请求,请求更多页数据
// 等数据请求完,并且将新的数据展示出来后
bscroll.finishPullUp(); // 因为只能上拉加载一次,执行完这个函数之后,才能进行下一次上拉加载更多
})
封装better-scroll与使用
- 在components/common下创建一个scroll文件夹,在里面封装Scroll
- 需要一个插槽
- 引入better-scroll,
- DOM被挂载时,初始化BScroll,
querySelector不能明确的指定拿到的是哪个元素,绑定ref属性。
例如在Home.vue中也有class=“wrapper”,在App.vue中也有class=“wrapper”,不知道到底取的是哪个,querySelector获取的可能是第一个。
ref如果绑定在组件中,
this.$refs.refname获取到的是一个组件对象 。一般绑定在子组件。
ref如果绑定在普通的元素中,获取到的是一个元素
- home的高度太高了,相当于所有内容的高度,给一个100的视口高度,在Home中给滚动的区域content一个固定的高度
#home{
height: 100vh;
position: relative;
}
.content{
overflow: hidden;
position: absolute;
top:44px;
bottom: 49px;
left:0;
right:0;
}
可滚动区域的问题
在决定有多少区域可以滚动时,根据scrollerHeight属性决定的,scrollerHeight属性是根据放在better-scroll的content中的子组件的高度,
但是在首页中,刚开始在计算scrollerHeight属性时,是没有将图片计算在内的,后来图片加载进来之后有了新的高度,但是scrollHeight属性并没有更新。
监听每一张图片是否加载完成,只要有一个图片加载完成了,执行一次refresh()。
如何监听图片是否加载完成?
原生js img.οnlοad=function() {}
vue @load=‘方法’
事件总线:和vuex很像,不是管理状态的,而是管理事件的。因为涉及到非父子组件通信。
在GoodsListItem中监听图片是否加载完成,
<img :src="showImage" alt="" @load="imageLoad"">
imageLoad(){
this.$bus.$emit('itemImageLoad')
},
在main.js中
Vue.prototype.$bus=new Vue()
在Scroll中
refresh(){
this.scroll && this.scroll.refresh && this.scroll.refresh()
},
在Home.vue的mounted中接收事件总线,refresh会执行很多次
this.$bus.$on('itemImageLoad',()=>{
this.$refs.scroll.refresh()
})
刷新频繁防抖处理
debounce
如果直接执行refresh,会执行很多次,可以将refresh函数传到debounce函数中,生成一个新的函数,之后在调用非常频繁的时候,就用新生成的函数,而新生成的函数,并不会非常频繁的调用,如果下一次执行来的非常快,那么会将上一次取消掉。
在common/utils.js中封装防抖函数,
export function debounce(func, delay){
let timer = null
// ...可以传几个参数
return function (...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, delay);
}
}
回到顶部
按钮的封装和使用
- 在components/content下创建一个backTop文件夹,在里面封装BackTop
- 在BackTop里面放一个图片,定位在右下角
- 在Home中引入,不需要放在滚动区域scroll中
- 在BackTop中监听回到顶部不太好,回到顶部需要拿到滚动区域scroll对象,所以监听回到顶部的点击事件放到Home中比较好
- 监听组件的点击事件,要调用
.native修饰符(监听一个组件的原生事件时);也可以在BackTop.vue内部监听点击,用$emit传给父组件Home.vue。像button,div是可以直接监听点击的。组件不能直接监听点击。
<back-top @click.native="backTopClick"></back-top>
- 在Scroll中封装
scrollTo()方法
scrollTo(x,y,time=300){
this.scroll && this.scroll.scrollTo(x,y,time)
},
- 给
<scroll>绑定ref,拿到滚动的对象
backTopClick(){
this.$refs.scroll.scrollTo(0,0,500)
},
按钮的显示和隐藏
滚动到一定区域的时候才显示,回到的时候隐藏
- 在Scroll中监听滚动的位置,在props中定义probeType
props:{
probeType:{
type:Number,
default:0,
},
- 在Home中要监听scroll的实时滚动,不加冒号会把probe-type当成字符串
<scroll class="content"
ref="scroll"
:probe-type="3">
- 在Scroll中要根据在不同地方的应用的时候,有没有传入probeType的值,决定别人要不要实时监听,在父组件中监听子组件传过来的自定义事件
@scroll="contentScroll"
if(this.probeType===2 || this.probeType === 3){
this.scroll.on('scroll',(position)=>{
this.$emit('scroll',position)
})
}
<scroll class="content"
ref="scroll"
:probe-type="3"
@scroll="contentScroll">
5.在Home中根据滚动的位置决定是否显示还是隐藏,用v-show ,默认一进去的时候是不显示的,isShowBackTop:false,
<back-top @click.native="backTopClick" v-show="isShowBackTop"></back-top>
contentScroll(position){
//1.判断回到顶部是否显示
this.isShowBackTop = (-position.y) > 1000
},
完成上拉加载更多
- 监听什么时候滚动到底部。在Scroll中,定义
pullUpLoad属性,
props:{
pullUpLoad:{
type:Boolean,
default:false
}
},
在Home.vue中
<scroll class="content"
ref="scroll"
:probe-type="3"
@scroll="contentScroll"
:pull-up-load="true">
在Scroll中,向父组件传递事件,pullingUp在一次上拉加载的动作后,这个时机一般用来去后端请求数据。
if(this.pullUpLoad){
this.scroll.on('pullingUp',()=>{
this.$emit('pullingUp')
})
}
- 在Home的
<scroll>中定义属性@pullingUp="loadMore",加载更多的时候是针对商品类型来加载的,选中谁就给谁上拉加载更多
<scroll class="content"
ref="scroll"
:probe-type="3"
@scroll="contentScroll"
:pull-up-load="true"
@pullingUp="loadMore">
loadMore(){
//只能上拉加载一次
this.getHomeGoods(this.currentType)
},
但是此时只能上拉加载一次,在数据加载完成之后,调用finishPullUp可以实现多次
getHomeGoods(type){
const page=this.goods[type].page + 1;
getHomeGoods(type,page).then(res=>{
this.goods[type].list.push(...res.data.data.list)
this.goods[type].page += 1
//可以多次上拉加载
this.$refs.scroll.finishPullUp()
})
},
Home离开时记录状态和位置
当滚动一半的时候,切换去了其他页面,再回到首页的时候,并没有保持在原来的位置,而是回到了顶部。
让Home不要随意销毁掉
使用keep-alive
<keep-alive>
<router-view/>
</keep-alive>
让Home保持原来的位置
离开时,保存一个位置信息saveY
进来时,将位置设置为原来保存的位置信息saveY即可
进来的时候可能刷新一下,不然可能会回到顶部
在Scroll.vue中
getCurrentY(){
return this.scroll ? this.scroll.y : 0
}
在Home.vue中
// 进来的时候设置位置
activated() {
this.$refs.scroll.scrollTo(0,this.saveY,0)
this.$refs.scroll.refresh();// 不然可能出现一些问题
},
// 离开的时候记录位置
deactivated() {
this.saveY=this.$refs.scroll.getCurrentY()
},
点击首页中的商品跳转到商品详情页
点击商品进去详情页,根据点击请求更加详细的信息,要传过来goodsItem的iid,根据id去服务器请求更加详细的信息;配置路由映射关系,点击进行跳转,带参数传递跳转。
在GoodsListItem中
itemClick(){
this.$router.push('/detail/'+this.goodsItem.iid)
/* this.$router.push({
path:'/detail',
query:{
iid:this.goodsItem.iid
}
}) */
}
但是获取到的iid在更换点击商品时没有改变,并没有发生新的请求,因为发生了路由跳转,router-view由keep-alive包着,不会每次销毁并重新创建,所以不会给iid给新的值,详情页不要使用keep-alive,使用exclude属性
<keep-alive exclude="Detail">
<router-view></router-view>
</keep-alive>