1.划分目录结构

  1. assets资源文件夹下添加两个文件夹:img和css
  2. src文件夹下添加views文件夹,用来放组件,放一些大的视图(如首页视图,购物车视图)
  3. components文件夹里面放公共的组件,components文件夹里分为两类:common(完全公用的组件,多个项目可以使用)和content(只适用于当前项目)
  4. src文件夹下还会添加router文件夹,store文件夹,network文件夹
  5. src文件夹下还会添加common文件夹,放公共的js文件(公共的常量、工具类方法)

2.css文件的引入

  1. 在github下载normalize.css
  2. 在base.css中引入normalize.css@import "./normalize.css";
  3. 在app.vue的style中引入base.css@import './assets/css/base.css'
  4. base.css中,:root是伪类获取根元素,html中获取到的根元素就是html
    在这其中定义了一些变量:colortint表示整体的背景颜色在这里插入图片描述可以在其他地方使用这些变量在这里插入图片描述

3.配置文件别名

  1. 创建vue.config.js文件在这里插入图片描述
  2. 则可以直接通过@import 'assets/css/base.css'在app.vue中引入css

4.editorconfig文件

  1. 创建这个.editorconfig文件,统一设置缩进等等在这里插入图片描述

5.tabbar开发

1.实现思路
  1. 思路在这里插入图片描述在这里插入图片描述
  2. 效果:在这里插入图片描述
2.目录:

在这里插入图片描述

3.代码及思路
  1. assets文件夹里面一般放资源,可以把css和img放在这里面
  2. 通用样式写在app.vue里,在style动态引入文件,style里面引用前面要加@:@import "./assets/css/base.css";
  3. 把下面抽取成一个大的组件tabbar,里面放插槽
  4. tabbar组件里的小组件item里的文字和图片不能写死,所以要用插槽;且插槽外面要包装一层div,防止替换的时候替换掉属性
  5. 这种方法不行的原因在于插槽会被替换掉,则没有这个属性在这里插入图片描述
  6. 点击发生切换,在组件内部监听,用方法属性点击后改变路径,但是每个路径不一样不能写死,故通过props传进来,只需要字符串,不需要加:,变量则需要加:(动态传父组件的值)
  7. 如何判断处于活跃状态:计算属性,看能否找到活跃路由的path
  8. 我们希望动态传入活跃状态的颜色:动态绑定样式,在计算属性里判断处于活跃状态时拿到这个颜色
//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.页面小图标的修改

  1. 在public里修改

7.首页开发

7.1.首页导航栏的封装

  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>
  1. 注意:插槽如果要添加class属性,需要在外面套一层div
  2. 在home组件中使用这个组件在这里插入图片描述
  3. 设置首页中导航栏的背景颜色应该在home.vue中设置,因为每个views中的背景颜色不一定相同在这里插入图片描述

7.2.请求首页的多个数据

  1. 网络封装,在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)
}
  1. 首页可能需要用到很多次request请求,但和vue写在一起会很混乱,所以在network文件夹中再添加home.js的文件,封装所有对首页数据的请求,更加方便管理
import { request } from './request'

export function getHomeData() {
  return request({
    url:'/home/multidata'
  })
}
  1. 补充知识点:函数调用:压入函数栈(保存函数调用过程中所有变量)
    函数调用结束:弹出函数栈(释放函数所有的变量)
  2. 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.导出方式
  1. 在swiper文件夹下新增一个index.js的文件在这里插入图片描述
  2. 则导出的时候可以以对象的方式导出
    import {Swiper,SwiperItem} from 'components/common/swiper'
2.代码
  1. 我们习惯将轮播图在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.推荐信息的展示

  1. 主要代码在这里插入图片描述
  2. 数据的获取用props父传子

7.5.TabControl的封装

