系列学习前端之第 10 章:一文掌握 Vue2(组件化编程)

 接上一篇博客:系列学习前端之第 8 章:一文掌握 Vue2(核心基础篇)

说明:本篇博客历经 40 多天完成学习和做笔记(利用工作之余和周末完成),一共八万二千多字,预计快速阅读完毕需要 30 分钟,跟着练习需要 2 天。

本篇博客对应代码:https://gitee.com/biandanLoveyou/vue2

1、模块与模块化、组件与组件化概念

 1.1 模块

1.1.1 什么是模块?

对外提供特定功能的 js 程序,一般是一个 js 文件

1.1.2 为什么要使用模块?

js 文件很多很复杂,需要拆分成多个小文件

1.1.3 使用模块有什么好处?

复用 js,简化 js 的编写,提高 js 的运行效率

1.2 模块化

当应用中的 js 都以模块来编写的,那这个应用就是一个模块化的应用。

1.3 组件

1.3.1 什么是组件?

用来实现局部或者特定功能效果的代码集合。代码集合一般包括 html、css、js、image等等

1.3.2 为什么要用组件?

在复杂的页面中,可以将功能拆分成很多小功能,减少页面功能的复杂度。

1.3.3 组件的作用?

可以复用编码,简化项目的编码,提高运行效率。

1.4 组件化

当应用中的功能都是多组件的方式来编写的,那这个应用就是一个组件化的应用。

2、组件的学习

2.1 组件的分类

组件一般可以分为:“非单文件组件”和“单文件组件”。

2.2 使用组件的三大步骤

  1. 定义组件(创建组件)
  2. 注册组件
  3. 使用组件(写组件标签)

2.2.1 定义组件

使用 Vue.extend(options) 创建,其中 options 和 new Vue(options) 时传入的那个 options 几乎一样,但也有点区别,区别如下:

  1. 组件中的 el 不要写,因为所有的组件最后都要由 vm 统一管理,由 vm 中的 el 决定服务哪个容器。
  2. 组件中的 data 必须写成函数,因为要避免组件被复用时数据存在引用关系。
  3. 使用 template 可以配置组件结构。

2.2.2 注册组件

注册组件分为:局部注册和全局注册,区别如下:

1、局部注册:靠 new Vue 的时候传入 components 选项

2、全局注册:靠 Vue.component('组件名',组件)

2.2.3 使用组件

在需要使用的地方增加组件的标签即可。比如:<MyComponent></MyComponent>

2.3 非单文件组件

2.3.1 什么叫非单文件组件?

非单文件组件是指在一个文件中包含有多个组件,非的意思是“不是”

2.3.2 非单文件组件有什么缺点?

  1. 模板编写没有代码高亮提示
  2. 没有构建过程,无法将 ES6 转换成 ES5
  3. 不支持组件的 CSS
  4. 在真正开发中,非单文件组件几乎不被使用而是使用单文件组件。

2.3.3 开发第一个组件

代码示例:

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>组件的基本使用</title>
    <!-- 本地引入 vue.js 库 -->
    <script src="../js/vue.js"></script>
    <script>
        window.onload = function () {//文档加载完成后执行
            // 第一步:创建 food 组件
            const food = Vue.extend({
                //定义模板数据
                template: `
                <div>
                    <h3>菜品:{{foodName}}</h3>
                    <h3>价格:{{price}}</h3>
                </div>
                `,
                //组件中的 data 必须写成函数
                data() {
                    return {
                        foodName: "猪肚鸡",
                        price: "¥108"
                    }
                },
            });

            //第二步:注册组件(全局注册)
            //Vue.component("food", food);

            //创建 Vue 对象
            new Vue({
                el: "#root",
                //第二步:注册组件(局部注册)
                components: {
                    food
                }
            })
        }
    </script>
</head>

<body>
    <!-- 准备一个容器 -->
    <div id="root">
        <!-- 第三步:使用组件(编写组件标签) -->
        <food></food>
    </div>
</body>

</html>

效果:

2.3.4 组件的嵌套

实际项目开发中,会非常频繁的使用组件的嵌套。一般会有一个最大的包含组件叫 App,用来管理所有的组件,然后 App 这个组件由 vm 来管理。这样一来,App 这个组件相当于“一人之下万人之上”的角色,用来统领三军。

代码示例:

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>组件的嵌套</title>
    <!-- 本地引入 vue.js 库 -->
    <script src="../js/vue.js"></script>
    <script>
        window.onload = function () {//文档加载完成后执行

            // 先创建被嵌入的组件“节日特色”
            const feature = Vue.extend({
                name: "feature",//组件名
                template: `
                <div>
                    <h3 style='color:blue'>节日特色:</h3>
                    <h4>出行人数:{{travelers}}</h4>
                    <h4>人均消费:{{consume}}</h4>
                    <hr>
                </div>
                `,
                data() {
                    return {
                        travelers: "10亿次",
                        consume: "¥500"
                    }
                }

            });

            // 创建 holiday 组件,要嵌套 feature 组件
            const holiday = Vue.extend({
                name: "holiday",//组件名
                //定义模板数据
                template: `
                <div>
                    <h3>节日名称:{{holidayName}}</h3>
                    <h3>所在日期:{{holidayDate}}</h3>
                    <h3>放假天数:{{days}}</h3>
                    <hr>
                    <!-- 嵌入“节日特色”组件 -->
                    <feature></feature>
                </div>
                `,
                //组件中的 data 必须写成函数
                data() {
                    return {
                        holidayName: "国庆节",
                        holidayDate: "10月1日~7日",
                        days: 7
                    }
                },
                //注册“节日特色”的组件
                components: {
                    feature
                }
            });

            //定义一个【老外】组件,作为参考者
            const foreigner = Vue.extend({
                name: "foreigner",//组件名
                template: `
                <h2 style='color:red'>{{say}}</h2>
                `,
                data() {
                    return {
                        say: "老外说:太犀利了!"
                    }
                }
            });

            //定义 app 组件
            const app = Vue.extend({
                template: `
                    <div>
                        <holiday></holiday>
                        <foreigner></foreigner>    
                    </div>
                `,
                components: {
                    holiday,
                    foreigner
                }
            })

            //创建 Vue 对象
            new Vue({
                el: "#root",
                template: `
                    <app></app>
                `,
                //第二步:注册组件(局部注册)
                components: {
                    app
                }
            })
        }
    </script>
</head>

<body>
    <!-- 准备一个容器 -->
    <div id="root">
    </div>
</body>

</html>

效果:可以通过 Vue 开发工具查看各个组件的嵌套关系

案例中,各个组件的关系如下图所示:

小结:

1、一般在定义组件的时候,都会给组件起一个名字。在 Vue.js 中,组件的文件名一般使用大写字母开头,并遵循驼峰式命名规范。这种命名方式有助于保持项目的一致性和可读性。例如,如果你有一个名为 "MyComponent" 的组件,那么它的文件名应该为 "my-component.vue"。

2、组件名尽可能回避 html 中已有的元素名称,例如 h3、p 这些都不允许。

3、组件本质上是 VueComponent的构造函数,是由 Vue.extend生成的,我们习惯上称之为 vc,每次调用 Vue.extend,返回的都是一个全新的 VueComponent 对象。

4、vc 与 vm 在属性上、内部函数、生命周期几乎相同,但是 vm 比 vc 多了几部分比如:vm 中有 el,但是 vc 中没有。vm 中的 data 可以是对象,但是 vc 中必须是函数。

5、一个重要的内置关系:VueComponent.prototype.__proto__ === Vue.prototype

6、所以组件实例对象 vc 可以访问到 Vue 原型上的属性、方法。所以,可以把 vc 看做是 vm 的副本。

2.4 单文件组件

实际开发中,用的都是单文件组件的方式,因此这块内容必须要掌握。

2.4.1 .vue 文件的组成

.vue 文件由 3 部分组成,分别是:模板页面、JS模块对象、样式。

2.4.1. 模板页面

语法:

<template>
  <div>
    内容
  </div>
</template>
2.4.1.2 JS模块对象

语法:

<script>
export default {
  name: "xxx",
  //引入其它组件
  components: {
    xxx,
  },
};
</script>
2.4.1.3 样式

语法:

<style>
/* 背景颜色:绿色 */
.holiday {
  background-color: green;
}
</style>

穿插一个知识点:VSCode 工具快捷生成 .vue 文件所需的模板页面、JS模块对象、样式。首先安装 vetur 插件,在 .vue 文件中,输入 <v + 回车即可。

2.4.1.4 单文件组件的代码示例

①代码结构以及代码关系:

②Movie.vue 代码

<template>
  <div id = "meirenyu">
    <h3>电影名称:{{ movieName }}</h3>
    <h4>主演:{{ mainRole }}</h4>
    <h4>票房:{{ sales }}</h4>
    <hr />
  </div>
</template>

<script>
export default {
  name: "Movie", //组件名
  data() {
    return {
      movieName: "美人鱼",
      mainRole: "邓超、罗志祥、张雨绮、林允、徐克",
      sales: "33.92亿元",
    };
  },
};
</script>

<style>
#meirenyu {
  color: green;
}
</style>

③Actor.vue 代码

<template>
  <div class="info">
    <h3>演员名称:{{ actorName }}</h3>
    <h3>所在地区:{{ place }}</h3>
    <h3>表演风格:{{ performStyle }}</h3>
    <hr />
    <!-- 嵌入 Movie 组件 -->
    <Movie></Movie>
  </div>
</template>

<script>
//导入组件
import Movie from "./Movie";
export default {
  name: "Actor",
  data() {
    return {
      actorName: "周星驰",
      place: "香港",
      performStyle: "喜剧,无厘头",
    };
  },
  //引入其它组件
  components: {
    Movie,
  },
};
</script>

<style>
</style>

④Audience.vue 代码

<template>
  <h2>{{ say }}</h2>
</template>

<script>
export default {
  name: "Audience", //组件名
  data() {
    return {
      say: "观众说:童心未泯,返璞归真,才能看到世界的美好。",
    };
  },
};
</script>

<style>
h2 {
  color: red;
}
</style>

⑤App.vue 代码

<template>
  <div>
    <Actor></Actor>
    <Audience></Audience>
  </div>
</template>

<script>
import Actor from "./Actor";
import Audience from "./Audience";
export default {
  name: "App",
  components: { Actor, Audience },
};
</script>

<style>
</style>

⑥main.js 代码

import App from "./App.vue";

new Vue({
    el: "#root",
    template: `
    <app></app>
    `,
    components: {
        App
    }
})

⑦index.html 代码

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>我的第一个单文件组件</title>
</head>

<body>
    <!-- 准备一个容器 -->
    <div id="root"></div>
</body>

</html>

注意,这个时候项目跑起来是一键空白的,因为浏览器只能解析 html、css、js 文件,对于 vue 目前还不能解析。所以,项目暂时无法跑起来,就引申出一个 Vue CLI 脚手架的概念。

3、使用 Vue 脚手架

3.1 什么是 Vue 脚手架?

Vue CLI (Command Line Interface 命令行接口工具,俗称脚手架) 是官方提供的基于 Webpack 的 Vue 工具链,可以快速生成 Vue 项目。它现在处于维护模式。官方现在建议使用 Vite 开始新的项目。目前我们先学习 Vue CLI,至于 Vite,要等学到 Vue3 之后才去学。

Vue CLI 官网地址:https://cli.vuejs.org/zh/

Vite 官网地址:https://cn.vitejs.dev/

查看脚手架可用版本:

npm view @vue/cli versions --json

说明:Vue的版本目前主要有 Vue2 和 Vue3,但是 Vue 的脚手架已经有很多个版本,最新版本已经到 5.0 的版本。

查看电脑已经安装的脚手架版本(大写的 V):

vue -V

结果:目前我们电脑只有比较低的脚手架版本 2.9.6(几年前装来玩的),显然我们需要安装更高版本的脚手架。

我们需要先卸载旧的 2.9.6 版本,如果不卸载干净旧版本的话,待会我们安装新版本之后,创建项目会默认使用旧版本的脚手架,导致创建项目失败并提示卸载旧版安装新版,这是一个坑:

会报错如下;

vue create is a Vue CLI 3 only command and you are using Vue CLI 2.9.6.
You may want to run the following to upgrade to Vue CLI 3:

npm uninstall -g vue-cli
npm install -g @vue/cli

查看 vue 的安装目录命令:

where vue

结果:

然后依次打开目录把 C 盘、D 盘把 vue、vue.cmd 这2个文件删掉。有多少个就删除多少个,删完为止。

删除完毕如图:

脚手架与 Vue 的版本对应关系大概是:

  • Vue CLI 4.5 以下,对应的是 Vue2
  • Vue CLI 4.5 及以上,对应的是 Vue3
  • Vue CLI 3.X 和 4.X 的脚手架的模板和 Vue CLI 2.X 的完全不同

3.2 脚手架使用步骤

3.2.1 全局安装 @vue/cli

这里会涉及 npm 包管理工具,如果你还没掌握,可以参考博客:

系列学习前端之第 9 章:一文搞懂 Node.js 和 nvm,掌握 npm-CSDN博客

使用 cmd 命令行全局安装 @vue/cli

npm i -g @vue/cli      或者     npm install -g @vue/cli

另外,卸载脚手架命令:npm uninstall -g vue-cli

 安装过程如果遇到卡顿的,在 cmd 命令行敲回车符,它就会继续安装。直到输出【added xxx packages 】就是安装成功了。

 验证是否安装成功:

vue -V

效果:出现 @vue/cli xxx 就是安装成功了

3.2.2 使用 vue create 在指定目录创建新项目

脚手架安装好之后,我们切换到某个硬盘的某个目录创建一个新的 vue 项目。

不会切到具体目录?查看博客:CMD 命令行进入到电脑硬盘的某个目录的几种方式-CSDN博客

我们在 D 盘的目录:D:\vue\workspace\study-vue\cli 下执行以下命令:

vue create my-vue

出现一个对话框,问我们选择哪个版本。我们使用键盘的上下键选择 Vue2,然后回车:

大概几分钟之后,Vue CLI 脚手架就帮我们在指定目录下创建了一个新项目。

效果:

对应的目录地址也创建了很多文件。这个就是一个完整的 Vue 项目。

3.2.3 启动项目 run serve

我们打开 Vue 脚手架帮忙创建的项目的 package.json 文件,找到 script 节点,如下:

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

也就是我们通过脚本 npm run serve 就可以启动该项目。这个知识点在上一篇博客已经提到。

然后在 package.json 所在目录的地址栏,输入 cmd 回车,唤起 cmd 命令行:

效果:

我们打开浏览器,输入地址:http://localhost:8080/

出现以下内容,说明我们成功完成通过脚手架初始化项目,并启动。

问:如何停掉服务?

1、关掉 cmd 命令行窗口

2、Ctrl+C

3.3 简单分析脚手架创建的项目代码

把脚手架创建的代码使用 VSCode 打开,如图:

1、main.js:是整个项目的入口

2、App.vue:是所有组件的父组件,放在 src 目录下,与 main.js 同一级

3、components:存放所有组件的目录

4、assets:存放公共静态资源的目录,比如:图片、视频、语音等

5、public:里面存放 html 代码。favicon.ico 是网站图标,固定名字。

6、node_modules:存放依赖包

3.4 把我们创建的单组件项目跑起来

把我们创建的单组件项目的文件拷贝过来,并做简单的调整。主要调整一下几个文件。

调整后的代码结构:

①App.vue 文件要放在与 src 下一级,并且要调整引入的组件的路径。

<template>
  <div>
    <Actor></Actor>
    <Audience></Audience>
  </div>
</template>

<script>
import Actor from "./components/Actor";
import Audience from "./components/Audience";
export default {
  name: "App",
  components: { Actor, Audience },
};
</script>

<style>
</style>

②main.js 代码如下:

// 引入 Vue 文件
import Vue from 'vue'

//引入 App 组件,它是所有组件的父组件
import App from './App.vue'

// 关闭 Vue 的生产提示
Vue.config.productionTip = false

// 创建 Vue 实例对象
new Vue({
  // 将 app 组件放入 vm 容器中
  render: h => h(App),
}).$mount('#root') 

③vue.config.js 这个配置文件要修改,增加一行关闭在开发环境下代码检查:lintOnSave: false

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  //关闭在开发环境下每次保存代码时都进行代码质量检查
  lintOnSave: false
})

否则运行的时候容易报错,导致编译不通过,信息如下:

D:\vue\workspace\study-vue\cli\my-vue\src\components\Audience.vue
  7:9  error  Component name "Audience" should always be multi-word  vue/multi-word-component-names

D:\vue\workspace\study-vue\cli\my-vue\src\components\Movie.vue
  12:9  error  Component name "Movie" should always be multi-word  vue/multi-word-component-names

✖ 3 problems (3 errors, 0 warnings)


webpack compiled with 1 error

准备就绪后,启动

npm run serve

结果:

浏览器打开页面:http://localhost:8080/

效果:

4、组件开发常见的技术

4.1 ref 属性

1、被用来给元素或者子组件注册引用信息(父获取子组件的信息,父 <—子)。
2、应用在 html 标签上,获取的是真实 DOM 元素;应用在组件标签上获取的是组件实例对象(vc)。
3、使用方式:
    ①给标签打上标识,比如:<h3 ref="xxx">***</h3>或者<Singer ref="xxx"></Singer>
    ②获取:this.$refs.xxx

代码示例(其它结构与上面的项目保持一致):

核心代码结构图

①Singer.vue 代码

<template>
  <div>
    <h3>歌手姓名:{{ name }}</h3>
    <h3>出自歌曲:{{ from }}</h3>
  </div>
</template>

<script>
export default {
  name: "Singer",
  data() {
    return {
      name: "海来阿木",
      from: "《不如见一面》",
    };
  }
};
</script>

<style>
</style>

②Song.vue 代码

<template>
  <div>
    <h3 ref="info">歌词:{{ lyric }}</h3>
    <button ref="btn" @click="showDomRefInfo">点我展示 DOM 信息</button>
    <hr />
    <Singer ref="sing"></Singer>
  </div>
</template>

