Vue-Cli 项目总结

527 阅读3分钟

Vue-Cli 项目总结

1. Vue 项目目录结构分析

public 文件夹:静态资源,webpack 进行打包的时候会原封不动打包到dist文件夹中。
pubilc / index.html:是一个模板文件,作用是生成项目的入口文件,webpack打包的js,css也会自动注入到该页面中。我们浏览器访问项目的时候就会默认打开生成好的index.html。\

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

assets: 存放公用的静态资源(放置在assets文件夹里面的静态资源,在webpack打包的时候,webpack会把静态资源当做一个模块,打包到JS文件里面)
components: 非路由组件(全局组件),其他组件放在views或者pages文件夹中
App.vue: 唯一的根组件
main.js: 程序入口文件,最先执行的文件

babel.config.js:配置文件(babel相关)
package.json:项目的详细信息记录
package-lock.json:缓存性文件(各种包的来源)

2. 项目配置

2.1 项目运行,浏览器自动打开

// package.json
    "scripts": {
    "serve": "vue-cli-service serve --open",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
    },

2.2 关闭 eslint 校验工具(不关闭会有各种规范,不按照规范就会报错)

  • 根目录下创建vue.config.js,进行配置
// vue.config.js
module.exports = {
  //关闭eslint
  lintOnSave: false
  }

2.3 配置 alias 别名

const path = require('path'); //引入path 要用到他的resolve 方法
const resolve = dir => path.resolve(__dirname, dir);  //这里为了方便直接引入了最里层

module.exports = {
    resolve: {
        // 设置别名
        alias: {
            '@': resolve('src'),// 这样配置后 @ 可以指向 src 目录
            'api': resolve('src/api')// 这样配置后 api 可以指向 src 目录下的api目录
        }
    },
    devServer: {
       // .....
    },
};

3. 路由

3.1 路由组件 和 非路由组件

  • 非路由组件放在components中,路由组件放在 pages 或 views 文件夹下
  • 非路由组件通过标签使用,路由组件通过路由 <router-view/>

3.2 $route 和 $router

  • 在 main.js 中注册完路由,所有的路由和非路由组件身上都会拥有$route$router 属性
  • $route: 一般获取路由信息(name、path、params等),this.$route--当前路由信息
  • $router:是“路由实例”对象包括了路由的跳转方法,钩子函数等。一般进行编程式导航进行路由跳转

image.png

image.png

3.3 路由跳转方式

  • 声明式导航<router-link/>标签,可以把它理解为一个<a>标签,它也可以加 class 修饰
  • 编程式导航 :声明式导航能做的编程式都能做this.$router.push()this.$router.replace(),而且还可以处理一些业务

3.4 路由元信息 (meta)

应用场景:
例子:判断路由的显示与隐藏

// router.js 文件中
onst routes = [
  {
    path: "/home",
    component: Home,    
    meta: {isShow:true} // 定义路由的时候可以配置 `meta` 字段:
  },
  {
    path: "/register",
    component: Register,
    meta: {isShow:false}
  }
];
// 在 vue 组件中
<Footer v-show = "$route.meta.isShow"></Footer>

3.5 路由传参

3.5.1 应用场景

一般都是应用在父路由跳转到子路由时,携带参数跳转。

3.5.2 传参方式 (2种)

params 传参和 query 传参

params 参数:属于路径当中的一部分(配置传参目的地路由时,需要占位符),地址栏表现形式如:/search/123
query 参数:不属于路径当中的一部分,地址栏表现形式如:/search?a=123&b=456

3.5.3 书写方式 (3种)

// Home.vue 组件中
<template>
        <div class="searchArea">
            <input v-model="keyword"/>
            <button @click="goSearch"> 搜索</button>
        </div>
</template>

<script>
export default {
  data() {
    return {      
      keyword: "", // 搜索输入框中的值  动态绑定
    };
  },
  methods: {
    // 点击搜索按钮的回调
    goSearch() {
      // 点击搜索,将 keyword 参数传递到 /search 路由中      
      this.$router.push(
      // 书写形式:
      // 1、字符串
      // "/search/" + this.keyword + "?k=" + this.keyword.toUpperCase()

      // 2、模板字符串
      // `/search/${this.keyword}` + `?k=${this.keyword.toUpperCase()}`
      // 注意:params参数和‘/’结合,query参数和‘?’结合

      // 3、对象形式(推荐)
       {
        name: "search",    // 需要在 search 路由配置项中,添加 name 字段
        params: { keyword: this.keyword },
        query: { k: this.keyword.toUpperCase() }
       });
    },
  },
};
</script>
// router.js 中的配置
const routes = [
  {
    path: "/home",
    component: Home,
  },
  {
    name:'search',   // 如果以对象的方式跳转路由,并传递参数时,需要配置 name 字段
    path: "/search/:keyword", // 用 params 传递参数时,需要在这里配置占位符(/:keyword)来接收参数 keyword
    component: Search,
  }]
