JS 流行框架(一):Vue

JS 流行框架(一):Vue

Vue 介绍

Vue.js 是一套构建用户界面的框架,不仅容易上手,而且可以和第三方库组合使用,学习框架可以提高开发效率,发展历程:

  1. 原生 JavaScript
  2. jQuery
  3. 模板引擎
  4. Vue / React / Angular

Vue、Angular 和 React 一起被称为前端三大框架,Vue 由国人编写,且在 Vue 中包含了 Angular 和 React 的许多优点

框架是一套完整的解决方案,如果项目必须更换框架,那么必须重构项目,库(插件)仅提供某个功能,如果某个库无法完成需要,那么可以很容易切换到其它库以实现需求

Vue 的优势如下:

  1. 通过数据驱动界面更新,不使用 DOM 来更新界面,在开发时,我们只需要关注如何获取和处理数据即可,Vue 可以自动将数据渲染到模板中(界面上)
  2. 组件化开发,我们可以将网页拆分成若干个独立的组件编写,以后可以将封装好的组件组装成一个完整的网页

基本使用

  1. 下载 Vue.js

Vue2.7.10 版本 下载地址

  1. 导入 Vue.js
<script src="./js/vue.js"></script>
  1. 创建一个元素
<div id="app">{{ name }}</div>
  1. 创建一个 Vue 实例、关联元素、数据
/* 创建一个 Vue 实例 */
let vue = new Vue({
    el: "#app", /* 关联元素 */
    data: {     /* 数据 */
        name: "Reyn Morales"
    }
});

数据绑定

Vue 是基于 MVVM 设计模式实现的,MVVM 设计模式由 3 部分组成:

  • Model(M)
    • 数据模型
    • 相当于 Vue 实例
  • View(V)
    • 视图
    • 相当于被 Vue 实例关联的元素
  • View-Model(VM)
    • 视图-数据模型(关联视图与数据模型的桥梁)
    • 相当于 Vue 实例中的 data 属性

MVVM 设计模式最大的特点是支持数据的双向传递,即可以将数据从 View 通过 View-Model 传递到 Model 中,也可以将数据从 Model 通过 View-Model 传递到 View 中

单向

Vue 默认情况下仅支持数据的单向绑定:Model -> View-Model -> View,当 Vue 实例中的 data 属性中的数据发生变化时,被 Vue 实例所关联的元素中的相应内容也随之改变

双向

Vue 在被关联的元素为 input、textarea 和 select 时可以通过 v-model 指令实现数据的双向绑定,示例如下:

<div id="app">
    <input type="text" v-model="content">
</div>
<script>
    let vue = new Vue({
        el: "#app",
        data: {
            content: "Reyn Morales"
        }
    })
</script>

上述示例中,若 input 元素中的内容发生变化时,与之关联的 Vue 实例中 data 属性下的 content 属性的内容也随之改变,此情形可以通过 Vue 调试工具(vue-js-devtools)观察,v-model 本质上是一个自定义属性,凡是具有此属性的表单元素将自动忽略 value、checked、selected 等属性的初始值,总是以 Vue 实例中相关的数据作为来源

常用指令

指令即为 Vue 内部定义的某些自定义属性,通过这些自定义属性可以实现某些功能,例如 v-model 属性可以在表单相关元素上实现数据的双向绑定,以下内容是 Vue 预定义的其它指令

v-once

指令 v-once 的作用是让绑定此指令的元素中的数据只被渲染一次,之后不再随着数据的变化而改变,示例如下:

  • HTML
<div id="app">
    <p v-once>原始数据:{{ number }}</p>
    <p>当前数据:{{ number }}</p>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        number: 521
    }
});

上述示例中,如果通过 Vue 调试工具修改 number 的数值,那么随之改变的只有 “当前数据” 所在行中的插值,“原始数据” 所在行中的插值不再改变

v-cloak

默认情况下,Vue 的简易渲染流程如下所示:

  1. 渲染原始元素(未绑定数据)
    2. 拷贝原始元素、加载 Vue 实例中关联的元素中的数据到拷贝元素中
    3. 将绑定数据之后的拷贝元素替换原始元素

由于在替换原始元素之前,Vue 将显示模板内容(未绑定数据的原始元素),所以如果用户的网速较慢或网页性能较差,那么用户将看到模板内容

指令 v-cloak 的作用是让绑定此指令的元素在未绑定数据之前隐藏,直到绑定数据之后再显示,示例如下:

  • CSS
[v-cloak] {
    display: none
}
  • HTML
<div id="app">
    <p v-cloak>{{ name }}</p>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        name: "Reyn Morales"
    }
});

v-text

指令 v-text 的作用是替换绑定此指令的元素的内容,不解析 HTML 代码,类似于 JavaScript 中的 innerText,示例如下:

  • HTML
<div id="app">
    <p v-text="name">填充内容</p>
    <p v-text="msg">填充内容</p>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        name: "Reyn Morales",
        msg: "<span>Never Give Up!</span>"
    }
});

v-html

指令 v-html 的作用是替换绑定此指令的元素的内容,解析 HTML 代码,类似于 JavaScript 中的 innerHTML,示例如下:

  • HTML
<div id="app">
    <p v-html="name">填充内容</p>
    <p v-html="msg">填充内容</p>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        name: "Reyn Morales",
        msg: "<span>Never Give Up!</span>"
    }
});

v-if

指令 v-if、v-else-if 和 v-else 的作用是条件渲染,类似于 JavaScript 中的 if-else 语句,当条件表达式的结果为真实渲染此元素,否则就不渲染此元素,示例如下:

  • HTML
<div id="app">
    <p v-if="score >= 90">优秀</p>
    <p v-else-if="score > 70">良好</p>
    <p v-else-if="score > 60">及格</p>
    <p v-else>不及格</p>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        score: 88
    }
});

如果使用 v-if 指令,那么通过调试工具查看页面结构时,凡是条件表达式为 false 的元素压根就不会被渲染出来,上述示例中,v-if 相关的取值可以是模型中的数据,也可以是一个表达式,此外,必须注意的是:

  • 指令 v-else 不能单独出现
    • 指令 v-if、v-else-if 和 v-else 之间不能出现任何内容

v-show

指令 v-show 的作用类似于指令 v-if,都可以用于条件渲染,区别在于前者将元素隐藏,后者将元素删除,示例如下:

  • HTML
<div id="app">
    <p v-show="show">被显示的元素</p>
    <p v-show="hidden">被隐藏的元素</p>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        show: true,
        hidden: false
    }
});

上述示例中,如果通过调式工具查看页面结构时,如果条件表达式的结果为 false,那么元素将被动态赋予 style 属性以通过 display 样式控制元素的显示和隐藏

v-for

指令 v-for 类似于 JavaScript 中的 for…in 循环,可以根据遍历的对象渲染若干个元素,指令 v-for 可以遍历的对象有数组、字符串、数字和实例,示例如下:

  • HTML
<div id="app">
    <h3>遍历数组</h3>
    <ul>
        <li v-for="(value, index) in people">{{ index }} --- {{ value }}</li>
    </ul>

    <h3>遍历数字</h3>
    <ul>
        <li v-for="(value, index) in 10">{{ index }} --- {{ value }}</li>
    </ul>

    <h3>遍历字符串</h3>
    <ul>
        <li v-for="(value, index) in 'Reyn'">{{ index }} --- {{ value }}</li>
    </ul>

    <h3>遍历实例</h3>
    <ul>
        <li v-for="(value, key) in info">{{ key }} --- {{ value }}</li>
    </ul>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        people: ["reyn", "jane", "lily", "jack"],
        info: {
            name: "Reyn Morales",
            age: 21,
            gender: "male",
            addr: "sxu"
        }
    }
});

Vue 中的 v-for 实际上可以嵌套循环,示例如下:

  • HTML
<div id="app">
    <ul v-for="(obj, index) in userInfo">
        <h3>User {{ index + 1 }}</h3>
        <li v-for="(value, key) in obj">{{ key }} --- {{ value }}</li>
    </ul>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        userInfo: [
            {
                name: "Reyn",
                age: 21,
                gender: "male"
            },
            {
                name: "Jane",
                age: 20,
                gender: "female"
            },
            {
                name: "Jack",
                age: 23,
                gender: "male"
            }
        ]
    }
});

必须注意的是,v-for 为了提升性能,在更新已渲染过的元素列表时,将采用 “就地复用” 的策略,正是由于此策略在某些时刻将导致数据出现混乱,示例如下:

  • HTML
<div id="app">
    <form>
        <input type="text" v-model="newName">
        <input type="submit" value="新增" @click.prevent="insertPerson">
    </form>
    <ul>
        <li v-for="(person, index) in people">
            <input type="checkbox">
            <span>{{ index }} --- {{ person.name }}</span>
        </li>
    </ul>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        newName: "",
        people: [
            {
                name: "reyn"
            },
            {
                name: "lily"
            },
            {
                name: "jack"
            }
        ]
    },
    methods: {
        insertPerson: function () {
            let newPerson = { name: this.newName };
            this.people.unshift(newPerson);
            this.newName = "";
        }
    }
});

在上述示例中,若选中名称为 lily 的复选框,然后再表单中新增一个名称为 jhon 的数据,那么此时被选中的数据将不再是 lily,而是 reyn,出现此现象的原因在于,通过指令 v-for 渲染若干元素时,在每添加一个元素时,将检查缓存中是否存在此格式的元素,若存在,那么将 “就地复用” 此元素,将此旧元素渲染到界面上,若不存在,那么将创建一个新的元素,并将此新元素渲染到网页上,由于每添加一个元素时,指令 v-for 格式化原界面上的内容,并重新根据缓存中已存在的数据渲染,所以在添加第一条数据时(jhon),指令 v-for 将采用缓存中原本渲染 reyn 的元素,渲染 reyn 时将采用缓存中原本渲染 lily 的元素,由于此元素之前被选中,所以新渲染的 reyn 也自然被选中,依次类推,渲染 lily 时将采用缓存中原本渲染 jack 的元素,而在渲染 jack 时,缓存中已经没有元素可以使用了,所以将在缓存中创建一个新的元素用来渲染 jack

解决此问题的方案也很简单,为每个元素绑定一个独一无二的 key 值即可,在渲染时,指令 v-for 除了将匹配元素格式之外,也将比较被渲染的元素和缓存中的元素的 key 是否相同,如此一来就不会出现缓存和界面上元素匹配混乱的情况了,示例如下:

  • HTML
<div id="app">
    <form>
        <input type="text" v-model="newName">
        <input type="submit" value="新增" @click.prevent="insertPerson">
    </form>
    <ul>
        <li v-for="(person, index) in people" v-bind:key="person.id">
            <input type="checkbox">
            <span>{{ index }} --- {{ person.name }}</span>
        </li>
    </ul>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        newName: "",
        people: [
            {
                id: 1,
                name: "reyn"
            },
            {
                id: 2,
                name: "lily"
            },
            {
                id: 3,
                name: "jack "
            }
        ],
        globalID: 4,
    },
    methods: {
        insertPerson: function () {
            let newPerson = { id: this.globalID++, name: this.newName };
            this.people.unshift(newPerson);
            this.newName = "";
        }
    }
});

必须注意的是,通过指令 v-bind 为元素的 key 属性绑定值时,一定不能使用指令 v-for 循环中的 index 索引,原因在于循环中的 index 索引都是动态生成的,且每次循环都是从 0 开始,如此并不能解决匹配混乱的问题

v-bind

不论是 Vue 中的插值,还是 v-text、v-html 指令,都用于为元素内容绑定数据,而指令 v-bind 则用于为元素属性绑定数据,示例如下:

  • HTML
<div id="app">
    <input v-bind:type="inputType">
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        inputType: "text"
    }
});

实际上,v-bind 指令可以简写为 :,且属性值可以是一个 JavaScript 表达式,示例如下:

  • HTML
<div id="app">
    <input v-bind:type="inputType" :maxlength="maxLen + 3">
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        inputType: "text",
        maxLen: 7
    }
});

必须注意的是,对于元素的 class 或 style 属性而言,v-bind 的格式比较特殊

class

通过指令 v-bind 为元素的 class 属性添加类名时,必须将每个类名以单引号包裹并将所有类名放在一个数组中才能保证类名来自于 style 元素而非 Vue 实例的数据模型,示例如下:

  • CSS
.size {
    font-size: 100px;
}

.color {
    color: red;
}

.bg-color {
    background-color: skyblue;
}
  • HTML
<div id="app">
    <p :class="['size', 'color', 'bg-color']">Reyn Morales</p>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app"
});

此外,通过 v-bind 为 class 属性绑定类名时可以通过三目运算符或实例对象实现是否绑定类名的功能,示例如下:

  • HTML
<div id="app">
    <p :class="['size', { 'color': true }, isActive ? 'bg-color' : '']">Reyn Morales</p>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        isActive: false
    }
});

实际上,上述示例的样式可以通过 Vue 实例中的数据模型实现,示例如下:

  • HTML
