Vue基础

VUE简介

1.什么是VUE

是一套用于构建用户界面的前端框架。

2.vue框架的主要特性

2.1 数据驱动试图

在使用了 vue 的页面中,vue 会监听数据的变化,从而自动重新渲染页面的结构


2.2 双向数据绑定

在填写表单时,双向数据绑定可以辅助开发者在不操作 DOM 的前提下,自动把用户填写的内容同步到数据源中。

好处:开发者不再需要手动操作 DOM 元素,来获取表单元素最新的值!

2.3 MVVM

MVVM 是 vue 实现数据驱动视图和双向数据绑定的核心原理。MVVM 指的是 Model、View 和 ViewModel,它把每个 HTML 页面都拆分成了这三个部分

MVVM工作原理

VUE的基本使用

VUE的指令与过滤器

1.指令的概念

指令(Directives)是 vue 为开发者提供的模板语法,用于辅助开发者渲染页面的基本结构。

  • vue 中的指令按照不同的用途可以分为如下 6 大类:
    ① 内容渲染指令
    ② 属性绑定指令
    ③ 事件绑定指令
    ④ 双向绑定指令
    ⑤ 条件渲染指令
    ⑥ 列表渲染指令

1.1 内容渲染指令

内容渲染指令用来辅助开发者渲染 DOM 元素的文本内容。
常用的内容渲染指令有如下 3 个:

  • v-text

    <div class="app">
        <p v-text="username">姓名:</p>
        <!-- 页面显示为 zhangsan -->
    </div>

    <script>
        // 创建 Vue 的实例对象
        const vm = new Vue({
            // el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
            el: '.app',
            // data 对象就是要渲染到页面上的数据
            data: {
                username: 'zhangsan'
            }
        })
    </script>

v-text 指令的缺点:会覆盖元素内部原有的内容!

  • {{ }}

vue 提供的 {{ }} 语法,专门用来解决 v-text 会覆盖默认文本内容的问题。
这种 {{ }} 语法的专业名称是插值表达式(英文名为:Mustache)

        <p>姓名:{{username}}</p>
        <!-- 页面显示为姓名:zhangsan -->
  • v-html

v-html 指令的作用:可以把带有标签的字符串,渲染成真正的 HTML 内容!
v-html会替换掉节点中所有的内容

严重注意:v-html有安全性问题!!!!

在网站上动态渲染任意HTML是非常危险的,容易导致XSS攻击。
一定要在可信的内容上使用v-html,永不要用在用户提交的内容上!
    <div class="app">
        <p v-html="info">欢迎语:</p>
        <!-- 页面显示为 带样式的你好 -->
    </div>

    <script>
        // 创建 Vue 的实例对象
        const vm = new Vue({
            // el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
            el: '.app',
            // data 对象就是要渲染到页面上的数据
            data: {
                username: 'zhangsan',
                info: '<h4 style=color:red;>你好</h4>'
            }
        })
    </script>

el属性的使用注意点

el: 'p'
这样写只会控制第一个 p标签

1.2 属性绑定指令

如果需要为元素的属性动态绑定属性值,则需要用到 v-bind 属性绑定指令(v-bind: + 属性名)

    <div class="app">
        <input type="text" v-bind:placeholder="tips">
        <img v-bind:src="photo" alt="">
    </div>

    <script>
        // 创建 Vue 的实例对象
        const vm = new Vue({
            // el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
            el: '.app',
            // data 对象就是要渲染到页面上的数据
            data: {
                tips: '请输入用户名',
                photo: 'https://cn.vuejs.org/images/logo.svg'
            }
        })
    </script>

注:v-bind: 也可以简写为:

在插值和属性中编写js表达式的运算
        {{number + 1}}

        {{ok ?'YES':'NO' }}

        {{message.split('').reverse().join('')}}

        <div v-bind:id="'list-' + id"></div>
        <!-- id=1,即list-1,id=2即list-2 -->

1.3 事件绑定指令

绑定事件

vue 提供了 v-on 事件绑定指令,用来辅助程序员为 DOM 元素绑定事件监听
格式为:v-on:事件="函数名"

    <div class="app">
        <p>count的数量是:{{ count }}</p>
        <button v-on:click="add">+1</button>
    </div>

注:v-on:可以简写为@

事件处理函数

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

    <script>
        // 创建 Vue 的实例对象
        const vm = new Vue({
            el: '.app',
            data: {
                count: 0
            },
            //methods 的作用是定义事件的处理函数
            methods: {
                //add: function () {}
                // 可以简写为
                add(){}
            }
        })
    </script>

通过this访问data中的数据

在上述代码中 data 里的 count 是属于实例对象 vm 的一个属性

因此想要让每次事件触发时 count+1,可以做如下操作:

add() {
    /* vm.count++
    因为 this 是指向实例对象 vm 的,所以可以替换为 this.count */

    this.count++;
}

绑定事件并传参

<button v-on:click="add(1)">+1</button>

//将 1 传给参数n
add(n) {
    this.count+=n
}

$event

  • v-on:click="add"没有传参,则默认函数接收到的是事件对象 e
    即:

    add(e) {
        this.count+=1
        e.target.style.backgroundColor='red'
    }
    

    e.target是事件对象

  • 如果有传参, vue 提供的内置变量 $event,用来表示原生的事件对象 e
    可以将它作为参数传递给函数

    <button v-on:click="add(1,$event)">+1</button>
    
    add(n, e) {
        this.count +=n
        e.target.style.backgroundColor = 'red'
    }
    

事件修饰符