1.基本代码
  1. 如果只是文字不一样,则没有必要用插槽,直接用props传递数据即可
  2. 导入的时候公共的组件放一起,方法放一起,子组件放一起,按空格区分
  3. 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值
  1. 不建议用position:sticky,有些浏览器适配不好
  2. 通过判断它滚动的位置,大于offsetTop则将定位改为fixed
  3. 组件没有offsetTop属性,我们应该拿组件对应的元素,而所有的组件都有一个属性$el用于获取组件中的元素
  4. 如果直接在生命周期函数mounted中console.log(this.$refs.tabControl.$el.offsetTop);的结果是不准确的,挂载虽然意味着所有的组件都被挂载在上面,但是图片不一定全部加载完成,则没有包括图片的高度
  5. 我们应该等图片加载完成,拿到的offsetTop才是正确的
  6. views/home/childComps/HomeSwiper.vue中:在这里插入图片描述在这里插入图片描述
  7. Home.vue中在这里插入图片描述
    并且在data中保存数据tabOffsetTop: 0默认为0
    methods:在这里插入图片描述
动态改变样式
  1. 动态的改变tabcontrol的样式时,会出现两个问题:下面的商品内容会突然上移,tabcontrol虽然设置了fixed,但也会随着better-scroll滚出去,因为better-scroll内部有个translate,会使fixed属性失效,而在bscroll中不好操作,
  2. 用其他方案来解决,我们可以在最上面多复制一份tabcontrol组件对象,当用户滚动到一定位置时显示出来,当没有滚动到一定位置时隐藏起来
  3. Home.vue中:
    在这里插入图片描述
    并且在data中保存数据isTabFixed: false
    保证两个tab-control的高亮是相同的:
    在这里插入图片描述
    在这里插入图片描述

7.6.保存商品的数据结构设计

1.思路
  1. 点击流行的时候展示流行相关的数据,点击什么展示什么的数据,如果我们点击的时候再请求数据,则会给用户造成延迟
  2. 所以我们要弄一个变量,变量中存储着三个的数据请求
//goods用来保存数据
goods: {
//page用来记录现在加载的数据是第几页
'pop':{page: 1 ,list[]},
'news':{page: 1 ,list[]},
'sell":{page: 1 ,list[]}
}
  1. 这些都是首页的数据,写在home.vue的data里
2.首页数据的请求和保存
  1. 首先在network的home.js里定义方法在这里插入图片描述
  2. 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.首页商品数据的展示
  1. 在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点击切换商品
  1. 点击不同的按钮决定展示什么数据,通过自定义事件
  2. 在TabControl.vue中:在这里插入图片描述
    在home.vue中在这里插入图片描述
    使用计算属性的原因是因为太长了
    在这里插入图片描述在这里插入图片描述

7.7.回到顶部BackTop

1.代码
  1. 组件是无法直接监听原生事件的,必须加一个修饰符.native
  2. 封装一个组件显示回到顶部的图标,而方法则应在需要回到顶部的组件中使用,因为需要拿到数据
  3. Home.vue中:给scroll设置ref在这里插入图片描述在这里插入图片描述
  4. 必须要给所在的content设置一个高度
2.BackTop的显示和隐藏
  1. 滚动了一定距离才显示,否则隐藏起来
  2. 因为有些时候并不需要监听滚动,所以不能写死,应该设置props
  3. scroll.vue中:在这里插入图片描述 4. Home.vue中:在这里插入图片描述
    用一个变量来保存是否显示backtop,默认是不显示在这里插入图片描述在这里插入图片描述

7.8.解决滚动区域bug

  1. better-scroll在决定有多少区域可以滚动时,是根据scrollerHeight属性决定的,而scrollerHeight是根据放better-scroll中的子组件的高度,但我们的首页在刚开始计算该属性时,没有将图片计算在内
  2. 如何解决:监听每一张图片是否加载完成,只要有一张图片加载完成,执行一次refresh()
  3. 如何监听图片加载完成:
    原生的js监听图片:img.onload=function() {}
    vue中监听:@load=“方法”
  4. 如何将GoodsListItem.vue这个中的事件传入到Home.vue中:因为涉及到非父子组件的通信,所以我们选择了事件总线