<div id="app">
    <p :class="styleObj">Reyn Morales</p>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        styleObj: {
            'size': true,
            'color': true,
            'bg-color': false
        }
    }
});
style

通过指令 v-bind 为元素的 style 属性添加样式时,必须将每个属性及其取值以键值对的形式放在一个实例中,属性值必须被单引号包裹,若属性名称中具有连字符,那么此属性名称也必须被单引号包裹,示例如下:

  • HTML
<div id="app">
    <p :style="{ color: 'red', 'font-size': '100px' }">Reyn Morales</p>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app"
});

实际上,可以直接将 Vue 中的一个保存样式代码的实例传递给 style 属性,示例如下:

  • HTML
<div id="app">
    <p :style="styleObj">Reyn Morales</p>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        styleObj: {
            color: 'red',
            'font-size': '100px',
            'background-color': 'blue'
        }
    }
});

如果样式代码被保存到多个实例中,那么可以将所有实例以数组的形式传递给 style 属性,示例如下:

  • HTML
<div id="app">
    <p :style="[style1, style2]">Reyn Morales</p>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        style1: {
            color: 'pink',
        },
        style2: {
            'font-size': '100px',
            'background-color': 'skyblue'
        }
    }
});

v-on

指令 v-on 用于为元素绑定事件监听,示例如下:

  • HTML
<div id="app">
    <button v-on:click="myFn">{{ content }}</button>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        content: "Click Me!"
    },
    methods: {
        myFn: function () {
            console.log("Reyn Morales");
        }
    }
});

上述示例中,必须注意的是,事件名称不用写为 onclick,此外,所有事件函数都必须写在 Vue 实例的 methods 属性中

实际上,v-on 指令可以简写为 @,且可以向回调函数传递参数,示例如下:

  • HTML
<div id="app">
    <button @click="myFn('Reyn Morales', $event)">{{ content }}</button>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        content: "Click Me!"
    },
    methods: {
        myFn: function (name, event) {
            console.log(name);
            console.log(event);
            console.log(this.content);
        }
    }
});

上述示例中,必须注意的是,$event 即为事件对象,此外,在事件回调函数中可以通过 this 访问 Vue 实例数据模型中的数据

此外,Vue 中通过 v-on 修饰符处理常见的诸如事件冒泡、事件捕获等问题

修饰符
事件

我们可以通过事件修饰符快速实现关闭事件冒泡、开启事件捕获、阻止元素默认行为等功能

.once

不论事件被如何触发,均只调用一次回调函数,示例如下:

  • HTML
<div id="app">
    <button @click.once="myFn">Click Me!</button>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    methods: {
        myFn: function () {
            console.log("Hello World!");
        }
    }
});
.prevent

阻止元素默认行为,类似于调用 event.preventDefault(),示例如下:

  • HTML
<div id="app">
    <a href="https://yingxinlau.blog.csdn.net/" @click.prevent="myFn">我的博客</a>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    methods: {
        myFn: function () {
            console.log("Hello World!");
        }
    }
});
.stop

阻止事件冒泡,类似于调用 event.stopPropagation(),示例如下:

  • CSS
.grand {
    width: 300px;
    height: 300px;
    background-color: red;
}

.father {
    width: 200px;
    height: 200px;
    background-color: orange;
}

.son {
    width: 100px;
    height: 100px;
    background-color: yellow;
}
  • HTML
<div id="app">
    <div class="grand" @click="gFn">
        <div class="father" @click="fFn">
            <div class="son" @click.stop="sFn"></div>
        </div>
    </div>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    methods: {
        gFn: function () {
            console.log("Grand");
        },
        fFn: function () {
            console.log("Father");
        },
        sFn: function () {
            console.log("Son");
        }
    }
});
.self

如果以此修饰符修饰 v-on 指令,那么事件只有被绑定此事件的元素触发时才有效,通过事件冒泡或事件捕获的触发无效,示例如下:

  • HTML
<div id="app">
    <div class="grand" @click="gFn">
        <div class="father" @click.self="fFn">
            <div class="son" @click="sFn"></div>
        </div>
    </div>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    methods: {
        gFn: function () {
            console.log("Grand");
        },
        fFn: function () {
            console.log("Father");
        },
        sFn: function () {
            console.log("Son");
        }
    }
});
.capture

开启事件捕获,示例如下:

  • HTML
<div id="app">
    <div class="grand" @click.capture="gFn">
        <div class="father" @click.capture="fFn">
            <div class="son" @click.capture="sFn"></div>
        </div>
    </div>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    methods: {
        gFn: function () {
            console.log("Grand");
        },
        fFn: function () {
            console.log("Father");
        },
        sFn: function () {
            console.log("Son");
        }
    }
});
按键

我们可以通过按键修饰符快速实现监听某个特定按键所触发的事件

预定义

Vue 预定的按键修饰符如下所示:

  • enter
    • tab
    • delete
    • esc
    • space
    • up
    • down
    • left
    • right

示例如下:

  • HTML
<div id="app">
    <input type="text" @keyup.enter="myFn">
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    methods: {
        myFn: function () {
            alert("输入完毕");
        }
    },
});
自定义

有时预定义按键修饰符并不能满足我们的所有需求,此时可以自定义修饰符,示例如下:

  • HTML
<div id="app">
    <input type="text" @keyup.113="myFn">
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    methods: {
        myFn: function () {
            alert("输入完毕");
        }
    },
});

上述示例中,修饰符 113 是键盘上 F2 键所对应的数字,实际上可以通过 Vue.config.keyCodes 全局配置以提升代码的可读性,示例如下:

  • HTML
<div id="app">
    <input type="text" @keyup.f2="myFn">
</div>
  • JavaScript
Vue.config.keyCodes.f2 = 113;
let vue = new Vue({
    el: "#app",
    methods: {
        myFn: function () {
            alert("输入完毕");
        }
    },
});

计算属性

实际上,在 View 的插值中可以书写 JavaScript 表达式,示例如下:

  • HTML
<div id="app">
    <p>{{ msg.split("").reverse().join("") }}</p>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        msg: "Reyn Morales"
    }
});

上述示例中,在被关联元素的插值中实现了将字符串翻转之后显示的业务逻辑,虽然以此形式可以实现业务逻辑,不过存在很多弊端 —— 编码和维护效率低下,为了解决此问题,Vue 提供了计算属性,示例如下:

  • HTML
<div id="app">
    <p>{{ reverseMsg }}</p>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        msg: "Reyn Morales"
    },
    computed: {
        reverseMsg: function () {
            return this.msg.split("").reverse().join("");
        }
    }
});

上述示例中,必须注意的是,虽然 reverseMsg 以函数的形式编写,但是在 Vue 实例中它本质上是一个计算属性,所以在插值中使用时不能写 () 以调用此函数,实际上,通过方法也可以实现此功能,示例如下:

  • HTML
<div id="app">
    <p>{{ myFn() }}</p>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        msg: "Reyn Morales"
    },
    methods: {
        myFn: function () {
            return this.msg.split("").reverse().join("");
        }
    },
    computed: {
        reverseMsg: function () {
            return this.msg.split("").reverse().join("");
        }
    }
});

计算属性和方法的区别在于,每当调用一次方法时,方法中的语句总是会被执行一次,而每当使用一次计算属性时,只要计算属性中的返回值结果相较于之前的结果未发生改变,那么将不再执行函数体中的语句,而是立即返回之前被缓存起来的结果值,示例如下:

  • HTML
<div id="app">
    <p>{{ myFn() }}</p>
    <p>{{ myFn() }}</p>
    <p>{{ myFn() }}</p>
    <p>{{ reverseMsg }}</p>
    <p>{{ reverseMsg }}</p>
    <p>{{ reverseMsg }}</p>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        msg: "Reyn Morales"
    },
    methods: {
        myFn: function () {
            console.log("myFn");
            return this.msg.split("").reverse().join("");
        }
    },
    computed: {
        reverseMsg: function () {
            console.log("reverseMsg");
            return this.msg.split("").reverse().join("");
        }
    }
});

上述示例中计算属性和方法分别被调用了 3 次,myFn 打印了 3 次,而 reverseMsg 由于返回值未发生改变,所以只打印了 1 次

自定义

指令

有时 Vue 预定义的指令并不能完全满足我们的需求,此时可以自定义指令以实现某种特殊的功能

全局

自定义全局指令可以在任何一个 Vue 实例所关联的视图中使用,示例如下:

  • HTML
<div id="app1">
    <p v-color>Reyn Morales</p>
</div>
<div id="app2">
    <p v-color>Never Give Up!</p>
</div>
  • JavaScript
Vue.directive("color", {
    bind: function (el) {
        el.style.color = "red";
    }
});
let vue1 = new Vue({
    el: "#app1"
});
let vue2 = new Vue({
    el: "#app2"
});

上述示例中,必须注意的是,通过 Vue 的类方法 directive 自定义指令时,指令名称不用写 v-,此外,第二个参数用于描述指令在不同生命周期时执行的回调函数,bind 所描述的生命周期即在自定义指令第一次被绑定到元素上时(不一定被渲染)调用此函数,参数 el 是 Vue 执行此回调函数是传入的参数,表示被绑定指令的元素

在某些情况下,例如被绑定指令的元素存在事件相关的问题,此时 bind 函数并不能满足我们的需求,必须使用 inserted 函数,示例如下:

  • HTML
<div id="app">
    <input type="text" v-focus>
</div>
  • JavaScript
Vue.directive("focus", {
    inserted: function (el) {
        el.focus();
    }
});
let vue = new Vue({
    el: "#app"
});

上述示例中,当网页被打开时将自动聚焦到输入框上,如果使用 bind 函数则无法实现此功能,原因在于 bind 在指令第一次被绑定到元素上时调用,此时虽然可以调用元素的 focus 函数,但此时元素不一定被渲染在页面上,所以看不到聚焦的效果,而 inserted 函数在被绑定指令的元素添加到父元素中时执行,由于此时元素一定被渲染了,所以可以看到聚焦的效果

实际上,在回调(钩子)函数被调用时,Vue 除了传入 el 以表示被绑定指令的元素之外,还将传入额外的参数,例如 binding 实例,此实例的 name 属性即为指令名称(不包括 v-),value 属性即为指令的绑定值,示例如下:

  • HTML
<div id="app1">
    <p v-color="'red'">Reyn Morales</p>
</div>
<div id="app2">
    <p v-color="'blue'">Never Give Up!</p>
</div>
  • JavaScript
Vue.directive("color", {
    bind: function (el, binding) {
        console.log(binding.name);
        el.style.color = binding.value;
    }
});
let vue1 = new Vue({
    el: "#app1"
});
let vue2 = new Vue({
    el: "#app2"
});

上述示例中,必须以字符串的形式传递参数,否则参数将出现在 binding 实例的 expression 属性中,此属性保存了表达式,此外,也可以访问 Vue 实例的数据模型,示例如下:

  • HTML
<div id="app1">
    <p v-color="curColor">Reyn Morales</p>
</div>
<div id="app2">
    <p v-color="curColor">Never Give Up!</p>
</div>
  • JavaScript
Vue.directive("color", {
    bind: function (el, binding) {
        console.log(binding.name);
        el.style.color = binding.value;
    }
});
let vue1 = new Vue({
    el: "#app1",
    data: {
        curColor: 'pink'
    }
});
let vue2 = new Vue({
    el: "#app2",
    data: {
        curColor: 'orangered'
    }
});
局部

局部自定义指令的特性与注意点和全局自定义指令完全相同,区别仅在于形式不同,且局部自定义指令只能作用于某个 Vue 实例,示例如下:

  • HTML
<div id="app">
    <p v-color="'yellowgreen'">Reyn Morales</p>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    directives: {
        "color": {
            bind: function (el, binding) {
                el.style.color = binding.value;
            }
        }
    }
});

过滤器

过滤器专门用于格式化文本数据,必须注意的是,过滤器只可以在插值和 v-bind 指令中使用

全局

自定义全局过滤器可以在任何一个 Vue 实例所关联的视图中使用,示例如下:

  • HTML
<div id="app1">
    <p>{{ name | initialToUpper }}</p>
</div>
<div id="app2">
    <p>{{ name | initialToUpper }}</p>
</div>
  • JavaScript
Vue.filter("initialToUpper", function (value) {
    let arr = value.split(" ");
    for (let i = 0; i < arr.length; i++) {
        let initialCode = arr[i].charAt();
        arr[i] = arr[i].replace(initialCode, initialCode.toUpperCase());
    }
    return arr.join(" ");
});
let vue1 = new Vue({
    el: "#app1",
    data: {
        name: "reyn morales"
    }
});
let vue2 = new Vue({
    el: "#app2",
    data: {
        name: "mary jane"
    }
});

上述示例中的 value 即为将被过滤的数据,由系统自动传入,且数据一定是函数的第一个参数,此外,由于系统将过滤后的数据渲染,所以回调函数必须存在返回值,否则无法渲染被过滤后的数据

必须注意的是,过滤器可以连续使用,示例如下:

  • HTML
<div id="app1">
    <p>{{ name | initialToUpper | joinByHyphen }}</p>