// Search.vue 组件中
<template>
  <div>
    <h1>我是搜索页</h1>
    <h3>{{ "params参数: " + $route.params.keyword }}</h3>
    <h3>{{ "query参数: " + $route.query.k }}</h3>
  </div>
</template>

3.5.4 params 传参问题(4个)

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

// 路由跳转传参的时候,对象写法可以使path、name形式,但是需要注意的是,path这种写法不能与params参数一起出现
this.$router.push({  
        path: "/search", // 错误
        params: { keyword: this.keyword },
        query: { k: this.keyword.toUpperCase() },
      });

(2)、如何指定 params 参数可传可不传?

  如果路由path要求传递params参数,但是没有传递,会发现地址栏URL有问题,详情如下:
  Search路由项的path已经指定要传一个keyword的params参数,如下所示:
  path: "/search/:keyword",
  执行下面进行路由跳转的代码:
  this.$router.push({name:"Search",query:{k:this.keyword}})
  当前跳转代码没有传递params参数
  地址栏信息:http://localhost:8080/#/?k=asd
  此时的地址信息少了/search
  正常的地址栏信息: http://localhost:8080/#/search?k=asd
  解决方法:可以通过改变path来指定params参数可传可不传 
  path: "/search/:keyword?",?表示该参数可传可不传

(3)、由(2)可知 params 可传可不传,但是如果传递的是空串,如何解决?

 this.$router.push({name:"Search",params:{keyword:''},query:{k:this.keyword}})
 出现的问题和1中的问题相同,地址信息少了/search
 解决方法: 加入||undefined,当我们传递的参数为空串时地址栏url也可以保持正常
 this.$router.push({name:"Search",params:{keyword:''||undefined},query:{k:this.keyword}})

(4)、路由组件能不能传递 props 数据?
路由组件传递props数据,可以像使用 props 数据一样(使用起来更加简便)

// Home.vue 组件中
<template>
        <div class="searchArea">
            <input v-model="keyword"/>
            <button @click="goSearch"> 搜索</button>
        </div>
</template>

<script>
export default {
  data() {
    return {      
      keyword: "", // 搜索输入框中的值  动态绑定
    };
  },
  methods: {
    // 点击搜索按钮的回调
    goSearch() {
      // 点击搜索,将 keyword 参数传递到 /search 路由中      
      this.$router.push(
      // 书写形式:
      // 1、字符串
      // "/search/" + this.keyword + "?k=" + this.keyword.toUpperCase()

      // 2、模板字符串
      // `/search/${this.keyword}` + `?k=${this.keyword.toUpperCase()}`
      // 注意:params参数和‘/’结合,query参数和‘?’结合

      // 3、对象形式(推荐)
       {
        name: "search",    // 需要在 search 路由配置项中,添加 name 字段
        params: { keyword: this.keyword },
        query: { k: this.keyword.toUpperCase() }
       });
    },
  },
};
</script>
// router.js 中的配置
const routes = [
  {
    path: "/home",
    component: Home,
  },
  {
    name:'search',   // 如果以对象的方式跳转路由,并传递参数时,需要配置 name 字段
    path: "/search/:keyword", // 用 params 传递参数时,需要在这里配置占位符(/:keyword)来接收参数 keyword
    component: Search,
    
    // 路由组件能不能传递参数 props 数据?
    // 1.布尔写法:只能传递params参数
    // props:true,
    // 2.对象写法:除了 params 参数,还能额外的给路由组件传递一些 props
    // props:{a:1,b:2},
    // 3.函数写法:可以把 params参数、query参数,通过props传递给路由组件
    //  props:()=>{
    //      return {keyword:$route.params.keyword,k:$route.query.key}
    //  }
      
    // 简写
       props:($route)=>({keyword:$route.params.keyword,k:$route.query.key})
  }]
// Search.vue 组件中
<template>
  <div>
    <h1>我是搜索页</h1>
    <h3>{{ "params参数: " + $route.params.keyword }}</h3>
    <h3>{{ "query参数: " + $route.query.k }}</h3>
    
    // 书写更简洁
    <h3>{{ "props参数: " + "params参数: " + keyword + "," + "query参数: " +k }}</h3>
  </div>