<script>
//导入组件
import Singer from "./Singer";
export default {
  name: "Song",
  data() {
    return {
      lyric: "不如见一面,哪怕是一眼"
    };
  },
  methods:{
    showDomRefInfo(){
      console.log("info=",this.$refs.info);//真实DOM元素
      console.log("btn=",this.$refs.btn);//真实DOM元素
      console.log("sing=",this.$refs.sing);//Singer 组件的实例对象(vc)
    }
  },
  //引入其它组件
  components: {
    Singer
  },
};
</script>

<style>
</style>

③App.vue 代码

<template>
  <div>
    <Song></Song>
  </div>
</template>

<script>
import Song from "./components/Song";
export default {
  name: "App",
  components: { Song },
};
</script>

<style>
</style>

效果:sing 对象是 vc 组件对象,不是 vm 对象

4.2 props 配置

1、作用:用于父组件给子组件传递数据(父 —> 子)。

2、读取方式一:简单读取,只指定名称。例如:

props:["name","sex","birthDate","height"]

3、读取方式二:指定名称和类型。例如:

  //接收的同时对数据进行类型限制
  props:{
    name: String,
    sex: String,
    birthDate: String,
    //要求身高传递数字类型
    height: Number
  }

4、读取方式三:指定名称/类型/必要性/默认值。例如:

  // 接收的同时对数据:进行类型限制+默认值的指定+必要性的限制
  props:{
    name:{
      type: String, //类型是字符串
      required: true, //必填项
    },
    sex:{
      type: String, //字符串类型
      required: false, // 非必填项
    },
    birthDate:{
      type: String,
      default: "1986年"
    },
    height:{
      type: Number, //数字类型
      required:true,
    }
  }

 代码示例:

4.2.1 props 简单读取

①Song.vue 代码

<template>
  <div>
    <h3>歌词:{{ lyric }}</h3>
    <hr />
    <Singer name="许嵩" sex="男" birthDate="1986年" height="180"></Singer>
  </div>
</template>

<script>
//导入组件
import Singer from "./Singer";
export default {
  name: "Song",
  data() {
    return {
      lyric: "她只是我的妹妹,妹妹说紫色很有韵味。"
    };
  },
  //引入其它组件
  components: {
    Singer
  },
};
</script>

<style>
</style>

②Singer.vue 代码

<template>
  <div class="nice">
    <h2>{{info}}</h2>
    <h3>歌手姓名:{{ name }}</h3>
    <h3>性别:{{ sex }}</h3>
    <h3>出生年份:{{ birthDate }}</h3>
    <h3>身高:{{ height }}</h3>
  </div>
</template>

<script>
export default {
  name: "Singer",
  data() {
    console.log(this);
    return {
      info: "歌手信息介绍",
    }
  },
  //简单声明接收
  props:["name","sex","birthDate","height"]
};
</script>

<style>
.nice{
  background-color: purple;
}
</style>

效果:

4.2.2 props 指定名称和类型

①Singer.vue 代码

<template>
  <div class="nice">
    <h2>{{info}}</h2>
    <h3>歌手姓名:{{ name }}</h3>
    <h3>性别:{{ sex }}</h3>
    <h3>出生年份:{{ birthDate }}</h3>
    <h3>身高:{{ height }}</h3>
  </div>
</template>

<script>
export default {
  name: "Singer",
  data() {
    console.log(this);
    return {
      info: "歌手信息介绍",
    }
  },
  //简单声明接收
  //props:["name","sex","birthDate","height"]

  //接收的同时对数据进行类型限制
  props:{
    name: String,
    sex: String,
    birthDate: String,
    //要求身高传递数字类型
    height: Number
  }

};
</script>

<style>
.nice{
  background-color: purple;
}
</style>

②Song.vue 代码。主要是修改 :height="180",加多一个冒号,即 v-bind

<template>
  <div>
    <h3>歌词:{{ lyric }}</h3>
    <hr />
    <Singer name="许嵩" sex="男" birthDate="1986年" :height="180"></Singer>
  </div>
</template>

<script>
//导入组件
import Singer from "./Singer";
export default {
  name: "Song",
  data() {
    return {
      lyric: "她只是我的妹妹,妹妹说紫色很有韵味。"
    };
  },
  //引入其它组件
  components: {
    Singer
  },
};
</script>

<style>
</style>

效果:

注意:案例中的身高 height 如果在父组件用双引号引起来,并且不做任何的调整(就是没有加 v-bind 或者 英文冒号),那么就不是表达式的形式,而是会当做是字符串形式传递到子组件,如果子组件限制 height 属性要数字类型 Number,控制台就会出现报错,报错如下:

4.2.3 props 接收的同时对数据:进行类型限制+默认值的指定+必要性的限制

我们增加一个按钮,可以让身高增加。需要注意到使用 props 的一个细节,就是从父组件传递进来的数据,优先级要高于自身组件的数据。因此如果我们不能直接修改父组件传递进来的数据,会报错。而是增加一个额外的字段来接收父组件传递的数据,然后对额外字段做逻辑的处理。

代码示例:

<template>
  <div class="nice">
    <h2>{{info}}</h2>
    <h3>歌手姓名:{{ name }}</h3>
    <h3>性别:{{ sex }}</h3>
    <h3>出生年份:{{ birthDate }}</h3>
    <h3>身高:{{ myHeight }}</h3>
    <button @click="updateHieght">点击修改身高</button>
  </div>
</template>

<script>
export default {
  name: "Singer",
  data() {
    console.log(this);
    return {
      info: "歌手信息介绍",
      // 使用自定义字段来接收父组件传递的数据
      myHeight: this.height
    }
  },
  methods:{
    updateHieght(){
      console.log("this.height=",this.height);
      this.myHeight ++;
    }
  },
  //简单声明接收
  //props:["name","sex","birthDate","height"]

  //接收的同时对数据进行类型限制
  /*
  props:{
    name: String,
    sex: String,
    birthDate: String,
    //要求身高传递数字类型
    height: Number
  }
  */

  // 接收的同时对数据:进行类型限制+默认值的指定+必要性的限制
  props:{
    name:{
      type: String, //类型是字符串
      required: true, //必填项
    },
    sex:{
      type: String, //字符串类型
      required: false, // 非必填项
    },
    birthDate:{
      type: String,
      default: "1986年"
    },
    height:{
      type: Number, //数字类型
      required:true,
    }
  }

};
</script>

<style>
.nice{
  background-color: purple;
}
</style>

效果:

4.3 mixin 混入

mixin混入
1、功能:可以把多个组件共用的配置提取成一个混入对象
2、使用方式:
    1、第一步定义混入,例如:

// 定义混入:演员信息
export const actor = {
    mounted(){
        console.log("大家好,我叫成龙。", this.$el);
    },
    methods:{
        showAuth(){
            console.log("这是我的代表作:", this.famous);
        }
    }
}

// 定义混入:其它信息
export const info = {
    data(){
        return{
            honor: "国家一级演员",
            birthday: "1954年"
        }
    }
}

    2、第二步使用混入,例如:

<script>
//引入混入
import {actor, info} from "../myMixin.js";

export default {
  name: "Song",
  data() {
    return {
      famous:"《神话》、《真的用了心》、《北京欢迎你》",
    };
  },
  //使用混入
  mixins:[actor, info]
};
</script>

代码示例:

核心代码结构如图,注意混入的文件 myMixin.js 与 main.js 同一级:

①myMixin.js 代码

// 定义混入:演员信息
export const actor = {
    mounted(){
        console.log("大家好,我叫成龙。", this.$el);
    },
    methods:{
        showAuth(){
            console.log("这是我的代表作:", this.famous);
        }
    }
}

// 定义混入:其它信息
export const info = {
    data(){
        return{
            honor: "国家一级演员",
            birthday: "1954年"
        }
    }
}

 ②Song.vue 代码

<template>
  <div class="good">
    <h3>著名歌曲:{{ famous }}</h3>
    <h3>这是我的荣誉:{{honor}}</h3>
    <h3>这是我的生日:{{birthday}}</h3>
    <button @click="showAuth">点我展示作者</button>
    <hr />
  </div>
</template>

<script>
//引入混入
import {actor, info} from "../myMixin.js";

export default {
  name: "Song",
  data() {
    return {
      famous:"《神话》、《真的用了心》、《北京欢迎你》",
    };
  },
  //使用混入
  mixins:[actor, info]
};
</script>

<style>
.good{
  background-color: skyblue;
}
</style>

③Movie.vue 代码

<template>
  <div class="nice">
    <h3>著名电影:{{ famous }}</h3>
    <h3>这是我的荣誉:{{honor}}</h3>
    <h3>这是我的生日:{{birthday}}</h3>
    <button @click="showAuth">点我展示作者</button>
    <hr />
  </div>
</template>

<script>
// 引入混入
import {actor, info} from "../myMixin.js";

export default {
  name: "Movie",
  data() {
    return {
      famous: "《A计划》、《红番区》、《警察的故事》",
      honor: "2016年,获得第89届奥斯卡金像奖终身成就奖",
      birthday: "2024年70岁",
    }
  },
  //使用混入
  mixins:[actor, info]
};
</script>

<style>
.nice{
  background-color: pink;
}
</style>

④App.vue代码

<template>
  <div>
    <Song></Song>
    <Movie></Movie>
  </div>
</template>

<script>
//先引用哪个,就先解析哪个
import Song from "./components/Song";
import Movie from "./components/Movie";
export default {
  name: "App",
  components: { Song, Movie },
};
</script>

<style>
</style>

效果:

说明:

1、在 App.vue 组件中,先引用哪个组件,就先解析哪个组件

2、如果要混入的 js 代码与组件中的 data 的某些属性名相同,则使用的是组件内的数据

3、引入混入的写法(是大括号):import {actor, info} from "../myMixin.js";

4、使用混入的写法(是中括号):mixins:[actor, info]

4.4 Vue 插件

1、Vue 插件是一个包含 install 方法的对象

2、通过 install 方法给 Vue 或 Vue 实例添加方法,定义全局指令等

3、使用插件:在 main.js 内使用插件,如:Vue.use(myPlugins, "成龙", "70岁")

代码示例:

核心代码结构图:

①myPlugins.js 代码

export default {
    //install方法可以包含多个自定义参数
    install(Vue, name, age){
        console.log(name, age);

        //可以实现:全局过滤器
        Vue.filter("mySlice", function(value){
            return value.slice(0, 4);
        });

        //可以实现:定义全局指令
        Vue.directive("fbind", {
            //指令与元素成功绑定时(一上来)
            bind(element, binding){
                element.value = binding.value;
            },
            //指令所在元素被插入页面时
            inserted(element, binding){
                element.focus();
            },
            //指令所在的模板被重新解析时
			update(element,binding){
				element.value = binding.value;
                element.focus();
			}
        });

        //可以实现:混入
        Vue.mixin({
            data(){
                return {
                    honor: "2016年,获得第89届奥斯卡金像奖终身成就奖",
                    birthday: "2024年70岁",
                }
            }
        });

        //可以实现:给 Vue 原型上添加一个方法,vm 和 vc 都可以使用
        Vue.prototype.say = () =>{console.log("大家好啊!")}
    }
}

②main.js 代码

// 引入 Vue 文件
import Vue from 'vue'

//引入 App 组件,它是所有组件的父组件
import App from './App.vue'
import myPlugins from './myPlugins'

// 关闭 Vue 的生产提示
Vue.config.productionTip = false

//使用插件
Vue.use(myPlugins, "成龙", "70岁")

// 创建 Vue 实例对象
new Vue({
  // 将 app 组件放入 vm 容器中
  render: h => h(App),
}).$mount('#root') 

③App.vue 代码

<template>
  <div>
    <Song></Song>
    <Movie></Movie>
  </div>
</template>

<script>
//先引用哪个,就先解析哪个
import Song from "./components/Song";
import Movie from "./components/Movie";
export default {
  name: "App",
  components: { Song, Movie },
};
</script>

<style>
</style>

④Song.vue 代码

<template>
  <div class="good">
    <h3>著名歌曲:{{ famous | mySlice }}</h3>
    <h3>这是我的荣誉:{{honor}}</h3>
    <h3>这是我的生日:{{birthday}}</h3>
    <button @click="test">点我打招呼</button>
    <hr />
  </div>
</template>

<script>
export default {
  name: "Song",
  data() {
    return {
      famous:"《神话》、《真的用了心》、《北京欢迎你》",
    };
  },
  methods:{
    test(){
      this.say();
    }
  }
};
</script>

<style>
.good{
  background-color: skyblue;
}
</style>

⑤Movie.vue 代码

<template>
  <div class="nice">
    <h3>著名电影:{{ famous }}</h3>
    <input type="text" v-fbind:value = "comment">
    <hr />
  </div>
</template>

<script>
export default {
  name: "Movie",
  data() {
    return {
      famous: "《A计划》、《红番区》、《警察的故事》",
      comment: "你想评论什么?"
    }
  },
};
</script>

<style>
.nice{
  background-color: pink;
}
</style>

效果:

4.5 scoped 样式

1、作用:让样式在局部生效,防止冲突

2、写法:<style scoped>

先演示开发中常见的问题:

还是以上面插件的代码为案例,我们修改一下 Song.vue 里面的 style(与 Movie.vue 里的 style 一样的 class 名,但是背景色是天蓝色)

<style>
.nice{
  /* 设置背景色为:天蓝色 */
  background-color: skyblue;
}
</style>

然后把 Song.vue 里面的 template 的 class 属性修改一下(与 Movie.vue 里的 class 属性名一样):

<template>
  <div class="nice">
    <h3>著名歌曲:{{ famous | mySlice }}</h3>
    <h3>这是我的荣誉:{{honor}}</h3>
    <h3>这是我的生日:{{birthday}}</h3>
    <button @click="test">点我打招呼</button>
    <hr />
  </div>
</template>

运行,发现 Song 模块的背景色竟然是跟 Movie 里的一样,都是粉红色 pink:

问题追溯:主要是在 App.vue 里面引用组件的时候,先引用 Song,然后再引用 Movie,然后 Vue 发现它们俩的模板名都一样,叫做 nice,那就直接把样式覆盖了。也就是 Movie 的 nice 样式把 Song 的 nice 样式覆盖了。

在实际开发过程中,会经常遇到这样的问题:2个程序员可能会起相同的样式名,但是在不同的组件中,总不能张三起了一个叫“btnClass”的样式名,其他人就不能起这个名字了吧?或者说每个开发起了一个样式名,然后叫大家不要起这个名字了?显然是不合理的,那么怎么解决这个问题呢?

解决办法:

在每个组件的 style 标签里增加一个关键字 scoped,表示仅该组件内生效。例如:

<style scoped>
.nice{
  /* 设置背景色为:天蓝色 */
  background-color: skyblue;
}
</style>

效果:

5、webStorage(浏览器存储)

说明:浏览器存储不是 Vue 团队开发的技术,而是 JavaScript 原生就有的技术。放在这里讲是为了后续的应用。

1、浏览器存储的内容一般是字符串,内容大小一般在 5M 左右(不同浏览器可能不同)。

2、浏览器通过 Window.sessionStorage(会话存储,关闭浏览器就清掉内容)和 Window.localStorage(本地存储,不清掉浏览器缓存就会一直存在)来实现内容的存储。

3、相关API

xxxStorage.setItem("key","value");//存储数据
xxxStorage.getItem("key");//读取数据
xxxStorage.removeItem("key");//删除数据
xxxStorage.clear();//清空所有数据

代码示例:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>浏览器存储</title>
</head>
<body>
    <h2>localStorage本地存储</h2>
    <button onclick="saveLocal()">点我存储本地数据</button>
    <button onclick="readLocal()">读取本地数据</button>
    <button onclick="deleteLocal()">删除本地数据</button>
    <button onclick="clearLocal()">清空本地数据</button>
    <hr>
    <h2>sessionStorage会话存储</h2>
    <button onclick="sessionSaveLocal()">点我存储会话数据</button>
    <button onclick="sessionReadLocal()">读取会话数据</button>
    <button onclick="sessionDeleteLocal()">删除会话数据</button>
    <button onclick="sessionClearLocal()">清空会话数据</button>

    <script>
        let person = {"userName":"杨丹妮","age":20,"hobby":"打游戏"};
        //存储数据,Window 关键字可以省略,默认是 window 作用域
        function saveLocal(){
            window.localStorage.setItem("userName", "林舒婷");
            localStorage.setItem("age", 18);
            localStorage.setItem("hobby", "写代码");
            localStorage.setItem("person", JSON.stringify(person));
        }

        //读取数据
        function readLocal(){
            console.log(localStorage.getItem("userName"));
            console.log(localStorage.getItem("age"));
            console.log(localStorage.getItem("hobby"));
            const person = JSON.parse(localStorage.getItem("person"));
            console.log(person);
        }

        //删除
        function deleteLocal(){
            localStorage.removeItem("userName");
            localStorage.removeItem("age");
            localStorage.removeItem("hobby");
            localStorage.removeItem("person");
        }

        //清空
        function clearLocal(){
            localStorage.clear();
        }
    </script>

    <script>
        let sessionPerson = {"userName":"靓仔","income":"2万","hobby":"夜跑"};
        //存储数据,Window 关键字可以省略,默认是 window 作用域
        function sessionSaveLocal(){
            window.sessionStorage.setItem("skill", "撩妹");
            sessionStorage.setItem("sessionPerson", JSON.stringify(sessionPerson));
        }

        //读取数据
        function sessionReadLocal(){
            console.log(sessionStorage.getItem("skill"));
            const sessionPerson = JSON.parse(sessionStorage.getItem("sessionPerson"));
            console.log(sessionPerson);
        }

        //删除
        function sessionDeleteLocal(){
            sessionStorage.removeItem("skill");
            sessionStorage.removeItem("sessionPerson");
        }

        //清空
        function sessionClearLocal(){
            sessionStorage.clear();
        }
    </script>
</body>
</html>

测试结果:

小结:

1、本地存储和会话存储,方法都是接收一个键值对作为参数,把内容添加到浏览器存储中。

2、sessionStorage存储的内容会随着浏览器窗口的关闭而消失。

3、xxxStorage.getItem("xxx")如果获取不到 value,返回 null

