vue.config.js中关闭eslint校验

添加第四行

1
2
3
4
5
1. const { defineConfig } = require("@vue/cli-service");
2. module.exports = defineConfig({
3. transpileDependencies: true,
4. lintOnSave: false, // 关闭eslint
5. });

安装less以及less-loader

  • less-loader指定版本5
1
npm install --save less less-loader@5 
  • npm版本太高,安装报错
1
npm install npm@6.14.10 -g // 改为低版本

清除默认样式

在public的index.html中引入reset.css

1
2
<!-- 引入清除默认的样式 -->
<link rel="stylesheet" href="./reset.css" />

安装配置路由

  • 安装路由插件

    1
    npm install --save vue-router

    报错:export ‘default’ (imported as ‘VueRouter’) was not found in ‘vue-router’

    1
    "vue-router": "^4.0.15"

    原因:版本过高

    指定版本安装

    1
    npm i vue-router@3.1.3
  • 配置路由

    1. 创建router文件夹以及index.js文件

    2. 配置index.js文件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      import Vue from "vue";
      import VueRouter from "vue-router";
      // 使用插件
      Vue.use(VueRouter);

      // 引入路由组件
      import Home from "@/pages/Home";

      // 配置路由
      export default new VueRouter({
      routes: [
      {
      path: "/home",
      component: Home,
      meta: { showFooter: true },
      },
      ]
      })
    3. 配置main.js

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      import Vue from "vue";
      import App from "./App.vue";

      Vue.config.productionTip = false;
      // 引入路由
      import router from "@/router";

      new Vue({
      render: (h) => h(App),
      // 注册路由
      router,
      }).$mount("#app");

$route和$router差别

$route:一般获取路由信息,路径、query、params、meta元信息等
$router:一般进行编程式导航进行路由跳转push、replace

meta配置路由元信息showFooter,判断是否需要显示Footer组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 配置路由
export default new VueRouter({
routes: [
{
path: "/home",
component: Home,
meta: { showFooter: true },
},
{
path: "/search",
component: Search,
meta: { showFooter: true },
},
{
path: "/login",
component: Login,
meta: { showFooter: false },
},
{
path: "/register",
component: Register,
meta: { showFooter: false },
},
],
});
1
2
 <!-- 在登录、注册页面隐藏,在首页、搜索页面显示 -->
<Footer v-show="$route.meta.showFooter" />

路由传参

  • params:属于路径中的一部分,在配置路由的时候,需要占位

    1
    path: "/search/:keyWord",  // keyWord占位
  • query:不属于路径中的一部分,不需要占位

  • props

  • params、query传参形式:

    1. 字符串

      1
      this.$router.push("/search/" + this.keyWord + "?k=" + this.keyWord);
    2. 模板字符串

      1
      this.$router.push(`/search/${this.keyWord}?k=${this.keyWord}`);
    3. 对象

      1
      2
      3
      4
      5
      6
      {
      path: "/search/:keyWord",
      component: Search,
      meta: { showFooter: true },
      name: "search", // 需先给路由起别名
      },
      1
      2
      3
      4
      5
      6
      7
      8
      9
      this.$router.push({
      name:"search",
      params:{
      keyWord:this.keyWord
      },
      query:{
      k:this.keyWord
      }
      });
      注意:
      1. 对象的写法可以有name或path,但path不能和params参数一起使用。

      2. params传参问题:

        • 如果已经占位了(params参数),但是传参时没有传递params参数,URL会出现问题。
        1
        2
        3
        4
        5
        6
        7
        8
        9
        this.$router.push({
        name: "search",
        // params: {
        // keyWord: "",
        // },
        query: {
        k: this.keyWord,
        },
        });

        问题:

        1
        2
        3
        4
        // 正常
        http://localhost:8080/#/search/哎哎哎?k=哎哎哎
        // 问题
        http://localhost:8080/#/?k=哎哎哎

        解决:

        1
        path: "/search/:keyWord?",  // 在占位符后面加上?即可
        • 如果已经在占位符后面加了?,但是传递的params参数值为空串,会出现同样的URL错误
        1
        2
        3
        4
        5
        6
        7
        8
        9
        this.$router.push({
        name: "search",
        params: {
        keyWord: "",
        },
        query: {
        k: this.keyWord,
        },
        });

        解决:

        1
        2
        3
        params: {
        keyWord: "" || undefined,
        },
  • props传参:

    1. 方式一:布尔值写法

      1
      2
      3
      4
      5
      6
      7
      {
      path: "/search/:keyWord?",
      component: Search,
      meta: { showFooter: true },
      name: "search",
      props: true, // 需设置成true 注意:props接收的是params参数
      },
    2. 方式二:对象写法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      {
      path: "/search/:keyWord?",
      component: Search,
      meta: { showFooter: true },
      name: "search",
      props: {
      keyWord: "111", // 这时keyWord不是params中的参数,而是自定义的
      }
      },
    3. 方式三:函数写法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      {
      path: "/search/:keyWord?",
      component: Search,
      meta: { showFooter: true },
      name: "search",
      props: ($route) => { // 可以传递params以及query参数
      return {
      keyWord: $route.params.keyWord,
      k: $route.query.k,
      };
      },
      },
  • 参数接收

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <template>
    <div>
    <h1>params参数:{{ $route.params.keyWord }}</h1>
    <h1>query参数:{{ $route.query.k }}</h1>
    <h1>props参数1:{{ keyWord }}</h1>
    <h1>props参数2:{{ k }}</h1>
    </div>
    </template>

    <script>
    export default {
    props: ["keyWord", "k"],
    };
    </script>

注册全局组件

main.js

1
2
3
4
// 注册三级联动全局组件
import TypeNav from "@/pages/Home/TypeNav";
// 第一个参数:全局组件名称;第二个参数:哪一个组件
Vue.component(TypeNav.name, TypeNav);

Postman测试接口

image-20220521160532070

安装axios及配置

  • 安装

    1
    npm install --save axios
  • 二次封装

    1. 创建api文件夹以及request.js

    2. 二次封装

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      // 对axios二次封装

      import axios from "axios";

      // 利用axios中的create方法,创建一个axios实例
      const requests = axios.create({
      // 路径中自动加上/api
      baseURL: "/api",
      // 请求超时时间
      timeout: 5000,
      });

      // 请求拦截器(请求发送前做处理)
      requests.interceptors.request.use((config) => {
      // config:header请求头等属性
      return config;
      });

      // 响应拦截器
      requests.interceptors.response.use(
      (res) => {
      // 成功回调
      return res.data;
      },
      (error) => {
      // 失败回调
      return Promise.reject(new Error("fail"));
      }
      );

      // 暴露
      export default requests;

接口统一管理

