Bootstrap

vue脚手架(vue-cli),vue2,vue3,ref引用,路由和路由插件(vue-router),路由导航守卫,插槽

一. 单页面应用程序

SPA是单页面应用程序

如何快速创建vue的SPA项目

vue官方提供了两种快速创建工程化的SPA项目的方式

1. 基于vite创建SPA项目

2. 基于vue-cli创建SPA项目

vite

vue-cli

支持的vue版本

仅支持vue3.x

支持3.x和2.x

是否支持webpack

运行速度

较慢

功能完整度

小而巧

大而全

是否建议在企业级开发中使用

目前不建议

建议在企业级开发中使用

1. 基于vite创建SPA项目

1. npm init vite-app 项目名称

2. cd 项目名称

3. npm install

4. npm run dev

2. 梳理项目结构

使用vite创建的项目结构中:

1. node_modules目录用来存放第三方依赖包

2. public是公共的静态资源目录

3. src是项目的源代码目录(程序员写的所有代码都要放在此目录下)

4. .gitignore是Git的忽略文件

5. index.html是SPA单页面应用程序中唯一的HTML页面

6. package.json是项目的包管理配置文件

src这个项目源代码目录之下,包含了如下的文件和文件夹

assets目录用来存放项目中所有的静态资源文件(CSS,fonts等)

components目录用来存放项目中所有的自定义组件

App.vue是项目的根组件

index.css是项目的全局样式表文件

main.js是整个项目的打包入口文件

3. vite项目的运行流程

vue要做的事情:通过main.js把APP.vue渲染到index.html的指定区域

其中:

1. App.vue用来编写待渲染的模板结构

2. index.html中要预留一个el区域

3. main.js把App.vue渲染到了index.html所预留的区域

按照vue3.x的标准用法,把App.vue中的模板渲染到index.html页面的el区域

二. vue-cli

vue脚手架用于自动生成vue和webpack的项目模板;

vue脚手架是一个快速构建vue项目的工具,可以自动安装vue所需要的插件,避免手动安装各种插件,以及寻找各种cdn并一个个引入的麻烦。

vue脚手架指的是vue-cli,vue-cli是由Vue提供的一个官方cli,专门为单页面应用快速搭建繁杂的脚手架。是vue官方提供的,快速生成vue工程化项目的工具

它是用于自动生成vue.js+webpack的项目模板,是为现代前端工作流提供了 batteries-included 的构建设置。只需要几分钟的时间就可以运行起来并带有热重载,保存时 lint 校验,以及生产环境可用的构建版本。

vue-cli是Vue.js开发的标准工具。它简化了程序员基于webpack创建工程化的Vue项目的过程。

vue-cli官网上说:程序员可以专注在撰写应用上,而不必花好几天去纠结webpack配置的问题。

1. 特点

1. 开箱即用;2. 基于webpack;3. 功能丰富易于扩展;4. 支持创建vue2和vue3的项目

vue-cli是基于Node.js开发出来的工具,因此需要使用npm将它安装为全局可用的工具。

vue --version查看vue-cli的版本,检验是否安装成功。

2. vue-cli创建项目的方式

#基于命令行的方式创建vue项目

vue create 项目名称

#基于可视化面板创建vue项目

vue ui

vue ui的本质:通过可视化的面板采集到用户的配置信息后,在后台基于命令行的方式自动初始化项目。

3. vue/cli的使用

1. 检查自己电脑里有没有vue

1. 1 检查自己是否装了npm

1.2  检查自己是否装了vue

1.3  如果没有装的话,执行如下命令全局安装

npm install -g @vue/cli

2. 创建项目 

切换到指定目录,用命令创建项目

使用cmd命令先切换到F://vueProject(你想要创建项目的文件夹)下,再使用创建项目命令。

使用vue create 后面的项目名,名字不要大写。(否则会有警告提示)

F:
cd vueProject
vue create vuetest

创建完项目→选择自定义(空格键选中、取消选中 )→选择babel、router、vuex(根据需求) →使用history模式的路由 →选中In dedicated config files(babel等配置写在单独的文件中)等等,根据自己的需要选就行。

3. 启动项目

不一定是dev,也有可能是serve,具体可以看package.json配置文件中写的是什么。

vue run dev

4. 打包项目

npm run build

4. 项目运行流程

//main.js核心代码
//1. 导入Vue
import Vue from 'vue'

//2. 导入App.vue
import App from './App.vue'

// 提示:当前处于什么环境(生产环境/开发环境)
Vue.config.productionTip = false

//3. 实例化Vue,将App.vue渲染到index.html容器中
new Vue ({
    render: h => h(App),
}).$mount('#app')


// el:'#app'的作用,和$mount('选择器')作用一致,用于指定Vue所管理容器
new Vue ({
    el:'#app',
})

new Vue ({
    render: (createElement) => {
        //基于App创建元素
        renturn createElement(App)
    }
}).$mount('#app')

//箭头函数只有一个形参,可以省略小括号;返回值只有一个,可以省略大括号。所以上述代码可以改写成
new Vue ({
    render: createElement => createElement(App)
}).$mount('#app')

//createElement只是一个形参,你把它写成什么都行,写成h就变成了
new Vue({
    render: h => h(App)
}).$mount('#app')

5. propxy跨域代理

接口的跨域问题:

vue项目运行的地址:http://localhost:8080/

API接口运行的地址:https://www.escook.cn/api/users

如果后端没有开启cors资源跨域共享的话,前端可以通过代理的方式来解决跨域的问题。

通过vue-cli创建的项目,在遇到接口跨域问题时,可以通过代理的方式来解决。

1. 把axios的请求根路径设置为vue项目的运行地址(接口请求不再跨域)

2. vue项目发现请求的接口不存在,把请求转交给proxy代理

3. 代理把请求根路径替换为devServer.proxy属性的值,发起真正的数据请求

4. 代理把请求到的数据,转发给axios

注意:devServer.proxy提供的代理功能,仅在开发调试阶段生效,

项目上线发布时,依旧需要API接口服务器开启CROS跨域资源共享。

6. ESlint代码规范

代码规范:一套写代码的约定规则。例如:赋值符号的左右是否需要空格...

JavaScript Standard Style规范说明https://standardjs.com/rules-zhcn.html

// 下面是这份规则中的一小部分

字符串使用单引号'abc'
无分号 const name = 'zs'
关键字后加空格 if (name = 'ls') {...}
函数名后加空格 function name (arg) {...}
坚持使用全等 === 摒弃 ==
...

 解决代码规范错误的两种方案:手动修正+自动修正

1. 手动修正

根据错误提示来一项一项手动修改纠正,如果不认识就去ESLint规则表中查找具体含义。

2. 自动修正 

基于vscode插件ESLint高亮错误,并通过配置自动帮助我们修复错误

//当保存的时候,eslint自动帮我们修复错误
"editor.codeActionsOnSave": {
    "source.fixAll": true
},
//保存代码时不自动格式化
"editor.formatOnSave": false

三. Vue

1. 概念

Vue是一套用于构建用户界面的框架。(帮程序员往页面里面填充数据的)

传统的web前端开发中,是基于jQuery+模板引擎的方式来构建用户界面的。

框架是一套现成的解决方案,程序员只能遵守框架的规范,去编写自己的业务功能。

学习Vue,就是学习Vue框架中规定的语法。

 

MVVM是vue实现数据驱动视图和双向数据绑定的核心原理。

Model表示当前页面渲染时所依赖的数据源

View表示当前页面所渲染的DOM结构

ViewModel表示vue的实例,它是MVVM的核心。它把当前页面的数据源(Model)和页面结构(View)连接在了一起。

2. vue要做什么

在工程化的项目中,vue要做的事,就是通过main.js把App.vue渲染到index.html的指定区域

其中,

APP.vue用来编写待渲染的模板结构

index.html中需要预留一个el区域

main.js把App.vue渲染到了index.html所预留的区域中

3. vue基本使用步骤

1. 导入vue.js的script脚本文件(开发版本/生产版本)

2. 在页面中声明一个将要被vue所控制的DOM区域

3. 创建vm实例对象(vue实例对象)

4. 指定配置项el,data=>渲染数据

<body>
<!-- 2. 在页面中声明一个将要被vue所控制的DOM区域-->
<div id=”app”>{{ username }}</div>

<!-- 1. 导入vue.js的script脚本文件,注意一旦引入了VueJS核心包,在全局环境,就有了Vue构造函数-->
<script src=”./lib/vue-2.6.12.js”></script>

<script>
//3. 创建vm实例对象(vue实例对象)
const vm = new Vue({
    //4.1 指定当前vm实例要控制页面的哪个区域
    el: ‘#app’,
    //4.2 指定Model数据源
    data: {
        username: ‘zs’
    }
})
</script>
</body>

vue官方提供的vue-devtools调试工具,能够方便开发者对vue项目进行调试与开发。

极简插件网址:https://chrome.zzzmh.cn/index

注意:vue2和vue3的浏览器调试工具不能交叉使用!

4. 最常用的vue组件库

PC端:Element UI
View UI
移动端:Mint UI
Vant

Element UI

是饿了么前端团队开源的一套PC端vue组件库。支持vue2和vue3的项目中使用。