4、JSON.parse(null) 的结果还是 null

6、组件的自定义事件

引言:html 元素有一些默认事件,比如:click 事件、keyup 事件、input 事件等等。我们这里主要介绍的事组件的自定义事件。

我们之前学过的,用 props 配置,用于父组件给子组件传递数据。也学过 ref 属性,提供父组件获取子组件信息的。

这里主要讲解通过组件的自定义事件,用于子组件给父组件传递数据(子 ===> 父)。

1、子组件想给父组件传递数据,就需要在父组件中给子组件绑定自定义事件,然后在父组件中处理事件的回调函数。

2、绑定自定义事件主要有 2 种方式:

方式①:在父组件中,使用 @ 或者 v-on 声明,例如:

<Song @zhoujielun="getSongName"></Song>

方式②:在父组件中使用挂载的方式,如:

<Song ref="sName" @click.native="showMessage"></Song>
... ... 其它代码
mounted(){
 this.$refs.sName.$on("zhoujielun", this.getSongName);
}

如果想让自定义事件只触发一次,可以使用 once 修饰符,或者 $once 方法

3、触发自定义事件:this.$emit("自定义事件名", 参数);    例如:

this.$emit("zhoujielun", this.songName);

4、解绑自定义事件:this.$off("自定义事件名");

this.$off("zhoujielun");

5、组件上也可以使用原生 DOM 事件,需要加 .native 修饰符。

6、注意:通过 this.$refs.xxx.$on("自定义事件名",回调); 绑定自定义事件时,回调要么配置在 methods 中,要么使用箭头函数。否则 this 指向会有问题。

this.$refs.sName.$on("zhoujielun", this.getSongName);

代码示例:

①App.vue

<template>
  <div>
    <h2>电影子组件传递的电影数据是:{{movieName}}</h2>
    <h2>歌曲子组件传递的电影数据是:{{songName}}</h2>

    <!-- 通过父组件给子组件传递函数类型的props实现:子给父传递数据 -->
    <Movie :getMovieName="getMovieName"></Movie>

    <!-- 通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第一种写法,使用@或v-on) -->
    <Song @zhoujielun="getSongName"></Song>

    <!-- 通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第二种写法,使用ref) -->
    <!-- 另外,如果组件里想要原生的事件,需要加 .native -->
    <Song ref="sName" @click.native="showMessage"></Song>

  </div>
</template>

<script>
//先引用哪个,就先解析哪个
import Movie from "./components/Movie";
import Song from "./components/Song";
export default {
  name: "App",
  components: { Movie, Song },
  data(){
    return {
      movieName:"",
      songName:"",
    }
  },
  // 写法一:通过回调函数来获取子组价传递的数据
  methods:{
    getMovieName(movieName){
      console.log("父组件App收到子组件传递的电影名:",movieName);
      this.movieName = movieName;
    },
    getSongName(songName){
      console.log("父组件App收到子组件传递的歌曲名:",songName);
      this.songName = songName;
    },
    showMessage(){
      console.log("我是通过第二种方式(挂载)来获取子组件数据的");
    }
  },
  // 写法二:通过挂载的方式,灵活性更高
  mounted(){
    //写法一
    this.$refs.sName.$on("zhoujielun", this.getSongName);
    //写法二
    /*
    this.$refs.sName.$on("zhoujielun",(songName)=>{
      console.log("通过箭头函数处理数据");
      this.songName = songName;
    });
    */
  }
};
</script>

<style>
</style>

②Movie.vue 代码

<template>
  <div class="nice">
    <h3>演员:{{userName}}</h3>
    <h3>电影:{{movieName}}</h3>
    <button @click="sendMovieName">把电影名称传递给父组件App</button>
  </div>
</template>

<script>
export default {
  name: "Movie",
  props:["getMovieName"],
  data() {
    return {
      userName: "周杰伦",
      movieName: "满城尽带黄金甲"
    }
  },
  methods:{
    sendMovieName(){
      this.getMovieName(this.movieName);
    }
  },

};
</script>

<style>
.nice{
  background-color: green;
}
</style>

③Song.vue 

<template>
  <div class="nice">
    <h3>歌手名:{{userName}}</h3>
    <h3>著名歌曲:{{songName}}</h3>
    <button @click="sendSongName">把歌曲名称传递给父组件App</button>
    <button @click="unbind">解绑 zhoujielun 事件</button>
    <button @click="destroy">销毁当前组件的事件</button>
    <hr />
  </div>
</template>

<script>
export default {
  name: "Song",
  data() {
    return {
      userName: "周杰伦",
      songName:"《夜曲》、《七里香》、《稻香》"
    };
  },
  methods:{
    sendSongName(){
      this.$emit("zhoujielun", this.songName);
    },
    unbind(){
      this.$off("zhoujielun");//解绑一个自定义事件
      //this.$off(["zhoujielun","other"]);//解绑多个自定义事件
      //this.$off();//解绑所有的自定义事件
    },
    destroy(){
      //销毁了当前组件的实例,销毁后所有Song实例的自定义事件全部失效。
      this.$destroy();
    }
  }
};
</script>

<style scoped>
.nice{
  /* 设置背景色为:天蓝色 */
  background-color: skyblue;
}
</style>

测试效果:

7、全局事件总线(GlobalEventBus)

全局事件总线(GlobalEventBus)
1、概念:全局事件总线是一种组件之间的通信方式适用于任意组件之间的通信
2、使用步骤:
①安装全局事件总线。

new Vue({
    ......
    beforeCreate(){
        Vue.prototype.$bus = this;//安装全局事件总线,$bus就是当前应用的 vm 对象
    }
    ......
})


②使用事件总线
接收数据:A组件想接收数据,则在A组件中给 $bus 绑定自定义事件,事件的回调留在 A 组件上。

method(){
    demo(data){......}
}
mounted(){
    this.$bus.$on("xxx", this.demo);
}

③提供数据:

this.$bus.$emit("xxx", 数据);

3、最好在 beforeDestroy 钩子中,用 $off 去解绑当前组件用到的所有事件。

代码示例:

①main.js

// 引入 Vue 文件
import Vue from 'vue'

//引入 App 组件,它是所有组件的父组件
import App from './App.vue'

// 关闭 Vue 的生产提示
Vue.config.productionTip = false

// 创建 Vue 实例对象
new Vue({
  // 将 app 组件放入 vm 容器中
  render: h => h(App),
  //安装全局事件总线
  beforeCreate() {
    Vue.prototype.$bus = this;
  }
}).$mount('#root') 

②App.vue

<template>
  <div>
    <Movie></Movie>
    <Singer></Singer>
  </div>
</template>

<script>
import Movie from "./components/Movie";
import Singer from "./components/Singer";
export default {
  name: "App",
  components: { Movie, Singer },
};
</script>

<style>
</style>

③Movie.vue

<template>
  <div id="movie">
    <h3>电影名称:{{ movieName }}</h3>
    <h3>电影主角:{{ actor }}</h3>
    <button @click="unbindBus">解绑事件总线</button>
    <hr />
  </div>
</template>

<script>
export default {
  name: "Movie", //组件名
  data() {
    return {
      movieName: "天若有情",
      actor: "",
    };
  },
  mounted() {
    // 绑定事件总线
    this.$bus.$on("transmitName", (data) => {
      console.log("电影组件,收到了数据=", data);
      this.actor = data;
    });
  },
  methods: {
    unbindBus() {
      // 解绑事件总线
      this.$bus.$off("transmitName");
      console.log("解绑了事件 transmitName");
    },
  },
  //一般建议在 beforeDestroy 钩子中解绑组件用到的所有总线
  beforeDestroy() {
    // 解绑事件总线
    this.$bus.$off("transmitName");
    console.log("解绑了事件 transmitName");
  },
};
</script>

<style>
#movie {
  color: green;
}
</style>

④Singer.vue

<template>
  <div id="singer">
    <h3>歌手名称:{{ singerName }}</h3>
    <h3>所在地区:{{ place }}</h3>
    <button @click="sendSingerNameToMovie">把歌手名传递给电影组件</button>
    <hr />
  </div>
</template>

<script>
export default {
  name: "Singer",
  data() {
    return {
      singerName: "刘德华",
      place: "香港",
    };
  },
  methods: {
    sendSingerNameToMovie() {
      // 分发事件
      this.$bus.$emit("transmitName", this.singerName);
    },
  },
};
</script>

<style>
#singer {
  color: blue;
}
</style>

效果:

8、消息订阅与发布

消息订阅与发布,与全局事件总线的思想相似。它包含以下操作:

1、订阅消息 —— 对应绑定事件监听

2、发布消息 —— 对应分发事件

3、取消订阅 —— 对应解绑事件监听

需要引入一个消息订阅与发布的第三方库,第三方开源的库有很多很多,我们方便学习选择:PubSub.js(即 publish+subscribe 的英文缩写)

具体步骤:

1、安装 pubsub:

 npm i pubsub-js

2、在组件中引入 pubsub:

import pubsub from "pubsub-js";

3、在组件中订阅消息(返回 messageId):

    this.messageId = pubsub.subscribe("transmitName", (msgName, data) => {
      console.log(
        "有人发布了 transmitName 消息,回调执行的数据为:",
        msgName,
        data
      );
      this.actor = data;
    });

4、取消订阅消息(注意这里的 this.messageId 要跟订阅消息的 id 保持一致)

pubsub.unsubscribe(this.messageId);

5、发布消息

pubsub.publish("transmitName", this.singerName);

代码示例:

①首先要在具体的项目安装 pubsub-js

 npm i pubsub-js

②Movie.vue

<template>
  <div id="movie">
    <h3>电影名称:{{ movieName }}</h3>
    <h3>电影主角:{{ actor }}</h3>
    <hr />
  </div>
</template>

<script>
import pubsub from "pubsub-js";
export default {
  name: "Movie", //组件名
  data() {
    return {
      movieName: "龙兄虎弟",
      actor: "",
    };
  },
  mounted() {
    // 订阅消息
    this.messageId = pubsub.subscribe("transmitName", (msgName, data) => {
      console.log(
        "有人发布了 transmitName 消息,回调执行的数据为:",
        msgName,
        data
      );
      this.actor = data;
    });
  },
  //一般建议在 beforeDestroy 钩子中取消订阅
  beforeDestroy() {
    // 取消订阅
    pubsub.unsubscribe(this.messageId);
    console.log("取消订阅了消息", this.messageId);
  },
};
</script>

<style>
#movie {
  color: green;
}
</style>

③Singer.vue

<template>
  <div id="singer">
    <h3>歌手名称:{{ singerName }}</h3>
    <h3>所在地区:{{ place }}</h3>
    <button @click="sendSingerNameToMovie">把歌手名传递给电影组件</button>
    <hr />
  </div>
</template>

<script>
import pubsub from "pubsub-js";
export default {
  name: "Singer",
  data() {
    return {
      singerName: "成龙",
      place: "香港",
    };
  },
  methods: {
    sendSingerNameToMovie() {
      // 发布消息
      pubsub.publish("transmitName", this.singerName);
    },
  },
};
</script>

<style>
#singer {
  color: blue;
}
</style>

测试效果:

9、$nextTick 延迟回调

在 Vue.js 中,$nextTick 是一个非常重要的方法,它允许你在 DOM 更新循环结束之后执行延迟回调。在某些情况下,你可能需要在 DOM 更新后立即执行某些操作,比如我们想让用户点击某个按钮后,能直接获取到某个输入框的焦点,Vue 官方给我们提供了 $nextTick 这个属性来解决此问题。

用法示例(获得焦点):

  this.$nextTick(function () {
    this.$refs.inputActor.focus();
  });

代码示例:

<template>
  <div id="movie">
    <h3>电影名称:{{ movieName }}</h3>
    电影主角:<input ref="inputActor" type="text" />
    <button @click="getFocus()">点击获取输入框焦点</button>
    <hr />
  </div>
</template>

<script>
export default {
  name: "Movie", //组件名
  data() {
    return {
      movieName: "叶问",
    };
  },
  methods: {
    getFocus() {
      this.$nextTick(function () {
        console.log("$nextTick 延迟回调");
        this.$refs.inputActor.focus();
      });
    },
  },
};
</script>

<style>
#movie {
  color: green;
}
</style>

测试效果:

10、配置代理

我们知道,在使用 AJAX 异步请求时,会经常遇到【跨域】的问题,标准的做法就是后端服务器来处理跨域问题(跨域也叫违背了“同源策略”,何为“同源”,即:协议、域名、端口号 必须完全相同),详情参考博客(也是本章节使用的后端请求案例,同时去掉 CorsConfig 配置类,不在后端处理跨域,而是通过 Vue 的方式来处理跨域):

系列学习前端之第 7 章:一文掌握 AJAX-CSDN博客

在 Vue 的开发过程中,我们要处理 AJAX 异步请求的跨域问题,可以使用代理服务器的方式。主要有两种方式。

10.1 简单配置代理

在 vue.config.js 中增加以下配置即可 

  devServer: {
    // 对应服务端的协议、地址和端口号
    proxy: "http://localhost:8000"
  }

具体演示:

①在 Vue 体系中,我们使用 axios 来发送 AJAX 请求。所以需要先引入 axios 组件。

npm i axios

②异常代码示例(默认没有做跨域处理,会出现跨域异常):

<template>
  <div>
    <button @click="getHelloWorld">获取 helloWorld 数据</button>
    <button @click="say">获取 say 数据</button>
  </div>
</template>

<script>
import axios from "axios";
export default {
  name: "App",
  methods: {
    getHelloWorld() {
      axios.get("http://localhost:8000/helloWorld").then(
        (response) => {
          console.log("请求成功了:", response.data);
        },
        (error) => {
          console.log("请求失败了:", error.message);
        }
      );
    },
    say() {
      axios.post("http://localhost:8000/say").then(
        (response) => {
          console.log("请求成功了:", response.data);
        },
        (error) => {
          console.log("请求失败了:", error.message);
        }
      );
    },
  },
};
</script>

<style>
</style>

③测试效果(出现跨域异常,注意:出现跨域主要是因为我们的端口号不一样导致,因为协议都是 http,主机名都是 localhost,而前端的端口号是 8080,后端的端口号是 8000。另外,请求已经到达了后端,并且后端正常返回了数据,只是数据卡在浏览器这里,发现跨域,拒绝给前端调用者):

10.1.1 解决跨域

在 vue.config.js 中增加配置,完整代码如下(注意要使用 localhost,而不是 127.0.0.1):

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  //关闭在开发环境下每次保存代码时都进行代码质量检查
  lintOnSave: false,
  devServer: {
    // 对应服务端的协议、地址和端口号
    proxy: "http://localhost:8000"
  }
})

同时,需要修改前端的请求接口成代理服务器的接口 8080,往代理服务器发请求,而不是直接往后端服务发请求。如图:

重启服务(因为修改了配置,增加了代理服务器),二次测试效果(响应成功):

使用代理服务器的架构图如下:

1、代理服务器相当于一个【中介】的角色,负责接收浏览器的请求,然后转发到后端服务器,并且保证了浏览器和代理服务器、代理服务器和后端服务器,都不存在跨域的问题。

2、浏览器访问接口的地址,要是代理服务器的地址和端口号,不能直接访问后端服务器。

10.1.2 简单配置代理的优缺点

优点:配置简单,请求资源时直接发给代理服务器(8080)即可。

缺点:不能配置多个代理,不能灵活的控制请求是否走代理。体现在:所有的请求优先匹配前端已有的资源,当请求了前端不存在的资源时,才会发请求到后端服务器。

验证简单配置代理的缺点。在 public 目录下,创建一个文件 helloWorld,输入几个字符串

然后重启服务,继续测试,发现直接读取了前端资源,而没有请求到后端服务器:

10.2 多代理配置

企业级项目的开发,我们基本是采用多代理配置的方式,它可以灵活的控制请求是否走代理。

proxy: {
  // 匹配所有以 api 开头的请求
  "/api": {
	target: "http://localhost:8000",//代理目标的基础路径
	changeOrigin: true,//默认值true。设置为true的话,后端服务器收到的请求头中的host为8000;设置为false,接收到host为8080。
	pathRewrite: {//替换请求路径,key值是正则表达式
	  "^/api": ""
	},
	ws: true,//开启websocket协议,默认开启 true
  }
}

1、"/api":表示匹配所有以 api 开头的请求,可以换成别的路径,如 study

2、target:代理目标的基础路径,就是真实的后端服务器地址

3、changeOrigin:默认为 true。设置为true的话,后端服务器收到的请求头中的host为8000;设置为false,接收到host为8080。就是让代理服务器【撒谎】。

4、pathRewrite:替换请求路径,key值是正则表达式

vue.config.js 完整配置如下:

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  //关闭在开发环境下每次保存代码时都进行代码质量检查
  lintOnSave: false,
  devServer: {
    // 简单代理配置:对应服务端的协议、地址和端口号
    // proxy: "http://localhost:8000"
    // 多代理配置
    proxy: {
      // 匹配所有以 api 开头的请求
      "/api": {
        target: "http://localhost:8000",//代理目标的基础路径
        changeOrigin: true,//默认值true。设置为true的话,后端服务器收到的请求头中的host为8000;设置为false,接收到host为8080。
        pathRewrite: {//替换请求路径,key值是正则表达式
          "^/api": ""
        },
        ws: true,//开启websocket协议,默认开启 true
      },
      "/study": {
        target: "http://localhost:8000",//代理目标的基础路径
        pathRewrite: {//替换请求路径,key值是正则表达式
          "^/study": ""
        }
      }
    }
  }
})

需要修改对应的组件请求路径,增加代理服务器设置的 url 规则,如图:

验证结果:当发送 helloWorld 请求时,代理服务器会把 /api 替换为空字符串,传递到后端服务器就是正常的路径。如果要走代理,必须要增加 url 前缀的配置,如 /api。

11、插槽

父组件向子组件传递带数据的标签,当一个组件有不确定的结构时,就需要使用 slot 技术,注意:插槽内容是在父组件中编译后,再传递给子组件的。

插槽的作用是让父组件可以向子组件的指定位置插入 html 代码结构,也是一种组件之间的通信方式。适用于 父组件 ——> 子组件 传递数据。