Vue.prototype.$bus = new Vue()
this.$bus.$emit('事件名称',参数)
this.$bus.$on('事件名称',回调函数(参数))
  1. 先在main.js里:因为默认情况下$bus是没有值的,所以要创建vue实例在这里插入图片描述GoodsListItem.vue:在这里插入图片描述
    scroll.vue中:在这里插入图片描述
    Home.vue中:
    不能写在created里,因为还没挂载,this.refs可能拿到的是undefined
    为什么要加this.scroll短路条件:因为可能图片加载完成后,scroll对象可能并没有初始化
    在这里插入图片描述

7.9刷新频繁的防抖函数处理

  1. 防抖函数:比如说在输入框输入时,每输入一个字符发送一次请求,会对服务器造成很大压力;我们可以等一定的时间,有改变再发送请求
  2. 防抖函数起的作用:可以将refresh函数传入到debouce函数中,生成一个新的函数,之后在调用非常频繁的时候,就使用新生成的函数,而新生成的函数,并不会非常频繁的调用,如果下一次执行来得非常快,那么会将上一次取消掉
  3. 封装函数:
//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上拉加载更多

  1. 监听什么时候滚动到顶部

  2. scroll.vue中:因为不是每一个都需要上拉加载更多,所以用props记录是否需要在这里插入图片描述
    在这里插入图片描述

  3. Home.vue中的使用:在这里插入图片描述在这里插入图片描述
    注意:若想下一次上拉也能加载到数据,必须调用scroll.vue中的finishPullUp;因为scroll默认加载一次在这里插入图片描述

7.11.Home离开时记录状态和位置

  1. 让Home不要随意销毁掉:在router-view外面套一层keep-alive在这里插入图片描述
  2. 让Home的内容保持原来的位置:离开时记录位置信息saveY,进来时将位置设置为saveY
    scroll.vue中的方法:在这里插入图片描述
    Home.vue中在data里定义一个变量saveY保存数据在这里插入图片描述

8.Better-scroll

1.安装和使用
  1. 移动端的原生滚动会非常卡顿,而iscroll框架已经不更新了,我们可以使用better-scroll框架
  2. 安装框架npm install better-scroll --save
  3. 使用方法:不能直接传content,必须在外面包装一层div在这里插入图片描述
    在这里插入图片描述
  4. 官网:https://better-scroll.github.io/docs/zh-CN/guide/
2.基本使用解析
  1. 默认情况下,better-scroll不可以实时的监听滚动位置,要想监听,必须在newBsroll后面传参数
  2. 在基本程序中的写法
    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

  1. 在首页的商品详情中点击时跳转到对应的详情页
  2. 监听GoodsListItem每个组件的点击,并跳转到对应的详情页,给详情页配置路由
    配置路由:在这里插入图片描述
    GoodsListItem.vue中:在这里插入图片描述
  3. Detail.vue中:如何知道跳转的iid是什么,注意这里获取iid一定是$route在这里插入图片描述

9.2.导航栏的封装

  1. 因为详情页的导航栏有点点复杂,所以可以在detail文件夹下创一个childComps的文件夹,里面放DetailNavBar.vue
  2. 代码注意点:
    ①导航栏中间的文字内容是保存在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.数据请求及轮播图展示

  1. 在network文件夹下添加detail.js
import { request } from './request'

export function getDetail(iid) {
  return request({
    url: '/detail',
    params: {
      iid
    }
  })
}
  1. 如何在Detail.vue中请求轮播图数据在这里插入图片描述
  2. 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>
  1. 遇到的问题:每次点击不同的详情页显示的图片确实一样的,问题出在keep-alive是写在App.vue里的,所有的数据都会被保存在这里插入图片描述

