简介

  • 概念

    构建用户界面的前端框架

  • 特性

    数据驱动视图、双向数据绑定

  • 核心原理

    MVVM

指令

  • 内容渲染

    v-text、插值表达式 {{}}v-html

  • 属性绑定

    v-bind:attribute 简写为 :attribute

  • 事件绑定

    v-bind:event 简写为 @event

    • 事件参数对象

      1. 若事件处理函数本身没有传参,则 event 事件本身会作为默认参数,传递给事件处理函数。

        <template>
        	<button @click="changeBgc">按钮变色</button>
        </template>
        <script>
            methods: {
                changeBgc(e) {
                    e.target.style.backgroudColor = 'red'
                }
            }
        </script>
        
      2. 若事件处理函数需要传参,则会覆盖掉隐式的默认的 event 事件参数,需要显示的传入,传值方式为 $event

        <template>
        	<button @click="changeBgc('blue', $event)">按钮变色</button>
        </template>
        <script>
            methods: {
                changeBgc(color, e) {
                    e.target.style.backgroudColor = color
                }
            }
        </script>
        
    • 事件修饰符

      在原生 js 中,我们会使用 event.preventDefault()event.stopPropagation() 来阻止默认事件的触发和传播。在 vue 中,提供了一些事件指令的修饰符,用于实现相同的功能。

      事件修饰符 说明
      .prevent 阻止默认事件行为
      .stop 阻止事件冒泡
      .capture 以捕获方式触发事件
      .once 绑定事件只触发一次
      .self 仅当 event.target 是当前元素自身时触发
    • 按键修饰符

      用于判断所监听按键事件的具体按键。

      <input @keyup.enter="submit">
      <input @keyup.esc="clearInput">
      
  • 双向绑定

    v-model 用于表单数据双向绑定

    修饰符

    修饰符 说明
    .number 自动转换为 数值类型
    .trim 去除首位空白字符
    .lazy 懒更新,change 时而非 input 时更新
  • 条件渲染

    v-ifv-else-ifv-else

    v-show

  • 列表渲染

    v-for 基于数组来渲染列表或其他子项

    • 普通

      <ul>
          <li v-for="item in list"> {{ item.name }} </li>
      </ul>
      
    • 可带第二个参数 index

      <ul>
          <li v-for="(item, index) in items"></li>
      </ul>
      
    • 使用 key 维护列表状态

      默认的 vue 虚拟 dom 渲染优化机制,它会尽可能复用已有元素以提高性能,但是会导致无法正确更新有状态的列表,所以,可以通过指定元素的唯一 key 属性,以便 vue 可以跟踪各节点状态。

      注意:key 必须唯一,只能为字符串或数字类型,不能是对象。

  • 过滤器

    过滤器本质就是一个函数,一般是用作文本格式化,提高复用性和代码的可阅读性。vue 3 已弃用。

    • 私有过滤器

      定义在各组件的 vue 文件的 filters 节点中

      filters: {
      	filterFunction(str, arg1, arg2, ...) {
      		...
      		return newStr
      	}
      }
      

      使用时,添加管道符,可在 插值表达式 和 v-bind 指令下的参数里使用

      <template>
      	<div>
              {{ name | filterFunc(arg1, arg2)}}
          </div>
      </template>
      
    • 全局过滤器

      除了定义位置与私有过滤器不一样,其他都一样。它定义在 main.js 中的 Vue 类中,类似静态函数

      <script>
      	//main.js
          Vue.filter(filterFunc, (str, arg1, arg2, ..) => {
              ...
              return newStr
          })
      </script>
      <div></div>
      

计算属性

  • 声明

    computed 的节点中,以函数的方式声明

    <script>
    export default = {
        name: 'MyApp',
        data(): {
            return {
            	count: 1,
            	price: 2.5,
            	money: 50
        	}
        }
        computed: {
            summary() {
                return this.money - this.count * this.price
            }
        }
    }
    </script>
    
  • 特性

    • 计算属性本身就是一个函数,更新值的时候调用对应函数即可。
    • 与普通函数的区别是它的调用时机,也就是更新调用时机:
      • 初始化时
      • 计算属性所依赖的 data 项发生更新时
      • 除上述情况外,使用计算属性仅会使用缓存值,而不调用更新