使用多个axios相同请求时,可对这些请求进行统一管理,当请求地址改变时便于维护。

  • 在api文件夹新建index.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // API统一管理
    import requests from "./request";

    // 三级联动接口
    // /api/product/getBaseCategoryList get 无参数
    export const reqCategoryList = () => {
    // 发请求
    return requests({
    url: "/product/getBaseCategoryList", // 无需加上api,对axios已进行封装
    method: "get",
    });
    };
  • 接口调用

    1
    2
    import { reqCategoryList } from "@/api";
    let data = reqCategoryList();

解决跨域

跨域:协议、域名、端口号不同的请求

  • 配置代理服务器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const { defineConfig } = require("@vue/cli-service");
    module.exports = defineConfig({
    transpileDependencies: true,
    lintOnSave: false, // 关闭eslint
    // 代理服务器
    devServer: {
    proxy: {
    "/api": {
    target: "http://gmall-h5-api.atguigu.cn",
    // pathRewrite: { "^/api": "" },
    },
    },
    },
    });

nprogress进度条的使用

可视化请求发送与接收

image-20220521171131639

  • 安装

    1
    npm install --save nprogress
  • 配置在request.js中的请求、响应拦截器中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // 引入进度条
    import nprogress from "nprogress";
    // 引入进度条样式
    import "nprogress/nprogress.css";

    // 请求拦截器
    requests.interceptors.request.use((config) => {
    // 进度条开始
    nprogress.start();
    return config;
    });

    // 响应拦截器
    requests.interceptors.response.use(
    (res) => {
    // 进度条结束
    nprogress.done();
    return res.data;
    },
    (error) => {
    return Promise.reject(new Error("fail"));
    }
    );
  • 更改样式

    可在node_modules中更改nprogress.css

vuex状态管理库

vuex是官方提供的插件,集中式管理项目中组件公用的数据。

当项目的数据维护费劲时,就可以引入vuex进行管理。

  • state:仓库存储数据的地方

  • mutations:修改state的唯一手段

  • actions:业务逻辑、异步等

  • getters:state的计算属性

  • modules:模块化

  1. 安装

    1
    npm install --save vuex
  2. 警告

    image-20220521173435210

  3. 版本过高

    1
    npm install vuex@3.4.0 // 安装低版本
  4. 配置仓库

    创建store文件夹及index.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    import Vue from "vue";
    import Vuex from "vuex";

    Vue.use(Vuex);

    const state = {
    count: 1,
    };
    const mutations = {
    ADD(state) {
    state.count++;
    },
    };
    const actions = {
    // 可以书写业务逻辑,不能修改直接state
    add({ commit }) {
    commit("ADD");
    },
    };
    const getters = {};

    // 暴露store类的实例
    export default new Vuex.Store({
    state,
    mutations,
    actions,
    getters,
    });
  5. main.js注册仓库

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 引入仓库
    import store from "@/store";

    new Vue({
    render: (h) => h(App),
    router,
    // 注册仓库
    store,
    }).$mount("#app");
  6. 案例

    1
    2
    <span>{{ count }}</span>
    <button @click="add">点击加1</button>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import { mapState } from "vuex";

    computed: { // 计算属性获取state中的值
    ...mapState(["count"]), // 数组写法
    },
    methods: {
    add() {
    //派发action
    this.$store.dispatch("add");
    },
    },
    • 获取state值的对象写法

      1
      2
      3
      4
      5
      6
      7
      computed: {
      ...mapState({
      categoryList: (state) => {
      return state.count;
      },
      }),
      },

      简写:

      1
      2
      3
      4
      5
      computed: {
      ...mapState({
      categoryList: (state) => state.count
      }),
      },
    过程相当于JAVA中的controller、service、dao层操作。
  7. 模块式开发

    即在大仓库里封装多个小仓库

    1. 创建home、search两个小仓库

      image-20220521183807782

    2. 配置小仓库

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      // home模块小仓库

      const state = {};
      const mutations = {};
      const actions = {};
      const getters = {};

      export default {
      state,
      mutations,
      actions,
      getters,
      };
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      // search模块小仓库

      const state = {};
      const mutations = {};
      const actions = {};
      const getters = {};

      export default {
      state,
      mutations,
      actions,
      getters,
      };
    3. 大仓库引入小仓库

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      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({
      modules: {
      home,
      search,
      },
      });

动态样式

思路:鼠标悬停在哪个一级菜单上,将currentIndex改成对应索引值index。

1
<h3 @mouseenter="changeCurrentIndex(index)" @mouseleave="changeCurrentIndex(-1)">  // 鼠标移进及移出事件
1
2
3
4
5
6
7
8
9
10
11
data() {
return {
currentIndex: -1, //鼠标悬停在哪个一级菜单
};
},

methods: {
changeCurrentIndex(index) {
this.currentIndex = index;
},
},
1
2
3
4
5
6
7
8
 <div
class="item"
v-for="(c1, index) in categoryList"
:key="c1.categoryId"
:class="{ cur: currentIndex == index }"
>
...
</div>
1
2
3
.cur{
background-color: skyblue;
}

上面仅仅为了练习动态样式,更简便的方式:

1
2
3
.item:hover{
background-color: skyblue;
}

防抖与节流+Lodash引入

防抖节流

  • 函数防抖

    如果连续的快速触发函数,则当连续动作结束之后,经过规定的时间才会触发且只会触发一次。

  • 函数节流

    设置两次函数触发之间的最小时间间隔。

引入Lodash

Lodash 是一个一致性、模块化、高性能的 JavaScript 实用工具库。

Lodash中文文档

防抖:

1
2
3
_.debounce(function(){
...
},1000);

节流:

1
2
3
_.throttle(function(){
...
},1000);

安装:(正常vue项目本身就有lodash存在,因为有些依赖依赖着lodash,如果没有在控制台再进行安装)

引入方式:

1
2
// 引入lodash所有功能函数
import _ from "lodash";
1
2
// 按需引入
import throttle from "lodash/throttle";

vue中写法:

1
2
3
4
5
//节流(防抖同理)
changeCurrentIndex: _.throttle(function (index) { //当按需引入时,则不需要加上_.
this.currentIndex = index;
console.log(index);
}, 50),

声明式导航、编程式导航的选择问题

声明式导航:router-link

编程式导航:push|replace

注意:

router-link是一个组件,当页面中多处地方使用router-link时,很耗内存。

如果在a标签上都添加单击事件进行编程式导航,页面会加载多个回调函数,也不是最理想的办法。

最好方法:编程式导航 + 事件委派
1
2
3
4
5
6
<div @click="goSearch">
<a></a>
<h1></h1>
<a></a>
<a></a>
</div>
存在的问题:

如何确定点击的是哪个a标签以及是否是a标签?

  • 自定义属性

    1
    2
    3
    4
    5
    6
    7
    <!-- 1.data-一定要加 2.解析出来的属性名为data-后面的值且全都是小写 -->
    <div @click="goSearch">
    <a :data-index="index1"></a>
    <h1></h1>
    <a :data-index="index2"></a>
    <a :data-index="index3"></a>
    </div>
  • event

    1
    2
    3
    4
    5
    6
    7
    goSearch(event) {
    let ele = event.target;
    let { index } = ele.dataset; // 返回对象,所以得解析对象里的index出来
    if (index) {
    console.log(index);
    }
    },

过渡动画

  • 前提

    组件或元素务必要有v-if或v-show指令才可以进行过渡动画。

  • 给组件或元素包上transition标签

    1
    2
    3
    4
    5
    6
    <!-- 过渡动画 -->
    <transition name="sort">
    <div class="sort" v-show="showSort">
    ...
    </div>
    </transition>
  • 样式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //sort过渡动画样式
    // 开始状态(进入)
    .sort-enter {
    height: 0px;
    }
    // 结束状态 (进入)
    .sort-enter-to {
    height: 461px;
    }
    // 定义动画时间、速率
    .sort-enter-active {
    transition: all 0.5s linear;
    }

优化全局组件

  • 问题:

    TypeNav全局组件被多个组件使用,而TypeNav挂载完毕会向服务器请求数据,这样导致多次向服务器请求相同的数据。

  • TypeNav原本挂载后执行请求获取数据的语句

    1
    2
    // 通知vuex发送请求,获取商品列表数据,存储于仓库中
    this.$store.dispatch("categoryList");
  • 将语句移到App.vue的挂载函数里头,因为app组件为跟组件,只会挂载一次

mockjs模拟数据

  • 作用:生成随机数据,拦截 Ajax 请求

  • 安装

    1
    npm install mockjs
  • 使用

    1. src中创建mock文件夹

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

      image-20220525191004846

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      [
      {
      "id":"1",
      "imgUrl":"/images/banner1.jpg"
      },
      {
      "id":"2",
      "imgUrl":"/images/banner2.jpg"
      },
      {
      "id":"3",
      "imgUrl":"/images/banner3.jpg"
      },
      {
      "id":"4",
      "imgUrl":"/images/banner4.jpg"
      }
      ]

      注意: webpack默认对外暴露:图片、JSON,所以这里无需对外暴露。

    3. 把mock数据需要的图片放在public文件夹中

      image-20220525193002389

    4. 创建mockServer.js

      image-20220525193751410

    5. 使用mockjs插件实现模拟数据

      1
      2
      3
      4
      5
      6
      7
      8
      9
      // 引入mockjs
      import Mock from "mockjs";
      // 引入JSON数据
      import banner from "./banner.json";
      import floor from "./floor.json";

      // mock数据:1、请求的地址 2、请求数据
      Mock.mock("/mock/banner",{code:200,data:banner}); // 模拟首页轮播图数据
      Mock.mock("/mock/floor",{code:200,data:floor}); // 模拟楼层数据
    6. 在main.js(入口文件)中引入mockServer.js

      1
      2
      // 引入mockServer.js
      import "@/mock/mockServer";

swiper使用(watch + nextTick)

  • https://swiper.com.cn

  • 安装

    1
    npm install --save swiper@5
  • 使用

    1. 需要用到的组件引包

      1
      import Swiper from 'swiper';
    2. main.js中引入样式

      1
      2
      // 引入swiper样式
      import "swiper/css/swiper.css";
    3. 页面结构

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      <!--banner轮播-->
      <div class="swiper-container" id="mySwiper">
      <div class="swiper-wrapper">
      <div
      class="swiper-slide"
      v-for="carousel in bannerList"
      :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>
    4. new Swiper实例

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      let mySwiper = new Swiper(".swiper-container",{
      loop:true,
      // 分页器
      pagination:{
      el:".swiper-pagination",
      clickable:true
      },
      // 前进后退按钮
      navigation:{
      nextEl:".swiper-button-next",
      prevEl:".swiper-button-prev"
      }
      });

      注意:Swiper实例必须在页面中数据全部存在(遍历完)后new

    5. 问题:

      这里不能在mouted中new Swiper,因为mouted中要发送ajax请求获取需要遍历的数据,而异步请求时数据还没获取到就new Swiper实例会导致轮播不能正常使用。可以在mouted ajax请求语句底下写一个定时器去new Swiper实例,但是这个解决办法不是完美的。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      mounted() {
      // 获取轮播图数据
      this.$store.dispatch("getBannerList");
      // new Swiper实例
      setTimeout(() => {
      new Swiper实例 // 如果这里没有发送ajax请求获取所需数据,则可以直接在mouted中new实例
      }, 1000);
      },
      computed: {
      ...mapState({
      // 拿到bannerList
      bannerList: (state) => state.home.bannerList,
      }),
      },
    6. 解决办法:watch + nextTick

      如果单纯使用watch监听bannerList也不行,因为当handler执行只能保证数据有了,但不能保证v-for已经执行结束了。

      1
      2
      3
      4
      5
      6
      7
      watch:{
      bannerList:{
      handler(newValue,oldValue){
      new Swiper实例
      }
      }
      }

      所以需要引入nextTick:在下次DOM更新循环结束之后执行延迟回调(相当于定时器,但是更精准)。在修改数据之后立即使用这个方法,获取更新之后的DOM。这样就可以保证v-for遍历结束,即页面中的需要的结构已经有了。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      watch:{
      bannerList:{
      handler(newValue,oldValue){
      this.$nextTick(()=>{
      // 当执行的时候,保证数据已经有了,并且v-for已经遍历完了。
      new Swiper实例
      })
      }
      }
      }
    7. 小细节

      将 id=”mySwiper” 改成 ref=”mySwiper”

      new Swiper(“.swiper-container”,{…}); 改成 new Swiper(this.$refs.mySwiper,{…});

父组件获取子组件实例

1
<Event1 ref="xm" /> 

则 this.$refs.xm 相当于子组件的 this

