尚品汇---笔记与思考

day1

1.1 项目结构介绍

node_modules文件夹:项目依赖文件夹

public文件夹:一般放置一些静态资源(图片),需要注意的是:放在public文件夹中的静态资源,webpack进行打包的时候会原封不动的打包到dist文件夹中

src文件夹(程序员源代码文件夹):

        assets文件夹:一般也是放置静态资源(一般放置多个组件共用的静态资源),需要注意:放置在assets文件夹里面的静态资源,在webpack打包的时候,webpack会把静态资源当做一个模块,打包到JS文件中

        components文件夹:一般放置的是非路由组件(全局组件)

        App.vue:唯一的根组件,Vue当中的组件(.vue)

babel.config.js:配置文件(babel相关)

package.json文件:认为是项目的“身份证”,记录项目叫做什么、项目中有哪些依赖、项目怎么运行

package-lock.json:缓存性文件

README.md:说明性文件

1.2 项目配置

项目运行起来的时候,让浏览器自动打开:

找到package.json文件

修改scripts配置项的serve命令(添加--open)

  "scripts": {
    "serve": "vue-cli-service serve --open",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  }

关闭eslint校验:

        添加下列语句即可

  lintOnSave:false

src文件夹简写方法:配置别名 @(在jsconfig.json文件夹中)

webstorm里自带下列代码,有需要的朋友可以复制一下放到jsonconfig.json中