插槽的分类有:默认插槽、具名插槽、作用域插槽。

11.1 默认插槽

使用步骤:

1、在父组件中定义 html 代码,如:

    <Mall title="书籍">
      <ul>
        <li v-for="(b, index) in books" :key="index">{{ b }}</li>
      </ul>
    </Mall>

2、在子组件中设置插槽,如:

<template>
  <div id="mall">
    <h3>{{ title }}分类</h3>
    <!-- 定义一个插槽,让组件使用者来填充 -->
    <slot>当使用者没有传递具体结构时,会出现托底展示的数据</slot>
  </div>
</template>

完整代码示例:

①App.vue

<template>
  <div class="container">
    <Mall title="美景">
      <img
        src="https://dd59176.cdn.bcebos.com/uploads/images/pageimg/20230523/1-2305231333323.png"
        alt=""
      />
    </Mall>
    <Mall title="书籍">
      <ul>
        <li v-for="(b, index) in books" :key="index">{{ b }}</li>
      </ul>
    </Mall>
    <Mall title="电影">
      <video
        controls
        src="https://fanyiapp.cdn.bcebos.com/app/video/yangliang/E77_focus.mp4"
      ></video>
    </Mall>
  </div>
</template>

<script>
import Mall from "./components/Mall";
export default {
  name: "App",
  components: { Mall },
  data() {
    return {
      books: ["《一千零一夜》", "《老人与海》", "《安徒生童话集》"],
    };
  },
};
</script>

<style scoped>
.container {
  display: flex;
  justify-content: space-around;
}
</style>

②Mall.vue

<template>
  <div id="mall">
    <h3>{{ title }}分类</h3>
    <!-- 定义一个插槽,让组件使用者来填充 -->
    <slot>当使用者没有传递具体结构时,会出现托底展示的数据</slot>
  </div>
</template>

<script>
export default {
  name: "Mall", //组件名
  props: ["title"],
};
</script>

<style>
#mall {
  background-color: skyblue;
  width: 200px;
  height: 300px;
}
h3 {
  text-align: center;
  background-color: orange;
}
video {
  width: 100%;
  height: 70%;
}
img {
  width: 100%;
}
</style>

测试效果:

11.2 具名插槽

使用步骤:

1、在父组件中,增加插槽的名字,如:

    <Mall title="书籍">
      <ul slot="center">
        <li v-for="(b, index) in books" :key="index">{{ b }}</li>
      </ul>
      <div class="bottom" slot="footer">
        <ul>
          <li v-for="(t, index) in otherBooks" :key="index">{{ t }}</li>
        </ul>
      </div>
    </Mall>

2、在子组件中,也要指定插槽的名字,如:

<template>
  <div id="mall">
    <h3>{{ title }}分类</h3>
    <!-- 定义一个插槽,让组件使用者来填充 -->
    <slot name="center">我是中间区域的插槽</slot>
    <slot name="footer">我是底部区域的插槽</slot>
  </div>
</template>

完整代码示例:

①Mall.vue 代码

<template>
  <div id="mall">
    <h3>{{ title }}分类</h3>
    <!-- 定义一个插槽,让组件使用者来填充 -->
    <slot name="center">我是中间区域的插槽</slot>
    <slot name="footer">我是底部区域的插槽</slot>
  </div>
</template>

<script>
export default {
  name: "Mall", //组件名
  props: ["title"],
};
</script>

<style>
#mall {
  background-color: skyblue;
  width: 200px;
  height: 300px;
}
h3 {
  text-align: center;
  background-color: orange;
}
video {
  width: 100%;
  height: 50%;
}
img {
  width: 100%;
}
</style>

②App.vue 代码

<template>
  <div class="container">
    <Mall title="美景">
      <img
        slot="center"
        src="https://dd59176.cdn.bcebos.com/uploads/images/pageimg/20230523/1-2305231333323.png"
        alt=""
      />
      <a slot="footer" href="http://www.baidu.com">点击查看更多美景</a>
    </Mall>
    <Mall title="书籍">
      <ul slot="center">
        <li v-for="(b, index) in books" :key="index">{{ b }}</li>
      </ul>
      <div class="bottom" slot="footer">
        <ul>
          <li v-for="(t, index) in otherBooks" :key="index">{{ t }}</li>
        </ul>
      </div>
    </Mall>
    <Mall title="电影">
      <video
        slot="center"
        controls
        src="https://fanyiapp.cdn.bcebos.com/app/video/yangliang/E77_focus.mp4"
      ></video>
      <!-- v-slot 的方式只适用于 template -->
      <template v-slot:footer>
        <div class="bottom">
          <a href="http://www.baidu.com">欧美</a>
          <a href="http://www.baidu.com">香港</a>
          <a href="http://www.baidu.com">大陆</a>
        </div>
        <h4>观影地址:万达影城</h4>
      </template>
    </Mall>
  </div>
</template>

<script>
import Mall from "./components/Mall";
export default {
  name: "App",
  components: { Mall },
  data() {
    return {
      books: ["《一千零一夜》", "《老人与海》", "《安徒生童话集》"],
      otherBooks: [
        "《Vue从入门到精通》",
        "《SpringBoot从入门到精通》",
        "《MySQL从入门到精通》",
      ],
    };
  },
};
</script>

<style scoped>
.container,
.bottom {
  display: flex;
  justify-content: space-around;
}
h4 {
  text-align: center;
}
</style>

测试效果:

11.3 作用域插槽

理解:数据在组件的自身,单根据数据生成的结构需要组件的使用者来决定。(books数据在 Mall 中,但是使用数据所遍历出来的结构由 APP 组件决定)

使用步骤:

1、父组件中

<template>
  <div class="container">
    <Mall title="书籍">
      <!-- 作用域插槽必须使用 template -->
      <template scope="four">
        <ul>
          <li v-for="(b, index) in four.books" :key="index">{{ b }}</li>
        </ul>
      </template>
    </Mall>

    <Mall title="书籍">
      <!-- 作用域插槽必须使用 template -->
      <!-- 使用 scope 或者 slot-scope 都可以 -->
      <template slot-scope="{ books }">
        <ul>
          <li style="color: red" v-for="(b, index) in books" :key="index">
            {{ b }}
          </li>
        </ul>
      </template>
    </Mall>
  </div>
</template>

2、子组件中

<template>
  <div id="mall">
    <h3>{{ title }}分类</h3>
    <!-- 定义一个插槽,让组件使用者来填充 -->
    <slot :books="mybooks">我是中间区域的插槽</slot>
  </div>
</template>

完整代码:

①App.vue 代码

<template>
  <div class="container">
    <Mall title="书籍">
      <!-- 作用域插槽必须使用 template -->
      <template scope="four">
        <ul>
          <li v-for="(b, index) in four.books" :key="index">{{ b }}</li>
        </ul>
      </template>
    </Mall>

    <Mall title="书籍">
      <!-- 作用域插槽必须使用 template -->
      <!-- 使用 scope 或者 slot-scope 都可以 -->
      <template slot-scope="{ books }">
        <ul>
          <li style="color: red" v-for="(b, index) in books" :key="index">
            {{ b }}
          </li>
        </ul>
      </template>
    </Mall>
  </div>
</template>

<script>
import Mall from "./components/Mall";
export default {
  name: "App",
  components: { Mall },
};
</script>

<style scoped>
.container,
.bottom {
  display: flex;
  justify-content: space-around;
}
</style>

②Mall.vue  代码

<template>
  <div id="mall">
    <h3>{{ title }}分类</h3>
    <!-- 定义一个插槽,让组件使用者来填充 -->
    <slot :books="mybooks">我是中间区域的插槽</slot>
  </div>
</template>

<script>
export default {
  name: "Mall", //组件名
  props: ["title"],
  data() {
    return {
      mybooks: ["《三国演义》", "《水浒传》", "《西游记》", "《红楼梦》"],
    };
  },
};
</script>

<style>
#mall {
  background-color: skyblue;
  width: 200px;
  height: 300px;
}
h3 {
  text-align: center;
  background-color: orange;
}
video {
  width: 100%;
  height: 50%;
}
img {
  width: 100%;
}
</style>

演示效果:

12、VueX(重点内容)

12.1 VueX 是什么?

1、概念:专门在 Vue 中实现集中式状态(数据)管理的一个 Vue 插件,对 vue 应用中多个组件的共享状态进行集中式的管理(读/写),也是一种组件间通信的方式,且适用于任意组件间通信。
2、Github 地址: https://github.com/vuejs/vuex

12.2 什么时候用 VueX?

1、多个组件依赖于同一状态
2、来自不同组件的行为需要变更同一状态

12.3 VueX 核心概念和API

摘自官网的 VueX 核心流程图如下:

在 VueX 中,主要有 3 个核心对象,分别是 Actions、Mutations、State。它们仨都是由 store 统一管控的,也就是由 store 提供容器让它们运转起来。

为了便于理解,可以把上图中的几个核心概念比喻成生活中的事物:

Vue Components:理解成去餐厅吃饭的客人。

Actions:理解成服务员,响应客户的需求,下单(非必须,客人可以直接跟后厨对接)。

Mutations:理解成后厨。接收到 Actions 的指令后,做菜。

State:理解成做好的菜,上菜给客人吃。

store:理解成餐厅,是他们运行的容器。

12.3.1 Actions

对 Actions 的理解:

1、值为一个对象,包含多个响应用户动作的回调函数;
2、通过 commit() 来触发 mutation 中函数的调用,间接更新 state;
3、如何触发 actions 中的回调?答案是:在组件中使用: $store.dispatch("对应的 action 回调名") 触发;
4、可以包含异步代码(定时器, ajax 等等)
5.、示例代码:

//准备 Actions 对象:响应组件中用户的动作
const actions = {
    //两数相加
    increment(context, value) {
        console.log("【两数相加】actions中的 increment 被调用了");
        context.commit("INCREMENT", value);
    },
    //两数相减
    decrement(context, value) {
        console.log("【两数相减】actions中的 decrement 被调用了");
        context.commit("DECREMENT", value);
    },
};

12.3.2 Mutations

对 Mutations 的理解:

1、值是一个对象,包含多个直接更新 state 的方法
2、谁能调用 mutations 中的方法?如何调用?答案:在 action 中使用:commit("对应的 mutations 方法名") 触发
3. mutations 中方法的特点:不能写异步代码、只能单纯的操作 state;

4、示例代码:

//准备 Mutations 对象:修改 State 中的数据
const mutations = {
    //两数相加
    INCREMENT(state, value) {
        console.log("【两数相加】mutations中的 INCREMENT 被调用了");
        state.sum += value;
    },
    //两数相减
    DECREMENT(state, value) {
        console.log("【两数相减】mutations中的 DECREMENT 被调用了");
        state.sum -= value;
    },

    //相加的结果为奇数时再加
    INCREMENT_ODD(state, value) {
        console.log("【相加的结果为奇数时再加】mutations中的 INCREMENT_ODD 被调用了");
        if (state.sum % 2 === 1) {
            state.sum += value;
        }
    },
    //延时相加
    INCREMENT_WAIT(state, value) {
        console.log("【延时相加】mutations中的 INCREMENT_Wait 被调用了");
        setTimeout(() => {
            state.sum += value;
        }, 500)
    },
};

12.3.3 State

对 State 的理解:

1、vuex 管理的状态对象;
2、它应该是唯一的;

3、组件中读取 vuex 中的数据:$store.state.sum
4、示例代码:

//准备 State 对象:保存具体的数据
const state = {
    //当前的和
    sum: 0
};

12.3.4 getters

对 getters 的理解:

1、值为一个对象,包含多个用于返回数据的函数;

2、如何使用?答案:$store.getters.xxx

12.3.5 Modules

对 modules 的理解:

1、包含多个 module

2、一个 module 是一个 store 的配置对象;

3、与一个组件(包含有共享数据)对应。

12.4 使用 VueX

12.4.1 安装 VueX

先明白如下版本关系:

vue2 中,要用 vuex 的3版本,即 vuex3
vue3 中,要用 vuex 的4版本,即 vuex4

在2022年2月7日,vue3成为了默认版本,如果直接执行 npm i vue,默认安装的是 vue3 版本,并且默认安装的是 vuex4 版本。
所以如果直接执行 npm i vuex,安装的就是默认的 vuex4 版本。但是,vuex4 版本只能在 vue3 环境中使用。本篇博客学习的是 vue2 版本,因此需要指定 vuex 的版本号。

执行如下命令,在项目中安装 vuex3 版本:

npm i vuex@3

12.4.2 基本使用

1、创建文件 src/store/index.js(或者是 src/vuex/store.js)

组件中修改 vuex 中的数据,可以使用 $store.dispatch("action中的方法名", 数据);   或者 $store.commit("mutations中的方法名", 数据);
备注:如果没有网络请求或者其他业务逻辑,组件中也可以越过 Actions,直接编写 commit。

//引入 Vue 核心库
import Vue from "vue";
//引入 Vuex
import Vuex from "vuex";
//应用 Vuex 插件
Vue.use(Vuex);

//准备 Actions 对象:响应组件中用户的动作
const actions = {
    //两数相加
    increment(context, value) {
        console.log("【两数相加】actions中的 increment 被调用了");
        context.commit("INCREMENT", value);
    },
    //两数相减
    decrement(context, value) {
        console.log("【两数相减】actions中的 decrement 被调用了");
        context.commit("DECREMENT", value);
    },
};

//准备 Mutations 对象:修改 State 中的数据
const mutations = {
    //两数相加
    INCREMENT(state, value) {
        console.log("【两数相加】mutations中的 INCREMENT 被调用了");
        state.sum += value;
    },
    //两数相减
    DECREMENT(state, value) {
        console.log("【两数相减】mutations中的 DECREMENT 被调用了");
        state.sum -= value;
    },

    //相加的结果为奇数时再加
    INCREMENT_ODD(state, value) {
        console.log("【相加的结果为奇数时再加】mutations中的 INCREMENT_ODD 被调用了");
        if (state.sum % 2 === 1) {
            state.sum += value;
        }
    },
    //延时相加
    INCREMENT_WAIT(state, value) {
        console.log("【延时相加】mutations中的 INCREMENT_Wait 被调用了");
        setTimeout(() => {
            state.sum += value;
        }, 500)
    },
};
//准备 State 对象:保存具体的数据
const state = {
    //当前的和
    sum: 0
};

//创建并暴露 store
export default new Vuex.Store({
    actions,
    mutations,
    state
})

2、在 main.js 中创建 vm 时传入 store 配置项

// 引入 Vue 文件
import Vue from 'vue'

//引入 App 组件,它是所有组件的父组件
import App from './App.vue'

//引入 store
import store from "./store";

// 关闭 Vue 的生产提示
Vue.config.productionTip = false

// 创建 Vue 实例对象
new Vue({
  // 将 app 组件放入 vm 容器中
  render: h => h(App),
  store,
}).$mount('#root') 

3、Calculate.vue

<template>
  <div>
    <h3>求和结果为:{{ $store.state.sum }}</h3>
    <select v-model.number="n">
      <option value="1">1</option>
      <option value="2">2</option>
      <option value="3">3</option>
    </select>
    <button @click="increment">两数相加</button>
    <button @click="decrement">两数相减</button>
    <button @click="incrementOdd">相加的结果为奇数时再加</button>
    <button @click="incrementWait">延时相加</button>
  </div>
</template>

<script>
export default {
  name: "Calculate",
  data() {
    return {
      n: 1, //用户选择的数字
    };
  },
  methods: {
    // 两数相加,使用 $store.dispatch 触发 actions 中的回调
    increment() {
      this.$store.dispatch("increment", this.n);
    },
    // 两数相减,使用 $store.dispatch 触发 actions 中的回调
    decrement() {
      this.$store.dispatch("decrement", this.n);
    },
    //相加的结果为奇数时再加,使用 $store.commit 触发 mutations 中的回调
    incrementOdd() {
      this.$store.commit("INCREMENT_ODD", this.n);
    },
    //延时相加,使用 $store.commit 触发 mutations 中的回调
    incrementWait() {
      this.$store.commit("INCREMENT_WAIT", this.n);
    },
  },
};
</script>

<style>
</style>

4、App.vue

<template>
  <div>
    <Calculate />
  </div>
</template>

<script>
import Calculate from "./components/Calculate";
export default {
  name: "App",
  components: { Calculate },
};
</script>

<style>
</style>

测试效果:

通过 Vue 的开发工具查看 Vuex,如图,在 Vue 工具的第二个图标,可以查看 Vuex 的执行过程。还可以根据需要进行调整。

12.4.3 Getters

1、概念:当 state 中的数据需要经过加工后再使用时,可以使用 getters 加工。

2、需要在 store.js 中追加 getters 配置,并对外暴露 store

3、在组件中读取数据:$store.getters.enlarge

代码示例

// 准备 getters:用于将 state 中的数据进行加工
const getters ={
    //将数据放大 10 倍
    enlarge(state){
        return state.sum * 10;
    }
}

//创建并暴露 store
export default new Vuex.Store({
    actions,
    mutations,
    state,
    getters,
})

完整代码示例:

①src/store/index.js

//引入 Vue 核心库
import Vue from "vue";
//引入 Vuex
import Vuex from "vuex";
//应用 Vuex 插件
Vue.use(Vuex);

//准备 Actions 对象:响应组件中用户的动作
const actions = {
    //两数相加
    increment(context, value) {
        console.log("【两数相加】actions中的 increment 被调用了");
        context.commit("INCREMENT", value);
    },
    //两数相减
    decrement(context, value) {
        console.log("【两数相减】actions中的 decrement 被调用了");
        context.commit("DECREMENT", value);
    },
};

//准备 Mutations 对象:修改 State 中的数据
const mutations = {
    //两数相加
    INCREMENT(state, value) {
        console.log("【两数相加】mutations中的 INCREMENT 被调用了");
        state.sum += value;
    },
    //两数相减
    DECREMENT(state, value) {
        console.log("【两数相减】mutations中的 DECREMENT 被调用了");
        state.sum -= value;
    },

    //相加的结果为奇数时再加
    INCREMENT_ODD(state, value) {
        console.log("【相加的结果为奇数时再加】mutations中的 INCREMENT_ODD 被调用了");
        if (state.sum % 2 === 1) {
            state.sum += value;
        }
    },
    //延时相加
    INCREMENT_WAIT(state, value) {
        console.log("【延时相加】mutations中的 INCREMENT_Wait 被调用了");
        setTimeout(() => {
            state.sum += value;
        }, 500)
    },
};
//准备 State 对象:保存具体的数据
const state = {
    //当前的和
    sum: 0
};