</template>

3.5.5 多次执行相同的 push 问题

  • 问题
this.$router.push({name:"search",params:{keyword:aaa},query:{k:bbb}})

多次执行出现警告: image.png

  • 原因

push 的结果是 一个 promisepromise 需要传递成功和失败两个参数,我们的 push 中没有传递。

     // 验证
    let result=  this.$router.push({
        name: "search",
        params: { keyword: this.keyword },
        query: { k: this.keyword.toUpperCase() },
      });
    console.log(result);

控制台输出结果: image.png

  • 解决方法

push 是 VueRouter.prototype 的一个方法,在 router.js 中重写该方法即可

// 在 router.js 文件中

import Vue from "vue";
import VueRouter from "vue-router";
Vue.use(VueRouter);

// 1、先把VueRouter原型对象的push,保存一份
let originPush = VueRouter.prototype.push;
let originReplace = VueRouter.prototype.replace;

// 2、重写push|replace
// 第1个参数:告诉原来的push,跳转的目标位置和传递了哪些参数
// 第2个参数:成功的回调
// 第3个参数:失败的回调
VueRouter.prototype.push = function (location, resolve, reject) {
  console.log(this); // this 是 VueRouter的一个实例,即可以调用 VueRouter原型上的方法
    if(resolve && reject){
        originPush.call(this,location,resolve,reject)  
    }else{
        originPush.call(this,location,() => {},() => {})
    }
}

VueRouter.prototype.replace = function (location, resolve, reject) {
  console.log(this); // this 是 VueRouter的一个实例,即可以调用 VueRouter原型上的方法
    if(resolve && reject){
     originReplace.call(this,location,resolve,reject)
    }else{
      originReplace.call(this,location,() => {},() => {})
    }
}

4. 全局组件

4.1 创建全局组件

// TypeNav 组件
<template>
  <div>
        我是全局组件
  </div>
</template>

<script>
export default {
  name: "TypeNav",  // 组件名,必填
  created() {},
  data() {
    return {};
  },
  methods: {},
};
</script>

4.2 注册全局组件

// 全局的配置都需要在main.js中配置
// 引入全局组件,并注册
import TypeNav from "@views/Home/TypeNav/Index.vue";
Vue.component(TypeNav.name,TypeNav)  // 参数 1 为在全局组件中定义好的name, 参数 2 为要注册的组件

4.3 使用全局组件

// Home 组件中
<template>
  <div>
     <!-- 注册好的全局组件,不需要再引入,可直接使用 -->
    <TypeNav></TypeNav>  
  </div>
</template>

5. Axios 二次封装

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

  1. api统一管理,不管接口有多少,所有的接口都可以非常清晰,容易维护
  2. 请求拦截器、响应拦截器:请求拦截器,可以在发起请求之前可以处理一些业务;响应拦截器,当服务器数据返回以后,可以处理一些事情;

5.2 封装 Axios

在根目录 /src/api 文件夹下创建一个文件 request.js,并加入以下代码

// request.js 文件中
import axios from "axios";

// 创建一个 axios 实例
const instance = axios.create({
    // 这里面都是该实例的配置项

    baseURL: "/api", // 基础路径,发起请求的时候,路径当中会出现 api ,  "/api"  已经在vue.config.js中配置好了
    timeout: 30000, // 请求超时为 30s 
})


// 请求拦截器:在发起请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事情
instance.interceptors.request.use((config) => {
    // config:配置对象,对象里面有一个属性很重要, headers 请求头
    (config) => {
        console.log(config);   
        return config;
      },
      (error) => {
        console.log(error);
      }
})

// 响应拦截器:
instance.interceptors.response.use((res) => {
    // 响应成功的回调函数:服务器相应数据过来以后,响应拦截器可以检测到,这里可以做一些事情
    return res.data
}, (error) => {
    // 响应失败的回调函数:
    return Promise.reject(new Error('faile')) // 终止 Promise 链
})


// 把实例暴露出去
export default instance;

5.3 请求接口统一封装

将每个请求封装为一个函数,并暴露出去,组件只需要调用相应函数即可,这样当我们的接口比较多时,如果需要修改只需要修改该文件即可。

在 src/api/index.js 文件中,封装所有请求

// 当前这个模块: 对 API 进行统一管理
import instance from "./request"