事件修饰符 说明
.prevent 阻止默认行为(例如:阻止 a 连接的跳转、阻止表单的提交等
.stop 阻止事件冒泡
.capture 以捕获模式触发当前的事件处理函数
.once 绑定的事件只触发1次
.self 只有在 event.target 是当前元素自身时触发事件处理函数
<a href="http://www.baidu.com" @click.prevent="show">百度</a>

按键修饰符

在监听键盘事件时,我们经常需要判断详细的按键。此时,可以为键盘相关的事件添加按键修饰符

1.4 双向绑定指令

vue 提供了 v-model 双向数据绑定指令,用来辅助开发者在不操作 DOM 的前提下,快速获取表单的数据

<input type="text" v-model="username">

v-model是双向绑定。v-bild是单向绑定

注:v-model 只应用于表单元素

        <select v-model="chance">
            <option value="1">check</option>
            <option value="2">2</option>
            <option value="3">3</option>
            <option value="4">4</option>
        </select>


            data: {
                count: 0,
                chance: 0
            },

v-model的修饰符

修饰符 作用 示例
.number 自动将用户的输入值转为数值类型 <input v-model.number="age" />
.trim 自动过滤用户输入的首尾空白字符 <input v-model.trim="msg" />
.lazy 在文本框失焦时才更新数据 <input v-model.lazy="msg" />

1.5 条件渲染指令

条件渲染指令用来辅助开发者按需控制 DOM 的显示与隐藏。条件渲染指令有如下两个,分别是:

  • v-if: 指令会动态地创建或移除DOM 元素,从而控制元素在页面上的显示与隐藏;

  • v-show: 指令会动态为元素添加或移除 style="display: none;" 样式,从而控制元素的显示与隐藏;

v-else

v-else 指令必须配合 v-if 指令一起使用,否则它将不会被识别!

v-else-if

注意:v-else-if 指令必须配合 v-if 指令一起使用,否则它将不会被识别!

1.6 列表渲染指令

v-for

  • vue 提供了 v-for 列表渲染指令,用来辅助开发者基于一个数组来循环渲染一个列表结构。v-for 指令需要使用 i in items 形式的特殊语法,其中:
    • i 是被循环的每一项

    • items 是待循环的数组

<ul>
    <li v-for="i in list">id:{{i.id}} &nbsp 姓名:{{i.name}}</li>
</ul>
data: {
    list: [
        { id: 1, name: 'zhangsan' },
        { id: 2, name: 'lisi' }
    ]
}
  • v-for 指令还支持一个可选的第二个参数,即当前项的索引。语法格式为 (item, index) in items
            <tbody v-for="(i,index) in list">

                <tr>
                    <td>{{index}}</td>
                    <td>{{i.id}}</td>
                    <td>{{i.name}}</td>
                </tr>

            </tbody>

注:item 和 index 除了可以被子元素引用,还可以被自身引用

key

  • 官方建议:只要用到了 v-for 指令,那么一定要绑定一个 :key 属性

  • 而且,尽量把 id 作为 key 的值

  • 官方对 key 的值类型,是有要求的:字符串或数字类型,且具有唯一性(索引index不具有唯一性)

  • key 的值是千万不能重复的,否则会终端报错:Duplicate keys detected

            <tbody v-for="(i,index) in list" :key="i.id">

                <tr>
                    <td>{{index}}</td>
                    <td>{{i.id}}</td>
                    <td>{{i.name}}</td>
                </tr>

            </tbody>

2.过滤器

vue3没有过滤器

  • 过滤器(Filters)是 vue 为开发者提供的功能,常用于文本的格式化。

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

过滤器应该被添加在 JavaScript 表达式的尾部,由“管道符”进行调用

2.1 定义过滤器

在创建 vue 实例期间,可以在 filters 节点中定义过滤器

<!-- 在{{}}中通过 管道符‘|’ 调用 过滤器 capi,对message的值进行格式化 -->
<!-- 这里值是以过滤器的返回值为准 -->
<!-- 例如要将 message的值 的首字母大写 -->
<p>message的值:{{message | capi}}</p>

// filters与 data,el 平级
// 过滤器函数必须被定义到filters节点之下
// 过滤器本质上是函数
filters: {
    // 定义过滤器函数
    // 这里的参数val是管道符前面的值
    capi(val) {
        var first = val.slice(0, 1).toUpperCase()//截取字符串,变为大写字母
        val = first + val.slice(1)
       // 过滤器中一定要return一个返回值,渲染到插值的位置
        return val
    }
}

2.2 私有过滤器和全局

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

  • 如果希望在多个 vue 实例之间共享过滤器,则可以按照如下的格式定义全局过滤器:
    (此方法应该写在实例前,写在实例后会报错)

        // 全局过滤器-独立于每个 vm 实例之外
        /* Vue.filter()方法接收两个参数:
        第一个参数 是全局过滤器的名字
        第二个参数 是全局过滤器的“处理函数”,即管道符前面的值 */
        Vue.filter('capi', function (val) {
            var first = val.slice(0, 1).toUpperCase()//截取字符串,变为大写字母
            val = first + val.slice(1)
            // 过滤器中一定要return一个返回值,渲染到插值的位置
            return val
        })

注:如果全局过滤器和私有过滤器名字一致,此时调用的是全局过滤器

2.3 连续调用多个过滤器

2.4 过滤器传参

vue基础入门

1.侦听器

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

1.1 方法格式的侦听器

// 与data,el同级
// 所有的侦听器,都应该被定义到 watch 节点下
watch: {
// 监听 username值的变化
// 侦听器本质上是一个函数,要侦听哪个数据变化,就将那个数据名作为函数名
// newVal 是变化后的新值,oldVal 是变化之前的旧值
    username(newVal, oldVal) {
    console.log(newVal, oldVal);
    }
}

查询用户名是否被占用

            <input type="text" v-model.lazy="username">

            watch: {
                // 监听 username值的变化
                // 侦听器本质上是一个函数,要侦听哪个数据变化,就将那个数据名作为函数名
                // newVal 是变化后的新值,oldVal 是变化之前的旧值
                username(newVal) {
                    if (newVal === '') return
                    // 调用ajax发起请求,判断 username 是否被占用
                    $.ajax({
                        type: 'get',//请求的方式,GET或 POST
                        url: 'https://www.escook.cn/api/finduser/' + newVal,//请求的url地址
                        success: function (result) {
                            console.log(result);
                        }//请求成功后的回调函数
                    })

                }
  • 缺点1:无法在刚进入页面的时候,自动触发!!!

  • 缺点2:如果侦听的是一个对象,如果对象中的属性发生了变化,不会触发侦听器!!!

1.2 对象格式的侦听器

  • 好处1:可以通过 immediate 选项,让侦听器自动触发!!!

    默认情况下,组件在初次加载完毕后不会调用 watch 侦听器。如果想让 watch 侦听器立即被调用,则需要使用 immediate 选项

//定义对象格式的监听器
username: {
    // 监听器的处理函数,只要username发生变化 就执行函数
    handler(newVal) {
        if (newVal === '') return
        // 调用ajax发起请求,判断 username 是否被占用
        $.ajax({
            type: 'get',//请求的方式,GET或 POST
            url: 'https://www.escook.cn/api/finduser/' + newVal,//请求的url地址
            success: function (result) {
                console.log(result);
            }//请求成功后的回调函数
        })
    },
    // 表示页面初次渲染好之后,就立即触发当前的 watch 侦听器
    immediate: true
}
  • 好处2:可以通过 deep 选项,让侦听器深度监听对象中每个属性的变化!!!
//如果侦听的是一个对象,如果对象中的属性发生了变化,不会触发侦听器
data: {
    info:{
        username: 'zhangsan'
    }               
},
info: {
    // 监听器的处理函数,只要username发生变化 就执行函数
    handler(newVal) {
        console.log(newVal);
    },
    deep: true
}

另一种写法:

// 如果侦听的是一个对象的属性,不用深度监听,也可以有如下写法
'info.username'(newVal) {
    console.log(newVal);
}

补充:模板嵌套字

在普通字符串中嵌入表达式

console.log('Fifteen is ' + (a + b) + ' and \n not ' + (2 * a + b) + '.');
// "Fifteen is 15 and
// not 20."

现在通过模板字符串,${表达式}

console.log(`Fifteen is ${a + b} and 
not ${2 * a + b}.`);
// "Fifteen is 15 and
// not 20."

2.计算属性

  • 计算属性指的是通过一系列运算之后,最终得到一个属性值。

  • 这个动态计算出来的属性值可以被模板结构或 methods 方法使用

        <div class="box" :style="{backgroundColor:rgb}">
            {{rgb}}
        </div>


// 所有的计算属性,都要定义到 computed 节点之下
// 计算属性在定义的时候,要定义成“方法格式”
computed: {
    // rgb 作为一个计算属性,被定义成了方法格式,
    // 最终,在这个方法中,要返回一个生成好的 rgb(x,x,x) 的字符串
    rgb() {
        return `rgb(${this.r}, ${this.g}, ${this.b})`
    }
}

优点:
1.实现了某些代码得到复用

2.只要计算属性中依赖的数据源变化了,则计算属性会自动重新求值!

3.vue-cil

3.1 什么是单页面应用程序

单页面应用程序(英文名:Single Page Application)简称 SPA,顾名思义,指的是一个 Web 网站中只有唯一的一个HTML 页面,所有的功能与交互都在这唯一的一个页面内完成

3.2 什么是 vue-cli

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

中文官网:https://cli.vuejs.org/zh/

3.3 基于 vue-cli 快速生成工程化的 Vue 项目:

vue create 项目的名称

( * ) Babel:解决js兼容性
( ) TypeScript:微软的一套脚本语言,现在建议不选
( ) Progressive Web App (PWA) Support:渐进式的web框架
( ) Router:路由
( ) Vuex
( * ) CSS Pre-processors:CSS预处理器
( ) Linter / Formatter:约束代码风格
( ) Unit Testing:单元测试
( ) E2E Testing:e2e测试


选择第一项

预设名称 first_demo

3.4 项目目录

src目录

  • assets 文件夹:存放项目中用到的静态资源文件,例如:css 样式表、图片资源

  • components 文件夹:程序员封装的、可复用的组件,都要放到 components 目录下

  • main.js 是项目的入口文件。整个项目的运行,要先执行 main.js

  • App.vue 是项目的根组件。

3.5 vue 项目的运行流程

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

  • 其中:
    ① App.vue 用来编写待渲染的模板结构
    ② index.html 中需要预留一个 el 区域
    ③ main.js 把 App.vue 渲染到了 index.html 所预留的区域中
import Vue from 'vue'
//导入要渲染的vue文件
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  // 把 render 函数指定的组件,渲染到HTML页面中
  // h(要渲染的文件名)
  render: h => h(App),
}).$mount('#app')
  // $mount('#app')的作用等同于 el:'#app'
  // 将 id='app'中的内容用 vue文件 替换掉

4.vue组件

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

4.1 vue 中的组件化开发

  • vue 是一个支持组件化开发的前端框架。

  • vue 中规定:组件的后缀名是.vue。

  • 之前接触到的 App.vue 文件本质上就是一个 vue 的组件。

4.2 vue 组件的三个组成部分

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

  • template -> 组件的模板结构
  • template 是 vue 提供的容器标签,只起到包裹性质的作用,它不会被渲染为真正的 DOM 元素
  • template 中只能包含一个的根节点(即 最外层代码折起来后,template中只有包含一个元素)
  • script -> 组件的 JavaScript 行为

vue 规定:.vue 组件中的 data 必须是一个函数,不能直接指向一个数据对象。否则会导致多个组件实例共用同一份数据的问题,请参考官方给出的示例:
https://cn.vuejs.org/v2/guide/components.html#data-必须是一个函数

  • style -> 组件的样式

要想启用less,结构应为:

<style lang="less">
</style>

其中,每个组件中必须包含 template 模板

<template>
  <div>
    <p>---{{ username }}</p>

    <button @click="change">按钮</button>
  </div>
</template>