// 准备 getters:用于将 state 中的数据进行加工
const getters = {
    //将数据放大 10 倍
    enlarge(state) {
        return state.sum * 10;
    }
}

//创建并暴露 store
export default new Vuex.Store({
    actions,
    mutations,
    state,
    getters,
})

②Calculate.vue

<template>
  <div>
    <h3>求和结果为:{{ $store.state.sum }}</h3>
    <h3>当前求和放大10倍为:{{ $store.getters.enlarge }}</h3>
    <select v-model.number="n">
      <option value="1">1</option>
      <option value="2">2</option>
      <option value="3">3</option>
    </select>
    <button @click="increment">两数相加</button>
    <button @click="decrement">两数相减</button>
    <button @click="incrementOdd">相加的结果为奇数时再加</button>
    <button @click="incrementWait">延时相加</button>
  </div>
</template>

<script>
export default {
  name: "Calculate",
  data() {
    return {
      n: 1, //用户选择的数字
    };
  },
  methods: {
    // 两数相加,使用 $store.dispatch 触发 actions 中的回调
    increment() {
      this.$store.dispatch("increment", this.n);
    },
    // 两数相减,使用 $store.dispatch 触发 actions 中的回调
    decrement() {
      this.$store.dispatch("decrement", this.n);
    },
    //相加的结果为奇数时再加,使用 $store.commit 触发 mutations 中的回调
    incrementOdd() {
      this.$store.commit("INCREMENT_ODD", this.n);
    },
    //延时相加,使用 $store.commit 触发 mutations 中的回调
    incrementWait() {
      this.$store.commit("INCREMENT_WAIT", this.n);
    },
  },
};
</script>

<style>
</style>

测试结果:

12.4.4 四个 Map 方法的使用

四个 Map 方法指的是 mapState、mapGetters、mapActions、mapMutations

mapState:用于帮助我们映射 state 中的数据为计算属性

mapGetters:用于帮助我们映射 getters 中的数据为计算属性

mapActions:用于帮助我们生成与 Actions 对话的方法,即包含 $store.dispatch(xxx) 的函数

mapMutations:用于帮助我们生成与 Mutations 对话的方法,即包含 $store.commit(xxx) 的函数

备注:mapActions与mapMutations使用时,若需要传递参数,在模板中绑定事件时需要传递参数,否则参数是事件对象。

12.4.4.1 mapState 方法

代码示例:

<script>
import { mapState } from "vuex";
export default {
  name: "Calculate",
  computed: {
    //借助 mapState 生成计算属性,从 state 中读取数据(对象写法)
    //...mapState({ pai: "pai", gouGu: "gouGu" }),

    //借助 mapState 生成计算属性,从 state 中读取数据(数组写法)
    ...mapState(["pai", "gouGu"]),
  },
};
</script>

12.4.4.2 mapGetters 方法

代码示例:

<script>
import { mapGetters } from "vuex";
export default {
  name: "Calculate",
  computed: {
    //借助 mapGetters 生成计算属性,从 getters中读取数据。(对象写法)
    //...mapGetters({ area: "area" }),

    //借助 mapGetters 生成计算属性,从 getters中读取数据。(数组写法)
    ...mapGetters(["area"]),
  },
};
</script>

12.4.4.3 mapActions 方法

注意:如果使用数组的写法,需要保证①调用函数的地方、②数组的写法、③index.js里 Actions 的函数名三者保持一致!

代码示例:

  methods: {
    /*************** 对象写法  *******************/
    //借助 mapActions 生成对应的方法,方法中会调用 dispatch 去联系 actions (对象写法)
    //...mapActions({ increment: "increment", decrement: "decrement" }),

    /*************** 数组写法  *******************/
    // 借助 mapActions 生成对应的方法,方法中会调用 dispatch 去联系 actions (数组写法)
    ...mapActions(["increment", "decrement"]),
  },

12.4.4.4 mapMutations 方法

注意:如果使用数组的写法,需要保证①调用函数的地方、②数组的写法、③index.js里mutations 的函数名三者保持一致!

代码示例:

  methods: {
    /*************** 对象写法  *******************/
    //借助 mapMutations 生成对应的方法,方法中会调用 commit 去联系 mutations (对象写法)
    ...mapMutations({
      incrementOdd: "INCREMENT_ODD",
      incrementWait: "INCREMENT_WAIT",
    }),

    /*************** 数组写法  *******************/
    //借助 mapMutations 生成对应的方法,方法中会调用 commit 去联系 mutations (数组写法)
    //注意:如果使用数组的写法,需要保证①调用函数的地方、②数组的写法、③index.js里mutations的函数名三者保持一致!
    //...mapMutations(["INCREMENT_ODD", "INCREMENT_WAIT"]),
  },

完整示例代码:

①src/store/index.js

//引入 Vue 核心库
import Vue from "vue";
//引入 Vuex
import Vuex from "vuex";
//应用 Vuex 插件
Vue.use(Vuex);

//准备 Actions 对象:响应组件中用户的动作
const actions = {
    //两数相加
    increment(context, value) {
        console.log("【两数相加】actions中的 increment 被调用了");
        context.commit("INCREMENT", value);
    },
    //两数相减
    decrement(context, value) {
        console.log("【两数相减】actions中的 decrement 被调用了");
        context.commit("DECREMENT", value);
    },
};

//准备 Mutations 对象:修改 State 中的数据
const mutations = {
    //两数相加
    INCREMENT(state, value) {
        console.log("【两数相加】mutations中的 INCREMENT 被调用了");
        state.sum += value;
    },
    //两数相减
    DECREMENT(state, value) {
        console.log("【两数相减】mutations中的 DECREMENT 被调用了");
        state.sum -= value;
    },

    //相加的结果为奇数时再加
    INCREMENT_ODD(state, value) {
        console.log("【相加的结果为奇数时再加】mutations中的 INCREMENT_ODD 被调用了");
        if (state.sum % 2 === 1) {
            state.sum += value;
        }
    },
    //延时相加
    INCREMENT_WAIT(state, value) {
        console.log("【延时相加】mutations中的 INCREMENT_Wait 被调用了");
        setTimeout(() => {
            state.sum += value;
        }, 500)
    },
};

//准备 State 对象:保存具体的数据
const state = {
    //当前的和
    sum: 0,
    pai: 3.141592654,
    gouGu: "勾三股四弦五",
    triangle: "三角形面积计算公式:底*高/2"
};

// 准备 getters:用于将 state 中的数据进行加工
const getters = {
    //将数据放大 10 倍
    enlarge(state) {
        return state.sum * 10;
    },
    //三角形面积公式
    area(state) {
        return state.triangle;
    }
}

//创建并暴露 store
export default new Vuex.Store({
    actions,
    mutations,
    state,
    getters,
})

②Calculate.vue

<template>
  <div>
    <h3>圆周率为:{{ pai }}</h3>
    <h3>勾股定理是:{{ gouGu }}</h3>
    <h3>三角形面积公式:{{ area }}</h3>
    <hr />
    <h3>求和结果为:{{ sum }}</h3>
    <h3>当前求和放大10倍为:{{ enlarge }}</h3>
    <select v-model.number="n">
      <option value="1">1</option>
      <option value="2">2</option>
      <option value="3">3</option>
    </select>
    <button @click="increment(n)">两数相加</button>
    <button @click="decrement(n)">两数相减</button>
    <button @click="incrementOdd(n)">相加的结果为奇数时再加</button>
    <button @click="incrementWait(n)">延时相加</button>
  </div>
</template>

<script>
import { mapState, mapGetters, mapMutations, mapActions } from "vuex";
export default {
  name: "Calculate",
  data() {
    return {
      n: 1, //用户选择的数字
    };
  },
  computed: {
    //靠程序员自己写的计算属性
    /*
    pai() {
      return this.$store.state.pai;
    },
    gouGu() {
      return this.$store.state.gouGu;
    },
    //getter 属性
    area() {
      return this.$store.getters.area;
    },
    */

    /**********************  对象写法  *******************/
    //借助 mapState 生成计算属性,从 state 中读取数据(对象写法)
    //...mapState({ sum: "sum", pai: "pai", gouGu: "gouGu" }),

    //借助 mapGetters 生成计算属性,从 getters中读取数据。(对象写法)
    //...mapGetters({ area: "area", enlarge: "enlarge" }),

    /**********************  数组写法  *******************/
    //借助 mapState 生成计算属性,从 state 中读取数据(数组写法)
    ...mapState(["sum", "pai", "gouGu"]),

    //借助 mapGetters 生成计算属性,从 getters中读取数据。(数组写法)
    ...mapGetters(["enlarge", "area"]),
  },
  methods: {
    // 程序员亲自写方法
    /*
    // 两数相加,使用 $store.dispatch 触发 actions 中的回调
    increment() {
      this.$store.dispatch("increment", this.n);
    },
    // 两数相减,使用 $store.dispatch 触发 actions 中的回调
    decrement() {
      this.$store.dispatch("decrement", this.n);
    },
    //相加的结果为奇数时再加,使用 $store.commit 触发 mutations 中的回调
    incrementOdd() {
      this.$store.commit("INCREMENT_ODD", this.n);
    },
    //延时相加,使用 $store.commit 触发 mutations 中的回调
    incrementWait() {
      this.$store.commit("INCREMENT_WAIT", this.n);
    },
    */

    /*************** 对象写法  *******************/
    //借助 mapActions 生成对应的方法,方法中会调用 dispatch 去联系 actions (对象写法)
    //...mapActions({ increment: "increment", decrement: "decrement" }),

    //借助 mapMutations 生成对应的方法,方法中会调用 commit 去联系 mutations (对象写法)
    ...mapMutations({
      incrementOdd: "INCREMENT_ODD",
      incrementWait: "INCREMENT_WAIT",
    }),

    /*************** 数组写法  *******************/
    // 借助 mapActions 生成对应的方法,方法中会调用 dispatch 去联系 actions (数组写法)
    ...mapActions(["increment", "decrement"]),

    //借助 mapMutations 生成对应的方法,方法中会调用 commit 去联系 mutations (数组写法)
    //注意:如果使用数组的写法,需要保证①调用函数的地方、②数组的写法、③index.js里mutations的函数名三者保持一致!
    //...mapMutations(["INCREMENT_ODD", "INCREMENT_WAIT"]),
  },
};
</script>

<style>
</style>

测试效果:

12.5 VueX 多组件共享数据

完整代码:

①App.vue

<template>
  <div>
    <Calculate />
    <hr />
    <Movie />
  </div>
</template>

<script>
import Calculate from "./components/Calculate";
import Movie from "./components/Movie";
export default {
  name: "App",
  components: { Calculate, Movie },
};
</script>

<style>
</style>

②src/store/index.js

//引入 Vue 核心库
import Vue from "vue";
//引入 Vuex
import Vuex from "vuex";
//应用 Vuex 插件
Vue.use(Vuex);

//准备 Actions 对象:响应组件中用户的动作
const actions = {
    //两数相加
    increment(context, value) {
        console.log("【两数相加】actions中的 increment 被调用了");
        context.commit("INCREMENT", value);
    },

};

//准备 Mutations 对象:修改 State 中的数据
const mutations = {
    //两数相加
    INCREMENT(state, value) {
        console.log("【两数相加】mutations中的 INCREMENT 被调用了");
        state.sum += value;
    },
    //添加电影名称
    ADD_MOVIE(state, value) {
        console.log("【添加电影名称】mutations中的 ADD_MOVIE 被调用了");
        state.movieList.unshift(value);
    }
};

//准备 State 对象:保存具体的数据
const state = {
    //当前的和
    sum: 0,
    movieList: [
        { id: "001", name: "《功夫》" }
    ]
};

// 准备 getters:用于将 state 中的数据进行加工
const getters = {
    //将数据放大 10 倍
    enlarge(state) {
        return state.sum * 10;
    },
}

//创建并暴露 store
export default new Vuex.Store({
    actions,
    mutations,
    state,
    getters,
})

③Calculate.vue

<template>
  <div>
    <h3>求和结果为:{{ sum }}</h3>
    <h3>当前求和放大10倍为:{{ enlarge }}</h3>
    <select v-model.number="n">
      <option value="1">1</option>
      <option value="2">2</option>
      <option value="3">3</option>
    </select>
    <button @click="increment(n)">两数相加</button>
    <h3 style="color: red">Movie组件的总电影数是:{{ movieList.length }}</h3>
  </div>
</template>

<script>
import { mapState, mapGetters, mapActions } from "vuex";
export default {
  name: "Calculate",
  data() {
    return {
      n: 1, //用户选择的数字
    };
  },
  computed: {
    //借助 mapState 生成计算属性,从 state 中读取数据(对象写法)
    ...mapState({ sum: "sum", movieList: "movieList" }),

    //借助 mapGetters 生成计算属性,从 getters中读取数据。(对象写法)
    ...mapGetters({ enlarge: "enlarge" }),
  },
  methods: {
    //借助 mapActions 生成对应的方法,方法中会调用 dispatch 去联系 actions (对象写法)
    ...mapActions({ increment: "increment" }),
  },
};
</script>

<style>
</style>

④Movie.vue

<template>
  <div>
    <h3>电影列表</h3>
    <h3 style="color: blue">Calculate 组件求和结果:{{ sum }}</h3>
    <input type="text" placeholder="请输入电影名称" v-model="name" />
    <button @click="addMovie">添加电影名称</button>
    <ul>
      <li v-for="m in movieList" :key="m.id">{{ m.name }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  name: "Movie",
  data() {
    return {
      name: "",
    };
  },
  computed: {
    //电影名集合
    movieList() {
      return this.$store.state.movieList;
    },
    //获取求和的结果
    sum() {
      return this.$store.state.sum;
    },
  },
  methods: {
    addMovie() {
      let randomId = Math.random().toString(36).substring(2, 9);
      const movieObj = { id: randomId, name: this.name };
      this.$store.commit("ADD_MOVIE", movieObj);
      this.name = "";
    },
  },
};
</script>

<style>
</style>

测试效果:点击两数相加,Movie组件可以获取到求和结果;增加电影,Calculate组件可以获取到总数。

12.6 VueX 模块化编码

 模块化+命名空间的作用及用法:

1、目的:让代码更好维护,让多种数据分类更加明确。
2、namespaced:true  开启命名空间
3、开启命名空间后,组件中读取 state 数据:

//方式1:自己直接读取
this.$store.state.movieModule.movieList;
//方式2:借助 mapState 读取
...mapState("movieModule", { movieList: "movieList" }),


4、开启命名空间后,组件中读取 getters 数据:

//方式1:自己直接读取
this.$store.getters["movieModule/firstMovieName"];
//方式2:借助 mapGetters 读取
...mapGetters("calculateModule", { enlarge: "enlarge" }),


5、开启命名空间后,组件中调用 dispatch

//方式1:自己直接 dispatch
this.$store.dispatch("movieModule/addMovieLang", movieObj);
//方式2:借助 mapActions
...mapActions("calculateModule", { increment: "increment" }),


6、开启命名空间后,组件中调用 commit

//方式1:自己直接 commit
this.$store.commit("movieModule/ADD_MOVIE", movieObj);
//方式2:借助 mapMutations
...mapMutations("calculateModule", {
      incrementOdd: "INCREMENT_ODD",
      incrementWait: "INCREMENT_WAIT",
    }),

 学习模块化编程用到后端服务器案例,以之前我们写过的为主:

系列学习前端之第 7 章:一文掌握 AJAX-CSDN博客

并且需要用到 axios,我们需要在项目里安装 axios:

npm i axios

完整案例代码结构图:

①main.js

// 引入 Vue 文件
import Vue from 'vue'

//引入 App 组件,它是所有组件的父组件
import App from './App.vue'

//引入 store
import store from "./store";

// 关闭 Vue 的生产提示
Vue.config.productionTip = false

// 创建 Vue 实例对象
new Vue({
  // 将 app 组件放入 vm 容器中
  render: h => h(App),
  store,
}).$mount('#root') 

②App.vue

<template>
  <div>
    <Calculate />
    <hr />
    <Movie />
  </div>
</template>

<script>
import Calculate from "./components/Calculate";
import Movie from "./components/Movie";
export default {
  name: "App",
  components: { Calculate, Movie },
};
</script>

<style>
</style>

③Calculate.vue

<template>
  <div>
    <h3>求和结果为:{{ sum }}</h3>
    <h3>当前求和放大10倍为:{{ enlarge }}</h3>
    <select v-model.number="n">
      <option value="1">1</option>
      <option value="2">2</option>
      <option value="3">3</option>
    </select>
    <button @click="increment(n)">两数相加</button>
    <h3 style="color: red">Movie组件的总电影数是:{{ movieList.length }}</h3>
  </div>
</template>

<script>
import { mapState, mapGetters, mapActions } from "vuex";
export default {
  name: "Calculate",
  data() {
    return {
      n: 1, //用户选择的数字
    };
  },
  computed: {
    //借助 mapState 生成计算属性,从 state 中读取数据(对象写法)
    ...mapState("calculateModule", { sum: "sum" }),
    ...mapState("movieModule", { movieList: "movieList" }),

    //借助 mapGetters 生成计算属性,从 getters中读取数据。(对象写法)
    ...mapGetters("calculateModule", { enlarge: "enlarge" }),
  },
  methods: {
    //借助 mapActions 生成对应的方法,方法中会调用 dispatch 去联系 actions (对象写法)
    ...mapActions("calculateModule", { increment: "increment" }),
  },
};
</script>

<style>
</style>

④Movie.vue