// 三级联动接口
// 发送请求:axios 发请求返回的结果是 Promise 对象
export const getCategoryList = () => instance({
            url:"/product/getBaseCategoryList",
            method:"GET"
})

可以在 main.js 中进行单元测试

import { getCategoryList } from "@api/index.js";

getCategoryList().then(res => {
  console.log(res.data);
})

6. nprogress 进度条插件

在 src/api/request.js 中

// 二次封装 axios
import axiox from "axios";

// 创建一个axios实例
const instance = axiox.create({
    baseURL: "/api",
    timeout:3000
})

// 引入 nprogress 进度条
import nprogress from "nprogress";
// 引入  nprogress 进度条样式
import "nprogress/nprogress.css"

// 请求拦截器:在发起请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事情
instance.interceptors.request.use((config) => {
    // config:配置对象,对象里面有一个属性很重要, headers 请求头

    // 进度条开始动 
    nprogress.start()
    return config;  
})

// 响应拦截器:服务器相应数据过来以后,响应拦截器可以检测到,这里可以做一些事情
instance.interceptors.response.use((res) => {
    // 进度条结束
    nprogress.done()
    return res.data
}, (error) => {
    // 响应失败的回调函数:
    return Promise.reject(new Error('faile')) // 终止 Promise 链
})

// 把实例暴露出去
export default instance;

7. Vuex 状态管理模式

7.1 Vuex 模块化开发

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module) 。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:

src/store/home/index.js 文件中 (小仓库 home)

// 存储仓库数据的地方
const state = {a:"home 模块的数据"}

// 修改 state 的唯一方式
const mutations = {}

// 处理 actions  可以书写自己的业务逻辑,也可以处理异步
const actions = {}

// 理解为计算属性,用于简化仓库数据,让组件获取仓库的数据更加方便
const getters = {}

export default {
    state,
    mutations,
    actions,
    getters
}

src/store/search/index.js 文件中 (小仓库 search)

// 存储仓库数据的地方
const state = {b:"search 模块的数据"}

// 修改 state 的唯一方式
const mutations = {}

// 处理 actions  可以书写自己的业务逻辑,也可以处理异步
const actions = {}

// 理解为计算属性,用于简化仓库数据,让组件获取仓库的数据更加方便
const getters = {}

export default {
    state,
    mutations,
    actions,
    getters
}

src/store/index.js 文件中 (模块集中管理仓库)

import Vue from "vue";
import Vuex from "vuex";

// 需要使用插件一次
Vue.use(Vuex);

// 引入小仓库
import home from "@store/home";
import search from "@store/search";

// 对外暴露 Store 类的一个实例
export default new Vuex.Store({
  // 实现 Vuex 仓库模块式开发存储数据
  modules: {
    home,
    search
  },
});

在 Vue.js devtools 中查看数据 image.png

7.2 State (在 Vue 组件中获得 Vuex 状态 )

// src/store/home/index.js
// 存储仓库数据的地方
const state = {
   a:1
}

// 修改 state 的唯一方式
const mutations = {}

// 处理 actions  可以书写自己的业务逻辑,也可以处理异步
const actions = {}

// 理解为计算属性,用于简化仓库数据,让组件获取仓库的数据更加方便
const getters = {}

export default {
    state,
    mutations,
    actions,
    getters
}
// index.vue 组件中
<template>
  <div>
    <h1>{{ a }}</h1>
    <h2>{{ localComputed }}</h2>
  </div>
</template>

<script>
import { mapState } from "vuex";

export default {
  computed: {
    // 局部计算属性
    localComputed() {
      return "局部计算属性";
    },
     
    // 方法一:
    // 如果 vuex 没有模块化,即可用以下方法获取 state 
    // ...mapState(['a', 'b']),
    
    // 方法二(推荐):更多灵活处理
    ...mapState({
      // 子模块的属性因有了命名空间 无法直接使用字符串数组 
      a: (state) => state.home.a, // 注意:此处拿到的是 home 模块的数据
    }),
  },
};
</script>

7.3 Mutations (同步更改 Store 中的状态)

// src/store/home/index.js
// 存储仓库数据的地方
const state = {
   a:1
}

// 修改 state 的唯一方式
const mutations = {
    increment(state, payload) {      
        state.a += payload.num;
    }
}

// 处理 actions  可以书写自己的业务逻辑,也可以处理异步
const actions = {}

// 理解为计算属性,用于简化仓库数据,让组件获取仓库的数据更加方便
const getters = {}