// script -> 组件的 JavaScript 行为
<script>
// 默认导出。这是固定写法
export default {
  // data数据源
  // 注意:vue组件中的data不能指向对象
  // 组件中的data必须是一个函数
  data() {
    // 这个return出去的{ }中,可以定义数据
    return {
      username: "admin",
    };
  },

  // 定义函数
  methods: {
    change() {
      //在组件中,this就表示当前组件的实例对象
      this.username = "zhangsan";
    },
  },

 // 当前组件中的侦听器
  watch: {},
  // 当前组件中的计算属性
  computed: {},
  // 当前组件中的过滤器
  filter: {},

};
</script>

4.3 组件之间的父子关系

使用组件的三个步骤

  • 步骤1:使用 import 语法导入需要的组件

  • 步骤2:使用 components 节点注册组件

  • 步骤3:以标签形式使用刚才注册的组件

通过 components 注册的是私有子组件

例如:

  • 在组件 A 的 components 节点下,注册了组件 F。

  • 则组件 F 只能用在组件 A 中;不能被用在组件C 中。

可以使用 name配置项 指定 组件在 开发者工具 中呈现的名字。

4.4 注册全局组件

在 vue 项目的 main.js 入口文件中,通过 Vue.component() 方法,可以注册全局组件

  • 建议注册组件的名称都是大写开头

4.5 组件的props

props 是组件的自定义属性

注:

  • props属性是只读的

  • 要想修改 props 的值,可以把 props 的值转存到 data 中,因为 data 中的数据都是可读可写的!

<template>
  <div>
    <p>这是count组件</p>
    <p>age的值是:{{ age }}</p>
    <button @click="age += 1">+1</button>
  </div>
</template>
<script>
export default {
  // props是自定义属性,允许使用者通过自定义属性 为当前组件指定初始值
  props: ["init"],
  data() {
    return {
      age: this.init,
    };
  },
};
</script>
//自定义属性的赋值是字符串属性,想要变为数字型 应该在属性前加 :(即v-bind属性)
应用于其他组件:<Count :init="3"></Count>

props 的 default 默认值

在声明自定义属性时,可以通过 default 来定义属性的默认值

props 的 type 值类型

在声明自定义属性时,可以通过 type 来定义属性的值类型

props 的 required 必填项

在声明自定义属性时,可以通过 required 选项,将属性设置为必填项,强制用户必须传递属性的值

4.6 组件之间的样式冲突问题

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

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

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

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

4.7 如何解决组件样式冲突的问题

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

  • 2. style 节点的 scoped 属性

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

    <style lang="less" scoped>
    
    </style>
    

scoped 属性的缺点:如果给当前组件的 style 节点添加了 scoped 属性,则当前组件的样式对其子组件是不生效的。

如果想让某些样式对子组件生效,可以使用 /deep/ 深度选择器

<style lang="less" scoped>

//加deep之前 h5[data-v-3c83f0b7]
//加deep之后 [data-v-3c83f0b7] h5

// 当使用第三方组件库的时候,如果有修改第三方组件默认样式的需求,需要用到 /deep/
/deep/ p {
  color: pink;
}
</style>

vue组件库:

4.8 vue组件的实例对象


vue文件是 经过上图的包,将每个vue文件解析转换为js文件,再在浏览器执行

6.组件之间的数据共享

6.1父子组件之间的数据共享

父子组件之间的数据共享又分为:

① 父 -> 子共享数据

  • 父组件向子组件共享数据需要使用自定义属性(props)

  • 父组件定义数据,子组件通过自定义属性来接收数据


//父组件
<template>
  <div>
    <h1>App根组件</h1>
    <Son :msg="message" :init="info"></Son>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: "2022年",
      info: { mon: "九月", day: "22日" },
    };
  },
};
</script>
//子组件
<template>
  <div>
    <h2>son组件</h2>
    <p>今天是:{{ msg }}-{{ init.mon }}-{{ init.day }}</p>
  </div>
</template>

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

页面显示结果:

② 子 -> 父共享数据

  • 子组件向父组件共享数据使用自定义事件

//子组件
methods: {
    add() {
      this.count += 1;
      //修改数据时,通过 $emit(自定义事件名,要传的数据)触发自定义事件
      this.$emit("numchange", this.count);
    },
  },
//父组件
<p>{{ countFromSon }}</p>

<!-- 绑定自定义事件 -->
<!--  这个自定义事件是通过子组件的点击事件触发的 -->
<Son2 @numchange="getnewcount"></Son2>