<template>
  <div>
    <h3>电影列表</h3>
    <h3 style="color: blue">Calculate 组件求和结果:{{ sum }}</h3>
    <h3>列表中第一个电影名是:{{ firstMovieName }}</h3>
    <input type="text" placeholder="请输入电影名称" v-model="name" />
    <button @click="addMovie">添加电影名称</button>
    <button @click="addMovieLang">添加带【狼】的电影名称</button>
    <button @click="addMovieServer">添加服务器返回的电影名称</button>
    <ul>
      <li v-for="m in movieList" :key="m.id">{{ m.name }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  name: "Movie",
  data() {
    return {
      name: "",
    };
  },
  computed: {
    //电影名集合
    movieList() {
      return this.$store.state.movieModule.movieList;
    },
    //获取求和的结果
    sum() {
      return this.$store.state.calculateModule.sum;
    },
    //getters 的模块化编码方式,带模块名和斜杠/
    firstMovieName() {
      return this.$store.getters["movieModule/firstMovieName"];
    },
  },
  methods: {
    addMovie() {
      let randomId = Math.random().toString(36).substring(2, 9);
      const movieObj = { id: randomId, name: this.name };
      this.$store.commit("movieModule/ADD_MOVIE", movieObj);
      this.name = "";
    },
    //带【狼】字电影名的判断
    addMovieLang() {
      let randomId = Math.random().toString(36).substring(2, 9);
      const movieObj = { id: randomId, name: this.name };
      this.$store.dispatch("movieModule/addMovieLang", movieObj);
      this.name = "";
    },
    //从服务器中获取数据
    addMovieServer() {
      this.$store.dispatch("movieModule/addMovieServer");
    },
  },
};
</script>

<style>
</style>

⑤src/store/index.js

//引入 Vue 核心库
import Vue from "vue";
//引入 Vuex
import Vuex from "vuex";
//引入自定义的计算js库
import calculateOptions from "./calculate";
import movieOptions from "./movie";
//应用 Vuex 插件
Vue.use(Vuex);

//创建并暴露 store
export default new Vuex.Store({
    modules: {
        calculateModule: calculateOptions,
        movieModule: movieOptions,
    }
})

⑥src/store/calculate.js

//求和相关逻辑
export default {
    //使用命名空间
    namespaced: true,
    actions: {
        //两数相加
        increment(context, value) {
            console.log("【两数相加】actions中的 increment 被调用了");
            context.commit("INCREMENT", value);
        }
    },
    //准备 Mutations 对象:修改 State 中的数据
    mutations: {
        //两数相加
        INCREMENT(state, value) {
            console.log("【两数相加】mutations中的 INCREMENT 被调用了");
            state.sum += value;
        },
        //添加电影名称
        ADD_MOVIE(state, value) {
            console.log("【添加电影名称】mutations中的 ADD_MOVIE 被调用了");
            state.movieList.unshift(value);
        }
    },

    //准备 State 对象:保存具体的数据
    state: {
        //当前的和
        sum: 0,
        movieList: [
            { id: "001", name: "《功夫》" }
        ]
    },

    // 准备 getters:用于将 state 中的数据进行加工
    getters: {
        //将数据放大 10 倍
        enlarge(state) {
            return state.sum * 10;
        },
    },
}

⑦src/store/movie.js

//电影相关逻辑
import axios from "axios";
export default {
    //使用命名空间
    namespaced: true,
    actions: {
        //带【狼】字电影名的判断
        addMovieLang(context, value) {
            if (value.name.indexOf("狼") >= 0) {
                context.commit("ADD_MOVIE", value);
            } else {
                alert("添加的电影名称必须带【狼】字");
            }
        },
        //从服务器中获取数据
        addMovieServer(context) {
            axios.get("http://localhost:8000/helloWorld")
                .then(
                    response => {
                        let randomId = Math.random().toString(36).substring(2, 9);
                        context.commit("ADD_MOVIE", { id: randomId, name: response.data });
                    },
                    error => {
                        alert(error.message);
                    }
                )
        },
    },
    mutations: {
        //添加电影名称
        ADD_MOVIE(state, value) {
            console.log("【添加电影名称】mutations中的 ADD_MOVIE 被调用了");
            state.movieList.unshift(value);
        }
    },
    state: {
        movieList: [
            { id: "001", name: "《功夫》" }
        ]
    },
    getters: {
        firstMovieName(state) {
            return state.movieList[0].name;
        }
    }
}

测试效果:

13、路由vue-router(重点内容)

13.1 什么是路由?

13.1.1 路由概念

  1. 一个路由就是一组映射关系(key - value)
  2. key 为路径, value 可能是 function 或 component
  3. vue-router 是 vue 的一个插件库,专门用来实现 SPA 应用
  4. router 是路由器,route 是路由,或者叫路由规则。

13.1.2 路由分类

  1. 后端路由
  2. 前端路由
13.1.2.1 后端路由

理解:value 是 function,用于处理客户端提交的请求。

工作过程:服务器接收到一个请求时,根据请求路径找到匹配的函数来处理请求,返回响应数据。

13.1.2.2 前端路由

理解:value 是 component,用于展示页面内容。
工作过程:当浏览器的路径改变时,对应的组件就会显示。

13.1.3 什么是 SPA

  1. 单页 Web 应用(single page web application,SPA)。
  2. 整个应用只有一个完整的页面。
  3. 点击页面中的导航链接不会刷新页面,只会做页面的局部更新。
  4. 数据需要通过 ajax 请求获取。

13.2 基本路由

先明白如下版本关系:

vue2 中,要用 vue-router 的3版本,即 vue-router3
vue3 中,要用 vue-router 的4版本,即 vue-router4

在2022年2月7日,vue3成为了默认版本,如果直接执行 npm i vue-router,默认安装的是 vue-router4 版本。
所以如果直接执行 npm i vue-router,安装的就是默认的 vue-router4 版本。但是,vue-router4 版本只能在 vue3 环境中使用。本篇博客学习的是 vue2 版本,因此需要指定 vue-router 的版本号。

1、执行如下命令,在项目中安装 vue-router3 版本

npm i vue-router@3

2、应用插件

Vue.use(VueRouter);

3、路由切换和指定展示位置

1、在组件中,使用标签 <router-link> 实现路由的切换。例如:

<router-link to="/travel">旅游胜地</router-link>

2、使用 <router-view> 实现路由内容的数据展示。

4、几个注意点

1、【路由组件】通常放在 pages 目录下,【一般组件】通常放在 components 目录下。
2、通过路由切换,会默认销毁掉之前的路由组件,在需要的时候 Vue 再给我们重新加载,也可以通过配置不销毁,即缓存路由组件
3、每个路由组件都有自己的 $route 属性里面存储着自己的路由信息。
4、整个应用只有一个 router(路由器),可以通过组件的 $router 属性获取到。

完整代码示例:

①代码结构图:

②main.js

// 引入 Vue 文件
import Vue from 'vue'
//引入 App 组件,它是所有组件的父组件
import App from './App.vue'
//引入VueRouter
import VueRouter from 'vue-router'
//引入路由器
import router from './router'
// 关闭 Vue 的生产提示
Vue.config.productionTip = false

//应用插件
Vue.use(VueRouter)

// 创建 Vue 实例对象
new Vue({
  // 将 app 组件放入 vm 容器中
  render: h => h(App),
  router: router
}).$mount('#root') 

③在 src 目录下,创建 router 目录,并创建 index.js

// 该文件专门用于创建整个应用的路由器
import VueRouter from 'vue-router'
//引入组件
import Travel from '../pages/Travel'
import Food from '../pages/Food'

//创建并暴露一个路由器
export default new VueRouter({
    routes: [
        {
            path: '/travel',
            component: Travel
        },
        {
            path: '/food',
            component: Food
        }
    ]
})

④组件 Food.vue

<template>
  <h2>我是 Food 各地美食的内容</h2>
</template>

<script>
export default {
  name: "Food",
  mounted() {
    console.log("Food 组件挂载完成");
    console.log(this.$route);
    console.log(this.$router);
  },
  beforeDestroy() {
    console.log("Food 组件即将被销毁");
  },
};
</script>

⑤组件 Travel.vue

<template>
  <h2>我是 Travel 旅游胜地的内容</h2>
</template>

<script>
export default {
  name: "Travel",
  mounted() {
    console.log("Travel 组件挂载完成");
    console.log(this.$route);
    console.log(this.$router);
  },
  beforeDestroy() {
    console.log("Travel 组件即将被销毁");
  },
};
</script>

⑥Banner.vue

<template>
  <div class="col-xs-offset-2 col-xs-8">
    <div class="page-header"><h2>欢迎来到 Vue Router 学习基地</h2></div>
  </div>
</template>

<script>
export default {
  name: "Banner",
};
</script>

⑦App.vue

<template>
  <div>
    <div class="row">
      <Banner />
    </div>
    <div class="row">
      <div class="col-xs-2 col-xs-offset-2">
        <div class="list-group">
          <!-- 原始 html 中我们使用a标签实现页面的跳转 -->
          <!-- <a class="list-group-item active" href="./travel.html">旅游胜地</a> -->
          <!-- <a class="list-group-item" href="./food.html">各地美食</a> -->

          <!-- Vue中借助router-link标签实现路由的切换 -->
          <router-link
            class="list-group-item"
            active-class="active"
            to="/travel"
            >旅游胜地</router-link
          >
          <router-link class="list-group-item" active-class="active" to="/food"
            >各地美食</router-link
          >
        </div>
      </div>
      <div class="col-xs-6">
        <div class="panel">
          <div class="panel-body">
            <!-- 指定组件的呈现位置 -->
            <router-view></router-view>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import Banner from "./components/Banner.vue";
export default {
  name: "App",
  components: { Banner },
};
</script>

⑧index.html

<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vue路由学习</title>
  <!-- 引入第三方 bootstrap 样式(CDN) -->
  <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.5/css/bootstrap.css" rel="stylesheet">
</head>

<body>
  <!-- 准备一个容器 -->
  <div id="root"></div>
</body>

</html>

运行服务:npm run serve ,测试效果(可以看到切换路由后,前面的路由组件会被销毁):

13.2.1 缓存路由组件

我们知道,通过路由切换,会默认销毁掉之前的路由组件,在需要的时候 Vue 再给我们重新加载,也可以通过配置不销毁,即缓存路由组件

1、缓存路由组件的作用:

让不展示的路由组件保持挂载状态,不被销毁。一般作用于:当用户在某个组件页面输入数据的时候,切换到其它组件页面再切回来,数据不被清除掉,或者其它应用场景。

2、代码示例,使用 include 属性,include 属性里填写的是组件名,如果不填写,默认缓存所有路由组件;多个组件以逗号隔开(本案例中修改 App.vue 内容):

<!-- 缓存路由组件,include 属性里填写的是组件名,如果不填写,默认缓存所有;多个组件以逗号隔开 -->
<keep-alive include="Travel,Food">
  <!-- 指定组件的呈现位置 -->
  <router-view></router-view>
</keep-alive>

<!-- 或者使用数组的写法 -->
<keep-alive :include="['Travel','Food']">
  <!-- 指定组件的呈现位置 -->
  <router-view></router-view>
</keep-alive>

13.3 嵌套(多级)路由

1、配置路由规则,使用 children 配置项,注意 children 里面的路径不能加 /,例如:

    routes: [
        {
            path: '/travel',
            component: Travel,
            // 通过 children 配置子路由
            children: [
                {
                    // 此处一定不要加 / ,不能写成 /shenzhen
                    path: "shenzhen",
                    component: Shenzhen
                },
                {
                    // 此处一定不要加 / ,不能写成 /beijing
                    path: "beijing",
                    component: Beijing
                }
            ]
        },
        {
            path: '/food',
            component: Food
        }
    ]

2、跳转,要写完整路径,例如:

<router-link to="/travel/shenzhen">深圳景点</router-link>

完整代码示例:

①index.js

// 该文件专门用于创建整个应用的路由器
import VueRouter from 'vue-router'
//引入组件
import Travel from '../pages/Travel'
import Food from '../pages/Food'
import Shenzhen from '../pages/Shenzhen'
import Beijing from '../pages/Beijing'

//创建并暴露一个路由器
export default new VueRouter({
    routes: [
        {
            path: '/travel',
            component: Travel,
            // 通过 children 配置子路由
            children: [
                {
                    // 此处一定不要加 / ,不能写成 /shenzhen
                    path: "shenzhen",
                    component: Shenzhen
                },
                {
                    // 此处一定不要加 / ,不能写成 /beijing
                    path: "beijing",
                    component: Beijing
                }
            ]
        },
        {
            path: '/food',
            component: Food
        }
    ]
})

②Travel.vue

<template>
  <div>
    <h2>我是 Travel 旅游胜地的内容</h2>
    <div>
      <!-- tab 样式 -->
      <ul class="nav nav-tabs">
        <li>
          <router-link
            class="list-group-item"
            active-class="active"
            to="/travel/shenzhen"
            >深圳景点</router-link
          >
        </li>
        <li>
          <router-link
            class="list-group-item"
            active-class="active"
            to="/travel/beijing"
            >北京景点</router-link
          >
        </li>
      </ul>
      <!-- 展示 router 数据内容 -->
      <router-view></router-view>
    </div>
  </div>
</template>

<script>
export default {
  name: "Travel",
};
</script>

③Shenzhen.vue

<template>
  <div>
    <ul>
      <li><a href="/shenzhenwan">深圳湾公园</a>&nbsp;&nbsp;</li>
      <li><a href="/huanle">深圳欢乐港湾</a>&nbsp;&nbsp;</li>
      <li><a href="/dameisha">大梅沙公园</a>&nbsp;&nbsp;</li>
    </ul>
  </div>
</template>

<script>
export default {
  name: "Shenzhen",
};
</script>

④Beijing.vue

<template>
  <div>
    <ul>
      <li><a href="/changcheng">长城</a>&nbsp;&nbsp;</li>
      <li><a href="/gugong">故宫</a>&nbsp;&nbsp;</li>
      <li><a href="/tiananmen">天安门</a>&nbsp;&nbsp;</li>
    </ul>
  </div>
</template>

<script>
export default {
  name: "Beijing",
};
</script>

测试效果:

13.4 路由传参及路由命名

路由传递参数主要有 2 种方式:query 和 params。

query 传递参数和接收参数:

1、传递参数有 2 中常用方式字符串方式和对象方式,如下:

<!-- 写法1:跳转路由并传递 query 参数,to 的字符串写法 -->
<router-link :to="`/travel/shenzhen/detail?id=${s.id}&name=${s.name}`">{{ s.name }}</router-link>

<!-- 写法2:跳转路由并传递 query 参数,to 的对象写法 -->
<router-link :to="{
	path: `/travel/shenzhen/detail`,
	query: {
	  id: s.id,
	  name: s.name,
	},
  }"> {{ s.name }}</router-link>

2、接收参数

{{ $route.query.id }}

params 传递参数和接收参数:

1、配置路由,声明接收 params 参数(一定要声明接收,否则无法获取到参数

// 此处一定不要加 / ,不能写成 /shenzhen
path: "shenzhen",
component: Shenzhen,
children: [
	{
		// 给路由命名
		name: "shenzhenDetail",
		//使用 params 的方式,需要提前声明接收参数,写好占位符
		path: "detail/:id/:name",
		component: Detail
	}
]

2、传递参数
①to字符串的方式

<!-- 写法1:跳转路由并传递 params 参数,to 的字符串写法 -->
<router-link :to="`/travel/beijing/detail/${s.id}/${s.name}`">{{s.name}}</router-link>

②to对象的方式,注意,使用 params 的写法不能使用 path 的方式,只能使用 name 方式

<!-- 写法2:跳转路由并传递 params 参数,to 的对象写法 -->
<router-link :to="{
	name: `beijingDetail`,
	//使用 params 写法不能使用 path 的方式
	//path: `/travel/beijing/detail`,
	params: {
	  id: s.id,
	  name: s.name,
	},
  }">{{ s.name }}</router-link>

3、接收参数
 

{{ $route.params.id }}

路由命名:

1、作用:可以简化路由的跳转
2、使用步骤:
①给路由命名,增加 name 属性:

path: "shenzhen",
component: Shenzhen,
children: [
	{
		// 给路由命名
		name: "shenzhenDetail",
		path: "detail",
		component: Detail
	}
]

②简化跳转,可以使用 name 属性,不需要使用 path 属性,因为使用 path 属性需要全路径的编写方式:
 

<router-link
  :to="{
	name: `shenzhenDetail`,
	//path: `/travel/shenzhen/detail`,
	query: {
	  id: s.id,
	  name: s.name,
	},
  }"
>{{ s.name }}</router-link>

13.4.1 query 传递参数和接收参数案例

参考上面的案例代码,只需要修改一下几个文件:

①index.js(增加 name 属性,即 beijingDetail、shenzhenDetail)

// 该文件专门用于创建整个应用的路由器
import VueRouter from 'vue-router'
//引入组件
import Travel from '../pages/Travel'
import Food from '../pages/Food'
import Shenzhen from '../pages/Shenzhen'
import Beijing from '../pages/Beijing'
import Detail from '../pages/Detail'

//创建并暴露一个路由器
export default new VueRouter({
    routes: [
        {
            path: '/travel',
            component: Travel,
            // 通过 children 配置子路由
            children: [
                {
                    // 此处一定不要加 / ,不能写成 /shenzhen
                    path: "shenzhen",
                    component: Shenzhen,
                    children: [
                        {
                            // 给路由命名
                            name: "shenzhenDetail",
                            path: "detail",
                            component: Detail
                        }
                    ]
                },
                {
                    // 此处一定不要加 / ,不能写成 /beijing
                    path: "beijing",
                    component: Beijing,
                    children: [
                        {
                            // 给路由命名
                            name: "beijingDetail",
                            path: "detail",
                            component: Detail
                        }
                    ]
                }
            ]
        },
        {
            path: '/food',
            component: Food
        }
    ]
})

②Shenzhen.vue

