介绍
vue.js
一个渐进式(全家桶)的Javascript框架,采用数据驱动视图的思想,即以数据为核心,优先操作数据而非视图
涉及工具
- Vue全家桶
- vue:适用于小型项目
- vue-router:适用于当项目中存在很多页面时
- vuex + axios:适用于当项目中数据也很多时
- webpack
一个前端模块化打包构建工具
MVC
一种软件架构模式
M
:Model,指数据层V
: View,指视图层C
:Controller,逻辑控制层
MVVM
- 介绍
MVVM是Vue采用的设计模式,实质就是对MVC中的C层进行了封装,使得M和V能够直接交互,得以实现的数据的双向绑定
- 使用这种设计模式的初衷
对于DOM,它是前端性能的瓶颈,即想要提高性能,就要尽可能地降低对DOM的操作,而DOM的操作是在Controller中进行的,Vue相当于将Controller进行了高性能的封装,用户可以无需考虑Controller层,只有V层和M层的直接对话,感受到一种数据双向绑定的体验,即V层中的数据改变,M层会自动同步改变,M层中数据改变,V层中也会自动同步改变
Vue的基本使用
代码示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<!--
插值表达式:即 {{}},mustache,也称小胡子语法
作用:可以直接读取data中的数据,常用于设置标签中的内容
注意:{{}} 当中只能放表达式,不能放入if、for等语句,且不能用在属性位置
-->
<h1>{{ msg }}</h1>
</div>
<!-- 1. 安装:npm i vue -->
<!-- 2. 引入 -->
<script src="../../node_modules/vue/dist/vue.js"></script>
<script>
/**
* 3. 创建vue实例并进行数据绑定
* el:[View] 值为一个选择器(推荐id选择器),表示指定vue管理的边界
* data:[Model]数据,
* vm:vue实例,注意一个边界只能对应一个实例
*/
const vm = new Vue({
el: '#app',
data: {
msg: 'hello, vue ~'
}
});
/**
* 可以使用vm直接获取data中的属性
* 原因是vue会对data中的属性进行遍历,并设置为vm属性
*/
console.log(vm.msg)
</script>
</body>
</html>
数据和视图的双向绑定
- 示例
v-model:将input的value和data中的数据绑定,二者将同步变化
现象:当修改data.msg时,input的value同步修改;当修改input内容时,data.msg也会同步改动<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <input type="text" v-model="msg" /> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: '#app', data: { msg: 'hello' } }); setTimeout(function() { vm.msg = 'data change'; }, 3000); setInterval(function() { console.log(vm.msg) }, 1000); </script> </body> </html>
- 数据劫持与
Object.defineProperty()
- 说明
Vue的数据双向绑定是通过
Object.defineProperty()
来实现的,而defineProperty
操作也被称为数据劫持,它是在ES5中提供的一个无法shim(兼容)的特性,这也是Vue无法支持IE8及以下版本的原因
- defineProperty的基本使用
/** * Object.defineProperties(要拦截的对象, 要拦截的属性名, 属性的set和get方法); */ let obj = {}; let temp; Object.defineProperty(obj, 'name', { set(value) { console.log("调用了set方法"); console.log("要设置的值:", value); temp = value; }, get() { console.log("调用了get方法"); return temp; } }); obj.name = 'xiao ming'; //会触发set方法 console.log(obj.name); //会触发get方法
- defineProperty实现数据双向绑定
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <input type="text" id="content"> <script src="../../node_modules/vue/dist/vue.js"></script> <script> let obj = {}; let temp; /* View -> Model */ let input = document.querySelector("#content"); input.onclick = function(){ obj.name = this.value; }; /* Model -> View */ Object.defineProperty(obj, 'name', { set(value) { temp = value; input.value = value; }, get() { return temp; } }) </script> </body> </html>
指令
简介
Directivies,vue中的指令是带有v-前缀的特殊属性,可以直接在html标签中使用,为对应的标签提供一些特殊的功能,常用的指令有:
v-model, v-bind, v-if, v-for
等
v-model
:数据双向绑定
代码示例见 Vue基本使用代码示例 ,该指令一般用在表单元素(如单选、多选、文本输入、下拉框等)中,要注意的是,绑定不同表单元素时,绑定的值也会不同
- 绑定多选框:绑定的是布尔类型的值
- 绑定文本框:绑定的是字符串类型的值
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <input type="text" v-model="msg" /> <input type="checkbox" v-model="isChecked"> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: '#app', data: { msg: 'hello', isChecked: true } }); </script> </body> </html>
v-html & v-text
:文本内容绑定
v-text
:效果同 {{}},相当于 innerText
v-html
:相当于innerHTML<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <div>{{msg}}</div> <div v-text="msgText"></div> <div v-html="msgHtml"></div> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", data: { msg: "msg", msgText: "text", msgHtml: "<span>html</span>" } }) </script> </body> </html>
v-bind
:数据动态绑定
- 示例
通常用在属性上
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <!--绑定title属性值--> <div v-bind:title="msg" id="app">this is a div</div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: '#app', data: { msg: "title" } }); </script> </body> </html>
- 简写
由于使用频繁,所以可以省略成一个
:
<div :title="msg"> this is a div </div>
- 用在样式上
:class='bar'
表示vue中存在bar属性,class为该属性的值,值对应为style标签中的一个类名
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <style> .red { color: red; background-color: #f99; } </style> </head> <body> <div id="app"> <div :class='r'>this is a div</div> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: '#app', data: { r: 'red' } }) </script> </body> </html>
:class='{clsname: isactive}'
classname对应为style标签中的一个类名,isactive表示是否生效,是个布尔值,需要在vue中声明
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <style> .red { color: red; background-color: #f99; } </style> </head> <body> <div id="app"> <div :class='{red: isRed}'>this is a div</div> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: '#app', data: { isRed: true } }) </script> </body> </html>
:class='[a, b]'
注意,数组中a和b没有引号,此时表示的是data中的属性值
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <style> .red-color { color: red; } .pink-bgc { background-color: #f99; } </style> </head> <body> <div id="app"> <div :class='[a, b]'>this is a div</div> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: '#app', data: { a: 'red-color', b: 'pink-bgc' } }) </script> </body> </html>
:class='["a", "b"]'
注意,数组中a和b有引号,此时表示的是style标签中的类名
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <style> .red-color { color: red; } .pink-bgc { background-color: #f99; } </style> </head> <body> <div id="app"> <div :class='["red-color", "pink-bgc"]'>this is a div</div> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: '#app', }) </script> </body> </html>
:style='{attr1: value1, attr2:value2}'
表示进行行内样式设置,值是个对象,对象中的属性对应行内样式style中可以设置的样式属性,属性值则对应vue中的属性值,多个样式之间用逗号相隔
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <div :style='{color: r, backgroundColor: p}'>this is a div</div> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: '#app', data: { r: 'red', p: 'pink' } }) </script> </body> </html>
v-on
:事件绑定
- 示例
v-on:事件名='函数名 事件名:如click,为点击事件的事件名 函数名:对应vue的methods中的一个方法
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <button v-on:click="click1">click me</button> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", methods: { //绑定事件的事件函数放在methods属性中 click1() { alert(1); } } }) </script> </body> </html>
- 简写
@click='click1'
- 事件中的this:vm实例
const vm = new Vue({ el: "#app", data: { msg: "message" } methods: { click1(){ alert(1) }, click2(){ alert(2) console.log(this === vm); //true this.click1(); //1 console.log(this.msg); //message } } });
- 传参
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <button @click="click('xiao ming')">click me</button> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", methods: { click(name) { alert('hello ' + name); } } }); </script> </body> </html>
- 事件对象
不传参时,为参数 e(参数名可自定义)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <button @click="click">click me</button> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", methods: { click(e) { console.dir(e); } } }); </script> </body> </html>
传参时,需使用vue预留的关键字
$event
作为事件参数额外传入<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <button @click="click($event, 12)">click me</button> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", methods: { click(e, num) { console.log(num + ": ", e); } } }); </script> </body> </html>
- 事件修饰符
prevent
相当于
e.preventDefault
,即阻止浏览器默认行为<a href="https://www.baidu.com" @click.prevent="fn">click me</a>
stop
相当于
e.stopPropagation
,即阻止事件冒泡<button @click.prevent="fn">click me</button>
capture
启用事件捕获
<button @click.capture="fn">click me</button>
self
不会受到冒泡或捕获的影响,只有点击自己时才会被触发
<button @click.self="fn">click me</button>
once
只触发一次事件
<button @click.once="fn">click me</button>
passive
提供移动端的性能,事件的三阶段:捕获-事件对象-冒泡,使用passive后,如果是冒泡,则会跳过捕获阶段,捕获同理
<button @click.passive="fn">click me</button>
- 按键修饰符
//注:enter键的键盘码为13 if(e.keyCode === 13) {//...} <=等价于=> @keyup.13='fn' <=等价于=> @keyup.enter='fn'
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <input @keyup.13="fn" type="text"/> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", methods: { //绑定事件的事件函数放在methods属性中 fn() { alert(1); } } }) </script> </body> </html>
v-for
:数据遍历
- 示例
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <!-- 遍历数组方式一:只要值 --> <ul> <li v-for="item in arr">{{item}}</li> </ul> <!-- 遍历数组方式二:要值和索引 --> <ul> <li v-for="(item, index) in arr">{{index}}: {{item}}</li> </ul> <!-- 遍历数组:数组中的元素是一个个对象 --> <ul> <li v-for="obj in jsonArr">{{obj.id}}: {{obj.name}}</li> </ul> <!-- 遍历对象:只要值 --> <ul> <li v-for="value in obj">{{obj}}</li> </ul> <!-- 遍历对象:要值和键 --> <ul> <li v-for="(value, key) in obj">{{key}}: {{value}}</li> </ul> <!-- 快速生成指定个数的元素:i将从1开始 --> <ul> <li v-for="i in 3">{{i}}</li> </ul> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", data: { arr: ["张三", "李四", "王五"], jsonArr: [ {id: 1, name: "张三"}, {id: 2, name: "李四"}, {id: 3, name: "王五"} ], obj: { name: "张三", gender: "男" } } }) </script> </body> </html>
- key属性
vue官网推荐在使用v-for指令时,加上key属性
- 问题
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <p v-for="item in list"> {{item}}: <input type="text" /> </p> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", data: { list: [ {id:1, name:'张三'}, {id:2, name:'李四'}, {id:3, name:'王五'} ] } }) </script> </body> </html>
页面效果为:
在李四对应文本框中输入以下内容:
继续在浏览器中给list添加元素并放在第一个元素位置上
可以看到输入的内容跑到张三对应的文本框中了,这是因为默认是跟着索引走的
- 方案
使用 key 之后就不会存在这样的问题,而key的使用又分为以下三种情况
- 当遍历的数据是个对象时:直接使用对象的key作为v-for的可以即可
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <p v-for="(value, key) in people" :key="key"> {{key}}: {{value}} </p> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", data: { people: { name: "张三", age: 12, gender: "男" } } }) </script> </body> </html>
- 当遍历的数据是个数组,且数组中存的是对象时:使用对象中的固定且唯一的属性作为v-for的key
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <p v-for="item in list" :key="item.id"> {{item}}: <input type="text" /> </p> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", data: { list: [ {id:1, name:'张三'}, {id:2, name:'李四'}, {id:3, name:'王五'} ] } }) </script> </body> </html>
- 当遍历的数据是个数组,但数组中存的不是对象,或者是对象但是没有固定且唯一的属性时:可以使用数组的索引作为v-for的key,但是切记不要让数组中元素的顺序发生改变
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <p v-for="(item, index) in list" :key="index"> {{item}}: <input type="text" /> </p> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", data: { list: ['张三', '李四', '王五'] } }) </script> </body> </html>
v-if, v-else-if, v-else
:条件渲染
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<h1 v-if="length > 15">long</h1>
<h1 v-else-if="length > 10">normal</h1>
<h1 v-else>short</h1>
</div>
<script src="../../node_modules/vue/dist/vue.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
length: 12
}
});
</script>
</body>
</html>
v-show
:隐藏显示
- 语法
v-show="boolean"; //主要用于元素的隐藏和显示,值为布尔类型,true表示显示
- 示例
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <div v-show="show">~~~~~~~~~~~~~~~~~~~~~~</div> <button :title="title" @click="toggle">click me</button> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", data: { show: true, title: "this is a click button" }, methods: { toggle() { console.log(this.show); this.show = !this.show; } } }); </script> </body> </html>
- 和
v-if
的异同值都是布尔类型,但是实现的方式不同,v-show是通过
display: none
实现节点的显示与否,而v-if则是通过创建和删除节点实现的,通常搭配其他条件渲染指令一起使用;此外频繁的切换显示隐藏时,使用v-show更加适合
v-pre
:不进行解析
该指令可以将指定标签中的表达式视为普通文本,不进行解析
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <h1>{{msg}}</h1> <!--显示:hello--> <h1 v-pre>{{msg}}</h1> <!--显示:{{msg}}--> <h1 v-pre v-text="msg"></h1> <!--显示:空(无论二者位置先后)--> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", data: { msg: "hello" } }); </script> </body> </html>
v-cloak
:解决闪烁问题
- 初衷
使用{{}},即mustache插值表达式时,渲染时偶尔会未渲染完毕,而先闪烁出{{msg}},之后才出现解析后的结果,v-cloak用于解决此问题
- 原理和使用
- 给要遮盖的元素添加这个指令
- 根据属性选择器,找到要遮盖的元素,设置display:none
- 当数据解析完毕后,vue会自动删除该指令
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <style> [v-cloak] { display: none; } </style> </head> <body> <div id="app"> <div v-cloak>{{msg}}</div> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", data: { msg: 'message' } }) </script> </body> </html>
v-once
:只解析一次
即只会解析一次,之后无论数据如何变化,都保持不变
自定义指令
- 全局自定义组件 & 基本介绍
钩子函数
Vue.directive('test', { bind() { //当自定义指令和标签绑定时触发,如 <h1 v-test></h1> }, inserted() { //当自定义指令绑定的标签插入到父元素时触发,如 <div id="app"><h1 v-test></h1></div> }, update() { //当自定义指令中的数据发生变化时触发,如 <h1 v-test>{{ num }}</h1>中的num变化时触发 }, componentUpdated() { //当自定义指令中的数据变化完成之后触发,如 <h1 v-test>{{ num }}</h1>中的num变化后触发 }, unbind() { //当自定义指令和标签解绑时触发,如 绑定的元素被移除(非v-show似的隐藏,而是v-if似的有无) } })
参数
bind(element, binding) { console.log(element); //自定义指令绑定的元素,如el console.log(binding.name); //自定义指令名称,如此例为 test console.log(binding.value); //指令绑定的值,如 v-test='1+1',则value为2 console.log(binding.oldValue); //指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用,且无论值是否改变都可用 console.log(binding.expression); //指令表达式,如 v-test='1+1',则为 1+1 console.log(binding.arg); //传给指令的参数,如 v-test:foo 中,参数为 foo console.log(binding.modifiers); //一个包含修饰符的对象,如 v-test.foo.bar 中,修饰符对象为 { foo: true, bar: true } }
使用
<div id="app"> <p v-test></p> </div>
- 局部组件 & 应用
<template> <div class="used_v"> <input type="text" v-test placeholder="v-test demo"> </div> </template> <script> export default { name: "UsedV", directives: { test: { inserted(el) { el.focus(); //自动聚焦 el.style.color = '#f99' } } } } </script> <style scoped> .used_v { color: aqua; } </style>
计算属性
指令和表达式的重新计算
指的是,当data中的数据变化时,指令(如v-for)和表达式(如{{}}、fn等)都会重新执行一遍
另外,如果进行了 v-model 双向数据绑定,view中数据改变,引起data改变,进而导致重新计算
所以,如果页面中使用了函数调用,即fn()的形式,每次重新计算都将进行一次调用,很影响性能
computed及特点
一般而言,数据放在
data
属性中,事件放在methods
属性中,而计算属性则放在computed
属性中
计算属性有以下几个特点
- 写起来是个方法,但是不能当方法用,即不能加()
- 一定要有返回值,返回值即为属性的值
- 不能和data中的属性重名,不过可以使用data中的属性值
- 一般用在根据一个已知的data值,得到一个data中不存在的新值
- 新值只和相关数据有关,而与其他数据的变化无关
示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<h1>{{result}}</h1> <!--当count值变化时,此表达式会重新计算-->
<input type="number" v-model="count">
</div>
<script src="../../node_modules/vue/dist/vue.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
count: 0
},
computed: {
result() {
return this.count > 0 ? parseInt(this.count) + 10 : 'none';
}
}
})
</script>
</body>
</html>
几个特殊的数据问题
$nextTick:vue的dom操作为异步
- 现象
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <h1>{{num}}</h1> <button @click="fn">click me</button> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", data: { num: 100 }, methods: { fn() { console.log(document.querySelector('h1').innerText); //100 this.num += 100; console.log(document.querySelector('h1').innerText); //100 } } }) </script> </body> </html>
- 解释
如题,当data中的num变化时,vue会进行指令的重新计算并更新dom,但是这个操作是异步的,可能尚未更新完成,主线程代码已经往下走,导致打印的num仍为之前的num
- 方案
vue提供了
$nextTick(callback)
解决此问题,可以将它理解为一个事件,当vue的dom操作完成时触发执行其中的callback回调函数<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <h1>{{num}}</h1> <button @click="fn">click me</button> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", data: { num: 100 }, methods: { fn() { console.log(document.querySelector('h1').innerText); //100 this.num += 100; this.$nextTick(() => { //注意this不能省略 console.log(document.querySelector('h1').innerText); //200 }) } } }) </script> </body> </html>
数据变化的统一更新
- 现象
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <h1>{{num}}</h1> <button @click="fn">click me</button> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", data: { num: 0 }, methods: { fn() { this.num += 100; for(var i = 0; i < 100; i++){ this.num += 1; } this.num += 100; } } }) </script> </body> </html>
click后,页面中直接显示的300,而非100,101,…,300
- 解释
vue对dom的重新更新操作,会等待fn函数结束,确定了num的值之后,再统一的进行一次更新操作
$set:数据响应式问题
- 现象
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <input type="text" v-model="obj.name"> <button @click="fn">button</button> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", data: { obj: {} }, methods: { fn() { this.obj.name = "张三" } } }) </script> </body> </html>
无论是在input中输入,还是点击button,可以在devTools中观察到obj始终为空
- 解释
Vue无法检测到对象属性的添加和删除,因为Vue只会在Vue对象实例化时对属性执行
getter & setter
,所以属性必须在其初始化之前就存在,才能让Vue将它转换成响应式的
- 方案
vue提供了
$set
,一个用于设置属性的方法来解决此问题<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <input type="text" v-model="obj.name"> <button @click="fn">button</button> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", data: { obj: {} }, methods: { fn() { this.$set(this.obj, 'name', "张三"); } } }) </script> </body> </html>
- 注意
$set(target, key, value); /* * target:不能是Vue实例,也不能是Vue的根属性 -- data属性 */
监听器
说明
监听器和计算属性的区别在于一是它不需要返回值,而是计算属性得到的是data中不存在的值,而监听器则监听的就是data中存在的值
可以用于数据持久化,即一旦数据发生变化,自动执行存储
放在watch属性中
,使用时根据监听对象类型(基本数据类型和引用数据类型)而有所不同
监听基本数据
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<input type="text" v-model="num">
</div>
<script src="../../node_modules/vue/dist/vue.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
num: 100
},
watch: {
num(newValue, oldValue) {
console.log("newValue: ", newValue);
console.log("oldValue: ", oldValue);
}
}
})
</script>
</body>
</html>
监听对象
方式一:直接监听对象的具体属性
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <input type="text" v-model="obj.name"> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", data: { obj: { name: "张三" } }, watch: { 'obj.name'(newValue, oldValue) { console.log("newValue: ", newValue); console.log("oldValue: ", oldValue); } } }) </script> </body> </html>
方式二:通过开启深度监听,监听整个对象
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <input type="text" v-model="obj.name"> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", data: { obj: { name: "张三" } }, watch: { obj: { deep: true, //开启深度监听(直接使用obj(newValue, oldValue)监听到的是引用类型obj的引用地址) immediate: true, //立即开启,即会监听到刚加载时的值 handler(newValue, oldValue) { console.log("newValue: ", newValue); console.log("oldValue: ", oldValue); } } } }) </script> </body> </html>
监听数组
方式一:数组也是个对象,所以可以用监听对象的方式进行监听
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <input type="text" v-model="arr[0].xiaoming.age"> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", data: { arr: [ { xiaoming: { gender: 'female', age: 12 } }, { xiaohong: { gender: 'male', age: 12 } } ] }, watch: { arr: { deep: true, handler (newArr) { console.log("newArr: ", newArr); } } } }) </script> </body> </html>
方式二:数组以拦截器方式进行监听
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <button @click="fn">button</button> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", data: { arr: [ { xiaoming: { gender: 'female', age: 12 } }, { xiaohong: { gender: 'male', age: 12 } } ] }, methods: { fn(){ /*特殊的不生效情形一:通过下标的方式修改数组的值*/ // this.arr[0] = {}; //解决方案:实质是内部使用了this.arr.splice(0, 1, 'abc') // this.$set(this.arr, 0, {}); /*特殊的不生效情形二*/ // this.arr.length = 0; //解决方案:即将所有的数据删除 this.arr.splice(0, this.arr.length); } }, watch: { /** * 监听的实现 * 基本数据和对象:通过setter方法实现 * 数组:通过拦截器,拦截push pop shift unshift reverse sort splice这7个方法 * 即一旦发现数组调用了这7个方法,就会触发监听 */ arr(newArr) { console.log("newArr: ", newArr); } } }) </script> </body> </html>
过滤器
语法
{{data | filterName}}
/*
* data:数据
* |:管道符
* filterName:过滤器名称
*/
全局过滤器
顾名思义,所有Vue实例都可以用的过滤器,注意注册代码要写在实例化代码之前
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <h1>{{date | dateFilter}}</h1> <h1>{{date | dateFilter('YYYY-MM-DD HH:mm:ss')}}</h1> </div> <script src="../lib/moment.js"></script> <script src="../../node_modules/vue/dist/vue.js"></script> <script> //如果带参数,则第一个参数为data中的数据,即要进行过滤操作的数据 Vue.filter('dateFilter', (date, format='YYYY-MM-DD') => { return moment(date).format(format); }); const vm = new Vue({ el: "#app", data: { date: new Date() } }) </script> </body> </html>
局部过滤器
只有当前vue实例可以使用,放在
filters
属性中,且当和全局过滤器的过滤器名重复时,局部过滤器优先生效<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <h1>{{date | dateFilter}}</h1> <h1>{{date | dateFilter('YYYY-MM-DD HH:mm:ss')}}</h1> </div> <script src="../lib/moment.js"></script> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", data: { date: new Date() }, filters: { dateFilter(date, format='YYYY-MM'){ return moment(date).format(format); } } }) </script> </body> </html>
生命周期
介绍
- 组件(实例)生命周期
所有的Vue组件,都是Vue实例,即一个组件就对应为一个实例,所以组件的生命周期即为实例的生命周期,具体而言,一个组件(实例)从开始到最后消失所经历的各种状态,就是它的一个生命周期
- 生命周期钩子函数
从组建被创建,到被挂到页面上运行,再到最后页面关闭、组件被销毁,这三个阶段总是伴随着组件的各种事件,而这些事件,就被称为组件的生命周期函数,也叫钩子函数,这些钩子函数的名称都是Vue中已经规定好的
- 生命周期的三个阶段
- 挂载阶段:进入页面
- 更新阶段:数据发生变化时
- 卸载阶段:关闭页面(实例卸载)
挂载阶段
数据初始化(找到数据)
new Vue(): instance
创建Vue实例
init: events & lifecycle
初始化Vue内部的事件,并开启Vue生命周期
- 钩子函数:
beforeCreate
此时尚未实现数据响应式,所以无法操作或获取data、methods中的数据或函数等
init: injections & reactivity
数据初始化,将data进行劫持并实现数据响应式
- 钩子函数:
created
此时已经实现数据响应式,可以发送ajax请求、操作data中的数据以及本地数据等
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"></div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", data: { msg: "hello" }, beforeCreate() { console.log("beforeCreate: ", this.msg); //undefined }, created(){ console.log("created: ", this.msg); //hello } }); </script> </body> </html>
找到模板
has 'el' option ?
判断是否有
el
属性,如果没有,可以手动调用$mount
来指定Vue实例管理的边界const vm = new Vue({ data: { msg: "hello" } }); vm.$mount("#app");
has 'template' option ?
判断是否有
template
属性,
如果没有则将el
指定的边界元素作为模板进行编译;
如果有则将该属性的值作为模板进行编译<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app" v-cloak>el模板:{{msg}}</div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", data: { msg: "hello" }, //template: `<h1>${this.msg}</h1>` --> 无法直接引入变量,显示为undefined template: `<h1>{{this.msg}}</h1>` //hello(有了template后,el的整个#app都不会显示) }) </script> </body> </html>
DOM渲染
- 钩子函数:
beforeMount
在渲染DOM之前调用,此时DOM中的数据还是模板
- 渲染DOM
创建
vm.$el
替换el
中的内容,即将写的模板内容翻译成DOM,然后渲染到页面中
- 钩子函数:
mounted
在DOM渲染完成后执行,此时可以操作DOM
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <h1>{{msg}}</h1> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", data: { msg: "hello" }, beforeMount() { console.log("beforeMount: ", document.querySelector('h1').innerHTML); //{{msg}} }, mounted(){ console.log("mounted: ", document.querySelector('h1').innerHTML); //hello } }); </script> </body> </html>
更新阶段
- 钩子函数:
beforeUpdate
当数据发生了变化就会进入更新阶段,而首先进入的就是该钩子函数,由于此时尚未开始更新,所以获取到的是更新前的DOM数据
Virtual DOM: rerander & patch
更新DOM,更新的方式是以patch补丁的方式进行的,而非将整个页面都进行重新渲染,即哪里发生改动,就更新哪里
- 虚拟DOM
每个标签上都有着众多的属性和方法,所以如果是直接使用真实DOM,一旦有标签发生变化,就会遍历该标签的所有属性和方法,若其含有子节点,还会再递归遍历其子节点,以查找是否存在差异,如果存在差异则执行更新。
虚拟DOM,实质上就是一个JS对象,因为操作js,更简单,速度也更快。
具体就是一个标签就对应着一个JS对象,该对象上含有该标签的所有信息(如tagName属性就是标签的名字),当标签变化时,会生成一个变化后的JS对象,通过DIFF算法,找到其中的差异部分,进而将该变化部分更新到真实DOM中
- 钩子函数:
updated
当更新完成后触发,此时获取到的是更新后的数据
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <h1>{{msg}}</h1> <button @click="fn">button</button> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const vm = new Vue({ el: "#app", data: { msg: "hello" }, beforeUpdate() { console.log("beforeUpdate: ", document.querySelector('h1').innerHTML); //hello }, updated(){ console.log("updated: ", document.querySelector('h1').innerHTML); //hello update. }, methods: { fn() { this.msg = "hello update." } } }); </script> </body> </html>
卸载阶段
- 钩子函数:
beforeDestroy
当页面关闭时,或者手动调用
vm.$destroy()
,将进入卸载阶段,首先触发的是就是该钩子函数,即在关闭之前执行的函数
Teardown
卸载过程,此时会将所有的watchers、子组件以及事件等都进行卸载,即Vue会主动清理自身的内容,包括响应式数据、@click绑定事件等
- 钩子函数:
destroy
- 常用于清理定时器、手动创建的DOM对象等
const vm = new Vue({ el: "#app", data: { msg: "第二阶段-更新", timerId: '' }, created() { this.timerId = setInterval(function () { console.log('正在执行定时任务中...') }, 1000); }, beforeDestroy() { clearInterval(this.timerId); //最好在beforeDestroy钩子函数中进行清理 }, destroy() { } });
组件
说明
- 介绍
- 可以看做是一个可以放复用的UI模块
- 小到一个标签,大到一个页面,都可以是一个组件
- 一个组件对应一个Vue实例
- 组件化开发
对于一个完整的页面,可以抽离成是有一个一个独立的组件组成的,而这些组件还可以复用在其他页面上,即它和模块化一样,都是一种可以实现复用的开发概念,只不过模块化侧重的是逻辑&业务方面,而组件则侧重UI&界面
全局组件和局部组件
- 全局组件
所有Vue实例都可以使用
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <!-- 2.组件的使用 --> <global-component></global-component> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> /* 1.组件的配置 */ Vue.component( 'globalComponent', //组件名 { //template:含义同el template: `<div>{{msg}} this is a global component demo</div>`, //data:含义同data属性,这里是个方法,但要求返回值为一个对象 //不直接为对象的原因:只让组件复用,而不让数据复用 data() { return { msg: 'hello' } }, //其余配置项同Vue示例中的配置项,常用如下所示 methods: {}, watch: {}, computed: {}, filters: {} } ); /* 3.组件的触发:使用时,必须创建对应的Vue实例才会生效 */ const vm = new Vue({ el: "#app" }) </script> </body> </html>
- 局部组件
只有当前Vue实例能用
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <!-- 3.使用 --> <child-component></child-component> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> /* 1.创建局部组件 */ const Child = { template: `<div>{{childMsg}}</div>`, data: function () { return { childMsg: '这是子组件(局部组件)中的数据' } } }; const vm = new Vue({ el: "#app", data: { parentMsg: "这是父组件的数据" }, /* 2.引入创建的局部组件,作为当前实例(组件)的子组件 */ components: { childComponent: Child } }); </script> </body> </html>
- 注意
- 注册的位置
组件代码需在Vue实例化代码之前编写
- 只能有一个根节点
template: `<div>component</div>` //正确 template: `<div>component</div><h1></h1>` //错误
组件的独立性
组件是独立的个体,不能直接访问其他组件的数据
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <child-component></child-component> <h1>{{childMsg}}</h1> <!-- 父组件无法访问子组件的数据 --> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const Child = { /* 子组件无法访问父组件的数据 */ template: `<div>{{parentMsg}}</div>`, data: function () { return { childMsg: '这是子组件(局部组件)中的数据' } } }; const vm = new Vue({ el: "#app", data: { parentMsg: "这是父组件的数据" }, components: { childComponent: Child } }); </script> </body> </html>
组件之间的通信机制
父传子
- 说明
指子组件访问父组件的数据
- 方式一:prop
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <Child :msg1="parentMsg1" :msg2="parentMsg2"></Child> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> Vue.component('child', { template: `<div>子组件:{{msg1}},{{msg2}}</div>`, props: ['msg1', 'msg2'] }); const vm = new Vue({ el: "#app", data: { parentMsg1: '父组件数据1', parentMsg2: '父组件数据2' } }) </script> </body> </html>
- 方式二:provide + inject
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <Parent></Parent> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> Vue.component('Child', { template: `<div>子组件:{{app.msg}}</div>`, inject: ['app'] }); Vue.component('Parent', { template: `<div>父组件:<Child></Child></div>`, data() { return { msg: '父组件数据' } }, provide() { return { app: this } } }); const vm = new Vue({ el: '#app' }) </script> </body> </html>
子传父
- 说明
指父组件访问子组件的数据
- 方式一:自定义事件 + $emit
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <Child @pfn="parentFunc"></Child> <span>{{msg}}</span> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> Vue.component('Child', { template: `<div>子组件</div>`, data() { return { childMsg: '子组件数据' } }, created() { /** * $emit:可以实现手动触发事件 * $emit传入的事件名称只能使用小写,不能使用大写的驼峰规则名称 */ this.$emit('pfn', this.childMsg); } }); const vm = new Vue({ el: "#app", data: { msg: '' }, methods: { parentFunc(childParam) { this.msg = childParam; } } }) </script> </body> </html>
- 方式二:sync + update
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <p>{{parentMsg}}</p> <Child :middleData.sync="parentMsg"></Child> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> Vue.component('Child', { template: ` <div>子组件: <input type="text" @keyup.enter="valueChanged" v-model="childMsg"> </div> `, data() { return { childMsg: '' } }, props: ['middledata'], //注意,会自动将middleData转为小写,即middledata,所以此处写的是小写的 methods: { valueChanged() { this.$emit('update:middledata', '子组件数据: ' + this.childMsg) } } }); const vm = new Vue({ el: '#app', data: { parentMsg: '父组件数据' } }) </script> </body> </html>
非父子
- 说明
指非父子关系的组件之间的数据传递
- 事件总线方式:接收方注册事件,发送方触发事件
如,当点击Foo组件时,Foo组件将数据传递给Bar组件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <Foo></Foo> <Bar></Bar> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> /** * 创建事件总线:实质上就是一个空的Vue实例 */ const bus = new Vue(); Vue.component('Foo', { template: `<div>Foo:<button @click="sendMsg2Bar">button</button></div>`, data() { return { fooMsg: 'Foo的数据' } }, methods: { sendMsg2Bar() { /** * 注意使用bus事件总线来触发 */ bus.$emit('barevent', this.fooMsg) } } }); Vue.component('Bar', { template: `<div>Bar:{{barMsg}}</div>`, data() { return { barMsg: '' } }, created() { /** * 一样的,用事件总线来注册事件 */ let that = this; bus.$on('barevent', dataFromFoo => { that.barMsg = dataFromFoo; }) } }); const vm = new Vue({ el: '#app' }) </script> </body> </html>
关于prop
- 单向数据流
指所有的prop属性,都会使得父子之间形成一个单向的下行绑定,即父级的更新都会向下流动到子组件中,反之则不行
之所以如此设计,是为了防止子组件改变父组件的状态,即子组件不允许修改父组件传来的prop数据,从而避免导致应用数据流向的难以理解
- 和双向数据绑定并不矛盾
双向数据绑定指的是同一组件的关系,而单向数据流则是组件之间的关系
- Vue是单向还是双向?
Vue是单向,
v-model
属于例外
- 只读属性
- 概述
对于prop中的属性,应该视为只读属性,原则上不建议对齐修改
- 基本数据类型:无法修改
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <Child :msg="parentMsg"></Child> </div> <script src="../../node_modules/vue/dist/vue.js">>> </script> <script> Vue.component('Child', { template: `<div>子组件:{{msg}}</div>`, props: ['msg'], mounted() { console.log(this.msg); //父组件数据 this.msg = "aaa"; //控制台报错 } }); const vm = new Vue({ el: "#app", data: { parentMsg: '父组件数据', } }) </script> </body> </html>
- 复杂数据类型:不修改引用地址即可
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <Child :msg="parentMsg"></Child> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> Vue.component('Child', { template: `<div>子组件:{{msg}}</div>`, props: ['msg'], mounted() { this.msg.key = 'hello'; //可以修改(但是不建议修改) console.log(this.msg); //父组件数据 this.msg = {}; //控制台报错,无法修改地址 } }); const vm = new Vue({ el: "#app", data: { parentMsg: { key: '父组件数据' }, } }) </script> </body> </html>
- 大小写问题
- 浏览器会自动将属性/标签转为小写
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <Child :childMsg="parentMsg"></Child> <!--会自动将属性childMsg转为childmsg--> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> Vue.component('Child', { template: `<DIV>子组件:{{childmsg}}</DIV>`, //会自动转为<div> props: ['childmsg'], //所以此处需要用小写的childmsg }); const vm = new Vue({ el: "#app", data: { parentMsg: '父组件数据' } }) </script> </body> </html>
- 使用
-
处理<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <Child :child-msg="parentMsg"></Child> <!--会自动将属性child-msg转为childMsg--> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> Vue.component('Child', { template: `<DIV>子组件:{{childMsg}}</DIV>`, //会自动转为<div> props: ['childMsg'], //此处可以直接用驼峰,无需转小写 }); const vm = new Vue({ el: "#app", data: { parentMsg: '父组件数据' } }) </script> </body> </html>
注意:
-
不适用于事件<child @parent-func="parentFunc"></child> //事件时:无法将parent-func转为parentFunc
- 类型和默认值
- 直接改属性赋予一个静态值:都是字符串类型
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <Child num="123" bool="true"></Child> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> Vue.component('Child', { template: `<div>{{num}} - {{bool}}</div>`, props: ['num', 'bool'], created() { console.log(typeof this.num); //string console.log(typeof this.bool); //string } }); const vm = new Vue({ el: "#app" }) </script> </body> </html>
- 属性前添加冒号:可获得真实类型
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <Child :num="123" :bool="true"></Child> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> Vue.component('Child', { template: `<div>{{num}} - {{bool}}</div>`, props: ['num', 'bool'], created() { console.log(typeof this.num); //number console.log(typeof this.bool); //boolean } }); const vm = new Vue({ el: "#app" }) </script> </body> </html>
- 指定所需类型:非指定类型则报错
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <Child :num="123" bool="true"></Child> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> Vue.component('Child', { template: `<div>{{num}} - {{bool}}</div>`, props: { num: Number, bool: Boolean //控制台报错 }, created() { console.log("num: ", this.num); //num: 123 console.log("bool: ", this.bool); //bool: true } }); const vm = new Vue({ el: "#app" }) </script> </body> </html>
- 指定默认值
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <Child :bool="true"></Child> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> Vue.component('Child', { template: `<div>{{num}} - {{bool}}</div>`, props: { num: { type: Number, default: 100 }, bool: { type: Boolean, default: false } }, created() { console.log("num: ", this.num); //num: 100 console.log("bool: ", this.bool); //bool: true } }); const vm = new Vue({ el: "#app" }) </script> </body> </html>
关于refs
- 作用
- 可以获取DOM元素 或 组件
- 可以实现子传父
- 原理
通过
ref
属性将组件或标签注册到refs
中,此后便可以用this.$refs
方式获取refs中的注册进来的标签或组件
- 示例
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <Child ref="child"></Child> <span ref="span">this is a span tag</span> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> Vue.component('Child', { template: `<div>子组件</div>`, data() { return { childMsg: "子组件数据" } } }); const vm = new Vue({ el: "#app", mounted() { console.log(this.$refs); //refs = { child: VueComponent {_uid: 1, _isVue: true, $options:...} 组件对象, span: <span ref="span"></span>} console.log(this.$refs.child.childMsg); //子组件数据(实现了子传父) console.log(this.$refs.span); //<span>this is a span tag</span> console.log(this.$refs.span.innerHTML); //this is a span tag } }) </script> </body> </html>
关于$attrs 和 $listeners
- 作用
$attrs:从上往下传递不被props引用的属性
$listeners:从上往下传递事件
- 示例
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <parent @penent="fn" a="aaa" b="bbb" c="ccc"></parent> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const Child = { template: `<button @click="cfn">child component</button>`, created() { console.log("child-attrs: ", this.$attrs); console.log("child-listeners: ", this.$listeners); }, methods: { cfn() { this.$emit('penent'); } } }; const Parent = { /** * 属性传递语法:v-bind="$attrs" * 事件传递语法:v-on="$listeners" */ template: `<div> 父组件:{{a}} <Child v-bind="$attrs" v-on="$listeners"></Child> </div>`, props: ['a'], created() { console.log("parent-attrs: ", this.$attrs); console.log("parent-listeners: ", this.$listeners); }, components: {Child} }; new Vue({ el: "#app", methods: { fn() { console.log("事件已经传递下去了") } }, components: {parent: Parent} }) </script> </body> </html>
组件复用的注意事项
- 问题
组件复用,即后续(非第一次)再使用该组件时,由于该组件已经存在,而如
created
生命周期函数只会在组件创建时数据初始化结束后调用一次,所以将不会再被触发
- 示例
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <router-link to="/one/1">one</router-link> <router-link to="/two/2">two</router-link> <router-view></router-view> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script src="../../node_modules/vue-router/dist/vue-router.js"></script> <script> const One = { template: `<div>one</div>`, created() { console.log(this.$route.params.id); //只会输出一次 } }; const router = new VueRouter({ routes: [ {path: '/one/:id', component: One}, {path: '/two/:id', component: One} ] }); new Vue({ router, el: "#app" }) </script> </body> </html>
- 方案
使用watch监听$route
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <router-link to="/one/1">one</router-link> <router-link to="/two/2">two</router-link> <router-view></router-view> </div> <script >> src="../../node_modules/vue/dist/vue.js"></script> <script src="../../node_modules/vue-router/dist/vue-router.js"></script> <script> const One = { template: `<div>one</div>`, watch: { $route(newValue) { console.warn(newValue.params.id); //每次点击都会输出 } }, created() { console.log(this.$route.params.id); //只会输出一次 } }; const router = new VueRouter({ routes: [ {path: '/one/:id', component: One}, {path: '/two/:id', component: One} ] }); new Vue({ router, el: "#app" }) </script> </body> </html>
单文件组件
- 说明
指后缀为
.vue
的组件,由三个部分组成
- template:模板结构
- script:组件代码逻辑
- style:样式
- 注意
单文件组件无法直接在浏览器中使用,需要经过如webpack等打包工具处理后,才能使用
- 示例:foo.vue
<template> </template> <script> export default { data() { return { msg: "hello vue" } }, computed() { }, methods: { } } </script> <style scoped> <!-- scoped:表示该css样式只限于在当前组件中使用 默认情况下:组件之间的样式会共享,如a组件的h1标签设置为红色背景,则b组件的h1标签背景也会变成红色 样式互不影响原理:Vue会给每个组件的所有DOM都添加上一个属性,格式为(data-v-组件唯一值),然后通过属性选择器进行DOM样式设置 注意:对于组件中动态添加的样式,如 vue-quill-editor, v-html等添加的样式,一样也是不起效果的 使动态添加的样式生效的方案: 方案一:再添加一个没有 scoped 的 style,并将动态添加的样式放入。(这种方式需要保证加入的样式不会影响到其他组件) 方案二:穿透语法( 固定父元素 /deep/ 动态子元素 ) 方案三:穿透语法( 固定父元素 >>> 动态子元素 )(不支持less) --> </style>
也可以将单个文件对接三个文件的引入
//User.vue <template src="./User.html"> //注意:User.html不需要其他标签,直接写template中的内容即可 </template> <script src="./User.js"> </script> <style scoped lang="less" src="./User.less"> </script>
混入
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<one></one>
<two></two>
</div>
<script src="../../node_modules/vue/dist/vue.js"></script>
<script>
const Mixin = {
methods: {
sayHi() {
console.log("Hello " + this.name)
}
}
}
Vue.component('one', {
template: `<button @click="sayHi">hi</button>`,
mixins: [Mixin],
data() {
return {
name: "ONE"
}
}
})
Vue.component('two', {
template: `<button @click="sayHi">hi</button>`,
mixins: [Mixin],
data() {
return {
name: "TWO"
}
},
methods: {
sayHi() { //如果组件自身就有该方法,优先使用组件自己的方法
console.log("Hello Two ~")
}
}
})
new Vue({
el: '#app'
})
</script>
</body>
</html>
内置组件
component
- 作用
控制要显示的组件
- 示例
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <button @click="show('one')">show one</button> <!--记得参数加上引号--> <button @click="show('two')">show two</button> <component :is="name"> <!--is的值表示要进行展示的组件,可以是组件名,也可以是组件的name属性值--> <one></one> <two></two> </component> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const ONE = { name: "one", template: `<p>ONE COMPONENT</p>` }; Vue.component("two", { name: "two", template: `<p>TWO COMPONENT</p>` }); new Vue({ el: "#app", data: { name: 'two' //先默认展示two组件 }, methods: { show(name) { this.name = name; } }, components: {one: ONE} }) </script> </body> </html>
keep-alive
- 缓存
上例中,如果组件one存在input,则再切到two再切回one时,input的值将不会被保留,keep-alive具有缓存作用
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <button @click="show('one')">show one</button> <!--记得参数加上引号--> <button @click="show('two')">show two</button> <keep-alive include="one,two"> <!--默认当中所有组件都会开启缓存,include用于指定开启缓存的组件(逗号前后不要有空格)--> <component :is="name"> <one></one> <two></two> </component> </keep-alive> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> const ONE = { name: "one", template: `<div><input type="text" placeholder="one" name="one"></div>` }; Vue.component("two", { name: "two", template: `<div><input type="text" placeholder="two" name="two"></div>` }); new Vue({ el: "#app", data: { name: 'two' //先默认展示two组件 }, methods: { show(name) { this.name = name; } }, components: {one: ONE} }) </script> </body> </html>
- 结合vue-touter的使用
<keep-alive> <router-view v-if="$route.meta.keepAlive"></router-view> </keep-alive> <router-view v-if="!$route.meta.keepAlive"></router-view>
路由
SPA
Single Page Application,单页面应用,即只有一个页面的应用
可以理解为原本的多个页面,抽取出共用部分(顶部、侧边等)作为唯一的页面,而页面的内容则通过组件的方式进行设置
优点是减少了请求体积,加快了页面响应速度,并可以进行局部刷新,提高用户体验,但是缺点是不利于SEO,且需要学习路由
路由的介绍
指的是浏览器URL中的哈希值(#hash)和展示视图内容之间的对应规则,是一套映射规则,一对一的映射规则,并由开发人员制定规则
具体为,当URL中的哈希值发生改变时,路由会根据制定好的规则,找到并展示对应的视图内容
对于Vue路由而言,一个哈希值,就对应为一个组件
路由的基本使用与声明式导航
- 声明式导航
点击标签进入入口,而无需手动去修改请求地址
- 安装
npm i vue-router
- 示例
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <router-link to="/one">ONE</router-link> <!--声明式导航形式的 入口--> <router-link to="/two">TWO</router-link> <!--最终会被转成<a href="#/two">TWO</a>--> <router-view></router-view> <!--出口--> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script src="../../node_modules/vue-router/dist/vue-router.js"></script> <script> const One = { template: `<div>one</div>` }; /*不能用以下方式注册的组件:Two is undefined*/ // Vue.component('Two', { // template: `<div>two</div>` // }); const Two = { template: `<div>two</div>` }; const router = new VueRouter({ routes: [ {path: '/one', component: One}, {path: '/two', component: Two} ] }); const vm = new Vue({ router, // 将路由实例挂载到Vue上 el: "#app", data: {} }) </script> </body> </html>
编程式导航
- $router
$route
为路由对象,$router
则是路由实例,主要用于编程式导航
- 常用API
- push
跳转,可实现点击One组件的button按钮,跳转到Two组件,有历史记录,可以回退到上一级
- back
回退,可实现点击One组件的button按钮,返回到上个页面
- replace
跳转,可实现点击One组件的button按钮,跳转到Two组件,没有历史记录,回退时只能直接返回到根路径
/
- 示例
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <router-link :to="{name: 'one'}">one</router-link> <router-link :to="{name: 'two'}">two</router-link> <router-view></router-view> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script src="../../node_modules/vue-router/dist/vue-router.js"></script> <script> const One = { template: ` <div> <button @click="handler('push')">push demo</button> <button @click="handler('back')">back demo</button> <button @click="handler('replace')">replace demo</button> </div> `, methods: { handler(type) { if (type === 'push') { this.$router.push('/two'); } else if (type === 'back') { this.$router.back(); } else if (type === 'replace') { this.$router.replace('/two'); } } } }; const Two = { template: `<div>two component</div>` }; const router = new VueRouter({ routes: [ {path: "/one", name: 'one', component: One}, {path: "/two", name: 'two', component: Two} ] }); new Vue({ el: "#app", router }) </script> </body> </html>
导航守卫
- 简介
所谓导航守卫,指的是在实现导航之前的拦截操作
- 需求
所有页面在进入前需先进入登录页进行登录
- 实现
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <router-link to="/other">other</router-link> <router-link to="/login">login</router-link> <router-view></router-view> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script src="../../node_modules/vue-router/dist/vue-router.js"></script> <script> const Login = { template: `<div>Login Page</div>` }; const Other = { template: `<div>Other Page</div>` }; const router = new VueRouter({ routes: [ {path: '/login', component: Login}, {path: '/other', component: Other} ] }); /** * to:目标路由对象$route * from:来源路由对象$route * next():执行下一步 * next(false):不执行 * next('/one'):跳转到one组件页面 */ router.beforeEach((to, from, next) => { if ('/login' === to.path) next(); else next('/login') }); new Vue({ router, el: "#app" }) </script> </body> </html>
- 全局导航守卫
/** * 全局前置守卫:beforeEach * 调用阶段:导航被确认之前 * 场景:一般用于登录拦截,如上例所示 **/ const router = new VueRouter({ //... }) router.beforeEach((to, from, next) => { //... }) /** * 全局解析守卫:beforeResolve * 调用阶段:也是导航被确认之前,但是它同时在所有组件内守卫 以及 异步路由组件被解析,即(const a = () => import('...'))之后才被调用 **/ /** * 全局后置守卫:afterEach * 调用阶段:导航被确认之后 **/ router.afterEach((to, from) => { //无需next})
- 单个路由独有的导航守卫
const router = new VueRouter({ routes: [ { path: '/foo', component: Foo, beforeEnter: (to, from, next) => { //... }, //... } ] })
- 组件级别的导航守卫
const ONE = { template: `<div></div>` beforeRouteEnter(to, from, next) { //进入组件路由之前触发 }, beforeRouteUpdate(to, from, next) { //当组件被复用时触发 //如先后调用了(http://...#/two/2)和(http://...#/two/3) }, beforeRouteLeave(to, from, next) { //当组件离开时触发,如跳转到其他组件 } }
动态路由和参数
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<router-link to="/detail/1">iphone</router-link> <!--参数:1-->
<router-link to="/detail/2">huawei</router-link> <!--参数:2-->
<router-link to="/detail">xiaomi</router-link> <!--参数:-->
<router-view></router-view>
</div>
<script src="../../node_modules/vue/dist/vue.js"></script>
<script src="../../node_modules/vue-router/dist/vue-router.js"></script>
<script>
const Detail = {
template: `<div>参数:{{ $route.params.id }}</div>`
};
const router = new VueRouter({
routes: [
{
/**
* :id表示获取id参数,此时id为必须有的参数
* :id?此时id参数可以不传
*/
path: '/detail/:id?',
component: Detail
}
]
});
const vm = new Vue({
router,
el: '#app'
})
</script>
</body>
</html>
路由-组件 传参
- 方式一:$route
const One = { template: `<div>{{ $route.params.id }}</div>` }; const router = new VueRouter({ routes: [ { path: '/one/:id', component: One, } ] });
- 方式二:props的布尔形式
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <router-link to="/test/100">click</router-link> <router-view></router-view> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script src="../../node_modules/vue-router/dist/vue-router.js"></script> <script> const Com = { template: `<div>{{ id }}</div>`, props: ['id'] }; const router = new VueRouter({ routes: [ { path: '/test/:id', name: 'com', component: Com, props: true //将参数(id)作为组件的属性存在 } ] }); new Vue({ router, el: "#app" }); </script> </body> </html>
- 方式三:props的对象形式
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <router-link :to="{name: 'com'}">click</router-link> <router-view></router-view> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script src="../../node_modules/vue-router/dist/vue-router.js"></script> <script> const Com = { template: `<div>{{ message }}</div>`, props: ['message'] }; const router = new VueRouter({ routes: [ { path: '/test', name: 'com', component: Com, props: {message: 'a demo'} //将参数(id)作为组件的属性存在 } ] }); new Vue({ router, el: "#app" }); </script> </body> </html>
- 方式四:props的函数形式
routes: [ { path: '/test', name: 'com', component: Com, props: to => {return {message: 'a demo'}} //将参数(id)作为组件的属性存在 } ]
入口高亮
- 说明
Vue预留了两个类,且被点击的入口将带有该类
router-link-exact-active
:精确匹配router-link-active
:模糊匹配
- 引用预留类
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <style> .hl-color { color: hotpink; } .hl-fz { font-size: 72px; } </style> </head> <body> <div id="app"> <router-link to="/one">one</router-link> <router-link to="/two">two</router-link> <router-view></router-view> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script src="../../node_modules/vue-router/dist/vue-router.js"></script> <script> const One = { template: `<div>---one---</div>` }; const Two = { template: `<div>---two---</div>` }; const router = new VueRouter({ routes: [ {path: '/one', component: One}, {path: '/two', component: Two} ], linkActiveClass: 'hl-color', linkExactActiveClass: 'hl-fz' }); const vm = new Vue({ router, el: '#app' }) </script> </body> </html>
- 精确匹配和模糊匹配
- 精确匹配
指
router-link-exact-active
的匹配模式,即当url#后的hash值,如/one,等于to
中的值,如:<router-link to="/one">ONE</router-link>
,会加上该类
- 模糊匹配
指
router-link-active
的匹配模式,即当url#后的hash值,如/one,包含to中的值,如:<router-link to="/">ONE</router-link>
中的/,会加上该类
- 只允许精确匹配
<router-link to="/" exact>only exact</router-link>
$route
- 说明
表示路由对象,是对
location.hash
以及路由实例new VueRouter({...})
的封装
- 常用属性
如有hash值:
#/api/:id?age=23#aaa
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <router-link to="/api/1?age=23#aaa">fullPath</router-link> <!-- /api/1?age=23#aaa --> <router-link to="/api/2?age=23#aaa">hash</router-link> <!-- #aaa --> <router-link to="/api/3/?age=23#aaa">params1</router-link> <!-- {} --> <!-- <router-link to="/api/3/?age=23#aaa">params2</router-link> <!– { "id": "3" } –>--> <router-link to="/api/4?age=23#aaa">path</router-link> <!-- /api/4 --> <router-link to="/api/5?age=23#aaa">query</router-link> <!-- { "age": "23" } --> <router-link to="/api/6?age=23#aaa">name</router-link> <!-- api_path --> <router-link to="/api/7?age=23#aaa">meta</router-link> <!-- {} --> <router-view></router-view> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script src="../../node_modules/vue-router/dist/vue-router.js"></script> <script> const ONE = { template: `<div>{{ $route.fullPath }}</div>` }; const TWO = { template: `<div>{{ $route.hash }}</div>` }; const THREE = { template: `<div>{{ $route.params }}</div>` }; const FOUR = { template: `<div>{{ $route.path }}</div>` }; const FIVE = { template: `<div>{{ $route.query }}</div>` }; const SIX = { template: `<div>{{ $route.name }}</div>` }; const SEVEN = { template: `<div>{{ $route.meta }}</div>` }; const router = new VueRouter({ routes: [ {path: '/api/1', component: ONE}, {path: '/api/2', component: TWO}, {path: '/api/3', component: THREE}, // {path: '/api/:id', component: THREE}, {path: '/api/4', component: FOUR}, {path: '/api/5', component: FIVE}, {name: "api_path", path: '/api/6', component: SIX}, {path: '/api/7', component: SEVEN}, ] }); const vm = new Vue({ router, el: '#app' }) </script> </body> </html>
- 一个hash url对应一个路由对象
- 元信息
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <router-link to="/test">click</router-link> <router-view></router-view> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script src="../../node_modules/vue-router/dist/vue-router.js"></script> <script> const Com = { template: `<div>component</div>`, created() { console.log(this.$route.meta.title); //应用举例 document.title = this.$route.meta.title; } }; const router = new VueRouter({ routes: [ { path: '/test', component: Com, meta: {title: 'test'} } ] }); new Vue({ el: "#app", router }) </script> </body> </html>
嵌套路由
- 需求
点击Parent:显示Parent组件内容
点击Child:显示Parent包含着Child 的内容
- 组件和出口的关系
出口
<router-view>
确定组件的显示位置
有几个出口,组件就显示几次
- 示例
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <router-link to="/parent">parent</router-link> <!-- 对应hash路径为 #/parent --> <router-link to="/child">child</router-link> <!-- 对应hash路径为 #/child --> <router-view></router-view> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script src="../../node_modules/vue-router/dist/vue-router.js"></script> <script> const Parent = { /** * <router-view/>用于显示children中相应的组件 * #/child * parent content * child content * child content */ template: `<div>parent content<router-view/><router-view/></div>` }; const Child = { template: `<div>child content</div>` }; const router = new VueRouter({ routes: [ { path: '/parent', component: Parent, children: [ {path: '/child', component: Child} //对应哈希路径为 #/child // {path: 'child', component: Child} 注意,此时对应的哈希路径为 #/parent/child ] } ] }); new Vue({ router, el: "#app" }) </script> </body> </html>
命名路由
- 场景
有时候通过一个名称来标识一个路由更加方便,特别是在链接一个路由,或者是执行一些跳转操作时
- 实现
routes: [ {path: '/one', name: 'one', component: One}, {path: '/two', component: Two} ] // ------------------------------- <router-link :to="{name: 'one'}">one</router-link> <!--冒号表示不解析为字符串-->
命名视图
- 需求
当哈希路径为/时,同时显示三个组件(header, main, footer)
- 实现
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <!-- 效果: header组件 header组件 main组件 footer组件 --> <router-view></router-view> <!--无name时,则使用default对应的组件,即header组件--> <router-view name="header"></router-view> <router-view name="main"></router-view> <router-view name="footer"></router-view> </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script src="../../node_modules/vue-router/dist/vue-router.js"></script> <script> const Header = { template: `<div>header组件</div>` }; const Main = { template: `<div>main组件</div>` }; const Footer = { template: `<div>footer组件</div>` }; const router = new VueRouter({ routes: [ { path: '/', components: { default: Header, header: Header, main: Main, footer: Footer } } ] }); new Vue({ router, el: "#app" }) </script> </body> </html>
重定向
- 需求
当进入
/
时,重定向到/one
组件
- 实现
方式一:通过
path
属性routes: [ {path: '/', redirect: '/one'}, {path: '/one', component: One} ]
方式二:通过
name
属性routes: [ {path: '/', redirect: {name: 'one'}}, {path: '/one', name: 'one', component: One} ]
方式三:通过
函数
routes: [ {path: '/', redirect: to => { return {name: 'one'} } }, {path: '/one', name: 'one', component: One} ]
脚手架:vue-cli
说明
vue-cli,一个可以快速生成 vue 项目的脚手架工具,只要执行一条命令即可,且生成的项目不仅包含基本目录结构,同时还将 webpack 配置项全部配置好
安装与使用
npm i vue-cli -g vue -V //查看的是脚手架 vue-cli 的版本号 vue init webpack 项目名称(vuecli_demo)
项目结构
build: "webpack配置文件目录" config: "vue项目配置文件目录" src: "源文件目录" src/assets: "静态资源目录(参与打包)" src/components: "组件目录" src/router: "路由目录" src/App.vue: "根组件" src/main.js: [ "1. 引入路由,创建Vue实例,并将路由挂载到实例中", "2. 引入根组件App.vue,通过使用 template 属性引用了该组件作为模板输出" "3. 同时有 el 和 template 属性,最终 template 会覆盖 el,但是 el 又不可删除,因为需要先用 el 进行占位" "4. import router from './router',一是 js 后缀可以省略,二是 index 名称可以省略,所以补全了应该是:'./router/index.js'" ] static: "静态资源目录(不参与打包)" static/.gitkeep: "git上传时,默认不会上传空文件夹,添加 .gitkeep 则可被上传" .editorconfig: "编辑器配置,用于不同编辑器(vscode、webstorm等)的规范约定,如缩进数等,不过需要配合插件使用才能生效" .eslintignore: "用于配置一些不需要进行 eslint 检测的文件" .postcssrc.js: "处理 css 的配置文件" index.html: "入口界面"
el & template & render & $mount
- el & template
- el 指定了Vue的管理范围,template 是Vue的html模板;
- 当template不存在时,el 指定范围中的html内容将作为模板;
- el和template同时存在时,template将替换掉el中的内容,如下,显示的是
child
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> abc </div> <script src="../../node_modules/vue/dist/vue.js"></script> <script> new Vue({ el: "#app", template: `<div id="app">child</div>` }) </script> </body> </html>
或者
<script> const Child = { template: `<div id="app">child</div>` }; new Vue({ el: "#app", components: { Child }, template: `<Child/>` }) </script>
- el & $mount
m o u n t 和 e l 一 样 都 是 指 定 挂 载 范 围 , 只 不 过 mount和el一样都是指定挂载范围,只不过 mount和el一样都是指定挂载范围,只不过mount需要手动挂载const vm = new Vue({ el: "#app", data: {} }); //等价于 const vm = new Vue({ data: {} }).$mount("#app");
- el & template & render
同时有 el 和 template 以及 render 时,将显示 render 的内容,即其优先级最高
const vm = new Vue({ el: "#app", template: "<div>template内容</div>", render: function(createElement) { return createElement(组件名); //除了createElement标准用法,这里还可以直接用组件名 } });
main.js & index.html & App.vue
- main.js & index.html
el: "#app" //可见mian.js中的el挂载的就是 index.html 中的 #app
二者间通过插件
html-webpack-plugin
产生关联关系,即在index.html中会引入main.js
- main.js & App.vue
components: { App } //先将App作为局部组件引入 template: '<App/>' //再将App作为模板显示(template会替换掉el)
Vue的两种编译模式
- 完整版(运行时 + 编译器)
除了可以使用
render
函数渲染组件,由于包含编译器,所以也可以直接使用template
模板属性;
使用script
标签引入的vue.js
就是完整版new Vue({ template: '<div>{{ hi }}</div>' });
- 运行时版
即
vue.runtime.esm.js
,只能使用render
函数来渲染组件;
通过import Vue from 'vue'
引入的即为运行时版本new Vue({ render (h) { return h('div', this.hi) } });
- 总结
当使用vue-loader或vueify的时候,*.vue文件内部的模板会在构建的时候预编译成JavaScript,在最终打好的包里实际上是不需要编译器的,所以只用运行时版本即可(vue-cli默认就是运行时版本)
脚手架3.0
//安装
npm i -g @vue/cli
//项目初始化
vue create 项目名称
//开发
npm run serve
//打包部署
npm run buiild
//添加 vuex
//1. 在src下创建 store/index.js (名字不限定)
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
const store = new Vuex.Store({
state: () => {
return {
msg: "hello vuex"
}
}
});
export default store;
//2. 在main.js中进行引入挂载
import Vue from 'vue'
import App from './App.vue'
import store from './store'
Vue.config.productionTip = false
new Vue({
store,
render: h => h(App),
}).$mount('#app')
//3. 在HelloWorld中使用
<template>
<div class="hello">
<p>{{msg}}</p>
<button @click="fn">say</button>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data: () => {
return {
msg: "i want to say ..."
}
},
methods: {
fn() {
this.msg = this.$store.state.commonMsg;
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.hello > p {
color: aqua;
}
</style>
Vuex
说明
Vuex是对组件的 data 的数据管理工具,在此之前,组件之间的数据通信是通过 如父传子、子传父 等方式进行的,vuex作为中间的数据管理,集中统一地管理项目中组件之间需要通讯的数据
基本使用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script src="../../node_modules/vue/dist/vue.js"></script>
<script src="../../node_modules/vuex/dist/vuex.js"></script>
<script>
/*创建vuex实例*/
const store = new Vuex.Store({
strict: true, //开启strict模式之后,只有在 mutations 中才能修改 state 中的数据
state: { //相当于vue的data
msg: "test"
},
mutations: { //相当于vue的methods
changeMsg(stat, payload) { //mutations中的所有方法的第一个参数都是 state
this.state.msg += payload.newMsg;
}
},
getters: { //相当于vue的computed
test(){}
}
});
//store.state.msg = "changed",strict模式已开启,无法直接修改
store.commit('changeMsg', {newMsg: " changed"}); //使用mutations中的方法进行修改
console.log(store.state.msg); //test changed
</script>
</body>
</html>
整合Vue
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<h1>{{$store.state.num}}</h1>
<button @click="fn">increment</button>
</div>
<script src="../../node_modules/vue/dist/vue.js"></script>
<script src="../../node_modules/vuex/dist/vuex.js"></script>
<script>
const store = new Vuex.Store({
state: {
num: 100
},
mutations: {
numIncrement(state) {
this.state.num += 1;
}
}
});
const vm = new Vue({
store,
el: "#app",
data: {},
methods: {
fn() {
this.$store.commit('numIncrement')
}
}
})
</script>
</body>
</html>
Actions
- 说明
类似于 mutations,不同之处在于Actions通过调用mutations中的方法,而不是直接自己去修改状态;另外Actions可以包含异步操作,而mutations中不能使用异步(会触发严格模式的校验而报错);还有就是 mutations通过commit触发执行,而actions则通过dispatch
- 示例
store/index.js
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ state: { commonMsg: "hello vuex", num: 100 }, mutations: { numIncrement(state, payload) { this.state.num += 1; } }, actions: { asyncNumIncrement(context, payload) { //context相当于 store 实例 setTimeout(function () { context.commit('numIncrement') }, 1000); } } })
HelloWorld.vue
<template> <div class="hello"> <p>{{msg}}</p> <p>{{$store.state.num}}</p> <button @click="fn">say</button> </div> </template> <script> export default { name: 'HelloWorld', data: () => { return { msg: "i want to say ..." } }, methods: { fn() { this.msg = this.$store.state.commonMsg; this.$store.dispatch('asyncNumIncrement') } } } </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> .hello > p { color: aqua; } </style>
辅助函数
mapGetters
- 作用
简化 getters 的引用
- 实现
将getters中的方法映射到具体组件的computed中,即当做组件的计算属性进行使用
- 示例
store/index.js
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ state: { commonMsg: "hello vuex", num: 100, date: new Date() }, getters: { showDate(state) { /** * 注意这里不能用this.date,因为转到具体组件时,该组件不一定有date属性 */ return "date is: " + state.date } }, mutations: { numIncrement(state, payload) { this.state.num += 1; } }, actions: { asyncNumIncrement(context, payload) { //context相当于 store 实例 setTimeout(function () { context.commit('numIncrement') }, 1000); } } })
HelloWorld.vue
<template> <div class="hello"> <p>{{msg}}</p> <p>{{$store.state.num}}</p> <p v-show="showDate">{{date}}</p> <button @click="fn">say</button> </div> </template> <script> import {mapGetters} from 'vuex' export default { name: 'HelloWorld', data: () => { return { msg: "i want to say ...", showDate: false } }, computed: { ...mapGetters({"date": "showDate"}) }, methods: { fn() { this.msg = this.$store.state.commonMsg; this.$store.dispatch('asyncNumIncrement'); this.showDate = !this.showDate; } } } </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> .hello > p { color: aqua; } </style>
mapMulations
- 作用
简化 mutations 的引用
- 实现
将 mutations 中的方法映射到组件的 methods 中
mapActions
- 作用
简化 actions 的引用
- 实现
将 actions 中的方法映射到组件的 methods 中
常搭配使用的其他框架/工具/插件
WebPack
介绍
- 说明
一个前端模块化打包(构建)工具
- 功能
- 依赖处理
可以 让模块更容易复用、避免全局注入导致的冲突、避免重复加载或加载不需要的依赖
- 代码合并
项目的模块化 以及 模块彼此之间的互相引用 都会导致文件分散问题,WebPack可以将分散的模块集中打包形成一个大文件,这样还能减少Http的链接请求次数,且打包时还会进行代码压缩,从而优化代码体积
- 各种插件
WebPack可以将一些浏览器不能直接运行的扩展语言,如SASS,LESS等转为可以识别的CSS, 且使用
babel
插件还可以将ES6+
语法转为浏览器可以识别的ES5-
语法
- 工作原理
- 代码分析
WebPack会分析代码,找到
require, import, define
等关键字,然后将其替换为对应模块的引用
- 任务处理
WebPack会将项目视为一个整体,只要给定一个主文件(
index.js
),WPack就会从这个文件开始,找到项目所有的依赖文件,并使用loaders
处理,最后打包成一个浏览器可识别的 js 文件
- 四个核心概念
entry
(入口)明确要打包哪个文件
output
(出口)明确打包到哪里
loader
(加载器)WebPack默认只能加载 js,通过 loader 可以加载其他文件
plugin
(插件)处理加载器无法完成的功能
使用步骤
- 先进行项目初始化
npm init //逐步执行并生成,注意项目名称不能有汉字,也不能叫webpack npm init -y //快速生成
- 安装 WebPack
npm i webpack webpack-cli -D /* * -D:只需在开发阶段使用 * webpack-cli:提供了一些可以在终端中使用的命令 */
- 创建待打包文件,如 main.js
- 编写打包脚本
在项目初始化中生成的
package.json
中添加脚本内容"script": { "build": "webpack main.js" } /* * 可以配置```mode```选项,它有两个取值 * development:开发环境 * production:生产环境(默认 & 会进行代码压缩) */ "script": { "build": "webpack main.js --mode production/development" }
- 执行打包
npm run build //打包生成的目录为 dist,目录中就含有 main.js 文件
Vue的两种安装模式
ES6 转 ES5
- 准备工作
cd demo npm init -y npm i webpack webpack-cli -D npm i jquery package.json "scripts": { "build": "webpack ./src/main.js --mode development" } md src/ cd src copy nul index.html type nul main.js
- index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>隔行变色</title> </head> <body> <ul> <li>***********</li> <li>***********</li> <li>***********</li> <li>***********</li> <li>***********</li> </ul> <script src="./main.js"></script> </body> </html>
- src/main.js
import $ from 'jquery' //import 是 es6 语法 $('ul > li:odd').css("color", "hotpink");
- 转换
直接查看 index.html 报错,浏览器无法识别 import 语句
执行:npm run build
- 替换
index.html 引用 dict 的 main.js,此时再打开不报错并有效果了
babel
- 说明
webpack自身只能处理 import/export 这些模块化语法,对于其他 js 新语法,则需要使用 babel 来转换
- 安装
安装方式一
npm i babel-core babel-loader@7 -D /* * babel-core:babel的核心包 * babel-loader:用于加载 js 文件,并将其代码内容交给 babel-core 处理并解析为 es5- 的 js 文件 */
安装方式二
npm i babel-preset-env babel-preset-stage-2 /* * babel-preset-*:指定解析什么版本的语法 * babel-preset-env:能够解析 es2015、16、17、18 这几个标准的 js 语法 * babel-preset-stage-2:可以解析暂时未被采纳为标准的语法,如: * 'abc'.padStart(10, '6'):表示在原有字符串'abc'的基础上,将字符数扩充到10个,不足部分在'abc'前面用6补充 * babel-polyfill和babel-plugin-transform-runtime:也是用来做兼容处理的 */
.babelrc
需要在根路径创建这个文件,作为 babel 的配置文件
{ "presets": ["env", "stage-2"] }
然后,可以在 main.js 中编辑些浏览器不支持的 es6 语法做校验
let obj = { name: "xiaoming", age: 12 } let o = { ...obj }; // edge浏览器不支持
webpack.config.js
如,对于打包命令的编写,可以在 package.json 中编写命令
/* * webpack 入口 --output 出口 */ webpack ./src/main.js --output ./dist/app.js --mode development
此外,如果指写明
webpack
,则会自动查找并执行根目录中默认的脚本文件webpack.config.js
// package.json "scripts": { "build": "webpack" } // webpack.config.js const path = require('path'); module.exports = { //指定入口 entry: path.join(__dirname, './src/main.js'), //指定出口 output: { path: path.join(__dirname, './dist'), filename: 'app.js' }, //指定模式 mode: 'development' };
执行
npm run build
即可
插件:html-webpack-plugin
- 作用
可以根据指定的模板文件(index.html),自动生成一份新的 index.html,并注入到 dist 目录中,这个新的 index.html 会自动引入入口文件 main.js,即无需在 index.html 中做任何引入了
- 使用
//1. 安装 npm i html-webpack-plugin -D //2. 在 webpack.config.js 中进行引入 const path = require('path'); const htmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: path.join(__dirname, './src/main.js'), output: { path: path.join(__dirname, './dist'), filename: 'app.js' }, mode: 'development', plugins: [ new htmlWebpackPlugin({ template: path.join(__dirname, './src/index.html') }) ] };
插件:webpack-dev-server
- 作用
为使用 webpack 打包提供一个服务器环境,它会将打包好的文件放到该服务器中,并自动开启服务,同时监控 src 中的源文件变化,然后在服务器中同步这些变化,并自动刷新浏览器
- 使用
// 1. 安装 npm i webpack-dev-server -D // 2. 配置 /* 命令行方式配置 :注意和 webpack 版本适配*/ /* * --open:自动刷新浏览器 * --hot:热更新,主要用在 css 上 */ "scripts": { "build": "webpack", //发布用 "dev": "webpack-dev-server --open --port 56789" //开发用 } /* webpack.config.js方式配置 */ // package.json "scripts": { "build": "webpack", "dev": "webpack-dev-server" } // webpack.config.js const path = require('path'); const htmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: path.join(__dirname, './src/main.js'), output: { path: path.join(__dirname, './dist'), //会自动创建dist目录 filename: 'app.js' }, mode: 'development', plugins: [ new htmlWebpackPlugin({ template: path.join(__dirname, './src/index.html') }) ], devServer: { open: true, port: 56789 } };
处理非 js 文件
- 处理 css
项目结构
src/ src/assets src/assets/demo.css src/index.html src/main.js
demo.cssul { list-style: none; }
main.jsimport $ from 'jquery' import './assets/demo.css' $('ul > li:odd').css("color", "hotpink");
安装npm i style-loader css-loader -D /* * css-loader:用于读取 css 文件内容,并放到一个模块中 * style-loader:用于创建 style 标签,将模块中的内容加载进来,然后再将该标签插入到页面中 */
引入const path = require('path'); const htmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: path.join(__dirname, './src/main.js'), output: { path: path.join(__dirname, './dist'), filename: 'app.js' }, mode: 'development', plugins: [ new htmlWebpackPlugin({ template: path.join(__dirname, './src/index.html') }) ], devServer: { open: true, port: 56789 }, module: { rules: [ { //test表示正则匹配,整个代码表示如果文件后缀为.css,用'style-loader'和'css-loader'这两个loader去处理 //读取顺序:从后向前读取,即先读取css-loader,再读取style-loader test: /\.css$/, use: ['style-loader', 'css-loader'] } ] } };
热更新:--hot即局部更新,如在 css 文件中更新了背景,这时页面也只会更新该背景
- 处理 less
npm i less -g //less-loader需要依赖less npm i style-loader css-loader less-loader -D use: ['style-loader', 'css-loader', 'less-loader']
- 处理 图片
安装
npm i url-loader file-loader -D
配置
/* * 默认会将图片转为 base64 格式(大图不适合用base64处理), * 将图片文件转换为base64编码并载入浏览器能够减少http请求数, * 但是增大了js或html文件的体积, * limit表示在多少字节内进行base64编码,默认10000byte,大约10kb * 另外如果使用base64:url-loader发挥作用 * 否则:file-loader发挥作用(将图片名称进行了md5编码) */ { test: /\.(jpg|gif|png)$/, use: ["url-loader?limit=100000\""] } //或者 { test: /\.(jpg|gif|png)$/, use: { loader: 'url-loader', options: { limit: 100000 } } }
demo.css
ul { list-style: none; background: url("./demo.png"); }
- 处理 字体图标
1.拷贝iconfont文件到assets中 2.import .../iconfont.css 3.使用<i class="iconfont icon-zan"></i>
项目打包及其优化
- dist
打包好的文件都在 dist 目录中,需要注意的是其中的 index.html虽然是入口页面,但是不能直接打开,需要以服务文件的方式去访问,如可以使用 http-server
- 打包目录说明
vendor: "第三方插件目录" app.js: "对应源文件 main.js" manifest: "主要用于处理依赖关系" map文件: "便于查找错误,因为打包的代码经压缩后,浓缩为一行,而通过这些 map 文件,可以定位到具体的出错位置 " index.html: "项目入口页面"
- 打包优化
- 优化一:降低 app 模块的大小
app.js是首先打开的入口加载文件,其大小决定了首屏加载速度,所以需要尽可能减少在 app.js 中引入其他模块,并且可以使用懒加载的方式引入
//原来 import Vue from 'vue' import VueRouter from 'vue-router' import Login from '../components/login/Login.vue' import Home from '../components/home/Home.vue' import Users from '../components/users/Users.vue' import Roles from '../components/roles/Roles.vue' import Rights from '../components/rights/Rights.vue' import Category from "../components/category/Category.vue"; import Goods from "../components/goods/Goods.vue" Vue.use(VueRouter); //优化后 import Vue from 'vue' import VueRouter from 'vue-router' import Login from '../components/login/Login.vue' const Home = () => import('../components/home/Home.vue'); //将组件定义为异步组件 const Users = () => import('../components/users/Users.vue'); const Roles = () => import('../components/roles/Roles.vue'); const Rights = () => import('../components/rights/Rights.vue'); const Category = () => import("../components/category/Category.vue"); const Goods = () => import("../components/goods/Goods.vue"); //-----------------------------只需要登录页,其他的重定向即可 const router = new VueRouter({ routes: [ //重定向 {path: '/', redirect: '/login'}, {path: '/login', name: 'login', component: Login}, {path: '/home', name: 'home', component: Home, children: [ {path: '/users/:page?', name: 'users', component: Users}, {path: '/roles', name: 'roles', component: Roles}, {path: '/rights', name: 'rights', component: Rights}, {path: '/categories', name: 'categories', component: Category}, {path: '/goods', name: 'goods', component: Goods}, {path: '/goods-add', name: 'goods-add', component: GoodsAdd} ]} ] }); //-----------------------------
- 优化二:降低 vendor 体积大小
vendor的体积较大,所以可以将本地一些依赖换成在线服务器地址
如 vue 和 依赖vue-router//1. index.html中进行引入 <script src="cdn地址"></script> //2.build/webpack.base.conf.js中添加 externals 配置项(在resolve前面) externals: { /* *key: 包名(import导包时from跟着的即为包名) *value: 插件的全局变量名(可以查看cdn在线文件中的function(e, t),第一个参数为window,看到e.V**,即大写开头的即为全局变量名称) */ vue: "Vue", axios: "axios", 'vue-router': "VueRouter", 'element-ui': "ELEMENT" }
Axios
说明
- 简介
一个发送ajax的工具库,基于Promise,是对ajax的封装,可以在浏览器或node中使用
- 安装
npm i axios
- 引入
//node const axios = require('axios'); //浏览器 <script src="**/axios.js"></script>
语法
axios.get/post/delete/put/patch(url [, config, data]).then(res => { //... });
/**
* then:请求成功时会调用此方法,参数res为请求获得的数据
* config:非必填,是个对象
* params:请求参数,是个对象,get请求时会用到
* headers:请求头,是个对象
* data:非必填,请求体对象,如post请求时会用到
*/
Vue整合
//main.js
Vue.prototype.$axios = axios; //方便调用
//此外还可以在main.js中配置拦截器,以统一设置token之类的