export default {
  data() {
    return {
      // 定义countFromSon来接收从子组件传来的数据
      countFromSon: 0,
    };
  },

  methods: {
    // 定义自定义事件函数
    // val形参表示新值
 
    getnewcount(val) {
      this.countFromSon = val;
      console.log(val);
    },
  },
  • 过程:点击子组件的按钮 -> 触发点击事件 -> 执行点击事件函数 -> 执行
    $emit() -> 触发自定义事件 -> 父组件绑定自定义事件 -> 执行父组件中自定义事件 -> 将子组件中的新值this.count通过val传给父组件

6.2 兄弟组件之间的数据共享

在 vue2.x 中,兄弟组件之间数据共享的方案是 EventBus

EventBus 的使用步骤

  • ① 创建 eventBus.js 模块,并向外共享一个 Vue 的实例对象

  • ② 在数据发送方,调用 bus.$emit('事件名称', 要发送的数据) 方法触发自定义事件

  • ③ 在数据接收方,调用 bus.$on('事件名称', 事件处理函数) 方法注册一个自定义事件

7.ref 引用

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

  • 每个 vue 的组件实例上,都包含一个 $refs 对象,里面存储着对应的 DOM 元素或组件的引用。默认情况下,组件的 $refs 指向一个空对象。

使用 ref 引用 DOM 元素

如果想要使用 ref 引用页面上的 DOM 元素,则可以按照如下的方式进行操作

<h1 ref="myh111">App根组件</h1>

使用格式:this.$refs.myh111
<template>
  <div>
    <h1 ref="myh111">App根组件</h1>
    <button @click="btn1">点击</button>
  </div>
</template>

<script>
export default {
  methods: {
    btn1() {
      this.$refs.myh111.style.color = "red";
    },
  },
};
</script>

使用 ref 引用组件实例

使用ref属性,为相应的‘组件’添加引用名称

控制文本框和按钮的按需切换

通过布尔值 inputVisible 来控制组件中的文本框与按钮的按需切换

尚硅谷

Object.defineProperty

let number = 18;
let person = {
  name: "zhangsan",
  sex: "男",
};

// 使 person和 number产生了联系,用 get 读取,用 set 修改
Object.defineProperty(person, "age", {
  // value=18,
  // enumerable:true,//控制属性是否可以枚举
  // writable:true,//控制属性是否可以被修改
  // configurable:true//控制属性是否可以被删除

  // 当有人读取person的age属性时,get函数(getter)就会被调用,且返回值就是age的值
  get() {
    console.log("读取了get属性");
    return number;
  },
  set() {
    console.log("修改了get属性,age=" + value);
    number = value;
  },
});
console.log(person);

数据代理

  • 通过Object.defineProperty()把data对象中所有属性添加到vm上。

  • 为每一个添加到vm上的属性,都指定一个getter/setter。

  • 在getter/setter内部去操作(读/写)data中对应的属性。

事件处理

<button @click="showInfo2($event,66)">点我提示信息2(传参)</button>

        methods:{
            // 如果vue模板没有写event,会自动传 event 给函数
            showInfo1(event){
                // console.log(event.target.innerText)
                // console.log(this) //此处的this是vm
                alert('同学你好!')
            },
            showInfo2(event,number){
                console.log(event,number)
                // console.log(event.target.innerText)
                // console.log(this) //此处的this是vm
                alert('同学你好!!')
            }

两个重要的小原则:

  • 1.所被Vue管理的函数,最好写成普通函数,这样this的指向才是vm 或 组件实例对象

  • 2.所有不被Vue所管理的函数(定时器的回调函数、ajax的回调函数等、Promise的回调函数),最好写成箭头函数,这样this的指向才是vm 或 组件实例对象

绑定样式

class

写法::class=“xxx” xxx可以是字符串、对象、数。

所以分为三种写法,字符串写法,数组写法,对象写法

    <div class="app">
        <!-- 字符串写法适用于:类名不确定,要动态获取。 -->
        <div class="basic" :class="a">
            {{name}}
        </div>
        <br>
        <!-- 数组写法适用于:要绑定多个样式,个数不确定,名字也不确定。 -->
        <div class="basic" :class="classArr">
            好好学习
        </div>
        <br>
        <!-- 对象写法适用于:要绑定多个样式,个数确定,名字也确定,但不确定用不用。 -->
        <div class="basic" :class="objClass">
            天天向上
        </div>
    </div>

            data: {
                name: 'zhangsan',
                a: 'normal',
                classArr: ['color1', 'color2', 'color3'],
                objClass: {
                    color1: false,
                    color2: false
                }
            },

style

通过动态绑定 style,可以通过参数动态的改变元素的样式。
对象写法,数组写法

    <!-- 绑定style样式--对象写法 -->
<div class="basic" :style="styleObj">{{name}}</div>
    <!-- 绑定style样式--数组写法 -->
	<div class="basic" :style="styleArr">{{name}}</div>

模板字符串写法:

<h2 :style="{css属性名:值}"> {<!-- \-->{message}} </h2>

    <p>font-size:</p>
    <input type="text" v-model="ifname" />
    <input type="range" v-model="ifname" />
    <p :style="{ fontSize: ifname + 'px' }">文本</p>
<script>
export default {
  data() {
    return {
      ifname: 18,
    };
  },
  methods: {
    btn1() {
      this.$refs.myh111.style.color = "red";
    },
  },
};
</script>

数组方式:

<div id="app">
    <h2 :style="[fontSize,bgc]">测试文本</h2>
  </div>

  <script>
    let vm = new Vue({
      el: "#app",
      data: {
        fontSize: { fontSize: "48px" },
        bgc: { backgroundColor: "red" },
      },
    });
  </script>

详情可以查看这位朋友的:https://blog.csdn.net/hangao233/article/details/123990192?spm=1001.2014.3001.5502

vue监视数据

Vue监视数据的原理:
1. vue会监视data中所有层次的数据。

2. 如何监测对象中的数据?
	通过setter实现监视,且要在new Vue时就传入要监测的数据。
		(1).对象中后追加的属性,Vue默认不做响应式处理
		(2).如需给后添加的属性做响应式,请使用如下API:
		Vue.set(target,propertyName/index,value) 或 
		vm.$set(target,propertyName/index,value)

3. 如何监测数组中的数据?
	通过包裹数组更新元素的方法实现,本质就是做了两件事:
	(1).调用原生对应的方法对数组进行更新。
	(2).重新解析模板,进而更新页面。

4.在Vue修改数组中的某个元素一定要用如下方法:
(1).使用这些API:push()、pop()、shift()、unshift()、splice()、sort()、reverse()
(2).Vue.set() 或 vm.$set()
				
特别注意:Vue.set() 和 vm.$set() 不能给vm 或 vm的根数据对象 添加属性!!!

收集表单数据:

1.若:<input type="text"/>,则v-model收集的是value值,用户输入的就是value值。
2.若:<input type="radio"/>,则v-model收集的是value值,且要给标签配置value值。
3.若:<input type="checkbox"/>
	(1).没有配置input的value属性,那么收集的就是checked(勾选 or 未勾选,是布尔值)
	(2).配置input的value属性:
		① v-model的初始值是非数组,那么收集的就是checked(勾选 or 未勾选,是布尔值)
		② v-model的初始值是数组,那么收集的的就是value组成的数组
备注:v-model的三个修饰符:
	lazy:失去焦点再收集数据
	number:输入字符串转为有效的数字
	trim:输入首尾空格过滤

v-cloak指令(没有值):

  • 本质是一个特殊属性,Vue实例创建完毕并接管容器后,会删掉v-cloak属性。

  • 使用css配合v-cloak可以解决网速慢时页面展示出{{xxx}}(未经解析的模板)的问题。

<style>
    [v-cloak]{
        display:none;
    }
</style>
<!-- 准备好一个容器-->
<div id="root">
    <h2 v-cloak>{{name}}</h2>
</div>
<script type="text/javascript" src="http://localhost:8080/resource/5s/vue.js"></script>

<script type="text/javascript">
    console.log(1)
    Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。

    new Vue({
        el:'#root',
        data:{
            name:'尚硅谷'
        }
    })
</script>

v-pre指令:(比较没用)

跳过其所在节点的编译过程
可利用它跳过:没有使用指令语法、没有使用插值语法的节点,会加快编译

自定义指令

局部指令

使用 directives 选项完成了指令的局部注册。

 <!-- 定义一个v-big指令,和v-text功能类似,但会把绑定的数值放大10倍。 -->
        <h1>放大10倍的值:</h1>
        <p v-big="n"></p>
        
    <script>
        const vm = new Vue({
            data: {
                n: 1
            },
            directives: {
            //big函数何时会被调用?(没有配置对象时)
            //1.指令与元素成功绑定时(一上来)。2.指令所在的模板被重新解析时。
                big(element, binding) {
                    element.innerText = binding.value * 10
                }
            }
        })
    </script>

全局指令

将一个自定义指令全局注册到应用层级也是一种常见的做法:
使用Vue.directive()定义

    //定义全局指令
//第一个参数为指令名,第二个参数为 配置对象 或 函数
    Vue.directive('fbind', {
        // 指令与元素成功绑定时(一上来)
        bind(element, binding){
            element.value = binding.value
        },
        // 指令所在元素被插入页面时
        inserted(element, binding){
            element.focus()
        },
        // 指令所在的模板被重新解析时
        update(element, binding){
            element.value = binding.value
        }
    })
Vue.directive('big', function (element, binding) {
            element.innerText = binding.value * 10
        })

自定义指令总结:

    一、定义语法:
    (1).局部指令:
    new Vue({
      directives:{指令名:配置对象} 或 directives{指令名:回调函数}
    })
    (2).全局指令:
    Vue.directive(指令名,配置对象) 或 Vue.directive(指令名,回调函数)
    
    二、配置对象中常用的3个回调:
    (1).bind:指令与元素成功绑定时调用。
    (2).inserted:指令所在元素被插入页面时调用。
    (3).update:指令所在模板结构被重新解析时调用。
    
    三、备注:
    1.指令定义时不加v-,但使用时要加v-;
    2.指令名如果是多个单词,要使用kebab-case命名方式,不要用camelCase命名。

生命周期

5.1 生命周期 & 生命周期函数

  • 生命周期(Life Cycle)是指一个组件从创建 -> 运行 -> 销毁的整个阶段,强调的是一个时间段。

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

注意:生命周期强调的是时间段,生命周期函数强调的是时间点

debugger 断点,放到哪个函数中,就停在哪里

创建前 刚创建 渲染前 刚渲染

  • 1. beforeCreate(创建前):

数据监测(getter和setter)和初始化事件还未开始,此时 data 的响应式追踪、event/watcher 都还没有被设置,也就是说不能访问到data、computed、watch、methods上的方法和数据。

  • 2. created(创建后):

实例创建完成,实例上配置的 options 包括 data、computed、watch、methods 等都配置完成,但是此时渲染得节点还未挂载到 DOM,所以不能访问到 $el属性。

  • 3. beforeMount(挂载前):

在挂载开始之前被调用,相关的render函数首次被调用。此阶段Vue开始解析模板,生成虚拟DOM存在内存中,还没有把虚拟DOM转换成真实DOM,插入页面中。所以网页不能显示解析好的内容。

  • 4. mounted(挂载后):(重)

在el被新创建的 vm.$el(就是真实DOM的拷贝)替换,并挂载到实例上去之后调用(将内存中的虚拟DOM转为真实DOM,真实DOM插入页面)。此时页面中呈现的是经过Vue编译的DOM,这时在这个钩子函数中对DOM的操作可以有效,但要尽量避免。一般在这个阶段进行:开启定时器,发送网络请求,订阅消息,绑定自定义事件等等

  • 5. beforeUpdate(更新前):

响应式数据更新时调用,此时虽然响应式数据更新了,但是对应的真实 DOM 还没有被渲染(数据是新的,但页面是旧的,页面和数据没保持同步呢)。

  • 6. updated(更新后) :

在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。此时 DOM 已经根据响应式数据的变化更新了。调用时,组件 DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。

当调用vm.$destroy()时,组件会进入销毁阶段

  • 7. beforeDestroy(销毁前):(重)

实例销毁之前调用。这一步,实例仍然完全可用,this 仍能获取到实例。在这个阶段一般进行关闭定时器,取消订阅消息,解绑自定义事件。

  • 8. destroyed(销毁后):

实例销毁后调用,调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务端渲染期间不被调用。

注:可以参考 vue 官方文档给出的“生命周期图示”,进一步理解组件生命周期执行的过程:
https://cn.vuejs.org/v2/guide/instance.html#生命周期图示


总结:

    常用的生命周期钩子:
    1.mounted: 发送ajax请求、启动定时器、绑定自定义事件、订阅消息等【初始化操作】。
    2.beforeDestroy: 清除定时器、解绑自定义事件、取消订阅消息等【收尾工作】。

    关于销毁Vue实例
    1.销毁后借助Vue开发者工具看不到任何信息。
    2.销毁后自定义事件会失效,但原生DOM事件依然有效。
    3.一般不会在beforeDestroy操作数据,因为即便操作数据,也不会再触发更新流程了。

VueComponent

关于VueComponent:
  1.school组件本质是一个名为VueComponent的构造函数,且不是程序员定义的,是Vue.extend生成的。

  2.使用组件时,创建构造函数
    我们只需要写<school />或<school></school>,Vue解析时会帮我们创建school组件的实例对象,即Vue帮我们执行的:new VueComponent(options)。

  3.特别注意:每次调用Vue.extend,返回的都是一个全新的VueComponent!!!!

  4.关于this指向:
    (1).组件配置中:
      data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【VueComponent实例对象】。
    (2).new Vue(options)配置中:
      data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【Vue实例对象】。

  5.VueComponent的实例对象,以后简称vc(也可称之为:组件实例对象)。
  Vue的实例对象,以后简称vm。

一个重要的内置关系

一个重要的内置关系:VueComponent.prototype.proto === Vue.prototype
为什么要有这个关系:让组件实例对象(vc)可以访问到 Vue原型上的属性、方法。

脚手架

使用前置:

第一步(没有安装过的执行):全局安装 @vue/cli

 npm install -g @vue/cli

第二步:切换到要创建项目的目录,然后使用命令创建项目

 vue create xxxxx

第三步:启动项目

 npm run serve

脚手架文件结构

├── node_modules 
├── public
│   ├── favicon.ico: 页签图标
│   └── index.html: 主页面
├── src
│   ├── assets: 存放静态资源
│   │   └── logo.png
│   │── component: 存放组件
│   │   └── HelloWorld.vue
│   │── App.vue: 汇总所有组件
│   │── main.js: 入口文件
├── .gitignore: git版本管制忽略的配置
├── babel.config.js: babel的配置文件
├── package.json: 应用包配置文件 
├── README.md: 应用描述文件
├── package-lock.json:包版本控制文件

index.html:

修改脚手架的默认配置

使用vue inspect > output.js可以查看到Vue脚手架的默认配置。

使用vue.config.js可以对脚手架进行个性化定制,详情见:https://cli.vuejs.org/zh

ref属性

  • 1.被用来给 元素 或 子组件 注册引用信息(id的替代者)

  • 2.应用在 html标签上 获取的是真实DOM元素,

    应用在 组件标签上 是组件实例对象(vc)

使用方式:
打标识:<h1 ref="xxx">.....</h1>或 <School ref="xxx"></School>
获取:this.$refs.xxx

props配置项

1.功能:让组件接收外部传过来的数据

2.传递数据:<Demo name="xxx"/>

<school name="西安石油大学" address="西安"></school>

3.组件 接收数据:

  • 第一种方式(只接收):
    props: ["name", "address", "num"],

  • 第二种方式(限制类型):

      props: {
        name: String,
        address: String,
        num: Number,
      },
    
  • 第三种方式(限制类型、限制必要性、指定默认值):

    props: {
        name: {
          type: String, //name的类型
          require: true, //是否必要
        },
        address: {
          type: String, //类型
          default: "西安", //如果不传,就给默认值
        },
        num: {
          type: String, //类型
          require: true,
        },
      },
    

备注:props是只读的

Vue底层会监测你对props的修改,如果进行了修改,就会发出警告.
若业务需求确实需要修改,那么请复制props的内容到data中一份,然后去修改data中的数据。

mixin(混入)

  • 混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。

  • 功能:可以把多个组件共用的配置提取成一个混入对象

例子:

//定义混合对象
export const mixin = {
    methods: {
        showName() {
            alert(this.name);
        },
    },
}
<h2 @click="showName">学校名称:{{ name }}</h2>

<script>
// 引入混合
import { mixin } from "../mixin";

export default {
  name: "School",
  data() {
    return {
      name: "西安石油大学",
      address: "西安",
    };
  },
  mixins: [mixin],
};
</script>

选项合并

当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。

比如,数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。

var mixin = {
  data: function () {
    return {
      message: 'hello',
      foo: 'abc'
    }
  }
}

new Vue({
  mixins: [mixin],
  data: function () {
    return {
      message: 'goodbye',
      bar: 'def'
    }
  },
  created: function () {
    console.log(this.$data)
    // => { message: "goodbye", foo: "abc", bar: "def" }
  }
})

同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。

var mixin = {
  created: function () {
    console.log('混入对象的钩子被调用')
  }
}

new Vue({
  mixins: [mixin],
  created: function () {
    console.log('组件钩子被调用')
  }
})

// => "混入对象的钩子被调用"
// => "组件钩子被调用"

值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。

var mixin = {
  methods: {
    foo: function () {
      console.log('foo')
    },
    conflicting: function () {
      console.log('from mixin')
    }
  }
}

var vm = new Vue({
  mixins: [mixin],
  methods: {
    bar: function () {
      console.log('bar')
    },
    conflicting: function () {
      console.log('from self')
    }
  }
})

vm.foo() // => "foo"
vm.bar() // => "bar"
vm.conflicting() // => "from self"

全局混入不建议使用

插件

  1. 插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制。

  2. 本质:包含install方法的一个对象,

    install 的第一个参数是Vue,第二个以后的参数是插件使用者传递的数据。

  3. 定义插件:

    对象.install = function (Vue, options) {
        // 1. 添加全局过滤器
        Vue.filter(....)
    
        // 2. 添加全局指令
        Vue.directive(....)
    
        // 3. 配置全局混入(合)
        Vue.mixin(....)
    
        // 4. 添加实例方法
        Vue.prototype.$myMethod = function () {...}
        Vue.prototype.$myProperty = xxxx
    }
    
  4. 使用插件:Vue.use()

    它需要在你调用 new Vue() 启动应用之前完成

例:

plugin.js

export default {
    install(Vue, x, y, z) {
        console.log(x, y, z)
        //全局过滤器
        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
            }
        })

        //定义混入
        Vue.mixin({
            data() {
                return {
                    x: 100,
                    y: 200
                }
            },
        })

        //给Vue原型上添加一个方法(vm和vc就都能用了)
        Vue.prototype.hello = () => { alert('你好啊aaaa') }
    }
}