@代表的是src文件夹,这样将来即使文件过多,找的时候也很方便

  "compilerOptions": {
    "target": "es5",
    "module": "esnext",
    "baseUrl": "./",
    "moduleResolution": "node",
    "paths": {
      "@/*": [
        "src/*"
      ]
    }

1.3 路由分析

项目路由的分析:vue-router

前端所谓路由:KV键值对

key:URL(地址栏中的路径)

value:相应的路由组件

项目为上中下结构

路由组件:Home首页路由组件、Search路由组件、Login登录组件、Register注册组件

非路由组件:Header头部组件(首页、搜索页)、Footer尾部组件(在首页、搜索页有,但是在登录页面是没有的)

开发项目的顺序:

        1.书写静态页面(HTML+CSS)

        2.拆分组件

        3.获取服务器的数据动态展示

        4.完成相应的动态业务逻辑

路由组件的搭建

下载vue-router:注意vue2对应vue-router@3,vue3对应vue-router@4

由上述分析,路由组件应该有四个:Home、Search、Login、Register

components文件夹:经常放置的是非路由组件(共用全局组件)

pages|views文件夹:经常放置路由组件

        配置路由:项目中配置的路由一般放置在router文件夹中

总结:

路由组件与非路由组件的区别:

        1.路由组件一般放置在pages|views文件夹下,非路由组件一般放置在components文件夹中

        2.路由组件一般需要在router文件夹中进行注册(使用的即为组件的名字),非路由组件在使用的时候,一般都是以标签的形式使用

        3.注册完路由,不管路由组件、还是非路由组件,身上都有$route、$router属性

        $route:一般用于获取路由信息(路径、query、params等等)

        $router:一般进行编程式导航,进行路由跳转(push、replace)

路由的跳转:

        路由的跳转有两种形式:

                声明式导航router-link,可以进行路由的跳转

                编程式导航push|replace,可以进行路由跳转

        编程式导航:

                声明式导航能做的,编程式导航都能做

                但是编程式导航除了可以进行路由跳转,还可以做一些其他的业务处理

    //搜索按钮的回调函数,需要向search路由进行跳转
    goSearch(){
      this.$router.push('/search')
    }

        声明式导航:

登录

1.4 Footer组件的显示与隐藏

显示或者隐藏组件:v-if|v-show

Footer组件:在Home、Search显示Footer组件

Footer组件:在登录、注册的时候隐藏

  

我们可以根据组件身上的$route获取当前路由信息,通过路由路径判断Footer的显示与隐藏

在配置路由的时候,可以给路由添加路由元信息[meta]

    routes:[
        {
            path:'/home',
            component:Home,
            meta:{show:true}
        },
        {
            path:"/search",
            component:Search,
            meta:{show:true}
        },
        {
            path:"/login",
            component:Login,
            meta:{show:false}
        },
        {
            path:"/register",
            component:Register,
            meta:{show:false}
        },
        //重定向:在项目跑起来的时候,访问"/",立马让它定向到首页
        {
            path:'*',
            redirect:"/home"
        }
    ]
  

1.5 路由传参

路由跳转有几种方式?

比如:A->B

声明式导航:router-link(务必要有to属性),可以实现路由的跳转

编程式导航:利用的是组件实例的$router.push|$router.replace方法,可以实现路由的跳转(可以书写一些自己的业务)

路由传参,参数有几种写法?

params参数:属于路径当中的一部分。需要注意,在配置路由的时候,需要占位

        {
            path:"/search/:keyword",
            component:Search,
            meta:{show:true}
        }

query参数:不属于路径当中的一部分,类似于ajax中的queryString   /home?k=v&k=v,不需要占位

路由传递参数:

字符串形式:

      this.$router.push('/search/'+this.keyword+"?k="+this.keyword.toUpperCase())

模板字符串形式:

      this.$router.push(`/search/${this.keyword}?k=${this.keyword.toUpperCase()}`)

对象的形式:

注意:使用对象的形式时,需要给要跳转的路由命名,而不能使用路径来跳转

      this.$router.push({
        name:'search',
        params:{
          keyword:this.keyword
        },
        query:{
          k:this.keyword.toUpperCase()
        }
      })

1.6 面试题

面试题1:路由传递参数(对象写法)path是否可以结合params参数一起使用?

不能,路由跳转传参的时候,对象的写法可以是name、path形式,但需要注意的是,path这种写法不能和params参数一起写

面试题2:如何指定params参数可传可不传?

如果路由要求传递params参数,但你不传,那么url会出现问题

如果想要指定params参数可传可不传,需要在配置路由的时候在占位的后面加上一个问号

        {
            name:'search',
            path:"/search/:keyword?",
            component:Search,
            meta:{show:true}
        }

面试题3:params参数可以传递也可以不传递,但如果传递的是空串,如何解决?

使用undefined解决:params参数可以传递、不传递或传递空的字符串

      this.$router.push({
        name:'search',
        params:{keyword:""||undefined},
        query:{k:this.keyword.toUpperCase()}
      })

面试题4:路由组件能不能传递props数据?

布尔值写法:只能传递params参数

        props:true

对象写法:额外的给路由组件传递一些props

        props:{a:1,b:2}

函数写法:可以params参数、query参数,通过props传递给路由组件

        props:($route)=>{

                return {keyword:$route.params.keyword,k:$route.query.k}

        }

day2

2.1 重写push和replace方法

编程式路由跳转到当前路由(参数不变),多次执行会抛出NavigationDuplicated的警告错误

 首先,底层push方法会返回一个Promise对象

function push(){
    return new Promise((resolve,reject)=>{

    })
}

解决方法:

1.通过给push方法传递相应的成功、失败的回调函数,可以捕获到当前的错误并解决

这种写法治标不治本,将来在别的组件当中push|replace,编程式导航还有类似错误

      this.$router.push({
        name:'search',
        params:{keyword:this.keyword},
        query:{k:this.keyword.toUpperCase()},
      },()=>{},()=>{})
   

2.

this:当前组件实例(search)

this.$router属性:当前的这个属性,属性值VueRouter类的一个实例,当在入口文件注册路由的时候,给组件实例添加$router|$route属性

      function VueRouter(){

      }
      //原型对象的方法
      VueRouter.prototype.push=function(){
        //函数的上下文为VueRouter类的一个实例
      }
      let $router=new VueRouter()
      $router.push(xxx)

重写方法:

//先把VueRouter原型对象的push保存一份
let originPush=VueRouter.prototype.push

//重写push|replace
//第一个参数:告诉原来的push方法,你往哪里跳转(传递哪些参数
VueRouter.prototype.push=function(location,resolve,reject){
    if(resolve&&reject){
        //call|apply区别
        //相同点:都可以调用函数依次,都可以篡改函数的上下文一次
        //不同点:call与apply传递参数:call传递参数用逗号隔开,apply方法执行传递数组
        originPush.call(this,location,resolve,reject)
    }else{
        originPush.call(this,location,()=>{},()=>{})
    }
}

2.2 三级联动组件

由于三级联动,在Home、Search、Detail中都有使用,所以把三级联动注册为全局组件

好处:只需要注册一次,就可以在项目的任何地方使用

全局组件的注册:

        第一个参数:全局组件的名字

        第二个参数:指定是哪一个组件

Vue.component(TypeNav.name,TypeNav)

2.3 axios的二次封装

为什么要对axios进行二次封装?

请求拦截器、响应拦截器:请求拦截器,可以在发请求之前处理一些业务

                                           响应拦截器,当服务器数据返回以后,可以处理一些事情

在项目中API文件夹常用来放置axios相应文件

接口当中,路径都带有/api

        ==>baseURL:"/api"

import axios from "axios";

const requests = axios.create({
    baseURL:"/api",
    timeout:5000
})

//请求拦截器
requests.interceptors.request.use((config)=>{
    //config是headers的请求头

    return config
})

//响应拦截器
requests.interceptors.response.use((res)=>{
    //成功的回调函数:服务器响应数据回来以后,响应拦截器可以检测到,并做一些处理
    return res.data
    },
    (error)=>{
        //响应失败的回调函数
        return Promise.reject(new Error(error))
    })


export default requests

2.4 跨域问题

什么是跨域?

        协议、域名、端口号不同的请求,称之为跨域

http://localhost:8080/#/home        ---前端项目本地服务器

http://39.98.123.211        ---后台服务器

  //配置代理跨域
  devServer:{
    proxy:{
      "/api":{
        target:"http://gmall-h5-api.atguigu.cn",
      }
    }
  }

2.5 nprogress进度条的使用

//引入进度条
import nprogress from 'nprogress'
//引入进度条的样式
import 'nprogress/nprogress.css'

进度条的开始:

    nprogress.start()

进度条的结束:

    nprogress.end()

2.6 vuex状态管理库

vuex是什么?

        vuex是官方提供的一个插件,状态管理库,集中式管理项目中共用的数据

2.7 mapState

mapState传入参数是一个对象,对象的键值对中,值是一个函数。当时用这个计算属性的时候,右侧的函数会立即执行一次

函数注入一个参数为state,即为大仓库中的数据

    ...mapState({
      categoryList:(state)=>{
        return state.typeNav.categoryList
      }
    })

2.8 三级联动动态背景颜色改变

动态添加类:

          

currentIndex的改变,初始值为-1:

    changeIndex(index){
      this.currentIndex=index
    },
    leaveIndex(){
      this.currentIndex=-1
    }

类:

        .cur{
          background-color: skyblue;
        }

2.9 通过JS控制二三级分类的显示与隐藏

            

2.10 演示卡顿现象并引入防抖和节流

卡顿现象:事情触发非常频繁,而且每一次的触发,回调函数都要去执行(如果时间很短,而回调函数内部有计算,那么很有可能出现浏览器卡顿)

函数的节流与防抖:

节流:在规定的间隔时间范围内不会重复处罚回调,只有大于这个时间才会触发回调,把频繁触发变为少量触发

防抖:前面所有的触发都被取消,最后一次执行在规定的时间之后才会触发,也就是说如果连续快速的触发,则只会执行一次

插件lodash:封装了函数的防抖与节流业务

三级联动节流:

引入throttle函数:

import throttle from 'lodash/throttle'

使用节流函数throttle:

    changeIndex:throttle(function(index){
      this.currentIndex=index
    },50),

day3

3.1 三级联动组件的路由跳转与参数传递

三级联动用户可以点击:一级分类、二级分类、三级分类

当你点击的时候,Home模块跳转到Search模块,一级会把用户选中的产品(产品的名字、产品的ID)在路由跳转的时候,进行传递

router-link是一个组件,当服务器的数据返回值后,会循环出很多的router-link组件实例

创建组件实例的时候,一瞬间创建1000+组件实例是很耗用内存的,因此可能出现卡顿现象

最好的解决方案:编程式导航+事件委派 实现路由的跳转和参数的传递

利用事件委派存在的一些问题:

        1.点击的不一定是a标签

                事件委派是把所有的子节点(h3/dt/dl/em)的时间委派给父亲结点。而只有点击a标签的时候,才会进行路由跳转,但如何确定点击的一定是a标签呢?

                即使你能确定点击的是a标签,如何区分是一级、二级还是三级分类的a标签呢?

        2.如何获取参数【1/2/3级分类的产品名字、id】

解决方案:

        1.把子节点当中的a标签加上自定义属性data-categoryName,而其他的子节点是没有的

{{c1.categoryName}}

        2.给每一个a标签添加上对应的data-category1Id/data-category2Id/data-category3Id,用来区分不同级别的分类

总的解决方案:

goSearch(event){
      let {categoryname,category1id,category2id,category3id} = event.target.dataset
      if(categoryname){
        //整理路由跳转的参数
        let location={name:'search'}
        let query={categoryName:categoryname}
        if(category1id){
          query.categoryName=category1id
        }else if(category2id){
          query.categoryName=category2id
        }else{
          query.categoryName=category3id
        }
        //整理参数
        location.query=query
        //路由跳转
        this.$router.push(location)
      }
    }

day4

4.1 三级联动效果在不同路由组件中的显示情况

需求:

在home组件下,会默认显示二级菜单和三级菜单

但search组件下,只默认显示一级菜单

解决方案:

通过v-show动态控制二级菜单和三级菜单的显示与隐藏(v-show="show")

通过路由控制,决定show属性的true/false

        原理:每跳转到一个组件,如果有TypeNav,都会重新创建一个TypeNav的实例,也就是说mounted函数会再次执行。可以将路由判断的逻辑写在mounted钩子里

  mounted(){
    //通知Vuex发送请求,获取数据,存储于仓库之中
    this.$store.dispatch('categoryList')
    if(this.$route.path!='/home'){
      this.show=false
    }
  }

在鼠标滑过“全部商品分类”时,应该有对应的二级分类、三级分类的展示

        对于home组件,鼠标离开时二级分类不会消失

        对于search组件,鼠标离开时只展示一级分类

逻辑判断:

TypeNav模块:

      
    enterShow(){
      this.show=true
    },
    leaveShow(){
      this.currentIndex=-1
      if(this.$route.path!='/home'){
        this.show=false
      }
    }

4.2 search模块中三级路由组件的过渡效果

过渡动画:前提是组件/元素务必要有v-if/v-show指令,才可以进行过渡动画

        
        
          
……中间内容省略
    //过渡动画的样式
    .sort-enter{
      height:0;
    }
    .sort-enter-to{
      height:461px;
    }
    .sort-enter-active{
      transition:all .7s linear
    }

4.3 TypeNav商品分类列表的优化

由于每次home/search组件的销毁和挂载都会导致TypeNav的销毁和挂载,即都会执行mounted函数,从而频繁地向服务器发送请求。但是三级联动组件获取到的内容是不变的,所以可以做优化

可以将三级联动的数据请求放在App.vue的钩子函数中:

  mounted(){
    //通知Vuex发送请求,获取数据,存储于仓库之中
    this.$store.dispatch('categoryList')
  }

4.4 合并参数

捎带params参数:

        //判断:如果路由跳转的时候带有params参数,也要捎带传递过去
        if(this.$route.params){
          location.params=this.$route.params
        }

合并参数:

    //搜索按钮的回调函数,需要向search路由进行跳转
    goSearch(){
      let location={
        name:'search',
        params:{keyword:this.keyword||undefined}
      }
      if(this.$route.query){
        location.query=this.$route.query
      }
      this.$router.push(location)
    }

4.5 mockjs模拟数据

开发Home首页当中的ListContainer组件与Floor组件:

        因为服务器返回的数据只有商品分类和菜单分类的数据,对于ListContainer组件与Floor组件数据服务器是没有提供的。可以通过mockjs模拟

使用步骤:

1)在项目当中src文件夹中创建mock文件夹