<template>
  <div>
    <ul>
      <li v-for="s in scenicList" :key="s.id">
        <!-- 写法1:跳转路由并传递 query 参数,to 的字符串写法 -->
        <!-- <router-link
          :to="`/travel/shenzhen/detail?id=${s.id}&name=${s.name}`"
          >{{ s.name }}</router-link
        > -->

        <!-- 写法2:跳转路由并传递 query 参数,to 的对象写法 -->
        <router-link
          :to="{
            name: `shenzhenDetail`,
            //path: `/travel/shenzhen/detail`,
            query: {
              id: s.id,
              name: s.name,
            },
          }"
        >
          {{ s.name }}
        </router-link>
      </li>
    </ul>
    <hr />
    <!-- 临时展示数据结果 -->
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: "Shenzhen",
  data() {
    return {
      //景点数组集合
      scenicList: [
        { id: "shenzhenwan", name: "深圳湾公园" },
        { id: "huanle", name: "深圳欢乐港湾" },
        { id: "dameisha", name: "大梅沙公园" },
      ],
    };
  },
};
</script>

③Beijing.vue

<template>
  <div>
    <ul>
      <li v-for="s in scenicList" :key="s.id">
        <!-- 写法1:跳转路由并传递 query 参数,to 的字符串写法 -->
        <router-link :to="`/travel/beijing/detail?id=${s.id}&name=${s.name}`">{{
          s.name
        }}</router-link>

        <!-- 写法2:跳转路由并传递 query 参数,to 的对象写法 -->
        <!-- <router-link
          :to="{
            name: `beijingDetail`,
            //path: `/travel/beijing/detail`,
            query: {
              id: s.id,
              name: s.name,
            },
          }"
        >
          {{ s.name }}
        </router-link> -->
      </li>
    </ul>
    <hr />
    <!-- 临时展示数据结果 -->
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: "Beijing",
  data() {
    return {
      //景点数组集合
      scenicList: [
        { id: "changcheng", name: "长城" },
        { id: "gugong", name: "故宫" },
        { id: "tiananmen", name: "天安门" },
      ],
    };
  },
};
</script>

④增加 Detail.vue 文件

<template>
  <ul>
    <li>景点编号:{{ $route.query.id }}</li>
    <li>景点名称:{{ $route.query.name }}</li>
  </ul>
</template>

<script>
export default {
  name: "Detail",
};
</script>

测试结果,点击不同的景点名称,数据展示区就展示对应的数据:

13.4.2 params 传递参数和接收参数案例

在上面案例的基础上,修改一下文件:

①index.js

// 该文件专门用于创建整个应用的路由器
import VueRouter from 'vue-router'
//引入组件
import Travel from '../pages/Travel'
import Food from '../pages/Food'
import Shenzhen from '../pages/Shenzhen'
import Beijing from '../pages/Beijing'
import Detail from '../pages/Detail'

//创建并暴露一个路由器
export default new VueRouter({
    routes: [
        {
            path: '/travel',
            component: Travel,
            // 通过 children 配置子路由
            children: [
                {
                    // 此处一定不要加 / ,不能写成 /shenzhen
                    path: "shenzhen",
                    component: Shenzhen,
                    children: [
                        {
                            // 给路由命名
                            name: "shenzhenDetail",
                            //使用 params 的方式,需要提前声明接收参数,写好占位符
                            path: "detail/:id/:name",
                            component: Detail
                        }
                    ]
                },
                {
                    // 此处一定不要加 / ,不能写成 /beijing
                    path: "beijing",
                    component: Beijing,
                    children: [
                        {
                            // 给路由命名
                            name: "beijingDetail",
                            //使用 params 的方式,需要提前声明接收参数,写好占位符
                            path: "detail/:id/:name",
                            component: Detail
                        }
                    ]
                }
            ]
        },
        {
            path: '/food',
            component: Food
        }
    ]
})

②Beijing.vue

<template>
  <div>
    <ul>
      <li v-for="s in scenicList" :key="s.id">
        <!-- 写法1:跳转路由并传递 params 参数,to 的字符串写法 -->
        <router-link :to="`/travel/beijing/detail/${s.id}/${s.name}`">{{
          s.name
        }}</router-link>

        <!-- 写法2:跳转路由并传递 params 参数,to 的对象写法 -->
        <!-- <router-link
          :to="{
            name: `beijingDetail`,
            //使用 params 写法不能使用 path 的方式
            //path: `/travel/beijing/detail`,
            params: {
              id: s.id,
              name: s.name,
            },
          }"
        >
          {{ s.name }}
        </router-link> -->
      </li>
    </ul>
    <hr />
    <!-- 临时展示数据结果 -->
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: "Beijing",
  data() {
    return {
      //景点数组集合
      scenicList: [
        { id: "changcheng", name: "长城" },
        { id: "gugong", name: "故宫" },
        { id: "tiananmen", name: "天安门" },
      ],
    };
  },
};
</script>

③Shenzhen.vue

<template>
  <div>
    <ul>
      <li v-for="s in scenicList" :key="s.id">
        <!-- 写法1:跳转路由并传递 params 参数,to 的字符串写法 -->
        <!-- <router-link :to="`/travel/shenzhen/detail/${s.id}/${s.name}`">{{
          s.name
        }}</router-link> -->

        <!-- 写法2:跳转路由并传递 params 参数,to 的对象写法 -->
        <router-link
          :to="{
            name: `shenzhenDetail`,
            //使用 params 写法不能使用 path 的方式
            //path: `/travel/shenzhen/detail`,
            params: {
              id: s.id,
              name: s.name,
            },
          }"
        >
          {{ s.name }}
        </router-link>
      </li>
    </ul>
    <hr />
    <!-- 临时展示数据结果 -->
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: "Shenzhen",
  data() {
    return {
      //景点数组集合
      scenicList: [
        { id: "shenzhenwan", name: "深圳湾公园" },
        { id: "huanle", name: "深圳欢乐港湾" },
        { id: "dameisha", name: "大梅沙公园" },
      ],
    };
  },
};
</script>

④Detail.vue

<template>
  <ul>
    <li>景点编号:{{ $route.params.id }}</li>
    <li>景点名称:{{ $route.params.name }}</li>
  </ul>
</template>

<script>
export default {
  name: "Detail",
};
</script>

测试效果:

13.4.3 props 接收参数

作用:让路由组件更方便的接收到参数

1、传递参数写法,一般有 3 种写法。其中函数式的写法又有 3 种写法。

{
	// 给路由命名
	name: "shenzhenDetail",
	//使用 params 的方式,需要提前声明接收参数,写好占位符
	path: "detail/:id/:name",
	component: Detail,
	//props 的第一种写法(数据固定,极少使用),值为对象,该对象中的所有key-value都会以props的形式传给Detail组件。
	//props: { id: "siheyuan", name: "四合院" }

	//props 的第二种写法,值为布尔值,若布尔值为真,就会把该路由组件收到的所有 params 参数,以props的形式传给Detail组件。
	//props: true

	//props的第三种写法,值为函数
	//函数写法的第一种写法:使用 $route 接收参数(如果使用 params 接收参数,就用 $route.params。如果使用 query 接收参数,就用 $route.query)
	// props($route) {
	//     return {
	//         id: $route.params.id,
	//         name: $route.params.name,
	//     }
	// }

	//函数写法的第二种写法:拿到参数的时候,就结构赋值
	// props({ params }) {
	//     return {
	//         id: params.id,
	//         name: params.name,
	//     }
	// }

	//函数写法的第三种写法:使用结构赋值的连续写法
	props({ params: { id, name } }) {
		return { id, name }
	}
}

2、接收参数

<script>
export default {
  name: "Detail",
  //接收 props 参数值
  props: ["id", "name"],
};
</script>

完整代码案例:

①index.js

// 该文件专门用于创建整个应用的路由器
import VueRouter from 'vue-router'
//引入组件
import Travel from '../pages/Travel'
import Food from '../pages/Food'
import Shenzhen from '../pages/Shenzhen'
import Beijing from '../pages/Beijing'
import Detail from '../pages/Detail'

//创建并暴露一个路由器
export default new VueRouter({
    routes: [
        {
            path: '/travel',
            component: Travel,
            // 通过 children 配置子路由
            children: [
                {
                    // 此处一定不要加 / ,不能写成 /shenzhen
                    path: "shenzhen",
                    component: Shenzhen,
                    children: [
                        {
                            // 给路由命名
                            name: "shenzhenDetail",
                            //使用 params 的方式,需要提前声明接收参数,写好占位符
                            path: "detail/:id/:name",
                            component: Detail,
                            //props 的第一种写法(数据固定,极少使用),值为对象,该对象中的所有key-value都会以props的形式传给Detail组件。
                            //props: { id: "siheyuan", name: "四合院" }

                            //props 的第二种写法,值为布尔值,若布尔值为真,就会把该路由组件收到的所有 params 参数,以props的形式传给Detail组件。
                            //props: true

                            //props的第三种写法,值为函数
                            //函数写法的第一种写法:使用 $route 接收参数(如果使用 params 接收参数,就用 $route.params。如果使用 query 接收参数,就用 $route.query)
                            // props($route) {
                            //     return {
                            //         id: $route.params.id,
                            //         name: $route.params.name,
                            //     }
                            // }

                            //函数写法的第二种写法:拿到参数的时候,就结构赋值
                            // props({ params }) {
                            //     return {
                            //         id: params.id,
                            //         name: params.name,
                            //     }
                            // }

                            //函数写法的第三种写法:使用结构赋值的连续写法
                            props({ params: { id, name } }) {
                                return { id, name }
                            }
                        }
                    ]
                },
                {
                    // 此处一定不要加 / ,不能写成 /beijing
                    path: "beijing",
                    component: Beijing,
                    children: [
                        {
                            // 给路由命名
                            name: "beijingDetail",
                            //使用 params 的方式,需要提前声明接收参数,写好占位符
                            path: "detail/:id/:name",
                            component: Detail,
                            //props 的第一种写法(数据固定,极少使用),值为对象,该对象中的所有key-value都会以props的形式传给Detail组件。
                            //props: { id: "siheyuan", name: "四合院" }

                            //props 的第二种写法,值为布尔值,若布尔值为真,就会把该路由组件收到的所有 params 参数,以props的形式传给Detail组件。
                            //props: true

                            //props的第三种写法,值为函数
                            //函数写法的第一种写法:使用 $route 接收参数(如果使用 params 接收参数,就用 $route.params。如果使用 query 接收参数,就用 $route.query)
                            // props($route) {
                            //     return {
                            //         id: $route.params.id,
                            //         name: $route.params.name,
                            //     }
                            // }

                            //函数写法的第二种写法:拿到参数的时候,就结构赋值
                            // props({ params }) {
                            //     return {
                            //         id: params.id,
                            //         name: params.name,
                            //     }
                            // }

                            //函数写法的第三种写法:使用结构赋值的连续写法
                            props({ params: { id, name } }) {
                                return { id, name }
                            }
                        }
                    ]
                }
            ]
        },
        {
            path: '/food',
            component: Food
        }
    ]
})

②Detail.vue

<template>
  <ul>
    <li>景点编号:{{ id }}</li>
    <li>景点名称:{{ name }}</li>
  </ul>
</template>

<script>
export default {
  name: "Detail",
  //接收 props 参数值
  props: ["id", "name"],
};
</script>

测试效果:

13.5 编程式路由导航

13.5.1 了解浏览器的后退、前进原理

1、浏览器有2个常用的按钮:后退、前进。这2个按钮都是依赖于浏览器的历史记录来工作的。
2、浏览器的历史记录是基于栈的数据结构(只有一端可以操作,遵循“后进后出”的模式)
3、浏览器的历史记录默认指向最后入栈的一条记录,当用户操作【后退】、【前进】时,指针会对应的指向上一个、下一个历史记录的访问路径。

13.5.2 router-link 的路由导航

1、router-link 的作用是控制路由跳转时操作浏览器历史记录的模式。

2、router-link 操作浏览器的历史记录有 2 种方式:push、replace。默认开启 push 模式,即跟浏览器的工作模式保持相同。replace 模式是每次都替换掉最新的那一条。

3、如何开启 replace 模式,方法是:

<router-link replace :to="xxx"></router-link>

4、弊端:只能处理超链接的路由跳转。不能实现按钮(button)或者其它场景的业务需求,比如延时跳转。

13.5.3 编程式路由导航

1、作用:不需要借助 <router-link> 实现路由跳转,让路由跳转更加灵活。

2、$router 的 2 个API ,主要是 push 和 replace

//this.$router.push(path) 相当于点击路由链接(可以返回到当前路由界面)
this.$router.push({
	name: `shenzhenDetail`,
	params: {
	  id: s.id,
	  name: s.name,
	},
});


//this.$router.replace(path): 用新路由替换当前路由(不可以返回到当前路由界面)
this.$router.replace({
	name: `beijingDetail`,
	params: {
	  id: s.id,
	  name: s.name,
	},
});

3、前进和后退

//后退。请求(返回)上一个记录路由
back() {
  this.$router.back();
},
//前进。请求(前进)一个记录路由
forward() {
  this.$router.forward();
},
//go函数,n>0表示前进n步,n<0表示后退n步
go(n) {
  this.$router.go(n);
},

4、注意使用的是 $router 路由器,这是全局唯一的。而不是 $route ,这是路由。因此放在页面中任何一个位置,都可以调用路由的前进、后退(不存在组件通信问题)。

完整代码示例(参考上面的代码,修改以下代码):

①Banner.vue

<template>
  <div class="col-xs-offset-2 col-xs-8">
    <div class="page-header">
      <h2>欢迎来到 Vue Router 学习基地</h2>
      <button @click="back">后退</button>
      <button @click="forward">前进</button>
      <button @click="go(2)">go前进2步</button>
      <button @click="go(-2)">go后退2步</button>
    </div>
  </div>
</template>

<script>
export default {
  name: "Banner",
  methods: {
    //后退
    back() {
      this.$router.back();
    },
    //前进
    forward() {
      this.$router.forward();
    },
    //go函数,n>0表示前进n步,n<0表示后退n步
    go(n) {
      this.$router.go(n);
    },
  },
};
</script>

②Shenzhen.vue

<template>
  <div>
    <ul>
      <li v-for="s in scenicList" :key="s.id">
        <!-- 写法1:跳转路由并传递 params 参数,to 的字符串写法 -->
        <!-- <router-link :to="`/travel/shenzhen/detail/${s.id}/${s.name}`">{{
          s.name
        }}</router-link> -->

        <!-- 写法2:跳转路由并传递 params 参数,to 的对象写法 -->
        <router-link
          replace
          :to="{
            name: `shenzhenDetail`,
            //使用 params 写法不能使用 path 的方式
            //path: `/travel/shenzhen/detail`,
            params: {
              id: s.id,
              name: s.name,
            },
          }"
        >
          {{ s.name }}
        </router-link>
        &nbsp;<button @click="pushShow(s)">push模式查看</button>
      </li>
    </ul>
    <hr />
    <!-- 临时展示数据结果 -->
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: "Shenzhen",
  data() {
    return {
      //景点数组集合
      scenicList: [
        { id: "shenzhenwan", name: "深圳湾公园" },
        { id: "huanle", name: "深圳欢乐港湾" },
        { id: "dameisha", name: "大梅沙公园" },
      ],
    };
  },
  methods: {
    pushShow(s) {
      this.$router.push({
        name: `shenzhenDetail`,
        params: {
          id: s.id,
          name: s.name,
        },
      });
    },
  },
};
</script>

③Beijing.vue

<template>
  <div>
    <ul>
      <li v-for="s in scenicList" :key="s.id">
        <!-- 写法1:跳转路由并传递 params 参数,to 的字符串写法 -->
        <router-link :to="`/travel/beijing/detail/${s.id}/${s.name}`">{{
          s.name
        }}</router-link>

        <!-- 写法2:跳转路由并传递 params 参数,to 的对象写法 -->
        <!-- <router-link
          :to="{
            name: `beijingDetail`,
            //使用 params 写法不能使用 path 的方式
            //path: `/travel/beijing/detail`,
            params: {
              id: s.id,
              name: s.name,
            },
          }"
        >
          {{ s.name }}
        </router-link> -->
        &nbsp;<button @click="replaceShow(s)">replace模式查看</button>
      </li>
    </ul>
    <hr />
    <!-- 临时展示数据结果 -->
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: "Beijing",
  data() {
    return {
      //景点数组集合
      scenicList: [
        { id: "changcheng", name: "长城" },
        { id: "gugong", name: "故宫" },
        { id: "tiananmen", name: "天安门" },
      ],
    };
  },
  methods: {
    replaceShow(s) {
      this.$router.replace({
        name: `beijingDetail`,
        params: {
          id: s.id,
          name: s.name,
        },
      });
    },
  },
};
</script>

测试效果:

13.6 路由的两个生命周期钩子

1、作用:路由组件所独有的两个钩子,用于捕获路由组件的激活状态、失活状态。
2、具体名字:
①activated:路由组件被激活时触发。
②deactivated:路由组件失活时触发。

代码示例:

activated() {
console.log("Food 组件被激活了");
},
deactivated() {
console.log("Food 组件失活了");
},

3、要使用这 2 个钩子,路由组件需要被 <keep-alive> 标签包裹,activated 和 deactivated 钩子只在 <keep-alive> 包裹的组件中起作用。。例如:

<keep-alive include="Food">
  <!-- 指定组件的呈现位置 -->
  <router-view></router-view>
</keep-alive>

完整代码示例(参考上面的代码):

①Food.vue

<template>
  <div>
    <h2>我是 Food 各地美食的内容</h2>
    <h3 :style="{ opacity }">一闪一闪亮晶晶</h3>
  </div>
</template>

<script>
export default {
  name: "Food",
  data() {
    return {
      opacity: 1,
    };
  },
  activated() {
    console.log("Food 组件被激活了");
    this.timer = setInterval(() => {
      console.log("定时器被执行");
      this.opacity -= 0.01;
      if (this.opacity <= 0) {
        this.opacity = 1;
      }
    }, 16);
  },
  deactivated() {
    console.log("Food 组件失活了");
    clearInterval(this.timer);
  },
};
</script>

使用 keep-alive 标签包裹组件(本案例写在 App.vue 中)