组件通信

  1. props:父向子组件通信

    注意1:当子组件接收到父组件props参数时,watch是监听不到的。

    解决:使用watch立即监听属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    props: ['floor'],
    watch: {
    floor: {
    immediate: true, // 立即监听
    handler() {
    console.log("sdf");
    },
    },
    },

    注意2:如果父组件给子组件传递的是函数,本质就是子组件给父组件传递数据

    • 三种书写方式

      数组:[“name”]

      对象:{type:String}

      对象:{type:String,default:”aaa”}

  2. 自定义事件:$on、$emit 可以实现子向父组件通信

    使用

    1
    <Event2 @click="handler2" @xxx="handler3" />
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <div>
    <h2>Event2组件</h2>
    <button @click="$emit('click', '触发自定义事件click')">
    分发自定义click事件
    </button>
    <button @click="$emit('xxx', '触发自定义事件xxx')">
    分发自定义xxx事件
    </button>
    </div>

    深入

    问题1:如何给组件标签绑定系统事件?

    1
    2
    3
    4
    5
    <!-- 原生DOM绑定系统事件 -->
    <button @click="handler">原生</button>
    <!-- Event1组件:不是原生DOM节点,绑定的click事件被认为是自定义事件,这时点击Event1组件并不会触发handler1函数 -->
    <!-- 只有子组件Event1 $emit触发该事件handler1才会被调用 -->
    <Event1 @click="handler1" />

    解决:加上.native,结果相当于给子组件Event1的根节点绑定了点击事件

    1
    2
    <button @click="handler">原生</button>
    <Event1 @click.native="handler1" />

    问题2:如何给原生DOM绑定自定义事件?

    解决:没法解决。

  3. 全局事件总线:$bus 全能

  4. pubsub-js(发布于订阅):React中使用较多,vue当中几乎不用 全能

  5. 插槽:父子之间的通信,一般通信的是结构(即标签)

    默认插槽

    具名插槽

    作用域插槽:子组件数据来源于父组件,子组件决定不了自身结构与外观

    • 场景

      image-20220604175847040

    • 实现

      父组件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      <template>
      <div>
      <h2>效果一:显示todo列表时,已完成todo为绿色</h2>
      <List :todos="todos">
      <!-- 接收子组件回传的数据 -->
      <template slot-scope="data">
      <!-- 给子组件传递的结构 -->
      <span :style="{ color: data.todo.isComplete ? 'green' : 'red' }">
      {{ data.todo.text }}
      </span>
      </template>
      </List>
      <hr />
      <h2>效果二:显示todo列表时,带序号,todo颜色为蓝绿搭配</h2>
      <List2 :todos="todos">
      <!-- 也可以解构 -->
      <template slot-scope="{ todo, index }">
      <span :style="{ color: todo.isComplete ? 'green' : 'blue' }">
      {{ index }}---{{ todo.text }}
      </span>
      </template>
      </List2>
      </div>
      </template>

      <script>
      import List from "./List";
      import List2 from "./List2";
      export default {
      components: { List, List2 },
      data() {
      return {
      todos: [
      { id: 1, text: "AAA", isComplete: false },
      { id: 2, text: "BBB", isComplete: true },
      { id: 3, text: "CCC", isComplete: false },
      { id: 4, text: "DDD", isComplete: false },
      ],
      };
      },
      };
      </script>

      子组件1

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      <template>
      <div style="background: #ccc">
      <ul>
      <li v-for="(item, index) in todos" :key="index">
      <!-- 作用域插槽(将子组件的数据回传给父组件) -->
      <slot :todo="item"></slot>
      </li>
      </ul>
      </div>
      </template>

      <script>
      export default {
      props: ["todos"],
      };
      </script>

      子组件2

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      <template>
      <div style="background: #ccc">
      <ul>
      <li v-for="(item, index) in todos" :key="index">
      <!-- 作用域插槽(将子组件的数据回传给父组件) -->
      <slot :todo="item" :index="index"></slot>
      </li>
      </ul>
      </div>
      </template>

      <script>
      export default {
      props: ["todos"],
      };
      </script>
  6. vuex:万能

  7. v-model:父子组件数据同步

    v-model实现原理:通过单向绑定+oninput事件(value改变)实现双向绑定

    1
    <input type="text" :value="msg" @input="msg = $event.target.value" />

    实现父子组件数据互通

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    <template>
    <div>
    <h1>深入v-model</h1>
    <input type="text" v-model="msg" />
    <Event1 v-model="msg" />
    <!-- 相当于 -->
    <!-- <Event1 :value="msg" @input="msg = $event" />
    :value 则不是单向绑定,而是props传参,子组件得接收value参数
    @input 则不是改变事件,而是自定义事件,子组件通过$emit调用input事件,$event获取子组件传递的参数
    注意:在原生事件中,$event是事件对象,在自定义事件中,$event是传递过来的数据(参数)
    -->

    </div>
    </template>

    <script>
    import Event1 from "./Event1";
    export default {
    components: { Event1 },
    data() {
    return {
    msg: "",
    };
    },
    };
    </script>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <template>
    <div style="background: #ccc">
    <h2>Event1组件</h2>
    <input
    type="text"
    :value="value"
    @input="$emit('input', $event.target.value)"
    />
    </div>
    </template>

    <script>
    export default {
    props: ["value"],
    };
    </script>
  8. 属性修饰符sync:父子组件数据同步

    1
    2
    <!-- 父组件给子组件传递了props money 并给子组件绑定了一个自定义事件,事件名称为 update:money 回调函数体:money = $event -->
    <Event1 :money.sync="money" />
    1
    2
    3
    4
    5
    6
    7
    <button @click="$emit('update:money', money - 100)">花100元</button>
    <span>还剩{{ money }}元</span>


    export default {
    props: ["money"],
    };
  9. $attrs与$listeners:可以获取到父组件给子组件传递的props参数、自定义事件

    image-20220604161900231

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <template>
    <div>
    <HintButton
    type="success"
    icon="el-icon-delete"
    size="mini"
    title="提示按钮"
    @click="handler"
    />
    </div>
    </template>

    <script>
    import HintButton from "./HintButton";
    export default {
    components: { HintButton },
    methods: {
    handler() {
    alert("点击");
    },
    },
    };
    </script>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <template>
    <div>
    <a :title="title">
    <el-button :type="$attrs.type" :icon="$attrs.icon" :size="$attrs.size">
    添加
    </el-button>
    <!-- 或 -->
    <el-button v-bind="$attrs" v-on="$listeners">添加</el-button>
    </a>
    </div>
    </template>

    <script>
    export default {
    props: ["title"],
    mounted() {
    // $attrs属于组件的一个属性,可以获取到父组件传递过来的props数据
    // 如果props某个参数被接收了,则$attrs就不会有被接收的参数
    console.log(this.$attrs);
    // $listeners获取父组件给子组件的自定义事件
    console.log(this.$listeners);
    },
    };
    </script>
  10. $children与$parent(包含混入mixin的使用)

    场景:

    image-20220604165838713

    父组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    <template>
    <div>
    <h2>安安,存款{{ money }}</h2>
    <button @click="jqFromXM(100)">找小明借100元</button>
    <button @click="jqFromXH(150)">找小红借150元</button>
    <button @click="jqFromAll(200)">找两个人都借200元</button>
    <hr />
    <Event1 ref="xm" />
    <Event2 ref="xh" />
    </div>
    </template>

    <script>
    import Event1 from "./Event1";
    import Event2 from "./Event2";
    export default {
    components: { Event1, Event2 },
    data() {
    return {
    money: 1000,
    };
    },
    methods: {
    jqFromXM(val) {
    this.money += val;
    this.$refs.xm.money -= val;
    },
    jqFromXH(val) {
    this.money += val;
    this.$refs.xh.money -= val;
    },
    jqFromAll(val) {
    this.money += 2 * val;
    // this.$refs.xm.money -= val;
    // this.$refs.xh.money -= val;
    // 或者
    this.$children.forEach((item) => { // $children:可以获取到当前组件全部子组件
    item.money -= val;
    });

    // 注意:this.$children[0] 别这样书写,如果组件过多,第几项是哪个组件是不确定的
    },
    },
    };
    </script>

    子组件之一、另一同理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <template>
    <div style="background: #ccc">
    <h2>小明,存款{{ money }}元</h2>
    <button @click="give(50)">给安安:50</button>
    </div>
    </template>

    <script>
    import myMixin from "../myMixin/myMixin";
    export default {
    mixins: [myMixin],
    data() {
    return {
    money: 30000,
    };
    },
    };
    </script>

    mixin

    1
    2
    3
    4
    5
    6
    7
    8
    9
    export default {
    // 可以放置组件重复的JS业务逻辑
    methods: {
    give(val) {
    this.money -= val;
    this.$parent.money += val;
    },
    },
    };