vue2的项目使用旧版的Element UI(https://element.eleme.cn/#/zh-CN)

vue3的使用旧版的Element Plus(https://element-plus.gitee.io/#/zh-CN)

在vue2的项目中安装element-ui

1. npm i element-ui -S

2. 引入element-ui

// 完整引入
import ElementUI from ‘element-ui’
//导入样式表
import ‘element-ui/lib/theme-chalk/index.css’
Vue.use(ElementUI)

// 按需引入
// 借助babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的。
npm install babel-plugin-component -D
// 修改根目录下的babel.config.js配置文件,新增plugin节点:
plugins: [
    [
        ‘component’,
        {
            libraryName:’element-ui’,
            styleLibraryName:’theme-chalk’,
        },
    ],
],
// 如果只希望引入部分组件,比如button,那么需要在main.js中写入以下内容:
import {Button} from ‘element-ui’
Vue.component(Button.name,Button)

把组件的导入和注册封装为独立的模块

在src目录下,新建element-ui/index.js模块,并声明如下的代码:

import Vue from ‘vue’
import {Button,Input} from ‘element-ui’

// 注册需要的组件
Vue.use(Button)
Vue.use(Input)

// 在main.js中导入
import ‘.//element-ui’

5. 配置axios

1. 在vue3的项目中全局配置axios

import axios from ‘axios’

axios.defaults.baseURL=’https://www.escook.cn’

app.config.globalProperties.$http=axios

2. 在vue2的项目中全局配置axios

// 需要在main.js入口文件中,通过Vue构造函数的prototype原型对象全局配置axios

import axios from ‘axios’

axios.defaults.baseURL=’https://www.escook.cn’

Vue.prototype.$http=axios

6. axios拦截器

拦截器会在每次发起ajax请求和得到响应的时候自动被触发。

应用场景:

1. Token身份认证

2. Loading效果

1. 配置请求拦截器

通过axios.interceptors.request.use(成功的回调,失败的回调)可以配置请求拦截器。

1. 请求拦截器——Token认证

import axios from ‘axios’

axios.defaults.baseURL=’https://escook.cn’

//配置请求的拦截器
axios.interceptors.request.use(config = >{
    //为当前请求配置Token认证字段
    config.headers.Authorization=’Bearer xxx’
    return config
})

Vue.prototype.$http=axios

2. 请求拦截——Loading效果

//按需导入Loading效果组件
import { Loading } from ‘element-ui’

//声明变量,用来存储Loading组件的实例对象
let loadingInstance = null

//配置请求的拦截器
axios.interceptors.request.use(config = >{
    //调用Loading组件的service()方法,创建Loading组件的实例,并全屏展示loading效果
    loadingInstance=Loading.service({ fullscreen: true })//是否全屏
    return config
})

3. 配置响应拦截器

通过axios.interceptors.response.use(成功的回调,失败的回调)可以配置响应拦截器。

// 响应拦截器——关闭Loading效果

// 调用Loading实例提供的close()方法即可关闭Loading效果

axios.interceptors.response.use(response = >{
    loadingInstance.close()
    return response   
})

四. Vue指令

Vue会根据不同的指令,针对标签实现不同的功能。

指令:带有v-前缀的特殊标签属性。

1. 内容渲染指令

用来辅助开发者渲染DOM元素的文本内容

v-text,   //会覆盖默认文本内容   只能渲染文本内容

{{}},插值表达式,   //只能渲染纯文本内容

v-html   //可以把包含HTML标签的字符串渲染为页面的HTML元素

2. 属性绑定指令(v-bind)

1. 作用

动态设置html的标签属性→src,url,title...

2. 语法

v-bind: 属性名=“表达式”,v-bind:简写为英文的:

<!--使用v-bind指令,为input的placeholder动态绑定属性值-->
<input type=”text” v-bind:placeholder=”inputValue”/>
<!--使用v-bind指令,为img的src动态绑定属性值-->
<img v-bind:src=”imgSrc” alt=””/>

data:{
    inputValue:’请输入内容’,
    imgSrc:’http://cn...’
}

3. v-bind对样式控制的增强

1. 操作class

语法::class="对象/数组"

1. 对象→键就是类名,值是布尔值。如果值为true,有这个类,否则没有这个类

适用场景:一个类名,来回切换

<div class="box" :class="{类名1:布尔值,类名2:布尔值}"></div>


​<h3 class=”thin” :class=”classObj”>MyDeep组件</h3>
<button @click=”classObj.isItalic=!classObj.isItalic”>Toggle Italic</button>
<button @click=”classObj.isDelete=!classObj.isDelete”>Toggle Delete</button>

data() {
    return {
        classObj:{
            isItalic: true,
            isDelete: false,
        }
    }
}

​

2. 数组→数组中所有的类,都会添加到盒子上,本质就是一个class列表

适用场景:批量添加或删除

<div class="box" :class="[类名1,类名2,类名3]"></div>


<h3 class=”thin” :class=”[isItalic ? ‘italic’ : ‘’, isDelete ? ‘delete’ :’’ ]”>MyDeep组件</h3>
<button @click=”isItalic=!isItalic”>Toggle Italic</button>
<button @click=”isDelete=!isDelete”>Toggle Delete</button>

data() {
    return {
        isItalic: true,
        isDelete: false,
    }
}

3. 用三元表达式动态为元素绑定class的类名

<h3 class=”thin” :class=” isItalic ? ‘italic’ : ’’ “>MyDeep组件</h3>
<button @click=”isItalic=!isItalic”>Toggle Italic</button>

data() {
    return {isItalic:true}
}

.thin {    //字体变细
    font-weight:200;
}
.italic {    //倾斜字体
    font-style: italic;
}

2. 操作style

语法::style="样式对象"

适用场景:某个具体属性的动态设置

:style的对象语法十分直观——看着非常像CSS,但其实是一个JavaScript对象。CSS property名可以用驼峰式或短横线分隔来命名。

<div class="box" :style="{css属性名1:css属性值,css属性名2:css属性值}"></div>

<div :style=”{color: active, fontSize: fsize + ‘px’, background-color’ : bgcolor}”>
黑马程序员
</div>
<button @click=”fsize +=1”>字号+1</button>
<button @click=”fsize -=1”>字号-1</button>

data() {
    return {
        active: ‘red’,
        fsize: 30,
        bgcolor: ‘pink’,
    }
}

3. 事件绑定指令(v-on)

v-on:简写为@,为DOM元素绑定事件监听。

1. 作用

注册事件=添加监听+提供处理逻辑

2. 语法

v-on:事件名 = “内联语句”

// 鼠标点击count变化
<button v-on:click="count++">按钮</button>

//鼠标划入count变化
<button v-on:mouseenter="count++">按钮</button>

v-on:事件名 = “methods中的函数名”

<button @click="fn">按钮</button>

const app = new Vue({
    el:'#app',
    data: {
        //提供数据
        count: 100
    },
    methods: {
        //提供处理逻辑函数
        fn() {
            console.log('提供逻辑代码')
        }
    }
})

注意:原生DOM对象有onclick, oninput, onkeyup等原生事件,替换为vue的事件绑定形式之后,分别为:v-on:click, v-on:input, v-on:keyup

通过v-on绑定的事件处理函数,需要在methods节点中进行声明

v-on 调用传参

<button @click="buy(2)">可乐2元</button>
<button @click="buy(10)">咖啡10元</button>
<button @click="buy(4.5)">牛奶4.5元</button>

const app = new Vue({
    el:'#app',
    data:{
        money:100
    },
    method:{
        buy(price) {
            this.money -= price
        }
    }
})

v-on指令所绑定的事件处理函数中,同样可以接收到事件对象event

const vm = new Vue({
    el: ‘#app’,
    data: { count:0 },
    methods: {
        addCount(e) {   //事件处理函数的名字
            const nowBgColor = e.target.style.backgroundColor
            e.target.style.backgroundColor = nowBgColor === ‘red’ ? ‘ ’ : ‘red’
            
            //this表示当前new出来的vm实例对象
            //通过this可以访问到data中的数据
            this.count +=1
        },
    },
})

$event是vue提供的特殊变量,用来表示原生的事件参数对象event。$event可以解决事件参数对象event被覆盖的问题。

<h3>count的值为:{{ count }}</h3>
<button @click=”addNewCount(2,$event)”>+2</button>

methods: {
    //在形参处用e接收传递过来的原生事件参数对象$event
    addNewCount(step,e) {
        const nowBgColor = e.target.style.backgroundColor
        e.target.style.backgroundColor = nowBgColor === ‘red’ ? ‘ ’ : ‘red’
        this.count += step
    }
}

4. vue指令修饰符

通过"."指明一些指令后缀,不同后缀封装了不同的处理操作→简化代码

1. 按键修饰符

@keyup.enter→键盘回车监听

<!--只有在’key’是’enter’时调用’vm.submit()’-->
<input @keyup.enter=”submit”>

<!--只有在’key’是’Esc’时调用’vm.clearInput()’-->
<input @keyup.Esc=”vm.clearInput”>

2. v-model修饰符

v-model.trim→去除首尾空格

v-model.number→转数字

.number自动将用户的输入值转为数值类型
.trim 自动过滤用户输入的首尾空白字符串
.lazy 在”change”时而非”input”时更新(文本框失去焦点时才更新)

3. 事件修饰符

@事件名.stop→阻止冒泡

@事件名.prevent→阻止默认行为 

.prevent阻止默认行为
.stop阻止事件冒泡
.capture以捕获模式触发当前的事件处理函数
.once绑定的事件只触发一次
.self

只有在event.target是当前元素自身时触发事件处理函数

.passive立即触发默认行为

4. 系统修饰符

.ctrl,.alt,.meta

5. 鼠标修饰符

.left(鼠标左键).right(鼠标右键).middle(鼠标中键)

6. 表单修饰符

.lazy(等输入完后再显示).trim(删除内容前后的空格).number(输入的是数字或转为数字)

5. 双向绑定指令(v-model)

1. 作用

表单元素使用,双向数据绑定→可以快速获取或设置表单元素内容。

数据变化→视图自动更新

视图变化→数据自动更新

2. 语法

v-model = '变量'

<div id="app">
    账户:<input type="text" v-mode="username">
    密码:<input type="password" v-mode="password">
    <button>登录</button>
    <button>重置</button>
<div>

<script src="https://cdn.jsdlivr.net/npm/vue@2/dist/vue.js></script>
<script>
    const app = new Vue({
        el:'#app',
        data:{
            username:''
        }
    })
</script>

6. 条件渲染指令(v-if)

用来辅助开发者按需控制DOM的显示与隐藏

v-if动态的创建或移除DOM元素,从而控制元素在页面上的显示与隐藏
v-show动态为元素添加或移除style=”display:none;”样式,从而控制元素

这两种方式的性能消耗不同:

1. v-if有更高的切换开销,v-show有更高的初始渲染开销

2. 如果需要频繁的切换,则使用v-show较好

3. 如果在运行时条件很少改变,则使用v-if较好。

v-if可以单独使用,或配合v-else指令一起使用

<div v-if=”Math.random()>0.5”>
    随机数大于0.5
</div>
<div v-else>
    随机数小于或等于0.5
</div>

// v-else-if,充当v-if的”else-if”块
<div v-if=”type ===’A’>优秀</div>
<div v-else-if = “type === ‘B’>良好</div>
<div v-else-if = “type === ‘C’>一般</div>
<div v-else>差</div>

7. 列表渲染指令(v-for)

1. 作用

基于数据循环,多次渲染整个元素→数组,对象,数字...

2. 遍历数组语法

v-for = "(item, index) in 数组"

item是当前的循环项,index是下标,items是待循环的数组。

<ul>
    <li v-for=”item in list”>姓名是:{{item.name}}</li>
</ul>

data: {
    List: [   //列表数据
        {id:1, name: ‘zs’ },
        {id:2, name: ‘ls’ }
    ]
}

v-for指令还支持一个可选的第二个参数,即当前项的索引

语法格式为(item,index) in items

<ul>
    <li v-for=”(item,index) in list”>索引是:{{ index }},姓名是:{{ item.name }}</li>
</ul>

data: {
    List: [   //列表数据
        {id:1, name: ‘zs’ },
        {id:2, name: ‘ls’ }
    ]
}

为每项提供一个唯一的key属性

<ul>
    <!--加key属性的好处:-->
    <!--1. 正确维护列表的状态-->
    <!--2. 复用现有的DOM元素,提升渲染的性能-->
    <li v-for=”user in userlist” :key=”user.id>
        <input type=”checkbox” />
        姓名:{{ user.name }}
    </li>
</ul>

3. 注意

1. key的值只能是字符串或数字类型

2. key的值必须具有唯一性(即:key的值不能重复)

3. 建议把数据项id属性的值作为key的值(因为id属性的值具有唯一性)

4. 使用index的值当作key的值没有任何意义(因为index的值不具有唯一性)

5. 建议使用v-for指令时一定要指定key的值(既提升性能,又防止列表状态紊乱)

8. 自定义指令

自己定义的指令,可以封装一些dom操作,扩展额外功能。

// 需求:当页面加载时,让元素将获得焦点
(autofocus在safari浏览器又兼容性)

// 可以操作dom:dom元素.focus()
mounter() {
    this.$refs.inp.focus()
}

// 但是,如果我在很多个页面都需要获取焦点,都需要这个功能,如果每一次都这样写一遍,是很麻烦的。
   我们可以把它(一段跟dom操作相关的代码)封装到指令当中。

vue允许开发者自定义指令,使用的时候要加上“v-”,声明的时候不用

1. 全局和局部注册语法

#在main.js页面
// 全局注册-语法
Vue.directive('指令名',{
    // inserted代表当前指令,被添加到页面里面去的时候,会自动调用
    "inserted"(el){
        // 可以对el标签扩展额外功能
        el.focus()
    }
})


#在使用该指令的组件页面里面
// 局部注册-语法
directives: {
    "指令名":{
        inserted(){
            //可以对el标签,扩展额外功能
            el.focus()
        }
    }
}


// 使用
<input v-指令名 type="text">

2. 全局和私有自定义指令

// 全局共享的自定义指令需要通过“单页面应用程序的实例对象”进行声明。
// 在main.js入口文件中
const app = Vue.createApp({})

// 注册一个全局自定义指令‘v-focus’
app.directive(‘focus’,{
    //当被绑定的元素插入到DOM中时,自动触发mounted函数
    //el是原生的DOM对象
    mounted(el) {
        el.focus()   //让被绑定的元素自动获得焦点
    }
})



// 声明私有自定义指令
// 在每个vue组件中,可以在directives节点下声明私有自定义指令。
directives: {
    //自定义一个私有指令
    focus: {
        //当被绑定的元素插入到DOM中时,自动触发mounted函数
        //el是原生的DOM对象
        mounted(el) {
            el.focus()   //让被绑定的元素自动获得焦点
        }
    }
}

3. updated函数

mounted函数只在元素第一次插入DOM时被调用,当DOM更新时mounted函数不会被触发。updated函数会在每次DOM更新完成后被调用

app.directive(‘focus’, {
    mounted(el) {   //第一次插入DOM时触发这个函数
        el.focus()
    },
    updated(el) {   //每次DOM更新时都会触发updated函数
        el.focus()
    }
})

注意:在vue2的项目中使用指令时,mounted->bind, updated -> update

函数简写

如果mounted和updated函数中的逻辑完全相同,则可以简写成:

app.directive(‘focus’,(el) = > {
    //在mounted和updated时都会触发相同的业务处理
    el.focus()
})

4. 指令的参数值

在绑定指令时,可以通过“等号”的形式为指令绑定具体的参数值。

// 在绑定指令时,通过“等号”的形式为指令绑定具体的参数值
<div v-color="color">我是内容</div>

// 可以通过binding.value可以拿到指令值,指令值修改会触发update函数
directives: {
    color: {
        //1. inserted提供的是元素被添加到页面中时的逻辑
        inserted(el,binding){
            el.style.color = binding.value
        },
        //2. update指令的值修改的时候触发,提供值变化后,dom更新的逻辑
        update(el,binding){
            el.style.color = binding.value
        }
    }
}

5. 练习-v-loading自定义指令封装

// 场景:实际开发过程中,发送请求需要时间,在请求数据未回来,页面会处于空白状态=>用户体验不好

// 需求:封装一个v-loading指令,实现加载中的效果

// 分析
// 本质loading效果就是一个蒙层,盖在了盒子上
// 数据请求中,开启loading状态,添加蒙层
// 数据请求完毕,关闭loading状态,移除蒙层

// 核心思路
1. 准备类名loading,通过伪元素提供遮罩层
2. 添加或移除类型,实现loading蒙层的添加移除
3. 利用指令语法,封装v-loading通用指令
inserted钩子中,binding.value判断指令的值,设置默认状态
update钩子中,binding.value判断指令的值,更新类名状态

五. 计算属性

1. 计算属性是什么

计算属性本质上就是一个function函数,它可以实时监听data中数据的变化,并return一个计算后的新值,供组件渲染DOM时使用。

计算属性需要以function函数的形式声明到组件的computed选项中。

<input type=”text” v-model.number=”count” />

<p> {{ count }}乘以2的值为:{{ plus }}</p>


data() {
    return { count : 1 }
},
computed: {
    plus() {
        //计算属性函数内部,可以直接通过this访问到app实例
        监听data中的count值的变化,自动计算出count*2之后的新值   
        return this.count * 2
    }
}

注意:计算属性侧重于得到一个计算的结果,因此计算属性中必须有return返回值

2. 计算属性使用的注意点

1. 计算属性必须定义在computed节点中

2. 计算属性必须是一个function函数

3. 计算属性必须有返回值

4. 计算属性必须当做普通属性使用

3. 计算属性vs方法

相对于方法来说,计算属性会缓存计算的结果,只有计算属性的依赖项发生变化时,才会重新进行计算,因此计算属性的性能更好。

computed: {
    plus() {   
        //计算属性的计算结果会被缓存,性能好
        console.log(‘计算属性被执行了’)
        return this.count *2
    },
},

methods: {
    plus() {
        //方法的计算结果无法被缓存,性能低   
        console.log(‘方法被执行了’)
        return this.count *2
    }
}

4. 计算属性完整写法

计算属性默认的简写,只能读取访问,不能“修改”。

computed:{
    计算属性名() {
        //一段代码逻辑(计算逻辑)
        return 结果
    }
}

如果要修改→需要写计算属性的完整写法

computed:{
    计算属性名:{
        get(){
            //一段逻辑代码(计算逻辑)
            return 结果
        },
        set(修改的值){
            //一段代码逻辑(修改逻辑)
        }
    }
}

六. 过滤器

过滤器仅在vue2.x和1.x中受支持,在vue3.x的版本中剔除了过滤器相关的功能。

1. 过滤器常用于文本的格式化

hello -> Hello

过滤器在JavaScript表达式的尾部,由“管道符”进行调用。

过滤器可以用在两个地方:插值表达式v-bind属性绑定

<!--在双花括号中通过“管道符”调用capitalize过滤器,对message的值进行格式化-->
<p> {{ message | capitalize }}</p>

<!--在v-bind中通过“管道符”调用formatId过滤器,对rawId的值进行格式化-->
<div v-bind:id=”rawId|formatId”></div>

2. 定义私有过滤器

filters节点中定义的过滤器,称为“私有过滤器”,因为它只能在当前vm实例所控制的el区域内使用。

const vm = new Vue({
    el: ‘#app’,
    data:{
        message:’hello vue.js’,
        info:’title info’
    },
    //在filters节点下定义“过滤器”
    filters:{
        //把首字母转为大写的过滤器   
        capitalize(str){   
            return str.charAt(0).toUpperCase()+str.slice(1)
        }  
    }
})

3. 定义全局过滤器

//全局过滤器-独立于每个vm实例之外

//Vue.filter()方法接收两个参数,第1个参数是全局过滤器的“名字”,第2个参数是全局过滤器的“处理函数”

Vue.filter(‘capitalize’,(str) =>{

    return str.charAt(0).toUpperCase()+str.slice(1)+’--’

})

4. 连续调用多个过滤器

过滤器可以串联地进行调用

<!--把message的值,交给filterA进行处理-->

<!--把filterA处理的结果,再交给filterB进行处理-->

<!--最终把filterB处理的结果,作为最终的值渲染到页面上-->

{{ message|filterA|filterB }}

5. 过滤器传参

过滤器的本质是JavaScript函数,因此可以接收参数。

<!--arg1和arg2是传递给filterA的参数-->
<p>{{message|filterA(arg1,arg2)}}</p>

//过滤器处理函数的形参列表中:
//第一个参数:永远都是“管道符”前面待处理的值
//第二个参数开始,才是调用过滤器时传递过来的arg1和arg2参数
Vue.filter(‘filterA’,(msg,arg1,arg2)=>{
    //过滤器的代码逻辑
})

七. 组件

1. 组件化开发思想

组件化开发:根据封装的思想,把页面上可重用的UI结构封装为组件,从而方便项目的开发和维护。

前端组件化开发的好处:

1. 提高了前端代码的复用性和灵活性

2. 提升了开发效率和后期的可维护性

vue中的组件化开发:vue是一个支持组件化开发的前端框架。vue中规定组件的后缀名是.vue。之前接触到的APP.vue文件本质上就是一个vue的组件。

组件分类:

1. 普通组件;2. 根组件(整个应用最上层的组件,包裹所有普通小组件)

2. vue的组件构成

每个.vue组件都由3部分构成,分别是:

template:组件的模板结构

script:组件的JavaScript行为

style:组件的样式

vue规定,每个组件对应的模板结构,需要定义到<template>节点中

<template>是vue提供的容器标签,只起到包裹性质的作用,它不会被渲染为真正的DOM元素。

1. 在组件的<template>节点中,支持使用前面所学的指令语法,辅助渲染当前组件的DOM结构。

2. 在vue2.x的版本中,<template>节点内的DOM结构仅支持单个根节点:

<template>
<!--vue 2.x中,template节点内的所有元素,最外层“必须有”唯一的根节点进行包裹,否则报错-->
    <div>
        <h1>这是App根组件</h1>
        <h2>这是副标题</h2>
    </div>
</template>

3. 在vue3.x的版本中,<template>中支持定义多个根节点:

<template>
    <!-- 这是包含多个根节点的template结构,因为h1标签和h2标签外层没有包裹性质的根元素 -->
    <h1>这是App根组件</h1>
    <h2>这是副标题</h2>
</template>

vue规定,组件内的<script>节点是可选的,开发者可以在<script>节点中封装组件的JavaScript业务逻辑。

<script>
    //今后,组件相关的data数据,methods方法等,
    //都需要定义到export default所导出的对象中
    export default {}
</script>
//name节点,name属性指向的是当前组件的名称,建议每个单词的首字母大写

//data节点,vue组件渲染期间需要用到的数据,可以定义在data节点中,data方法中return出去的对象,就是当前组件渲染期间需要用到的数据对象
data() {
    return {
        username:’哇哈哈’
    }
}
//methods节点,组件中的事件处理函数,必须定义到methods节点中。
methods: {
    addCount() {
        this.count++
    },
}

vue规定,组件内的<style>节点是可选的,开发者可以在<style>节点中编写样式美化当前组件的UI结构

// 让style支持less

// 1. 给style加上 lang="less"
<style lang="less>
</style>

// 2. 安装依赖包 less less-loader
yarn add less less-loader -D

3. vue的组件注册

1. 组件的注册

1. 组件之间可以进行相互的引用,组件引用的原则:先注册,再使用

2. 全局注册组件,被全局注册的组件,可以在全局任何一个组件内使用

3. 局部注册组件,被局部注册的组件,只能在当前注册的范围内使用

2. main.js文件的作用

main.js是项目的入口文件,项目中所有的页面都会加载main.js。

主要有三个作用:

1.实例化Vue

2.放置项目中经常会用到的插件和CSS样式

3.存储全局变量

3. 组件注册时名称的大小写

在进行组件的注册时,定义组件名称的方式有两种:

1. 短横线命名法(例如my-swiper和my-search)

2. 帕斯卡命名法或大驼峰命名法(例如MySwiper和MySearch)

帕斯卡命名法的特点:既可以严格按照帕斯卡名称进行使用,又可以转化为短横线名称进行使用。

在实际开发中,推荐使用帕斯卡命名法为组件注册名称,因为它的适用性更强。

在注册组件期间,除了可以直接提供组件的注册名称之外,还可以把组件的name属性作为注册后组件的名称。

4. 全局组件注册

首先要创建.vue文件(三个组成部分),然后再在main.js中进行全局注册。

// 假设这里是一个components文件夹,里面有一个组件
HmButton.vue


// 在main.js文件中

// 导入需要全局注册的组件
import HmButton from './components/HmButton'
// 调用Vue.component进行全局注册
// Vue.component('组件名', 组件对象)
Vue.component('HmButton', HmButton)
//1. 在main.js中导入Swiper和Test两个组件

import Swiper from ‘./components/MySwiper.vue’
import Test from ‘./components/MyTest.vue’

//2. 在main.js中调用app实例的component()方法,在全局注册my-swiper和my-test两个组件
app.component(‘my-swiper’, Swiper)
app.component(‘my-test’, Test)



//3. 在其他组件中,直接以标签的形式,使用刚才注册的全局组件即可
<my-swiper></my-swiper>
<my-test></my-test>

2. 在main.js中用Vue.use()

新建一个userdata目录 下面有一个index.vue和index.js文件

index.vue文件是常规的vue文件

index.js文件注册该组件:

//这里是index.js文件
//这里,将自定义组件注册成插件了
import index from './index.vue'
const indexLists = {
    install: function(Vue) {

        // 注册并获取组件,然后在main.js中引用,再Vue.use()就可以了
        Vue.component('indexLists', index)
    }
}
export default indexLists

在main.js引用并注册: 

import userdata from '@/components/index.js'
Vue.use(userdata);

5. 局部组件注册

// 假设这是compoents文件夹,下面有三个.vue文件
HmFooter.vue
HmHeader.vue
HmMain.vue


<template>
    <h1>这是App根组件</h1>
    <!-- 头部组件-->
    <HmHeader></HmHeader>
    <!-- 主体组件-->
    <HmMain></HmMain>
    <!-- 底部组件-->
    <HmFooter></HmFooter>
</template>

<script>
//导入需要注册的组件
//import 组件对象 from './vue文件路径'
import HmHeader from './components/HmHeader'
import HmMain from './components/HmMain'
import HmFooter from './components/HmFooter'
export default {
    //局部注册
    components: {
        //'组件名':组件对象,
        HmHeader: HmHeader,
        HmMain,
        HmFooter
}
</script>

6. 全局方法注册

在目录下面建立一个common.js文件

//这里是common.js文件
const obj={
    fun(){
        console.log('hello word')
    }
}
export default obj

//这里是别的文件
import comm form './common.js'
Vue.protopyte.$common = comm
this.$common.fun1() // hello word

4. 组件的生命周期

1. 组件的运行过程

开始→import导入组件→components注册组件→以标签形式使用组件→在内存中创建组件的是对象→把创建的组件实例渲染到页面上→组件切换时销毁需要被隐藏的组件→结束。

组件的生命周期指的是:组件从创建运行(渲染)→销毁的整个过程,强调的是一个时间段。

2. 监听组件的不同时刻

vue框架为组件内置了不同时刻的生命周期函数,生命周期函数会伴随着组建的运行而自动调用。1. 当组件在内存中被创建完毕后,会自动调用created函数(发ajax请求数据)

2. 当组件被成功的渲染到页面上之后,会自动调用mounted函数(操作DOM)

3. 当组件被销毁完毕后,会自动调用unmounted函数

3. 监听组件的更新

当组件的data数据更新之后,vue会自动重新渲染组件的DOM结构,从而保证View视图展示的数据和Model数据源保持一致

当组件被重新渲染完毕之后,会自动调用updated生命周期函数。

4. 组件生命周期函数

是由vue框架提供的内置函数,会伴随着组件的生命周期,自动按次序执行。

1. 组件创建阶段

(组件生命周期的第一个阶段)

beforeCreate组件的props/data/methods尚未被创建,都处于不可用状态
created组件的props/data/methods已创建好,都处于可用的状态,但是组件的模板结构尚未生成!
beforeMount将要把内存中编译好的HTML结构渲染到浏览器中。此时浏览器中还没有当前组件的DOM结构
mounted已经把内存中的HTML结构,成功渲染到了浏览器之中。此时浏览器中已然包含了当前组件的DOM结构

2. 组件运行阶段

(组件生命周期的第二个阶段)

beforeUpdate将要,根据变化过后,最新的数据,重新渲染组件的模板结构
updated已经根据最新的数据,完成了组件DOM结构的重新渲染

3. 组件销毁阶段

(组件生命周期的第三个阶段)

beforeDestory将要销毁此组件,此时尚未销毁,组件还处于正常工作的状态
destoryed组件已经被销毁,此组件在浏览器中对应的DOM结构已被完全移除

5. keep-alive对应的生命周期

keep-alive是Vue的内置组件,当它包裹动态组件时,会缓存不活动的组件实例,而不是销毁

keep-alive是一个抽象组件,它自身不会渲染成一个DOM元素,也不会出现在父组件链中。

keep-alive保持状态

默认情况下,切换组件时,无法保持组件的状态。

此时可以使用vue内置的<keep-alive>组件保持动态组件的状态

当组件被缓存时,会自动触发组件的deactivated生命周期函数。

当组件被激活时,会自动触发组件的activated生命周期函数。

keep-alive的三个属性:include,exclude,max 

keep-alive的include属性

include属性用来指定:只有名称匹配的组件会被缓存。多个组件名之间使用英文的逗号分隔。

<template>
    <div class="h5-wrapper" :include="['LayoutPage']">
        <keep-alive>
            <router-view></router-view>
        </keep-alive>
    </div>
</template>

keep-alive的exclude属性

组件名数组,任何匹配的组件都不会被缓存

keep-alive的max属性

最多可以缓存多少组件实例

5. 动态组件

vue提供了一个内置的<component>组件,专门用来实现动态组件的渲染。

指的是动态切换组件的显示与隐藏。vue提供了一个内置的<component>组件,专门用来实现组件的动态渲染。

<component>是组件的占位符

通过is属性动态指定要渲染的组件名称

<component :is=”要渲染的组件的名称”></component>

6. 组件之间的样式冲突

1. 什么是样式冲突

默认情况下,写在.vue组件中的样式会全局生效,因此很容易造成多个组件之间的样式冲突问题。

1. 全局样式:默认组件中的样式会作用到全局。

2. 局部样式:可以给组件加上scoped属性,可以让样式只作用于当前组件。

2. 为什么会有样式冲突

导致组件之间样式冲突的根本原因是:

1. 单页面应用程序,所有组件的DOM结构,都是基于唯一的index.html页面进行呈现的。

2. 每个组件中的样式,都会影响整个index.html页面中的DOM元素。

3. 如何解决样式冲突

1. 为每个组件分配唯一的自定义属性,在编写组件样式时,通过属性选择器来控制样式的作用域。

2. 为了提高开发效率和开发体验,vue为style节点提供了scoped属性,从而防止组件之间的样式冲突问题。

<style scoped>
    /* style节点的scoped属性,用来自动为每个组件分配唯一的“自定义属性”,
    并自动为当前组件的DOM标签和style样式应用这个自定义属性,防止组件的样式冲突问题*/
    .container {
        border:1px solid red;
    }
</style>

3. 如果给当前组件的style节点添加了scoped属性,则当前组件的样式对其子组件是不生效的。如果想让某些样式对子组件生效,可以使用/deep/深度选择器。

/deep/ .title{
    color:blue;
}

4. 注意:/deep/是vue2.x中实现样式穿透的方案。在vue3.x中推荐使用:deep()替代/deep/

:deep(.title) {
    color: blue;
}

4. scoped原理

1. 当前组件内标签都被添加data-v-hash值的属性

2. css选择器都被添加 [data-v-hash] 的属性选择器

最终效果:必须是当前组件的元素,才会有这个自定义属性,才会被这个样式作用到。

7. data是一个函数

一个组件的data选项必须是一个函数。保证每个组件实例,维护独立的一份数据对象。

每次创建新的组件实例,都会执行一次data函数,得到一个新对象。

//这里是BaseCount.vue组件
<template>
    <div class="base-count">
        <button @click="count--">-</button>
        <span>{{ count }}</span>
        <button @click="count++">+</button>
    </div>
</template>

<script>
export default{
    //data必须是一个函数→保证每个组件实例,维护独立的一个数据对象
    data () {
        console.log('data函数执行了')
        return {
            count:999
        }
    }
}
</script>
// 这里是App.vue组件

<template>
    <div id="app">
        <BaseCount></BaseCount>
        <BaseCount></BaseCount>
        <BaseCount></BaseCount>
    </div>
</template>

<script>
import BaseCount from './components/BaseCount.vue'
export default {
    name:'App',
    components:{
        BaseCount
    }
}
</script>

可以看到,‘data函数执行了’打印了三次。每次执行都会得到独立的对象。 

8. 组件的props

1. 封装组件的原则

为了提高组件的复用性,在封装vue组件时要遵守如下的规则:

1. 组件的DOM结构,Style样式要尽量复用

2. 组件中要展示的数据,尽量由组件的使用者提供。

props是组件的自定义属性,组件的使用者可以通过props把数据传递到子组件内部,供子组件内部进行使用。

props的作用:父组件通过props向子组件传递要展示的数据。

props的好处:提高了组件的复用性。

2. 在组件中声明props

<!--my-article组件的定义-->
<template>
    <h3>标题:{{title}}</h3>
    <h5>作者:{{author}}</h5>
</template>

<script>
export default {
    props:[‘title’, ‘author’],   //父组件传递给my-article组件的数据,必须在props节点中声明
}
</script>

3. 无法使用未声明的props

如果父组件给子组件传递了未声明的props属性,则这些属性会被忽略,无法被子组件使用。

<template>
    <h3>标题:{{title}}</h3>
    <h5>作者:{{author}}</h5>
</template>

<script>
export default {
    props:[‘title’],   //author属性没有声明,因此子组件中无法访问到author的值
}
</script>

4. 动态绑定props的值

可以使用v-bind属性绑定的形式,为组件动态绑定props的值。

<!--通过v-bind属性绑定,为title动态赋予一个变量的值-->
<!--通过v-bind属性绑定,为author动态赋予一个表达式的值-->
<my-article :title=”info.title” :author=”’post by’ + info.author”></my-article>

5. props的大小写命名

组件中,如果使用”camelCase(驼峰命名法)”声明了props属性的名称,则有两种方式为其绑定属性的值。

<template>
    <p>发布时间:{{pubTime}}</p>
</template>


<script>
export default {
    props: [‘pubTime’],   //采用“驼峰命名法”为当前组件声明了pubTime属性
}
</script>

<!--既可以直接使用“驼峰命名”的形式为组件绑定属性的值-->

<my-article pubTime=”1989”></my-article>

<!--也可以使用其等价的“短横线分隔命名”的形式为组件绑定属性的值-->

<my-article pub-time=”1989”></my-article>

9. props校验

在封装组件时对外界传递过来的props数据进行合法性的校验,从而防止数据不合法的问题。

使用对象类型的props节点,可以对每个prop进行数据类型的校验。

props: {
    count: Number,
    state: Boolean
}

对象类型的props节点提供了多种数据验证方案,例如:

1. 基础的类型检查

props: {
    propA: String,   字符串类型
    propB: Number,   数字类型
    ...
}

2. 多个可能的类型

如果某个prop属性值的类型不唯一,此时可以通过数组的形式,为其指定多个可能的类型。

props: {
    //propA属性的值可以是“字符串”或“数字”
    propA: [String,Number],
}

3. 必填项校验

如果组件的某个prop属性是必填项,必须让组件的使用者为其传递属性的值。此时,可以通过如下的方式将其设置为必填项。

props: {
    //通过“配置对象”的形式,来定义propB属性的“验证规则”
    propB: {
        //当前属性的值必须是String字符串类型
        type: String,   
        //当前属性的值是必填项,如果使用者没指定propB属性的值,则在终端进行警告提示
        required: true,   
    },
}

4. 属性默认值

props: {
    propC: {
        type: Number,
        //如果使用者没有指定propC的值,则propC属性的默认值为100
        default: 100   
    }
}

5. 自定义验证函数

在封装组件时,可以为prop属性指定自定义的验证函数,从而对prop属性的值进行更加精确的控制:

props: {
    //通过“配置对象”的形式,来定义propD属性的“验证规则”
    PropD: {
        //通过validator函数,对propD属性的值进行校验,“属性的值”可以通过形参value进行接收
        validator(value) {
            //propD属性的值,必须匹配下列字符串中的一个
            //validator函数的返回值为true表示验证通过,false表示验证失败
            return [‘success’, ‘warning’, ‘danger’].indexOf(value) !=-1
        }
    }
}

10. 自定义事件

在封装组件时,为了让组件的使用者可以监听到组件内状态的变化,此时需要用到组件的自定义事件。

只要count值发生变化,立即通过自定义事件的形式,把最新的值发送给组件的使用者。

1. 自定义事件的使用步骤

1. 在封装组件时

1. 声明自定义事件

开发者为自定义组件封装的自定义事件,必须事先在emits节点中声明。

export default {
    //my-counter组件的自定义事件,必须事先声明到emits节点中
    emits: [‘change’],
}

2. 触发自定义事件

在emits节点下声明的自定义事件,可以通过this.$emit(‘自定义事件的名称’)方法进行触发。

export default {
    emits: [‘change’],
    methods: {
        onBtnClick() {
            //当点击+1按钮时,调用this.$emit()方法,触发自定义的change事件
            this.$emit(‘change’)   
        }
    }
}

2. 使用组件时

在使用自定义的组件时,可以通过v-on的形式监听自定义事件

<!--使用v-on指令绑定事件监听-->

<my-counter @change=”getCount”></my-count>

methods: {
    getCount() {
        console.log(‘监听到了count值的变化’)
    }
}

2. 自定义事件传参

在调用this.$emit()方法触发自定义事件时,可以通过第2个参数为自定义事件传参。

export default {
    emits: [‘change’],
    methods: {
        onBtnClick() {
            this.$emit(‘change’, this.count)   //触发自定义事件时,通过第二个参数传参
        },
    },
}

11. 组件之间的数据共享

v-model是双向数据绑定指令,当需要维护组件内外数据的同步时,需要用到。

父子关系:props,$emit

非父子关系:provide&inject,eventbus

在组件上使用v-model的步骤:

1. 父传子:

1. 父组件通过v-bind:属性绑定的形式,把数据传递给子组件

2. 子组件中,通过props接收父组件传递过来的数据

2. 子传父:

1. 在v-bind:指令之前添加v-model指令

2. 在子组件中声明emits自定义事件,格式为update:xxx

3. 调用$emit()触发自定义事件,更新父组件中的数据

1. 组件之间的关系

在项目开发中,组件之间的关系分为如下3种:

父子关系,兄弟关系,后代关系

2. 父子组件之间的数据共享

父→子共享数据

父组件通过v-bind属性绑定向子组件共享数据。同时子组件需要使用props接收数据。

//父组件
<template>
    <div>
        //给子组件以添加属性的方式传值
        <Son :msg=”message” :user=”userinfo”></Son>
    </div>
</template>

<script>
import Son from './components/Son.vue'
export default{
    data() {
        return {
            message: ‘hello vue’,
            userinfo: {name:’zs’,age:20},
        }
    },
    components:{
        Son
    }
}
</script>


//子组件Son.vue
<template>
    <h3>测试父子传值</h3>
    <p>{{ msg }}</p>
    <p>{{ user }}</p>
</template>

<script>
export default {
    props: [‘msg’, ‘user’],
}
</script>

子→父共享数据

子组件通过自定义事件的方式向父组件共享数据。

//子组件
<template>
    <div>
        我是Son组件{{ title }}
        <button @click="changeFn">修改title</button>
    </div>
</template>
<script>
export dafault { 
    props:['title'],
    methods: {
        changeFn() {
            // 1. 通过$emit,向父组件发送消息通知
            this.$emit(‘changeTitle’,'小熊超级棒')
        }
    },
}
</script>


//父组件
<template>
    <div>
        //2. 父组件,对消息进行监听
        <Son :title="myTitle" @changeTitle="handleChange"></Son>
    </div>
</template>
<script>
import Son from './components/Son.vue'
export default{
    data() {
        return {
            myTitle:'小熊很棒'
        }
    },
    methods:{
        //3. 提供处理函数,提供逻辑
        handleChange(newTitle){
            this.myTitle = newTitle
        }
    }
    components:{
        Son
    }
}
</script>

父子双向数据同步

父组件在使用子组件期间,可以使用v-model指令维护组件内外数据的双向同步。

//父组件
<my-test-1 v-model:num=”count”></my-test-1>

//子组件
export default {
    props: [‘num’],
    emits: [‘update: num’],
    methods: {
        addN1() {
            this.$emit(‘update:num’,this.num+1)
        }
    },
}

3. 兄弟组件之间的数据共享

兄弟组件之间实现数据共享的方案是EventBus(复杂场景→Vuex)。可以借助于第三方的包miit来创建eventBus对象。

// 这里是utils文件夹下面的EventBus.js文件
// 3.1 首先npm install [email protected]
// 3.2 接着创建公共的EventBus模块
//eventBus.js
//导入miit包
import mitt from ‘mitt’
//创建EventBus的实例对象
const bus = mitt()
//将EventBus的实例对象共享出去
export default bus


// 这里是接收数据的组件
// 3.3 在数据接收方自定义事件
import bus from ‘./eventBus.js’
export default {
    data() { return { count :0 } },
    created() {        
        //调用bus.on()方法注册一个自定义事件,通过事件处理函数的形参接收数据
        bus.on(‘countChange’,(count) => {
            this.count = count
        })
    }
}


// 这里是发送数据的组件
// 3.4 在数据发送方触发事件
import bus from ‘./eventBus.js’
export default{
    data() { return { count :0 } },
    methods:{
        addCount() {
            this.count++
            bus.emit(‘countChange’,this.count)   //调用方法触发事件
        }
    }
}

4. 后代组件之间的数据共享

指的是父节点的组件向其子孙组件共享数据。可以使用provide和inject实现。

provide&inject作用:跨层级共享数据。

1. 父节点通过provide提供数据

export default {
    data() {
        return {
            //1. 定义父组件要向子孙组件共享的数据
            color: ‘red’   
        }
    },
    provide() {
        //2. provide函数return的对象中,包含了要向子孙组件共享的数据   
        return {
            // 普通类型【非响应式】
            color:this.color,
            // 复杂类型【响应式】
            userInfo:this.userInfo,
        }
    },
}

2. 子孙节点可以使用inject数组,接收父级节点向下共享的数据

<template>
    <h5>子孙组件---{{color}}</h5>
</template>

<script>
export default {
    //子孙组件,使用inject接收父节点向下共享的color数据,并在页面上使用
    inject:[‘color’],
}
</script>

3. 父节点对外共享响应式的数据

父节点使用provide向下共享数据时,可以结合computed函数向下共享响应式的数据。

import { computed } from ‘vue’
export default {
    data() {
        return {
            color: ‘red’   //1. 定义父组件要向子孙组件共享的数据
        }
    },

    provide() {   //2. provide函数return的对象中,包含了要向子孙组件共享的数据
        return {
            //要共享的数据的名称:要共享的数据的值
            color:computed( ()=>this.color),
        }
    },
}

4. 子孙节点使用响应式的数据

如果父级节点共享的是响应式的数据,则子孙节点必须以.value的形式进行使用。

<template>
    <!-- 响应式的数据,必须以.value的形式进行使用-->
    <h5>子孙组件---{{ color.value }}</h5>
</template>

<script>
export default {
    //子孙组件,使用inject接收父节点向下共享的color数据,并在页面上使用
    inject:[‘color’],
}
</script>

5. vuex(全局数据共享)

vuex是终极的组件之间的数据共享方案。在企业级的vue项目开发中,vuex可以让组件之间的数据共享变得高效,清晰,且易于维护。

12. prop&data,单向数据流

1. 共同点

都可以给组件提供数据。

2. 区别

data的数据是自己的→随便改

prop的数据是外部的→不能直接改,要遵循单向数据流。

八. Vue组件高级

1. watch侦听器

watch侦听器允许开发者监视数据的变化,从而针对数据的变化做特定的操作。

开发者需要在watch节点下,定义自己的侦听器。

例如:监视用户名的变化并发起请求,判断用户名是否可用。

export default {
    data() {
        return {username: ‘’ }
    },
    watch: {
        //监听username的值的变化,
        //形参列表中,第一个值是“变化后的新值”,第二个值是“变化之前的旧值”
        username(newVal,oldVal) {
            console.log(newVal,oldVal)
        },
    }
}

2. immediate选项

在默认情况下,组件在初次加载完毕后不会调用watch侦听器。

如果想让watch侦听器立即被调用,则需要使用immediate选项。

watch: {
    //1. 监听username值的变化
    username: {
        //2. hander属性是固定写法:当username变化时,是调用handler
        async handler(newVal,oldVal) {
            const { data:res }=await axios.get(`url地址/${newVal}`)
            console.log(res)
        },
        //3. 表示组件加载完毕后立即调用一次当前的watch侦听器
        immediate: true
    },
}

3. deep选项

当watch侦听的是一个对象,如果对象中的属性值发生了变化,则无法被监听到。此时需要使用deep选项。

data() {
    return {
        info: { username : ‘admin’},  //info中包含username属性
    }
},
watch: {
    info: {   //直接监听info对象的变化
        async handler(newVal, oldVal) {
            const {data:res}=await axios.get(`url地址/${newVal.username}`)
            console.log(res)
        },
        deep: true   //需要使用deep选项,否则username值的变化无法被监听到。
    },
}

4. 监听对象单个属性的变化

如果只想监听对象中单个属性的变化,则可以按照如下方式定义watch监听器。

data() {
    return {
        info: {
            username: ‘admin’,
            password:’’
        },
    }
},

watch: {
    ‘info.username’: {   //只想监听info.username属性值的变化
        async handler(newVal,oldVal) {
            const { data:res }=await axios.get(`url地址/${newVal}`)
            console.log(res)
        },
    },
},

5. 计算属性vs侦听器

计算属性和侦听器侧重的应用场景不同:

计算属性侧重于监听多个值的变化,最终计算并返回一个新值

侦听器侧重于监听单个数据的变化,最终执行特定的业务处理,不需要有任何返回值。

6. ref引用

在Vue里面,以$符号开头的,都是vue的内置成员。

特点:查找范围→当前组件内(更加精确稳定)

querySelector查找范围→整个页面

ref用来辅助开发者在不依赖jQuery的情况下,获取DOM元素或组件的引用。

每个vue组件实例上,都包含一个$refs对象,里面存储着对应的DOM元素或组件的引用。

默认情况下,组件的$refs指向一个空对象

1. 使用ref引用DOM元素

<!--使用ref属性,为对应的DOM添加引用名称-->
<h3 ref=”myh3”>MyRef组件</h3>
<button @click=”getRef”>获取$refs引用</button>

methods: {
    getRef() {
        //通过this.$refs.引用名称,可以获取到DOM元素的引用
        console.log(this.$refs.myh3)
        //操作DOM元素,把文本颜色改为红色
        this.$refs.myh3.style.color=’red’
    },
}

2. 使用ref引用组件实例

<!--使用ref属性,为对应的“组件”添加引用名称-->
<my-counter ref=”counterRef”></my-counter>
<button @click=”getRef”>获取$refs引用</button>

methods: {
    getRef() {
        //通过this.$refs.引用名称,可以引用组件的实例
        console.log(this.$refs.counterRef)
        //引用到组件的实例之后,就可以调用组件上的methods方法
        this.$refs.counterRef.add()
    },
}

3. this.$nextTick(cb)方法

// 需求:编辑标题,编辑框自动聚焦
<template>
    <div>
        <!-- 是否在编辑状态 -->
        <div v-if="isShowEdit">
            <input v-model="editValue" type="text">
            <button>确认</button>
        </div>
        <!-- 默认状态-->
        <div v-else>
            <span>{{ title }}</span>
            <button @click="handleEdit">编辑</button>
        </div>
    </div>
</template>
<script>
export default{
    data(){
        return{
            title:'大标题',
            editValue:'',
            isShowEdit: false
        }
    },
    methods:{
        handleEdit(){
            //1. 点击编辑,显示编辑框
            this.isShowEdit = true 
            //2. 让编辑框,立刻获取焦点
            this.$refs.inp.focus()
        }
    }
}
</script>
// 问题:“显示之后”,立刻获取焦点是不能成功的!
// 原因:Vue是异步更新DOM(提升性能)

组件的$nextTick(cb)方法,等DOM更新之后,才会触发执行此方法里的函数体。

语法:this.$nextTick(函数体)

this.$nextTick(()=>{
    this.$refs.inp.focus()
})

也就是,等组件的DOM异步地重新渲染完成之后,再执行cb回调函数。从而保证cb回调函数可以操作到最新的DOM元素。

<script>
export default{
    data(){
        return{
            title:'大标题',
            editValue:'',
            isShowEdit: false
        }
    },
    methods:{
        handleEdit(){
            //1. 点击编辑,显示编辑框
            this.isShowEdit = true 
            //2. 让编辑框,立刻获取焦点
            this.$nextTick(()=>{
                this.$refs.inp.focus()
            })
        }
    }
}
</script>

4. resetFields

resetFields方法是Vue.js自带的重置表单方法

resetFields方法通常与表单验证和表单提交一起使用。

在表单验证失败时,将输入数据重置为初始值是一个常见的做法。resetFields方法可以快速、方便地执行此操作。

在表单提交后,需要将表单数据清空以便用户再次使用表单时能够开始一个新的会话。

上面的代码中,使用resetFields方法将包含username和password的表单重置为初始状态。在单击Reset form按钮后,表单数据将被重置。

5. validate

Vue.validate()是Vue.js 提供的验证方法,可以方便地在Vue项目中进行表单验证。

Vue.validate()可以轻松实现对表单输入内容的验证,包括必填字段、数字、邮箱、手机号、长度限制、正则表达式等等。

同时Vue.validate()还提供了更加自定义的API接口,可以通过传入回调函数来进行特定场景的验证工作。

7. v-model原理

原理:v-model本质上是一个语法糖。例如应用在输入框上,就是value属性和input事件的合写。

作用:提供数据的双向绑定。

1. 数据变,视图跟着变:value

2. 视图变,数据跟着变@input

注意:$event用于在模板中获取事件的形参

<template>
    <div id="app">
        // 原本写法,是双向绑定,不仅数据变化视图会变,当视图被修改数据也会变
        <input v-model="msg" type="text">

        // 数据变化,视图会自动更新
        <input :value="msg2" type="text">

        // 把value属性和input事件合写,就可以实现数据的双向绑定
        <input :value="msg" @input="msg=$event.target.value" type="text>
    </div>
</template>

<script>
export default {
    data() {
        return {
            msg1:'',
            msg2:''
        }
    }
}
</script>

8. 表单类组件封装

1. 普通写法

// App.vue组件
<template>
    <div class="app">
        <BaseSelect
            :cityId="selectId"
            @changeId="selectId = $event"
        >
        </BaseSelect>
    </div>
</template>
<script>
import BaseSelect from './components/BaseSelect.vue'
export default {
    data() {
        return {
            selectId: '103'
        }
    },
    components: {
        BaseSelect
    }
}
</script>


//BaseSelect.vue组件
<template>
    <div>
        <select :value="cityId" @change="handleChange">
            <option value="101">北京</option>
            <option value="102">上海</option>
            <option value="103">武汉</option>
            <option value="104">深圳</option>
            <option value="105">广州</option>
        </select>
    </div>
</template>
<script>
export default {
    props: {
        cityId: String
    },
    methods: {
        handleChange(e){
            this.$emit('changeId',e.target.value)
        }
    }
}
</script>

2. 用v-model简化代码

子组件中:props通过value接收,事件触发input

父组件中:v-model给组件直接绑定数据(:value+@input)

// 父组件使用
<BaseSelect :value="selectId" @input="selectId=$event" />
//写成这样
<BaseSelect v-model="selectId"/>


//子组件封装
<select :value="value" @change="handleChange">...</select>
props: {
    value:String
}
methods: {
    handleChange(e){
        this.$emit('input',e.target.value)
    }
}

9. .sync修饰符

1. 作用

可以实现子组件与父组件数据的双向绑定,简化代码。

2. 特点

prop属性名,可以自定义,非固定为value

// 父组件
<BaseDialog :visible="isShow" @update:visible="isShow=$event" />

// 本质就是:属性名和@update:属性名合写
<BaseDialog :visible.sync="isShow" />


// 子组件
props: {
    visible: Boolean
},
this.$emit('update:visible',false)

九. 插槽

插槽是<slot>vue为组件的封装者提供的能力。允许开发者在封装组件时,把不确定的,希望由用户指定的部分定义为插槽。‘ v-slot: ’ 的简写是#

可以把插槽认为是组件封装期间,为用户预留的内容的占位符。

1. 插槽的用法

<template>
    <p>这是MyCom1组件的第1个p标签</p>
    <!--通过slot标签,为用户预留内容占位符(插槽)-->
    <slot></slot>
    <p>这是MyCom1组件最后一个p标签</p>
</template>

// 在别的地方使用该组件
<my-com-1>
    <!-- 在使用MyCom1组件时,为插槽指定具体的内容-->
    <p>---用户自定义的内容---</p>
</my-com-1>

2. 后备内容

封装组件时,可以为预留的<slot>插槽提供后备内容(默认内容)。如果组件的使用者没有为插槽提供任何内容,则后备内容会生效。

<template>
    <p>这是MyCom1组件的第1个p标签</p>
    <slot>这是后备内容</slot>
    <p>这是MyCom1组件最后一个p标签</p>
</template>

3. 具名插槽

如果在封装组件时需要预留多个插槽节点,则需要为每个<slot>插槽指定具体的name名称。这种带有具体名称的插槽叫做“具名插槽”。

具名插槽的简写形式:跟v-on和v-bind一样,v-slot也有缩写。把“v-slot:”替换为字符#。

例如v-slot:header,可以被重写为#header。

<div class=”container”>
    <header>
        <!--我们希望把页头放这里-->
        <slot name=”header”></slot>
    </header>
    <main>
        <!--我们希望把主要内容放这里-->
        <!--没有指定name名称的插槽,会有隐含的名称叫做default-->
        <slot></slot>
    </main>
    <footer>
        <!--我们希望把页脚放这里-->
        <slot name=”footer”></slot>
    </footer>
</div>


// 使用该组件
<my-com-2>
    // 在一个<template>元素上使用v-slot指令。
    <template v-slot:header>
        <h1>滕王阁序</h1>
    </template>
    <template v-slot:default>
        <p>螃蟹在剥我的壳</p>
        <p>笔记本在写我</p>
        <p>我落在漫天飞舞的雪花上</p>
    </template>
    <template v-slot:footer>
        <p>落款:三行情书</p>
    </template>
</my-com-2>

4. 作用域插槽

作用域插槽是一种特殊的插槽,它能够接收来自父组件的数据,并在子组件中使用

在Vue2中,作用域插槽是通过slot-scope实现的。而在Vue3中,作用域插槽是通过v-slot实现的

1. 作用域插槽的作用

父组件可以拿到子组件的数据并且在父组件加工处理之后再传递给子组件。

2. 基本使用步骤

//1. 给slot标签,以添加属性的方式传值
<slot :id="item.id" msg="测试文本"></slot>

//2. 所有添加的属性,都会被收集到一个对象中
// { id:3, msg:'测试文本' }


<MyTable :list="list">
    //3. 在template中,通过' #插槽名="obj" '接收,默认插槽名为default
    <template #default="obj">
        <button @click="del(obj.id)">删除</button>
    </template>
</MyTable>

3. 定义作用域插槽

子组件:(通过 :数据别名=“数据” 来给到插槽,将数据绑定给这个插槽)

// 定义一个组件插槽
<template>
  <div>
    <h2>子组件</h2>
    <slot :lian="Lianhua"> 莲花楼来啦! </slot>
  </div>
</template>
<script>
export default {
  name: 'slotdemoson',
   data() {
    return {
      Lianhua: {
        'friends':[
          ['李相夷','方多病', '笛飞声', '角丽樵', '乔婉娩'],
          ['李莲花','方小宝', '展云飞', '糖果', '狐狸精', '萝卜'],
          ['石水','妙手空空', '苏小庸', '两仪仙子'],
        ],
        'coolFruit':[
          ['黑果','东北冻梨', '黑枣'],
          ['蓝果','蓝莓', '蓝靛果'],
          ['紫果','飓风葡萄', '西梅', '桑葚', '黑布林李子'],
        ],
      },
    }
  },
}
</script>

// 在这个.vue文件中使用组件插槽
<template>
  <div>
    <h2>最近在追的剧的角色~</h2>
    <slotdemoson>
      <!--通过slot-scope来定义一个对象scope(名字可以随便起)来接收子组件传递过来的数据-->
      <template slot-scope="scope">
        <!--使用插值表达式直接把接收到的插槽数据渲染到页面(scope保存数据,所以真正的数据在scope的下一层)-->
        {{ scope.lian }}
      </template>
    </slotdemoson>
  </div>
</template>
<script>
import slotdemoson from './Slotdemoson.vue'
export default {
  components: { slotdemoson },
}
</script>

这里会将slot-scope属性的属性值作为一个对象-----用于保存子组件插槽中绑定的数据

这个用于保存插槽数据的slot-scope的属性值要通过子组件插槽定义的数据别名来访问数据

4. 具名作用域插槽

//这里是Test.vue文件
<template>
    <div>
        <header>
            <slot name="header" v-bind:row="{title:'你叫什么名字?'}" />
        </header>
    </div>
</template>
<script>
export default{
    name:'TestCmp'
}
</script>


//这里是App.vue
<template>
    <div id="app">
        <Test>
            // 这里的scope其实就是这个“接收到的对象数据”的对象名,然后这个scope对象有一个属性的属性名叫row。
            <template #header="scope">
                // 可以看到,scope其实就长这样:{"row":{"title":"你叫?"}}
                console.log(scope)  
                <h1>{{scope.row.title}}</h1>
            </template>
        </Test>
    </div>
</template>

//也可以利用解构(把row拿出来),写成下面的这种形式
<template>
    <div id="app">
        <Test>
            <template #header="{row}">
                <h1>{{row.title}}</h1>
            </template>
        </Test>
    </div>
</template>

5. 使用Vue+ElementUI

使用Vue+ElementUI完成一个项目时,会用到作用域插槽。

场景就是:需要根据服务器响应来的数据渲染成一个表格到页面上,而表格中每一行的后面都有个操作列,可以对这一行的数据进行修改、删除等。

一提到对这一行的数据进行修改、删除就必然得获取到这一行数据中的唯一标识(俗话说就是ID),那在ElementUI中就给table-column赋予了一项技能,就是使用插槽。

ElementUI预设了三个参数(row, column, $index)

可以通过插槽中定义的接收数据的对象来调用,调用row也就可以获取到这一行的数据,再从中提取id也就拿到了这一行数据所对应的id值,也就可以根据id向服务器发起修改或删除的请求了。

list就是服务器响应的数据,并将其绑定给el-table。ElementUI提供了一个很nice的功能:给表<el-table>的列<el-table-column>使用作用域插槽,可以通过slot-scope的属性值来调用row获取这一行的数据。

组件template 代码:

<el-table :data="list">
    <el-table-column label="标题" prop="stem"></el-table-column>
    <el-table-column label="作者" prop="creator"></el-table-column>
    <el-table-column label="点赞" prop="likeCount"></el-table-column>
    <el-table-column label="浏览数" prop="views"></el-table-column>
    <el-table-column label="更新时间" prop="createAt"></el-table-column>
    <el-table-column label="操作" width="120px">
        <template v-slot="{row}">
            <div class="actions">
              <i class="el-icon-view" @click="doShowDrawer('preview',row.id)"></i>
              <i class="el-icon-edit-outline" @click="doShowDrawer('edit',row.id)"></i>
              <i class="el-icon-delete" @click="doDelete(row.id)"></i>
            </div>
        </template>
    </el-table-column>
</el-table>

十. 路由

1. 概念

路由(router)就是对应关系。

在前端里面:路由就是“地址”与页面上组件的对应关系。

通俗理解:Hash地址与组件之间的对应关系,不同的hash会展现出不同的组件页面。

用户点击了页面上的路由链接(本质上是a链接),导致了URL地址栏中的Hash地址的变化

前端路由监听到了Hash地址的变化,前端路由把当前Hash地址对应的组件渲染到浏览器中

2. 路由分为两大类

1. 后端路由

后端路由指的是:请求方式,请求地址与function处理函数之间的对应关系。

在node.js课程中,express路由的基本用法如下:

const express=require(‘express’)
const router=require.Router()

router.get(‘/userlist’,function(req,res){
    /*路由的处理函数*/
})
router.post(‘/adduser’,function(req,res){
    /*路由的处理函数*/
})

module.exports = router

2. 前端路由

SPA与前端路由:SPA是一个web网站只有唯一一个HTML页面,所有组件的展示与切换都在这唯一的一个页面内完成。此时,不同组件之间的切换需要通过前端路由来实现。

也就是:Hash地址与组件之间的对应关系。

3. 前端路由的工作方式

1. 用户点击了页面上的路由链接

2. 导致了URL地址栏中的Hash值发生了变化

3. 前端路由监听到了Hash地址的变化

就是在created生命周期函数中,用window.onhashchange监听。

created() {
window.onhashchange = () =>{
    switch(location.hash) {
        case ‘#/home’:   //点击了首页的链接
        this.comName=’my-home’
        break
        case ‘#/movie’:   //点击了电影的链接
        this.comName=’my-movie’
        break
        case ‘#/about’:   //点击了关于的链接
        this.comName=’my-about’
        break
    }
}
}

前端路由把当前Hash地址对应的组件渲染到浏览器中。

实际开发中,不会使用上述办法。

4. vue-router4.x的基本使用步骤

1. 在项目中安装vue-router

2. 定义路由组件

3. 声明路由链接和占位符

使用<router-link>标签(本质还是a链接)来声明路由链接,并使用<router-view>标签来声明路由占位符。

<template>
<h1>App组件</h1>

<!--声明路由链接-->
//Hash地址之前不需要#
<router-link to=”/home”>首页</router-link>
<router-link to=”/movie”>电影</router-link>
<router-link to=”/about”>关于</router-link>

<!--声明路由占位符-->
<router-view></router-view>
</template>

创建路由模块

在项目中创建router.js路由模块,在其中按照如下步骤创建并得到路由对象。

//从vue-router中按需导入两个方法
//createRputer用于创建路由的实例对象
//createWebHashHistory用于指定路由的工作模式(hash模式)
import{createRouter,createWebHashHistory}from‘vue-router’

//导入需要使用路由控制的组件
import Home from ‘./MyHome.vue’
import Movie from ‘./MyMovie.vue’
import About from ‘./MyAbout.vue’

//创建路由实例对象
const router=createRouter({
    //history属性指定路由的工作模式
    history:createRouter(),
    //通过routes数组,指定路由规则
    routes:[
        //path是hash地址,component是要展示的组件
        {path:’/home’,component:Home},
        {path:’/movie’,component:Movie},
        {path:’/about’,component:About},
    ],
})
// 向外共享路由实例对象
export default router

// 在main.js中导入并挂载路由模块
import router from ‘./components/router’
// 导入并挂载路由模块
const app = createApp(App)
//挂载路由模块
app.use(router)
app.mount(‘#app’)

5. 路由重定向

用户在访问地址A的时候,强制用户跳转到地址C,从而展示特定的组件页面。通过路由规则的redirect属性,指定一个新的路由地址,可以很方便地设置路由地重定向

const router=createRouter({
    //history属性指定路由的工作模式
    history:createRouter(),
    //通过routes数组,指定路由规则
    routes:[
        //其中,path表示需要被重定向的“原地址”,redirect表示将要被重定向到的“新地址”
        {path:’/’,redirect:’/home’},
        {path:’/home’,component:Home},
        {path:’/movie’,component:Movie},
        {path:’/about’,component:About},
    ],
})

6. 路由高亮

vue-router提供了一个全局组件router-link(取代a标签)

1. 能跳转,配置to属性指定路径(必须)。本质还是a标签,to无需#

2. 能高亮,默认就会提供高亮类名,可以直接设置高亮样式。

router-link自动给当前导航添加了两个高亮类名

1. router-link-active 模糊匹配(用的多)

2. router-link-exact-active 精确匹配

router-link的两个高亮类名太长了,我们希望能定制怎么办?

const router = new VueRouter({
    routes:[...],
    linkActiveClass:"类名1",
    linkExactActiveClass:"类名2"
})

1. 使用默认的高亮class类

被激活的路由链接,默认会应用一个叫做router-link-active的类名。开发者可以使用此类名选择器,为激活的路由链接设置高亮的样式。

/*在index.css全局样式表中*/
.router-link-active{
    background-color: red;
    color: white;
    font-weight:bold;
}

2. 自定义路由高亮的class类

在创建路由的实例对象时,开发者可以基于linkActiveClass属性,自定义路由链接被激活时应用的类名。

const router=createRouter({
    //history属性指定路由的工作模式
    history:createRouter(),
    //指定被激活的路由链接,会应用router-active这个类名
    //默认的router-link-active类名会被覆盖掉
    linkActiveClass:’router-active’,
    //通过routes数组,指定路由规则
    routes:[
        //其中,path表示需要被重定向的“原地址”,redirect表示将要被重定向到的“新地址”
        {path:’/’,redirect:’/home’},
        {path:’/home’,component:Home},
        {path:’/movie’,component:Movie},
        {path:’/about’,component:About},
    ],
})

7. 嵌套路由

通过路由实现组件的嵌套展示,叫做嵌套路由。

1. 在about组件内,声明子路由链接和子路由占位符

<template>
    <h3>MyAbout组件</h3>
    <!--在关于页面中,声明两个子路由链接-->
    <router-link to=”/about/tab1”>Tab1</router-link>
    <router-link to=”/about/tab2”>Tab2</router-link>
    <!--在关于页面中,声明tab1和tab2的路由占位符-->
    <router-view></router-view>
</template>

2. 在父路由规则中,通过children属性嵌套声明子路由规则

import Tab1 from ‘./components/tabs/MyTab1.vue’
import Tab2 from ‘./components/tabs/MyTab2.vue’
const router=createRouter({
    routes:[
        {   
            //about页面的路由规则(父级路由规则)
            path:’/about’,
            component:About,
            children:[   //通过children属性嵌套子级路由规则
                    //注意,子路由规则的path不要以/开头
                    {path:’tab1’,component:Tab1},
                    {path:’tab2’,component:Tab2}
            ],
        },
    ],
})

8. 跳转传参

1. 查询参数传参

// 查询参数传参,比较适合传多个参数
// 跳转
to="/path?参数名1=值&参数名2=值"


// 对应页面组件接收传递过来的值
$route.query.参数名

// 在created里面,要通过this.$route.query.参数名 获取
<script>
export default{
    created(){
        console.log(this.$route.query)
    }
}
</script>

 2. 动态路由传参

把Hash地址中可变的部分定义为参数项,从而提高路由规则的复用性。

// 动态路由传参,优雅简洁,传单个参数比较方便
//1. 配置动态路由
const router = new VueRouter({
    routes:[
        ...,
        {
            // 路由中的动态参数以:进行声明,冒号后面是动态参数的名称
            path:'/search/:参数名',
            //如果不穿参数,也希望匹配,可以加个可选符"?",path:'/search/:参数名?'
            component:Search
        }
    ]
})

//2. 配置导航链接跳转
to="/path/参数值'

//3. 获取传递过来的值
$route.params.参数名

//将以下3个路由规则,合并成了一个,提高了路由规则的复用性

{path:’/movie/1’,component:Movie}
{path:’/movie/2’,component:Movie}
{path:’/movie/3’,component:Movie}

使用props接收路由参数

为了简化路由参数的获取形式,vue-router允许在路由规则中开启props传参。

{path:’/movie/:id’,component:Movie,props:true}

<template>
    <!-3.直接使用props中接收的路由参数-->
    <h3>MyMovie组件---{{ id }}</h3>
</template>

<script>
export default {
    //2. 使用props接收路由规则中匹配到的参数
    props:[‘id’]  
}
</script>

9. 路由的封装抽离

所有的路由配置都堆在main.js中合适吗?

我们应该将路由模块抽离出来。好处:拆分模块,利于维护。 

应该新建一个router文件夹专门管理路由

10. Vue路由-404

作用:当路径找不到匹配时,给个提示页面

位置:配在路由最后

语法:path:" * "(任意路径)-前面不匹配就命中最后这个

import NotFind from '@/views/NotFind'
const router = new VueRouter({
    routes: [
        { path: '/',redirect:'/home' },
        { path:'/home',component:Home },
        { path:'/search/:words?',component: Search },
        { path:'*',component: NotFind }
    ]
})

十一. vue-router

1. 什么是vue-router

vue-router是官方的路由插件,它与vue.js是深度集成的。可让构建单页面应用变得易如反掌。

vue的单页面应用是基于路由和组件的,路由用于设定访问路径,并将路径和组件映射起来;

在vue-router单页面应用中,可用路径之间的切换来实现页面切换和跳转的。

路由模块的本质 就是建立起url和页面之间的映射关系

vue2        VueRouter3.x        Vuex3.x

vue3        VueRouter4.x        Vuex4.x

2. vue-router的作用

传统的页面应用,是用一些超链接来实现页面切换和跳转的。在vue-router单页面应用中,则是路径之间的切换,也就是组件的切换

3. vue-router的实现原理

SPA(single page application):单一页面应用程序,只有一个完整的页面。

它在加载页面时,不会加载整个页面,而是只更新某个指定的容器中内容。

单页面应用(SPA)的核心之一是: 更新视图而不重新请求页面;

vue-router在实现单页面前端路由时,提供了两种方式:Hash模式和History模式。

1. Hash模式

http://localhost:8080/#/home

vue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。 hash(#)是URL 的锚点,代表的是网页中的一个位置,单单改变#后的部分,浏览器只会滚动到相应位置,不会重新加载网页,也就是说 #是用来指导浏览器动作的,对服务器端完全无用,HTTP请求中也不会不包括#;同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置;所以说Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据

2. History模式

http://localhost:8080/home

由于hash模式会在url中自带#,如果不想要很丑的 hash,我们可以用路由的 history 模式,只需要在配置路由规则时,加入"mode: 'history'",这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。

const router = new VueRouter({
    routes,
    mode:"history"
})

4. vue-router的功能

1. 嵌套的路由/视图表

2. 模块化的、基于组件的路由配置

3. 路由参数、查询、通配符

4. 基于 Vue.js 过渡系统的视图过渡效果

5. 细粒度的导航控制

6. 带有自动激活的 CSS class 的链接

7. HTML5 历史模式或 hash 模式,在 IE9 中自动降级

8. 自定义的滚动条行为

5. vue-router的基本使用(5+2)

1. 安装vue-router

npm install vue-router

2. 在vue项目中引入并使用

import Vue from 'vue'
// 引入
import VueRouter from 'vue-router'
// 安装注册
Vue.use(VueRouter)

// 创建路由对象
const router = new VueRouter({
  routes: [
    // 配置路由映射关系
  ]
})

// 注入,将路由对象注入到Vue实例中,建立关联
new Vue({
    render: h => h(App),
    router:router,
    // 简写
    //router
}).$mount('#app')

3. 初始化vueRouter实例

传入一个routes数组,用于配置路由映射关系。routes数组中的每一项都是一个路由配置对象,包含以下属性:

  • path:路由路径,可以是一个字符串或者一个正则表达式
  • name:路由名称
  • component:路由对应的组件
  • redirect:重定向路由
  • alias:别名路由
  • meta:元信息,可以在路由切换时使用 下面是一个简单的例子:
const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  }
]

在Vue组件中,我们可以使用<router-link>组件来创建链接,使用<router-view>组件来显示对应的组件。例如: 

<template>
  <div>
    <router-link to="/">Home</router-link>
    <router-link to="/about">About</router-link>
    <router-view></router-view>
  </div>
</template>

6. 相关方法,API和属性

1. 方法

router.beforeEach

这个方法会在每次路由跳转前执行,可以用来做一些权限控制或者全局路由守卫

router.beforeEach((to, from, next) => {
  // to:即将跳转的路由对象
  // from:当前所在的路由对象
  // next:必须调用该方法才能跳转到下一个路由
  // 如果不调用该方法,则不会跳转
  next()
})

我们判断用户是否已经登录,如果没有登录且不是前往登录页面,则强制跳转到登录页面。 

router.beforeEach((to, from, next) => { const isAuthenticated = localStorage.getItem('token') if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' }) else next() })
 router.afterEach

这个方法会在每次路由跳转后执行,可以用来做一些页面切换后的处理,例如页面滚动到顶部等。

我们在页面跳转后将页面滚动到顶部。

router.afterEach((to, from) => {
  // to:即将跳转的路由对象
  // from:当前所在的路由对象
  window.scrollTo(0, 0)
})
router.push

这个方法用来跳转到一个新的路由,可以是一个字符串或者一个路由配置对象。

我们跳转到根路由

router.push('/')
router.replace

这个方法用来替换当前路由,可以是一个字符串或者一个路由配置对象。

我们替换当前路由为根路由

router.replace('/')
router.go

这个方法用来在路由历史记录中前进或后退若干步。

我们后退一步。 

router.go(-1)
router.back

这个方法用来后退一步,等同于router.go(-1)

我们后退一步

router.back()
router.forward

这个方法用来前进一步,等同于router.go(1)

我们前进一步

router.forward()
router.currentRoute

这个属性会返回当前的路由对象,包含以下属性:

  • path:路由路径
  • name:路由名称
  • hash:URL中的哈希值
  • query:URL中的查询参数
  • params:路由路径中的参数
  • fullPath:完整的URL路径
  • matched:当前路由的匹配记录,包含一个或多个路由记录对象
    console.log(router.currentRoute)
    // 输出:{path: "/", name: "Home", hash: "", query: {}, params: {}, fullPath: "/"}

2. 属性

import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/Home'
import About from '@/components/About'
Vue.use(Router)
export default new Router({
  mode: 'history', // 设置路由模式为history,会将URL中的#去掉
  base: process.env.BASE_URL, // 指定应用的基路径,可以在不同的域名下使用相同的路由配置
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path: '/about/:id?',
      name: 'About',
      component: About,
      props: true // 将路由参数作为组件的props传递给组件
    },
    {
      path: '/login',
      name: 'Login',
      component: () => import('@/components/Login.vue') // 使用懒加载,只有访问该路由时才会加载该组件
    },
    {
      path: '/404',
      name: 'NotFound',
      component: () => import('@/components/NotFound.vue')
    },
    {
      path: '*', // 当访问其它不存在的路由时,重定向到'/404'
      redirect: '/404'
    }
  ],
  scrollBehavior (to, from, savedPosition) {
     //该属性用于指定路由切换时页面如何滚动
    // 当切换路由时,返回页面顶部
    return { x: 0, y: 0 }
  }
})

3. 路由懒加载

在应用程序较大时,我们需要考虑性能问题。一种优化方式是将应用程序拆分为多个小块,并在需要时动态加载这些块,这就是所谓的懒加载。在Vue Router中,使用懒加载非常简单,只需要将组件作为函数返回即可。

在这个例子中,我们使用了import()函数来动态加载组件。注意,import()函数返回一个Promise对象,因此我们可以使用then()方法来处理组件加载完成后的逻辑。

const Foo = () => import(/* webpackChunkName: "foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "bar" */ './Bar.vue')
const routes = [
  {
    path: '/foo',
    name: 'Foo',
    component: Foo
  },
  {
    path: '/bar',
    name: 'Bar',
    component: Bar
  }
]

4. 路由元信息

路由元信息是指在路由配置中定义的一些额外信息,例如页面标题、页面描述、页面关键字等。这些信息可以在路由切换时使用。

在这个例子中,我们在路由配置中定义了三个元信息:titledescriptionkeywords。我们可以在路由切换时使用这些信息来动态修改页面的标题、描述和关键字。

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
    meta: {
      title: '首页',
      description: '这是首页',
      keywords: '首页,Vue,路由'
    }
  },
  {
    path: '/about',
    name: 'About',
    component: About,
    meta: {
      title: '关于我们',
      description: '这是关于我们',
      keywords: '关于我们,Vue,路由'
    }
  }
]

我们在beforeEach()方法中获取当前路由的元信息,并根据元信息动态修改页面的标题、描述和关键字

router.beforeEach((to, from, next) => {
  const meta = to.meta
  if (meta.title) {
    document.title = meta.title
  }
  if (meta.description) {
    const description = document.querySelector('meta[name="description"]')
    description.setAttribute('content', meta.description)
  }
  if (meta.keywords) {
    const keywords = document.querySelector('meta[name="keywords"]')
    keywords.setAttribute('content', meta.keywords)
  }
  next()
})

十二. 导航

导航,是指路由的改变。

1. 导航的分类

1. 声明式导航

在浏览器中实现导航的方式,叫做声明式导航。

例如:普通网页中点击<a>链接,vue项目中点击<router-link>都属于声明式导航。

<template>
    <h3>MyHome组件</h3>
    // 动态绑定to属性的值,并通过name属性指定要跳转到的路由规则。
    // 期间还可以用params属性指定跳转期间要携带的路由参数。
    <router-link:to=”{name:’mov’,params:{id:3}}”></router-link>
</template>

2. 编程式导航

调用API方法实现导航的方式,叫做编程式导航。例如普通网页中调用location.href跳转到新页面的方式,属于编程式导航。

两种语法

1. path路径跳转(简易方便)

this.$router.push('路由路径')

// 完整写法
this.$router.push({
    path:'路由路径'
})

2. name命名路由跳转(适合path路径长的场景) 

methods:{
    gotoMovie(id){
        //调用push函数期间指定一个配置对象,name是要跳转到的路由规则 
        this.$router.push({name:’mov’})
    },
}


// 需要先给路由起个名字
{ name:'路由名',path:'/path/xxx',component:xxx}

3. 路由传参(查询参数+动态路由传参)//  两种跳转方式,对于两种传参方式都支持

//1. path路径跳转传参(query传参)
this.$router.push('/路径?参数名1=参数值1&参数名2=参数值2')
this.$router.push({
    path:'/路径',
    query: {
        参数名1:'参数值1',
        参数名2: '参数值2'
    }
})

//2. path路径跳转传参(动态路由传参)
this.$router.push('/路径/参数值')
this.$router.push({
    path:'/路径/参数值'
})


//3. name命名路由跳转传参(query传参)
this.$router.push({
    name:'路由名字',
    query:{
        参数名1:'参数值1',
        参数名2:'参数值2'
    }
})

//4. name命名路由跳转传参(动态路由传参)
this.$router.push({
    name:'路由名字',
    params:{
        参数名:'参数值',
    }
})

4. vue-router提供了许多编程式导航的API,其中最常用的导航API分别是:

this.$router.push(‘hash地址’)跳转到指定hash地址,并增加一条历史记录
this.$router.replace(‘hash地址’)跳转到指定hash地址,并替换掉当前的历史记录
this.$router.go(数值n)在浏览历史中前进和后退
$router.back()后退一层
$router.forward()前进一层

2. 导航守卫

可以控制路由的访问权限

1. 什么是导航守卫

比如我们有三个父级路由,分别是“/”,“/login”,“/user”,我们在这些路由间进行切换的时候,就是在做导航。

我们可以在这些路由切换过程当中,给它设置对应的导航守卫,来对“切换”这样一个行为,做一个限制。

简单来说路由导航守卫就是可以让我们对用户要跳转的路由做一次检查,符合条件的就放行,不符合条件则强制用户跳转至指定位置。

//创建路由实例对象
const router = createRouter({...})
//调用路由实例对象的beforeEach函数,声明全局前置守卫
//fn必须是一个函数,每次拦截到路由的请求,都会调用fn进行处理
router.beforeEach(fn)

2. 守卫方法的3个形参

全局守卫的守卫方法中接收3个形参,格式为:

router.beforeEach((to,from,next)=>{
    //to目标路由对象
    //from当前导航正要离开的路由对象
    //next是一个函数,表示放行
})

注意:

1. 在守卫方法中,如果不声明next形参,则默认允许用户访问每一个路由!

2. 在守卫方法中如果声明了next形参,则必须调用next()函数,否则不允许用户访问任何一个路由。

3. next函数的三种调用方式

 

直接放行:next()

强制其停留在当前页面:next(false)

强制其跳转到登录页面:next(‘/login’)

4. 结合token控制后台主页的访问权限

如果用户登录成功了,浏览器里面会存储着一个token值。

router.beforeEach(()=>{
    //1. 读取token
    const token = localStorage.getItem(‘token’)
    //2. 想要访问“后台主页”且token值不存在
    if(to.path===’/main’ && !token){
        //3. 强制跳转到登陆页面
        next(‘/login’)
    }else{
        //直接放行,允许访问后台主页
        next()
    }
})

3. 路由导航守卫的分类

1. 全局守卫

全局导航守卫会拦截每个路由规则,从而对每个路由进行访问权限的控制,它又分为全局前置守卫和全局后置守卫。

全局前置守卫

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

执行时机:初始化时执行以及每次路由跳转切换前执行

使用场景:全局前置守卫是最常用的导航守卫,它主要作用于登录验证,获取用户权限信息等场景。

使用方式:可以使用router.beforeEach注册一个全局前置守卫。

const router = new VueRouter({ ... })

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

// ...

//to: 即将要进入的目标路由对象(这个对象中包含name,params,meta等属性)

//from:当前导航正要离开的路由对象(这个对象中包含name,params,meta等属性)

// next():跳转下一个路由。必须要调用next方法来resolved这个钩子,只有全部钩子执行完了,导航的状态才是confirmed(确认的)。

})