main.js

// 引入插件
import plugin from './plugin'

// 使用插件
Vue.use(plugin)

然后就可以在别的组件使用插件里的功能了。

scoped样式

子组件的 style样式 最终是会汇总在一起的,因此会有 类名相同 导致 样式冲突 的情况

1. 作用:让样式在局部生效,防止冲突。
2. 写法:<style scoped>

注:App (根) 组件不适用

总结TodoList案例

1.组件化的编码流程

(1).拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突。

(2).实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:

  • 一个组件在用:放在组件自身即可。

  • 一些组件在用:放在他们共同的父组件上(状态提升)。

(3).实现交互:从绑定事件开始。

2.props适用于:

​	(1).父组件 ==> 子组件 通信

​	(2).子组件 ==> 父组件 通信(要求父先给子一个函数)

3.使用v-model时要切记:

v-model绑定的值不能是props传过来的值,因为props是不可以修改的!

注:props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但不推荐这样做。

组件自定义事件

一种组件间通信的方式,适用于:子组件 ===> 父组件

父组件中绑定:

第一种方式:

<!-- 通过自定义事件实现,,子给父传递数据 -->
    <Student @getName="getStudentName"></Student>

    getStudentName(studentName) {
      console.log("收到学生名", studentName);
    },

第二种方式(灵活性强)

使用this.$refs.xxx.$on()这样写起来更灵活,比如可以加定时器啥的。

<Student ref="student"></Student>
export default {
  methods: {
    getStudentName(studentName) {
      console.log("收到学生名", studentName);
    },
  },
  mounted() {
    //绑定自定义事件 
    //获取 student的DOM元素 给其绑定事件 $on('事件名',回调函数)
    this.$refs.student.$on("getName", this.getStudentName);

    // 设置定时器,三秒之后才绑定事件
    setTimeout(()=>{
    this.$refs.student.$on("getName", this.getStudentName);
    },3000)

  },
};

在子组件中触发:

  methods: {
    postName() {
      // 触发student组件上的自定义组件
      // this.$emit("组件名",传递的数据);
      this.$emit("getName", this.name);
    },
  },

解绑自定义事件this.$off()

      //解绑一个自定义事件
      this.$off("getName"); 

      //解绑多个自定义事件
      this.$off(["getName",["demo1"]]); 

      //解绑全部自定义事件
      this.$off();

注意:通过this.$refs.xxx.$on('事件名',回调)绑定自定义事件时,回调要么配置在methods中要么用箭头函数,否则this指向会出问题
(普通函数 this 指向的是触发 mounted函数 的实例对象,即绑定自定义事件的组件)
组件上也可以绑定原生DOM事件(click,keyup等),需要使用 native 修饰符。

<Student ref="student" @click.native="show"/>

全局事件总线(GlobalEventBus)

  • 一种组件间通信的方式,适用于任意组件间通信

  • 安装全局事件总线:

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

    1. 接收数据

    A组件想接收数据,则在A组件中给$bus绑定自定义事件,事件的回调留在A组件自身。

      methods(){
        demo(data){......}
      }
      ......
      mounted() {
        this.$bus.$on('xxxx',this.demo)
      }
    

    或直接使用箭头函数

        mounted() {
          this.$bus.$on("getSchoolName", (schoolName) => {
            console.log("收到了学校传的数据", schoolName);
          });
        },
    

    2. 提供数据:

      this.$bus.$emit('xxxx',数据)
    