共用组件carousel

  • carousel组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    <template>
    <div class="swiper-container" ref="mySwiper">
    <div class="swiper-wrapper">
    <div
    class="swiper-slide"
    v-for="carousel in carouselList"
    :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";
    export default {
    name: "Carousel",
    props: ["carouselList"],
    watch: {
    carouselList: {
    immediate: true,
    handler() {
    this.$nextTick(() => {
    let mySwiper = new Swiper(this.$refs.mySwiper, {
    loop: true,
    // 分页器
    pagination: {
    el: ".swiper-pagination",
    clickable: true,
    },
    // 前进后退按钮
    navigation: {
    nextEl: ".swiper-button-next",
    prevEl: ".swiper-button-prev",
    },
    });
    });
    },
    },
    },
    };
    </script>
  • main.js中注册成为全局组件

    1
    2
    import Carousel from "@/components/Carousel";
    Vue.component(Carousel.name, Carousel);
  • 使用

    只需传递props参数过去就行

    1
    <Carousel :carouselList="bannerList" />

getters属性

主要作用就是简化仓库中的数据。

state中searchInfo属性值:

image-20220526143554060

原先获取searchInfo里头的goodList数据需要:

1
2
3
4
5
computed: {
...mapState({
goodList: (state) => state.search.searchInfo.goodList,
}),
},

getters简化数据,使更易获取:

1
2
3
4
5
6
7
const getters = {
// state当前小仓库的state
goodsList(state) {
// 如果没有数据至少返回空数组,避免遍历时报错
return state.searchInfo.goodsList || [];
},
};

获取数据 :

1
2
3
computed: {
...mapGetters(["goodsList"]),
},

监听路由

1
2
3
4
5
watch:{
$route(){
...
}
}

传参给服务器undefined的使用

1
2
3
4
5
6
7
8
9
10
11
12
searchParams: {
keyword: "",
categoryName: "",
category1Id: undefined, // 如果某个属性值为undefined,当传searchParams参数给服务器时,则不会传category1Id
category2Id: "",
category3Id: "",
order: "",
pageNo: 1,
pageSize: 10,
props: [],
trademark: "",
},

配置$bus全局事件总线

main.js

1
2
3
4
5
6
7
8
9
new Vue({
render: (h) => h(App),
// 配置全局事件总线
beforeCreate() {
Vue.prototype.$bus = this;
},
router,
store,
}).$mount("#app");

使用

1
2
// 通知兄弟组件Header清楚关键字
this.$bus.$emit("clear");
1
2
3
4
5
mounted() {
this.$bus.$on("clear", () => {
this.keyWord = "";
});
},

自定义事件实现子给父通信

父组件

1
<SearchSelector @trademarkInfo="trademarkInfo" />
1
2
3
4
5
6
methods: {
// 自定义事件(接收子组件数据)
trademarkInfo(trademark) {
console.log(trademark);
},
}

子组件

1
2
3
4
5
methods: {
trademarkHandler(trademark) {
this.$emit("trademarkInfo", trademark);
},
},

阿里图标库使用

地址:https://www.iconfont.cn

使用:

  1. 寻找图标添加到购物车

  2. 将购物车中的图标添加至项目中

  3. 项目中点击查看在线链接

    image-20220527142125953

  4. 生成代码

    image-20220527142207075

  5. 复制Font class代码

    image-20220527142344693

  6. index.html引入样式

    1
    2
    <!-- 引入阿里图标 -->
    <link rel="stylesheet" href="https://at.alicdn.com/t/font_3432904_z2geolzap3l.css"
  7. 组件使用图标

    image-20220527145211440

    1
    <span class="iconfont icon-down"></span>
  8. 动态更改图标样式

    1
    <span class="iconfont" :class="{ 'icon-down': isDesc, 'icon-up': isAsc }"></span>
    1
    2
    3
    4
    5
    6
    7
    8
    computed: {
    isAsc() {
    return this.searchParams.order.indexOf("asc") != -1;
    },
    isDesc() {
    return this.searchParams.order.indexOf("desc") != -1;
    },
    },

v-for

v-for:数组|数字|字符串|对象 都可以遍历。

滚动行为

设置路由跳转后滚动条的位置

1
2
3
4
5
6
7
8
// 配置路由
export default new VueRouter({
routes,
// 滚动行为
scrollBehavior(to, from, savedPosition) {
return { y: 0 };
},
});

async await 判断请求成功与否

场景:用户将商品加入购物车,仅需判断是否添加成功。

使用:

  1. actions函数:返回结果没有需要保存的数据,不需要写mutation,可以直接返回结果。

    注意:async函数返回的结果一定是promise【要么成功,要么失败】

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 将产品添加到购物车或者更新购买数量
    async addOrUpdateShopCart({ commit }, { skuId, skuNum }) {
    let result = await reqAddOrUpdateShopCart(skuId, skuNum);
    // 自定义返回信息
    if (result.code == 200) {
    return "ok";
    } else {
    return Promise.reject(new Error("加入购物车失败"));
    }
    },
  2. 组件methods中对应的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 加入购物车
    async addShopCart() {
    try {
    await this.$store.dispatch("addOrUpdateShopCart", {
    skuId: this.$route.params.skuid,
    skuNum: this.skuNum,
    });
    // 路由跳转
    ...
    } catch (error) {
    alert(error.message);
    }
    },