</div>
<div id="app2">
    <p>{{ name | initialToUpper | joinByHyphen }}</p>
</div>
  • JavaScript
Vue.filter("initialToUpper", function (value, isJoinByHyphen) {
    let arr = value.split(" ");
    for (let i = 0; i < arr.length; i++) {
        let initialCode = arr[i].charAt();
        arr[i] = arr[i].replace(initialCode, initialCode.toUpperCase());
    }
    return arr;
});
Vue.filter("joinByHyphen", function (value) {
    return value.join("-");
});
let vue1 = new Vue({
    el: "#app1",
    data: {
        name: "reyn morales"
    }
});
let vue2 = new Vue({
    el: "#app2",
    data: {
        name: "mary jane"
    }
});

上述示例中,过滤器 joinByHyphen 的 value 是上一个过滤器 initialToUpper 处理后所返回的数据

实际上,过滤器在调用时可以传入参数,示例如下:

  • HTML
<div id="app1">
    <p>{{ name | initialToUpper(true) }}</p>
</div>
<div id="app2">
    <p>{{ name | initialToUpper(false) }}</p>
</div>
  • JavaScript
Vue.filter("initialToUpper", function (value, isJoinByHyphen) {
    let arr = value.split(" ");
    for (let i = 0; i < arr.length; i++) {
        let initialCode = arr[i].charAt();
        arr[i] = arr[i].replace(initialCode, initialCode.toUpperCase());
    }
    if (isJoinByHyphen) {
        return arr.join("-");
    }
    return arr.join(" ");
});
let vue1 = new Vue({
    el: "#app1",
    data: {
        name: "reyn morales"
    }
});
let vue2 = new Vue({
    el: "#app2",
    data: {
        name: "mary jane"
    }
});

上述示例中,必须注意的是,在过滤器被调用时手动传入的所有实参在函数原型中必须从第二个形参开始使用,原因在于第一个形参永远是原始数据或上一个过滤器处理之后的数据

局部

局部自定义过滤器的特性与注意点和全局自定义过滤器完全相同,区别仅在于形式不同,且局部自定义指令只能作用于某个 Vue 实例,示例如下:

  • HTML
<div id="app">
    <p>{{ time | dateFormatter("yyyy-MM-dd") }}</p>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        time: Date.now()
    },
    filters: {
        "dateFormatter": function (value, format) {
            let date = new Date(value);

            let year = date.getFullYear() + "";
            let month = date.getMonth() + 1 + "";
            let day = date.getDate() + "";
            let hour = date.getHours() + "";
            let minute = date.getMinutes() + "";
            let second = date.getSeconds() + "";

            if (format && format == "yyyy-MM-dd") {
                return `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")}`;
            }

            return `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")} ${hour.padStart(2, "0")}:${minute.padStart(2, "0")}:${second.padStart(2, "0")}`;
        }
    }
});

上述示例中的 dateFormatter 用于格式化时间,值得注意的是,padStart 方法是 String 类的原型方法,可以检测字符串的长度是否满足一定长度,如果不满足可以在调用此方法的字符串之前填充特定的字符,相应的 padEnd 方法则是在字符串末尾填充字符

过渡动画

Vue 提供了一系列方式以实现快速添加过渡动画的目的,示例如下:

  • HTML
<div id="app">
    <button @click.prevent="myFn">Toggle</button>
    <transition>
        <div class="box" v-show="isShow"></div>
    </transition>
</div>
  • CSS
.box {
    width: 100px;
    height: 80px;
    border: 1px solid black;
    border-radius: 5px;
    box-shadow: 0 0 10px gray;
}

.v-enter {
    opacity: 0;
}

.v-enter-to {
    opacity: 1;
}

.v-enter-active {
    transition: all 2s;
}

.v-leave {
    opacity: 1;
}

.v-leave-to {
    opacity: 0;
}

.v-leave-active {
    transition: all 2s;
}
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        isShow: false
    },
    methods: {
        myFn: function () {
            this.isShow = !this.isShow;
        }
    }
});

上述示例中,通过 Vue 的预定义 transition 组件实现了 .box 元素的过渡动画,凡是以 transition 组件为父元素的元素都将被添加预定义的类名,在元素出现或离开的不同阶段将自动为元素添加相应的 CSS 类以实现过渡动画,内容如下:

类名含义
v-enter说明元素出现之前的样式
v-enter-to说明元素出现之后的样式
v-enter-active说明元素出现过程中的样式
v-leave说明元素离开之前的样式
v-leave-to说明元素离开之后的样式
v-leave-active说明元素离开过程中的样式

必须注意的是,每个 transition 组件只能容纳一个子元素,所以要想实现为多个元素添加动画,必须定义多个 transition 组件并将每个元素分别放在一个 transition 组件中,另外,为了让元素在网页被刷新或打开时可以立即执行动画效果,此时可以为 transition 组件添加 appear 属性,示例如下:

  • HTML
<div id="app">
    <button @click.prevent="myFn">Toggle</button>
    <transition appear>
        <div class="box" v-show="isShow"></div>
    </transition>
    <transition appear>
        <div class="box" v-show="isShow"></div>
    </transition>
</div>
  • CSS
.v-enter {
    opacity: 0;
}

.v-enter-to {
    opacity: 1;
}

.v-enter-active {
    transition: all 2s;
}
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        isShow: true
    },
    methods: {
        myFn: function () {
            this.isShow = !this.isShow;
        }
    }
});

虽然可以通过上述方式实现同时为多个元素添加过渡动画,不过每个元素的过渡动画都是相同的,原因在于每个元素所依赖的过渡动画的类都是预定义的,此时若想让每个元素执行不同的动画,就必须以某种方式为每个元素指定不同的类名,示例如下:

  • HTML
<div id="app">
    <button @click.prevent="myFn">Toggle</button>
    <transition appear name="fstBox">
        <div class="box" v-show="isShow"></div>
    </transition>
    <transition appear name="sndBox">
        <div class="box" v-show="isShow"></div>
    </transition>
</div>
  • CSS
.fstBox-enter {
    opacity: 0;
}

.fstBox-enter-to {
    opacity: 1;
    margin-left: 500px;
}

.fstBox-enter-active {
    transition: all 2s;
}

.sndBox-enter {
    opacity: 0;
}

.sndBox-enter-to {
    opacity: 1;
    margin-top: 300px;
}

.sndBox-enter-active {
    transition: all 2s;
}
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        isShow: true
    },
    methods: {
        myFn: function () {
            this.isShow = !this.isShow;
        }
    }
});

上述示例中,通过为 transition 组件添加 name 属性从而指定了每个 transition 组件的子元素所依赖的类名的前缀,必须注意的是,由于在动画执行完毕后会将所有预定义的类名删除,所以在执行动画结束后元素将回到原来的位置,实际上,Vue 也可以通过钩子函数实相同的过渡动画效果,示例如下:

  • HTML
<div id="app">
    <button @click.prevent="myFn">Toggle</button>
    <transition appear v-bind:css="false" v-on:before-enter="beforeEnter" v-on:enter="enter"
        v-on:after-enter="afterEnter">
        <div class="box" v-show="isShow"></div>
    </transition>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        isShow: true
    },
    methods: {
        myFn: function () {
            this.isShow = !this.isShow;
        },
        beforeEnter: function (el) {
            el.style.opacity = "0";
        },
        enter: function (el, done) {
            el.offsetHeight;
            el.style.transition = "all 3s";
            setTimeout(() => {
                done();
            }, 0);
        },
        afterEnter: function (el) {
            el.style.opacity = "1";
            el.style.marginLeft = "500px";
        }
    }
});

上述示例中,必须注意如下内容:

  • 虽然使用钩子函数可以实现动画效果,默认情况下系统仍将自动识别预定义的类名以实现动画,所以为了避免此情形,可以在 transition 组件中依赖 v-bind 指令添加 css 属性并将其设置为 false
    • 在 enter 或 leave 方法中必须调用系统传入的 done 方法以通知系统调用 afterEnter 或 afterLeave 方法,此外,如果要让网页被打开或刷新时立即执行动画,必须在延迟一小段时间后再调用 done 方法,另外,所有钩子函数都有一个 el 参数用于保存需要执行动画的元素
    • 在动画执行过程的函数中,即 enter 和 leave 函数,必须写上 el.offsetWidth 或者 el.offsetHeight 语句动画才能正常执行

与动画相关的钩子函数内容如下:

函数含义
v-on:before-enter=“beforeEnter”释放动画之前
v-on:enter=“enter”释放动画过程中
v-on:after-enter=“afterEnter”释放动画之后
v-on:enter-cancelled=“enterCancelled”释放动画被取消
v-on:before-leave=“beforeLeave”离开动画之前
v-on:leave=“leave”离开动画过程中
v-on:after-leave=“afterLeave”离开动画之后
v-on:leave-cancelled=“leaveCancelled”离开动画被取消

如果通过钩子函数实现动画,我们可以通过 Velocity 等动画 JavaScript 库实现过渡效果,示例如下:

  • HTML
<div id="app">
    <button @click.prevent="myFn">Toggle</button>
    <transition appear v-bind:css="false" v-on:enter="enter">
        <div class="box" v-show="isShow"></div>
    </transition>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        isShow: true
    },
    methods: {
        myFn: function () {
            this.isShow = !this.isShow;
        },
        enter: function (el, done) {
            Velocity(el, { opacity: 1, marginLeft: "500px" }, { duration: 3000 });
            done();
        }
    }
});

实际上,Vue 的 transition 组件也可以依赖自定义的 CSS 类名实现过渡动画,示例如下:

  • HTML
<div id="app">
    <button @click.prevent="myFn">Toggle</button>
    <transition appear enter-class="my-enter-reyn" enter-active-class="my-enter-active-reyn"
        enter-to-class="my-enter-to-reyn">
        <div class="box" v-show="isShow"></div>
    </transition>
</div>
  • CSS
.my-enter-reyn {
    opacity: 0;
}

.my-enter-active-reyn {
    transition: all 2s;
}

.my-enter-to-reyn {
    opacity: 1;
}
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        isShow: true
    },
    methods: {
        myFn: function () {
            this.isShow = !this.isShow;
        }
    }
});

与动画相关的自定义 CSS 类的属性内容如下:

属性含义
enter-class释放动画之前
enter-active-class释放动画过程中
enter-to-class释放动画之后
leave-class离开动画之前
leave-active-class离开动画过程中
leave-to-class离开动画之后

如果通过自定义 CSS 类名实现动画,我们可以通过 Animate 等动画 CSS 库实现过渡效果,示例如下:

  • HTML
<div id="app">
    <button @click.prevent="myFn">Toggle</button>
    <transition appear enter-active-class="animate__animated animate__backInRight">
        <div class="box" v-show="isShow"></div>
    </transition>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        isShow: true
    },
    methods: {
        myFn: function () {
            this.isShow = !this.isShow;
        }
    }
});

在 Vue 中,为了实现让类似的元素共享相同的过渡动画效果,可以使用 transition-group 组件,此组件的特性与注意点和 transition 组件完全相同,二者的区别在于可以容纳的子元素数量不同,示例如下:

  • HTML
<div id="app">
    <form>
        <input type="text" v-model="newName">
        <input type="submit" value="新增" @click.prevent="insertPerson">
    </form>
    <transition-group tag="ul" appear enter-active-class="animate__animated animate__backInRight"
        leave-active-class="animate__animated animate__backOutRight">
        <li v-for="(person, index) in people" v-bind:key="person.id">
            <input type="checkbox">
            <span @click="remove(index)">{{ index }} --- {{ person.name }}</span>
        </li>
    </transition-group>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        newName: "",
        people: [
            {
                id: 1,
                name: "reyn"
            },
            {
                id: 2,
                name: "lily"
            },
            {
                id: 3,
                name: "jack "
            }
        ],
        globalID: 4,
    },
    methods: {
        insertPerson: function () {
            let newPerson = { id: this.globalID++, name: this.newName };
            this.people.unshift(newPerson);
            this.newName = "";
        },
        remove: function (index) {
            this.people.splice(index, 1);
        }
    }
});

上述示例中,必须注意的是,预定义组件 transition-group 默认将所有子元素放在 span 元素下,此时可以通过 tag 属性指定 transition-group 下的元素被渲染完毕后放在哪一个标签下,此外,若同时为多个元素添加动画时出现错误,通常情况下是由于指令 v-for 的匹配错乱,此时为每个元素绑定独一无二的 key 值即可,此处不再详细说明

组件

在 Vue 中,最大的优势之一即为组件化开发,以组件为单位开发可以简化代码,除了可以将 HTML 原生元素作为一个组件使用,我们也可以自定义组件

自定义

全局

自定义全局组件可以在任何一个 Vue 实例所关联的视图中使用,示例如下:

  • HTML
<div id="app1">
    <cpn></cpn>
</div>
<div id="app2">
    <cpn></cpn>
</div>
  • JavaScript
/* 1. 创建组件构造器 */
let profile = Vue.extend({
    template: `
        <div>
            <h1>我不是怪兽</h1>
            <p>Reyn Morales</p>
        </div>
    `
});

