尚品汇项目笔记

这里写目录标题

D1

01 脚手架目录结构

  • node_modules:放置项目依赖的地方
  • public:一般放置一些共用的静态资源,打包上线的时候,public文件夹里面资源原封不动打包到dist文件夹里面
  • src:程序员源代码文件夹
  • -----assets文件夹:经常放置一些静态资源(图片),assets文件夹里面资源webpack会进行打包为一个模块(js文件夹里面)
  • -----components文件夹:一般放置非路由组件(或者项目共用的组件)
  • ​ App.vue 唯一的根组件
  • ​ main.js 入口文件【程序最先执行的文件】
  • ​ babel.config.js:babel配置文件
  • ​ package.json:看到项目描述、项目依赖、项目运行指令
  • ​ README.md:项目说明文件

02 创建非路由组件(2个:Header、Footer)

非路由组件使用分为几步:
第一步:创建

第二步:引入

import Header from './components/Header/index.vue'

第三步:注册

components: {
    Header,
    Footer
}

第四步:使用

<template>
  <div id="app">
    <Header></Header>
    <Footer></Footer>
  </div>
</template>

项目采用的less样式,浏览器不识别less语法,需要一些loader进行处理,把less语法转换为CSS语法

1:安装less less-loader@5
切记less-loader安装5版本的,不要安装在最新版本,安装最新版本less-loader会报错,–> setOption函数未定义

2:需要在style标签的身上加上lang=“less”,不添加样式不生效

03 创建路由组件

vue-router 前端路由: k,v键值对, key: URL(地址栏中的路径), value: 相应的路由组件

项目路由组件:Home、Search、Login、Register, 创建的路由组件一般放在pages文件夹中, 配置路由一般在, router文件夹下

// 配置路由
import Vue from 'vue';
import VueRouter from 'vue-router'
// 使用插件
Vue.use(VueRouter)
// 引入路由组件
import Home from '@/pages/Home'
import Search from '@/pages/Search'
import Register from '@/pages/Register'
import Login from '@/pages/Login'
// 暴露路由VueRouter实例
export default new VueRouter({
  // 配置路由
  routes: [
    {
      path: "/home",
      component: Home
    },
    {
      path: "/search",
      component: Search
    },
    {
      path: "/register",
      component: Register
    },
    {
      path: "/login",
      component: Login
    },
    // 重定向,使项目一上来就打开 home 首页
    {
      path: '*', //全部路径
      redirect: "/Home"
    }
  ]
})

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

  1. 路由组件一般放置在pages文件夹下, 非路由组件一般放置在components文件夹下(共用全局组件)

  2. 路由组件在使用时, 使用<router-view></router-view>, 非路由组件在使用时, 使用<Header></Header>

  3. 注册完路由, 非路由组件, 与路由组件, 身上都会有$route, $router属性

    $route: 一般获取路由信息(路径, query, params参数等)

    • params参数:路由需要占位,程序就崩了,属于URL当中一部分
    • query参数:路由不需要占位,写法类似于ajax当中query参数

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

    • push参数: 页面跳转后存在历史记录,
    • replace参数: 页面跳转后不存在历史记录

路由的跳转

  1. 路由的跳转就两种形式:声明式导航(router-link:务必要有to属性)编程式导航push, replace
    声明式导航能做的, 编程式导航都能做, 且可以书写自己的业务逻辑(比如一些点击事件, 显示与隐藏等方法)

04 Footer组件的显示与隐藏

  1. 功能实现: Footer组件, 在 Home, Search 显示, 在Login、Register隐藏<Footer v-show="$route.meta.show"></Footer>

    可以给路由添加路由元信息【meta】, meta: {show:true}

v-show与v-if区别?
v-show:通过样式display控制
v-if:通过元素上树与下树进行操作

05 路由传参

methods: {
    goSearch(){
      // 路由传参 params + query
      // 第一种: 字符串形式
      // 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()}})
    }
  }
 {
      path: "/search/:keyword", //params 参数需要进行占位
      component: Search,
      meta: {show:true},
      name: 'search'   //使用对象方式时, 要添加一个name属性
    },

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

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

不可以:对象的写法可以是name, path形式, 但是path这种写法不能与params参数一起使用, 可以与query参数一起使用

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

在配置路由时, 再添加params占位时, 在占位后面加上问号(则表示, params参数可传或不不传) path: "/search/:keyword?"

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

使用undefined解决, params参数可以不传递参,但传了一个空串

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

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

  1. 第一种(布尔值形式): 在配置路由时, 添加props:true, 这种布尔值写法只能传递params参数

  2. 第二种(对象形式): 配置路由时, 额外给路由组件传递一些props props:{a:1,b:2}

  3. 第三种(函数形式):

props:($route) => {
    return {keyword:$route.params, k:$route.query.k};
}

路由组件接收: props:[keyword, k]

D2

01 错误处理

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

注意:编程式导航(push|replace)才会有这种情况的异常,声明式导航是没有这种问题,因为声明式导航内部已经解决这种问题。
为什么会出现这种现象:
由于vue-router最新版本3.5.2,引入了promise,当传递参数多次且重复,会抛出异常,
第一种解决方案:是给push函数,传入相应的成功的回调与失败的回调,但是以后再用push|replace还是会出现类似现象,因此我们需要从‘根’治病;

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

第二种解决方案: 在配置路由时重写push方法

// 把VueRouter 原型对象的push, replace方法, 保存一份
let originPush = VueRouter.prototype.push;
let originReplace = VueRouter.prototype.replace;
// 重写push, replace
// 第一个参数: 告诉原来的push方法, 要往哪里跳(传递那些参数)
// 第二个参数: 成功时的回调
// 第三个参数: 失败时的回调
// call || apply的区别
// 相同点: 都可以调用函数一次, 都可以修改this指向一次
// 不同点: call传递参数用逗号隔开, apply则使用数组
VueRouter.prototype.push = function(location, resolve, reject){
  if(resolve && reject) {
    // originPush()  不能直接调用, 此时this会指向window, 要使用 .call进行调用
    originPush.call(this, location, resolve, reject, );
  }else {
    originPush.call(this, location, ()=>{}, ()=>{})
  }
},
VueRouter.prototype.replace = function(location, resolve, reject){
  if(resolve && reject) {
    originReplace.call(this, location, resolve, reject, );
  }else {
    originReplace.call(this, location, ()=>{}, ()=>{})
  }
}

02 三级联动组件

由于三级联动组件要在 Home, Search, Detail 中使用, 所以注册三级联动组件为全局组件, 使其可以在任何地方使用

// 三级联动组件 --- 全局组件
import TypeNav from '@/pages/Home/TypeNav';
// 第一个参数: 全局组件的名字 第二个参数: 哪一个组件
Vue.component(TypeNav.name, TypeNav);

在Home中使用组件

<!-- 三级联动全局组件, 已经注册为了全局组件, 因此不需要再引入, 可以直接使用-->
  <TypeNav></TypeNav>

03 其他静态组件的拆分

这块没什么难度, 要注意图片的路径, 和设置 <style lang="less">

<template>
  <div>
    <!-- 三级联动全局组件, 已经注册为了全局组件, 因此不需要再引入, 可以直接使用-->
    <TypeNav></TypeNav>
    <!-- 使用其他组件  -->
    <ListContainer></ListContainer>
    <Recommend></Recommend>
    <Rank></Rank>
    <Like></Like>
    <Floor></Floor>
    <Brand></Brand>
  </div>
</template>
<script>
// 引入其余组件
import ListContainer from '@/pages/Home/ListContainer';
import Recommend from '@/pages/Home/Recommend';
import Rank from '@/pages/Home/Rank';
import Like from '@/pages/Home/Like';
import Floor from '@/pages/Home/Floor';
import Brand from '@/pages/Home/Brand';
export default {
  name: "",
  components: {
    ListContainer,
    Recommend,
    Rank,
    Like,
    Floor,
    Brand
  }
}
</script>

04 axios的二次封装

AJAX:客户端可以’敲敲的’向服务器端发请求,在页面没有刷新的情况下,实现页面的局部更新。
XMLHttpRequest、jq、fetch、axios
工作的时候src目录下的API文件夹,一般关于axios二次封装的文件

// 对于 axios 进行二次封装
import axios from 'axios'
// 利用axios对象的方法 create, 去创建一个axios实例
const requests = axios.create({
  // 配置对象, 基础路径, 发送请求的时候, 路径中会出现api
  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('faile'));
});

05解决请求跨域

跨域:如果多次请求协议、域名、端口号有不同的地方,称之为跨域

  1. cors: 服务器响应返回数据时会携带特殊的响应头, 从真正意义上解决跨域, 但这种方法需要后端做一些配置, 且响应头不能随便配置(可能会造成任何人都会找这台服务器请求数据)

  2. jsonp: 借助script标签里的src属性, 在引入外部资源的时候不受, 同源策略限制这一特点(这种方式很巧妙, 面试的时候会问)解决跨域, 但是只能解决get请求, 不能解决post, PUT, DELETE, 且需要前后端相互配合

  3. 配置代理服务器: 开发中使用最多的一种方法,

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wPDtsu56-1660641621514)(C:/Users/w3112/Desktop/vue%E7%AC%94%E8%AE%B0/vue%E9%A1%B9%E7%9B%AEimg/%E4%BB%A3%E7%90%86%E6%9C%8D%E5%8A%A1%E5%99%A8.png)]

    开启代理服务器: 1, 使用 nginx 2, 借助vue-cli(比较简单)

     // 借助vue-cli开启代理跨域
      devServer: {
        proxy: {
          '/api': { //请求前缀, 只有含有请求前缀的请求, 才会走代理服务器, 比较灵活
            target: 'http://gmall-h5-api.atguigu.cn/',  //请求地址
            // pathRewrite: { '^/api': '' },  //重写路径把符合第一个参数(类似于正则匹配)转成第二个参数 用于请求服务器时路径不匹配
          },
        },
      },
    

06引入进度条

// start: 进度条开始  done: 进度条结束
import nprogress from 'nprogress';
// 引入进度条样式
import "nprogress/nprogress.css";

修改进度条样式, 如 进度条颜色, 宽度, 高度等, 可以在nprogress下的nprogress/nprogress.css 文件中修改

#nprogress .bar {
  /* background: #29d; */
  background: rgb(11, 226, 144);
  position: fixed;
  z-index: 1031;
  top: 0;
  left: 0;
  width: 100%;
  height: 2px;
}

07 Vuex

vuex:Vue官方提供的一个插件,状态管理库, 集中式管理项目中组件共用的数据
vuex:书写任何项目都需要vuex?–> 项目大的时候,需要有一个地方‘统一管理数据’即为仓库store
Vuex基本使用:

// home模块小仓库
// state: 仓库存储数据的地方
const state = {};
// mutations: 修改state的唯一手段
const mutations = {};
// actions:  处理cation, 可以书写自己的业务逻辑, 也可以处理异步
const actions = {};
// getters: 类似于计算属性, 用于简化仓库数据, 让组件获取仓库的数据更加方便
const getters = {};
export default {
  state,
  mutations,
  actions,
  getters,
}

当项目过大, 组件过多, 接口多, 数据多时, 要使用vuex的模块式开发: 一个大仓库, 分为多个小仓库, 统一暴露, 集中引入

import Vue from 'vue';
import Vuex from 'vuex';
// 安装插件
Vue.use(Vuex)
// 引入小仓库
import home from './home'
import search from './search'
// 对外暴露Store类的一个实例
export default new Vuex.Store({
  // 实现Vuex仓库模块式开发存储数据
  modules: {  //用于引入子模块 
    home,
    search,
  }
});

D3

01 商品分类三级联动(TypeNav组件)展示动态数据

向服务器请求数据

<script>
import {mapState} from 'vuex';
export default {
  name: 'TypeNav',
  // 组件挂载完毕: 可以向服务器发送请求
  mounted() {
    // 通知Vuex发送请求, 获取数据, 存储在仓库中
    this.$store.dispatch('categoryList')
    //mounted|created:都可以mounted:模板已经变为真是DOM【只不过没有数据,显示空白】,因为ajax是异步,需要时间的。
  },
  computed: {
    ...mapState({
      categoryList:(state) => { //当使用这个计算属性时, 函数会调用一次, 并传入一个state参数,即为大仓库中的数据,大仓库包含home和search
        return state.home.categoryList;   // 拿到state下的home下的categoryList
      }
    })
  }
}
</script>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bShm8G6S-1660641621515)(C:/Users/w3112/Desktop/vue%E7%AC%94%E8%AE%B0/vue%E9%A1%B9%E7%9B%AEimg/%E5%A4%A7%E4%BB%93%E5%BA%93.png)]

服务器发送数据

// home模块小仓库
// state: 仓库存储数据的地方
const state = {
  // 服务器返回数组, 对象   (是根据接口返回值初始化的)
  categoryList: [],
};
// mutations: 修改state的唯一手段
const mutations = {
  CATEGORYLIST(state, categoryList) {
    state.categoryList = categoryList
  }
};
// actions:  处理cation, 可以书写自己的业务逻辑, 也可以处理异步
const actions = {
  // 通过api里面的接口函数调用, 向服务器发请求, 获取数据
  async categoryList({commit}) {
    let result = await reqCategoryList();
    // console.log(result);
    if(result.code == 200) {
      commit('CATEGORYLIST', result.data)
    }
  }
};