本地存储和会话存储

本地存储:持久化的(存储上限5MB)

会话存储:非持久化(浏览器关闭则销毁)

本地存储|会话存储一般存储的是字符串。

路由传参结合会话存储

1
2
3
4
5
// 路由跳转
this.$router.push({
name: "addcartsuccess",
query: { skuInfo: this.skuInfo, skuNum: this.skuNum }, // skuInfo为对象
});

当路由传递的参数中有对象时,地址栏中会将Object转成字符串显示,能通过this.$route.query获取对象参数,只是地址栏不好看。

1
http://localhost:8080/#/addcartsuccess?skuInfo=%5Bobject%20Object%5D&skuNum=1

像比较复杂的数据【skuInfo】,通过会话存储

1
2
3
4
5
6
// 路由跳转
sessionStorage.setItem("SKUINFO", JSON.stringify(this.skuInfo)); // 需先将对象转成字符串
this.$router.push({
name: "addcartsuccess",
query: { skuNum: this.skuNum },
});
1
2
3
4
5
computed: {
skuInfo() {
return JSON.parse(sessionStorage.getItem("SKUINFO")); // 将字符串转成对象
},
},

UUID+本地存储 生成临时游客身份

  1. 安装uuid,先查看node_modules中是否已经有了再安装

  2. 创建uuid_token.js文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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;
    };
  3. 在vuex中定义uuid_token代表游客身份

    1
    2
    3
    4
    import { getUUID } from "@/utils/uuid_token";
    const state = {
    uuid_token:getUUID(), // 生成随机字符串(不会再变)
    };
  4. requests.js中引入仓库并在请求头加上游客id

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 引入store仓库
    import store from "@/store";

    // 请求拦截器(请求发送前做处理) config:header请求头等属性
    requests.interceptors.request.use((config) => {
    // 如果detail仓库中的uuid_token有值,则在请求头加上它
    if (store.state.detail.uuid_token) {
    config.headers.userTempId = store.state.detail.uuid_token;
    }
    // 进度条开始
    nprogress.start();
    return config;
    });

actions中的context、以及Promise.all()的使用

1
2
3
4
5
const actions = {
deleteAllCheckedCart(context) {
console.log(context);
},
}

image-20220530214131890

context中存在dispatch属性,可见可以使用context来派发actions中的其它方法。其它属性同理

举例使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const actions = {
// 删除购物车商品
async deleteCartListBySkuId({ commit }, skuId) {
let result = await reqDeleteCartById(skuId);
if (result.code == 200) {
return "ok";
} else {
return Promise.reject(new Error("删除失败"));
}
},
// 删除所有选中的商品
deleteAllCheckedCart({ dispatch, getters }) {
// 存放所有返回的promise对象
let PromiseAll = [];
// 遍历商品,将选中的商品删除
getters.cartInfoList.forEach((item) => {
// 定义p变量接收deleteCartListBySkuId返回的promise对象
let p =
item.isChecked == 1
? dispatch("deleteCartListBySkuId", item.skuId)
: "";
// 将返回的promise对象存入PromiseAll数组中
PromiseAll.push(p);
});
// 调用Promise的all()方法:即当数组中的所有promise对象全部成功时,返回成功;只要一个失败,则返回失败。
return Promise.all(PromiseAll);
},
}

prevent取消默认事件

1
<button class="btn" @click.prevent="login">登录</button>

导航守卫

导航:表示路由正在发生改变。(进行路由跳转)

守卫:

全局守卫:全局前置守卫、全局解析守卫、全局后置守卫

1
2
3
4
5
6
7
8
// 全局守卫:前置守卫(路由跳转之前)
router.beforeEach((to, from, next) => {
// to、from 获取前往的路由、出发的路由
// next 为一个函数
// next(); // 表示放行
// next("/login"); // 放行到指定路由中
// next(false); // 中断当前导航,即回到from对应的路由中
});

路由独享守卫

1
2
3
4
5
6
7
8
9
10
11
12
13
{
path: "/trade",
component: Trade,
meta: { showFooter: true },
// 路由独享守卫
beforeEnter: (to, from, next) => {
if (from.path == "/shopcart") {
next();
} else {
next(false);
}
},
},

组件内守卫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default {
name: "PaySuccess",
// 组件内守卫(该方法中不能获取组件实例this,因为实例还没被创建)
beforeRouteEnter(to, from, next) {
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
}
};

持久化存储token以及通过token获取用户信息(本地存储+导航守卫)

问题1:用户登陆成功后像后台获取token存储在vuex中,并且之后每次请求都在请求头中带上token,以便可以获取用户信息。当首页挂载前发请求获取用户信息,后台通过请求头中的token返回对应的用户信息。之后如果用户刷新页面,此时没有再次向后台获取token(因为token只有点击登录按钮并且登录成功后才会获取到),而且vuex不是持久化存储,这时vuex中的token就会消失。之后每次请求的请求头都不会有token,也就不能获取到用户信息。

解决办法:将获取到的token存储在本地

1
2
3
token: localStorage.getItem("TOKEN"),  // state

localStorage.setItem("TOKEN", result.data.token); // actions

记得退出登录后将TOKEN删除

1
localStorage.removeItem("TOKEN"); 

问题2:此时token已经持久化存在本地,但是要在什么地方发请求获取用户信息。用户信息需要在Header组件中展示,而Header组件在登录之前就会挂载,登录之后除非刷新页面否则不会再挂载。原本是在Home组件挂载前获取用户信息存储在vuex中,Header组件在vuex中获取用户信息就行,但是如果当前为Search组件,当刷新页面,vuex中的用户信息清空,Search组件因为没有在挂载函数中写相应的请求,即使token有了,也没有再发请求获取用户信息。

解决办法:导航守卫

  1. router文件夹的index.js中引入store

    1
    import store from "@/store"
  2. 配置全局前置守卫

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 全局守卫:前置守卫
    router.beforeEach(async (to, from, next) => {
    next();
    let token = store.state.user.token;
    let name = store.state.user.userInfo.name;
    if (token) {
    // 用户登录了还想去登录页面则跳到首页
    if (to.path == "/login") {
    next("/");
    } else {
    // 判断是否有用户信息
    if (name) {
    next();
    } else {
    await store.dispatch("getUserInfo");
    next();
    }
    }
    } else if (to.path == "/register") {
    next("/register");
    } else {
    next("/login");
    }
    });

函数

trim

去除字符串空格。

find

查询数组中符合条件的元素

1
2
3
4
// 用户最终选择的地址
userDefaultAddress() {
return this.addressInfo.find((item) => item.isDefault == 1);
},

split

将attrValue用:进行分割,拿到分割后的第二个值