2)第二步准备JSON数据(mock文件夹中创建相应的JSON文件)

3)把mock数据需要的图片放置到public文件夹中(public文件夹在打包的时候,会把相应资源原封不动地放在dist文件夹下)

4)创建mockServer.js,开始实现mock虚拟数据

5)把mockServer.js文件在入口文件中引入(至少需要执行一次,才能模拟数据)

问:JSON数据格式文件根本没有对外暴露,为什么可以直接引入?

答:webpack默认对外暴露图片和JSON数据格式文件

Mock.mock()函数有两个参数

        第一个参数是请求的地址

        第二个参数是请求的数据

//先引入mockjs模板
import Mock from 'mockjs'
//把JSON数据格式引入进来
import banner from './banner.json'
import floor from './floor.json'

Mock.mock("/mock/banner",{
    code:200,
    data:banner
})
Mock.mock("/mock/floor",{
    code:200,
    data:floor
})

封装mockServer.js

import axios from 'axios'
import nprogress from 'nprogress'
import 'nprogress/nprogress.css'

let requests=axios.create({
    baseURL:"/mock",
    timeout:5000
})

requests.interceptors.request.use((config)=>{
    nprogress.start()
    return config
})

requests.interceptors.response.use(
    (res)=>{
        nprogress.done()
        return res.data
    },
    (err)=>{
        return Promise.reject(new Error(err))
    }
)

export default requests

发送请求的函数:

//获取banner(Home首页轮播图接口)
export const reqGetBannerList=()=>mockRequest.get('/banner')
//获取floor
export const reqGetFloor=()=>mockRequest.get('/floor')

获取banner轮播图的数据:

vuex处理:

import { reqGetBannerList} from "@/api";

const state={
    bannerList:[]
}
const mutations={
    GETBANNERLIST(state,bannerList){
        state.bannerList=bannerList
    }
}
const actions={
    async getBannerList({commit}){
       let result=await reqGetBannerList()
        if(result.code===200){
            commit('GETBANNERLIST')
        }
    }
}
const getters={}

export default{
    state,mutations,actions,getters
}

获取数据:

  computed:{
    ...mapState({
      bannerList:state=>state.home.bannerList
    })
  }

4.6 swiper

第一步:引包(引入相应的JS/CSS)

在main.js引入一次即可:

import 'swiper/css/swiper.css'
import 'swiper/js/swiper'

第二步:页面中的结构

            

第三步:new Swiper实例,给轮播图添加动态效果

          new Swiper(document.querySelector('.swiper-container'),{
            loop:true,
            pagination:{
              el:'.swiper-pagination',
              //点击小球的时候也切换图片
              clickable:true
            },
            //前进后退的按钮
            navigation:{
              nextEl:".swiper-button-next",
              prevEl:".swiper-button-prev"
            }
          })

安装swiper5版本比较稳定

4.7 Banner轮播图的实现

首先需要明确,如果直接将swiper实例写在mounted函数中,尽管mounted是在页面结构加载完成后执行,但此时从服务器拿到的数据还没有放到仓库中,故swiper实例中拿不到bannerList的数据

解决方式一:

可以通过setTimeout等待一段时间,这段时间里从服务器拿到的数据就已经放到仓库中了

    setTimeout({
      //swiper实例的实现
    },2000)

解决方式二:

watch+nextTick

watch:数据监听,监听已有数据的变化

        监听bannerList数据的变化:从空数组变为数组里有四个元素

        如果执行handler方法,代表组件实例身上的这个属性的属性值已经有了

但是仅有watch是不够的,因为数据更新了,但是页面渲染可能还没结束

nextTick:

        Vue.nextTick([callback,context])

        用法:在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立刻使用这个方法,获取更新后的DOM

        $nextTick可以保证页面中的结构一定是有的,经常和很多需要DOM已经存在才能实现功能的插件一起使用

watch+nextTick实现功能:

  watch:{
    bannerList:{
      handler(newValue,oldValue){
        this.$nextTick(()=>{
          var mySwiper=new Swiper(document.querySelector('.swiper-container'),{
            loop:true,
            pagination:{
              el:'.swiper-pagination',
              //点击小球的时候也切换图片
              clickable:true
            },
            //前进后退的按钮
            navigation:{
              nextEl:".swiper-button-next",
              prevEl:".swiper-button-prev"
            }
          })
        })
      }
    }
  }