/* 2. 注册组件 */
Vue.component("cpn", profile);

let vue = new Vue({
    el: "#app1"
});
let vue = new Vue({
    el: "#app2"
});

上述示例中,通过 Vue 的 extend 方法可以创建指定模板的构造器,然后利用此构造器注册组件,必须注意的是,创建组件构造器时,通过 template 属性创建组件模板时,模板只能有一个根元素,所以在构造模板时,通常将所有内容元素置于一个 div 标签下,实际上,我们也可以类似于模板引擎的方式创建组件,示例如下:

  • HTML
<div id="app1">
    <cpn></cpn>
</div>
<div id="app2">
    <cpn></cpn>
</div>  

<template id="info">
    <div>
        <h1>我不是怪兽</h1>
        <p>Reyn Morales</p>
    </div>
</template>
  • JavaScript
Vue.component("cpn", {
    template: "#info"
});

let vue = new Vue({
    el: "#app1"
});
let vue = new Vue({
    el: "#app2"
});
局部

局部自定义组件的特性与注意点和全局自定义组件完全相同,区别仅在于形式不同,且局部自定义组件只能作用于某个 Vue 实例,示例如下:

  • HTML
<div id="app">
    <cpn></cpn>
</div>

<template id="info">
    <div>
        <h1>我不是怪兽</h1>
        <p>Reyn Morales</p>
    </div>
</template>
  • JavaScript
let vue = new Vue({
    el: "#app",
    components: {
        "cpn": {
            template: "#info"
        }
    }
});

数据模型

在 Vue 中,自定义组件可以有自己的数据模型,示例如下:

  • HTML
<div id="app">
    <cpn></cpn>
</div>

<template id="info">
    <div>
        <p>{{ name }}</p>
    </div>
</template>
  • JavaScript
Vue.component("cpn", {
    template: "#info",
    data: function () {
        return {
            name: "Reyn Morales"
        }
    }
});

let vue = new Vue({
    el: "#app"
});

在上述示例中,值得注意的是,为自定义组件添加数据模型时,属性 data 的内容必须以函数返回值的形式编写,原因在于避免同时创建多个相同的自定义组件时导致的数据混乱,如果以函数的形式编写 data 属性,那么在系统以模板创建每一个组件时,都将调用一次函数得到组件相应的数据模型,并将数据模型与此组件绑定,使每个组件都有自己的一份数据,从而避免数据混乱

方法

在 Vue 中,自定义组件也可以有自己的方法,示例如下:

  • HTML
<div id="app">
    <cpn></cpn>
</div>

<template id="info">
    <div>
        <button @click="toggle">Toggle</button>
        <p v-show="isShow">{{ name }}</p>
    </div>
</template>
  • JavaScript
Vue.component("cpn", {
    template: "#info",
    data: function () {
        return {
            isShow: true,
            name: "Reyn Morales"
        }
    },
    methods: {
        toggle: function () {
            this.isShow = !this.isShow;
        }
    }
});

let vue = new Vue({
    el: "#app"
});

组件切换

类似于将 HTML 原生元素作为组件时,可以使用指令 v-if 切换组件的显示和隐藏,也可以通过此指令控制各自定义组件的显示和隐藏,示例如下:

  • HTML
<div id="app">
    <button @click.prevent="toggle">Toggle</button>
    <reyn-info v-if="isShow"></reyn-info>
    <reyn-para v-else></reyn-para>
</div>

<template id="info">
    <div>
        <h1>我不是怪兽</h1>
    </div>
</template>
<template id="para">
    <div>
        <p>Reyn Morales</p>
    </div>
</template>
  • JavaScript
Vue.component("reyn-info", {
    template: "#info"
});
Vue.component("reyn-para", {
    template: "#para"
});

let vue = new Vue({
    el: "#app",
    data: {
        isShow: true
    },
    methods: {
        toggle: function () {
            this.isShow = !this.isShow;
        }
    },
});

实际上,Vue 提供了专门用于切换自定义组件的预定义组件 component,示例如下:

  • HTML
<div id="app">
    <button @click.prevent="toggle">Toggle</button>
    <component :is="cpnName"></component>
</div>

<template id="info">
    <div>
        <h1>我不是怪兽</h1>
    </div>
</template>
<template id="para">
    <div>
        <p>Reyn Morales</p>
    </div>
</template>
  • JavaScript
Vue.component("reyn-info", {
    template: "#info"
});
Vue.component("reyn-para", {
    template: "#para"
});

let vue = new Vue({
    el: "#app",
    data: {
        cpnName: "reyn-info"
    },
    methods: {
        toggle: function () {
            this.cpnName = this.cpnName === "reyn-info" ? "reyn-para" : "reyn-info";
        }
    },
});

在 Vue 中,组件 component 被称为动态组件,可以通过属性 is 指定显示哪一个组件,此外,必须注意的是,不论是通过指令 v-if 还是预定义组件 component 切换组件,最大的缺陷在于无法保存组件被切换之前的状态,示例如下:

  • HTML
<div id="app">
    <button @click.prevent="toggle">Toggle</button>
    <component :is="cpnName"></component>
</div>

<template id="info">
    <div>
        <span>Are you a monster?</span>
        <input type="checkbox">
    </div>
</template>
<template id="para">
    <div>
        <p>Reyn Morales</p>
    </div>
</template>
  • JavaScript
Vue.component("reyn-info", {
    template: "#info"
});
Vue.component("reyn-para", {
    template: "#para"
});

let vue = new Vue({
    el: "#app",
    data: {
        cpnName: "reyn-info"
    },
    methods: {
        toggle: function () {
            this.cpnName = this.cpnName === "reyn-info" ? "reyn-para" : "reyn-info";
        }
    },
});

之所以出现上述现象的原因在于,系统在切换组件时将删除被切换的组件,然后渲染将被显示的组件,所以根本无法保存被切换组件的原状态,此时可以通过预定义组件 keep-alive 将被切换的组件不被完全删除,而是将其存储在缓存中以保存原状态,从而解决此问题,示例如下:

  • HTML
<div id="app">
    <button @click.prevent="toggle">Toggle</button>
    <keep-alive>
        <component :is="cpnName"></component>
    </keep-alive>
</div>

<template id="info">
    <div>
        <span>Are you a monster?</span>
        <input type="checkbox">
    </div>
</template>
<template id="para">
    <div>
        <p>Reyn Morales</p>
    </div>
</template>
  • JavaScript
Vue.component("reyn-info", {
    template: "#info"
});
Vue.component("reyn-para", {
    template: "#para"
});

let vue = new Vue({
    el: "#app",
    data: {
        cpnName: "reyn-info"
    },
    methods: {
        toggle: function () {
            this.cpnName = this.cpnName === "reyn-info" ? "reyn-para" : "reyn-info";
        }
    },
});

组件动画

类似于将 HTML 原生元素作为组件时,可以使用预定义组件 transition 或 transition-group 为单一或若干组件添加过渡动画,此预定义组件同样适用于自定义组件,示例如下:

  • HTML
<div id="app">
    <button @click.prevent="toggle">Toggle</button>
    <transition>
        <component :is="cpnName"></component>
    </transition>
</div>
<template id="info">
    <div>
        <h1>我不是怪兽</h1>
    </div>
</template>
<template id="para">
    <div>
        <p>Reyn Morales</p>
    </div>
</template>
  • CSS
.v-enter {
    opacity: 0;
    margin-left: 500px;
}

.v-enter-to {
    opacity: 1;
    margin-left: 0;
}

.v-enter-active {
    transition: all 2s;
}

.v-leave {
    opacity: 1;
    margin-left: 0;
}

.v-leave-to {
    opacity: 0;
    margin-left: 500px;
}

.v-leave-active {
    transition: all 2s;
}
  • JavaScript
Vue.component("reyn-info", {
    template: "#info"
});
Vue.component("reyn-para", {
    template: "#para"
});
let vue = new Vue({
    el: "#app",
    data: {
        cpnName: "reyn-info"
    },
    methods: {
        toggle: function () {
            this.cpnName = this.cpnName === "reyn-info" ? "reyn-para" : "reyn-info";
        }
    },
});

上述示例中,必须注意的是,在切换组件时,各个组件动画的释放和离开是同时执行的,此时可以通过 mode 属性指定各个组件执行动画的顺序,示例如下:

  • HTML
<div id="app">
    <button @click.prevent="toggle">Toggle</button>
    <transition mode="out-in">
        <component :is="cpnName"></component>
    </transition>
</div>
<template id="info">
    <div>
        <h1>我不是怪兽</h1>
    </div>
</template>
<template id="para">
    <div>
        <p>Reyn Morales</p>
    </div>
</template>
  • JavaScript
Vue.component("reyn-info", {
    template: "#info"
});
Vue.component("reyn-para", {
    template: "#para"
});
let vue = new Vue({
    el: "#app",
    data: {
        cpnName: "reyn-info"
    },
    methods: {
        toggle: function () {
            this.cpnName = this.cpnName === "reyn-info" ? "reyn-para" : "reyn-info";
        }
    },
});

预定义组件 transition 的 mode 属性(动画模式)的相关内容如下:

动画模式说明
in-out被切换的组件先释放动画,完成之后当前组件执行离开动画
out-in当前组件先执行离开动画,完成之后被切换的组件释放动画

父子组件

在 Vue 中可以以嵌套的形式排列组件,例如,如果我们在某个 Vue 实例中定义一个局部组件,那么此局部组件实际上和 Vue 实例构成了一个父子关系,Vue 实例所关联的视图即为一个父组件,而局部组件即为一个子组件,此外,我们也可以在自定义的组件中通过 components 属性定义局部组件,从而构成父子组件,示例如下:

  • HTML
<div id="app">
    <father></father>
</div>

<template id="father">
    <div>
        <h1>我不是怪兽</h1>
        <son></son>
    </div>
</template>
<template id="son">
    <div>
        <p>Reyn Morales</p>
    </div>
</template>
  • JavaScript
Vue.component("father", {
    template: "#father",
    components: {
        "son": {
            template: "#son"
        }
    }
});

let vue = new Vue({
    el: "#app"
});

必须注意的是,子组件只在父组件中有效,即不能跨级使用

数据传递

默认情况下,每个组件只能使用自己的数据,如果两个组件以父子组件的形式排列,那么可以将父组件中的数据传递到子组件中,示例如下:

  • HTML
<div id="app">
    <father></father>
</div>

<template id="father">
    <div>
        <h1>我不是怪兽</h1>
        <son :f-name="name"></son>
    </div>
</template>
<template id="son">
    <div>
        <p>{{ fName }}</p>
    </div>
</template>
  • JavaScript
Vue.component("father", {
    template: "#father",
    data: function () {
        return {
            name: "Reyn Morales"
        }
    },
    components: {
        "son": {
            template: "#son",
            props: ["fName"]
        }
    }
});

let vue = new Vue({
    el: "#app"
});

上述示例中,父组件在使用子组件时,通过指令 v-bind 将数据 name 绑定到子组件的一个自定义属性上,子组件在配置项中通过属性 props 声明获取此自定义属性,之后便可以在子组件中使用此数据,必须注意的是,在传递、声明和使用此数据时的命名方式:如果想在使用时采用驼峰命名,那么在传递此数据时,必须以短横线分隔命名,而声明此数据时可以采用驼峰命名,也可以以短横线分隔命名,此外必须补充的是,如果在注册组件时采用了驼峰命名,那么在使用组件时必须以短横线分隔命名

方法传递

默认情况下,每个组件只可以使用自己的方法,如果两个组件以父子组件的形式排列,那么可以将父组件中的方法传递到子组件中,示例如下:

  • HTML
<div id="app">
    <father></father>
</div>

<template id="father">
    <div>
        <button @click="fn">Click Me!</button>
        <h1>我不是怪兽</h1>
        <son :f-name="name" @f-fn="fn"></son>
    </div>
</template>
<template id="son">
    <div>
        <button @click="myFn">Me! Click</button>
        <p>{{ fName }}</p>
    </div>
</template>
  • JavaScript
Vue.component("father", {
    template: "#father",
    data: function () {
        return {
            name: "Reyn Morales"
        }
    },
    methods: {
        fn: function () {
            alert("Hello World!");
        }
    },
    components: {
        "son": {
            template: "#son",
            props: ["fName"],
            methods: {
                myFn: function () {
                    this.$emit("f-fn");
                }
            }
        }
    }
});

let vue = new Vue({
    el: "#app"
});

上述示例中,父组件在使用子组件时,通过指令 v-on 将方法 fn 绑定到子组件的一个自定义事件上,子组件在属性 methods 下的任意一个自定义方法中通过调用函数 this.$emit() 的方式调用了父组件传递的方法,之后在使用时,子组件使用自定义的方法即可,必须注意的是,在传递方法时,不能采用驼峰命名,而必须使用短横线分隔命名

实际上,通过 this.$emit() 调用父组件所传递的方法时,可以传入额外参数,额外的参数将被作为父组件的方法的参数传入,从而实现子组件向父组件传递数据的效果,示例如下:

  • HTML