export default {
    state,
    mutations,
    actions,
    getters
}
// index.vue 组件中
<template>
  <div>
    <el-button @click="add(params)">每次+1</el-button>
    <h1>{{ "home 模块中 state.a=" + a }}</h1>
  </div>
</template>

<script>
import { mapState } from "vuex";

export default {
  data() {
    return {
      params: { num: 1 },
    };
  },
  computed: {  
    // 方法二(推荐):更多灵活处理
    ...mapState({
      // 子模块的属性因有了命名空间 无法直接使用字符串数组 
      a: (state) => state.home.a, // 注意:此处拿到的是 home 模块的数据
    }),
    
   methods: {
    // 常规方法:
    // add() {
    //   this.$store.commit("increment", { num: 1 });
    // },

    // 推荐方法:
    ...mapMutations({
      add: "increment", // 需要传递参数直接把参数放在 add()方法的实参中
    }),
  },
  },
};
</script>

7.3 Actions (异步更改 Store 中的状态)

// src/store/home/index.js 文件中
import { getCategoryList } from "@api";

// 存储仓库数据的地方
const state = {
    // state 中数据默认初始值
    categoryList:[]
}

// 修改 state 的唯一方式
const mutations = {
    CATEGORYLIST(state, payload) {
        state.categoryList = payload;
    }
}

// 处理 actions  可以书写自己的业务逻辑,也可以处理异步
const actions = {
    // 调用接口,发起请求
    // 方法的形参可以直接将commit解构出来,这样可以方便后续操作
   async categoryList({commit}) {
       let result = await getCategoryList();
        
        if (result.code == 200) {
            // 提交到 mutstions 中
            commit("CATEGORYLIST",result.data)
        }
    }
}

// 理解为计算属性,用于简化仓库数据,让组件获取仓库的数据更加方便
const getters = {}

export default {
    state,
    mutations,
    actions,
    getters
}
// index.vue 组件中
<template>
  <div></div>
</template>

<script>
export default {
   mounted() {
    // 通知 Vuex 发起请求,存储于仓库中
    this.$store.dispatch("categoryList");   // categoryList 是 Actions 中的方法名
  },
};
</script>

image.png

8. Lodash 插件 防抖和节流

8.1 防抖(debounce)

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

应用场景:

  • DOM 元素的拖拽功能实现(mousemove)
  • 搜索联想(keyup)
  • 计算鼠标移动的距离(mousemove)
  • Canvas 模拟画板功能(mousemove)
  • 射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)
  • 监听滚动事件判断是否到页面底部自动加载更多:给 scroll 加了 debounce 后,只有用户停止滚动后,才会判断是否到了页面底部;如果是 throttle 的话,只要页面滚动就会间隔一段时间判断一次.
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

    <script src="./lodash.js"></script>
</head>
<body>
        搜索:<input type="text">

    <script>   
        let inp =document.querySelector("input")

        // 应用场景:在搜索引擎中输入时,合理的业务逻辑应该是 在最后一个字输入后等待 1s,再发起Ajax请求获取搜索建议
        // oninput 事件在用户输入时触发,它是在元素值发生变化时立即触发----显然需要做相应的逻辑处理;
        inp.oninput =  _.debounce(function () { 
                console.log('用户输入完毕,1s后发起Ajax请求!');  
        },1000);              
    </script>
</body>
</html>

8.2 节流(throttle)

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

应用场景:

  • scroll事件滚动触发事件
  • 搜索框输入查询,如果用户一直在输入中,没有必要不停地调用去请求服务端接口,等用户停止输入的时候,再调用,设置一个合适的时间间隔,有效减轻服务端压力。
  • 表单验证
  • 按钮提交事件。
  • 浏览器窗口缩放,resize事件(如窗口停止改变大小之后重新计算布局)等。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

    <script src="./lodash.js"></script>
</head>
<body>

    <h2>0</h2>
    <button>加1</button>

    <script>
       let h2 = document.querySelector("h2");
       let btn = document.querySelector("button");
       let num =0;

        //    场景:点击按钮每次+1,不能太频繁,(1s 内点击按钮最多能加一次)
        // btn.onclick=function(){
        //     h2.innerHTML = num++
        // }

        btn.onclick = _.throttle(function(){
            h2.innerHTML = num++
        },1500)
    </script>
</body>
</html>

9. 编程式跳转 + 事件委托

场景: 我想点击 1/2/3级菜单,然后进行跳转,并获取该级别菜单的 categoryName、categoryId