全局后置守卫

 当跳转到了当前界面时需要执行的操作。

执行时机:初始化时执行以及每次路由跳转切换后执行

使用场景:主要作用于分析、更改页面标题、声明页面等辅助功能场景。

注意事项:后置守卫钩子不会接受next函数也不会改变导航本身。

使用方式:可以使用router.afterEach注册一个全局后置守卫。

const router = new VueRouter({ ... })

router.afterEach((to, from) => {

    document.title = to.meta.title //修改网页的title

})

2. 路由独享守卫

顾名思义就是只针对当前路由生效,和其他路由没有关系。

执行时机:只在进入当前路由时触发,不会在 params、query 或hash改变时触发。它只有在从一个不同的路由导航跳转时,才会被触发。

注意事项:路由独享守卫没有后置守卫,但是可以和全局后置守卫搭配使用。

使用方式:直接在路由配置上使用beforeEnter定义独享守卫。

const routes = [

{

path: '/foo',

component: Foo,

beforeEnter: (to, from,next) => {

    if(to.meta.isAdmin) {

        next()

    }else {

        alert('暂无权限')

    }
    
},

},

]

4. 组件守卫

在组件内直接定义路由导航守卫,组件内守卫又分为进入守卫,更新守卫(2.2新增),离开守卫。