9.4.店铺信息的解析和展示

  1. 在给组件传数据时,应把数据整合好,即数据虽然很多很杂,但可以整合成一个对象
  2. 如何整合成一个对象,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
  }
}
  1. Detail.vue中:import {getDetail, Goods} from 'network/detail.js',并在data中存储数据goods:{}在这里插入图片描述

  2. v-for="index in nums"如果in后面是一个数字,则遍历的是从1到nums

  3. 如何判断一个对象是否为空对象:

const obj ={}
//判断方法:
Object.keys(obj).length //===0即为空对象
  1. 注意细节:父组件和子组件中储存对象都应该用{}

9.5.加入滚动的效果

  1. 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
    }
  }
}
  1. Detail.vue中:

9.6.商品评论信息的展示

  1. 服务器返回时间基本都返回的是时间戳
  2. 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;
}
  1. 如何将时间戳转成时间格式化字符串
<script>
//1.将时间戳转成Date对象
//时间戳是秒为单位,我们要拿到毫秒为单位,所以乘以1000
const date = new Date(value*1000)
//2.将date进行格式化,转成对应的字符串
</script>
  1. yyyy代表年份占四位,也可以写yy则默认取年份后两位,-符号代表返回时年月日用-分隔,还可以写hh小时(h为12小时制,H为24小时制),m为分钟,s为秒在这里插入图片描述

9.7. 首页和详情页监听全局事件和mixin的使用

  1. 在推荐模块我们用的是goodsListItem.vue组件,会在所有图片加载完成后通知Home使其刷新,并不合理,我们可以如何区分:通过路由做个判断

  2. 通过路由做判断:goodsListItem.vue里:在这里插入图片描述

  3. 方法二:Home.vue中:
    data里存储:在这里插入图片描述
    在这里插入图片描述

  4. 如果在详情页里也想监听:Detail.vue里在这里插入图片描述

  5. Home.vue和Detail.vue中的mounted里都有一些公共的代码,如何抽取:应用mixin(混入),这里不能用继承,因为继承是减少类里面的重复代码,而我们这里是两个对象(exprot default)
    common/mixin.js:
    在这里插入图片描述

  6. 使用:在这里插入图片描述

9.8.点击详情页标题滚到对应内容

1.点击标题滚到对应的主题内容
  1. 重点是获取每个内容的offsetTop
  2. 获取offsetTop不能写在mounted里:拿到的this.$refs.recommends.$el.offsetTopundefined,即意味着没有$el,原因是在子组件里我们用了v-if="Object.keys(detailInfo).length !== 0"只有在有值的时候才渲染,意味着在没有请求过来数据时,不能保证mounted里数据一定请求完成,子组件可能没有加载完成
  3. 写在getDetail里也是错误的,因为虽然有值,但是还没有渲染完成在这里插入图片描述
  4. this.$nextTick函数,需要传入回调函数,会等前面的代码渲染完成,回调该函数,在getDetail函数里:注意这里的值不对,因为这里offsetTop是不包括图片的在这里插入图片描述
  5. 但是上面这样写当进入其他商品的详情页时,跳转的位置不对:因为上面那样写仅仅是将dom渲染出来了,图片还没加载完成
    经验:offsetTop的值不对的时候,基本是因为图片的问题
  6. 正确做法:在图片加载完成后,获取的高度才是正确的
    data里存储数据:getThemeTopY: null在这里插入图片描述
    methods里图片记加载完成后去调用函数在这里插入图片描述
2.滚动内容显示对应标题
  1. 代码:给navbar设置了属性ref=“nav”
    在这里插入图片描述
3.对复杂判断条件分析和优化
  1. 上面的this.currentIndex !== i是为了防止赋值的过程过于频繁,后面的条件是分了两种情况(防止下标越界)
  2. 优化: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

  1. 和Home.vue中共同代码很多,可以做个抽取,用mixin
  2. 如果是生命周期函数,即可在mixin里写也可以在组件里写,即可以同时写,而methods里的函数不行会覆盖,抽取时需注意