路由跳转问题:
对于导航式路由,我们有多少个 a 标签就会生成多少个 router-link 标签,这样当我们频繁操作时会出现卡顿现象。 对于编程式路由,我们是通过触发点击事件实现路由跳转。同理有多少个 a 标签就会有多少个触发函数。虽然不会出现卡顿,但是也会影响性能。

事件委派问题:
(1)如何确定我们点击的一定是a标签呢?如何保证我们只能通过点击 a 标签才跳转呢? (2)如何获取子节点标签的商品名称和商品 id (我们是通过商品名称和商品 id 进行页面跳转的)

解决方法:编程式导航 + 事件委托
对于问题1:为三个等级的a标签添加自定义属性 date-categoryName 绑定商品标签名称来标识 a 标签(其余的标签是没有该属性的)。
对于问题2:为三个等级的a标签再添加自定义属性 data-category1Id、data-category2Id、data-category3Id 来获取三个等级 a 标签的商品id,用于路由跳转。

我们可以通过在函数中传入 event 参数,获取当前的点击事件,通过 event.target 属性获取当前点击节点,再通过 dataset 属性获取节点的属性信息。

<template>
  <div>
    <div class="box" @click="goSearch" style="display: flex">
      <h1 v-for="c1 in menu" :key="c1.categoryId">
        <!-- 循环出来的一级菜单 -->
        <a
          :data-categoryName="c1.categoryName"
          :data-category1Id="c1.categoryId"
          style="color: red"
          >{{ c1.categoryName }}</a
        >

        <h2 v-for="c2 in c1" :key="c2.categoryId">
          <!-- 循环出来的二级菜单 -->
          <a
            :data-categoryName="c2.categoryName"
            :data-category2Id="c2.categoryId"
            style="color: green"
            >{{ c2.categoryName }}</a
          >

          <h3 v-for="c3 in c2.categoryChild" :key="c3.categoryId">
            <!-- 循环出来的三级菜单 -->
            <a
              :data-categoryName="c3.categoryName"
              :data-category3Id="c3.categoryId"
              >{{ c3.categoryName }}</a
            >
          </h3>
        </h2>
      </h1>
    </div>
  </div>
</template>

<script>
export default {
  name: "",
  created() {},
  data() {
    return {
      menu: [
        {
          categoryChild: {
            categoryChild: [
              {
                categoryName: "电子书",
                categoryId: 5,
              },
              {
                categoryName: "网络原创",
                categoryId: 6,
              },
              {
                categoryName: "数字杂志",
                categoryId: 7,
              },
              {
                categoryName: "多媒体图书",
                categoryId: 8,
              },
            ],
            categoryName: "电子书刊",
            categoryId: 3,
          },

          categoryName: "书籍",
          categoryId: 1,
        },
        {
          categoryChild: {
            categoryChild: [
              {
                categoryName: "游戏机",
                categoryId: 9,
              },
              {
                categoryName: "游戏耳机",
                categoryId: 10,
              },
              {
                categoryName: "手柄/方向盘",
                categoryId: 11,
              },
              {
                categoryName: "游戏软件",
                categoryId: 12,
              },
              {
                categoryName: "游戏周边",
                categoryId: 13,
              },
            ],
            categoryName: "游戏设备",
            categoryId: 4,
          },
          categoryName: "电子设备",
          categoryId: 2,
        },
      ],
    };
  },
  methods: {
    goSearch(event) {
      // event.currentTarget : 指的是绑定了事件监听的元素(可以理解为触发事件元素的父级元素)
      // event.target   获取触发特定事件的元素
      // element.dataset  获取 HTML元素自定义的 data-* 属性

      // 当前点击的 DOM元素
      let element = event.target;
      // 注意:data-* 属性  在 html 中会把大写转为小写
      //获取目前鼠标点击标签的categoryname,category1id,category2id,category3id,
      // 通过四个属性是否存在来判断是否为a标签,以及属于哪一个等级的a标签
      let { categoryname, category1id, category2id, category3id } =
        element.dataset;

      // categoryname 存在,表示为 a 标签
      if (categoryname) {
        let location = { name: "Search" }; // 跳转路由 name
        let query = { categoryName: categoryname }; // 路由参数

        if (category1id) {
          //category2id 一级a标签
          query.category1Id = category1id;
        } else if (category2id) {
          //category2id 二级a标签
          query.category2Id = category2id;
        } else {
          //category2id 三级a标签
          query.category3Id = category3id;
        }

        //整理完参数
        location.query = query;

        this.$router.push(location);
      }
    },
  },
};
</script>