<div id="app">
    <father></father>
</div>

<template id="father">
    <div>
        <h1>我不是怪兽</h1>
        <son :f-name="name" @f-fn="fn"></son>
    </div>
</template>
<template id="son">
    <div>
        <button @click="myFn">Me! Click</button>
        <p>{{ fName }}</p>
    </div>
</template>
  • JavaScript
Vue.component("father", {
    template: "#father",
    data: function () {
        return {
            name: "Reyn Morales"
        }
    },
    methods: {
        fn: function (cnt) {
            alert(cnt); // myFn
        }
    },
    components: {
        "son": {
            template: "#son",
            props: ["fName"],
            methods: {
                myFn: function () {
                    this.$emit("f-fn", "myFn");
                }
            }
        }
    }
});

let vue = new Vue({
    el: "#app"
});
多级传递

如果想在子组件中使用祖先组件的数据或方法,必须采用逐级传递的方式,示例如下:

  • HTML
<div id="app">
    <grand></grand>
</div>
<template id="grand">
    <div>
        <h3>番茄不烦恼</h3>
        <father :g-name="name" @g-fn="gFn"></father>
    </div>
</template>
<template id="father">
    <div>
        <h1>我不是怪兽</h1>
        <son :f-name="gName" @f-fn="fFn"></son>
    </div>
</template>
<template id="son">
    <div>
        <button @click="sFn">Me! Click</button>
        <p>{{ fName }}</p>
    </div>
</template>
  • JavaScript
Vue.component("grand", {
    template: "#grand",
    data: function () {
        return {
            name: "Reyn Morales"
        }
    },
    methods: {
        gFn: function () {
            console.log("Hello World");
        }
    },
    components: {
        "father": {
            template: "#father",
            props: ["gName"],
            methods: {
                fFn: function () {
                    this.$emit("g-fn");
                }
            },
            components: {
                "son": {
                    template: "#son",
                    props: ["fName"],
                    methods: {
                        sFn: function () {
                            this.$emit("f-fn");
                        }
                    }
                }
            }
        }
    }
});

插槽

某些情况下,我们有可能在使用时才知道子组件中的内容,此时,我们可以通过 Vue 预定义组件 slot 实现在父组件渲染子组件时填充子组件内容的功能,示例如下:

  • HTML
<div id="app">
    <father></father>
</div>

<template id="father">
    <div>
        <son>
            <h4>我不是怪兽</h4>
            <h6>Reyn Morales</h6>
        </son>
    </div>
</template>
<template id="son">
    <div>
        <h2>Header</h2>
        <slot></slot>
        <h2>Footer</h2>
    </div>
</template>
  • JavaScript
Vue.component("father", {
    template: "#father",
    components: {
        "son": {
            template: "#son"
        }
    }
});

let vue = new Vue({
    el: "#app"
});

上述示例中,在父组件使用子组件时,在子组件下添加了 h4 和 h6 标签,由于在子组件中使用了 slot 组件,所以在渲染时,系统将 h4 和 h6 标签替换了 slot 组件,slot 组件就像子组件中未被填充的 “坑”,等待被某个组件使用时填充内容

实际上,在子组件的 slot 组件中也可以编写内容,此时父组件在使用子组件时,是否在子组件下添加内容成为可选项,若父组件并未在子组件下添加内容,那么子组件将使用 slot 组件中的默认内容,若父组件在子组件下添加了内容,那么子组件中的 slot 组件将被替换为特定的内容,示例如下:

  • HTML
<div id="app">
    <father></father>
</div>

<template id="father">
    <div>
        <son></son>
    </div>
</template>
<template id="son">
    <div>
        <h2>Header</h2>
        <slot>
            <h5>番茄不烦恼</h5>
        </slot>
        <h2>Footer</h2>
    </div>
</template>
  • JavaScript
Vue.component("father", {
    template: "#father",
    components: {
        "son": {
            template: "#son"
        }
    }
});

let vue = new Vue({
    el: "#app"
});

此外,在一个子组件中可以编写多个 slot 组件,示例如下:

  • HTML
<div id="app">
    <father></father>
</div>

<template id="father">
    <div>
        <son>
            <h4>我不是怪兽</h4>
            <h6>Reyn Morales</h6>
        </son>
    </div>
</template>
<template id="son">
    <div>
        <h2>Header</h2>
        <slot></slot>
        <slot></slot>
        <h2>Footer</h2>
    </div>
</template>
  • JavaScript
Vue.component("father", {
    template: "#father",
    components: {
        "son": {
            template: "#son"
        }
    }
});

let vue = new Vue({
    el: "#app"
});

若打开网页观察,将发现两个 slot 组件都被替换为了父组件在子组件下添加的内容,即添加的内容被拷贝了两份分别用于替换两个 slot 组件,实际上,由于上述示例的 slot 组件没有名称,所以被称为匿名插槽,虽然在一个组件中可以编写任意数量的匿名插槽,但在渲染时,所有匿名插槽都将被替换为子组件被使用时所添加的内容,如果想在不同的 slot 中添加不同的内容,此时可以通过具名插槽实现此功能,示例如下:

  • HTML
<div id="app">
    <father></father>
</div>

<template id="father">
    <div>
        <son>
            <h4 slot="inner">我不是怪兽</h4>
            <h6 slot="outter">Reyn Morales</h6>
            <h5 slot="inner">番茄不烦恼</h5>
        </son>
    </div>
</template>
<template id="son">
    <div>
        <h2>Header</h2>
        <slot name="inner"></slot>
        <h2>Footer</h2>
        <slot name="outter"></slot>
    </div>
</template>
  • JavaScript
Vue.component("father", {
    template: "#father",
    components: {
        "son": {
            template: "#son"
        }
    }
});

let vue = new Vue({
    el: "#app"
});

上述示例中,通过为子组件的 slot 组件添加 name 属性使其成为具名插槽,在父组件为子组件添加内容时,可以通过 Vue 的预定义属性 slot 指定此内容将替换到子组件的哪个插槽中,此外,父组件也可以通过 slot-scope 属性获取插槽通过 v-bind 指令所暴露的数据,示例如下:

  • HTML
<div id="app">
    <father></father>
</div>
<template id="father">
    <div>
        <son>
            <h6 slot="outter">Reyn Morales</h6>
            <template slot-scope="son" slot="inner">
                <li v-for="(value, index) in son.names">{{ value }}</li>
            </template>
        </son>
    </div>
</template>
<template id="son">
    <div>
        <h2>Header</h2>
        <slot name="inner" :names="names"></slot>
        <h2>Footer</h2>
        <slot name="outter"></slot>
    </div>
</template>
  • JavaScript
Vue.component("father", {
    template: "#father",
    components: {
        "son": {
            template: "#son",
            data: function () {
                return {
                    names: [
                        "reyn",
                        "jane",
                        "jack",
                        "lily"
                    ]
                }
            }
        }
    }
});

let vue = new Vue({
    el: "#app"
});

上述示例中,子组件下的名为 inner 的 slot 组件通过 v-bind 指令将子组件的 names 属性暴露,在父组件使用子组件时,将用于替换名为 inner 的 slot 组件的内容放在 template 组件下,并在 template 组件中添加 slot 属性用于说明将替换子组件的哪一个 slot 组件,slot-scope 属性用于获取子组件所暴露的数据,必须注意的是,使用 slot-scope 属性时必须使用 template 组件,且获取数据时的 slot 属性必须和暴露数据的 slot 组件一致,实际上,在 Vue 2.6.X 之后的版本中,提供了 v-slot 指令用于实现 slot 属性和 slot-scope 属性所实现的功能,示例如下:

  • HTML
<div id="app">
    <father></father>
</div>

<template id="father">
    <div>
        <son>
            <template v-slot:default="son">
                <li v-for="(value, index) in son.names"> {{ value }}</li>
            </template>
            <template #score="son">
                <li v-for="(value, index) in son.scores"> {{ value }}</li>
            </template>
        </son>
    </div>
</template>
<template id="son">
    <div>
        <h2>Information</h2>
        <h4>Name</h4>
        <slot :names="names"></slot>
        <h4>Score</h4>
        <slot name="score" :scores="scores"></slot>
    </div>
</template>
  • JavaScript
Vue.component("father", {
    template: "#father",
    components: {
        "son": {
            template: "#son",
            data: function () {
                return {
                    names: [
                        "reyn",
                        "jane",
                        "jack",
                        "lily"
                    ],
                    scores: [
                        95,
                        88,
                        76,
                        97
                    ]
                }
            }
        }
    }
});

let vue = new Vue({
    el: "#app"
});

必须注意的是,指令 v-slot 通常和 template 组件一起使用,通过 v-slot 指令所说明的自定义属性必须是插槽的名称,匿名插槽的名称为 default,此属性的值即为 slot 组件所暴露的数据,此外,类似于指令 v-bind 和指令 v-on,指令 v-slot 的缩写为字符 #

核心插件

Vuex

在通过原生 Vue 的组件系统时,存在如下缺陷:

  1. 若想让子组件使用祖先组件(非父组件)的数据或方法,必须逐级传递
    2. 若想让父组件下的某个子组件使用另一个子组件的数据,必须通过父组件传递

Vuex 是 Vue 配套的公共数据管理工具,可以将某些组件的共享数据保存到 Vuex 中,从而可以使程序中的任何组件可以直接访问或修改公共数据

在使用时,将公共数据保存到 Vuex 中即可,每个组件的私有数据则保存在各自的数据模型中即可

基本使用

通过 Vuex 的基础功能可以解决共享数据逐级传递的问题,结构如下:

<div id="app">
    <grand></grand>
</div>

<template id="grand">
    <div>
        <h1>Grand</h1>
        <p>{{ this.$store.state.msg }}</p>
        <father></father>
    </div>
</template>
<template id="father">
    <div>
        <h1>Father</h1>
        <p>{{ this.$store.state.msg }}</p>
        <son></son>
    </div>
</template>
<template id="son">
    <div>
        <h1>Son</h1>
        <p>{{ this.$store.state.msg }}</p>
    </div>
</template>

上述示例中的 this.$store.state.msg 是访问 Vuex 中所保存的共享数据 msg

方法如下:

  1. 下载并导入 Vuex
<script src="./js/vuex.js"></script>

在导入 vuex.js 之前,必须先导入 vue.js

  1. 创建 Vuex 实例
const myStore = new Vuex.Store({
    state: {
        msg: "Reyn Morales"
    }
});

上述示例中,属性 state 用于所有组件的共享数据

  1. 将 Vuex 实例保存到最外层的组件中
Vue.component("grand", {
    template: "#grand",
    store: myStore,
    components: {
        "father": {
            template: "#father",
            components: {
                "son": {
                    template: "#son"
                }
            }
        }
    }
});

let vue = new Vue({
    el: "#app"
});

上述示例中,通过组件的属性 store 用于存储创建好的 Vuex 实例

公共方法

虽然通过 Vuex 可以在程序的任何地方访问并修改所有组件的共享数据,不过缺点也很明显,如果出现错误,那么维护的效率将比较低,此时可以将所有修改共享数据的代码封装为若干个方法,并将这些方法保存到 Vuex 实例的 mutations 属性中,示例如下:

  • HTML
<div id="app">
    <father></father>
</div>

<template id="father">
    <div>
        <fst-son></fst-son>
        <snd-son></snd-son>
    </div>
</template>
<template id="fst-son">
    <div>
        <button @click.prevent="add">Add Count</button>
        <button @click.prevent="sub">Sub Count</button>
        <input type="text" :value="this.$store.state.count">
    </div>
</template>
<template id="snd-son">
    <div>
        <h4>{{ this.$store.state.count }}</h4>
    </div>
</template>
  • JavaScript
const myStore = new Vuex.Store({
    state: {
        count: 0
    },
    mutations: {
        mAC: function (state) {
            state.count++;
        },
        mSC: function (state) {
            state.count--;
        }
    }
});

Vue.component("father", {
    template: "#father",
    store: myStore,
    components: {
        "fst-son": {
            template: "#fst-son",
            methods: {
                add: function () {
                    this.$store.commit("mAC");
                },
                sub: function () {
                    this.$store.commit("mSC");
                }
            }
        },
        "snd-son": {
            template: "#snd-son"
        }
    }
});

let vue = new Vue({
    el: "#app"
});

上述示例中,Vuex 实例的 mutations 属性用于保存处理共享数据的公共方法,在调用公共方法时,系统将自动传入 Vuex 实例的共享数据,此外,在调用公共方法时,通过 this.$store.commit() 方法将要调用的公共方法以字符串的形式传入,从而实现调用指定公共方法的功能

计算属性

实际上,Vuex 提供了类似于 Vue 中的 computed 属性的 getters 属性,示例如下:

  • HTML
<div id="app">
    <grand></grand>
</div>

<template id="grand">
    <div>
        <h1>Grand</h1>
        <p>{{ this.$store.getters.format }}</p>
        <father></father>
    </div>