10.购物车

10.1.将商品添加到购物车

  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中代码的重构

  1. vuex中代码的重构:mutations唯一的目的就是修改state中的状态,尽可能保证它的每个方法完成的事情比较单一
    异步操作、判断逻辑放在actions中
  2. 代码:
    mutation-type.js:在这里插入图片描述
    mutations.js:尽可能使每个方法做的事情单一在这里插入图片描述
    actions.js:将逻辑判断放在这里在这里插入图片描述

index.js:在这里插入图片描述

10.3.导航栏实现

1.代码
  1. 代码:getters.js在这里插入图片描述
    Cart.vue:在这里插入图片描述
2.getters映射
  1. vuex知识点:getters实现映射如何直接把getters里的当成计算属性来使用在这里插入图片描述

10.4.购物车列表的item展示

  1. 如何传数据,用props在这里插入图片描述

10.5.购物车按钮

1.基本封装
  1. 封装成checkButton组件在这里插入图片描述
2.item选中和不选中的切换
  1. item的选中不选中应有个东西来记录,不能用属性记录,一定是在对象模型里记录
  2. store/mutations.js:在这里插入图片描述
  3. cartListItem.vue:在这里插入图片描述
    在这里插入图片描述

10.6.底部总价及选中汇总工具栏

  1. 模板及效果图在这里插入图片描述
  2. 如何计算价格和商品数目:计算属性
    计算价格:选中的商品将数目乘以单价在这里插入图片描述

10.7.全选按钮

1.状态显示
  1. 判断是否有一个不选中,全选则不选中
  2. 回顾:子组件checkButton.vue中用props接收数据在这里插入图片描述
  3. cartBottom.vue:在这里插入图片描述
    在这里插入图片描述
2.点击效果
  1. 点击全选按钮全部选中,取消则全部商品取消
  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.代码
  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映射
  1. 如何实现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.普通方式封装
  1. 需求:当点击加入购物车时,显示一个弹窗,这个弹窗称为toast
  2. 代码:
//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>
  1. 使用方法:保存isShow和message的数据在这里插入图片描述
  2. 但是这种方法使用起来过于麻烦
2.插件方式封装
  1. 如何封装
//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)

  1. 如何使用:任何地方使用都只需要this.$toast.show(res,2000)在这里插入图片描述

12.fastclick

  1. 作用:解决移动端300ms延迟
  2. 使用:
    安装:npm install fastclick --save
    导入:import FastClick from 'fastclick'(main.js)
    调用attach函数:FastClick.attach(document.body)(main.js)

13.图片懒加载:vue-lazyload框架

  1. 使用:
    安装:npm install vue-lazyload --save
    导入:import VueLazyLoad from 'vue-lazyload'(main.js)
    使用懒加载插件:Vue.use(VueLazyLoad)(main.js)
    修改img:将:src=" "改成v-lazy=" "
  2. 使用懒加载时后面还可以传入options在这里插入图片描述

14.px2vw:css单位转化插件

  1. 在不改代码的情况下把所有单位改成vw
  2. 安装:npm install postcss-px-to-viewport --save-dev
  3. 配置: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中部署

  1. windows中部署:windwos安装nginx,将项目进行打包部署
  2. 在这里插入图片描述在这里插入图片描述
    把项目的dist文件夹拷贝一份放到安装根目录下

2.远程Linux部署

  1. centos安装nginx,上传打包项目部署
  2. 远程部署在这里插入图片描述

16.响应式原理

  1. 响应式图解
    每个属性都有dep对象在这里插入图片描述
    解析过程:在这里插入图片描述

  2. 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>
  1. 当数据发生改变,Vue是如何知道要通知哪些人,界面发生刷新:
    发布订阅者模式
  2. 结合vue官网文档
Logo

前往低代码交流专区

更多推荐