侦听器

  • 作用

    监听某项数据变化,进而执行特定逻辑。常用场景:用户名合法性、唯一性校验。

  • 声明

    在 watch 节点下,声明与 data 中所监听项同名的监听函数,当监听值发生变化时触发函数。

    export default {
        data(): {
            return {
                username: '',
        		info: {
    				age: 0,
        			gender: true
                }
            }
        },
        watch: {
            // 普通监听
            username(newVal, oldVal) {
                if(newVal ....) {
                    handleServie()
                }
            },
            // 监听对象的某个属性
            'info.age': {
                handler(newVal, oldVal) {
                    ...
                }
            }
        }
    }
    
  • 选项

    • immediate 选项

      默认情况下,watch 不会在组件加载完毕后调用,添加 immediate 选项,可在组件加载后立即调用。

    • deep 选项

      默认情况下,watch 在监听一个对象时,是不会监听其属性值的变化,这也是无意义的监听,只有当 deep 项为 true 时,才会监听对象属性值的变化

      export default {
          data(): {
              return {
                  username: ''
              }
          },
          watch: {
              username: {
                  handler(newVal, oldVal) {
                      ...
                  },
                  // 组件首次加载后,立即调用
                  immediate: true,
                  // 监听对象属性值
                  deep: true
              }
          }
      }
      
  • 侦听器 vs 计算属性

    计算属性,本质是一个属性作用,用来监听属性依赖项的更新而更新自身。

    侦听器,本质是一个业务处理逻辑函数,用来对监听数据发生变化时而做出的具体业务逻辑处理。

单页面应用程序 SPA

  • 特点

    Single Page Application 整个 web 网站只有一个 html 页面,所有功能与交互都在其上进行。

  • 优点

    • 交互体验好,无白屏、无跳转
    • 前后端分离更彻底,无需像传统 web 那样返回多个页面或模板
    • 基于第二点,服务器只专注处理数据,无需渲染页面和组织合成逻辑,压力降低,吞吐能力增大
  • 缺点

    • 首屏加载慢

      路由懒加载、cdn 加速、代码压缩、网络传输压缩

    • 不利于 SEO

      SSR 服务端渲染

vite & vue-cli

vue 官方提供的快速创建工程化的 SPA 项目的两种方式,对比如下

vite vue-cli
支持 vue 版本 vue 3 vue 2 和 vue 3
基于 webpack
运行速度 较慢
功能完善度 小而巧 大而全
  • vite

    • 创建项目

      npm init vite-app my-app
      
      cd my-app
      npm install
      npm run dev
      
    • 运行流程

      vite 做的事就是:通过 main.jsApp.vue 渲染到 index.html 的指定区域。

      // main.js
      import  { createApp } from 'vue'
      import App from './App.vue'
      
      const app = createApp(App)
      app.mount('#app')
      
  • vue-cli

    • 创建项目

      // 安装 vue-cli
      npm install -g @vue/cli
      
      // 创建项目,参数初始化
      vue create my-app  // 命令行形式
      vue ui			   // 可视化界面
      
    • vue 2.0 项目

      • main.js

        基本结构

        import Vue from 'vue'
        import App from '@/App.vue'
        
        Vue.config.productionTip = False
        // 类似于开发模式,在 console 面板中会有提示
        
        new Vue({
            render: h => h(App)
        }).$mount('#app')
        
      • vue-router 3.x 路由

        1. 创建路由

          // router/index.js
          import Vue from 'vue'
          import VueRouter from 'vue-router'
          
          Vue.use(VueRouter)
          // 将 VueRouter 配置为 Vue 的插件
          
          const router = VueRouter({
              routes: [
                  { path: '/', redirect: '/movie'},
                  { path: '/movie', component: Movie }
              ]
          })
          
          export default router
          
        2. 挂载路由

          // main.js
          import router from '@/router/index.js'
          
          new Vue({
              render: h => h(App),
              router: router
          }).$mount('#app')
          