</template>
<template id="father">
    <div>
        <h1>Father</h1>
        <p>{{ this.$store.getters.format }}</p>
        <son></son>
    </div>
</template>
<template id="son">
    <div>
        <h1>Son</h1>
        <p>{{ this.$store.getters.format }}</p>
    </div>
</template>
  • JavaScript
const myStore = new Vuex.Store({
    state: {
        msg: "Reyn Morales"
    },
    getters: {
        format: function (state) {
            console.log("Format Attr Called!");
            return state.msg + " Never Give Up!";
        }
    }
});

Vue.component("grand", {
    template: "#grand",
    store: myStore,
    components: {
        "father": {
            template: "#father",
            components: {
                "son": {
                    template: "#son"
                }
            }
        }
    }
});

let vue = new Vue({
    el: "#app"
});

上述示例中,在访问 Vuex 的计算属性时,不再使用 state 而使用 getters,类似于 Vue 的计算属性,虽然计算属性以方法的形式编写,而在调用时则以属性的形式编写,类似的,在调用计算属性时,系统将自动传入 Vuex 实例的共享数据

Vue Router

Vue Router 和 v-if / v-show 指令的功能类似,都用于切换组件,区别在于,指令 v-if / v-show 通过标记(布尔值)切换,Vue Router 通过 URL 哈希切换,此外,Vue Router 不仅可以切换组件,而且在切换组件时可以传递参数

基本使用
  1. 下载并导入 Vue Router
<script src="./js/vue-router.js"></script>

Vue Router 必须在 Vue 之后导入

  1. 定义组件
const fstPage = {
    template: "#fst-page"
};
const sndPage = {
    template: "#snd-page"
};

上述实例本质上是组件的选项,即 Vue.component 的第二个参数

  1. 创建规则
const routes = [
    { path: "/fst", component: fstPage },
    { path: "/snd", component: sndPage }
];

数组中的每一个实例即为一条规则,规定了哪一个路径显示哪一个组件

  1. 根据规则创建路由实例
const router = new VueRouter({
    routes: routes
});
  1. 将路由实例绑定到 Vue 实例上
let vue = new Vue({
    el: "#app",
    router: router
});

上述示例中,值得注意的是,在 Vue 实例中并没有必要将 fstPage 和 sndPage 添加到 components 属性中,因为此时在 Vue 实例所关联的视图容器中并未显式引用子组件,而是通过 router-view 动态渲染子组件

  1. 通过预定义组件 router-view 显示组件
<div id="app">
    <a href="#/fst">Go To Fst Page</a>
    <a href="#/snd">Go To Snd Page</a>
    <router-view></router-view>
</div>

<template id="fst-page">
    <div class="fstp">
        <p>The First Page</p>
    </div>
</template>
<template id="snd-page">
    <div class="sndp">
        <p>The Second Page</p>
    </div>
</template>

<style>
    .fstp,
    .sndp {
        width: 240px;
        height: 80px;
        background-color: orangered;
        text-align: center;
        line-height: 80px;
        color: white;
    }

    .sndp {
        background-color: yellowgreen;
    }
</style>

上述示例中,Vue 的预定义组件 router-view 专门用于渲染通过 Vue Router 显示的组件

router-link

实际上,Vue Router 专门提供了一个用于设置 URL 哈希的标签 router-link,示例如下:

  • HTML
<div id="app">
    <router-link to="/fst">Go To Fst Page</router-link>
    <router-link to="/snd">Go To Snd Page</router-link>
    <router-view></router-view>
</div>

使用 router-link 元素切换元素时,通过 to 属性指定切换后的 URL 哈希,与 a 标签的区别在于不用写 # 符号,如果观察渲染后的结果,将发现系统本质上将 router-link 渲染为了 a 标签,实际上,可以通过 tag 属性更改渲染结果,示例如下:

  • HTML
<div id="app">
    <router-link to="/fst" tag="button">Go To Fst Page</router-link>
    <router-link to="/snd" tag="button">Go To Snd Page</router-link>
    <router-view></router-view>
</div>

此时如果通过调试工具观察,将发现 router-link 被渲染为了 button 标签,且在被激活的标签上添加了 router-link-active 类,通过在 CSS 中编写此类可以看到关联标签被激活时的样式,不过官方并不推荐此方法,而是提供了更好的解决方式,示例如下:

  • JavaScript
const router = new VueRouter({
    routes: routes,
    linkActiveClass: "my-btn-active"
});
  • CSS
.my-btn-active {
    background-color: burlywood;
    box-shadow: 0 0 10px gray;
}

上述示例中,在创建 Router 实例时,除了指定路由规则之外,还指定了激活状态时应为元素绑定的样式类,此时,如果通过调试工具观察,将发现被激活的标签上添加了 my-btn-active 类

路由重定向

如果想在页面被打开时立刻加载某个组件,那么可以使用 Vue Router 的路由重定向功能,示例如下:

  • JavaScript
const routes = [
    { path: "/", redirect: "/snd" },
    { path: "/fst", component: fstPage },
    { path: "/snd", component: sndPage }
];

默认情况下,利用 Vue Router 导航的默认网页的 URL 以 / 结尾,所以上述示例的第一条规则即将默认路径重定向到了 /snd,即在网页加载时,将立刻显示此路径下的内容

参数传递

Vue Router 的参数传递有两种方式,不论哪一种方式,都可以通过 Vue 实例的 $route 属性下的特定的属性获取参数

URL 参数

示例如下:

  • HTML
<div id="app">
    <router-link to="/fst?name=reyn&age=21" tag="button">Go To Fst Page</router-link>
    <router-link to="/snd" tag="button">Go To Snd Page</router-link>
    <router-view></router-view>
</div>
  • JavaScript
const fstPage = {
    template: "#fst-page",
    created() {
        console.log(this.$route);
        console.log(this.$route.query.name);    // reyn
        console.log(this.$route.query.age);     // 21
    }
};

如果通过 URL 参数传递,那么可以通过 Vue 实例的 $route 属性下的 query 属性访问参数

占位符

示例如下:

  • HTML
<div id="app">
    <router-link to="/fst" tag="button">Go To Fst Page</router-link>
    <router-link to="/snd/jane/20" tag="button">Go To Snd Page</router-link>
    <router-view></router-view>
</div>
  • JavaScript
const sndPage = {
    template: "#snd-page",
    created() {
        console.log(this.$route);
        console.log(this.$route.params.name);   // jane
        console.log(this.$route.params.age);    // 20
    }
};

const routes = [
    { path: "/", redirect: "/snd" },
    { path: "/fst", component: fstPage },
    { path: "/snd/:name/:age", component: sndPage }
];

上述示例中,在路由规则的特定路径下添加占位符,在切换此组件的相应标签下的 to 属性中,以 / 分隔每个参数,之后可以通过 Vue 实例的 $route 属性下的 params 属性访问参数,必须注意的是,若使用占位符传递参数,那么在 router-link 中必须传递参数,否则将无法正常渲染组件

路由嵌套

嵌套路由又被称为子路由,即在被切换的组件中切换另外的子组件,示例如下:

  • HTML
<div id="app">
    <router-link to="/fst" tag="button">Go To Fst Page</router-link>
    <router-link to="/snd" tag="button">Go To Snd Page</router-link>
    <router-view></router-view>
</div>

<template id="fst-page">
    <div class="fstp">
        <p>The First Page</p>
        <router-link to="/fst/fstsub1" tag="button">Go To Sub Page1</router-link>
        <router-link to="/fst/fstsub2" tag="button">Go To Sub Page2</router-link>
        <router-view></router-view>
    </div>
</template>

<template id="fst-sub-page1">
    <div>
        <p>The Sub Page1 Of First Page</p>
    </div>
</template>
<template id="fst-sub-page2">
    <div>
        <p>The Sub Page2 Of First Page</p>
    </div>
</template>
  • JavaScript
const fstSubPage1 = {
    template: "#fst-sub-page1"
};
const fstSubPage2 = {
    template: "#fst-sub-page2"
};

const fstPage = {
    template: "#fst-page",
    components: {
        "fst-sub-page1": fstSubPage1,
        "fst-sub-page2": fstSubPage2
    }
};

const routes = [
    { path: "/", redirect: "/snd" },
    {
        path: "/fst",
        component: fstPage,
        children: [
            {
                path: "fstsub1",
                component: fstSubPage1
            },
            {
                path: "fstsub2",
                component: fstSubPage2
            }
        ]
    },
    { path: "/snd", component: sndPage }
];

在上述示例中,必须注意的是,为 router-link 组件绑定路径时,不能省略一级路径 /fst,在创建路由规则时,如果是嵌套路由,那么必须在某个路径规则下的 children 属性中编写子路由规则,且子路由规则中的 path 属性省略一级路径 /fst

命名视图

Vue Router 中的 router-view 组件实际上和 slot 组件相似,都是待被填充的组件,如果存在多个未命名的 router-view 组件,那么通过路由切换组件时,所有 router-view 都将被填充相同的内容,示例如下:

  • HTML
<div id="app">
    <router-view></router-view>
    <router-view></router-view>
</div>

<template id="fst-page">
    <div class="fstp">
        <p>The First Page</p>
    </div>
</template>
<template id="snd-page">
    <div class="sndp">
        <p>The Second Page</p>
    </div>
</template>
  • JavaScript
const fstPage = {
    template: "#fst-page"
};
const sndPage = {
    template: "#snd-page"
};

const routes = [
    { path: "/", component: fstPage }
];
const router = new VueRouter({
    routes: routes
});

let vue = new Vue({
    el: "#app",
    router: router
});

为了解决上述问题,类似于 slot 组件的具名插槽,router-view 组件提供了命名视图,二者的功能相同,都是让不同的出口显示不同的内容,示例如下:

  • HTML
<div id="app">
    <router-view name="view1"></router-view>
    <router-view name="view2"></router-view>
</div>

<template id="fst-page">
    <div class="fstp">
        <p>The First Page</p>
    </div>
</template>
<template id="snd-page">
    <div class="sndp">
        <p>The Second Page</p>
    </div>
</template>
  • JavaScript
const fstPage = {
    template: "#fst-page"
};
const sndPage = {
    template: "#snd-page"
};

const routes = [
    {
        path: "/",
        components: {
            view1: fstPage,
            view2: sndPage
        }
    }
];
const router = new VueRouter({
    routes: routes
});

let vue = new Vue({
    el: "#app",
    router: router
});

上述示例中,通过为 router-view 组件绑定 name 属性使其成为命名视图,在创建路由规则时,通过 components 属性指定在哪一个视图中显示哪一个组件

监听路由

实际上,在 Vue 实例中提供了 watch 属性可以监听数据的变化,当数据发生变化时,将调用相应的回调函数,示例如下:

  • HTML
<div id="app">
    <input type="text" v-model="num1"> + <input type="text" v-model="num2"> = <input type="text" disabled
        v-model="res">
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        num1: 0,
        num2: 0,
        res: 0
    },
    watch: {
        num1: function () {
            this.res = parseFloat(this.num1) + parseFloat(this.num2);
        },
        num2: function () {
            this.res = parseFloat(this.num1) + parseFloat(this.num2);
        }
    }
});

上述示例中,当 num1 或 num2 的取值发生改变时,将自动调用相应的函数

通过 watch 属性,我们也可以监听路由的变化,示例如下:

  • HTML
<div id="app">
    <router-link to="/fst" tag="button">Go To Fst Page</router-link>
    <router-link to="/snd" tag="button">Go To Snd Page</router-link>
    <router-view></router-view>
</div>

<template id="fst-page">
    <div class="fstp">
        <p>The First Page</p>
    </div>
</template>
<template id="snd-page">
    <div class="sndp">
        <p>The Second Page</p>
    </div>
</template>
  • JavaScript
const fstPage = {
    template: "#fst-page"
};
const sndPage = {
    template: "#snd-page"
};

const routes = [
    { path: "/fst", component: fstPage },
    { path: "/snd", component: sndPage }
];
const router = new VueRouter({
    routes: routes
});

let vue = new Vue({
    el: "#app",
    router: router,
    watch: {
        "$route.path": function (oldValue, newValue) {
            console.log(oldValue, newValue);    // /fst /snd
        }
    }
});

如果为 Vue 实例添加了 router 属性,那么通过 Vue 实例可以访问 $route 属性,此属性下的 path 属性保存了路由的路径信息,通过 watch 属性可以监听此属性,当系统调用相应的回调函数时,将自动传入两个参数,分别是属性的旧值和新值

底层原理

生命周期

实际上,任何一个 Vue 实例都有生命周期,内容如下:

  1. 创建一个 Vue 实例
  2. 初始化生命周期相关事件
    • beforeCreate()
  3. 初始化数据模型和方法
    • created()
  4. 根据数据模型编译模板,此时模板存储与内存中
    • beforeMount()
  5. 将编译好的模板挂载到特定的容器中
    • mounted()
  6. 监听数据,如果数据发生变化,那么
    • beforeUpdate()
    1. 根据最新的数据编译模板,再使用新元素替换原来的内容
    • updated()
    1. 更新元素后,继续监听数据
    • beforeDestory()
  7. 销毁实例
    • destoryed()