1. 进入守卫

执行时机:通过路由规则进入该组件时触发。

注意事项:进入守卫不能访问this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。

使用方式:可以使用beforeRouteEnter注册一个进入守卫。

beforeRouteEnter (to, from, next) {

//可以通过传一个回调给 next 来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数
    next(vm => {

    // 通过 `vm` 访问组件实例

    })

}

2. 更新守卫

执行时机:在当前路由改变,但是该组件被复用时触发

使用方式:直接在路由配置上使用beforeRouteUpdate定义独享守卫

beforeRouteUpdate (to, from, next) {

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

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

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

// 可以访问组件实例 `this`

}

3. 离开守卫

执行时机:通过路由规则离开该组件时触发。

使用场景:离开守卫通常用来预防用户在还未保存修改前突然离开。该导航可以通过返回false来取消。

使用方式:可以使用beforeRouteLeave注册一个离开守卫。

beforeRouteLeave (to, from, next) {

    const answer = alert('还未保存,确定要离开吗')

    if (answer) {

        next() //放行

    }else {

        return false //取消

    }

}

5. 路由导航守卫的应用

在跳转到界面前, 进行用户权限检查限制(如是否已登陆/是否有访问路由权限);

在跳转到登陆界面前, 先判断用户是否登陆,用户没有登陆才进行显示。

比如我们想要访问个人中心,或者购物车,我们要判断用户有没有登陆:如果登陆,就跳转个人中心或者购物车,如果没有登陆,就跳转登陆页面

;