在页面中展示

      <div class="item" v-for="(c1, index) in categoryList" :key="c1.categoryId">
			//遍历categoryList数组, 获取id, categoryName的数据, 并用插值语法在页面中展示
            <h3>
              <a href="">{{c1.categoryName}}</a>  
            </h3>
            <div class="item-list clearfix">
              <div class="subitem" v-for="(c2, index) in c1.categoryChild" :key="c2.categoryId">
                <dl class="fore">
                  <dt>
                    <a href="">{{c2.categoryName}}</a>
                  </dt>
                  <dd>
                    <em v-for="(c3, index) in c2.categoryChild" :key="c3.categoryId">
                      <a href="">{{c3.categoryName}}</a>
                    </em>
                  </dd>
                </dl>
              </div>
            </div>
          </div>

02完成一级分类的背景效果

页面结构:

<div @mouseleave="leaveIndex"> 
      <!-- 事件委派: 把子元素的事件交给父元素管理(@mouseleave="leaveIndex") -->
        <h2 class="all">全部商品分类</h2>
        <div class="sort">
          <div class="all-sort-list2">
            <div class="item" v-for="(c1, index) in categoryList" :key="c1.categoryId"
              :class="{ cur: currentIndex == index }">  
                //动态绑定class样式当满足currentIndex == index时cur才会触发
              <h3 @mouseenter="changeIndex(index)">
                <a href="">{{ c1.categoryName }}</a>
              </h3>
              <div class="item-list clearfix">
                <div class="subitem" v-for="(c2, index) in c1.categoryChild" :key="c2.categoryId">
                  <dl class="fore">
                    <dt>
                      <a href="">{{ c2.categoryName }}</a>
                    </dt>
                    <dd>
                      <em v-for="(c3, index) in c2.categoryChild" :key="c3.categoryId">
                        <a href="">{{ c3.categoryName }}</a>
                      </em>
                    </dd>
                  </dl>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>

对应方法:

methods: {
    // 一级分类鼠标移入修改响应式currentIndex属性
    changeIndex(index) {
      this.currentIndex = index;
    },
    // 一级分类鼠标移出修改响应式currentIndex属性
    leaveIndex() {
      this.currentIndex = -1
    }
  }

03 解决卡顿问题

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

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

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

// 按需引入(引入lodash下的throttle)  一般node依赖包里面会自带, 不用在安装
import throttle from 'lodash/throttle'
// 设置节流
    changeIndex: throttle(function (index) {
      this.currentIndex = index;
      // console.log('---');
    }, 100),  //每个100ms请求一次

04 三级联动组件(TypeNav组件)的路由跳转与传参

  1. 三级联动, 用户可以点击的: 一级分类, 二级分类, 三级分类, 当用户点击的时候Home模块会跳转到search模块, 一级分类会把用户选中的产品(产品名字, 产品id), 在路由跳转时, 进行传递

  2. 路由的跳转:

    1. 第一种声明式导航: 为什么使用router-link组件的时候,页面会出现卡顿?router-link是一个组件:相当VueComponent类的实例对象,一瞬间new VueComponent很多实例(1000+),很消耗内存,因此导致卡顿。

    2. 第二种编程式导航: push|replace三级分类由于使用router-link的时候,会出现卡顿现象,因此采用编程式导航:@click="goSearch"。路由跳转的时候【home->search】:需要进行路由传递参数【分类的名字、一、二、三级分类的id】, 但过多的回调也会导致性能不高

    3. 最好的方案是采用: 编程式导航 + 事件委派

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

      1. 如何确定点击的是a标签?

        把子节点中的a标签, 动态绑定自定义的属性:data-categoryName="c1.categoryName", 带有这个属性的就是a标签

        • 在a标签中定义自定义属性:data-categoryName="c1.categoryName":
        
        <div class="all-sort-list2" @click="goSearch">
                    <div class="item" v-for="(c1, index) in categoryList" :key="c1.categoryId"
                      :class="{ cur: currentIndex == index }">
                      <!-- 一级分类 -->
                      <h3 @mouseenter="changeIndex(index)">
                        //在每个a标签中动态绑定:data-categoryName="c1.categoryName"自定义属性来区分 其他标签
                        <a :data-categoryName="c1.categoryName">{{ c1.categoryName }}</a>
                      </h3>
                      <!--, 三级分类 -->
                      <!-- 鼠标事件:显示或显示三级分类 -->
                      <div class="item-list clearfix" :style="{display:currentIndex == index? 'block': 'none'}">
                        <div class="subitem" v-for="(c2, index) in c1.categoryChild" :key="c2.categoryId">
                          <dl class="fore">
                            <dt>
                              <a :data-categoryName="c2.categoryName">{{ c2.categoryName }}</a>
                            </dt>
                            <dd>
                              <em v-for="(c3, index) in c2.categoryChild" :key="c3.categoryId">
                                <a :data-categoryName="c3.categoryName">{{ c3.categoryName }}</a>
                              </em>
                            </dd>
                          </dl>
                        </div>
                      </div>
                    </div>
                  </div>
        
        • 拿到a标签:

        [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T6RSTvCU-1660641621516)(C:/Users/w3112/Desktop/vue%E7%AC%94%E8%AE%B0/vue%E9%A1%B9%E7%9B%AEimg/%E8%8E%B7%E5%8F%96a%E6%A0%87%E7%AD%BE.png)]

        goSearch(event) {
              // this.$router.push('/search')
              //获取点击目标元素
              let element = event.target; 
              // 节点中有一个属性dataset, 可以获取到自定义节点的属性与属性值
              // console.log(element.dataset); 
              // 注意: 自定义属性都被浏览器转为了小写
              let {categoryname} = element.dataset;
              if(categoryname){
               alert('拿到了') 
              }
            }
        
      2. 如何区分那个是 1, 2, 3 级分类的名字, id?

        把子节点中的a标签, 动态绑定自定义的属性:data-category1Id="c1.categoryId", 利用不同的id来区分

        • 在a标签中定义自定义属性:data-category1Id="c1.categoryId":
        <div class="all-sort-list2" @click="goSearch">
                    <div class="item" v-for="(c1, index) in categoryList" :key="c1.categoryId"
                      :class="{ cur: currentIndex == index }">
                      <!-- 一级分类 -->
                      <h3 @mouseenter="changeIndex(index)">
                      //在每个a标签中动态绑定:data-category1Id="c1.categoryId"自定义属性来区分不同级别的分类
                        <a :data-categoryName="c1.categoryName" :data-category1Id="c1.categoryId">{{ c1.categoryName }}</a>
                      </h3>
                      <!--, 三级分类 -->
                 	  <!-- 鼠标事件:显示或显示三级分类 -->
                      <div class="item-list clearfix" :style="{ display: currentIndex == index ? 'block' : 'none' }">
                        <div class="subitem" v-for="(c2, index) in c1.categoryChild" :key="c2.categoryId">
                          <dl class="fore">
                            <dt>
                              <a :data-categoryName="c2.categoryName" :data-category2Id="c2.categoryId">{{ c2.categoryName
                              }}</a>
                            </dt>
                            <dd>
                              <em v-for="(c3, index) in c2.categoryChild" :key="c3.categoryId">
                                <a :data-categoryName="c3.categoryName" :data-category3Id="c3.categoryId">{{ c3.categoryName
                                }}</a>
                              </em>
                            </dd>
                          </dl>
                        </div>
                      </div>
                    </div>
                  </div>
        
        • 实现点击a标签进行路由跳转

        [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2RHUETzd-1660641621516)(C:/Users/w3112/Desktop/vue%E7%AC%94%E8%AE%B0/vue%E9%A1%B9%E7%9B%AEimg/%E5%9C%B0%E5%9D%80%E6%A0%8Flocation+query%E5%8F%82%E6%95%B0.png)]

        [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oEDhfspb-1660641621517)(C:/Users/w3112/Desktop/vue%E7%AC%94%E8%AE%B0/vue%E9%A1%B9%E7%9B%AEimg/%E8%B7%AF%E7%94%B1%E8%B7%B3%E8%BD%AC.png)]

        goSearch(event) {
              // this.$router.push('/search')
              //获取点击目标元素
              let element = event.target;
              // 节点中有一个属性dataset, 可以获取到自定义节点的属性与属性值
              console.log(element.dataset); 
              // 自定义属性都为小写
              let { categoryname, category1id, category2id, category3id } = element.dataset;
              // 判断是否为a标签
              if (categoryname) {
                // 整理路由参数
                let location = {name: 'search'};
                let query = {categoryName: categoryname}; //定义地址路径的初始值categoryName:categoryname
                // 判断1, 2, 2级分类
                if (category1id) {
                  //时自定义属性获取到的id赋给query参数下的Id
                  query.category1Id = category1id;
                }else if(category2id){
                  query.category2Id = category2id;
                }else {
                  query.category3Id = category3id;
                }
                // 整理完参数, 使location与query合并
                location.query = query;
                // 路由跳转
                this.$router.push(location);
              }
            }
        

D4

00 复习一下

三级联动业务:
1.前面基础课程当中v-for很少使用index,以后在写项目的时候,index索引值切记加上
2.防抖与节流【面试经常出现】
3.vuex可以模块式开发
vuex经常用的套路是state、mutations、actions、getters、modules

4.组件name属性的作用?
1开发者工具中可以看见组件的名字
2注册全局组件的时候,可以通过组件实例获取相应组件的名字

01控制(TypeNav组件)中三级分类在search模块的显示与隐藏和过渡效果

1, 三级联动在search一会显示、一会隐藏 —解决方案:通过一个响应式属性(v-show="show")控制三级联动显示与隐藏

mounted() {
    // 通知Vuex发送请求, 获取数据, 存储在仓库中
    this.$store.dispatch('categoryList')
    // 当组件挂载完毕, 让show的值变为false
    // 如果不是Home组件则将typeNav隐藏
    if (this.$route.path != '/home') {
      this.show = false
    }
  },
methods: {
    // 一级分类鼠标移入修改响应式currentIndex属性
    // changeIndex(index) {
    //   this.currentIndex = index;
    // },
    // 设置节流, 每隔100ms触发一次
    changeIndex: throttle(function (index) {
      this.currentIndex = index;
      // console.log('---');
    }, 100),
    // 一级分类鼠标移出修改响应式currentIndex属性
    leaveIndex() {
      this.currentIndex = -1;
      // 鼠标移出, 隐藏search组件的一级分类商品列表显示
      if (this.$route.path != '/home') {  //判断是否在home模块的路径中
        this.show = false
      }
    },
    // 鼠标移入 显示search组件的一级分类商品列表显示
    enterShow() {
      this.show = true
    },
}

2, 过渡效果

通过Vue的内置组件transition完成
注意: 你可以给 (某一个节点)|(某一个组件)添加过渡动画效果, 但是需要注意,节点|组件务必出现v-if|v-show指令才可以使用。

 <!-- 三级联动:  -->
        <transition name='sort'> // 在三件联动的外部套一个 transition 就可以使用Vue的内置组件transition了
          <div class="sort" v-show="show">
   			//1, 2, 3,级分类...
          </div>
        </transition>

相关样式:

// 一级分类鼠标移入过渡动画
    // 过渡动画开始
    .sort-enter {
      height: 0px;
      opacity: 0.4;
    }
    // 过渡动画结束
    .sort-enter-to {
      height: 461px;
      opacity: 1;
    }
    // 定义动画的过渡时间, 速率
    .sort-enter-active {
      transition: all 1s linear;
      overflow: hidden;  //使用overflow: hidden解决, 过渡时文字先显示的问题
    }
//移出就是.sort-leave

02TypeNav组件三级联动性能优化

项目:home切换到search或者search切换到home,你会发现一件事情,组件在频繁的向服务器发请求,
获取三级联动的数据进行展示。项目中如果频繁的向服务器发请求,很耗费性能,因此咱们需要进行优化。

为什么会频繁的向服务器发请求获取三级联动的数据那?

因为路由跳转的时候,组件会进行销毁的【因此频繁的获取三级联动的数据】只需要发一次请求,获取到三级联动的数据即可,不需要多次。

解决方案: 在App中发送请求:

mounted() {
    // 发送一个action || 获取商品分类的三级列表数据(因为App组件最先执行, 在其他组件请求数据时, App里早已准备就绪, 所以请求只会发送一次)
    //this.$store.dispatch('getCategoryList')  //出错
    this.$store.dispatch('categoryList')
  }

出现了错误: 未知的操作类型(unknown action type: getCategoryList)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ARTC1GM-1660641621517)(C:/Users/w3112/Desktop/vue%E7%AC%94%E8%AE%B0/%E9%94%99%E8%AF%AF%E6%97%A5%E5%BF%97img/%E6%9C%AA%E7%9F%A5%E7%9A%84%E6%93%8D%E4%BD%9C%E7%B1%BB%E5%9E%8B.png)]