4.8 通过ref获取要操作的DOM

        
          new Swiper(this.$refs.mySwiper,{

day5

5.1 开发floor组件

由于Floor组件在Home中被调用两次,如果是在Floor组件中触发getFloorList,很难实现两次调用数据不同

        故应该在Home组件中触发

v-for可以在自定义组件上使用

组件间通信的方式有哪些?

props:用于父子组件通信

自定义事件:@on 与 @emit

全局事件总线:$bus 全能

pubsub-js:vue中几乎不用(react使用的较多)

插槽

vuex

List组件中的轮播图,是当前组件内部发送请求、动态渲染解构服务器返回的数据,因此必须使用watch+nextTick

Floor组件中的轮播图,请求由父组件Home发送,并且数据是父组件发送过来的,此时结构已经完全解析好了,所以可以在mounted里写轮播图实例

  mounted(){
    new Swiper(this.$refs.cur,{
      loop:true,
      pagination:{
        el:'.swiper-pagination',
        //点击小球的时候也切换图片
        clickable:true
      },
      //前进后退的按钮
      navigation:{
        nextEl:".swiper-button-next",
        prevEl:".swiper-button-prev"
      }
    })
  }

5.2 共用组件Carousel

切记:以后在开发项目的时候,如果看到某一个组件在很多地方都使用,可以把它变成全局组件





5.3 search仓库(主讲getters)

getters,相当于计算属性

项目中getters的主要作用:简化仓库中的数据(简化数据而生)

可以把我们将来在组件当中需要用的数据简化一下【将来组件获取数据就简单多了】

数据的返回:

以goodsList为例,如果服务器数据回来了,返回一个数组

如果网络不给力/没有网,state.searchList.goodsList返回的就是undefined

const getters={
    goodsList(state){
        return state.searchList.goodsList||[]
    },
    trademarkList(state){
        return state.searchList.trademarkList||[]
    },
    attrsList(state){
        return state.searchList.attrsList||[]
    }
}

与getters对应的是mapGetters,此函数接收一个数组,不像state那样划分模块

  computed:{
    ...mapGetters(['goodsList'])
  }

5.4 Search中的数据处理

使用Object.assign方法,快速合并数据

在beforeMount里整理数据,在mounted发送请求

export default {
  name: 'Search',

  components: {
    SearchSelector
  },

  data(){
    return {
      //带给服务器的参数
      searchParams:{
        category1Id:"",
        category2Id:"",
        category3Id:"",
        categoryName:"",
        keyword:"",
        order:"",
        pageNo:1,
        pageSize:3,
        props:[],
        trademark:""
      }
    }
  },

  beforeMount(){
    Object.assign(this.searchParams,this.$route.query,this.$route.params)
  },

  mounted(){
    this.getData()
  },

  methods:{
    getData(){
      this.$store.dispatch('getSearchList',this.searchParams)

    }
  },

  computed:{
    ...mapGetters(['goodsList'])
  }

}

5.5 监听路由变化再次发送请求数据

当点击三级联动组件或者搜索框时,路径中的query或params会发生改变。那么路由就会改变,监听路由即可。

每一次请求完毕,应该把相应的1/2/3级分类的id只看,让它接收下一次相应的1/2/3级分类

分类名字和关键字不用清理:因为每一次路由发生变化,都会赋予它新的数据


           
  • {{searchParams.categoryName}} x
  •   watch:{
        $route(newValue,oldValue){
          Object.assign(this.searchParams,this.$route.query,this.$route.params)
          this.getData()
          this.searchParams.category1Id=''
          this.searchParams.category2Id=''
          this.searchParams.category3Id=''
        }
      }

    5.6 动态删除面包屑

    即使带给服务器的参数是为空的字符串,仍然会把相应的字段带给服务器

    但如果把相应的字段变为undefined,当前这个字段就不会带给服务器了(性能优化)

    骚操作:

            对自己所在组件进行路由跳转,可以清除地址栏的指定参数(比如query参数)

        //删除分类的名字
        removeCategoryName(){
          //把带给服务器的参数置空,并给服务器发请求
          this.searchParams.categoryName=undefined
          this.searchParams.category1Id=undefined
          this.searchParams.category2Id=undefined
          this.searchParams.category3Id=undefined
          this.getData()
          this.$router.push({name:'search',params:this.$route.params})
        }

    5.6 动态开发面包屑中的分类名

    
              
  • {{searchParams.keyword}} x
  •     removeQueryName(){
          this.searchParams.keyword=undefined
          this.getData()
    
        }

    5.7 动态开发面包屑中的关键字

    当面包屑中的关键字清除以后,需要让兄弟组件Header组件中的关键字清除

    设计组件间通信:

    props:父子

    自定义事件:子父

    vuex:仓库数据

    插槽:父子

    $bus:全局事件总线

    注册全局事件总线:

    new Vue({
      render: h => h(App),
      beforeCreate(){
        Vue.prototype.$bus=this
      },
      router,
      store,
    }).$mount('#app')

    在Search组件中触发事件:

          this.$bus.$emit('clear')
    

    在Header组件中绑定事件:

      mounted(){
        this.$bus.$on("clear",()=>{
          this.keyword=""
        })
      }

    更新params参数:(Search组件)

          this.$router.push({name:'search',query:this.$route.query})
    

    5.8 面包屑处理品牌信息

    在Search组件中展示品牌的面包屑:

    注意trademark.tmName的展示方式

    
                
  • {{searchParams.trademark.split(":")[1]}} x
  • 子父组件间通信:

    父传子自定义事件:

            
            
    

    由于传递过来的trademark是个对象,而传递给服务器的trademark是个字符串,所以要进行字符串拼接

        trademarkInfo(trademark){
          console.log(trademark)
          this.searchParams.trademark=`${trademark.tmId}:${trademark.tmName}`
          this.getData()
        }
    
    

    子触发父中的函数:

        trademarkHandler(trademark){
          this.$emit('trademarkInfo',trademark)
        }

    5.9 平台售卖属性的操作

    父组件:

    
                
  • {{attrValue.split(":")[1]}} x
  •     attrInfo(attr,attrValue){
          let props=`${attr.attrId}:${attrValue}:${attr.attrName}`
          //数组去重
          if(this.searchParams.props.indexOf(props)===-1)
            this.searchParams.props.push(props)
        },
        removeAttr(index){
          //再次整理参数
          this.searchParams.props.splice(index,1)
          //再次发送请求
          this.getData()
        }

    父子组件间通信:

              
            
    
        attrInfo(attr,attrValue){
          this.$emit("attrInfo",attr,attrValue)
        }

    day6

    6.1 排序操作(上)

    排序方式:

            1:综合

            2:价格

            asc:升序

            desc:降序

    ①order属性的属性值最多有4种写法:

    1:asc

    1:desc

    2:asc

    2:desc

            //排序的初始状态:综合:降序
            order:"1:desc",

    ②综合和价格谁应该具有类名active?

            通过order属性值当中是包含1还是包含2来判断

                  

    由于模板中不建议使用太长的表达式语句,可以用计算属性书写:

                  
      computed:{
        ...mapGetters(['goodsList']),
        isOne(){
          return this.searchParams.order.indexOf('1')!==-1
        },
        isTwo(){
          return this.searchParams.or.indexOf('2')!==-1
        }
      }

    ③综合和价格,谁应该有箭头?

            谁有类名active,谁就有箭头

    ④箭头用什么制作?

            阿里图标库iconfont

    尚品汇---笔记与思考_第1张图片

    在复制的地址前加 https:

    放入index.html静态页面即可使用

          
    

    图标的基本使用:

                  

    通过计算属性动态决定箭头上下:

        isAsc(){
          return this.searchParams.order.indexOf("asc")!==-1
        },
        isDesc(){
          return this.searchParams.order.indexOf("desc")!==-1
        }
                  

    6.2 排序操作(下)

    设定改变上下箭头的规则:

        changeOrder(flag){
          let originFlag=this.searchParams.order.split(":")[0]
          let originSort=this.searchParams.order.split(":")[1]
          let newOrder=''
          if(flag===originFlag){
            originSort=originSort==="desc"?"asc":"desc"
            newOrder=`${originFlag}:${originSort}`
          }else{
            newOrder=`${flag}:desc`
          }
          this.searchParams.order=newOrder
          this.getData()
        }

    大家注意newOrder=`${flag}:desc`中的冒号一定要用英文字符,否则切换失败!

    day7

    7.1 分页功能分析

    为什么很多项目采用分页功能?

            因为比如电商平台同时展示的数据有很多(1w+),需要采用分页功能一次加载少量数据,防止卡顿

    分页器展示,需要哪些数据(条件)?

            需要知道当前是第几页:pageNo字段代表当前页数

            需要知道每一页需要展示多少条数据:pageSize字段进行代表

            需要知道整个分页器一共有多少条数据:total字段进行代表---【获取另外一条信息:总共有多少页】

            需要知道分页器连续页面个数:5|7(一般是奇数,对称比较好看)

    7.2 分页器起始与结束数字计算

    ①总页数小于连续页数

            仅展示总页数即可

    ②总页数大于/等于连续页数

    根据当前页算起始、结束页码:

            i.起始页码小于1,此时置起始页码为1,结束页码为连续页码数

            ii.结束页码大于总页码,此时置结束页码为总页码,起始页码为:总页码-连续页码+1

            iii.正常情况下,直接计算

      computed:{
        totalPage(){
          return Math.ceil(this.total/this.pageSize)
        },
        startNumAndEndNum(){
          let start=1,end=1;
          //不正常现象:总页数没有连续页码数多
          if(this.continues>this.totalPage){
            end=this.totalPage
          }else{
            start=this.pageNo-parseInt(this.continues)/2
            end=this.pageNo+parseInt(this.continues)/2
            if(start<1){
              start=1
              end=this.continues
            }
            if(end>this.totalPage){
              start=end-this.continues+1
              end=this.totalPage
            }
          }
          return {start,end}
        }

    7.3 分页器的动态展示

    v-for也可以用于循环数字。指定要循环的数字,比如说10

    则v-for会遍历0~10

    如果希望从指定的数字开始遍历,可以用v-if加以限制

    7.4 分页器的完成

    7.5 分页器类名的添加

                :class="{active:pageNo===page}"
    

    7.6 滚动行为

    开发某一个产品的详情页面?

    1.静态组件

    2.发请求

    3.vuex

    4.动态展示组件

    当点击商品的图片的时候,跳转到详情页面,在路由跳转的时候需要带上产品的ID给详情页面

    detail路由:

            {
                path:'/detail/:id',
                component:Detail,
                meta:{isShow:true}
            },

    路由跳转:

    
        
    

    滚动行为:

    使用前端路由,当切换到新的路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。vue-router能做到,且做得更好,他可以让你自定义路由切换时页面如何滚动

    注意:这个功能只在支持history.pushState的浏览器中可用

    export default new VueRouter({
        routes,
        scrollBehavior(to,from,savePosition){
            //返回的这个y=0,代表页面在最上面
            return {y:0}
        }
    })

    7.7 产品详情数据获取

    获取数据的接口:

    //获取产品详细信息的接口 URL:/api/item/{skuId} 请求方式:get
    export const reqGoodsInfo=(skuId)=>requests({
        url:`/item/${skuId}`,
        method:'get'
    })

    vuex仓库捞数据:

    import {reqGoodsInfo} from "@/api";
    
    const state={
        goodsInfo:{}
    }
    const mutations={
        GETGOODSINFO(state,goodsInfo){
            state.goodsInfo=goodsInfo
        }
    }
    const actions={
        //获取产品信息的action
        async getGoodsInfo({commit},skuId){
            let result=await reqGoodsInfo(skuId)
            if(result.code===200){
                commit(GETGOODSINFO(result.data))
            }
        }
    }
    const getters={}
    
    export default {
        state,mutations,actions,getters
    }

    7.8 产品详情展示动态数据

    报错,但不影响程序运行

    尚品汇---笔记与思考_第2张图片

    这是因为仓库中的getters方法:

            state.goodsInfo初始状态为空对象,空对象的categoryView属性值为undefined。undefined.category1Id肯定会报错。

    const getters={
        categoryView(state){
            return state.goodsInfo.categoryView
        }
    }

    可以改为:

            那么此时计算出来的categoryView属性值至少是一个空对象,假的报错就不会有了

    const getters={
        categoryView(state){
            return state.goodsInfo.categoryView||{}
        }
    }

    Detail组件和Zoom组件通信的时候,通用会报错,可以修改为:

          skuImageList(){
            return this.skuInfo.skuImageList||[{}]
          }

    day8

    8.1 产品售卖属性值排他操作

    先通过遍历父数组将所有子元素的isChecked取消

    再将点中的子元素isChecked点亮

          changeActive(spuSaleAttrValue,spuSaleAttrValueList){
              spuSaleAttrValueList.forEach(item=>{
                item.isChecked='0'
              })
            spuSaleAttrValue.isChecked='1'
          }

    8.2 Zoom和ImgList兄弟组件间通信

    通过ImgList传递给Zoom的index,使得轮播图中的图片改变导致Zoom中展示的大图一起改变(联动效应)

    ImgList:

          changeCurrentIndex(index){
            this.currentIndex=index
            this.$bus.$emit('getIndex',this.currentIndex)
          }

    Zoom:

    
        data(){
          return {
            currentIndex:0
          }
        },
        computed:{
          imgObj(){
            return this.skuImageList[this.currentIndex]||{}
          }
        },
        mounted(){
          this.$bus.$on('getIndex',(index)=>{
            this.currentIndex=index
          })
        }

    8.3 轮播图实例的完成

    注意没见过的新属性slidesPereView和slidesPerGroup

        watch:{
          //监听数据:虽然可以保证skuImageList数据已经传递过来,但v-for未必已经遍历完成
          skuImageList(newValue,oldValue){
            this.$nextTick(()=>{
              new Swiper(".swiper-container",{
                navigation:{
                  nextEl:".swiper-button-next",
                  prevEl:".swiper-button-prev"
                },
                //显示一次显示3个图片
                slidesPerView:3,
                //设置一次切换1张图片
                slidesPerGroup:1
              })
            })
          }
        }

    8.4 放大镜的实现

    遮罩层方法剖析:

    big是用来显示mask滑过的区域,mask向右的时候,big对应的图片应该向左,大小为两倍差

          handler(event){
            let mask=this.$refs.mask
            let big=this.$refs.big
            let left=event.offsetX-mask.offsetWidth/2
            let top=event.offsetY-mask.offsetHeight/2
            //约束范围
            if(left<=0) left=0
            else if(left>=mask.offsetWidth) left=mask.offsetWidth
            if(top<=0) top=0
            else if(top>=mask.offsetHeight) top=mask.offsetHeight
            //修改元素的left|top
            mask.style.left=left+'px'
            mask.style.top=top+'px'
            big.style.left=-2*left+'px'
            big.style.top=-2*top+'px'
          }
        }

    总代码:

    
    
    
    
    

    8.5 购买产品个数的操作

          //表单元素修改产品个数
          changeSkuNum(event){
            //用户输入进来的文本*1
            let value=event.target.value*1
            //如果用户输入进来的非法
            if(isNaN(value)||value<1){
              this.skuNum=1
            }else{
              this.skuNum=parseInt(value)
            }
          }

    8.6 加入购物车

    接口api:

    血的教训,千万不要把method写成methods!!!!

    export const reqAddOrUpdateShopCart=(skuId,skuNum)=>requests({
        url:`/cart/addToCart/${skuId}/${skuNum}`,
        method:"post"
    })

    加入购物车成功的路由:

        {
            name:'addcartsuccess',
            path:'/addcartsuccess',
            component:AddCartSuccess,
            meta:{isShow:true}
        }

    仓库中的action:

        //将产品添加到购物车中
        async addOrUpdateShopCart({commit},{skuId,skuNum}){
            //服务器写入数据成功以后,并没有返回其他的数据,只是返回了code=200,代表此处操作成功
            let result= await reqAddOrUpdateShopCart(skuId,skuNum)
            if(result.code==200){
                return 'ok'
            }else{
                return Promise.reject(new Error('fail'))
            }
        }

    浏览器存储功能:

            本地存储:持久化的——5M

            会话存储:并非持久——会话结束就消失

    路由传递参数结合会话存储:

    进行路由跳转的时候需要将产品信息带给下一级的路由组件

    一些简单的数据skuNum,通过query形式给路由组件传递过去

    复杂的产品信息数据(比如skuInfo),通过会话存储即可(不持久化,会话结束数据消失)

          async addShopCart(){
            //1.发请求——将产品加入到数据库(通知服务器)
            try {
              await this.$store.dispatch('addOrUpdateShopCart', {
                    skuId: this.$route.params.skuId,
                    skuNum: this.skuNum})
              //2.服务器存储成功——进行路由跳转传递参数
              sessionStorage.setItem('SKUINFO',JSON.stringify(this.skuInfo))
                this.$router.push({
                  name: 'addcartsuccess',
                  query:{
                    skuNum:this.skuNum
                  }
                })
            }catch(err) {
              //3.失败——给用户进行提示
              alert(err.message)
            }
          }

    AddCartSuccess组件中使用到sessionStorage存储的item:

        computed:{
          skuInfo(){
            return JSON.parse(sessionStorage.getItem('SKUINFO'))
          }
        }

    day9

    9.1 购物车静态组件与修改

    购物车静态组件——需要修改样式结构

    1.调整css让各个项目对齐

    2.向服务器发送ajax,获取购物车数据

    3.UUID临时游客身份

    4.动态展示购物车

    9.2 uuid游客身份获取购物车数据

    获取购物车数据的接口:

    //获取购物车列表数据接口
    export const reqCartList=()=>requests({
        url:"/cart/cartList",
        method:"get"
    })

    向服务器发送ajax请求获取购物车数据:

            发请求的时候无法获取你购物车里面的数据,因为服务器不知道你是谁

            可以使用UUID临时游客身份

    uuid函数逻辑:

    随机生成一个字符串,且每次执行不能发生变化,游客身份持久存储

    ①从本地存储中获取uuid

    ②如果没有

            i.生成游客临时身份

            ii.本地存储

    一定要有返回值!!!

    import {v4 as uuidv4} from 'uuid'
    
    export const getUUID=()=>{
        let uuid_token=localStorage.getItem("UUIDTOKEN")
        if(!uuid_token){
            uuid_token=uuidv4()
            localStorage.setItem('UUIDTOKEN',uuid_token)
        }
        return uuid_token
    }

    在请求拦截器中将存储好的uuid发往后台:

    //请求拦截器
    requests.interceptors.request.use((config)=>{
        if(store.state.detail.uuid_token){
            config.headers.userTempId=store.state.detail.uuid_token
        }
    
        //config是headers的请求头
        nprogress.start()
        return config
    })

    9.3 修改购物车产品的数量完成

    尚品汇---笔记与思考_第3张图片

                -
                
                +
          //修改某一个产品的个数
          async handler(type,disNum,cart) {
            switch (type) {
                //加号
              case 'add':
                disNum = 1
                break
              case 'minus':
                disNum = cart.skuNum > 1 ? -1 : 0
                break
              case 'change':
                if(isNaN(disNum)||disNum<1){
                  disNum=0
                }else{
                  disNum=parseInt(disNum)-cart.skuNum
                }
            }
            try {
              await this.$store.dispatch('addOrUpdateShopCart', {
                skuId: cart.skuId,
                skuNum: disNum
              })
              this.getData()
            }catch(error){
              alert('修改失败')
            }
          }

    特注:因为改变数量的时候如果点击过快可能会发生意想不到的结果,所以可以进行节流处理

          //修改某一个产品的个数
          handler:throttle(async function(type,disNum,cart){
            switch (type) {
                //加号
              case 'add':
                disNum = 1
                break
              case 'minus':
                disNum = cart.skuNum > 1 ? -1 : 0
                break
              case 'change':
                if(isNaN(disNum)||disNum<1){
                  disNum=0
                }else{
                  disNum=parseInt(disNum)-cart.skuNum
                }
            }
            try {
              await this.$store.dispatch('addOrUpdateShopCart', {
                skuId: cart.skuId,
                skuNum: disNum
              })
              this.getData()
            }catch(error){
              alert('修改失败')
            }
          })
          },

    9.4 删除购物车产品的操作

    接口:

    //删除购物产品的接口
    export const reqDeleteCartById=(skuId)=>requests({
        url:`/cart/deleteCart/${skuId}`,
        method:"delete"
    })

    具体的删除操作:

          //删除某一个产品的操作
          async deleteCartById(cart){
            try{
              await this.$store.dispatch('deleteCartListBySkuId',cart.skuId)
              //如果删除成功,再次发请求获取新的数据展示
              this.getData()
            }catch(error){
              alert(error.message)
            }
          }

    9.5 修改产品状态

    接口:

    //修改商品的选中状态
    export const reqUpdateCheckedById=(skuId,isChecked)=>requests({
        url:`/cart/checkCart/${skuId}/${isChecked}`,
        method:"get"
    })

    仓库:

        //修改购物车某一个产品的选中状态
        async updateCheckedById({commit},{skuId,isChecked}){
            let result =  await reqUpdateCheckedById(skuId,isChecked)
            if(result.code===200){
               return 'ok'
            }else{
                return Promise.reject(new Error('fail'))
            }
        }

    修改template模板内容:

                

    修改script脚本内容:

          async updateChecked(cart,event){
            try{
              let checked=event.target.checked?"1":"0"
              await this.$store.dispatch('updateCheckedById',{skuId:cart.skuId,isChecked:checked})
              this.getData()
            }catch(error){
              
            }
          }

    9.6 复习

    1.加入购物车

    UUID:点击加入购物车的时候,通过请求头给服务器带临时身份给服务器,存储某一个用户购物车数据

    会话存储:去存储产品的信息以及展示功能

    2.购物车功能

            修改产品的数量

            删除某一个产品的接口

            某一个产品的勾选状态切换

    day10

    10.1 删除全部选中的商品

    context:小仓库,包含commit(提交mutations修改state)、getters(计算属性)、dispatch(派发action)、state(当前仓库数据)

        //删除全部勾选的产品
        deleteAllCheckedCart(context) {
    
        }

    Promise.all([p1,p2,p3])

    p1|p2|p3:每一个都是Promise对象,如果有一个Promise失败,都失败;如果都成功,返回成功

    删除全部选中的商品

            action逻辑:

        //删除全部勾选的产品
        deleteAllCheckedCart({dispatch,getters}) {
            let promiseAll=[]
            //获取购物车中的全部产品
            getters.cartList.cartInfoList.forEach(item=>{
                let promise=item.isChecked===1?dispatch("deleteCartListBySkuId",item.skuId):''
                promiseAll.push(promise)
            })
            return Promise.all(promiseAll)
        }

            组件里的逻辑:

    
          async deleteAllCheckedCart(){
            try{
              await this.$store.dispatch("deleteAllCheckedCart")
              this.getData()
            }catch(error){
             alert(error.message) 
            }
          }

    10.2 全部商品的勾选状态修改

    actions里的逻辑:

        updateAllCartIsChecked({dispatch,state},isChecked){
            let promiseAll=[]
            state.cartList[0].cartInfoList.forEach(item=>{
                let promise=dispatch("updateCheckedById",{
                    skuId:item.skuId,
                    isChecked:isChecked
                })
                promiseAll.push(promise)
                return Promise.all(promiseAll)
            })
        }

    组件里的逻辑:

          //修改全部产品选中的状态
          async updateAllCartChecked(event){
            try{
              let isChecked=event.target.checked?"1":"0"
              await this.$store.dispatch("updateAllCartIsChecked",isChecked)
              this.getData()
            }catch(error){
              alert(error.message)
            }
          }

    组件的input全选框判断:

          
    全选

    10.3 注册业务

    assets文件夹打包以后,整个项目在dist目录下,assets文件夹会消失

            assets文件夹——放置全部组件共用的静态资源

    在样式中也可以使用@符号:

            url中使用@代表src路径:

                background-image: url(~@/assets/images/icons.png);
    

    接口api:

    //获取验证码
    export const reqGetCode=(phone)=>requests({
        url:`/user/passport/sendCode/${phone}`,
        method:"get"
    })

    仓库:

    import {reqGetCode} from "@/api";
    
    const state={
        code:''
    }
    const mutations={
        GETCODE(state,code){
            state.code=code
        }
    }
    const actions={
        //获取验证码
        async getCode({commit},phone){
            let result=await reqGetCode(phone)
            if(result.code===200){
                commit("GETCODE",result.data)
            }else{
                return Promise.reject(new Error('fail'))
            }
        }
    }
    const getters={}
    
    export default {
        state,mutations,actions,getters
    }

    发送请求到仓库:

            
    

    或:

          async getCode(){
            try{
             this.phone&&(await this.$store.dispatch("getCode",this.phone))
              console.log(this.$store.state.user.code)
            }catch(error){
              alert(error.message)
            }
          }

    注册成功后跳转到登录页面:

    接口api:

    //注册
    export const reqUserRegister=(data)=>requests({
        url:`/user/passport/register`,
        data,
        method:"post"
    })

    仓库里的actions逻辑:

        //用户注册
        async userRegister({commit},user){
            let result=await reqUserRegister(user)
            if(result.code===200){
                return 'ok'
            }else{
                return Promise.reject(new Error('fail'))
            }
        }

    组件里的逻辑:

          async userRegister(){
            try{
              const {phone,code,password,rePassword}=this
              phone&&code&&password===rePassword&&(await this.$store.dispatch("userRegister",{phone,code,password}))
              this.$router.push("/login")
            }catch(error) {
              alert(error.message)
            }
          }

    10.4 登录业务

    登录业务:

    注册:通过数据库存储用户信息(名字、密码)

    登录:登录成功的时候,后台为了区分你这个用户是谁,会让服务器下发token(令牌:唯一标识符)

    登陆接口:一般登陆成功后服务器会下发token,前台持久化存储token,然后前台带着token 去找服务器登录

    接口api:

    //登录
    export const reqUserLogin=(data)=>requests({
        url:"/user/passport/login",
        data,
        method:"post"
    })

    组件逻辑:

          async userLogin(){
            try{
              const {phone,password}=this
              (phone&&password)&&(await this.$store.dispatch("userLogin",{phone,password}))
              this.$router.push("/home")
            }catch(error){
              alert(error.message)
            }
          }

    仓库逻辑:

    actions:

        //登录业务
        async userLogin({commit},data){
            let result=await reqUserLogin(data)
            if(result.code===200){
                commit("USERLOGIN",result.data.token)
                return 'ok'
            }else{
                return Promise.reject(new Error('fail'))
            }
        }

    mutations:

        USERLOGIN(state,token){
            state.token=token
        }

    前台携带token获取用户信息:

    export const reqUserInfo=()=>requests.get("/user/passport/auth/getUserInfo")
    

    处理请求拦截器:

    //请求拦截器
    requests.interceptors.request.use((config)=>{
        if(store.state.detail.uuid_token){
            config.headers.userTempId=store.state.detail.uuid_token
        }
        if(store.state.user.token){
            config.headers.token=store.state.user.token
        }
    
        //config是headers的请求头
        nprogress.start()
        return config
    })

    获取服务器返回的用户信息:

        async getUserInfo({commit}){
            let result=await reqUserInfo()
            if(result.code===200){
                commit("GETUSERINFO",result.data)
                return 'ok'
            }else{
                return Promise.reject(new Error('fail'))
            }
        }
        GETUSERINFO(state,userInfo){
            state.userInfo=userInfo
        }

    处理Header组件:

            

    尚品汇欢迎您!

    登录 免费注册

    {{userName}} 退出登录

    vuex仓库存储的数据并不持久化,一刷新数据就没了。可以通过本地存储持久化token:

        //登录业务
        async userLogin({commit},data){
            let result=await reqUserLogin(data)
            if(result.code===200){
                commit("USERLOGIN",result.data.token)
                localStorage.setItem("TOKEN",result.data.token)
                return 'ok'
            }else{
                return Promise.reject(new Error('fail'))
            }
        }
    const state={
        code:'',
        token:localStorage.getItem("TOKEN"),
        userInfo:{}
    }

    目前存在的bug:

    ①多个组件展示用户信息需要在每一个组件的mounted中触发this.$store.dispatch("getUserInfo")

    ②用户已经登陆,就不能再跳转到登录页

    10.5 退出登录

    退出登录需要做的事:

    ①需要发送请求,通知服务器退出登录(清除一些数据,比如token)

    ②清除项目当中的数据

    接口api:

    export const reqLogout=()=>requests.get("/user/passport/logout")
    

    清除记录的数据:

        async userLogout({commit}){
            let result=await reqLogout()
            if(result.code===200){
                commit("CLEAR")
                return "ok"
            }else{
                return Promise.reject(new Error("fail"))
            }
        }
        CLEAR(state){
            state.token=""
            state.userInfo={}
            localStorage.clear()
        }

    派发action:

        async logout(){
          try{
            await this.$store.dispatch("userLogout")
            this.$router.push("/home")
          }catch(error){
            alert(error.message)
          }
        }

    10.6 导航守卫用户登录操作

    导航:表示路由正在发生变化,进行路由跳转

    导航守卫可以简单地分为全局守卫、路由独享守卫、组件内守卫

    全局前置守卫:

            当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫resolve完之前一直处于等待中

    参数剖析:

            to:可以获取到你要跳转到的那个路由的信息

            from:可以获取到你从哪个路由而来的信息

            next:放行函数

                    next()放行       

                    next("/login")放行到指定的路由中

                    next(false)中断当前的导航。如果浏览器的URL改变了(可能是用户手动或者浏览器后退按钮),那么URL地址会重置到from路由对应的地址

    router.beforeEach((to, from, next)=>{
        
    })
    

    全局路由守卫逻辑:

    1.如果已经登录

            i.想去的是login路由,跳转到主页

            ii.想去的不是login路由

                    ①如果有用户名,直接放行(不能用userInfo判断,因为空对象为真值)

                    ②如果没有用户名,可能是刷新导致仓库的数据消失了,再去捞一下数据,然后放行

                            如果捞不到数据,就直接退出登录,回到登录界面

    2.如果没有登陆

            直接放行(此时还没有做处理,后续可能会有追加)

    router.beforeEach(async (to, from, next)=>{
        let token=store.state.user.token
        let name=store.state.user.userInfo.name
        if(token) {
            //登陆了订单还想去login或register(不能去,停留在首页)
            if (to.path === '/login'||to.path === '/register') {
                next("/home")
            } else {
                //登陆了,去的不是login
                //如果用户信息
                if (name) {
                    next()
                } else {
                    try {
                        //没有用户信息,派发action让仓库存储用户信息再跳转
                        await store.dispatch("getUserInfo")
                        next()
                    } catch (error) {
                        //token失效,获取不到用户信息,需要重新登陆
                        //清除token
                        await store.dispatch("userLogout")
                        next("/login")
                    }
                }
            }
        }else{
            next()
        }
    })

    10.7 获取交易页数据及展示

    api接口:

    //获取用户地址信息
    export const reqAddressInfo=()=>requests({
        url:"/user/userAddress/auth/findUserAddressList",
        method:"GET"
    })
    //获取商品清单
    export const reqOrderInfo=()=>requests({
        url:"/order/auth/trade",
        method:"GET"
    })

    仓库:

    import {reqAddressInfo, reqOrderInfo} from "@/api";
    
    const state={
        address:[],
        orderInfo:{}
    }
    const mutations={
        GETUSERADDRESS(state,address){
            state.address=address
        },
        GETORDERINFO(state,orderInfo){
            state.orderInfo=orderInfo
        }
    }
    const actions={
        async getUserAddress({commit}){
           let result = await reqAddressInfo()
            if(result.code===200){
                commit("GETUSERADDRESS",result.data)
            }
        },
        async getOrderInfo({commit}){
            let result = await reqOrderInfo()
            if(result.code===200){
                commit("GETORDERINFO",result.data)
            }
        }
    }
    const getters={}
    
    export default {
        state,mutations,actions,getters
    }

    组件逻辑:

    注意mapState和mapGetters的区别

    day11

    11.1 提交订单

    ①静态组件

    ②点击提交订单的按钮时,还需要向服务器发送一次请求,把一些支付的相关信息传递给服务器

    接口:

    export const reqSubmitOrder=(tradeNo,data)=>requests({
        url:`/order/auth/submitOrder?tradeNo=${tradeNo}`,
        data,
        method:"post"
    })

    统一接口api文件夹里面全部的请求函数:

    import * as API from "@/api"
    
    
    new Vue({
      render: h => h(App),
      beforeCreate(){
        Vue.prototype.$bus=this
        Vue.prototype.$API=API
      },
      router,
      store,
    }).$mount('#app')

    发送请求到支付页面:

          async submitOrder(){
            let {tradeNo}=this;
            let data={
              consignee:this.userDefaultAddress.consignee,
              consigneeTel:this.userDefaultAddress.phoneNum,
              deliveryAddress:this.userDefaultAddress.fullAddress,
              paymentWay:"ONLINE",
              orderComment:this.msg,
              orderDetailList:this.detailArrayList
            }
            let result=await this.$API.reqSubmitOrder(tradeNo,data)
            console.log(result)
            if(result.code===200){
              this.orderId=result.data
              this.$router.push(`/pay?orderId=${this.orderId}`)
            }else{
              alert(result.data)
            }
          }

    11.2 获取订单号与展示支付信息

    接口:

    //获取支付的信息
    export const reqPayInfo=(orderId)=>requests({
        url:`/payment/weixin/createNative/${orderId}`,
        method:"get"
    })

    生命周期函数中不能使用async

    11.3 支付页面中使用ElementUI以及按需引入

    //注册全局组件
    import {Button,MessageBox} from 'element-ui'
    //ElementUI注册组件的时候还有一种写法:挂在原型上
    Vue.use(Button)
    Vue.prototype.$msgbox = MessageBox;
    Vue.prototype.$alert = MessageBox.alert;

    ElementUI按需引入:

    借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的

    ①安装 babel-plugin-component

    yarn add babel-plugin-component -D
    

    ②将babel.config.js修改为:

    module.exports = {
      presets: [
        '@vue/cli-plugin-babel/preset',
      ],
      "plugins":[
          [
              "component",
            {
              libraryName:'element-ui',
              "StyleLibraryName":'theme-chalk'
            }
          ]
      ]
    
    }
    

    直接使用:

              立即支付
    
          //弹出框
          open(){
            this.$alert(`这是 HTML 片段`,'HTML片段',{
              dangerouslyUseHTMLString:true,
              //中间布局
              center:true,
              //是否显示取消按钮
              showCancelButton:true,
              //取消按钮的文本内容
              cancelButtonText:"支付遇见问题",
              //确定按钮的文本内容
              confirmButtonText:"已经支付成功",
              //右上角的叉叉是否显示
              showClose:false
            })
          }

    11.4 微信支付业务

    使用qrcode,一个用于生成二维码的JavaScript库

    获取支付订单状态的接口:

    //获取支付订单状态
    export const reqPayStatus=(orderId)=>requests({
        url:`/payment/weixin/queryPayStatus/${orderId}`,
        method:"get"
    })

    弹出框业务:

          //弹出框
          async open(){
            //生成二维码地址
            let url=await QRCode.toDataURL(this.payInfo.codeUrl)
            this.$alert(``,'HTML片段',{
              dangerouslyUseHTMLString:true,
              //中间布局
              center:true,
              //是否显示取消按钮
              showCancelButton:true,
              //取消按钮的文本内容
              cancelButtonText:"支付遇见问题",
              //确定按钮的文本内容
              confirmButtonText:"已经支付成功",
              //右上角的叉叉是否显示
              showClose:false,
              //关闭弹出框的配置值
              beforeClose:(type,instance,done)=>{
                //type:区分取消|确定按钮
                //instance:当前组件实例
                //done:关闭弹出框的方法
                if(type==='cancel'){
                  alert('请联系管理员')
                  clearInterval(this.timer)
                  this.timer=null
                  //关闭弹出框
                  done()
                }else{
                  //判断是否真的支付了
                  if(this.code===200){
                    clearInterval(this.timer)
                    this.timer=null
                    done()
                    this.$router.push('/paysuccess')
                  }
                }
              }
            })
            if(!this.timer){
              this.timer=setInterval(async ()=>{
                //发请求获取用户支付状态
                let result=await this.$API.reqPayStatus(this.orderId)
                //如果code===200
                if(result.code===200){
                  //第一步:清除定时器
                   clearInterval(this.timer)
                  this.timer=null
                  //保存支付成功返回的code
                   this.code=result.code
                  //关闭弹出窗
                  this.$msgbox.close()
                  //跳转到下一路由
                  this.$router.push('/paysuccess')
                }
              },1000)
            }
          }

    day12

    12.1 个人中心二级路由的搭建

    如果只进入/center,可能会出现右侧没有内容的情况,所以需要重定向到/center/myorder

        {
            path:'/center',
            component:Center,
            meta:{isShow:true},
            children:[
                {
                    path:'myorder',
                    component:MyOrder
                },
                {
                    path:'grouporder',
                    component:GroupOrder
                },
                {
                    path:'/center',
                    redirect:"/center/myorder"
                }
            ]
        }

    12.2 我的订单

    接口api:

    //获取个人中心的数据
    export const reqMyOrderList=(page,limit)=>requests({
        url:`/order/auth/${page}/${limit}`,
        method:"get"
    })

    12.3 未登录的导航守卫判断

    全局前置守卫逻辑处理:

            //未登录:不能去交易相关、支付相关的页面(pay|paysuccess)、不能去个人中心
            //未登录若去上述页面---跳转至登录页面
            let toPath=to.path
            if(toPath.indexOf('/trade')!==-1||toPath.indexOf('/pay')!==-1||toPath.indexOf('/center')!==-1){
                next('/login?redirect='+toPath)
            }else {
             //去的不是上面这些路由(home|search|shopCart)---放行
                next()
            }

    对login的处理:增加了对query参数的判断

          async userLogin(){
            try{
              const {phone,password}=this;
              (phone&&password)&&(await this.$store.dispatch("userLogin",{phone,password}))
              let toPath=this.$route.query.redirect||'/home'
              this.$router.push(toPath)
            }catch(error){
              alert(error.message)
            }
          }

    12.4 用户登录(路由独享与组件内守卫)

    路由独享守卫:

    只有从购物车界面才能跳转到交易页面(创建订单)

        {
            path:"/trade",
            component: Trade,
            meta:{isShow:true},
            //路由独享守卫
            beforeEnter:(to,from,next)=>{
                if(from.path==='/shopcart'){
                    next()
                }else{
                    next(false)
                }
            }
        }

    只有从交易页面(创建订单)页面才能跳转到支付页面

        {
            path:"/pay",
            component:Pay,
            meta:{isShow:true},
            beforeEnter:(to,from,next)=>{
                if(from.path==='/trade'){
                    next()
                }else{
                    next(false)
                }
            }
        }

    只有从支付页面才能跳转到支付成功页面

    组件内守卫:

    beforeRouterEnter(to,from,next){        }:

            在渲染该组件的对应路由被confirm前调用

            不能获取组件的实例“this”

            因为当守卫执行前,组件实例还没有被创建

      export default {
        name: 'PaySuccess',
        beforeRouteEnter(to,next,from){
          if(from.path==='/pay'){
            next()
          }else{
            next(false)
          }
        }
      }

    beforeRouteUpdate(to,from,next){        }:

            在当前路由改变、但是该组件被复用时调用

            举例来说,对于一个带有动态参数的路径/foo/:id,在/foo/1和/foo/2之间跳转的时候

            由于会渲染同样的Foo组件,因此组件实例会被复用,而这个钩子就是这种情况下被调用

            可以访问组件实例“this”

    beforeRouteLeave(to,from,next){        }:

            导航离开该组件的对应路由时调用

            可以访问组件实例的“this”

    12.5 图片懒加载

    ①下载插件vue-lazyload

    注意vue2对应1.3.3版本的vue-lazyload,否则会报路径错误

    yarn add [email protected]
    

    ②图片、json不需要对外暴露就可引入

    import VueLazyLoad from 'vue-lazyload'
    import winter from '@/assets/winter.jpg'
    Vue.use(VueLazyLoad,{
      //懒加载默认图片
      loading:winter
    })
    

    在需要默认图片的模板中插入:

                          
    

    12.6 vee-validate表单验证的使用

    ①安装:注意vue2对应vee-validate2版本

    yarn add vee-validate@2   

    ②在main.js中引入表单校验插件

    //引入表单校验插件
    import '@/plugins/validate'

    ③编写表单校验插件内容

    import Vue from 'vue'
    import VeeValidate from 'vee-validate'
    import zh_CN from 'vee-validate/dist/locale/zh_CN'
    Vue.use(VeeValidate)
    
    //表单验证
    VeeValidate.Validator.localize("zh_CN",{
        messages:{
            ...zh_CN.messages,
            is:(field)=>`${field}必须与密码相同`,
        },
        attributes:{
            phone:"手机号",
            code:"验证码",
            password:"密码",
            rePassword:"确认密码",
            agree:"协议"
        }
    })

    ④改写模板里的代码(以手机号校验为例)

          
    {{errors.first("phone")}}

    ⑤效果图

    不输入时:

     输入错误时:

    尚品汇---笔记与思考_第4张图片

     

    格式正确时警告会消失:

    尚品汇---笔记与思考_第5张图片

     

    验证码校验:

          
    {{errors.first("code")}}

    密码校验:

          
    {{errors.first("password")}}

    确认密码校验:

          
    {{errors.first("rePassword")}}

    效果图:

    尚品汇---笔记与思考_第6张图片

     尚品汇---笔记与思考_第7张图片

     

    对于勾选同意协议的复选框,必须自定义规则

    //自定义校验规则
    VeeValidate.Validator.extend("agree",{
        validate:value=>{
            return value
        },
        getMessage:field=>field+"必须同意"
    })

    修改模板里的内容:

          
    同意协议并注册《尚品汇用户协议》 {{errors.first("agree")}}

    如果所有的表单验证都通过,再向服务器发送请求进行注册

          async userRegister(){
            const success=await this.$validator.validateAll()
            if(success){
              try{
                const {phone,code,password,rePassword}=this
                await this.$store.dispatch("userRegister",{phone,code,password})
                this.$router.push("/login")
              }catch(error) {
                alert(error.message)
              }
            }
          }

    12.7 路由懒加载

    当打包构建应用时,JavaScript包会变的非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,就会更加高效

    以Search组件为例:

        {
            name:'search',
            path:"/search/:keyword?",
            component:()=>import("@/pages/Search.vue"),
            meta:{show:true}
        }

    12.8 处理map文件

    打包:yarn build

    项目打包后,代码都是经过压缩加密的,如果运行时报错,输出的错误信息无法准确得知是哪里的代码报错。有了map就可以像未加密的代码一样,准确的输出是哪一行哪一列又错了

    文件如果项目不需要可以去掉

    Vue.config.js配置中productionSourceMap:false可以不生成体积很大的map文件

      productionSourceMap:false
    

    12.9 购买服务器

    可以去阿里云或者腾讯云购买服务器,腾讯云更便宜


    完结撒花❀❀~

    你可能感兴趣的:(前端,javascript,webpack,前端)