1
{{ attrValue.split(":")[1] }}

indexOf与splice

indexOf:

1
2
3
4
// 不添加重复数据
if (this.searchParams.props.indexOf(prop) == -1) {
this.searchParams.props.push(prop);
}

splice:

1
this.searchParams.props.splice(index, 1);  // 删除对应index下标的元素

every遍历

1
2
3
let arr = [{cur:1},{cur:2},{cur:3}];
let result = arr.every(item=>item.cur==1);
console.log(result); // 结果为false

Math.ceil、parseInt取整

Math.ceil:

1
2
3
4
mounted() {
console.log(this.total / this.pageSize); // 30.333333333333332
console.log(Math.ceil(this.total / this.pageSize)); // 31
},

parseInt:

1
2
3
4
mounted() {
console.log(5 / 2); // 2.5
console.log(parseInt(5 / 2)); // 2
},

Object.assign对象合并

ES6新增语法,合并对象。

1
2
3
4
5
6
7
8
9
10
11
let searchParams = {
category1Id:"",
category2Id:"",
category3Id:"",
categoryName:"",
keyword:"",
order:""
};
let query = {category1Id:"110",categoryName:"手机"};
let params = {keyword:"华为"};
console.log(Object.assign(searchParams,query,params)); // 合并后两个对象到searchParams对象中

路由对象参数合并

1
2
3
beforeMount() {
Object.assign(this.searchParams, this.$route.params, this.$route.query);
},

filter

过滤😂

isNaN

判断文本是否为纯数字

场景:用户输入购买数量,但是输入的不一定是纯数字,需判断。

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 修改产品个数
changeSkuNum(event) {
let value = event.target.value; // 获取输入框的value
console.log(value * 1); // 如果不是纯数字,则结果为NaN
// 如果用户输入的为非法
if (isNaN(value * 1) || value < 1) {
// value*1为NaN代表不是纯数字
this.skuNum = 1;
} else {
// 存在1.5等小数形式,取整
this.skuNum = parseInt(value);
}
},

统一api中的全部请求函数

之前只是在组件或者vuex中引入需要的请求函数。

1
import { reqGoodsDetail, reqAddOrUpdateShopCart } from "@/api";

现在将api中的所有请求函数变成全局

  1. 将所有请求函数引入main.js

    1
    2
    // 统一api文件夹的全部请求函数
    import * as API from "@/api";
  2. 自定义$API

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    new Vue({
    render: (h) => h(App),
    // 配置全局事件总线以及统一api
    beforeCreate() {
    Vue.prototype.$bus = this;
    Vue.prototype.$API = API;
    },
    router,
    store,
    }).$mount("#app");
  3. methods中使用

    1
    let result = await this.$API.reqSubmitOrder(tradeNo, data);

    之前如果在api文件夹中有进行分类,形如:

    image-20220629183236071

    在index.js进行配置,将product文件夹中的js统一暴露

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 将四个模块请求接口函数统一暴露
    import * as trademark from "./product/tradeMark";
    import * as attr from "./product/attr";
    import * as spu from "./product/spu";
    import * as sku from "./product/sku";

    // 对外暴露
    export default {
    trademark,
    attr,
    sku,
    spu,
    };

    然后做1.2步后就这样调用

    1
    this.$API.trademark.reqDeleteTradeMark(row.id);

尽量别在生命周期函数使用async|await

1
2
3
async mounted() {
await this.$API.reqPayInfo(this.orderId);
},

解决:

1
2
3
4
5
6
7
8
mounted() {
this.getPayInfo();
},
methods: {
async getPayInfo() {
await this.$API.reqPayInfo(this.orderId);
},
},

elementUI使用+按需引入

  1. 安装

    1
    npm install --save element-ui
  2. 按需引入

    需借助babel-plugin-component

    1
    npm install babel-plugin-component -D

    配置babel.config.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    module.exports = {
    presets: ["@vue/cli-plugin-babel/preset"],
    plugins: [
    [
    "component",
    {
    libraryName: "element-ui",
    styleLibraryName: "theme-chalk",
    },
    ],
    ],
    };

    之后在main.js中按需引入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import { Button, Select } from 'element-ui';

    Vue.component(Button.name, Button);
    Vue.component(Select.name, Select);
    /* 或写为
    * Vue.use(Button)
    * Vue.use(Select)
    */
    // 另一种写法(挂在原型上)
    // Vue.prototype.$msgbox = MessageBox
    // Vue.prototype.$alert = MessageBox.alert

    使用

    1
    2
    3
    4
    5
    6
    // 打开支付窗
    open() {
    this.$alert("<strong>这是<i>HTML</i></strong>", "HTML片段", {
    dangerouslyUseHTMLString: true,
    });
    },

    image-20220531223203913

qrcode二维码插件

安装

1
npm i qrcode --save

组件中引入依赖

1
import QRCode from "qrcode"

使用

1
2
// 生成二维码
QRCode.toDataURL("要生成二维码的链接")

返回Promise对象,所以需要用到async await

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 打开支付窗
async open() {
// 生成二维码(地址)
let url = await QRCode.toDataURL("https://cn.vuejs.org/index.html");
//
this.$alert(`<img src=${url} /></img>`, "请你微信支付", {
dangerouslyUseHTMLString: true,
center: true,
showCancelButton: true,
cancelButtonText: "支付遇见问题",
confirmButtonText: "已支付",
showClose: false,
});
},

image-20220531225709947

路由重定向

场景:默认第一次访问显示的页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
path: "/center",
component: Center,
meta: { showFooter: true },
// 二级路由组件
children: [
{
path: "myorder",
component: MyOrder,
},
{
path: "grouporder",
component: GroupOrder,
},
{
path: "/center",
redirect: "/center/myorder",
},
],
},

图片懒加载

安装

1
npm install --save vue-lazyload@1.3.3 

main.js配置

1
2
3
4
5
6
7
8
// 引入懒加载插件及图片
import VueLazyload from "vue-lazyload";
import atm from "@/assets/atm.gif";
// 注册
Vue.use(VueLazyload, {
// 懒加载默认图片
loading: atm,
});

使用

1
<img v-lazy="goods.defaultImg" />

自定义插件

src中创建plugins文件夹

定义

1
2
3
4
5
6
7
8
9
10
11
let myPlugins = {};

myPlugins.install = function (Vue, options) {
console.log("安装");
Vue.directive(options.name, (element, params) => {
console.log(element, params);
element.innerHTML = params.value.toUpperCase();
});
};

export default myPlugins;

image-20220602191214941

modifiers:修饰符,可以自定义属性

1
<h1 v-upper.erha="msg"></h1>

image-20220602191553082

main.js配置