vue 组件

组件构成

无需多赘述

export default = {
    name: 'MyApp',
    data(): {
        return {
			username: '',
    		passwd: ''
        }
    },
    props: ['item1', 'at'],
    methods: {
        fuc1() {
            ....
        }
    },
  
}

组件注册

  • 全局注册

    main.js 中,给 app 实例进行注册

    // main.js
    import  { createApp } from 'vue'
    import App from './App.vue'
    
    import MyButton from './components/MyButton.vue'
    
    const app = createApp(App)
    
    // 注册名称可自定义,使用时必须与命名一致
    app.component('my-button', MyButton)
    
    app.mount('#app')
    
  • 局部注册

    在需要使用的组件内部,components 节点进行注册

    // Example.vue
    
    import MyButton from './components/MyButton.vue'
    
    export default = {
        components: {
            my-button: MyButton,
            // 或者命名为大驼峰命名法,该种方式在使用的时候,使用大驼峰或者短横线组件名,都是允许的
            MyButton: MyButton,
            // 或者简写
            MyButton
        }
    }
    

组件间样式冲突

  • 问题

    默认情况下,style 节点的样式属性会对所有组件生效,因为整个 spa 程序只有单个的 html 页面。

  • 添加 scoped 属性

    为了解决上述问题,vue 给 style 提供了 scope d 属性,以防止组件间的样式冲突。实现原理是:自动地、给每个组件内的选择器及其所对应元素,添加一个全局唯一的属性名。

  • /deep/ 样式穿透

    假如对 style 添加了 scoped 属性,又想让某些样式选择器对当前及其子组件生效,则需要使用到 /deep/ 样式穿透语法。

    <style lang="less" scoped>
        .title {
            color: 'blue' // 对应实际编译后的选择器为 title[data-v-asdf1]
        }
    
        /deep/ .title {
            color: 'blue' // 对应选择器为 [data-v-asdf1] title
        }
    
        // 在 vue3 中,/deep/ 语法改成 :deep()
    </style>
    

组件的 props

  • 定义

    组件本质就是一种抽象封装,如果类比函数的话,props 就相当于函数的参数。

    如果要灵活复用组件,接收外界传参就是一个必不可少的设计。

    vue 组件设计的传参方式是:通过在组件的 props 中声明参数变量,在使用时通过自定义属性进行传参。

  • 声明

    普通的 props 声明方式,就是在 props 节点处指定一个 字符串变量列表。如果需要对 props 属性的类型、是否允许缺省、值验证,则需要使用 对象声明 的形式。

  • props 验证

    • 类型检查
    • 必填项校验
    • 默认值
    • 自定义验证函数
    export default = {
        props: {
            age: {
                type: Number,
                // 共八种类型:String Array Object Boolean Function Symbol Date
                required: true,
                default: 18,
                validator(value) {
                    // bool 函数
                    if(value < 18)
                        return false
                }
            }
        }
    }
    

组件动态样式 Class 与 Style 绑定

注意:

在 vue 中,我们不再建议通过获取元素对象,来直接操作 DOM 元素样式,包括使用 $this.refs.refName.style.transfrom = '...' 也不推荐,因为这样是直接绕过了 vue 的虚拟 DOM 渲染机制,甚至不会触发 transition 过渡动画。所以,当我们在 vue 中,有修改元素样式以触发动画的需求时,例如做一些动画时,建议通过动态的 class 属性,或者 style 属性,去修改和指定元素样式。

为实现组件的动态样式,可以使用动态的 class 属性,以及 style 属性。

  • 动态绑定 class 的几种方式

    v-bind

    三目运算符

    数组

    对象

  • 动态绑定 Style

    只能以对象的形式,指定样式属性和值。属性的指定可以是驼峰命名,也可以是原生 css 属性名称;值可以是原生 css 值,也可以是表达式。

    <template>
    	<div :class='title'></div>
    	<div :class='isItalic? "italic" : ""'></div>
    	<div :class=[类名, 三目运算符, ...]></div>
    	<div :class='clsObj'></div>
    	<div :style={ color: 'red', fontSize: '42px', 'background-color': bgc}></div>
    </template>
    
    <script>
    export default = {
        data(): {
        	return {
        		isItalic: true,
        		clsObj: {
    				italic: true,
        			bold: false
                },
        		bgc: 'gray'
            }
        }
    }
    </script>
    