上述内容中所有的函数即为生命周期函数,不同的生命周期函数有不同的特点

创建阶段
  1. beforeCreate

示例如下:

  • HTML
<div id="app">
    <p>{{ name }}</p>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        name: "Reyn Morales"
    },
    methods: {
        fn: function () {
            console.log("Hello World!");
        }
    },
    beforeCreate() {
        console.log(this.name, this.fn);    // undefined undefined
    }
});

由于 beforeCreate 生命周期方法是在 Vue 实例初始化生命周期相关事件之后,初始化数据模型和方法之前由系统自动调用,所以此时不能访问数据模型和方法中的内容

  1. created

示例如下:

  • HTML
<div id="app">
    <p>{{ name }}</p>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        name: "Reyn Morales"
    },
    methods: {
        fn: function () {
            console.log("Hello World!");
        }
    },
    created() {
        console.log(this.name); // Reyn Morales
        this.fn();  // Hello World!
    }
});

生命周期方法 created 是最早可以访问并使用 Vue 实例数据模型和方法的位置

  1. beforeMount

示例如下:

  • HTML
<div id="app">
    <p>{{ name }}</p>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        name: "Reyn Morales"
    },
    methods: {
        fn: function () {
            console.log("Hello World!");
        }
    },
    beforeMount() {
        console.log(document.querySelector("p").innerHTML); /* {{ name }} */
        console.log(document.querySelector("p").innerText); /* {{ name }} */
    }
});

虽然 beforeMount 方法在 Vue 实例根据模板数据编译好的元素之后调用,但此时的新元素存储在内存中,并未挂载到容器上,所以访问视图中的内容时打印编译之前的数据

  1. mounted

示例如下:

  • HTML
<div id="app">
    <p>{{ name }}</p>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        name: "Reyn Morales"
    },
    methods: {
        fn: function () {
            console.log("Hello World!");
        }
    },
    mounted() {
        console.log(document.querySelector("p").innerHTML); /* Reyn Morales */
        console.log(document.querySelector("p").innerText); /* Reyn Morales */
    }
});

将渲染好的元素挂载到容器中之后,既可以访问容器中元素的内容

运行阶段

beforeUpdate / updated

示例如下:

  • HTML
<div id="app">
    <p>{{ name }}</p>
</div>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        name: "Reyn Morales"
    },
    methods: {
        fn: function () {
            console.log("Hello World!");
        }
    },
    beforeUpdate() {
        console.log(this.name); // Reyn Morales Never
        console.log(document.querySelector("p").innerHTML); // Reyn Morales
        console.log(document.querySelector("p").innerText); // Reyn Morales
    },
    updated() {
        console.log(this.name); // Reyn Morales Never
        console.log(document.querySelector("p").innerHTML); // Reyn Morales Never
        console.log(document.querySelector("p").innerText); // Reyn Morales Never
    }
});

通过 Vue 调试工具修改 Reyn Morales 为 Reyn Morales Never 后,在利用新模板替换就模板之前调用 beforeUpdate 方法,在利用新模板替换就模板之后调用 updated 方法,所以在方法中访问模板数据时,均为最新数据,而访问视图容器中的元素内容时,beforeUpdate 方法中的依旧为原来的内容

销毁阶段

beforeDestroy / destoryed

示例如下:

  • HTML
<div id="app">
    <button @click="toggle">Toggle</button>
    <info v-if="isShow"></info>
</div>

<template id="info">
    <div>
        <h2>{{ content }}</h2>
    </div>
</template>
  • JavaScript
let vue = new Vue({
    el: "#app",
    data: {
        isShow: true
    },
    methods: {
        toggle: function () {
            this.isShow = !this.isShow;
        }
    },
    components: {
        "info": {
            template: "#info",
            data: function () {
                return {
                    content: "Reyn Morales"
                }
            },
            methods: {
                print: function () {
                    console.log("Hello World!");
                }
            },
            beforeDestroy() {
                console.log(this.content);  // Reyn Morales
                this.print();   // Hello World!
            },
            destroyed() {
                console.log(this.content);  // Reyn Morales
                this.print();   // Hello World!
            }
        }
    }
});

代码实现

补充内容

特殊特性

在项目开发时,我们有时必须获取视图中的元素,此时虽然可以通过原生 JavaScript 获取 DOM 元素,不过在 Vue 中并不推荐这样做,而是通过 Vue 的 ref 属性,示例如下:

  • HTML
<div id="app">
    <button @click="fn">Get Elements</button>
    <p ref="rp">DOM</p>
    <cpn id="cpn-ins" ref="rcpn"></cpn>
</div>

<template id="cpn">
    <div>
        <p>{{ content }}</p>
    </div>
</template>
  • JavaScript
Vue.component("cpn", {
    template: "#cpn",
    data: function () {
        return {
            content: "CPN"
        }
    },
    methods: {
        print: function () {
            console.log("Hello World!");
        }
    }
});

let vue = new Vue({
    el: "#app",
    methods: {
        fn: function () {
            console.log(this.$refs);
            console.log(this.$refs.rp);
            console.log(this.$refs.rcpn);
            console.log(this.$refs.rcpn.content);
            console.log(this.$refs.rcpn.print);
        }
    }
});

上述示例中,通过为元素和组件添加 Vue 的预定义属性 ref 指定一个引用,之后便可以在 Vue 实例中通过 this.$refs 访问所有绑定了 ref 属性的元素和组件,如果绑定此属性的是元素,那么访问的结果即为原生 DOM 元素,如果绑定此属性的是组件,那么访问的结果即为组件,通过此组件可以访问数据模型和方法

渲染方式

将自定义组件渲染到某个容器中时,除了可以通过标签的方式渲染,也可以通过 Vue 实例的 render 方法,示例如下:

  • HTML
<div id="app"></div>

<template id="info">
    <div>
        <h2>Reyn Morales</h2>
    </div>
</template>
  • JavaScript
Vue.component("info", {
    template: "#info"
});

let vue = new Vue({
    el: "#app",
    render: function (ce) {
        let html = ce("info");
        return html;
    }
});

上述示例中,通过 Vue 实例的 render 属性所绑定的方法返回被编译的 HTML 代码,系统将此 HTML 渲染到容器中,在调用此方法时,系统将自动传入一个参数,此参数可以将特定的组件编译为 HTML 代码,必须注意的是,如果通过标签的方式渲染,并不会覆盖 Vue 实例中的内容,如果通过 render 方法渲染,那么将覆盖 Vue 实例中原有的内容,示例如下:

  • HTML
<div id="app">
    <p>我不是怪兽</p>
</div>

<template id="info">
    <div>
        <h2>Reyn Morales</h2>
    </div>
</template>
  • JavaScript
Vue.component("info", {
    template: "#info"
});

let vue = new Vue({
    el: "#app",
    render: function (ce) {
        return ce("info");
    }
});

Vue-CLI

Vue CLI (Command Line Interface) 是 Vue 官方提供的脚手架工具,默认已经帮助我们搭建好了一套利用 webpack 管理 Vue 的项目结构

基本使用

  1. 下载 Vue CLI(全局)
npm install -g @vue/cli

可以通过 vue --version 命令查看是否下载成功

  1. 通过 Vue CLI 初始化项目
vue create <project-name>

必须注意的是,项目名称不能包含大写字母,此外,在启动命令后,系统将询问使用哪一个预设,类型如下:

预设类型含义
Default ([Vue 3] babel, eslint)默认的基于 Vue3.x 的预设,包含 babel 和 eslint
Default ([Vue 2] babel, eslint)默认的基于 Vue2.x 的预设,包含 babel 和 eslint
Manually select features手动选择项目特性
  1. 启动项目
# 切换到项目目录下
cd <project-name>
# 以 dev-server 模式启动项目
npm run serve
# 通过 webpack 编译打包项目
npm run build

目录结构:

  • /node_modules
    • 存储了项目所依赖的包
    • /public
      • 存储了项目的静态资源,静态资源不被 webpack 处理
      • 必须以绝对路径的形式引用此目录下的静态资源
      • 通常用于存储永远不会改变的静态资源或 webpack 不支持的第三方库
    • /src
      • 源代码目录
      • /assets
        • 存储项目中的静态资源(图片、字体等)
      • /components
        • 存储项目中的自定义组件(小组件、公共组件等)
      • /views
        • 存储项目中的自定义组件(大组件、页面级组件、路由级组件)
      • /router
        • 存储 VueRouter 相关的文件
      • /store
        • 存储 Vuex 相关的文件
      • App.vue
        • 根组件
      • main.js
        • 项目入口 JavaScript 文件

项目开发

构建框架
  1. 在 public 目录下编写 index.html
<!DOCTYPE html>
<html lang='en'>

<head>
  <meta charset='UTF-8'>
  <meta http-equiv='X-UA-Compatible' content='IE=edge'>
  <meta name='viewport' content='width=device-width, initial-scale=1.0'>
  <title>My Project</title>
</head>

<body>
  <div id='app'></div>
</body>

</html>
  1. 在 src 目录下编写根组件 App.vue
/* 组件模板 */
<template>
  <div id='app'>
    <h1>App.vue 根组件</h1>
  </div>
</template>

/* 组件业务 */
<script>
export default {
  name: 'App'
}
</script>

/* 组件样式 */
<style scoped>
h1 {
  color: orangered;
}
</style>

上述示例中,style 元素的 scoped 属性用于限定样式的范围,如果此元素没有 scoped 属性,那么此 style 元素下的样式将应用在所有组件上,如果此元素有 scoped 属性,那么此 style 元素下的样式将应用在此元素对应的组件上

  1. 在 src 目录下编写入口文件 main.js
/* eslint-disable no-new */
import Vue from 'vue'
import App from './App.vue'

new Vue({
  el: '#app',
  render: c => c(App)
})

上述示例中,通过 render 方法将 App 根组件渲染到 Vue 实例所关联的视图中

添加组件
  1. 在 src 目录的 components 目录下创建组件
<template>
  <div>
    <h2>First Component</h2>
  </div>
</template>

<script lang="ts">
export default {
  name: 'FirstComponent'
}
</script>

<style scoped>
h2 {
  color: yellowgreen;
}
</style>
  1. 在根组件 App.vue 下添加组件
<template>
  <div id='app'>
    <h1>App.vue 根组件</h1>
    <fst-cpn></fst-cpn>
  </div>
</template>

<script>
import FirstComponent from './components/FirstComponent.vue'

export default {
  name: 'App',
  components: {
    'fst-cpn': FirstComponent
  }
}
</script>

<style scoped>
h1 {
  color: orangered;
}
</style>
共享数据
  1. 在 src 目录的 store 目录下创建 index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    name: 'Reyn Morales'
  }
})

export default store

上述示例中,在使用 Vuex 之前,必须通过 Vue.use 方法注册此插件

  1. 在 src 目录下的 main.js 中添加此插件
/* eslint-disable no-new */
import Vue from 'vue'
import App from './App.vue'
import store from '@/store'

new Vue({
  el: '#app',
  store: store,
  render: c => c(App)
})

上述示例中,路径中的 @ 符号表示 src 目录,详情可以访问 jsconfig.json

  1. 在某个组件中引用此共享数据
<template>
  <div>
    <h2>First Component</h2>
    <p>{{ this.$store.state.name }}</p>
  </div>
</template>

<script lang="ts">
export default {
  name: 'FirstComponent'
}
</script>

<style scoped>
h2 {
  color: yellowgreen;
}
</style>
创建路由
  1. 在 src 目录的 router 目录下创建 index.js
import Vue from 'vue'
import Router from 'vue-router'
import FirstComponent from '@/components/FirstComponent'

Vue.use(Router)

const routes = [
  { path: '/first', component: FirstComponent }
]

const router = new Router({
  // 缩写,相当于 routes: routes
  routes
})

export default router
  1. 在 src 目录下的 main.js 中添加此插件
/* eslint-disable no-new */
import Vue from 'vue'
import App from './App.vue'
import store from '@/store'
import router from '@/router'

new Vue({
  el: '#app',
  store: store,
  router: router,
  render: c => c(App)
})
  1. 在根组件中引用此路由
<template>
  <div id='app'>
    <h1>App.vue 根组件</h1>
    <p>{{ this.$store.state.name }}</p>
    <fst-cpn></fst-cpn>
    <router-link to='/first' tag='button'>Go To First</router-link>
    <router-view></router-view>
  </div>
</template>

<script>
import FirstComponent from './components/FirstComponent.vue'

export default {
  name: 'App',
  components: {
    'fst-cpn': FirstComponent
  }
}
</script>

<style scoped>
h1 {
  color: orangered;
}
</style>

webpack 配置

Vue CLI 为了降低初学者的学习难度,特意隐藏了 webpack 相关的配置文件,不过,通过 Vue CLI 配置的项目有时并不能完全满足使用者的需求,所以 Vue CLI 实际上也提供了类似于 webpack.config.js 的 vue.config.js 以实现对项目的配置,示例如下:

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  outputDir: 'bundle'
})