在网上找了半天, 有说是命名空间的问题, 有说是action 少加了 s, 有说是在仓库(store)的js文件中创建store类实例时没加 modules,

但都解决不了, 最后发现时, 老师改了函数名, 😅麻了

store文件下home模块的index.js文件(配置接口请求)

// actions:  处理cation, 可以书写自己的业务逻辑, 也可以处理异步
const actions = {
  // 通过api里面的接口函数调用, 向服务器发请求, 获取数据
  async categoryList({commit}) {  
      //这个地方的categoryList 要与调用时的名字一样this.$store.dispatch('categoryList')
    let result = await reqCategoryList();
    // console.log(result);
    if(result.code == 200) {
      commit('CATEGORYLIST', result.data)
    }
  }
};

03query与params参数合并

header文件夹下的index.vue: 实现功能: 先在搜索框里输入关键字, 再点击分类

<button class="sui-btn btn-xlarge btn-danger" type="button" @click="goSearch">搜索</button> //搜索按钮

---------分割线----------

goSearch() {
		//判断路由跳转时是否含有query参数
        let location = {
          name: 'search',
          //动态给location配置对象添加params参数
          params: { keyword: this.keyword || undefined },
        }
      if (this.$route.query) {
        // 动态给location配置对象添加query参数
        location.query = this.$route.query;
        // 路由跳转
        this.$router.push(location)
      }
    }
  }

TapNav公共模块index.vue : 实现功能: 先点击分类, 再在搜索框里输入关键字

// 路由跳转的方法
    goSearch(event) {
        //判断路由跳转时是否含有params参数
        if (this.$route.params) {
          //动态给location配置对象添加query参数
          location.query = query;
          //动态给location配置对象添加params参数
          location.params = this.$route.params;
          // 路由跳转
          this.$router.push(location);
        }
      }
    },

注意这里控制台会警告:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6I3SSLGh-1660641621518)(C:/Users/w3112/Desktop/vue%E7%AC%94%E8%AE%B0/%E9%94%99%E8%AF%AF%E6%97%A5%E5%BF%97img/parmas%E5%8F%82%E6%95%B0%E6%9C%AA%E4%BC%A0%E9%80%92.png)]

这里警告意思是 params参数没有传递, 因为params传递参数都需要配置占位符 。如果 /search后面的占位符没有值的时候, 也就是没有传递params参数时, 是无法跳转到 search组件的,

解决方式:

routes: {
	{ 
	//在 :keyword占位符后面加上一个问号 ? 表示 params参数 可传可不传 
      path: "/search/:keyword?", 
      	//params 参数需要进行占位, 注意路径里不要有空格, 否则也会报错
      component: Search,
      meta: {show:true},
      name: 'search'  
    }
}

地址栏显示: http://192.168.1.3:8080/#/search/华为?categoryName=手机&category3Id=61 ?前面为params, ?后面为query参数

04 首页轮播图

04.1mock模拟数据

  1. 开发listContainer|Floor组件业务
    场景:开发项目,产品经理画出原型,前端与后端人员需要介入(开发项目),leader(老大)刚开完会,前端与后端负责哪些模块,后端人员(…开发服务器),
    前端人员【项目起步、开发静态页面、查分静态组件】,回首一看后台‘哥哥’,接口没有写好,这种情况,前端人员可以mock一些数据【前端程序员自己模拟的一些假的接口】,当中工作中项目上线,需要把mock数据变为后台哥哥给的接口数据替换。

  2. mock数据。
    注意:因为后台老师没有给我们写好其他接口【老师们特意的:因为就是想练习mock数据】但是项目中mock数据,你就把他当中真实接口数据用就行。

注意:mock(模拟数据)数据需要使用到mockjs模块,可以帮助我们模拟数据, mockjs【并非mock.js mock-js】
官方地址: http://mockjs.com/

mock官网一句话:生成随机数据,拦截 Ajax 请求
拦截ajax请求:请求发布出去【浏览器会进行拦截】,只是项目当中本地自己玩耍数据。

步骤:

  1. 安装依赖包mockjs : npm install --sace mockjs 有镜像的话用 cnpm

  2. 在src文件夹下创建一个文件夹,文件夹mock文件夹。

  3. 准备模拟的数据
    把mock数据需要的图片放置于public文件夹中!
    scr/mock 文件夹下的 banner.json: (模拟的数据), JSON:没有空格,最好使用格式化插件进行格式化

    [
      {
        "id": "1",
        "imgUrl": "/images/banner1.jpg"
      },
      {
        "id": "2",
        "imgUrl": "/images/banner2.jpg"
      },
      {
        "id": "3",
        "imgUrl": "/images/banner3.jpg"
      },
      {
        "id": "4",
        "imgUrl": "/images/banner4.jpg"
      }
    ]
    

第四步:在mock文件夹中创建一个mockserver.js文件
注意:在mockserver.js文件当中对于banner.json||floor.json的数据没有暴露,但是可以在server模块中使用。
对于webpack当中一些模块:图片、json,不需要对外暴露,因为默认就是对外暴露。

第六步:回到入口文件(根目录src下的main.js),引入mockserver.js

import Vue from 'vue'
import App from './App.vue'
// 三级联动组件 --- 全局组件
import TypeNav from '@/components/TypeNav';
// 第一个参数: 全局组件的名字 第二个参数: 哪一个组件
Vue.component(TypeNav.name, TypeNav);
// 引入路由
import router from '@/router'
// 引入仓库
import store from '@/store'
//  引入mockServer.js -- mock数据
import '@/mock/mockServer';

第七步:在API文件夹中创建mockRequest【axios实例:baseURL:‘/mock’】
专门获取模拟数据用的axios实例。

api文件下的mockAjax.js (配置模拟请求):

const requests = axios.create({
  // 配置对象
  // baseURL: '/api',  //真实的接口 /api/xxxx
  baseURL:'/mock',  //模拟的数据/mock/xxxx
  // 设置响应超时 时间
  timeout: 5000,
});

api文件下的index.js (对api进行统一管理, 获取接口):

// 获取banner(home首页轮播图接口)
export const reqGetBannerList = () => {
  return mockAjax.get('/banner');
}

在开发项目的时候:切记,单元测试,某一个功能完毕,一定要测试是否OK

04.2在home仓库(store)中引入

store/home 文件夹下的 index.js :

// 引入首页三级联动列表接口, 引入首页轮播图的接口(模拟)
import { reqCategoryList, reqGetBannerList } from "@/api";

// home模块小仓库
// state: 仓库存储数据的地方
const state = {
  // 服务器返回数组, 对象   (是根据接口返回值初始化的)
  categoryList: [],
  //轮播图数据
  bannerList: [],
};
// mutations: 修改state的唯一手段
const mutations = {
  CATEGORYLIST(state, categoryList) {
    state.categoryList = categoryList
  },
  GETBANNERLIST(state, bannerList) {
    state.bannerList = bannerList //把请求到的bannerList里的数据存到state里的bannerList, 
  }
};
// actions:  处理cation, 可以书写自己的业务逻辑, 也可以处理异步
// 获取首页的三级联动列表数据, 首页轮播图的数据(模拟)
const actions = {
  // 通过api里面的接口函数调用, 向服务器发请求, 获取数据
  async categoryList({commit}) {
    let result = await reqCategoryList();
    // console.log(result);
    if(result.code == 200) {
      commit('CATEGORYLIST', result.data)
    }
  },
  async getBannerList() {
    let result = await reqGetBannerList();  //await 等待异步(async)请求响应结果
    // console.log(result);
    if(result.code == 200) {
      //commit: 把GETBANNERLIST提交到 result.data, 但actions里不能修改数据, 所以要到mutation里进行修改
      commit('GETBANNERLIST', result.data);  
    }
  }
};
// getters: 类似于计算属性, 用于简化仓库数据, 让组件获取仓库的数据更加方便
const getters = {};
export default {
  state,
  mutations,
  actions,
  getters,
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NJz2Btbs-1660641621519)(C:/Users/w3112/Desktop/vue%E7%AC%94%E8%AE%B0/vue%E9%A1%B9%E7%9B%AEimg/home%E4%BB%93%E5%BA%93.png)]

04.3swiper的基本使用

  1. swiper基本的使用?
  2. swiper可以在移动端使用?还是PC端使用?
    答:swiper移动端可以使用,pc端也可以使用。
  3. swiper常用于哪些场景?
    常用的场景即为轮播图----【carousel:轮播图】
    swiper最新版本为7版本的,项目当中使用的是5版本

https://www.swiper.com.cn/ 官网地址

  • Swiper使用步骤:
    第一步:引入依赖包【swiper.js|swiper.css】
    第二步:静态页面中结构必须完整【container、wrap、slider】,类名不能瞎写
    第三步:初始化swiper实例

D5

01 完成首页轮播图

  • 第一种解决方案,
    同学的想法:都不是完美的【错误的想法】
    created里面:created执行与mounted执行,时间差可能2ms,白扯
    updated里面:如果组件有很多响应式(data),只要有一个属性值发生变化updated还会再次执行,再次初始化实例。

总结:第一种解决方案可以通过延迟器(异步)去解决问题,
但是这种解决方案存在风险(无法确定用户请求到底需要多长时间),因此没办法确定
延迟器时间。

  • 第二种解决方案: wacth + $nextTick

    watch:监听属性,watch可以检测到属性值的变化,当属性值发生变化的时候,可以执行一次。

    nextTick官网解释:
    下次DOM更新, 循环结束之后,执行延迟回调。在 修改数据之后 立即使用这个方法,获取更新后的DOM
    注意:组件实例的$nextTick方法,在工作当中经常, 结合第三方插件使用,获取更新后的DOM节点

  watch: {
     // 在new swiper实例之前, 页面中结构必须已经存在
    bannerList(newValue, oldValue) {
      this.$nextTick(() => {
        // 当执行这个回调的时候: 保证服务器的数据一定回来了(这时候v-for已经执行完毕了)
        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',
          },
        })
      })
    }
  }

02 开发Floor组件

与前面的三级联动组件, 轮播图 流程一样:

  1. 在api获取模拟数据接口
// 获取banner(home首页轮播图接口)
export const reqGetBannerList = () => {
  return mockRequests.get('/banner');
}
// 获取floor数据
export const reqGetFloorList = () => {
  return mockRequests.get('/floor');
}
  1. 在仓库(store发送请求)
import { reqCategoryList, reqGetBannerList, reqGetFloorList } from "@/api";
// state: 仓库存储数据的地方
const state = {
  // 服务器返回数组, 对象   (是根据接口返回值初始化的)
  categoryList: [],
  //轮播图数据
  bannerList: [],
  // floor数据
  floorList: []
};
// mutations: 修改state的唯一手段
const mutations = {
  CATEGORYLIST(state, categoryList) {
    state.categoryList = categoryList
  },
  GETBANNERLIST(state, bannerList) {
    state.bannerList = bannerList //把请求到的bannerList里的数据存到state里的bannerList,
  },
  GETFLOORLIST(state, floorList) {
    state.floorList = floorList
  }
};
const actions = {
  // 获取首页的三级联动列表数据
  async categoryList({commit}) {
    let result = await reqCategoryList();
    // console.log(result);
    if(result.code == 200) {
      commit('CATEGORYLIST', result.data)
    }
  },
  // 获取首页轮播图的数据(模拟)
  async getBannerList({commit}) {
    let result = await reqGetBannerList();  //await 等待异步(async)请求响应结果
    // console.log(result);
    if(result.code == 200) {
      commit('GETBANNERLIST', result.data);  
    }
  },
  // 获取Floor数据
  async getFloorList({commit}) {
    let result = await reqGetFloorList();
    if(result.code == 200) {
      commit('GETFLOORLIST', result.data)
    }
  }
};
  1. 在组件内派发(触发)action, (不过这里不是在floor组件内, 而是在父组件home组件内, 因为floor要进行复用, 在floor组件内进行v-for遍历, 无法遍历出两个不同的对象, 因此要在父组件内遍历, 在传给子组件floor)
//遍历从仓库获取到的数据
<Floor v-for="(floor, index) in floorList" :key="floor.id" :lsit="floor"></Floor>
------分割线--------
import { mapState } from "vuex";
export default {
  name: "Home",
  mounted() {
    // 派发action, 通知vuex发送请求
    this.$store.dispatch('getFloorList');
  },
   //获取仓库里存储的数据 
  computed: {
    ...mapState({
      floorList: (state) => {
        return state.home.floorList;
      }
    })
  }
}

v-for|v-show|v-if|这些指令可以在自定义标签(组件)的身上使用

03组件之间通信

组件间通信面试必问的东西
props:父传子(最常用)
插槽:父传子
自定义事件($on, $emit):子传父
全局事件总线$bus:万能
pubsub:万能(但在vue中几乎不用)
Vuex:万能
$ref:父子通信

在home组件:floorList='floor'把数据转给子组件Floor

子组件Floor接受数据, 并进行操作