组件的自定义事件

  • 定义

    在组件化开发中,组件都是高度封装抽象的,父组件一般不能也不会直接获取子组件的元素,进而去添加监听事件,这是不符合封装原则的。之前的 props 解决了组件传参的问题,那么 “自定义事件” 就是为了解决父组件对子组件内部操作信号的获取问题,因为,作为一个组件,除了能接收外部传递的数据进行展示外,还肯能发生一些交互,这些交互行为应该被父组件所获取,并进行相应的逻辑处理。所以,自定义事件也可以称作子组件向父组件释放信号。

  • 声明-触发-监听

    • 子组件 声明 事件
    • 子组件在某种交互行为下 触发 事件(或称作释放信号)
    • 父组件 监听 事件

    子组件

    <template>
    	<button @click="onBtnClick">改变</button>
    </template>
    <script>
    export default = {
        name: Counter
        emit: ['change', 'add'] // ①声明
        methods: {
    		onBtnClick(){
                this.$emit('change') // ②触发
                this.$emit('add', 10)
            }
        }
    }
    </script>
    

    父组件

    <template>
    	<counter @change='onChange' @add='onAdd'></counter>  // ③监听
    </template>
    <script>
    export default = {
        methods: {
    		onChange(){
                console.log('接收到子组件change事件')
            },
            onAdd(count) {
                console.log('接收到子组件的add事件,以及参数:' + count)
            }
        }
    }
    </script>
    
  • 传参

    在子组件触发事件的位置,可以传参;同时,在父组件监听事件的处理函数可以接受参数。

  • 组件的 v-model

    • 说明

      组件的 v-model 指令,是在 props 传参 的基础上添加 自定义事件,用于保证组件内外数据双向绑定的作用。

      (个人理解,这其实是不符合封装思想的,在子组件内部修改父组件的数据,是非常不安全的。虽然实际上他不是在子组件内部修改的数据,但是这种行为机制不符合封装逻辑。)

    • 使用

      相较于普通的 props 传参自定义事件传参,v-model 需要额外做两个地方

      1. 事件名称必须为 update:propsVariable
      2. 在父组件中使用子组件时,原来的传参形式 v-bind 改成 v-model
    • 原理

      vue 3 只帮我们额外做了一件事,那就是自动监听了 update: 开头的自定义事件,并帮我们把该事件传递回来的参数,更新到了 v-model 的传值对象。

    子组件

    <template>
    	<P> {{ count }} </P>
    	<button @click="onBtnClick">+1</button>
    </template>
    <script>
    export default = {
        name: Counter,
        props: ['count']
        emit: ['update:count'] // ①声明: 特殊形式 update:props 
        methods: {
    		onBtnClick(){
                this.$emit('update:count', this.count +1) // ②触发:正常传值
            }
        }
    }
    </script>
    

    父组件

    <template>
    	<counter v-model:count='totalClicks'></counter> 
    </template>
    <script>
    data(): {
        return {
            totalClicks: 0
        }
    }
    </script>
    

组件的生命周期

  • 运行流程

    导入 => 注册 => 内存中创建组件实例 => 将实例渲染到页面 => 切换时销毁

  • 生命周期

    生命周期函数 执行时机 所属阶段 执行次数 应用场景
    created 创建完毕时 创建 1 发送 ajax 请求
    mounted 首次渲染到页面完毕时 创建 1 操作 dom
    updated 每次被更新渲染完毕时 运行 0-n
    unmounted 组件销毁后 销毁 1

