Vue3项目开发——商城
1.
1.划分目录结构
- assets资源文件夹下添加两个文件夹:img和css
- src文件夹下添加views文件夹,用来放组件,放一些大的视图(如首页视图,购物车视图)
- components文件夹里面放公共的组件,components文件夹里分为两类:common(完全公用的组件,多个项目可以使用)和content(只适用于当前项目)
- src文件夹下还会添加router文件夹,store文件夹,network文件夹
- src文件夹下还会添加common文件夹,放公共的js文件(公共的常量、工具类方法)
2.css文件的引入
- 在github下载normalize.css
- 在base.css中引入normalize.css
@import "./normalize.css";
- 在app.vue的style中引入base.css
@import './assets/css/base.css'
- base.css中,:root是伪类获取根元素,html中获取到的根元素就是html
在这其中定义了一些变量:colortint表示整体的背景颜色可以在其他地方使用这些变量
3.配置文件别名
- 创建vue.config.js文件
- 则可以直接通过
@import 'assets/css/base.css'
在app.vue中引入css
4.editorconfig文件
- 创建这个.editorconfig文件,统一设置缩进等等
5.tabbar开发
1.实现思路
- 思路
- 效果:
2.目录:
3.代码及思路
- assets文件夹里面一般放资源,可以把css和img放在这里面
- 通用样式写在app.vue里,在style动态引入文件,style里面引用前面要加@:
@import "./assets/css/base.css";
- 把下面抽取成一个大的组件tabbar,里面放插槽
- tabbar组件里的小组件item里的文字和图片不能写死,所以要用插槽;且插槽外面要包装一层div,防止替换的时候替换掉属性
- 这种方法不行的原因在于插槽会被替换掉,则没有这个属性
- 点击发生切换,在组件内部监听,用方法属性点击后改变路径,但是每个路径不一样不能写死,故通过props传进来,只需要字符串,不需要加:,变量则需要加:(动态传父组件的值)
- 如何判断处于活跃状态:计算属性,看能否找到活跃路由的path
- 我们希望动态传入活跃状态的颜色:动态绑定样式,在计算属性里判断处于活跃状态时拿到这个颜色
//App.vue文件
<template>
<div id="app">
<router-view></router-view>
<main-tabbar></main-tabbar>
</div>
</template>
<script>
import MainTabbar from './components/MainTabbar.vue'
export default {
name: 'App',
components: {
MainTabbar
}
}
</script>
<style>
/* 在style里面引用有一个固定的格式,前面要加@ */
@import "./assets/css/base.css";
</style>
//MainTabbar.vue文件
<template>
<tab-bar>
<item path="/home" activeColor="blue">
<img slot="item-icon" src="../assets/img/tabbar/home.png" alt="">
<img slot="item-icon-ac" src="../assets/img/tabbar/home-ac.png" alt="">
<div slot="item-text">首页</div>
</item>
<item path="/tabbar">
<img slot="item-icon" src="../assets/img/tabbar/tabbar.png" alt="">
<img slot="item-icon-ac" src="../assets/img/tabbar/tabbar-ac.png" alt="">
<div slot="item-text">分类</div>
</item>
<item path="/notice">
<img slot="item-icon" src="../assets/img/tabbar/notice.png" alt="">
<img slot="item-icon-ac" src="../assets/img/tabbar/notice-ac.png" alt="">
<div slot="item-text">购物车</div>
</item>
<item path="/user">
<img slot="item-icon" src="../assets/img/tabbar/user.png" alt="">
<img slot="item-icon-ac" src="../assets/img/tabbar/user-ac.png" alt="">
<div slot="item-text">我的</div>
</item>
</tab-bar>
</template>
<script>
import TabBar from '../components/tabbar/TabBar.vue'
import Item from '../components/tabbar/Item.vue'
export default {
name: 'MainTabbar',
components: {
TabBar,
Item
}
}
</script>
//TabBar.vue文件
<template>
<div id="tab-bar">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'TabBar'
}
</script>
<style>
#tab-bar {
display: flex;
background-color: #f2f2f2;
position: fixed;
bottom: 0;
left: 0;
right: 0;
box-shadow: 0 -2px 3px rgba(100,100,100,0.2);
}
</style>
//Item.vue文件
<template>
<div class="tab-bar-item" @click="itemClick">
<!-- 图片和文字都不能写死,所以插槽 -->
<!-- 插槽包装一层div,保证替换的时候不会替换掉属性 -->
<div v-if="!isActive"><slot name="item-icon"></slot></div>
<div v-else><slot name="item-icon-ac"></slot></div>
<div :style="activeSyle"><slot name="item-text"></slot></div>
</div>
</template>
<script>
export default {
name: 'Item',
props: {
path:String,
activeColor: {
type: String,
default: "red"
}
},
computed: {
isActive() {
return this.$route.path.indexOf(this.path) !== -1
},
activeSyle() {
return this.isActive ? {color:this.activeColor} : {}
}
},
methods: {
itemClick() {
this.$router.push(this.path)
}
}
}
</script>
<style>
.tab-bar-item {
flex: 1;
text-align: center;
height: 49px;
font-size: 14px;
}
.tab-bar-item img {
width: 25px;
height: 25px;
margin-top: 3px;
margin-bottom: 2px;
vertical-align: middle;
}
</style>
//index.js文件
import Vue from 'vue'
import Router from 'vue-router'
const Home = () => import('../view/home/Home.vue')
const Notice = () => import('../view/notice/Notice.vue')
const Tabbar = () => import('../view/tabbar/Tabbar.vue')
const User = () => import('../view/user/User.vue')
Vue.use(Router)
export default new Router({
routes: [
{
path: '',
redirect: '/home'
},
{
path: '/home',
component: Home
},{
path: '/notice',
component: Notice
},{
path: '/user',
component: User
},{
path: '/tabbar',
component: Tabbar
}
]
})
6.页面小图标的修改
- 在public里修改
7.首页开发
7.1.首页导航栏的封装
- 在common中添加一个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>
<script>
export default {
name: 'NavBar'
}
</script>
<style>
.nav-bar {
display: flex;
height: 44px;
line-height: 44px;
text-align: center;
box-shadow: 0 1px 1px rgba(100,100,100,.1);
}
.left,
.right {
width: 60px;
}
.center {
/* 将剩余位置全部占据 */
flex: 1;
}
</style>
- 注意:插槽如果要添加class属性,需要在外面套一层div
- 在home组件中使用这个组件
- 设置首页中导航栏的背景颜色应该在home.vue中设置,因为每个views中的背景颜色不一定相同
7.2.请求首页的多个数据
- 网络封装,在network文件夹中添加request.js文件
import axios from 'axios'
export function request(config) {
// 1.创建axios的实例
const instance = axios.create({
baseURL: 'http://123.207.32.32:8000',
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)
}
- 首页可能需要用到很多次request请求,但和vue写在一起会很混乱,所以在network文件夹中再添加home.js的文件,封装所有对首页数据的请求,更加方便管理
import { request } from './request'
export function getHomeData() {
return request({
url:'/home/multidata'
})
}
- 补充知识点:函数调用:压入函数栈(保存函数调用过程中所有变量)
函数调用结束:弹出函数栈(释放函数所有的变量) - home.vue文件:
在组件创建完后发送网络请求,使用生命周期函数created()
在data中保存请求到的数据
<script>
import NavBar from 'components/common/navbar/NavBar.vue'
import {getHomeData} from 'network/home.js'
export default {
name: 'Home',
components: {
NavBar
},
data () {
return {
banners: [],
recommends: []
}
},
// 组件一旦创建完后发送网络请求
created () {
// 1.请求多个数据,包括轮播图数据……
getHomeData().then(res => {
// this在箭头函数里往上找作用域
//created里有this,created里的this其实是组件对象
// 保存在data里,则数据当函数调用完后也不会消失
this.banners = res.data.banner.list;
this.recommends = res.data.recommend.list;
})
}
}
</script>
7.3.首页轮播图
1.导出方式
- 在swiper文件夹下新增一个index.js的文件
- 则导出的时候可以以对象的方式导出
import {Swiper,SwiperItem} from 'components/common/swiper'
2.代码
- 我们习惯将轮播图在Home.vue中的代码抽成一个组件分离出去,并在该组件中利用props传递数据
//homeSwiper.vue组件
<template>
<div>
<swiper>
<swiper-item v-for="(item, id) in banners" :key="id">
<a :href="item.link">
<img :src="item.image" alt="">
</a>
</swiper-item>
</swiper>
</div>
</template>
<script>
import {Swiper,SwiperItem} from 'components/common/swiper'
export default {
name: 'HomeSwiper',
props: {
banners: {
type: Array,
default() {
return []
}
}
},
components: {
Swiper,
SwiperItem
}
}
</script>
Home.vue中:
7.4.推荐信息的展示
- 主要代码
- 数据的获取用props父传子
7.5.TabControl的封装
1.基本代码
- 如果只是文字不一样,则没有必要用插槽,直接用props传递数据即可
- 导入的时候公共的组件放一起,方法放一起,子组件放一起,按空格区分
- tabcontrol需要实现点击变颜色的功能
<template>
<div class="tab-control">
<div v-for="(item,index) in titles" :key="index"
class="tab-item" :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;
}
}
}
</script>
Home.vue中传入数据的方式
2.实现‘sticky’的效果
获取offsetTop值
- 不建议用position:sticky,有些浏览器适配不好
- 通过判断它滚动的位置,大于offsetTop则将定位改为fixed
- 组件没有offsetTop属性,我们应该拿组件对应的元素,而所有的组件都有一个属性
$el
用于获取组件中的元素 - 如果直接在生命周期函数mounted中
console.log(this.$refs.tabControl.$el.offsetTop);
的结果是不准确的,挂载虽然意味着所有的组件都被挂载在上面,但是图片不一定全部加载完成,则没有包括图片的高度 - 我们应该等图片加载完成,拿到的offsetTop才是正确的
- views/home/childComps/HomeSwiper.vue中:
- Home.vue中
并且在data中保存数据tabOffsetTop: 0
默认为0
methods:
动态改变样式
- 动态的改变tabcontrol的样式时,会出现两个问题:下面的商品内容会突然上移,tabcontrol虽然设置了fixed,但也会随着better-scroll滚出去,因为better-scroll内部有个translate,会使fixed属性失效,而在bscroll中不好操作,
- 用其他方案来解决,我们可以在最上面多复制一份tabcontrol组件对象,当用户滚动到一定位置时显示出来,当没有滚动到一定位置时隐藏起来
- Home.vue中:
并且在data中保存数据isTabFixed: false
保证两个tab-control的高亮是相同的:
7.6.保存商品的数据结构设计
1.思路
- 点击流行的时候展示流行相关的数据,点击什么展示什么的数据,如果我们点击的时候再请求数据,则会给用户造成延迟
- 所以我们要弄一个变量,变量中存储着三个的数据请求
//goods用来保存数据
goods: {
//page用来记录现在加载的数据是第几页
'pop':{page: 1 ,list[]},
'news':{page: 1 ,list[]},
'sell":{page: 1 ,list[]}
}
- 这些都是首页的数据,写在home.vue的data里
2.首页数据的请求和保存
- 首先在network的home.js里定义方法
- Home.vue中代码:
注意:created里调用methods要加this,否则同名函数调用的将会是import里的方法
使用this.goods[type].list.push(...res.data.list)
将请求到的数据一个个放进数组里(当然也可以使用for循环来做)
<script>
import {getHomeData,getHomeGoods} from 'network/home.js'
export default {
name: 'Home',
components: {
NavBar,
HomeSwiper,
RecommendView,
FeatureView,
TabControl
},
data () {
return {
banners: [],
recommends: [],
goods: {
'pop': {page: 0,list: []},
'new': {page: 0,list: []},
'sell': {page: 0,list: []}
}
}
},
// 组件一旦创建完后发送网络请求
created () {
// 1.请求多个数据,包括轮播图数据……
// 加了this才是methods里的函数,否则调用的是import里的getHomeData
this.getHomeData();
//2. 请求商品数据
this.getHomeGoods('pop');
this.getHomeGoods('new');
this.getHomeGoods('sell');
},
methods: {
getHomeData() {
getHomeData().then(res => {
// this在箭头函数里往上找作用域,created里有this,而created里的this其实是组件对象
// 保存在data里,则数据当函数调用完后也不会消失
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 => {
// 把每次请求到的数据一个个放到数组里
// 可以用for循环的方法
// for (let n of nums1) {
// totalNums.push(n)
// }
this.goods[type].list.push(...res.data.list)
this.goods[type].page += 1
})
}
}
}
</script>
3.首页商品数据的展示
- 在content里添加GoodsList.vue和GoodsListItem.vue
//GoodsList.vue
<template>
<div class="goods">
<goods-list-item v-for="(item,index) in goods"
:goods-item="item" :key=index></goods-list-item>
</div>
</template>
<script>
import GoodsListItem from './GoodsListItem.vue'
export default {
name: 'GoodsList',
components: {
GoodsListItem
},
props: {
goods: {
type: Array,
default() {
return []
}
}
}
}
</script>
//GoodsListItem.vue
<template>
<div class="goods-item" @click="itemClick">
<img :src="showImage" alt="" @load="imageLoad">
<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 {}
}
}
},
computed: {
showImage() {
return this.goodsItem.image || this.goodsItem.show.img
}
},
methods: {
imageLoad() {
this.$bus.$emit('itemImageLoad')
},
itemClick() {
this.$router.push('/detail/' + this.goodsItem.iid)
}
}
}
</script>
4.TabControl点击切换商品
- 点击不同的按钮决定展示什么数据,通过自定义事件
- 在TabControl.vue中:
在home.vue中
使用计算属性的原因是因为太长了
7.7.回到顶部BackTop
1.代码
- 组件是无法直接监听原生事件的,必须加一个修饰符
.native
- 封装一个组件显示回到顶部的图标,而方法则应在需要回到顶部的组件中使用,因为需要拿到数据
- Home.vue中:给scroll设置ref
- 必须要给所在的content设置一个高度
2.BackTop的显示和隐藏
- 滚动了一定距离才显示,否则隐藏起来
- 因为有些时候并不需要监听滚动,所以不能写死,应该设置props
- scroll.vue中: 4. Home.vue中:
用一个变量来保存是否显示backtop,默认是不显示
7.8.解决滚动区域bug
- better-scroll在决定有多少区域可以滚动时,是根据scrollerHeight属性决定的,而scrollerHeight是根据放better-scroll中的子组件的高度,但我们的首页在刚开始计算该属性时,没有将图片计算在内
- 如何解决:监听每一张图片是否加载完成,只要有一张图片加载完成,执行一次refresh()
- 如何监听图片加载完成:
原生的js监听图片:img.onload=function() {}
vue中监听:@load=“方法” - 如何将GoodsListItem.vue这个中的事件传入到Home.vue中:因为涉及到非父子组件的通信,所以我们选择了事件总线:
Vue.prototype.$bus = new Vue()
this.$bus.$emit('事件名称',参数)
this.$bus.$on('事件名称',回调函数(参数))
- 先在main.js里:因为默认情况下$bus是没有值的,所以要创建vue实例GoodsListItem.vue:
scroll.vue中:
Home.vue中:
不能写在created里,因为还没挂载,this.refs
可能拿到的是undefined
为什么要加this.scroll短路条件:因为可能图片加载完成后,scroll对象可能并没有初始化
7.9刷新频繁的防抖函数处理
- 防抖函数:比如说在输入框输入时,每输入一个字符发送一次请求,会对服务器造成很大压力;我们可以等一定的时间,有改变再发送请求
- 防抖函数起的作用:可以将refresh函数传入到debouce函数中,生成一个新的函数,之后在调用非常频繁的时候,就使用新生成的函数,而新生成的函数,并不会非常频繁的调用,如果下一次执行来得非常快,那么会将上一次取消掉
- 封装函数:
//utlis.js中:
// 防抖函数
export function debounce(func,delay) {
let timer = null;
// ...意味着可以传入多个参数
// 这里的timer是函数是闭包
// 一旦有引用的时候,不会被销毁(虽然上面定义的是局部变量)
return function(...args) {
if(timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
},delay)
}
}
//Home.vue中:
import {debounce} from '../../common/utlis'
mounted () {
// 监听图片加载完成
// refresh后面不能加括号,因为加小括号即把返回值传进去了,我们需要传的是函数
const refresh = debounce(this.$refs.scroll.refresh)
this.$bus.$on('itemImageLoad',() => {
refresh()
})
}
//scroll.vue中:
methods: {
refresh() {
this.scroll && this.scroll.refresh()
}
}
7.10上拉加载更多
-
监听什么时候滚动到顶部
-
scroll.vue中:因为不是每一个都需要上拉加载更多,所以用props记录是否需要
-
Home.vue中的使用:
注意:若想下一次上拉也能加载到数据,必须调用scroll.vue中的finishPullUp;因为scroll默认加载一次
7.11.Home离开时记录状态和位置
- 让Home不要随意销毁掉:在router-view外面套一层keep-alive
- 让Home的内容保持原来的位置:离开时记录位置信息saveY,进来时将位置设置为saveY
scroll.vue中的方法:
Home.vue中在data里定义一个变量saveY保存数据
8.Better-scroll
1.安装和使用
- 移动端的原生滚动会非常卡顿,而iscroll框架已经不更新了,我们可以使用better-scroll框架
- 安装框架
npm install better-scroll --save
- 使用方法:不能直接传content,必须在外面包装一层div
- 官网:https://better-scroll.github.io/docs/zh-CN/guide/
2.基本使用解析
- 默认情况下,better-scroll不可以实时的监听滚动位置,要想监听,必须在newBsroll后面传参数
- 在基本程序中的写法
position是实时滚动的位置
1.参数probeType决定是否派发scroll事件
- 值为0:任何时候都不派发scroll事件
- 值为1:仅仅当手指按在滚动区域上,每隔momentumLimitTime毫秒派发一次scroll事件(非实时)
- 值为2:仅仅当手指按在滚动区域上,一直派发scroll事件(实时),手指离开后的惯性滚动过程中不侦测
- 值为3:只要是滚动都派发scroll事件,包括调用scrollTo或者触发momentum滚动动画
2.click参数
- 如果用better-scroll来管理wrapper的时候,它的管理范围之内如果如果想点击是默认监听不到的
3.pullUpload属性
- 默认值:false
- 这个配置用于做上拉加载功能,当设置为true或者是一个object时,可以开启上拉加载
3.下拉加载更多写法
因为pullingUp只能回调一次,所以还要调用finishPullUp才能进行下一次回调
4.在vue项目中使用过程
5.封装
- 如果每个项目都要
import BScroll from 'better-scroll'
,则对它的依赖性太强,若该框架不再维护,则非常麻烦 - 封装到公共组件的common里,必须要自己设置高度
- 如果直接用document.querySelector的方式,而且有多个同样的class值时,获取到的不一定是我们想要的那个
- 在vue中若想明确的拿到某个元素,有个方法为ref;
ref若绑定在组件中,通过this.$refs.refname
获取到的是一个组件对象;
若绑定在普通的元素中,通过this.$refs.refname
获取到的是一个元素对象; - 注意:如果style中有scoped,则所有样式只针对当前组件起效果;若没有,则只要属性名相同,都会有效果
<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: {
probeType: {
type: Number,
default: 0
},
pullUpLoad: {
type: Boolean,
default: false
}
},
data () {
return {
scroll: null
}
},
mounted () {
this.scroll = new BScroll(this.$refs.wrapper,{
observeDOM: true,
click: true,
probeType: this.probeType,
pullUpLoad: this.pullUpLoad
})
// 监听滚动的位置(不是所有都需要监听)
// 用if条件判断,让性能更高
if(this.probeType === 2 || this.probeType === 3) {
this.scroll.on('scroll',(position) => {
this.$emit('scroll',position)
})
}
// 监听滚动到底部
if(this.pullUpLoad) {
this.scroll.on('pullingUp',() => {
this.$emit('pullingUp')
})
}
},
methods: {
// 可以直接写time=300,即time的默认值为300
scrollTo(x,y,time) {
this.scroll && this.scroll.scrollTo(x,y,time)
},
refresh() {
this.scroll && this.scroll.refresh()
},
finishPullUp() {
this.scroll.finishPullUp()
},
getScrollY() {
// 判断有没有值
return this.scroll ? this.scroll.y :0
}
}
}
</script>
应用:记住需要对scroll设置固定的高度
给父元素设置100vh,而滚动的高度则是100%减去导航栏的高度
9.详情页
9.1.跳转到详情页并携带id
- 在首页的商品详情中点击时跳转到对应的详情页
- 监听GoodsListItem每个组件的点击,并跳转到对应的详情页,给详情页配置路由
配置路由:
GoodsListItem.vue中: - Detail.vue中:如何知道跳转的iid是什么,注意这里获取iid一定是$route
9.2.导航栏的封装
- 因为详情页的导航栏有点点复杂,所以可以在detail文件夹下创一个childComps的文件夹,里面放DetailNavBar.vue
- 代码注意点:
①导航栏中间的文字内容是保存在data中的,用v-for遍历
②点击返回键返回上一层this.$router.back()
<template>
<div>
<nav-bar>
<div slot="left" class="back" @click="backClick">
<img src="~assets/img/common/back.svg" alt="">
</div>
<div slot="center" class="title">
<div v-for="(item,index) in titles" :key="index"
class="title-item" :class="{active: index===currentIndex}"
@click="titleClick(index)">{{item}}</div>
</div>
</nav-bar>
</div>
</template>
<script>
import NavBar from 'common/navbar/NavBar.vue'
export default {
name: 'DetailNavBar',
components: {
NavBar
},
data () {
return {
titles: ['商品','参数','评论','推荐'],
currentIndex: 0
}
},
methods: {
titleClick(index) {
this.currentIndex = index
},
backClick() {
this.$router.back()
}
}
}
</script>
9.3.数据请求及轮播图展示
- 在network文件夹下添加detail.js
import { request } from './request'
export function getDetail(iid) {
return request({
url: '/detail',
params: {
iid
}
})
}
- 如何在Detail.vue中请求轮播图数据
- childComps/DetailSwiper.vue:
<template>
<div class="detail-swiper">
<swiper class="swiper-img">
<swiper-item v-for="(item,index) in topImages" :key="index">
<img :src="item" alt="">
</swiper-item>
</swiper>
</div>
</template>
<script>
import {Swiper, SwiperItem} from 'common/swiper'
export default {
name: 'DetailSwiper',
props: {
topImages: {
type: Array,
default() {
return []
}
}
},
components: {
Swiper,
SwiperItem
}
}
</script>
- 遇到的问题:每次点击不同的详情页显示的图片确实一样的,问题出在keep-alive是写在App.vue里的,所有的数据都会被保存
9.4.店铺信息的解析和展示
- 在给组件传数据时,应把数据整合好,即数据虽然很多很杂,但可以整合成一个对象
- 如何整合成一个对象,network/detail.js:
export class Goods {
constructor(itemInfo, columns, services) {
this.title = itemInfo.title
this.desc = itemInfo.desc
this.newPrice = itemInfo.newPrice
this.oldPrice = itemInfo.oldPrice
this.discount = itemInfo.discountDesc
this.columns = columns
this.services = services
this.realPrice = itemInfo.lowNowPrice
}
}
-
Detail.vue中:
import {getDetail, Goods} from 'network/detail.js'
,并在data中存储数据goods:{}
-
v-for="index in nums"
如果in后面是一个数字,则遍历的是从1到nums -
如何判断一个对象是否为空对象:
const obj ={}
//判断方法:
Object.keys(obj).length //===0即为空对象
- 注意细节:父组件和子组件中储存对象都应该用
{}
9.5.加入滚动的效果
- DetailInfo.vue中是放了很多很多的图片,滚动的时候滚到一定位置无法在滚动,因为刚开始并没有把图片的高度计算在内
//DetailInfo.vue中:
data () {
return {
// counter记录当前加载到第几张图片,imagesLength为图片个数
counter: 0,
// 不能直接写etailInfo.detailImage[0].list.length
// 因为刚开始的时候传过来的是空对象
imagesLength: 0
}
},
methods: {
imgLoad() {
// 判断条件是为了防止发送太多次事件
// 所以判断所有图片加载完成后,进行一次回调即可
if(++this.counter === this.imagesLength) {
this.$emit('imageLoad')
}
}
},
watch: {
// 监听对象的变化
detailInfo() {
this.imagesLength = this.detailInfo.detailImage[0].list.length
}
}
}
- Detail.vue中:
9.6.商品评论信息的展示
- 服务器返回时间基本都返回的是时间戳
- common/utlis.js中定义函数将date格式化
export function formatDate(date, fmt) {
if(/(y+)/.test(fmt)){
//第一种:利用字符串连接符“+”给date.getFullYear()+"",加一个空字符串便可以将number类型转换成字符串。
fmt=fmt.replace(RegExp.$1,(date.getFullYear()+"").substr(4-RegExp.$1.length));
}
let o = {
"M+": date.getMonth()+1,
"d+": date.getDate(),
"h+": date.getHours(),
"m+": date.getMinutes(),
"s+": date.getSeconds()
};
//因位date.getFullYear()出来的结果是number类型的,所以为了让结果变成字符串型,下面有两种方法:
for(let k in o){
if (new RegExp("(" + k +")").test(fmt)){
//第二种:使用String()类型进行强制数据类型转换String(date.getFullYear()),这种更容易理解。
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(String(o[k]).length)));
}
}
return fmt;
}
- 如何将时间戳转成时间格式化字符串
<script>
//1.将时间戳转成Date对象
//时间戳是秒为单位,我们要拿到毫秒为单位,所以乘以1000
const date = new Date(value*1000)
//2.将date进行格式化,转成对应的字符串
</script>
- yyyy代表年份占四位,也可以写yy则默认取年份后两位,-符号代表返回时年月日用-分隔,还可以写hh小时(h为12小时制,H为24小时制),m为分钟,s为秒
9.7. 首页和详情页监听全局事件和mixin的使用
-
在推荐模块我们用的是goodsListItem.vue组件,会在所有图片加载完成后通知Home使其刷新,并不合理,我们可以如何区分:通过路由做个判断
-
通过路由做判断:goodsListItem.vue里:
-
方法二:Home.vue中:
data里存储:
-
如果在详情页里也想监听:Detail.vue里
-
Home.vue和Detail.vue中的mounted里都有一些公共的代码,如何抽取:应用mixin(混入),这里不能用继承,因为继承是减少类里面的重复代码,而我们这里是两个对象(exprot default)
common/mixin.js:
-
使用:
9.8.点击详情页标题滚到对应内容
1.点击标题滚到对应的主题内容
- 重点是获取每个内容的offsetTop
- 获取offsetTop不能写在mounted里:拿到的
this.$refs.recommends.$el.offsetTop
undefined,即意味着没有$el,原因是在子组件里我们用了v-if="Object.keys(detailInfo).length !== 0"
只有在有值的时候才渲染,意味着在没有请求过来数据时,不能保证mounted里数据一定请求完成,子组件可能没有加载完成 - 写在getDetail里也是错误的,因为虽然有值,但是还没有渲染完成
- 用
this.$nextTick
函数,需要传入回调函数,会等前面的代码渲染完成,回调该函数,在getDetail函数里:注意这里的值不对,因为这里offsetTop是不包括图片的 - 但是上面这样写当进入其他商品的详情页时,跳转的位置不对:因为上面那样写仅仅是将dom渲染出来了,图片还没加载完成
经验:offsetTop的值不对的时候,基本是因为图片的问题 - 正确做法:在图片加载完成后,获取的高度才是正确的
data里存储数据:getThemeTopY: null
methods里图片记加载完成后去调用函数
2.滚动内容显示对应标题
- 代码:给navbar设置了属性
ref=“nav”
3.对复杂判断条件分析和优化
- 上面的
this.currentIndex !== i
是为了防止赋值的过程过于频繁,后面的条件是分了两种情况(防止下标越界) - 优化:hack做法,最大值为
Number.MAX_VALUE
<script>
contentScroll(position) {
const positionY = -position.y;
let length = this.themeTopY.length;
for(let i =0;i<length-1;i++) {
if(this.currentIndex !== i &&
(positionY >= this.themeTopY[i] && positionY < this.themeTopY[i+1])) {
this.currentIndex = i;
this.$refs.nav.currentIndex = this.currentIndex
}
}
}
</script>
9.9.回到顶部BackTop
- 和Home.vue中共同代码很多,可以做个抽取,用mixin
- 如果是生命周期函数,即可在mixin里写也可以在组件里写,即可以同时写,而methods里的函数不行会覆盖,抽取时需注意
10.购物车
10.1.将商品添加到购物车
- 代码:
Detail.vue中:
store/index.js:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
cartList: []
},
mutations: {
addCart(state, payload) {
// // 方法一
// let oldProduct = null;
// // 判断商品有没有添加过,没有的话添加到数组里
// // 有的话数量+1
// for (let item of state.cartList) {
// if (item.iid = payload.iid) {
// // 浅拷贝
// oldProduct = item;
// }
// }
// if (oldProduct) {
// // payload.count也加了1
// oldProduct.count += 1;
// } else {
// payload.count = 1;
// state.cartList.push(payload)
// }
// 方法二
let oldProduct = state.cartList.find((item) => item.iid === payload.iid)
if (oldProduct) {
oldProduct.count += 1
} else {
payload.count = 1;
state.cartList.push(payload)
}
}
},
actions: {
},
modules: {
}
})
10.2.vuex中代码的重构
- vuex中代码的重构:mutations唯一的目的就是修改state中的状态,尽可能保证它的每个方法完成的事情比较单一
异步操作、判断逻辑放在actions中 - 代码:
mutation-type.js:
mutations.js:尽可能使每个方法做的事情单一
actions.js:将逻辑判断放在这里
index.js:
10.3.导航栏实现
1.代码
- 代码:getters.js
Cart.vue:
2.getters映射
- vuex知识点:getters实现映射如何直接把getters里的当成计算属性来使用
10.4.购物车列表的item展示
- 如何传数据,用props
10.5.购物车按钮
1.基本封装
- 封装成checkButton组件
2.item选中和不选中的切换
- item的选中不选中应有个东西来记录,不能用属性记录,一定是在对象模型里记录
- store/mutations.js:
- cartListItem.vue:
10.6.底部总价及选中汇总工具栏
- 模板及效果图
- 如何计算价格和商品数目:计算属性
计算价格:选中的商品将数目乘以单价
10.7.全选按钮
1.状态显示
- 判断是否有一个不选中,全选则不选中
- 回顾:子组件checkButton.vue中用props接收数据
- cartBottom.vue:
2.点击效果
- 点击全选按钮全部选中,取消则全部商品取消
- 代码
methods: {
clickAll() {
// 全部选中
// isAll是上面的计算属性,全选为true,没有全选为false
if(this.isAll) {
this.$store.state.cartList.forEach(item => item.checked = false)
} else {
// 部分或全部不选中
this.$store.state.cartList.forEach(item => item.checked = true)
}
}
},
10.8.点击添加到购物车显示弹窗
1.代码
- 详情页时点击加入购物车弹出弹窗
//action.js中:
export default {
addCart(context, payload) {
return new Promise((reslove, reject) => {
let oldProduct = context.state.cartList.find((item) => item.iid === payload.iid)
if (oldProduct) {
// 数量+1
// 不能直接修改state,要经过mutations
context.commit(ADD_COUNTER, oldProduct)
reslove('当前的商品数量+1')
} else {
// 添加新商品
payload.count = 1;
context.commit(ADD_TO_CART, payload)
reslove('添加新商品')
}
})
}
}
Detail.vue:
2.actions映射
- 如何实现actions的映射
<script>
import {mapActions} from 'vuex'
export default {
name: 'Detail'
mixins: [itemListenMixin],
methods: {
// 数组里写要映射的函数
...mapActions(['addCart']),
addGoods() {
// 获取购物车需要展示的商品信息
const product = {}
product.image = this.topImages[0];
……
this.addCart(product).then(res => {
console.log(res);
})
}
}
}
</script>
11.Toast封装
1.普通方式封装
- 需求:当点击加入购物车时,显示一个弹窗,这个弹窗称为toast
- 代码:
//Toast.vue
<template>
<div class="toast" v-show="isShow">
<div>{{message}}</div>
</div>
</template>
<script>
export default {
name: 'Toast',
props: {
message: {
type: String,
default: ''
},
isShow: {
type: Boolean,
default: false
}
}
}
</script>
- 使用方法:保存isShow和message的数据
- 但是这种方法使用起来过于麻烦
2.插件方式封装
- 如何封装
//Toast.vue
<template>
<div class="toast" v-show="isShow">
<div>{{message}}</div>
</div>
</template>
<script>
export default {
name: 'Toast',
data () {
return {
message: '',
isShow: false
}
},
methods: {
show(message,duration=2000) {
this.isShow = true;
this.message = message;
setTimeout(() => {
this.isShow = false;
this.message = '';
},duration)
}
}
}
</script>
//toast下的index.js文件:
import Toast from './Toast.vue'
const obj = {}
obj.install = function (Vue) {
// 1.创建组件构造器
const toastContrustor = Vue.extend(Toast)
// 2.new的方式,根据组件构造器,可以创建出来一个组件对象
const toast = new toastContrustor()
// 3.将组件对象,手动挂载到某一元素上
toast.$mount(document.createElement('div'))
// 4.toast.$el对应的就是div
document.body.appendChild(toast.$el)
// Vue.prototype.$toast = 对象
Vue.prototype.$toast = toast;
}
export default obj
//main.js文件:
import toast from 'common/toast'
// 安装toast插件
Vue.use(toast)
- 如何使用:任何地方使用都只需要
this.$toast.show(res,2000)
12.fastclick
- 作用:解决移动端300ms延迟
- 使用:
安装:npm install fastclick --save
导入:import FastClick from 'fastclick'
(main.js)
调用attach函数:FastClick.attach(document.body)
(main.js)
13.图片懒加载:vue-lazyload框架
- 使用:
安装:npm install vue-lazyload --save
导入:import VueLazyLoad from 'vue-lazyload'
(main.js)
使用懒加载插件:Vue.use(VueLazyLoad)
(main.js)
修改img:将:src=" "
改成v-lazy=" "
- 使用懒加载时后面还可以传入options
14.px2vw:css单位转化插件
- 在不改代码的情况下把所有单位改成vw
- 安装:
npm install postcss-px-to-viewport --save-dev
- 配置:postcss.config.js
module.exports = {
plugins: {
autoprefixer: {},
"postcss-px-to-viewport": {
unitToConvert: "px", // 默认值`px`,需要转换的单位
viewportWidth: 750,//视窗的宽度,对应的是我们设计稿的宽度
viewportHeight: 1334, // 视窗的高度,根据750设备的宽度来指定,一般指定1334,也可以不配置
unitPrecision: 3,//指定`px`转换为视窗单位值的小数位数,默认是5(很多时候无法整除)
viewportUnit: 'vw',//指定需要转换成的视窗单位,建议使用vw
fontViewportUnit: 'vw', //指定字体需要转换成的视窗单位,默认vw;
selectorBlackList: ['.ignore'],//指定不转换为视窗单位的类
minPixelValue: 1,// 小于或等于`1px`不转换为视窗单位
mediaQuery: false,// 允许在媒体查询中转换`px`,默认false
exclude:[/node_modules/], //如果是regexp, 忽略全部匹配文件;如果是数组array, 忽略指定文件.
}
}
}
15.nginx
1.windows中部署
- windows中部署:windwos安装nginx,将项目进行打包部署
- 。
把项目的dist文件夹拷贝一份放到安装根目录下
2.远程Linux部署
- centos安装nginx,上传打包项目部署
- 远程部署
16.响应式原理
-
响应式图解
每个属性都有dep对象
解析过程: -
Vue内部是如何监听数据的改变:通过Object.defineProperty监听对象属性的改变
<script>
// 内部相当于把data:
const obj = {
message: '哈哈哈',
name: 'guess'
}
Object.keys(obj).forEach(key => {
let value = obj[key]
// 给obj定义key属性,虽然原来有这个属性,但是不好监听
// 所以重新定义
Object.defineProperty(obj,key,{
set(newValue) {
// 监听key改变
// 根据解析html代码,获取到哪些人有用这个属性
value = newValue
dep.notify()
},
get() {
// 每次用key都会调用一次get
return value
}
})
})
// 发布订阅者
class Dep {
constructor() {
// 数组记录谁要订阅属性
this.subs = []
}
addSub(watcher) {
this.subs.push(watcher)
}
notify() {
//遍历所有watcher
this.subs.forEach(item => {
item.update()
})
}
}
class Watcher {
constructor(name) {
this.name = name;
}
upDate() {
console.log(this.name + '发生update');
}
}
const dep = new Dep()
</script>
- 当数据发生改变,Vue是如何知道要通知哪些人,界面发生刷新:
发布订阅者模式 - 结合vue官网文档
更多推荐
所有评论(0)