1
2
3
4
5
6
// 引入自定义插件
import myPlugins from "@/plugins/myPlugins"
// use会调用myPlugins中的install方法
Vue.use(myPlugins,{
name:"upper"
})

使用

1
<h1 v-upper="msg"></h1>
1
2
3
4
5
data() {
return {
msg: "dsfadfasd",
};
},

效果

image-20220602191146033

vee-validate表单验证

安装

1
npm i vee-validate@2 --save

配置

  1. image-20220602193134176

  2. main.js引入

    1
    import "@/plugins/validate";

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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}必须与密码相同`, // 修改内置规则的message,让确认密码与密码相同
},
attributes: {
// 映射中文名称
phone: "手机号",
code: "验证码",
password: "密码",
password1: "确认密码",
agree: "协议",
},
});

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<!-- 注册内容 -->
<div class="register">
<h3>
注册新用户
<span class="go"
>我有账号,去 <a href="login.html" target="_blank">登陆</a>
</span>
</h3>
<div class="content">
<label>手机号:</label>
<input
placeholder="请输入你的手机号"
v-model="phone"
name="phone"
v-validate="{ required: true, regex: /^1\d{10}$/ }"
:class="{ invalid: errors.has('phone') }"
/>
<span class="error-msg">{{ errors.first("phone") }}</span>
</div>
<div class="content">
<label>验证码:</label>
<input
placeholder="请输入验证码"
v-model="code"
name="code"
v-validate="{ required: true, regex: /^\d{6}$/ }"
:class="{ invalid: errors.has('code') }"
/>
<button style="width: 100px; height: 40px" @click="getCode">
获取验证码
</button>
<span class="error-msg">{{ errors.first("code") }}</span>
</div>
<div class="content">
<label>登录密码:</label>
<input
type="password"
placeholder="请输入密码"
v-model="password"
name="password"
v-validate="{ required: true, regex: /^[0-9A-Za-z]{8,20}$/ }"
:class="{ invalid: errors.has('password') }"
/>
<span class="error-msg">{{ errors.first("password") }}</span>
</div>
<div class="content">
<label>确认密码:</label>
<input
type="password"
placeholder="确认密码"
v-model="password1"
name="password1"
v-validate="{ required: true, is: password }"
:class="{ invalid: errors.has('password1') }"
/>
<span class="error-msg">{{ errors.first("password1") }}</span>
</div>
<div class="controls">
<input
type="checkbox"
:checked="agree"
name="agree"
v-validate="{ required: true, agree: true }"
:class="{ invalid: errors.has('agree') }"
/>
<span>同意协议并注册《尚品汇用户协议》</span>
<span class="error-msg">{{ errors.first("agree") }}</span>
</div>
<div class="btn">
<button @click="register">完成注册</button>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 注册
async register() {
// 是否全部验证成功
const success = await this.$validator.validateAll();
if (success) {
const { phone, code, password } = this;
try {
await this.$store.dispatch("register", { phone, code, password });
this.$router.push("/login");
} catch (error) {
alert(error.message);
}
}
},

路由懒加载

当路由被访问的时候才加载对应组件,更高效。

1
2
3
4
5
6
7
8
9
10
11
12
13
// import Home from "@/pages/Home";
// 路由懒加载
const Home = () => {
return import("@/pages/Home");
};
// 简写
// const Home = () => import("@/pages/Home");
// 或者直接
{
path: "/home",
component: () => import("@/pages/Home"),
meta: { showFooter: true },
},

打包后的map文件

项目打包后(npm run build),代码都是经过压缩加密的,如果运行时报错,输出的错误信息无法准确得知是哪里的代码报错。

有了map就可以像未加密的代码一样,准确的输出是哪一行哪一列有错。

所以该文件是可以去除的

vue.config.js 配置

productionSourceMap:false

image-20220602204145308

引入JQuery

安装

1
npm install --save jquery

版本:”jquery”: “^3.6.0”,

配置:修改vue.config.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const { defineConfig } = require("@vue/cli-service");
const webpack = require("webpack");
module.exports = defineConfig({
transpileDependencies: true,
lintOnSave: false,
// 引入jquery
chainWebpack: (config) => {
//引入ProvidePlugin
config.plugin("provide").use(webpack.ProvidePlugin, [
{
$: "jquery",
jquery: "jquery",
jQuery: "jquery",
"window.jQuery": "jquery",
},
]);
},
});

后台摸板介绍

简洁版:https://github.com/PanJiaChen/vue-admin-template

加强版:https://github.com/PanJiaChen/vue-element-admin

简洁版使用:npm install 以及 npm run dev

image-20220604182017369

浅拷贝与深拷贝

1)格式

浅拷贝形如

1
this.attrInfo = { ...row }; // es6语法好像,lodash应该也有封装

深拷贝形如

1
2
// 按需引入lodash当中的深拷贝
import cloneDeep from "lodash/cloneDeep";
1
this.attrInfo = cloneDeep(row);

2)注意

深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型。

3)实例

1
2
3
4
5
6
7
8
9
10
// 对象赋值
let obj1 = {
'name': '张三',
'age': '18',
'language': [1,[2,3],[4,5]]
};
let obj2 = obj1;
obj2.name = '李四';
obj2.language[1] = ['二','三']
// 这时obj1,obj2的值都改变了,即obj1与obj2等同
1
2
3
4
5
6
7
8
9
10
// 浅拷贝
let obj1 = {
name: "张三",
age: "18",
language: [1, [2, 3], [4, 5]],
};
let obj2 = { ...obj1 };
obj2.name = "李四";
obj2.language[1] = ["二", "三"];
// 这时obj1的name属性没变,但是language变了,说明obj2没有拷贝obj1深层,obj2只有浅层是跟obj1独立开的
1
2
3
4
5
6
7
8
9
10
// 深拷贝
let obj1 = {
name: "张三",
age: "18",
language: [1, [2, 3], [4, 5]],
};
let obj2 = cloneDeep(obj1);
obj2.name = "李四";
obj2.language[1] = ["二", "三"];
// 这时obj1跟obj2是完全独立的两个对象

$set

用法:向响应式对象中添加一个属性,并确保这个新属性同样是响应式的(值改变,视图跟着更新),必须用于向响应式对象上添加新属性。

this.Object.newProperty = 'hi'此时newProperty不是响应式的。

this.$set(Object,'newProperty','hi')此时newProperty为响应式的。

深度选择器

scoped样式并不是只对当前组件有效,对子组件的根标签同样有效。

需求:给style加上了scoped属性,还想对子组件有效?

解决:深度选择器:可以实现样式穿透。

格式:

1
2
3
原生css >>>
less /deep/
scss ::v-deep

实现:

1
2
3
4
5
6
/* 此时就能影响到子组件的h3标签 */
<style scoped>
>>>h3{
color:red;
}
</style>