组件间数据共享

  • 父子数据传递

    父 => 子:属性绑定,props 接收

    子 => 父:自定义事件,监听接收

    父 <=> 子:v-model

  • 兄弟数据传递

    event bus 全局变量,依赖于 mitt,使用步骤如下:

    1. 创建 event bus 对象
    2. 接收方:监听 bus 对象的自定义事件及定义数据处理函数
    3. 发送方:通过 bus 对象触发自定义事件及传参
    // eventBus.js
    import mitt from 'mitt'
    
    const bus = mitt()
    
    export default bus
    
    
    // 数据接收方
    import bus from './eventBus.js'
    
    bus.on('自定义事件名', (data) => {...})
    
    
    // 数据发送方
    import bus from './eventBus.js'
    
    bus.emit('自定义事件名, data')
    
  • 祖先向子孙组件节点数据传递

    provide 传递

    inject 接收

    // 祖先节点
    export default = {
        provide: {
            color: this.color,
            height: computed(this.height)
        }
    }
    
    // 子孙节点
    export default = {
        inject: ['color', 'height']
    }
    
  • 全局存储 vuex

ref 引用

  • 是什么

    在原生 js 或者 jQuery 中,我们要获取元素对象都是通过方法和选择器,而在 vue 中,无论是数据驱动视图的做法,还是组件封装的思想,我们都是有意去忽略对元素本身的属性、值获取和更新,进而更关注业务实现本身。

    但有时候是无法避免的要获取元素本身,来获取更多原生的 api,且要获取到组件实例,所以 vue 使用了 ref 属性,用于选择获取元素和组件实例。

  • 如何引用

    1. 给元素或组件添加 ref 属性

    2. 通过 this.$refs.refName 获取引用

      <input type='text' ref='ipt'>
      <button @click='showInput'>展示输入框</button>
      
      methods: {
        showInput() {
              this.$refs.ipt.foucs()
          }
      }
      
  • this.$nextTick(callback) 方法

    该方法,将传递进去的 callback 推迟到下一个 dom 更新周期之后执行,即 updated 之后,或者说是 updated 的最后。

动态组件

  • 使用

    vue 提供了一个内置组件 <component> 作为占位符,通过指定其 is 属性动态的切换组件。为后续软路由的实现和逻辑做铺垫。

    <component is='组件名'></component>
    
    // is属性也可以通过 v-on 属性绑定,指定一个变量,进而实现动态变换
    <component :is='comName'></component>
    
    data(): {
    	return {
            comName: null
        }
    }
    
  • 保持组件状态 keep-alive

    默认情况下,随着动态组件的切换,旧组件的实例将会在内存中被销毁,再次切换回来也将重新创建新的组件实例并渲染至页面。

    如果需要保持组件的存活状态,则需要在动态组件占位符 <component> 外层包裹 <keep-alive> 组件。

    <keep-alive>
    <component :is='comName'></component>
    </keep-alive>
    