注:最好在beforeDestroy钩子中,用$off去解绑当前组件所用到的事件。(只解绑当前,千万不能全部解绑)

消息订阅与发布(pubsub)

  • 一种组件间通信的方式,适用于任意组件间通信

  • 使用步骤:

    1. 安装pubsub:npm i pubsub-js

    2. 引入: import pubsub from 'pubsub-js'

    3. 接收数据:A组件想接收数据,则在A组件中订阅消息,订阅的回调留在A组件自身。

      methods(){
        demo(data){......}
      }
      ......
      mounted() {
        this.pid = pubsub.subscribe('xxx',this.demo) //订阅消息
      }
      
    4. 提供数据:pubsub.publish('xxx',数据)

    5. 最好在beforeDestroy钩子中,用PubSub.unsubscribe(pid)取消订阅。

nextTick

  • 语法:this.$nextTick(回调函数)

  • 作用:在下一次 DOM 更新结束后执行其指定的回调。

  • 什么时候用:当改变数据后,要基于更新后的新DOM进行某些操作时,要在nextTick所指定的回调函数中执行。

this.$nextTick(function(){
	this.$refs.inputTitle.focus()
}

Vue封装的过度与动画

  • 作用:在插入、更新或移除 DOM元素时,在合适的时候给元素添加样式类名。

  • 图示:

  • 写法

    1. 准备好样式:

      • 元素进入的样式:
        1. v-enter:进入的起点
        2. v-enter-active:进入过程中
        3. v-enter-to:进入的终点
      • 元素离开的样式:
        1. v-leave:离开的起点
        2. v-leave-active:离开过程中
        3. v-leave-to:离开的终点
    2. 使用<transition>包裹要过度的元素,并配置name属性:

      <transition name="hello">
      	<h1 v-show="isShow">你好啊!</h1>
      </transition>
      
    3. 备注:若有多个元素需要过度,则需要使用:<transition-group>,且每个元素都要指定key值。

    <template>
      <div>
        <button @click="isShow = !isShow">显示/隐藏</button>
        <transition-group name="hello" appear>
          <h1 v-show="!isShow" key="1">你好啊!</h1>
          <h1 v-show="isShow" key="2">尚硅谷!</h1>
        </transition-group>
      </div>
    </template>
    

案例:

<template>
  <div>
    <button @click="isShow = !isShow">显示与隐藏</button>
    <transition name="Show">
      <h1 v-show="isShow">"来是come"</h1>
    </transition>
  </div>
</template>

动画:

<style scoped>
.Show-enter-active {
  animation: go 2s ease reverse;
}
.Show-leave-active {
  animation: go 2s ease;
}
@keyframes go {
  /* 定义动画 */
  from {
    transform: translateX(0);
  }

  to {
    transform: translateX(-500px);
  }
}
</style>

过渡:

<style scoped>
	h1{
		background-color: orange;
	}
	/* 进入的起点、离开的终点 */
	.hello-enter,.hello-leave-to{
		transform: translateX(-100%);
	}
	.hello-enter-active,.hello-leave-active{
		transition: 0.5s linear;
	}
	/* 进入的终点、离开的起点 */
	.hello-enter-to,.hello-leave{
		transform: translateX(0);
	}

</style>

动画效果可以使用第三方的库:

库的名称:Animate.css
安装:npm i animate.css
引入:import ‘animate.css’

vue脚手架配置代理

方法一

​ 在 vue.config.js 中添加如下配置:

devServer:{
  proxy:"http://localhost:5000"
}

说明:

  1. 优点:配置简单,请求资源时直接发给前端(8080)即可。

  2. 缺点:不能配置多个代理,不能灵活的控制请求是否走代理。

  3. 工作方式:若按照上述配置代理,当请求了前端不存在的资源时,那么该请求会转发给服务器 (优先匹配前端资源)

方法二

​ 编写 vue.config.js 配置具体代理规则:

module.exports = {
	devServer: {
      proxy: {
      '/api1': {// 匹配所有以 '/api1'开头的请求路径
        target: 'http://localhost:5000',// 代理目标的基础路径
        changeOrigin: true,
        pathRewrite: {'^/api1': ''}
      },
      '/api2': {// 匹配所有以 '/api2'开头的请求路径
        target: 'http://localhost:5001',// 代理目标的基础路径
        changeOrigin: true,
        pathRewrite: {'^/api2': ''}
      }
    }
  }
}
/*
   changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
   changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:8080
   changeOrigin默认值为true
*/

在App中使用axios请求数据:

methods: {
 getStudents(){
        //加前缀api就走代理服务器,不加就不走
	axios.get('http://localhost:8080/api1/students').then(
		response => {
			console.log('请求成功了',response.data)
		},
		error => {
			console.log('请求失败了',error.message)
		}
	)
 },
 getCars(){
	axios.get('http://localhost:8080/api2/cars').then(
		response => {
			console.log('请求成功了',response.data)
		},
		error => {
			console.log('请求失败了',error.message)
		}
	)
 }
},

说明:

  1. 优点:可以配置多个代理,且可以灵活的控制请求是否走代理。

  2. 缺点:配置略微繁琐,请求资源时必须加前缀。

导入bootstrap样式

方式一

在 src => assets => css 导入bootstrap样式,然后在 App 中引用

import './assets/css/bootstrap.css'

缺点:bootstrap中会引入一些没有下载的资源,如果暂时不使用这些资源,不想下载的话,就不适用于这种方法

方式二

在 public=>css 导入bootstrap样式,然后在 index.html 中引入

  <link rel="stylesheet" href="<%= BASE_URL %>css/bootstrap.css">

slot插槽

  • 作用:让父组件可以向子组件指定位置插入html结构,也是一种组件间通信的方式,适用于 父组件 ===> 子组件

  • 分类:默认插槽、具名插槽、作用域插槽

  1. 默认插槽
     父组件中:
         <Category>
            <div>html结构1</div>
         </Category>
     子组件中:
         <template>
             <div>
                <!-- 定义插槽 -->
                <slot>插槽默认内容...</slot>
             </div>
         </template>
    
  2. 具名插槽
     父组件中:
         <List>
            <img slot="foodimg" src="" alt=""/>
    
           //多个同名插槽可以用 template包裹,
           //v-slot: 这种结构只能用在 template中
            <template v-slot:footmore>
                <div>html结构</div>
                <div>html结构</div>
            </template>
         </List>
     子组件中:
             <div>
                <!-- 定义插槽 -->
                <slot name="foodimg">插槽默认内容...</slot>
                <slot name="foodmore">插槽默认内容...</slot>
             </div>
    
  3. 作用域插槽
    理解:数据在组件的自身(子组件),但根据数据生成的结构需要组件的使用者(父组件)来决定。
    (games数据在Category(子)组件中,但使用数据所遍历出来的结构由App(父)组件决定)
    父组件中:
    	<Category>
    		//这里不允许使用template 
    		<template scope="scopeData">
    			<!-- 生成的是ul列表 -->
    			<ul>
    				<li v-for="g in scopeData.games" :key="g">{{g}}</li>
    			</ul>
    		</template>
    	</Category>
    
    	<Category>
    		<template slot-scope="scopeData">
    			<!-- 生成的是h4标题 -->
    			<h4 v-for="g in scopeData.games" :key="g">{{g}}</h4>
    		</template>
    	</Category>
    子组件中:
        <template>
            <div>
            <!-- 通过数据绑定就可以把子组件的数据传到父组件 -->
                <slot :games="games"></slot>
            </div>
        </template>
    	
        <script>
            export default {
                name:'Category',
                props:['title'],
                //数据在子组件自身
                data() {
                    return {
                        games:['红色警戒','穿越火线','劲舞团','超级玛丽']
                    }
                },
            }
        </script>
    

vuex

  • 概念:

在Vue中实现集中式状态(数据)管理的一个Vue插件,对vue应用中多个组件的共享状态进行集中式的管理(读/写),也是一种组件间通信的方式,且适用于任意组件间通信。

  • 何时使用?

​ 多个组件需要共享数据时

  • 原理图

  • 构建环境

  1. 安装npm i vuex

  2. 创建文件src/store/index.js

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

//准备actions对象——响应组件中用户的动作
const actions = {}
//准备mutations对象——修改state中的数据
const mutations = {}
//准备state对象——保存具体的数据
const state = {}

//创建并导出store
export default new Vuex.Store({
	actions,
	mutations,
	state
})
  1. main.js中创建 vm 时传入 store配置项
//引入store
import store from './store'

new Vue({
  render: h => h(App),
  store,
  beforeCreate() {
    Vue.prototype.$bus = this 
  },
}).$mount('#app')
  1. 组件中读取vuex中的数据:$store.state.sum

    注:这里如果不在index.js中处理数据 而是在store中新建了js文件来处理写数据
    读取数据格式应该为: $store.state.存放数据的文件名.sum

  2. 组件中修改vuex中的数据:$store.dispatch('action中的方法名',数据)$store.commit('mutations中的方法名',数据)

    备注:若没有网络请求或其他业务逻辑,组件中也可以越过actions,即不写dispatch,直接编写commit

案例:
index.js

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

//准备actions对象——响应组件中用户的动作
// 业务逻辑(判断,定时器 等)写在 actions 中
const actions = {
    addodd(context, value) {
        //context为上下文对象(部分的store) value为传递的参数值
        if (state.sum % 2) {
            context.commit('ADD', value)
        }
    },
    addwait(context, value) {
        setTimeout(() => {
            context.commit('ADD', value)
        }, 1000);
    }
}

//准备mutations对象——修改state中的数据
const mutations = {
    ADD(state, value) {
        // console.log('mutations中的jia被调用了')
        state.sum += value
    },
    DEL(state, value) {
        state.sum -= value
    },
}

//准备state对象——保存具体的数据
const state = {
    sum: 0
}

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

组件:

<template>
  <div>
    <h1>当前求和为{{ $store.state.sum }}</h1>
    <select v-model.number="n">
      <option value="1">1</option>
      <option value="2">2</option>
      <option value="3">3</option>
    </select>
    <button @click="add">+</button>
    <button @click="del">-</button>
    <button @click="addOdd">当前和为奇数时加</button>
    <button @click="addWait">等一会加</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      n: 1,
    };
  },
  methods: {
    // 直接连接mutations
    add() {
      this.$store.commit("ADD", this.n);
    },
    del() {
      this.$store.commit("DEL", this.n);
    },
    addOdd() {
      this.$store.dispatch("addodd", this.n);
    },
    addWait() {
      this.$store.dispatch("addwait", this.n);
    },
  },
};
</script>