<script>
import Swiper from 'swiper';
export default {
  name: 'Floor',
  props: ['floorList'], //接受父组件传过来的数据
  mounted() {
    var mySwiper = new Swiper(this.$refs.cur, {
      loop: true, // 循环模式选项
      // 如果需要分页器
      pagination: {
        el: '.swiper-pagination',
        clickable: true
      },
      // 如果需要前进后退按钮
      navigation: {
        nextEl: '.swiper-button-next',
        prevEl: '.swiper-button-prev',
      },
    })
  }
}
</script>

为什么在Floor组件的mounted中初始化SWiper实例轮播图可以使用. 因为父组件的mounted发请求获取Floor组件,当父组件的mounted执行的时候。Floor组件结构可能没有完整,但是服务器的数据回来以后Floor组件结构就一定是完成的了,因此v-for在遍历来自于服务器的数据,如果服务器的数据有了,Floor结构一定的完整的。
否则,你都看不见Floor组件

04 carousel全局组件

如果项目当中出现类似的功能,且重复利用,封装为全局组件----【不封装也可以】,但要有封装的思想

为了封装全局的轮播图组件:让Floor与listContainer组件中的代码一样,如果一样完全可以独立出来封装为一个全局组件。

<template>
  <div class="swiper-container" ref="mySwiper">
    <div class="swiper-wrapper">
//遍历 一个数组list, 这个list数组里的数据取决于 父元素传过来的数据, 其他没太大改动,还是swiper结构
      <div class="swiper-slide" v-for="(carousel, index) in list" :key="carousel.id">
        <img :src="carousel.imgUrl" />
      </div>
    </div>
    <!-- 如果需要分页器 -->
    <div class="swiper-pagination"></div>
    <!-- 如果需要导航按钮 -->
    <div class="swiper-button-prev"></div>
    <div class="swiper-button-next"></div>
  </div>
</template>
<script>
import Swiper from 'swiper';  // 引入 swiper
export default {
  name: 'Carousel',
  props: ['list'],  //接受父组件传来的数据
    //这个watch对象会对, Floor组件与首页轮播图组件都能生效, 也因此才能封装全局组价
  watch: {
    list: {
      immediate: true,
      // 在new swiper实例之前, 页面中结构必须已经存在
      handler() {
        this.$nextTick(() => {
          // 当执行这个回调的时候: 保证服务器的数据一定回来了(这时候v-for已经执行完毕了)
          var mySwiper = new Swiper(this.$refs.mySwiper, {
            loop: true, // 循环模式选项
            // 如果需要分页器
            pagination: {
              el: '.swiper-pagination',
              clickable: true
            },
            // 如果需要前进后退按钮
            navigation: {
              nextEl: '.swiper-button-next',
              prevEl: '.swiper-button-prev',
            },
          })
        })
      } } } }
</script>

D6

01 获取search模块的数据

vue模块化开发的步骤:

  1. 书写静态页面【布局、样式】
  2. 拆分组件
  3. 获取服务器数据展示数据
  4. 玩业务

流程还是老样子:

  1. 在api获取数据接口 (也包括了之前的轮播图,三级联动等等, 全部复制过来看的清除一点)

    // 对api 进行统一管理
    // 引入二次封装的axios(请求, 响应拦截器)
    import requests from "./request";
    // 引入mock模拟数据请求
    import mockRequests from "./mockAjax"
    // 三级联动接口: /api/product/getBaseCategoryList get请求, 无参数
    export const reqCategoryList = ()=> {
      // 发送请求: axios 发请求返回结果是一个Promise对象
      return requests({url: '/product/getBaseCategoryList', method:'get'})
    }
    // 获取banner(home首页轮播图接口)
    export const reqGetBannerList = () => {
      return mockRequests.get('/banner');
    }
    // 获取floor数据
    export const reqGetFloorList = () => {
      return mockRequests.get('/floor');
    }
    // 获取search数据接口: 地址:/api/list, 请求方式:post, 需要参数
    // 这个接口在获取search模块的数据时, 要给服务器传递一个默认参数(至少是一个空对象)
    export const reqGetSearchInfo = (params) => {
      return requests({url: '/list', method:'post', data:params});
    }
    
  2. 在仓库(store)发送请求, 不过之前都是在home仓库中进行, 这里要在search里写

    // search模块小仓库
    import { reqGetSearchInfo } from "@/api";
    // state: 仓库存储数据的地方
    const state = {
      searchList: {},   // 初始值为一个对象
    };
    // mutations: 修改state的唯一手段
    const mutations = {
      GETSEARCHLIST(state, searchList) {
        state.searchList = searchList;
      }
    };
    // actions:  处理cation, 可以书写自己的业务逻辑, 也可以处理异步
    const actions = {
      // 获取search模块数据
      async getSearchList({commit}, params={}) {
      // 当前这个reqSearchInfo这个函数在调用获取服务器数据的时候, 至少要传递一个参数(至少是空对象)
        // params形参是在: 用户派发action的时候, 作为第二个参数传进来的, 至少是一个空对象
        let result = await reqGetSearchInfo(params);
        if(result.code == 200) {
          commit('GETSEARCHLIST', result.data);
        }
      }
    };
    // getters: 类似于计算属性, 用于简化仓库数据, 让组件获取仓库的数据更加方便
    const getters = {};
    export default {
      state,
      mutations,
      actions,
      getters,
    }
    
  3. 在组件内(search)派发action, 获取仓库数据

    mounted() {
       // 派发action
       this.$store.dispatch('getSearchList', {});
    },
    computed: {
        // mapGetters里面的写法: 传递的数组, 因为Getters计算是没有划分模块的(没有分home, search模块)
        ...mapGetters(['goodsList'])
      },
    

02 search模块的根据不同参数显示不同数据

  data() {
    return {
      searchParams: {
        // 1, 2, 3, 级分类的id, 从home跳转到search时传入
        category1Id: "",
        category2Id: "",
        category3Id: "",
        // 分类名字, 关键字
        categoryName: "",
        keyword: "",
        // 排序
        order: "",
        // 分页器, 代表当前是第几页
        pageNo: 1,
        // 代表每一个展示数据的个数
        pageSize: 3,
        // 平台售卖属性操作带的参数
        props: [],
        // 品牌
        trademark: "",
      }
    }
  },
  beforeMount() {  //在挂载之前合并参数
    // 复杂的写法
    // this.searchParams.category1Id = this.$route.query.category1Id;
    // this.searchParams.category2Id = this.$route.query.category2Id;
    // this.searchParams.category3Id = this.$route.query.category3Id;
    // this.searchParams.categoryName = this.$route.query.categoryName;
    // this.searchParams.keyword = this.$route.query.keyword;
    Object.assign(this.searchParams, this.$route.query, this.$route.params);
    console.log(this.searchParams, this.$route.query, this.$route.params);
  },
  mounted() {  //由于只在挂载的时候,调用了getData()(请求search数据), 所以只能搜索一次商品
    // // 派发action
    // this.$store.dispatch('getSearchList', {});
    this.getData()
  },
  computed: {
    // mapGetters里面的写法: 传递的数组, 因为Getters计算是没有划分模块的(没有分home, search模块)
    ...mapGetters(['goodsList'])
  },
  methods: {
    // 向服务器发送请求获取search模块数据,(根据参数不同,返回不同的数据)
    // 把这次请求封装为一个函数, 当需要调用时,调用即可
    getData() {
      this.$store.dispatch('getSearchList', this.searchParams);
    }
  }

03解决search只能搜索一次

// 数据监听: 监听组件实例身上的属性的属性值变化 
  watch: {
    // 监听路由的信息是否发生变化, 如果发生变化, 则再次发起请求
    $route(newValue, oldValue) {
      // 第一次请求完毕, 把相应的1, 2, 3级分类的id清空, 让他接受下一次的三级分类
      // this.searchParams.category1Id = '';
      // this.searchParams.category2Id = '';
      // this.searchParams.category3Id = '';
      // 再次发请求之前整理带给服务器参数
      Object.assign(this.searchParams, this.$route.query, this.$route.params);
      // 在次发送 ajax请求
      this.getData()
      console.log(this.searchParams, this.$route.query, this.$route.params);  
    },
  } 

这里我把清空相应的1, 2, 3级分类的id, 放在了第二次与第一次之间, 才生效 (老师放在最后面好像有bug😂)

04 面包屑展示业务

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3kPTtMfh-1660641621519)(C:/Users/w3112/Desktop/vue%E7%AC%94%E8%AE%B0/vue%E9%A1%B9%E7%9B%AEimg/%E9%9D%A2%E5%8C%85%E5%B1%91.png)]

<li class="with-x" v-if="searchParams.categoryName">{{ searchParams.categoryName }}
              <i @click="removeCategoryName">×</i></li> 
// 当有searchParams.categoryName属性时才会显示面包屑
methods: {
    // 向服务器发送请求获取search模块数据,(根据参数不同,返回不同的数据)
    // 把这次请求封装为一个函数, 当需要调用时,调用即可
    getData() {
      this.$store.dispatch('getSearchList', this.searchParams);
    },
    // 删除面包屑名字
    removeCategoryName() {
      // 把带给服务器的参数(name, id)清空, 还需要再次向服务器发请求
      // 清空时传空字符串, 还会把相应的字段带给服务器,  如果设为undefined则不会把相应的字段带给服务器(节省宽带), 但前提是这些字段不是必要的
      this.searchParams.categoryName = undefined;
      this.searchParams.category1Id = undefined;
      this.searchParams.category2Id = undefined;
      this.searchParams.category3Id = undefined;
      this.getData();
      // 地址栏也需要修改, 进行路由跳转(当跳转时如果带有params参数则, 则携带params参数跳转到search, 就是只把query参数干掉)
      if(this.$route.params) {
        this.$router.push({name:'search', params: this.$route.params});
      }
    }
  },

D7

01面包屑处理关键字

<!-- 分类面包屑 -->
        <li class="with-x" v-if="searchParams.categoryName">{{searchParams.categoryName }}<i @click="removeCategoryName">×</i></li>
 <!-- 关键字的面包屑 -->
        <li class="with-x" v-if="searchParams.keyword">{{ searchParams.keyword }}<i @click="removeKeyword">×</i></li>//当有searchParams.keyword属性时才会显示面包屑
----------分割线---------
// 删除面包屑的关键字
    removeKeyword() {
      // 把带给服务器的参数(keyword)清空, 还需要再次向服务器发请求
      this.searchParams.keyword = undefined;
      this.getData();
      // 发出清除关键字的请求
      this.$bus.$emit('clear');
      // 清除params参数
      if(this.$route.query) {
        // 携带query参数进行路由跳转
      	this.$router.push({name: 'search', query: this.$route.query})
      }
    }
  1. 使用去全局组件通信进行兄弟组件间的通讯
//main入口文件 
beforeCreate() {
    // 把vue实例(vm)赋给, Vue原型对象上的$bus属性
    Vue.prototype.$bus = this
  }
---------------------
//Header
mounted() {
    // 在挂载时, 回应兄弟组件search的清除关键字请求
    this.$bus.$on('clear', () => {
      this.keyword = '';
    })
  }

02 完成品牌 与 平台属性的业务

我们还是需要收集用户选择的数据,把用户选择的数据信息,给服务器传递获取,获取相应的数据进行展示
组件通信-----(工作使用频率非常高、面试的时候经常出现)
父子:props、插槽、ref
子父:自定义事件 (如: @xx)
万能:vuex、$bus、pubsub

<!-- 分类面包屑 -->
            <li class="with-x" v-if="searchParams.categoryName">{{ searchParams.categoryName }}<i @click="removeCategoryName">×</i></li>
<!-- 关键字的面包屑 -->
            <li class="with-x" v-if="searchParams.keyword">{{ searchParams.keyword }}<i @click="removeKeyword">×</i></li>