插槽

  • 作用

    组件作为一个高度封装的对象,使用者可以通过 props 传参的方式,更加自由的使用它。但是,仅自定义组件里的数据并不能满足多样化的需求,所以,插槽的设计,就是为了更加灵活的自定义组件的呈现形式,而无需修改组件本身。

    有了它,我们能够:

    1. 在组件的插槽位置,插入复杂的元素、组件、数据。
    2. 通过擦作用于插槽传值的形式,自定义组件内数据的呈现形式。
  • 基本使用 & 后备内容

    1. 定义

      slot 标签之间的内容为后备内容,即插槽默认内容

      // SubComp.vue 子组件
      
      <template>
      	<h1>标题</h1>
      	<slot>正文预留插槽,此段文字也为后备内容</slot>
      	<p>署名</p>
      </template>
      
    2. 使用

      使用时需要在组件内, 使用 template 模板,并携带 v-slot 指令

      <template>
      	<sub-comp>
              <template v-slot>正文部分,将替代后备内容</template>
          </sub-comp>
      </template>
      
  • 具名插槽

    1. 定义

      在一个组件中,可能需要预留多个插槽,为区分不同插槽,需要通过指定 name 属性,来定义具名插槽

    2. 使用

      在使用时,在组件内的插槽模板中,在 v-slot 指令后添加具名插槽名称即可

      <template>
      	<sub-comp>
              <template v-slot:default>正文部分</template>
      		<template v-slot:footer>页脚</template>
      		<template #footer>v-slot 的简写形式</template>
          </sub-comp>
      </template>
      
  • 作用域插槽

    1. 定义-传值

      在定义插槽时,可能需要向外暴露数据,以求使用插槽时更好的自定义数据呈现形式,所以,可以通过在定义插槽时,添加 v-bind 属性,以实现类似 props 传参的形式供插槽使用者使用。这类具有 v-bind 属性的 slot,也称作作用域插槽。

    2. 使用-接收

      在使用作用域插槽时,所有 v-bind 属性会被包装成一个对象,我们进需要在 v-slot 指令后,使用变量接收即可。

      // SubComp.vue 子组件
      
      <template>
      	<table>
              <tr>
                  <th>表头1</th>
                  ...
          </tr>
              <tr v-for="item in items" :key="item.id">
                  <slot :lineData="item"></slot>
          </tr>
          </table>
      </template>
      
      
      // 父组件
      <template>
      	<sub-comp>
              <template v-slot:default="scope">
                  <td>{{ scope.lineData.id }}</td>
                  <td>{{ scope.lineData.name }}</td>
                  ...
      </template>
          </sub-comp>
      </template>
      

      当然,我们也可以在接收作用域插槽的 props 时,使用解构语法。

      <template>
      	<sub-comp>
              <template #default="{ lineData }">
                  <td>{{ lineData.id }}</td>
                  ...
      

自定义指令

  • 声明

    私有自定义指令的声明,是在 directives 节点下定义。

    全局自定义指令的声明,是在 main.js 下的 app 实例,使用 directives 方法。

    // 私有自定义指令,subCom.vue
    export default = {
        directives: {
            focus: {
       			// mounted 仅在首次渲染到 dom 时触发
                mounted(el) {
                    el.focus() // el 为使用质量的元素
                },
    
                // updated 在每次 dom 更新时都触发
                updated(el) {
                    el.focus()
                }
            },
            // 如果 mouted 和 updated 实现一样,则可以简写为如下,触发时机也是上述二者的并集
            focus(el) {
                el.focus()
            }
        }
    }
    
    // 全局自定义指令,main.js
    ...
    const app = createApp(App)
    
    app.directive('focus', (el) => {
        mounted(el) {
            ...
        },
        updated(el) {
            ...
        }
    })
    
  • 生命周期函数

    mounted:仅在元素首次渲染到 dom 时触发

    updated:在 dom 每次更新时都触发

  • 指令传参

    传参:像内置指令一样,通过 = 接收参数。v-focus="'red'"

    接收:在指令内部的 mounted 和 updated 函数中,默认隐式的传递了一个 el 参数,所以在第二个参数位置进行接收。

    <template>
    <input type="text" v-focus="'red'">
    </template>
    
    <script>
    export default = {
        directives: {
            focus(el, color) {
                el.focus()
                el.style.color = color.value // 注意这里 color 不是值本身,类似一个 ref 对象,通过 .value 访问值
            }
        }
    }
    </script>
    

路由

后端路由 vs 前端路由

  • 后端路由

    请求方式、请求地址、处理函数间的对应关系。

  • 前端路由

    由于 SPA 中,各个功能页面的切换是通过动态组件的切换实现的,所以就需要,将 hash 地址和组件之间对应起来,而这就是前端路由的作用。

    工作方式:

    点击路由链接

    => 浏览器 URL 的 Hash 值变化

    => 被监听到并查询路由对应组件

    => 渲染组件到指定占位符