<template>
  <div>
    <div class="row">
      <Banner />
    </div>
    <div class="row">
      <div class="col-xs-2 col-xs-offset-2">
        <div class="list-group">
          <!-- 原始 html 中我们使用a标签实现页面的跳转 -->
          <!-- <a class="list-group-item active" href="./travel.html">旅游胜地</a> -->
          <!-- <a class="list-group-item" href="./food.html">各地美食</a> -->

          <!-- Vue中借助router-link标签实现路由的切换 -->
          <router-link
            class="list-group-item"
            active-class="active"
            to="/travel"
            >旅游胜地</router-link
          >
          <router-link class="list-group-item" active-class="active" to="/food"
            >各地美食</router-link
          >
        </div>
      </div>
      <div class="col-xs-6">
        <div class="panel">
          <div class="panel-body">
            <!-- 缓存路由组件,include 属性里填写的是组件名,如果不填写,默认缓存所有;多个组件以逗号隔开 -->
            <keep-alive include="Food">
              <!-- 指定组件的呈现位置 -->
              <router-view></router-view>
            </keep-alive>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import Banner from "./components/Banner.vue";
export default {
  name: "App",
  components: { Banner },
};
</script>

测试效果:

13.7 路由守卫

1、作用:对路由进行权限控制
2、分类:全局守卫、独享守卫、组件内守卫。

13.7.1 全局路由守卫

1、全局路由守卫分为:全局前置守卫和全局后置守卫。全局前置守卫在初始化时执行,以及每次路由切换时执行。全局后置守卫在初始化时执行,以及每次路由切换后执行。

2、代码示例

//全局前置路由守卫:初始化的时候被调用,每次路由切换之前被调用
//有3个参数:to:要切换的目标路由;from:来自哪个路由组件;next:放行
router.beforeEach((to, from, next) => {
    console.log("前置路由守卫", to, from);
    //判断是否需要鉴权(从 meta 的元信息中判断)
    if (to.meta.isAuth) {
        //实际开发中从服务端获取数据进行判断(这里我们从浏览器缓存里模拟数据)
        if (localStorage.getItem("userName") === "流放深圳") {
            next();
        } else {
            alert("Sorry,用户名不正确,你无权查看此页面!");
        }
    } else {
        next();
    }
})

//全局后置路由守卫:初始化的时候被调用,每次路由切换之后被调用
router.afterEach((to, from) => {
    console.log("后置路由守卫", to, from);
    //设置网页标题
    document.title = to.meta.title || "旅行网";
})

完整代码示例(其它代码参考上一个案例):

①index.js

1、修改暴露内容,先创建一个路由器,再加上全局路由守卫的逻辑,再对外暴露。

2、在路由规则中,增加 meta 路由元信息,可以添加自定义的数据。

// 该文件专门用于创建整个应用的路由器
import VueRouter from 'vue-router'
//引入组件
import Travel from '../pages/Travel'
import Food from '../pages/Food'
import Shenzhen from '../pages/Shenzhen'
import Beijing from '../pages/Beijing'
import Detail from '../pages/Detail'

//创建一个路由器
const router = new VueRouter({
    routes: [
        {
            path: '/travel',
            component: Travel,
            //路由元信息,可以添加自定义数据
            meta: { isAuth: false, title: "旅行" },
            // 通过 children 配置子路由
            children: [
                {
                    // 此处一定不要加 / ,不能写成 /shenzhen
                    path: "shenzhen",
                    component: Shenzhen,
                    //路由元信息,可以添加自定义数据
                    meta: { isAuth: true, title: "旅行·深圳" },
                    children: [
                        {
                            // 给路由命名
                            name: "shenzhenDetail",
                            //使用 params 的方式,需要提前声明接收参数,写好占位符
                            path: "detail/:id/:name",
                            component: Detail,
                            //函数写法的第一种写法:使用 $route 接收参数(如果使用 params 接收参数,就用 $route.params。如果使用 query 接收参数,就用 $route.query)
                            props($route) {
                                return {
                                    id: $route.params.id,
                                    name: $route.params.name,
                                }
                            }

                        }
                    ]
                },
                {
                    // 此处一定不要加 / ,不能写成 /beijing
                    path: "beijing",
                    component: Beijing,
                    meta: { isAuth: true, title: "旅行·北京" },
                    children: [
                        {
                            // 给路由命名
                            name: "beijingDetail",
                            //使用 params 的方式,需要提前声明接收参数,写好占位符
                            path: "detail/:id/:name",
                            component: Detail,
                            //props的第三种写法,值为函数
                            //函数写法的第一种写法:使用 $route 接收参数(如果使用 params 接收参数,就用 $route.params。如果使用 query 接收参数,就用 $route.query)
                            props($route) {
                                return {
                                    id: $route.params.id,
                                    name: $route.params.name,
                                }
                            }

                        }
                    ]
                }
            ]
        },
        {
            path: '/food',
            component: Food,
            //路由元信息,可以添加自定义数据
            meta: { isAuth: false, title: "美食" },
        }
    ]
})

//全局前置路由守卫:初始化的时候被调用,每次路由切换之前被调用
//有3个参数:to:要切换的目标路由;from:来自哪个路由组件;next:放行
router.beforeEach((to, from, next) => {
    console.log("前置路由守卫", to, from);
    //判断是否需要鉴权(从 meta 的元信息中判断)
    if (to.meta.isAuth) {
        //实际开发中从服务端获取数据进行判断(这里我们从浏览器缓存里模拟数据)
        if (localStorage.getItem("userName") === "流放深圳") {
            next();
        } else {
            alert("Sorry,用户名不正确,你无权查看此页面!");
        }
    } else {
        next();
    }
})

//全局后置路由守卫:初始化的时候被调用,每次路由切换之后被调用
router.afterEach((to, from) => {
    console.log("后置路由守卫", to, from);
    //设置网页标题
    document.title = to.meta.title || "旅行网";
})

//暴露一个路由器
export default router;

测试效果:

然后在浏览器本地存储中添加数据(【应用】下【本地存储空间】,增加要校验的数据):

13.7.2 独享路由守卫

1、独享路由守卫是某一个路由独有的。

2、代码示例(需要放在具体的路由配置中):

//独享路由守卫
beforeEnter: (to, from, next) => {
	console.log("独享路由守卫", to, from);
},

3、注意只有 beforeEnter,没有 afterEnter

完整代码示例(其它代码参考之前的):

①index.js

// 该文件专门用于创建整个应用的路由器
import VueRouter from 'vue-router'
//引入组件
import Travel from '../pages/Travel'
import Food from '../pages/Food'
import Shenzhen from '../pages/Shenzhen'
import Beijing from '../pages/Beijing'
import Detail from '../pages/Detail'

//创建一个路由器
const router = new VueRouter({
    routes: [
        {
            path: '/travel',
            component: Travel,
            //路由元信息,可以添加自定义数据
            meta: { isAuth: false, title: "旅行" },
            // 通过 children 配置子路由
            children: [
                {
                    // 此处一定不要加 / ,不能写成 /shenzhen
                    path: "shenzhen",
                    component: Shenzhen,
                    //路由元信息,可以添加自定义数据
                    meta: { isAuth: true, title: "旅行·深圳" },
                    //独享路由守卫
                    beforeEnter: (to, from, next) => {
                        console.log("独享路由守卫", to, from);
                        if (to.meta.isAuth) {
                            if (localStorage.getItem("userId") === "666") {
                                next();
                            } else {
                                alert("Sorry!用户ID不正确,无权限查看");
                            }
                        } else {
                            next();
                        }
                    },
                    children: [
                        {
                            // 给路由命名
                            name: "shenzhenDetail",
                            //使用 params 的方式,需要提前声明接收参数,写好占位符
                            path: "detail/:id/:name",
                            component: Detail,
                            //函数写法的第一种写法:使用 $route 接收参数(如果使用 params 接收参数,就用 $route.params。如果使用 query 接收参数,就用 $route.query)
                            props($route) {
                                return {
                                    id: $route.params.id,
                                    name: $route.params.name,
                                }
                            }

                        }
                    ]
                },
                {
                    // 此处一定不要加 / ,不能写成 /beijing
                    path: "beijing",
                    component: Beijing,
                    meta: { isAuth: true, title: "旅行·北京" },
                    children: [
                        {
                            // 给路由命名
                            name: "beijingDetail",
                            //使用 params 的方式,需要提前声明接收参数,写好占位符
                            path: "detail/:id/:name",
                            component: Detail,
                            //props的第三种写法,值为函数
                            //函数写法的第一种写法:使用 $route 接收参数(如果使用 params 接收参数,就用 $route.params。如果使用 query 接收参数,就用 $route.query)
                            props($route) {
                                return {
                                    id: $route.params.id,
                                    name: $route.params.name,
                                }
                            }

                        }
                    ]
                }
            ]
        },
        {
            path: '/food',
            component: Food,
            //路由元信息,可以添加自定义数据
            meta: { isAuth: false, title: "美食" },
        }
    ]
})

//全局前置路由守卫:初始化的时候被调用,每次路由切换之前被调用
//有3个参数:to:要切换的目标路由;from:来自哪个路由组件;next:放行
router.beforeEach((to, from, next) => {
    console.log("全局前置路由守卫", to, from);
    //判断是否需要鉴权(从 meta 的元信息中判断)
    if (to.meta.isAuth) {
        //实际开发中从服务端获取数据进行判断(这里我们从浏览器缓存里模拟数据)
        if (localStorage.getItem("userName") === "流放深圳") {
            next();
        } else {
            alert("Sorry,用户名不正确,你无权查看此页面!");
        }
    } else {
        next();
    }
})

//全局后置路由守卫:初始化的时候被调用,每次路由切换之后被调用
router.afterEach((to, from) => {
    console.log("全局后置路由守卫", to, from);
    //设置网页标题
    document.title = to.meta.title || "旅行网";
})

//暴露一个路由器
export default router;

测试效果:

浏览器存储增加 userId = 666,校验通过:

13.7.3 组件内路由守卫

1、作用:如果组件内想写一些单独的业务逻辑,推荐使用组件内路由守卫。
2、通过路由规则(也就是从路由组件跳到路由组件)进入该组件时被调用。示例:

//通过路由规则,进入该组件时被调用
beforeRouteEnter(to, from, next) {
	console.log("组件内路由守卫:Food--beforeRouteEnter", to, from);
	if (to.meta.isAuth) {
	  //判断是否需要鉴权
	  if (localStorage.getItem("userName") === "流放深圳") {
		next();
	  } else {
		alert("Sorry,用户名不正确,你无权查看此页面!");
	  }
	} else {
	  next();
	}
}

3、通过路由规则离开该组件时被调用。示例:

//通过路由规则,离开该组件时被调用
beforeRouteLeave(to, from, next) {
	console.log("组件内路由守卫:Food--beforeRouteLeave", to, from);
	next();
}

完整代码示例:

①Food.vue

<template>
  <div>
    <h2>我是 Food 各地美食的内容</h2>
    <h3 :style="{ opacity }">一闪一闪亮晶晶</h3>
  </div>
</template>

<script>
export default {
  name: "Food",
  data() {
    return {
      opacity: 1,
    };
  },
  activated() {
    console.log("Food 组件被激活了");
    this.timer = setInterval(() => {
      console.log("定时器被执行");
      this.opacity -= 0.01;
      if (this.opacity <= 0) {
        this.opacity = 1;
      }
    }, 16);
  },
  deactivated() {
    console.log("Food 组件失活了");
    clearInterval(this.timer);
  },
  //通过路由规则,进入该组件时被调用
  beforeRouteEnter(to, from, next) {
    console.log("组件内路由守卫:Food--beforeRouteEnter", to, from);
    if (to.meta.isAuth) {
      //判断是否需要鉴权
      if (localStorage.getItem("userName") === "流放深圳") {
        next();
      } else {
        alert("Sorry,用户名不正确,你无权查看此页面!");
      }
    } else {
      next();
    }
  },
  //通过路由规则,离开该组件时被调用
  beforeRouteLeave(to, from, next) {
    console.log("组件内路由守卫:Food--beforeRouteLeave", to, from);
    next();
  },
};
</script>

测试效果:

13.8 路由 hash 模式与 history 模式

路由器的 2 种工作模式 hash 模式(默认)和 history 模式:
1、对于一个 URL 来说,什么是 hash 值?
回答:# 号以及后面的内容就是 hash 值。

2、hash 值不会包含在 HTTP 请求中,不会传递到后端服务器。
3、hash模式:
①地址中永远带着 # 号,不美观。
②若以后将地址通过第三方手机 APP 分享,且 APP 校验严格,则地址会被标记为不合法。
③兼容性较好。

4、history 模式:
①地址干净、美观
②兼容性和 hash 模式相比略差
③应用部署上线时需要后端人员支持,解决刷新页面服务器 404 的问题。

代码示例(router/index.js ):

//创建一个路由器
const router = new VueRouter({
    //路由的工作模式,默认 hash(可以不写),可选:hash、history
    mode:"hash",
    routes: [
        {
		*** ***
        }
    ]
})

//暴露一个路由器
export default router;

13.8.1 hash 模式

我们把路由的工作模式配置成 hash 模式,或者不配置默认也是 hash 模式,然后打包项目,并将项目部署到服务器中,我们搭配 Express 框架来部署。部署流程可以查看我的博客:

系列学习前端之第 11 章:将前端项目部署到本机运行起来 2 种方式(使用 Express 和 Nginx),使用花生壳做内网穿透,让外网可以访问网站

我们使用以下命令来构建项目:

npm run build

构建成功后,会在项目下生成一个 dist 文件夹,里面存放项目所需的 js、css、html 和网站图标 favicon.ico。

将 dist 文件夹搭配 Express 框架部署,测试效果: 

hash 模式下,我们无论怎么刷新都没有问题。因为前端不会把 # 后面的内容当做 URL 路径传递给后端。

13.8.2 history 模式

然后将路由的工作模式改成 history(修改 router/index.js 文件)

mode: "history",

重新打包,搭配 Express 框架部署。

 这时候,我们多点击几下按钮,比如点击到【故宫】,然后刷新浏览器当前页面,出现异常:

这就是 history 模式的特点:

兼容性和 hash 模式相比略差
应用部署上线时需要后端人员支持,解决刷新页面服务器 404 的问题。

因此,我们需要解决 history 模式的兼容性问题,解决刷新页面服务器 404 的问题。

13.8.2.1 Express 解决 hisotry 模式兼容性问题

 详情可以查看博客: 

系列学习前端之第 11 章:将前端项目部署到本机运行起来 2 种方式(使用 Express 和 Nginx),使用花生壳做内网穿透,让外网可以访问网站

14、引入第三方 UI 组件

PC 端常用 UI 组件库

Element UI:https://element.eleme.cn

IView UI:https://www.iviewui.com

移动端常用 UI 组件库

Vant:https://vant-ui.github.io/vant/#/zh-CN

Cube UI:https://didi.github.io/cube-ui/#/zh-CN/

第三方库比较大,我们在实际开发中只需要按需引入即可。

以饿了么开源的组件库为例,快速开始文档:

Element - 开发指南

14.1 npm 安装 Element 组件

npm i element-ui

14.2 按需引入组件

14.2.1 借助 babel-plugin-component 插件

如果要按需引入 Element 组件,就得借助 babel-plugin-component 插件。

首先,安装 babel-plugin-component:

npm install babel-plugin-component -D

然后,将项目的 babel.config.js 文件修改成以下代码配置:

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

特别要注意:在使用 babel-plugin-component 时,babel.config.js 配置文件 presets 里头要写成 @babel/preset-env  不能按照官网的提示配置 es2015, 否则启动报错 Error: Cannot find module 'babel-preset-es2015'
        Require stack:
        - E:\vue\study\vue2\node_modules\@babel\core\lib\config\files\plugins.js    
        - E:\vue\study\vue2\node_modules\@babel\core\lib\config\files\index.js      
        - E:\vue\study\vue2\node_modules\@babel\core\lib\index.js
        - E:\vue\study\vue2\node_modules\@vue\cli-plugin-babel\index.js
        - E:\vue\study\vue2\node_modules\@vue\cli-service\lib\Service.js
        - E:\vue\study\vue2\node_modules\@vue\cli-service\bin\vue-cli-service.js 

14.3 按需引入组件

本案例引入 Button、Row、DataPicker 这 3 个组件。至于样式,组件会自动分析来引入。

main.js 代码如下:

// 引入 Vue 文件
import Vue from 'vue'
//引入 App 组件,它是所有组件的父组件
import App from './App.vue'

//完整引入
//引入 ElementUI 组件库
//import ElementUI from "element-ui";
//引入ElementUI全部样式
//import "element-ui/lib/theme-chalk/index.css";

//按需引入 ElementUI 组件库
import { Button, Row, DatePicker } from "element-ui";
Vue.component(Button.name, Button);
Vue.component(Row.name, Row);
Vue.component(DatePicker.name, DatePicker);

/**
 * 或者使用下面的写法
Vue.use(Button);
Vue.use(Row);
Vue.use(DatePicker);
 */

// 关闭 Vue 的生产提示
Vue.config.productionTip = false

// 创建 Vue 实例对象
new Vue({
  // 将 app 组件放入 vm 容器中
  render: h => h(App),
}).$mount('#root') 

App.vue

<template>
  <div>
    <button>原生的按钮</button>
    <el-row>
      <el-button>默认按钮</el-button>
      <el-button type="primary">主要按钮</el-button>
      <el-button type="success">成功按钮</el-button>
      <el-button type="info">信息按钮</el-button>
      <el-button type="warning">警告按钮</el-button>
      <el-button type="danger">危险按钮</el-button>
    </el-row>
    <hr />
    <el-date-picker type="date" placeholder="选择日期"> </el-date-picker>
    <hr />
    <el-row>
      <el-button icon="el-icon-search" circle></el-button>
      <el-button type="primary" icon="el-icon-s-check" circle></el-button>
      <el-button type="success" icon="el-icon-check" circle></el-button>
      <el-button type="info" icon="el-icon-message" circle></el-button>
      <el-button type="warning" icon="el-icon-star-off" circle></el-button>
      <el-button type="danger" icon="el-icon-delete" circle></el-button>
    </el-row>
  </div>
</template>

<script>
export default {
  name: "App",
};
</script>

启动服务,测试:

 本篇博客对应代码:https://gitee.com/biandanLoveyou/vue2

—  end —

  • 17
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值