getters的使用

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

  • 在store.js中追加getters配置

const getters = {
	bigSum(state){
		return state.sum * 10
	}
}

//创建并导出store
export default new Vuex.Store({
	......
	getters
})
  • 组件中读取数据:$store.getters.bigSum

map

  • 导入

    import {mapState, mapGetters, mapActions, mapMutations} from 'vuex'
    
  • mapState方法:用于帮助我们映射state中的数据为计算属性

    computed: {
        /* // 普通的计算属性
        sum() {
          return this.$store.state.sum;
        },
        address() {
          return this.$store.state.address;
        },
        day() {
          return this.$store.state.day;
        }, */
    
        //借助mapState生成计算属性:sum、school、subject(对象写法)
        ...mapState({ sum: "sum", address: "address", day: "day" }),
    
        //计算属性名 和 state中的数据名 相同,使用数组写法
        ...mapState(["sum", "address", "day"]),
    }
    
  • mapGetters方法:用于帮助我们映射getters中的数据为计算属性

    computed: {
        //借助mapGetters生成计算属性:bigSum(对象写法)
        ...mapGetters({bigSum:'bigSum'}),
    
        //计算属性名 和 state中的数据名 相同,使用数组写法
        ...mapGetters(['bigSum'])
    },
    
  • mapMutations方法:用于帮助我们生成与mutations对话的方法,即:包含$store.commit(xxx)的函数

    methods:{
        /*普通事件回调函数
        add() {
          this.$store.commit("ADD", this.n);
        },
        del() {
          // this.sum -= this.n;
          this.$store.commit("DEL", this.n); 
          },*/
    
    
        //靠mapActions生成:increment、decrement(对象形式)
        ...mapMutations({ add: "ADD", del: "DEL" }),
    
        //函数名与 mutations中的函数名相同,使用数组写法
        ...mapMutations(['ADD','DEL']),
    }
    
  • mapActions方法:用于帮助我们生成与actions对话的方法,即:包含$store.dispatch(xxx)的函数

    methods: {
        /* addOdd() {
          this.$store.dispatch("addodd", this.n);
        },
        addWait() {
          this.$store.dispatch("addwait", this.n);
        }, */
    
        //靠mapActions生成:incrementOdd、incrementWait(对象形式)
        ...mapActions({ addOdd: "addodd", addWait: "addwait" }),
    
        //函数名与 actions中的函数名 相同,使用数组写法
        ...mapActions(['addodd','addwait'])
      },
    

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

 <button @click="add(n)">+</button>
 <button @click="del(n)">-</button>
 <button @click="addOdd(n)">当前和为奇数时加</button>
 <button @click="addWait(n)">等一会加</button>

7.模块化+命名空间

  1. 目的:让代码更好维护,让多种数据分类更加明确。

  2. 修改store.js

    const countAbout = {
      namespaced:true,//开启命名空间
      state:{x:1},
      mutations: { ... },
      actions: { ... },
      getters: {
        bigSum(state){
           return state.sum * 10
        }
      }
    }
    
    const personAbout = {
      namespaced:true,//开启命名空间
      state:{ ... },
      mutations: { ... },
      actions: { ... }
    }
    
    const store = new Vuex.Store({
      modules: {
        countAbout,
        personAbout
      }
    })
    
  3. 开启命名空间后,组件中读取state数据:

    //方式一:自己直接读取
    this.$store.state.personAbout.list
    
    //方式二:借助mapState读取:
    ...mapState('countAbout',['sum','school','subject']),
    
  4. 开启命名空间后,组件中读取getters数据:

    //方式一:自己直接读取
    this.$store.getters['personAbout/firstPersonName']
    
    //方式二:借助mapGetters读取:
    ...mapGetters('countAbout',['bigSum'])
    
  5. 开启命名空间后,组件中调用dispatch

    //方式一:自己直接dispatch
    this.$store.dispatch('personAbout/addPersonWang',person)
    
    //方式二:借助mapActions:
    ...mapActions('countAbout',{incrementOdd:'jiaOdd',incrementWait:'jiaWait'})
    
  6. 开启命名空间后,组件中调用commit

    //方式一:自己直接commit
    this.$store.commit('personAbout/ADD_PERSON',person)
    
    //方式二:借助mapMutations:
    ...mapMutations('countAbout',{increment:'JIA',decrement:'JIAN'}),
    

路由

理解: 一个路由(route)就是一组映射关系(key - value),多个路由需要路由器(router)进行管理。
前端路由:key是路径,value是组件。

  • 基本使用
  1. 安装vue-router,命令:npm i vue-router

  2. 应用插件:(main.js

    // 引入VueRouter
    import VueRouter from "vue-router";
    // 引入路由器
    import router from "./router";
    Vue.use(VueRouter)
    
    new Vue({
      render: h => h(App),
      router: router,
    }).$mount('#app')
    
  3. 编写 router 配置项:(router/index.js

    //引入VueRouter
    import VueRouter from 'vue-router'
    //引入Luyou 组件
    import About from '../components/About'
    import Home from '../components/Home'
    
    //创建router实例对象,去管理一组一组的路由规则
    const router = new VueRouter({
    	routes:[
    		{
    			path:'/about',
    			component:About
    		},
    		{
    			path:'/home',
    			component:Home
    		}
    	]
    })
    
    //暴露router
    export default router
    
  4. 实现切换(active-class可配置高亮样式)

    <router-link active-class="active" to="/about">About</router-link>
    
  5. 指定展示位置

    <router-view></router-view>
    

几个注意点

  1. 路由组件通常存放在pages文件夹,一般组件通常存放在components文件夹。
  2. 通过切换,“隐藏”了的路由组件,默认是被销毁掉的,需要的时候再去挂载。
  3. 每个组件都有自己的$route属性,里面存储着自己的路由信息。
  4. 整个应用只有一个router,可以通过组件的$router属性获取到。

多级路由

  1. 配置路由规则,使用children配置项:
routes: [
    {
        path: '/about',
        component: About,
    },
    {
        path: '/home',
        component: Home,
        children: [ //通过children配置子级路由
            {
                path: 'news', //此处一定不要写:/news
                component: News
            },
            {
                path: 'message',//此处一定不要写:/message
                component: Message
            }
        ]
    }
]
  1. 跳转(要写完整路径,添加父路径):
<router-link to="/home/news">News</router-link>

路由的query参数

  1. 传递参数
<template>
  <div>
    <ul>
      <li v-for="m in messageList" :key="m.id">
        <!-- 跳转并携带query参数,to的字符串写法 -->
        <!-- <router-link :to="`/home/message/detail?id=${m.id}&title=${m.title}`">
         {{ m.title }}</router-link> -->

        <!-- 跳转并携带query参数,to的对象写法 -->
        <router-link
          :to="{
            path: '/home/message/detail',
            query: {
              id: m.id,
              title: m.title,
            },
          }"
          >{{ m.title }}</router-link
        >
      </li>
    </ul>
    <hr />
    <router-view></router-view>
  </div>
</template>
  1. 接收参数
<template>
  <div>
    <li>消息编号:{{ $route.query.id }}</li>
    <li>消息标题:{{ $route.query.title }}</li>
  </div>
</template>

命名路由

  1. 作用:可以简化路由的跳转。

  2. 如何使用

    • 给路由命名:
    routes: [
        {
            path: '/about',
            component: About
        },
        {
            path: '/home',
            component: Home,
            //二级路由
            children: [
                {
                    path: 'news',
                    component: News,
                },
                {
                    path: 'message',
                    component: Message,
                    children: [
                        {
                            name: 'hello', //给路由命名
                            path: 'detail',
                            component: Detail,
                        },
                    ]
                }
            ]
        },
    ]
    
    • 简化跳转:

      <!--简化前,需要写完整的路径 -->
      <router-link to="/demo/test/welcome">跳转</router-link>
      
      <!--简化后,直接通过名字跳转 -->
      <router-link :to="{name:'hello'}">跳转</router-link>
      
      <!--简化写法配合传递参数 -->
      <router-link 
      	:to="{
      		name:'hello',
      		query:{
      		   id:666,
                  title:'你好'
      		}
      	}"
      >跳转</router-link>
      