vue-router

  • 定义 & 安装

    vue-router 是 vue 官方的路由解决方案

    安装

    // vue3 必须安装 vue-router 4.x版本
    
    npm install vue-router@next -S
    
  • 创建路由模块

    1. 创建路由实例对象

      // router/index.js
      import { createRouter, createWebHashHistory } from 'vue-router'
      
      const router = new createRouter({
          history: createWebHashHistory(),
          routes: [
              { path: '/', redirect: '/movie'},
              { path: '/movie', component: 'Movie'}
          ]
      })
      
      export default router
      
    2. 在 main.js 中给 spa 实例挂载路由模块

      // main.js
      ...
      import router from './router/index.js'
      
      const app = createApp(App)
      
      app.use(router)
      ...
      
  • 嵌套路由——子路由

    嵌套路由可以直接在对应的父路由下,添加 children 路由节点。

    const router = new createRouter({
        history: createWebHashHistory(),
        routes: [
            { path: '/', redirect: '/movie'},
            { path: '/movie', component: 'Movie'},
            {
                path: '/about',
                component: 'About',
                children: [
                    { path: 'tab1', component: 'Tab1'},
                    { path: 'tab2', component: 'Tab2'}
                ]
            }
        ]
    
  • 动态路由——传参

    可以在路由声明中用 :param 项,给组件传递数据。

    1. 路由声明

      const router = new createRouter({
          history: createWebHashHistory(),
          routes: [
              { path: '/', redirect: '/movie'},
              // 声明一个 id 参数项
              { path: '/movie/:id', component: 'Movie'},
              // 允许 props 传值
              { path: '/movie/:id', component: 'Movie', props: true},
      
    2. 接收

      组件有两种方式接收动态路由的传参,

      第一,使用 $router.params.id 访问

      第二,使用 props 接受参数,但在声明要令 props 值为 true

  • 声明式导航

    上面已经讲述了路由的声明,那么接下来的工作就是,如何让程序动态展示路由组件,以及如何正确传值,这就是导航。

    • 占位符 <router-view>

      导航需要动态展示组件的位置。

    • 路由链接 <router-link>

      类似 <a> 标签,点击即可跳转到响应路由 hash 地址,进而在对应的 <router-view> 占位符渲染相应组件。

      可通过 :to 属性指定跳转路由及参数。

      <template>
          <router-link :to="'/about'">关于</router-link>
          <router-link :to="'/movie/' + movieId">电影</router-link>
      </template>
      
    • 路由高亮

      路由高亮即对点击选中的路由连接 <router-link> 组件的选择器和样式控制问题,有两种方式。

      • 默认的高亮 class 类

        在点击路由链接后,vue 会给对应的 <router-link> 元素添加一个 router-link-active 类名选择器。

        <style lang='less' scoped>
        .router-link-active {
            background-color: grey,
            font-weight: bold
        }
        </style>
        
      • 自定义路由高亮 class 类

        当然,我们也可以在路由定义处,去自定义高亮 class 类名

        const router = new createRouter({
            history: createWebHashHistory(),
            linkActiveClass: 'router-active'
            routes: [
                { path: '/', redirect: '/movie'},
                ...
        
  • 编程式导航

    相较于声明式导航使用路由链接进行导航,编程式导航调用 api 实现导航,类似于原生 js 中的 location.href 函数。

    常用的编程式导航如下:

    1. $router.push()

      跳转到指定 hash 地址

      <button @click='gotoMovie(3)'>跳转 Movie 3</button>
      
      methods: {
      	gotoMovei(id) {
      		this.$router.push('/movie/${id}')
          }
      }
      
    2. $router.go()

      表示前进,后退,常用的是 $router.go(-1) 表示返回上一页

  • 命名路由

    当嵌套路由过长时,可以使用 name 属性给路由规则命名,命名路由的 name 属性必须唯一。

    // 命名路由
    const router = createRouter({
        history: createWebHistory(),
        routes: [
            {
                path: '/movie/:id',
                name: 'mov',
                component: Movie,
                props: true
            }
        ]
    })
    
    // 声明式导航
    <router-link :to="{ name: 'mov', params: { id: 3} }">跳转到电影</router-link>
    
    // 编程式导航
    <button @click="gotoMovie(3)">跳转</button>
    
    methods: {
    	gotoMovie(id) {
    		this.$router.push({ name: 'mov', params: { id: 3} })
        }
    }
    
  • 导航守卫

    导航守卫是用来控制路由的访问权限的。

    1. 定义导航守卫

      router 对象的 beforeEach() 函数接收一个函数对象参数,每次拦截到路由请求都会调用其中函数。

      同时,该参数函数对象,含有三个形参

      to :目标路由对象,即 routes 数组中的元素对象。

      from :离开的路由

      next :放行函数,如果该形参缺省,则表示都方向。否则,只有调用了 next() 才能放行。

    2. next 函数的三种常用法

      next() 直接放行

      next(false) 禁止放行,停留在当前页面

      next('/login') 强制路由重定向

    3. 案例-验证 token 并放行或拒绝访问

      const router = createRouter({...})
      
      router.beforeEach((to, from, next) => {
      
          const token = localStorage.getItem('token')
      
          if (to.path === 'manage' && !verify(token)) {
              next('/login')
          } else {
              next()
          }
      })
      

axios 配置和拦截器

  • 全局导入,指定别名 $http

  • 配置 baseURL

    vue 3

    // main.js
    import { createAPP } from 'vue'
    import App from './App.vue'
    import axios from 'axios'
    
    const app = createAPP(App)
    
    app.config.globalProperties.$http = axios
    axios.defaults.baseURL = 'https://example.api.com'
    
    app.mount('#app')
    

    vue 2

    import Vue from 'vue'
    import App from './App.vue'
    import axios from 'axios'
    
    Vue.prototype.$http = axios
    axios.defaults.baseURL = 'https://api.example.com'
    
    new Vue({
        render: h => h(App)
    }).$mount('#app')
    
  • 拦截器

    在每次发起 ajax 请求和接收响应时自动触发,常用的场景就是 token 验证和 loading 效果

    • 请求拦截器

      axios.interceptors.request.use(成功的回调, 失败的回调)

      // main.js
      
      axios.interceptors.request.use(config => {
          // 成功开始请求的回调
          ...
          return config;
      }, error => {
          return Promise.reject(error)
      })
      
    • 响应拦截器

      axios.interceptors.response.use(成功的回调, 失败的回调)

      // main.js
      
      axios.interceptors.resopnse.use(response => {
          // 成功响应的回调
          ...
          return reponse
      }, error => {
          return Promise.reject(error)
      })
      
    • 场景演示

      // main.js
      import loading from 'element-ui'
      
      let loadingInstance = null
      
      axios.interceptors.request.use(config => {
      
          // 1. 添加 token 请求头
          config.headers.Authorization = 'Bearer xxx'
          // 2. 添加 loading 展示效果,在响应拦截器一同演示
          loadingInstance = loading.service({ fullscreen: true })
      
          return config;
      })
      
      axios.interceptors.resopnse.use(response => {
      
          // 响应成功,关闭 loading 展示效果
          loadingInstance.close()
      
          return reponse
      })
      
  • 跨域代理 proxy

    域是同源策略的反面,同源策略就是同协议、同域名、同端口,是浏览器的一个安全行为策略,不符合同源策略的 ajax 请求,将会发送跨域请求,浏览器根据服务器响应头中的跨域资源共享 (CORS) 规则而决定是否允许发送跨域请求。

    为解决开发时,本地运行的项目域名和生产环境 api 的接口域名端口不一致,导致的跨域请求限制问题 ,vue 提供了一个内置代理,会自动的将本地项目地址 localhost 不具有的 api 通过代理转发到真实的 api 地址,从而绕过了前端浏览器中的同源策略。(可能用到了请求拦截器)

    • 配置跨域代理的真实地址

      // vue.config.js
      module.exports = {
          devServer: {
              proxy: 'https://api.real.com'
          }
      }
      
      // main.js 与此同时将 aixos 的 baseURL 修改为本地项目地址
      axios.defaults.baseURL = 'http://localhost:8080'
      
    
    

组件库 element-ui

组件库的使用查看文档即可,主要关注以下几个点

  • 完整引入

  • 按需引入

  • 对组件库导入的封装

    可以将组件库的导入代码,从 main.js 中抽离出来,单独封装到 element-ui/index.js 中,使用时导入即可。

    // 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'