<!-- 品牌的面包屑 -->
            <li class="with-x" v-if="searchParams.trademark">{{
             //split('xx')以xx分割字符串为数组                                                 searchParams.trademark.split(":")[1] }}<i @click="removeTrademark">×</i></li> 
<!-- 平台售卖属性的面包屑 -->
            <li class="with-x" v-for="(attrValue, index) in searchParams.props" :key="index">{{ attrValue.split(":")[1] }}
              <i @click="removeAttr(index)">×</i>  </li>
-----------------分割-----------------------------------
// 删除面包屑名字
    removeCategoryName() {
      // 把带给服务器的参数(name, id)清空, 还需要再次向服务器发请求
      // 清空时传空字符串, 还会把相应的字段带给服务器,  如果设为undefined则不会把相应的字段带给服	务器(节省宽带)
      this.searchParams.categoryName = undefined;
      this.searchParams.category1Id = undefined;
      this.searchParams.category2Id = undefined;
      this.searchParams.category3Id = undefined;
      this.getData();
      // 地址栏也需要修改, 进行路由跳转(当跳转时如果带有params参数则, 则携带params参数跳转到		       search, 就是只把query参数干掉)
      if (this.$route.params) {
        this.$router.push({ name: 'search', params: this.$route.params });
      }
    },
// 删除面包屑的关键字
    removeKeyword() {
      // 把带给服务器的参数(keyword)清空, 还需要再次向服务器发请求
      this.searchParams.keyword = undefined;
      this.getData();
      // 发出清除关键字的请求
      this.$bus.$emit('clear');
      // 清除params参数
      if (this.$route.query) {
        // 携带query参数进行路由跳转
        this.$router.push({ name: 'search', query: this.$route.query })
      }},
        
// 获取子组件发送的参数: trademark
    trademarkInfo(trademark) {
      // 整理品牌字段参数, (id:品牌名称)
      this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`
      // 再次发送请求
      this.getData();
      console.log('我是父组件', trademark);},
// 删除品牌面包屑
    removeTrademark() {
      // 把带给服务器的参数(tmId, tmName)清空, 还需要再次向服务器发请求
      this.searchParams.trademark = undefined;
      this.getData();}

 //  获取子组件发送的参数: attr, attrValue
    attrInfo(attr, attrValue) {
      console.log(attr, attrValue);
      // 整理参数 属性id: 属性值: 属性名  (注意模板字符串里不能有空格)
      let props = `${attr.attrId}:${attrValue}:${attr.attrName}`;
      // 将整理的参数添加到 data 里的props数组
      // 数组去重(当indexOf检测到数组下标为-1时, 即数组里没有数据时, 才进行添加)
      if (this.searchParams.props.indexOf(props) == -1) {
        this.searchParams.props.push(props);}
      // 再次发送请求
      this.getData();},
// 删除平台售卖属性面包屑
    removeAttr(index) {
      console.log(index);
      // 整理参数 (从点击获取到的索引值开始删除, 每次删除一个)
      this.searchParams.props.splice(index,1);
      // 再次发送请求
      this.getData();}

经典面试题:数组去重[1,2,2,3,4,2];

03完成排序业务

  • :综合与价格按钮,点击谁,谁的背景颜色变为红色。(类名:active)
    谁有类这件事情,区分开综合与价格
  • 将来点击综合||价格,还是需要给服务器发请求
    【价格升序:把这个信息给服务器传递过去,服务器接收到信息,数据库自动把排序这件事情做了,把排序做好的数据返回给你,你展示即可】

order:服务器需要字段,代表的是排序方式
order这个字段需要的是字符串(可以传递也可以不传递)
1:代表综合
2:代表价格
3:asc代表升序
4:desc代表降序
告诉服务器排序方式有几种情况?
“1:asc” “1:desc” “2:asc” “2:desc”

  • 综合与价格箭头
  • 箭头【可以选用阿里图标库】 https://www.iconfont.cn/
  • 对于综合|价格旁边的箭头【动态显示:时而又,时而没有】,带有类名active,拥有箭头
  • 根据1、2区分谁有类名(背景)、谁有箭头
    根据asc|desc区分它用哪一个箭头【上、下】
<li :class="{ active: isOne }" @click="changeOrder('1')">
   <a>综合<span v-show="isOne" class="iconfont":class="{ 'icon-upload': isAsc, 'icon-download': isDesc }"></span></a>
</li>
<li :class="{ active: isTwo }" @click="changeOrder('2')">
    <a>价格<span v-show="isTwo" class="iconfont":class="{ 'icon-upload': isAsc, 'icon-		download': isDesc }"></span></a>
</li>
--------分割线----------------------
    isOne() {
      return this.searchParams.order.indexOf('1') != -1;
    },
    isTwo() {
      return this.searchParams.order.indexOf('2') != -1;
    },
    isAsc() {
      return this.searchParams.order.indexOf('asc') != -1;
    },
    isDesc() {
      return this.searchParams.order.indexOf('desc') != -1;
    },
// 排序操作
    // flag形参: 他是一个标记, 代表用户点的是综合(1) 或者是价格(2) [会在用户点击时传入]
    changeOrder(flag) {
      // 把order的出初始值(data里的order)赋给originOrder
      let originOrder = this.searchParams.order;
      // 获取第一次点击状态 (点击的是综合(1)或者是价格(2))
      // let originFlag = originOrder.split(':')[0];
      let originFlag = flag; // 这里可以用flag 代替originOrder.split(':')[0] 
      // 获取第一次点击状态 (点击的是desc 或者是 asc)
      let originSort = originOrder.split(':')[1];
      // 准备一个空数组 用于存放点击的数据(容器)
      let newOrder = '';
      // 判断点击的是同一个按钮 (要么是综合1(第一次默认值), 要么是价格2)
      if(flag == originFlag) {
        // 如果点击的同一个按钮, 则判断他的originSort 
        newOrder = `${originFlag}:${originSort == 'desc'? 'asc': 'desc'}`
      }else {
        newOrder = `${flag}:${'desc'}`
      }
      this.searchParams.order = newOrder;  
        //注意这个this.searchParams.order是再出的order(data里面的order)
      this.getData();
    }

04分页功能。

分页功能: 防止页面一次渲染大量的数据, 造成卡顿, 面试的时候可能会问呢(自己封装过分页业务吗?)

1:分页器组件需要知道我一共展示多少条数据 ----total【100条数据】

2:每一个需要展示几条数据------pageSize【每一页3条数据】

3:需要知道当前在第几页-------pageNo[当前在第几页]

4:需要知道连续页码数【起始数字结束数字:连续页码数市场当中一般5、7、9】奇数,对称好看----[continues]

01静态分页器封装

分页器结构:

<div class="sui-pagination clearfix">
      <ul>
        <button :disabled="pageNo == 1" @click="$emit('getPageNo', pageNo - 1)">«上一页</button>
        <!-- 注意下面配置的都是起始页与结合页码, 我们动态绑定的时当前页码 -->
        <!-- 起始页码大于1时显示 -->
        <li><a v-if="startNumAndEndNum.start > 1" @click="$emit('getPageNo', 1)">1</a></li>
        <!-- 起始页码大于2时显示 -->
        <li><span v-if="startNumAndEndNum.start > 2">...</span></li>

        <!-- 中间部分 从1开始遍历到结束页码-->
        <li><a v-for="(page, index) in startNumAndEndNum.end" :key="index" v-if="page >= startNumAndEndNum.start"
            @click="$emit('getPageNo', page)" :class='{ active: pageNo == page }'>
            {{ page }}</a></li>
        <!-- 结束页页码小于总页数减1时显示 -->
        <li class="dotted"><span v-if="startNumAndEndNum.end < totalPage - 1">...</span></li>
        <!-- 结束页页码小于总页数时显示 -->
        <li><a v-if="startNumAndEndNum.end < totalPage" @click="$emit('getPageNo', totalPage)">{{ totalPage }}</a></li>
        <button :disabled="pageNo == totalPage">下一页»</button>
        <div class="end"><span>共{{ total }}页</span></div>
      </ul>
    </div>

分页器逻辑:

<script>
export default {
  name: 'Pagination',
  props: ['pageNo', 'pageSize', 'total', 'continues'],
  computed: {
    // 总共多少页
    totalPage() {
      // Math.ceil: 向上取整(总数据 / 分的页数)
      return Math.ceil(this.total / this.pageSize);},
    //计算出连续的页码的起始页码与结束页码(连续的页码为5页)
    startNumAndEndNum() {
      // 结构赋值, 不用使用this.continues this.xxx 
      const { continues, pageNo, totalPage } = this;
      // 定义变量用于保存起始页码与结束页码
      let start = 0, end = 0;
      // 如果总页数没有连续的页码多
      if (continues > totalPage) {
        start = 1;  //起始页码则为1
        end = totalPage;  //结束页码就是总页数
      } else {
        // 总页数比连续的页码多(连续页数分为5页的标准: 当前页码前有两个连续页码, 当前页码后有两个连续页码)
       start = pageNo - parseInt(continues / 2);//起始页码 = 当前页码 - 连续页数 / 2的取整
       end = pageNo + parseInt(continues / 2); //结束页码 = 当前页码 + 连续页码 / 2 的取整
        //因为当前页为1的时候star为负数, 因此要判断start(起始页)的最小初始值  -- 最小不能小于1(如果小于则强制使其等于1)
        if (start < 1) {
          start = 1,  //start 必须是1 不能为0 | 负数
          end = continues;  // 结束页码就等于 连续页码
        }
        //当结束页码大于总页数时超过了页码的长度, 因此要判断end(结束页码)的最大初始值 -- 最大不能大于总页数(如果大于则强制使其等于总页数)
        if (end > totalPage) {
          end = totalPage, //结束页码超过最大页数时, 强制等于最大页数
          start = totalPage - continues + 1; //结束页码超过最大页数时,强制等于最大页数后,在最大结束页码前有四个连续页码, 第一个即为开始页码, 
        }}
      return { start, end }}}}
</script>

经典面试题:v-for与v-if优先级? v-for优先级更高(vue3中v-if更高)

02动态页面完成

在组件标签里传入参数:

<Pagination :pageNo="searchParams.pageNo" :pageSize="searchParams.pageSize" :total="total" :continues="5" @getPageNo="getPageNo"></Pagination>

接受子组件的参数:

 // 自定义事件回调函数, 获取当前第几页
    getPageNo(pageNo) {
      // console.log(pageNo);
        //把子组件传过来的pagNo参数, 赋给this.searchParams.pageNo
      this.searchParams.pageNo = pageNo;
        //再次发送请求
      this.getData();
    }

D8

01 滚动条初始位置

// 滚动行为
  scrollBehavior(to, from, savedPosition) {
    // 返回的y:0代表滚动条在最上方
    return { y:0 }
  }

02 商品详情页

5)push与replace区别?
编程式导航:push 与 replace
能不能记录历史记录:push(能记住历史记录) replace(不能记住历史记录)
目前项目当中:进行路由跳转(编程式导航)基础都是push

  • 开发详情业务

    • 熟悉静态页面、书写样式, 拆分组件

    • 获取服务器动态展示(发送请求, 接收数据—api)

    • vuex

    • 完成动态业务(展示数据)

  1. 完成静态页面, 拆分组价(pages/detail)
  2. 获取接口, 请求数据(api/index)
// 获取产品详情信息的接口 URL: /api/item/{skuId} 请求方式: get
export const reqGoodsInfo = (skuId) => {
  return requests({url:`/item/${skuId}`,method:'get'});
}
  1. vuex(store/detail)
import { reqGoodsInfo } from "@/api"

const state = {
  goodInfo: {}
}
const mutations = {
  GETGOODSINFO(state, goodInfo) {
      state.goodInfo = goodInfo;
  }}
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,
  actions,
  mutations,
  getters,
}
  1. 派发请求并,展示请求数据(pages/detail)
<script>
  import ImageList from './ImageList/ImageList'
  import Zoom from './Zoom/Zoom'
  import { mapGetters } from 'vuex'
  export default {
    name: 'Detail',
    components: {
      ImageList,
      Zoom,},
    mounted() {
      // 派发action 获取产品详情的信息
      this.$store.dispatch('getGoodsInfo', this.$route.params.skuid)},
    computed: {
      ...mapGetters(['categoryView'])
    }}
</script>

03 放大镜静态组件

computed: {
      // 获取仓库中的数据
      ...mapGetters(['categoryView', 'skuInfo','spuSaleAttrList']),
      // 给子组件的数据, 并在没有数据返回时, 至少返回一个空数组
      skuImageList() {
        return this.skuInfo.skuImageList || [];
      }
    }
----------------------------------
props: ['skuImageList'],
 computed: {
      // 接受到父组件传过来的数据时没有对象数据, 至少返回一个空对象
      imgObj() {
        return this.skuImageList[0] || {}
        //如果至少返回一个空对象则会报假错
      }
    }

在工作中假报错现象很常见,因为什么导致的,尽可能解决掉-----【不解决掉对于你的程序没有任何影响】

D9

01完成放大镜业务

5.1遮罩层为什么能动。
获取节点(DOM:必须要定位),通过JS动态修改left|top、定位元素才有left、top属性

methods: {
      // 鼠标移动事件(获取鼠标坐标)
      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;
        if(left >= mask.offsetWidth) left = mask.offsetWidth
        if( top<= 0) top = 0;
        if(top >= mask.offsetHeight) top= mask.offsetHeight
        // 修改遮罩层元素的left|top属性值 (把减去后的边距 赋给遮罩层)
        mask.style.left = left + 'px';
        mask.style.top = top + 'px';
        // 修改大图框元素的left|top属性值
        // 乘以-2 : 大图等于两倍的遮罩框, 且大图的移动方向应该与遮罩框的移动方向相反
        big.style.left = -2 *left + 'px';
        big.style.top= -2 *top + 'px';
      }
    }

02购物车

01 判断加入购物车成功

加入购物车的业务? 购物车项目第二个重要地方
购物车:每一个人都有属于自己的购物车,那为什么不同用户登录自己账号,可以看见属于自己产品
一定是用户点击加入购物车,把你的产品信息提交给服务器进行保存,当你下次在进入购物车的时候,
需要向服务器发请求,获取你购物车里面的信息展示

项目:点击加入购物车按钮的时候,以前经常进行路由跳转【调到另外一个路由】,
但是你要注意,点击加入购物车这个按钮的时候,将用户选择产品,提交给服务器进行存储,如果服务器存储成功,之后在进行路由跳转

  1. 请求接口:
// 获取购物车的数据接口 URL: api/cart/addToCart/{skuId}/{skuName} 方式post
export const reqAddOrUpdateShopCart = (skuId, skuName) => {
  return requests({url:`/cart/addToCart/${skuId}/${skuName}`, method:'post'})
}
  1. 在store里获取action(但没有保存数据在仓库中, 之前都是把数据存在仓库中, 然后组件调用再去捞数据)
// 获取购物车的action
  async addOrUpdateShopCart({commit}, {skuId,skuNum}) {
    let result = await reqAddOrUpdateShopCart(skuId, skuNum)
    // 服务器加入购物车成功
    if(result.code == 200 ) {
      return 'ok'
    }else {
      //加入购物车失败
      return Promise.reject(new Error('faile'));
    }
  }
  1. 派发action
<a @click="addShopCar">加入购物车</a>
-------------------------------------
// 加入购物车的回调函数
    async addShopCar() {  
      //派发action, 通知服务器发送请求, 调用了store(仓库中的addOrUpdateShopCart), 但是要判断加入购物车成功还是失败, 
        //就要在这个方法上添加async, 使其返回一个Promise对象(Promise对象返回要么成功要么失败)
      try {
        await this.$store.dispatch('addOrUpdateShopCart',{skuId:this.$route.params.skuid, skuNum:this.skuNum});
        // 成功时进行路由跳转
		this.$router.push({name: 'addcartsuccess'});
      } catch (error) {
        alert(error.message);
      }
    }

02 购物车进行路由传参结合会话存储

 // 加入购物车的回调函数
    async addShopCar() {  
      //派发action, 通知服务器发送请求, 调用了store(仓库中的addOrUpdateShopCart), 但是要判断加入购物车成功还是失败, 
        //就要在这个方法上添加async, 使其返回一个Promise对象(Promise对象返回要么成功要么失败)
      try {
        await this.$store.dispatch('addOrUpdateShopCart',{skuId:this.$route.params.skuid, skuNum:this.skuNum});
        // 成功时进行路由跳转, 传递简单的query参数skuNum(产品个数)
				this.$router.push({name: 'addcartsuccess', query:{skuNum:this.skuNum}});
				// 会话存储, 一般存储的是字符串,  存储复杂的skuInfo参数(产品数据)
				sessionStorage.setItem('SKUINFO', JSON.stringify(this.skuInfo))  //json.stringify转化为字符串,才能存到真正的数据
      } catch (error) {
        alert(error.message);
      }
    }
--------------------------
//在AddCartSuccess里获取会话存储数据
    computed: {
      skuInfo() {
        //JSON.parse把会话存储里的字符串 转化对象才能用 .xx 捞到数据
        return JSON.parse(sessionStorage.getItem('SKUINFO')) 
      }
    }

面试题:本地存储与会话存储区别?

本地存储: 持久化 —存储容量为 5M

会话存储: 临时性 ---- 会话结束, 数据销毁

面试题:GET与POST
相同点:都是HTTP协议。
不同点:
1:GET请求携带参数是有上限的 post请求携带的参数是没有’上限的’
2:GET请求相对而言不安全,POST安全

面试题:H5新增那些特性?
CSS4、本地存储、多媒体、canvas

D10

01购物车详情

      <h3><i class="sui-icon icon-pc-right"></i>商品已成功加入购物车!</h3>
      <div class="goods">
        <!-- 左侧商品信息 -->
        <div class="left-good">
          <div class="left-pic">
            <img :src="skuInfo.skuDefaultImg">
          </div>
          <!-- 右侧信息 -->
          <div class="right-info">
            <p class="title">{{skuInfo.skuNum}}</p>
            <p class="attr">{{skuInfo.skuDesc}} 数量:{{$route.query.skuNum}}</p>
          </div>
        </div>
        <!-- 右侧购物车 -->
        <div class="right-gocart">
          <router-link class="sui-btn btn-xlarge" :to="`/detail/${skuInfo.id}`">查看商品详情</router-link> //点击携带商品id 调回详情页面
          <router-link to="/shopcart">去购物车结算</router-link>
        </div>
      </div>

02 购物车结算

01临时游客身份

  1. 使用临时id身份 —>uuid, 并携带id发送请求

    • 创建一个uuid_token.js文件
    import {v4 as uuidv4} from 'uuid'
    
    export const getUUID = () => {
      // 从本地获取uuid(查看本地存储里面是否存在)
      let uuid_token = localStorage.getItem('UUIDTOKEN');
      // 如果没有就生成uuid
      if(!uuid_token) {
        // 生成uuid
        uuid_token = uuidv4();
        // 在本地 存储一次
        localStorage.setItem('UUIDTOKEN', uuid_token);
      }
      // 返回uuid
      return uuid_token;
    }
    
    • 在store(仓库)中的detail.js里引入:
    import {getUUID} from '@/utils/uuid_token';
    const state = {
      goodInfo: {},
      // 临时游客身份
      uuid_token: getUUID()
    }
    
    • 在配置api里的request.js(重写axios)中引入store, 配置请求响应头:
    // 引入store仓库
    import store from '@/store'
    ---------
    // 请求拦截器: 在发送请求之前. 请求拦截器可以检测到, 在请求发出去之前做一些事情
    requests.interceptors.request.use((config) => {
      // console.log(store)
      // config: 配置对象, 对象里面有 headers 请求头 这个重要属性
      // 进度条开始
      if (store.state.detail.uuid_token) {
        // 请求头添加一个字段(userTempId)
        config.headers.userTempId = store.state.detail.uuid_token;
      }
      nprogress.start();
      return config;
    });
    
  2. vuex :

  • 创建store大仓库里的 shopcart.js小仓库
import { reqCartList } from "@/api";
const state = {
  // 初始值
  cartList: [],
};
const mutations = {
  GETCARTLIST(state, cartList) {
    state.cartList = cartList
  }
};
const actions = {
  // 获取购物车结算列表数据
  async getCartList({commit}) {
    let result = await reqCartList();
    if(result.code = 200) {
      commit('GETCARTLIST', result.data)
    }
  }
};
const getters = {
  // 购物车数据的外层对象
  cartList(state) {
    return state.cartList[0] || {}
  },  
};
export default {
  state,
  mutations,
  actions,
  getters,
}
  • 在shopcart组件里进行动态配置
import { mapGetters } from 'vuex';
export default {
  name: 'ShopCart',
  mounted() {
    this.getData();
  },
  methods: {
    getData() {
      this.$store.dispatch('getCartList');
    }
  },
  computed: {
    ...mapGetters(['cartList']),
    //数组里的真正数据, 数组
    cartInfoList() {
      return this.cartList.cartInfoList || []
    },
    // 计算购买产品的总价
    totalPrice() {
      let sum = 0
      // 遍历
      this.cartInfoList.forEach(item => {
        sum+=item.skuNum * item.skuPrice
      });
      return sum;
    },
    // 判断底部的全选复选框是否勾选
    isAllCheck() {
      // every 遍历cartInfoList数组,  如果数组中有一个为1 则返回false
      return this.cartInfoList.every(item =>item.isChecked == 1)
    }
  }
}

02处理购物车列表商品数量

复习, 什么时候用router-link、什么时候用编程式导航?
1.1如果一个按钮点击过后,如果只有路由跳转的业务【声明式导航、编程式导航】
1.2如果点击一个按钮除了路由跳转,还有其他的业务,可以选择编程式导航。

          <li class="cart-list-con5">
            <a href="javascript:void(0)" class="mins" @click="handler('minus', -1, cart)">-</a>
            <input autocomplete="off" type="text" minnum="1" class="itxt" :value="cart.skuNum"
              @change="handler('change', $event.target.value * 1, cart)">
            <a href="javascript:void(0)" class="plus" @click="handler('add', 1, cart)">+</a>
          </li>
--------------------------------------------
methods: {
    getData() {
      this.$store.dispatch('getCartList');
    },
    // 修改购物车中商品的数量, 并添加函数节流
    handler: throttle(async function(type, disNum, cart){
      // type: 区分这三个元素(+ - 文本框输入)
      // disNum: 变化量: + (1), - (-1), input输入的数量(不是变化量)
      // cart: 区别是哪一个商品(商品身上带有 id)
      // 最终目的: 向服务器发送请求, 服务器响应, 返回数据修改数量
      switch (type) {
        //加号:
        case "add":
          disNum = 1;
          break;
        //减号
        case "minus":
          // 当商品数量大于一时(商品数量才可以减少) 才可以给服务器传递 -1, 如果等于小于1则传递给服务器0 (商品数量不变)
          disNum = cart.skuNum > 1 ? -1 : 0;
          break;
        // 文本框输入
        case "change":
          //如果输入的是非法数字: 带有汉字, 英文, 特殊符号, 负号等 则给服务器传递 0
          if (isNaN(disNum) || disNum < 1) {
            disNum = 0;
          } else {
            // 取整再见与原来的数量 (防止输入的是小数)
            disNum = parseInt(disNum) - cart.skuNum;
          };
          break;
      }
      // 向服务器派发action
      try {
        //修改成功
        await this.$store.dispatch('addOrUpdateShopCart', {
          // 那个商品的id
          skuId: cart.skuId,
          // 商品的数量
          skuNum: disNum,
        });
        // 再次获取服务器最新数据
        this.getData();
      } catch (error) {
        alert(error.message)
      }
    },1000)

03 购物车商品 的删除 与选中状态

    // 删除商品操作
    async deleteCartById(cart) {
      try {
        await this.$store.dispatch('deleteCartListBySkuId', cart.skuId);
        this.getData();
      } catch (error) {
        alert(error.message)
      }
    },
    // 修改商品的选中状态
    async updateChecked(cart, event) {
      // 修改商品
      try {
        // 带给服务器的参数isChecked, 不是布尔值, 应该是0|1
        let isChecked = event.target.checked ? '1' : '0';
        await this.$store.dispatch('updateCheckedById', { skuId: cart.skuId, isChecked });
        // 再次发送请求
        this.getData()
      } catch (error) {
        alert(error.message)
      }
    },
----------------------------------------
// 获取删除购物车商品的接口 URL: api//cart/deleteCart/{skuId} 方式: delete
export const reqDeleteCartById = (skuId) => {
  return requests({url:`/cart/deleteCart/${skuId}`, method:'delete'})
}
// 修改购物车商品选中状态 URL:/api/cart/checkCart/{skuId}/{isChecked} 方式:get
export const reqUpdateCheckedById = (skuId, isChecked) => {
  return requests({url:`/cart/checkCart/${skuId}/${isChecked}`, method: 'get'})
}

04 删除选中状态的商品与全选

    // 删除选中的全部商品
    async deleteAllCheckedCart() {
      try {
        // 派发actions
        await this.$store.dispatch('deleteAllCheckedCart');
        // 重新发送请求
        this.getData();

      } catch (error) {
      }
    },
    // 全部商品的全选
    async updateAllCartChecked(event) {
      try {
        // 判断是否点击, 点击为1 否则为0
      let isChecked = event.target.checked? "1" : "0"
      // 派发action
      await this.$store.dispatch('updateAllCartIsChecked', isChecked);
      this.getData();
      } catch (error) {
        alert(error.message)
      }}
----------------------------
  // 删除选中的全部商品
  deleteAllCheckedCart({dispatch, getters}) {
    // context: 小仓库, commit(提交mutations修改state) getters(计算属性) dispatch(派发action) state(当前仓库的数据)
    // 获取购物车里的全部数据
    let PromiseAll = [];
    getters.cartList.cartInfoList.forEach(item => {
    let promise = item.isChecked == 1?  dispatch('deleteCartListBySkuId', item.skuId) : '';
    // 将每一次的返回的promise添加到数组中
    PromiseAll.push(promise);
    });
    // 只要全部返回成功, 则都成功,   只要有一个失败,则都失败
    return Promise.all(PromiseAll);
  },
  // 修改全选的状态
  updateAllCartIsChecked({dispatch, state}, isChecked) {
    // 初始数组状态
    let promiseAll = [];
    // 遍历购物车商品列表
    state.cartList[0].cartInfoList.forEach((item) => {
      // 派发updateCheckedById 的 action 携带skuId, isChecked
      let promise = dispatch('updateCheckedById', {skuId: item.skuId, isChecked: isChecked,});
      promiseAll.push(promise);
    });
    // 返回最终结果, 只要全部返回成功, 则都成功,   只要有一个失败,则都失败
    return Promise.all(promiseAll);
  }

D11

01 注册与登录静态组件

  1. 获取接口:
// 获取验证码 URL: /api/user/passport/sendCode/{phone}  方式: get
export const reqGetCode = (phone) => {
  return requests({url:`/user/passport/sendCode/${phone}`, method: 'get'})
}

// 获取注册 URL: /api/user/passport/register 方式: post 参数: phone, code, password
export const reqUserRegister = (data) => {
  return requests({url:`/user/passport/register`, data, method:'post'});
}

// 登录 URL: /api/user/passport/login 方式: post 参数: phone password
export const reqUserLogin = (data) => {
  return requests({url:`/user/passport/login`, data, method:'post'});
}
  1. 配置登录(Login)与注册(Register)仓库
//登录与注册仓库
import { reqGetCode, reqUserRegister, reqUserLogin} from "@/api";
// 登录与注册
const state = {
  code: '',
};
const mutations = {
  GETCODE(state, code) {
    state.code = code;
  },
  USERLOGIN(state, token) {
    state.token = token;
  }};
const actions = {
  // 获取验证码
  async getCode({ commit }, phone) {
    let result = await reqGetCode(phone);
    if(result.code == 200) {
      commit('GETCODE', result.data); //需要返回请求结果
      return 'ok'
    }else {
      return Promise.reject(new Error('faile'));}},
  // 用户注册
  async userRegister({commit}, user) {
    let result = await reqUserRegister(user);
    if(result.code == 200) {   //只需要注册返回成功或失败的结果, 不需要返回结果,
      return 'ok';
    }else {
      return Promise.reject(new Error('faile'))}},
  // 用户登录[token]
  async userLogin({commit}, data) {
    let result = await reqUserLogin(data);
    // 拿到token, 用户唯一标识符, 通过token来向服务器获取用户信息
	console.log(result)
    };
const getters = {};
export default {
  state,
  mutations,
  actions,
  getters,}
  1. 派发action
    • 注册组件
  name: 'Register',
  data() {
    return {
      // 收集表单数据 ---手机号
      phone: '',
      // 验证码
      code: '',
      // 密码
      password: '',
      // 确认密码
      password1: '',
      // 是否同意协议
      agree: true
    }},
  methods: {
    // 获取验证码
    async getCode() {
      try {
        const { phone } = this;
        // state 里定义的属性都会放到 vue实例上
        // console.log(this);
        phone && (await this.$store.dispatch('getCode', phone))
        // 将组件中的code属性变为仓库中的验证码
        console.log(this.$store);
        this.code = this.$store.state.user.code;
      } catch (error) {}},
    // 完成注册
    async userRegister() {
      try {
        const { phone, code, password, password1 } = this;
        (phone && code && password == password1) && (await this.$store.dispatch('userRegister', { phone, code, password }));
        // 成功则进行路由跳转
        this.$router.push('/login')
      } catch (error) {
        alert('错误')}}}
    • 登录组件
  name: 'Login',
  data() {
    return {
      phone: '',
      password: '',
    }},
  methods: {
    async userLogin() {
      // 登录回调
      try {
        const { phone, password } = this;
          //同时存才phon与password才能登录成功
        (phone && password) && (await this.$store.dispatch('userLogin', { phone, password }));
        // 登录成功进行路由跳转
        this.$router.push('/home');
      } catch (error) {
        alert(error.message);
      }}}

02携带token登录

获取接口

// 获取用户信息 URL:/api/user/passport/auth/getUserInfo  方式:get 参数: 请求头配置
export const reqUserInfo = () => {
  return requests({url:`/user/passport/auth/getUserInfo`, method:`get`});
}

请求拦截器配置token

// 请求拦截器: 在发送请求之前. 请求拦截器可以检测到, 在请求发出去之前做一些事情
requests.interceptors.request.use((config) => {
  // console.log(store)
  // config: 配置对象, 对象里面有 headers 请求头 这个重要属性
  // uuid的请求头
  if (store.state.detail.uuid_token) {
    // 请求头添加一个字段(userTempId)
    config.headers.userTempId = store.state.detail.uuid_token;
  }
  // 配置token带给服务器
  if(store.state.user.token) {
    config.headers.token = store.state.user.token;
  }
  // 进度条开始
  nprogress.start();
  return config;
});

vuex, user仓库

// 用户登录[token]
  async userLogin({ commit }, data) {
    let result = await reqUserLogin(data);
    // 拿到token, 用户唯一标识符, 通过token来向服务器获取用户信息
    if (result.code == 200) {
      commit("USERLOGIN", result.data.token);
      // 持久化存储 token
      setToken(result.data.token);
      return 'ok'
    } else {
      return Promise.reject(new Error('faile'));
    }
  },
  // 获取用户信息
  async getUserInfo({commit}) {
    let result = await reqUserInfo();
    if(result.code == 200) {
      commit('GETUSERINFO', result.data);
    }},
 --------------------mutations-------
   // 登录
  USERLOGIN(state, token) {
    state.token = token;
  },
  // 用户信息
  GETUSERINFO(state, userInfo) {
    state.userInfo = userInfo
  },
--------------------state------------------
const state = {
  code: '',
  // 如果本地存储的有token则使用本地token
  token: getToken(),
  userInfo: {},
};

token.js:

// 存储token
export const setToken = (token) =>{
  localStorage.setItem("TOKEN", token);
}
// 读取token
export const getToken = () => {
  return localStorage.getItem("TOKEN");
}
// 清空token (退出登录)
export const removeToken = () => {
  localStorage.removeItem("TOKEN");
}

token面试题:项目当中token过期、失效如何处理?
答:清除本地token(本地存储),让用户回到登录页,获取最新的token

一、用户登录以后获取用户信息进行展示

企业项目:登录成功以后,服务器会返回token【存储于vuex当中】,如果想获取用户信息
还需要再发请求【用户信息】,携带token给服务器。
api/user/passport/auth/getUserInfo 获取用户信息的接口

1.1:为什么刷新页面,用户信息就消失
用户刷新页面,用户信息消失没了获取不到,因为token没有携带给服务器。
Vuex存储数据是否持持久化的? 并非持久化, 当浏览器刷新时数据会丢失
1.2:本地存储持久化存储token ----> localStorage.setItem(result.data.token)

1.2为什么去别的模块【非home模块】获取用户信息失败?
因为你去别的模块根本没有发请求获取用户信息,没办法展示用户信了

怎么解决:
每一个组件都在mounted里面发起获取用户信息,进行展示(可以太麻烦)
残留的问题:用户在home模块刷新的时候,用户信息一直在展示(mounted执行的时候在向服务器发请求、获取用户信息展示)

home->search[用户信息刷新数据就没了,因为在search模块当中根本没有发请求获取用户信息]
search-detail[根本没有获取用户信息进行展示]

03退出登陆

二、退出登录
2.1发请求,需要通知服务器,把现在用户身份token【销毁】
2.2清除仓库数据+本地存储数据【都需要清理】

获取接口

// 退出登录 URL: /api/user/passport/logout 方式get
export const reqLogout = () => {
  return requests({url:`/user/passport/logout`, method:'get'});
}

vuex, user仓库

------acations-----
  // 退出登录
  async userLogout({commit}) {
    let result = await reqLogout();
    if(result.code == 200) {
      commit("CLEAR");
      return 'ok';
    }else {
      return Promise.reject(new Error('faile'));
    }}
----------mutations------------
  // 退出登录
  CLEAR(state) {
    state.token = '';
    state.userInfo = {};
    // 清空本地存储
    removeToken();
  }

派发action

------template------
<a class="register" @click="logout">退出登录</a>
-------methods-------  
methods: {
    async logout() {
      // 退出登录, 清空 userInfo, token
      try {
        await this.$store.dispatch('userLogout');
        // 退出成功则返回首页home
        this.$router.push('/home')
      } catch (error) {
}},

三、演示一些操作,你看一下是否正常?
用户已经登录了,用户想从home路由跳转到login路由,不应该这么操作了。
现在用户登录以后,home路由不应该跳转到login路由当中【因为登陆了】,

D12

01 导航守卫

三大守卫:

  1. 全局守卫:
    项目当中任何路由变化都可以检测到,通过条件判断可不可以进行路由跳转。
  2. 前置守卫:路由跳转之前可以做一些事情。
  3. 后置守卫:路由跳转已经完成在执行。

使用前置守卫判断:

// 全局守卫: 前置守卫(路由跳转之前进行判断(守卫))
router.beforeEach(async (to, form, next) => {
  // to: 获取到要跳转到那个路由的信息 console.log(to)
  // from: 获取到你从那个路由跳过来的信息
  // next: (放行,下一步)函数  : next(),  next(path):放行到指定路由的路径 next(false): url路径发生变化, 退回到from点
  // 用户登陆了才会有token
  let token = store.state.user.token;
  // 用户信息(用户名)
  // let userInfo = store.state.user.userInfo;  不能直接判断userInfo, (因为在未登录的状态下, userInfo为空对象, 但是空对象的Boolean值永远为true)
  let name = store.state.user.userInfo.name;
  // 如果用户已经登陆, 则不能跳转login
  if(token) {
    // 如果已经登录则, 把去login的路径, 重定向为 home
    if(to.path == '/login') {
      next('/home')
    }else {
      // 已登录, 但不是跳转login
      // 如果存在用户名
      if(name) {
        next();
      }else {
        // 没有用户信息, 则派发action让仓库存储用户信息在进行跳转
        try {
           //解决了在不同组件, 刷新浏览器, 用户信息(不是登录状态)不存在的问题
          await store.dispatch('getUserInfo');
        } catch (error) {
          // token 过期,  则清空token, 重定向为login再次登录
          // 清空token
          await store.dispatch('userLogout');
          // 重定向路径为login
          next('/login');
    }}}
  }else {
    // 未登录
    next();
  }});

D13

01交易业务

项目十三天:
发现:基础知识点–>回首复习:, 分页, 购物车, 登录、注册

用户地址信息与交易页面

  1. 获取接口
// 获取用户地址信息 URL: /api/user/userAddress/auth/findUserAddressList 方式: get
export const reqAddressInfo = () => {
  return requests({url:`/user/userAddress/auth/findUserAddressList`, method: 'get'});
}
// 获取订单交易页信息 URL: /api/order/auth/trade 方式:get
export const reqOrderInfo = () => {
  return requests({url:`/order/auth/trade`, method:'get'});
}
  1. vuex, trade.js仓库
------------state--------
const state = {
  address: [],
  orderInfo: {},
};
------------mutations--------
const mutations = {
  GETUSERADDRESS(state, address) {
    state.address = address;
  },
  GETORDERINFO(state, orderInfo) {
    state.orderInfo = orderInfo;
  }
};
-----------actions--------------
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);
    } }};
  1. 派发请求action
 mounted() {
    this.$store.dispatch('getUserAddress');
    this.$store.dispatch('getOrderInfo');
  },
computed: {
    ...mapState({
      addressInfo: (state) => {
        return state.trade.address;
      },
      orderInfo: state => state.trade.orderInfo
    }),
    // 提交订单, 最终地址
    userDefaultAddress() {
      // 查找数组中点击的那个地址(isDefault == 1), 且至少有一个空对象
      return this.addressInfo.find(item => item.isDefault == 1) || {};
    }
  },

02 提交订单业务

当用户点击提交订单按钮的时候,需要发请求的
提交订单的请求地址:/api/order/auth/submitOrder?tradeNo={tradeNo}

  1. 获取接口
// 获取提交订单接口 URL: /api/order/auth/submitOrder?tradeNo={tradeNo}  方式: post
export const reqSubmitOrder = (tradeNo, data) => {
  return requests({url:`/order/auth/submitOrder?tradeNo=${tradeNo}`, data, method: 'post'});
}
  1. 不使用vuex , 在main.js入口文件进行配置
// 统一接口api文件夹里面的全部请求函数, 统一引入
import * as API from '@/api';
new Vue({
  render: h => h(App),
  // 注册路由(k,v 省略写法)
  router,
  // 注册仓库: 组件实例身上会多了一个$store 的属性
  store,
  // 配置全局总线, 进行组件通信
  beforeCreate() {
    // 把vue实例(vm)赋给, Vue原型对象上的$bus属性
    Vue.prototype.$bus = this
    // 把所有的api请求, 挂载到Vue原型对象上的$API属性, 所有组件都能使用
    Vue.prototype.$API = API;
  }
}).$mount('#app')
  1. 在Trade组件发送请求
 methods: {
    // 提交订单
    async submitOrder() {
      // 交易编码
      let {tradeNo} = this.orderInfo;
      let data = {
        consignee: this.userDefaultAddress.consignee,  //收件人名字
        consigneeTel: this.userDefaultAddress.phoneNum, //收件人手机号
        deliveryAddress: this.userDefaultAddress.fullAddress, //收件人地址
        paymentWay: "ONLINE", //支付方式 : 在线支付
        orderComment:this.msg,  //买家留言
        orderDetailList: this.orderInfo.detailArrayList, //商品订单
      }
      let result = await this.$API.reqSubmitOrder(tradeNo, data);
      console.log(result);
    }
  }

03 微信支付

前台:需要告诉服务器:谁买的、买的啥、买几个、 支付多少钱、留言信息…
后台:订单号,这笔交易的一个标识符【支付的】

  1. 获取支付信息与支付状态
// 获取支付信息 URL: /api/payment/weixin/createNative/{orderId} 方式: get
export const reqPayInfo = (orderId) => requests({url:`/payment/weixin/createNative/${orderId}`, method: 'get'});

// 获取支付订单状态 URL: /api/payment/weixin/queryPayStatus/{orderId} 方式: get
export const reqPayStatus = (orderId) => requests({url:`/payment/weixin/queryPayStatus/${orderId}`, method: 'get'})
  1. 在pay.vue组件内发送请求
 mounted() {
    // 不要在生命周期函数中写 async|await
    this.getPayInfo();
  },      
mrthods: {
     // 获取支付信息
    async getPayInfo() {
      let result = await this.$API.reqPayInfo(this.orderId);
      // 如果成功, 组件当中存储支付信息
      if (result.code == 200) {
        this.payInfo = result.data;
      }
    }},
  1. 在pay.vue组价里, 使用element-ui创建弹出框, 使用qrcode生成二维码
import QRCode from 'qrcode'
--------------data-------------------
  data() {
    return {
      payInfo: {},
      timer: null,
      // 支付状态码
      code: '',
    }},
---------------methods-------------------
// element 弹出框
    async open() {
      // 生成二维码(地址)
      let url = await QRCode.toDataURL(this.payInfo.codeUrl);  //字符串转图片
      // 弹出框样式
      this.$alert(`<img src=${url} />`, '请你微信支付', {
        dangerouslyUseHTMLString: true,
        // 中间布局
        center: true,
        // 是否显示取消按钮
        showCancelButton: true,
        // 取消按钮的文本样式
        cancelButtonText: '取消支付',
        // 确认按钮文本样式
        confirmButtonText: '支付成功',
        // 显示右上角的'X'
        showClose: false,
      });
      // 支付成功,路由跳转, 支付失败,提示信息
      // 如果没有定时器则, 创建一个定时器
      if (!this.timer) {
        this.timer = setInterval(async () => {
          // 发送请求,获取用户支付状态
          let result = await this.$API.reqPayStatus(this.orderId);
          // 支付成功
          if (result.code == 200) {
            // 清除定时器
            clearInterval(this.timer);
            this.timer = null;
            // 保存支付成功并返回支付码(code)
            this.code = result.code;
            // 关闭弹出框
            this.$msgbox.close();
            // 跳转路由
            this.$router.push('/paysuccess');
            // 关闭弹出框的配置值
            beforeClose: (type, instance, done) => {
              // type: 区分取消|确定按钮
              // instance: 当前组件实例
              // done: 关闭弹出框的方法
              if (type == 'cancel') {
                alert('请联系管理人员');
                // 清除定时器
                clearInterval(this.timer);
                this.timer = null;
                // 关闭弹出框
                done();
              } else {
                // 判断是否真的支付成功了
                if(type == 200){
                  // 清除定时器
                  clearInterval(this.timer);
                  this.timer = null;
                  // 关闭弹出框
                  done();
                }}}}}, 1000) }}

element-ui官方UI组件库(插件)?

react框架:
UI组件库antd【蚂蚁金服旗下PC端UI组件库】
antd-mobile【蚂蚁金服旗下的移动端UI组件库】

Vue框架:
element-UI【饿了吗旗下的UI组件库,官方承认的PC组件库插件】
vant【Vue官方提供移动端UI组件库】

官网地址:https://element.eleme.cn/#/zh-CN
官网地址:https://youzan.github.io/vant/#/zh-CN/

第一步:项目中安装element-ui组件库 [2.15.6版本:Vue2]

第二步:在入口文件引入elementUI组件库
第一种:全部引入【不采用:因为项目中只是用到一个组件,没必要全都引入进来】
第二种:按需引入【按照开发需求引入相应的组件,并非全部组件引入】

第三步:按需引入,安装相应的插件
cnpm install babel-plugin-component -D
文档中说的.babelrc文件,即为babel.config.js文件
修改完babel.config.js配置文件以后,项目重启

第四部:按照需求引入相应的组件即可

Vue.component();
Vue.prototype.$xxx = xxx;

element-ui

使用messageBox显示弹框
6.3展示二维码----qrcode插件
通过qrCode.toDataUrl方法,将字符串转换为加密的在线二维码链接,通过图片进行展示。
moment.js, swiper.js, nprogress.js, qrcode.js

GET|POST:短轮询,请求发一次,服务器响应一次,完事。

第一种做法:前端开启定时器,一直找服务器要用户支付信息【定时器】

第二种做法:项目务必要上线 + 和后台紧密配合
当用户支付成功以后,需要后台重定向到项目某一个路由中,将支付情况通过URL参数形式传给前端,
前端获取到服务器返回的参数,就可以判断了。

D14

01 个人中心路由搭建

1.1当年学习路由的时候:一级路由、二级路由、三级路由 【二级路由搭建】
1.2完成个人中心数据的展示【分页】

2)未登录全局守卫的判断

在前面课程当中:导航守卫【导航:路由发生变化,守卫可以检测到,通过判断,确定这次路由跳转】

前置守卫:在路由跳转之前,进行判断
后置守卫:路由都已经跳转完毕才执行。

未登录的情况:
全局守卫:只要的项目当中任何某一个路由发生变化,就会出发。
项目守卫使用:一般有用前置全局守卫

用户登录:

用户未登录:点击购物车的结算按钮->交易页面【没有登录:去不了】
未登录不能调到支付页面
未登录不能调到支付成功页面
未登录不能去个人中心【都不知道你是谁:展示谁的个人中心啊】

3)路由独享守卫
路由独享守卫:需要在配置路由的地方使用
导航守卫:全局守卫->项目当中有任何路由变化【a->b,b->d】触发。
路由独享守卫:专门负责某一个路由

用户登陆了:
去交易页面:从购物车才能跳转到交易页面。

next():你本来想去哪里,我就放行,你就去完事了。

next(‘/login’):执行守卫放行到执行的路由。

next(false):路由跳转的时候,从哪里来回那里去。

4)组件内守卫---->一般很少用【全局 + 路由独享守卫】
组件内守卫:也是专门负责某一个路由【并非负责全部路由】,写法和路由独享守卫有区别?
组件内守卫需要书写在组件内部

beforeRouteEnter
beforeRouteUpdate (2.2 新增)
beforeRouteLeave

6)路由懒加载
面试【高频的面试】:项目的性能优化手段有哪些?
v-if|v-show:尽可能采用v-show
按需引入【lodash、elementUI】
防抖与节流
路由懒加载:当用户访问的时候,加载对应组件进行展示。

7)图片懒加载
vue-lazyload:图片懒加载
图片:比用用户网络不好,服务器的数据没有回来,
总不可能让用户看白色,至少有一个默认图片在展示。

8)表单验证【后台管理系统:大量使用elementUI】
以后工作的时候经常会进行表单验证【element-ui】进行表单验证,so 简单。
项目当中表单验证功能比较常见的。

8.1vee-validate插件:Vue官方提供的一个表单验证的插件【老师接下来的操作能大概看懂即可】
这个插件很难用:如果你翻看它的文档(看一个月:不保证能看懂),依赖文件很多(文档书写的很难理解)
花大量时间学习,很难搞懂。

8.2哪怕将来工作了,真的使用vee-valadiate【老师项目搞出来:改老师代码即可】

使用步骤:
1:安装vee-valadite,别安装最新版本@2
2:在plugins文件夹中创建一个validate.js[专门注册vee-valadite]
3:注册插件
4:注册插件的时候,用中文,以及需要验证的字段【用中文显示提示形式】
5:在入口文件需要引入执行一次
6:使用vee-valadiate插件

8)vee-validate 基本使用

第一步:插件安装与引入
cnpm i vee-validate@2 --save 安装的插件安装2版本的

import VeeValidate from ‘vee-validate’
import zh_CN from ‘vee-validate/dist/locale/zh_CN’ // 引入中文 message
Vue.use(VeeValidate)

第二步:提示信息
VeeValidate.Validator.localize(‘zh_CN’, {
messages: {
…zh_CN.messages,
is: (field) => ${field}必须与密码相同 // 修改内置规则的 message,让确认密码和密码相同
},
attributes: { // 给校验的 field 属性名映射中文名称
phone: ‘手机号’,
code: ‘验证码’,
password:‘密码’,
password1:‘确认密码’,
isCheck:‘协议’
}
})

第三步:基本使用

{{ errors.first(“phone”) }}

const success = await this.$validator.validateAll(); //全部表单验证
//自定义校验规则
//定义协议必须打勾同意
VeeValidate.Validator.extend(‘agree’, {
validate: value => {
return value
},
getMessage: field => field + ‘必须同意’
})

第一步:项目中安装element-ui组件库 [2.15.6版本:Vue2]

第二步:在入口文件引入elementUI组件库
第一种:全部引入【不采用:因为项目中只是用到一个组件,没必要全都引入进来】
第二种:按需引入【按照开发需求引入相应的组件,并非全部组件引入】

第三步:按需引入,安装相应的插件
cnpm install babel-plugin-component -D
文档中说的.babelrc文件,即为babel.config.js文件
修改完babel.config.js配置文件以后,项目重启

第四部:按照需求引入相应的组件即可

Vue.component();
Vue.prototype.$xxx = xxx;

element-ui

使用messageBox显示弹框
6.3展示二维码----qrcode插件
通过qrCode.toDataUrl方法,将字符串转换为加密的在线二维码链接,通过图片进行展示。
moment.js, swiper.js, nprogress.js, qrcode.js

GET|POST:短轮询,请求发一次,服务器响应一次,完事。

第一种做法:前端开启定时器,一直找服务器要用户支付信息【定时器】

第二种做法:项目务必要上线 + 和后台紧密配合
当用户支付成功以后,需要后台重定向到项目某一个路由中,将支付情况通过URL参数形式传给前端,
前端获取到服务器返回的参数,就可以判断了。

D14

01 个人中心路由搭建

1.1当年学习路由的时候:一级路由、二级路由、三级路由 【二级路由搭建】
1.2完成个人中心数据的展示【分页】

2)未登录全局守卫的判断

在前面课程当中:导航守卫【导航:路由发生变化,守卫可以检测到,通过判断,确定这次路由跳转】

前置守卫:在路由跳转之前,进行判断
后置守卫:路由都已经跳转完毕才执行。

未登录的情况:
全局守卫:只要的项目当中任何某一个路由发生变化,就会出发。
项目守卫使用:一般有用前置全局守卫

用户登录:

用户未登录:点击购物车的结算按钮->交易页面【没有登录:去不了】
未登录不能调到支付页面
未登录不能调到支付成功页面
未登录不能去个人中心【都不知道你是谁:展示谁的个人中心啊】

3)路由独享守卫
路由独享守卫:需要在配置路由的地方使用
导航守卫:全局守卫->项目当中有任何路由变化【a->b,b->d】触发。
路由独享守卫:专门负责某一个路由

用户登陆了:
去交易页面:从购物车才能跳转到交易页面。

next():你本来想去哪里,我就放行,你就去完事了。

next(‘/login’):执行守卫放行到执行的路由。

next(false):路由跳转的时候,从哪里来回那里去。

4)组件内守卫---->一般很少用【全局 + 路由独享守卫】
组件内守卫:也是专门负责某一个路由【并非负责全部路由】,写法和路由独享守卫有区别?
组件内守卫需要书写在组件内部

beforeRouteEnter
beforeRouteUpdate (2.2 新增)
beforeRouteLeave

6)路由懒加载
面试【高频的面试】:项目的性能优化手段有哪些?
v-if|v-show:尽可能采用v-show
按需引入【lodash、elementUI】
防抖与节流
路由懒加载:当用户访问的时候,加载对应组件进行展示。

7)图片懒加载
vue-lazyload:图片懒加载
图片:比用用户网络不好,服务器的数据没有回来,
总不可能让用户看白色,至少有一个默认图片在展示。

8)表单验证【后台管理系统:大量使用elementUI】
以后工作的时候经常会进行表单验证【element-ui】进行表单验证,so 简单。
项目当中表单验证功能比较常见的。

8.1vee-validate插件:Vue官方提供的一个表单验证的插件【老师接下来的操作能大概看懂即可】
这个插件很难用:如果你翻看它的文档(看一个月:不保证能看懂),依赖文件很多(文档书写的很难理解)
花大量时间学习,很难搞懂。

8.2哪怕将来工作了,真的使用vee-valadiate【老师项目搞出来:改老师代码即可】

使用步骤:
1:安装vee-valadite,别安装最新版本@2
2:在plugins文件夹中创建一个validate.js[专门注册vee-valadite]
3:注册插件
4:注册插件的时候,用中文,以及需要验证的字段【用中文显示提示形式】
5:在入口文件需要引入执行一次
6:使用vee-valadiate插件

8)vee-validate 基本使用

第一步:插件安装与引入
cnpm i vee-validate@2 --save 安装的插件安装2版本的

import VeeValidate from ‘vee-validate’
import zh_CN from ‘vee-validate/dist/locale/zh_CN’ // 引入中文 message
Vue.use(VeeValidate)

第二步:提示信息
VeeValidate.Validator.localize(‘zh_CN’, {
messages: {
…zh_CN.messages,
is: (field) => ${field}必须与密码相同 // 修改内置规则的 message,让确认密码和密码相同
},
attributes: { // 给校验的 field 属性名映射中文名称
phone: ‘手机号’,
code: ‘验证码’,
password:‘密码’,
password1:‘确认密码’,
isCheck:‘协议’
}
})

第三步:基本使用

{{ errors.first(“phone”) }}

const success = await this.$validator.validateAll(); //全部表单验证
//自定义校验规则
//定义协议必须打勾同意
VeeValidate.Validator.extend(‘agree’, {
validate: value => {
return value
},
getMessage: field => field + ‘必须同意’
})

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值