路由的params参数

  1. 配置路由,声明接收params参数

         {
             path: '/home',
             component: Home,
             children: [
                 {
                     path: 'news',
                     component: News
                 },
                 {
                     component: Message,
                     children: [
                         {
                             name: 'xiangqing',
                             path: 'detail/:id/:title', //使用占位符声明接收params参数
                             component: Detail
                         }
                     ]
                 }
             ]
         }
    
  2. 传递参数

    <!-- 跳转并携带params参数,to的字符串写法 -->
    <router-link :to="/home/message/detail/666/你好">跳转</router-link>
    				
    <!-- 跳转并携带params参数,to的对象写法 -->
    <router-link 
    	:to="{
    		name:'xiangqing',
    		params:{
                      id:666,
                      title:'你好'
    		}
    	}"
    >跳转</router-link>
    

    特别注意:路由携带params参数时,若使用to的对象写法,则不能使用path配置项,必须使用name配置!

  3. 接收参数:

    $route.params.id
    $route.params.title
    

路由的props配置

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

  • 路由:
children: [
    {
        name: 'hello', //给路由命名
        path: 'detail/:id/:title',
        component: Detail,
        //第一种写法:props值为对象,该对象中所有的key-value的组合最终都会通过props传给Detail组件
        // 只能传固定参数
        // props: { a: 900 }

        //第二种写法:props值为布尔值,布尔值为true,则把路由收到的所有params参数通过props传给Detail组件
        // 只适用于params传参
        // props: true

        //第三种写法:props值为函数,该函数返回的对象中每一组 key-value 都会通过 props 传给Detail组件
        props($route) {
            return {
                id: $route.query.id,
                title: $route.query.title
            }
        }
    },
]
  • 跳转去的组件
<template>
  <div>
    <li>消息编号:{{ id }}</li>
    <li>消息标题:{{ title }}</li>
  </div>
</template>

<script>
export default {
  name: "Detail",
  props: ["id", "title"],
};
</script>

<router-link>的replace属性

  1. 作用:控制路由跳转时操作浏览器历史记录的模式

  2. 浏览器的历史记录有两种写入方式:分别为pushreplace
    push是追加历史记录,
    replace是替换当前记录。路由跳转时候默认为push

  3. 如何开启replace模式:<router-link replace .......>News</router-link>

编程式路由导航

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

  2. 具体编码:

    //$router的两个API
    this.$router.push({
    	name:'xiangqing',
    		params:{
    			id:xxx,
    			title:xxx
    		}
    })
    
    this.$router.replace({
    	name:'xiangqing',
    		params:{
    			id:xxx,
    			title:xxx
    		}
    })
    this.$router.forward() //前进
    this.$router.back() //后退
    this.$router.go() //可前进也可后退,传具体参数,负数表示后退,正数表示前进
    

缓存路由组件

  1. 作用:让不展示的路由组件保持挂载,不被销毁。

  2. 具体编码:

    <!-- 缓存多个路由组件 -->
    <keep-alive :include="['News','Message']">
        <router-view></router-view>
    </keep-alive>
    
    <!-- 缓存一个路由组件 -->
    <keep-alive include="News"> 
        <router-view></router-view>
    </keep-alive>
    

    注:

    1. include="组件名"

    2. 如果没有 include 那么所有的路由组件都缓存(子路由组件)

两个新的生命周期钩子

  1. 作用:路由组件所独有的两个钩子,用于捕获路由组件的激活状态。

  2. 具体名字:

    • activated路由组件被激活时触发。

    • deactivated路由组件失活时(切换到别的组件)触发。

路由守卫

  1. 作用:对路由进行权限控制
  • 全局守卫:

    全局前置守卫:router.beforeEach((to,from,next)

    全局后置守卫:router.afterEach((to,from)

    //全局前置守卫:初始化时执行、每次路由切换前执行
    //to:到哪去,from:从哪来,next:放行
    router.beforeEach((to,from,next)=>{
    	
         //判断当前路由是否需要进行权限控制
    	if(to.meta.isAuth){ 
                 //权限控制的具体规则
    		if(localStorage.getItem('school') === 'atguigu'){ 
    			next() //放行
    		}else{
    			alert('暂无权限查看')
    		}
    	}else{
    		next() //放行
    	}
    })
    
    //全局后置守卫:初始化时执行、每次路由切换后执行
    router.afterEach((to,from)=>{
    	console.log('afterEach',to,from)
    	if(to.meta.title){ 
    		document.title = to.meta.title //修改网页的title
    	}else{
    		document.title = 'vue_test'
    	}
    })
    
    const router = new VueRouter({
        routes: [
            {
                path: '/home',
                component: Home,
                meta: { title: '主页' },//网页标题
                children: [
                    {
                        path: 'news',
                        component: News,
                        //可以将自定义的一些属性放入
                        //isAuth:组件是否有权限设置
                        meta: { isAuth: true, title: '新闻' }
                    },
                    {
                        path: 'message',
                        name: 'mess',
                        component: Message,
                        meta: { isAuth: true, title: '消息' },
                        children: [{...}]
                    }
                ]
            }
        ]
    })
    
  • 独享守卫:

    独属于某一个路由的,写在路由组件里

    beforeEnter(to, from, next){}

    const router = new VueRouter({
        routes: [
            {
                path: '/home',
                component: Home,
                meta: { title: '主页' },//网页标题
                children: [
                    {
                        path: 'news',
                        component: News,
                        //可以将自定义的一些属性放入
                        //isAuth:组件是否有权限设置
                        meta: { isAuth: true, title: '新闻' }
                    },
                    {
                        path: 'message',
                        name: 'mess',
                        component: Message,
                        meta: { isAuth: true, title: '消息' },
                        //独享路由守卫,在路由组件里
                        beforeEnter(to, from, next) {
                            if (to.meta.isAuth) { //判断当前路由是否需要进行权限控制
                                if (localStorage.getItem('school') === 'atguigu') {
                                    next()
                                } else {
                                    alert('暂无权限查看')
                                    // next({name:'guanyu'})
                                }
                            } else {
                                next()
                            }
                        },
                        children: [{}]
                    }
                ]
            }
        ]
    })
    
     注:可以和全局后置守卫配合使用
    
  • 组件内守卫:

    在具体组件内写守卫

    //进入守卫:通过路由规则,进入该组件时被调用
    beforeRouteEnter (to, from, next) {
    },
    //离开守卫:通过路由规则,离开该组件时被调用
    beforeRouteLeave (to, from, next) {
    }
    
    <script>
    export default {
      name: "Home",
      beforeRouteEnter(to, from, next) {
        if (to.meta.isAuth) {
          //判断当前路由是否需要进行权限控制
          if (localStorage.getItem("school") === "atguigu") {
            next();
          } else {
            alert("暂无权限查看");
            // next({name:'guanyu'})
          }
        } else {
          next();
        }
      },
      beforeRouteEnter(to, from, next) {
        next();
      },
    };
    </script>
    

路由器的两种工作模式

  1. 对于一个url来说,什么是hash值?—— #及其后面的内容就是hash值。

  2. hash值不会包含在 HTTP 请求中,即:hash值不会带给服务器。

  3. hash模式:

    • . 地址中永远带着#号,不美观 。

    • . 若以后将地址通过第三方手机app分享,若app校验严格,则地址会被标记为不合法。

    • . 兼容性较好。

  4. history模式:

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

Vue UI组件库

移动端:

vant: http://vant3.uihtm.com/#/zh-CN
Cube UI:https://didi.github.io/cube-ui/#/zh-CN
Mint UI:https://mint-ui.github.io/#!/zh-cn

PC端:

Element UI:https://element.eleme.cn/
IView UI:https://www.iviewui.com/

posted @ 2022-09-22 21:35  准备开始  阅读(141)  评论(0)    收藏  举报