默认情况下,属性 outputDir 的取值为 dist,即默认将项目打包在 dist 目录下,上述示例中,将属性 outputDir 的取值修改为 bundle,那么系统将把项目打包在 bundle 目录下,实际上 vue.config.js 配置文件中的 outputDir 属性等价于 webpack.config.js 配置文件中 output 属性下的 path 属性,所以 vue.config.js 实际上是在 webpack.config.js 原有属性的基础上进行了一层封装,以便于开发者使用,不过 vue.config.js 并没有封装所有属性,若 vue.config.js 中的属性不能满足我们的需求,那么我们可以在 configureWebpack 属性中编写 webpack 的配置属性,示例如下:

const { defineConfig } = require('@vue/cli-service')
const webpack = require('webpack')
module.exports = defineConfig({
  transpileDependencies: true,
  outputDir: 'bundle',
  configureWebpack: {
    plugins: [
      new webpack.BannerPlugin({
        banner: 'Reyn Morales'
      })
    ]
  }
})

插件 BannerPlugin 是 webpack 捆绑的插件,可以在编译打包压缩之后的文件中自动添加注释

注册插件

实际上,Vue 中不仅可以使用 Vue.component 注册全局组件,也可以使用 Vue.use 注册全局组件,不过必须将组件封装为一个插件,步骤如下:

  1. 在 src 下创建一个 plugin 目录,在此目录下,所有插件将以目录的形式保存,示例如下:
  • /src
    • /plugin
      • /loading
        • index.js
        • Loading.vue

上述示例中,将 Loading 组件保存在 plugin 目录的 loading 目录下,相应的 index.js 用于将组件封装为一个插件

  1. 编写组件
<template>
    <div id="loading">
        <div class="flower"></div>
        <p class="tip-text">正在加载中</p>
    </div>
</template>

<script>
    export default {
        // eslint-disable-next-line vue/multi-word-component-names
        name: 'LoadingC'
    }
</script>

<style scoped lang="scss">
    #loading {
        width: 200px;
        height: 200px;
        background-color: rgba(0,0,0,0.5);
        border-radius: 50px;
        position: absolute;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);

    .flower {
        width: 100px;
        height: 100px;
        border: 5px solid #fff;
        border-radius: 50%;
        margin: 20px auto;
        border-right-color: #3a8ee6;
        animation: loading 2s linear infinite;
    }
    @keyframes loading {
        from{
            transform: rotate(0deg);
        }
        to{
            transform: rotate(360deg);
        }
    }

    .tip-text {
        text-align: center;
        font-size: 16px;
        color: #fff;
    }
    }
</style>

  1. 将组件封装为插件
import LoadingC from '@/plugin/loading/Loading'

export default {
    install: function (Vue) {
        // 1. 根据组件生成构造函数
        const LoadingConstructor = Vue.extend(LoadingC)
        // 2. 利用构造函数创建实例
        const loadingInstance = new LoadingConstructor()
        // 3. 任意创建一个元素
        const oDiv = document.createElement('div')
        // 4. 将此元素添加到页面上
        document.body.appendChild(oDiv)
        // 5. 将创建好的实例挂载到创建好的元素上
        loadingInstance.$mount(oDiv)
    }
}
  1. 在 src 目录下的 main.js 中注册此组件
/* eslint-disable no-new */
import Vue from 'vue'
import App from './App.vue'
import store from '@/store'
import router from '@/router'

// import Loading from '@/plugin/loading/Loading.vue'
// Vue.component(Loading.name, Loading)

import LoadingC from '@/plugin/loading/index'
Vue.use(LoadingC)

new Vue({
  el: '#app',
  store: store,
  router: router,
  render: c => c(App)
})

上述示例中,注释部分是通过 Vue.component 注册全局组件的方式,此外,必须注意的是,如果通过 Vue.use 方法注册全局组件,那么 import 导入的是组件相应的 index.js 文件,实际上,通过 use 方法注册组件时,可以向 install 方法传递参数从而提高组件的灵活性,示例如下:

  • Loading.vue
<template>
    <div id="loading">
        <div class="flower"></div>
        <p class="tip-text">{{ tip }}</p>
    </div>
</template>

<script>
    export default {
        // eslint-disable-next-line vue/multi-word-component-names
        name: 'LoadingC',
        data: function () {
            return {
                tip: '正在加载中'
            }
        }
    }
</script>
  • index.js
import LoadingC from '@/plugin/loading/Loading'

export default {
  install: function (Vue, options) {
    // 1. 根据组件生成构造函数
    const LoadingConstructor = Vue.extend(LoadingC)
    // 2. 利用构造函数创建实例
    const loadingInstance = new LoadingConstructor()
    // 3. 任意创建一个元素
    const oDiv = document.createElement('div')
    // 4. 将此元素添加到页面上
    document.body.appendChild(oDiv)
    // 5. 将创建好的实例挂载到创建好的元素上
    loadingInstance.$mount(oDiv)

    /* 根据选项初始化组件的数据模型 */
    if (options && options.tip !== null && options.tip !== undefined) {
      loadingInstance.tip = options.tip
    }
  }
}

通过构造函数创建的实例可以访问组件的数据和方法

  • main.js
import LoadingC from '@/plugin/loading/index'
Vue.use(LoadingC, {
  tip: '加载中'
})

此外,在组件相应的 index.js 中也可以绑定全局方法或实例方法,示例如下:

  • Loading.vue
<template>
  <div id="loading" v-show="isShow">
    <div class="flower"></div>
    <p class="tip-text">{{ tip }}</p>
  </div>
</template>

<script>
export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: 'LoadingC',
  data: function () {
    return {
      tip: '正在加载中',
      isShow: false
    }
  }
}
</script>
  • index.js
import LoadingC from '@/plugin/loading/Loading'

export default {
  install: function (Vue, options) {
    // 1. 根据组件生成构造函数
    const LoadingConstructor = Vue.extend(LoadingC)
    // 2. 利用构造函数创建实例
    const loadingInstance = new LoadingConstructor()
    // 3. 任意创建一个元素
    const oDiv = document.createElement('div')
    // 4. 将此元素添加到页面上
    document.body.appendChild(oDiv)
    // 5. 将创建好的实例挂载到创建好的元素上
    loadingInstance.$mount(oDiv)

    /* 根据选项初始化组件的数据模型 */
    if (options && options.tip !== null && options.tip !== undefined) {
      loadingInstance.tip = options.tip
    }

    /* 添加全局方法 */
    Vue.showLoadingC = function () {
      loadingInstance.isShow = true
    }

    /* 添加实例方法 */
    Vue.prototype.$hideLoadingC = function () {
      loadingInstance.isShow = false
    }
  }
}
  • App.vue
<template>
  <div id='app'>
    <button @click.prevent="sLoading">Show Loading</button>
    <button @click.prevent="hLoading">Hide Loading</button>
  </div>
</template>

<script>
import Vue from 'vue'

export default {
  name: 'App',
  methods: {
    sLoading: function () {
      Vue.showLoadingC()
    },
    hLoading: function () {
      this.$hideLoadingC()
    }
  }
}
</script>

<style scoped>
h1 {
  color: orangered;
}
</style>

Vue 总结(项目)

Vue Router

  • 路由懒加载
  • 路由模式
    • history
    • hash
  • 如果在某个页面跳转到另一个页面时,不需要 router-link 和 router-view
  • 如果在某个页面中的若干子页面之间相互跳转,那么推荐使用 router-link 和 router-view
  • 路由规则中的路径属性相当于一个标识,以便于通过 router-link 的 to 属性静态导航或 router 实例的 push 方法动态导航

Vuex

实际上,可以将 Vuex 视作 Vue 项目的一个全局类,内容如下:

  • state
    • 属性
  • mutations
    • 方法
  • getters
    • 访问 state 属性
  • actions
    • 调用 mutations 方法(可以请求或预处理处理)

Iscroll

此处说明 Iscroll 无法工作的原因

  • 当祖先元素(外层)的高度大于或等于子元素(内层)的高度时,Iscroll 计算的滚动距离为零
    • 此时必须将祖先元素的高度固定
  • 如果由 Iscroll 控制的滚动组件在页面初始化时未显示,那么此时组件不论是祖先元素还是子元素的高度均为零,Iscroll 计算的滚动距离为零
    • 此时必须在组件切换到显示状态时,通过 Iscroll 的 refresh 实例方法在延迟短暂的时间后重新计算滚动距离
  • 通常情况下,如果子元素动态渲染网络请求的数据,那么 Iscroll 计算的滚动距离为零的情况也可能出现
    • 此时可以在子元素动态渲染之后通过 Iscroll 的 refresh 实例方法在延迟短暂的时间后重新计算滚动距离
    • 此时可以通过 observer 实例监听子元素高度的改变,在每一次发生变化时就调用 Iscroll 的 refresh 实例方法

Axios

  • 可以通过 axios 的拦截器在请求数据和响应数据时进行类似预处理的工作
  • 可以通过 axios 的 all 方法同时发送多个请求

开发技巧(Vue 项目)

  • 在调用 axios 相关的数据请求方法时,可以在组件的生命周期方法中,也可以在 Vuex 的 actions 中,不论在何处,都可以预处理数据,过滤掉我们不需要的数据以便于处理(包括在 axios 请求数据时)
  • 在开始项目时,必须尽量规划好组件的实现和组合方式,尽可能地将所有页面的公共组件提取出来,利用插槽或路由传参的方式提升组件的可重用性(必要时可以封装为一个插件)
    • 如果利用插槽,那么必须注意的是,不能直接设置 slot 组件的样式,必须将 slot 组件置于某个原生元素下,设置此原生元素的样式
    • 如果利用路由传参,那么可以在生命周期方法中通过参数判断组件的内容类型,从而请求不同的数据

SPA

SPA (Single Page web Application) 表示单页 web 应用,即仅有一个 HTML 文件的 Web 应用,通过 Vue 开发的项目就是典型的 SPA 应用

特点如下:

  • SPA 应用仅有一个 HTML 文件,所有的内容都在此页面中呈现
  • SPA 应用仅加载一次 HTML 文件,当用户和应用程序交互时,通过动态更新页面内容的方法来呈现不同的内容

优点如下;

  • 良好的交互体验
  • 服务器压力减轻

缺点如下:

  • SEO 难度较高
  • 初次加载耗时较多

渲染方式

客户端渲染

在 CSR (Client Side Render) 中,后端部分提供数据,前端部分提供视图和交互逻辑,SPA 应用就是典型的客户端渲染,过程如下:

  1. 客户端请求 HTML
  2. 服务端返回 HTML
  3. 客户端渲染 HTML,查询依赖的 CSS / JS 文件
  4. 客户端请求相应的 CSS / JS 文件
  5. 服务端返回相应的 CSS / JS 文件
  6. 客户端等待 CSS / JS 文件下载完成
  7. 客户端加载并初始化下载好的 JS 文件
  8. 客户端执行 JS 代码后向服务端请求数据
  9. 服务端返回数据
  10. 客户端利用服务端返回的数据渲染网页

客户端渲染的优点是交互体验好、可以局部更新,缺点是首屏加载较慢

服务端渲染

在 SSR (Server Side Render) 中,后端部分提供数据、视图和交互逻辑,即服务器收到客户端请求之后,根据特定的数据找到相应的数据并生成相应的视图,将包含数据的视图返回到客户端,客户端可以立刻渲染,流程如下:

  1. 客户端请求 HTML
  2. 服务端查询相应的 HTML 和数据
  3. 服务端根据 HTML 和相应数据生成完整的网页
  4. 服务端返回完整的网页
  5. 客户端渲染 HTML,查询依赖的 CSS / JS 文件
  6. 客户端请求相应的 CSS / JS 文件
  7. 客户端等待 CSS / JS 文件下载完成
  8. 客户端展示网页

服务端渲染的优点是首屏加载快、每次请求返回一个独立完成的网页,更利于 SEO,缺点是网络传输数据量大

预渲染

预渲染无需服务器实时动态编译,采用预渲染,在构建时针对特定路由简单的生成静态 HTML 文件,本质上依然是客户端渲染,不过与 SPA 的区别在于预渲染有多个界面

预渲染的优点是有多个页面、利于 SEO,缺点是首屏加载较慢、预编译很慢

  • 如果注重 SEO 的网页(新闻、电商网站),那么采用服务端渲染
  • 如果强交互的页面、不注重 SEO,那么采用客户端渲染
  • 如果介于二者之间、以加强少数页面的 SEO,那么采用预渲染

在 Vue 中可以通过插件实现预渲染以解决 SEO 问题,此处不再详细说明

  • vue-cli-plugin-prerender-spa
    • 预渲染
  • vue-meta-info
    • 统一管理所有页面的 SEO 标签
  • jsdom
    • 在 Node.js 环境下使用 DOM

其它

  • 利用自定义 HTML 属性 + Scss 条件判断 + 混合可以实现特定样式的切换
  • 通过 keep-alive 组件可以缓存某个组件的状态,以减少网络的请求次数
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值