Vue的核心建立思路
Vue是一套用于构建用户界面的渐进式框架,Vue 被设计为可以自底向上逐层应用。他是一个组合的思想,需要什么就一层层往上堆。
- 红色:声明式渲染,借用mvvm的设计思想,最底层的一个核心
- 粉色:组件系统
- 蓝色:路由
- vue-router
- 天蓝色:状态管理
- vuex
- 绿色:构建系统
- vue-cli 命令行工具
- vue-test-utils 单元测试
- 安全网:测试
- 单元测试
- jest 语法
第一层:声明式渲染
安装
-
利用script导入
- 学习的最新版本
- 生产的稳定版本
- 原生esm的版本
- script导入vue
在页面中打印一个Vue就会发现一个构造器,我们就可以使用这个构造器构造我们的页面。
简单使用
- mvvm思想 在vue里面的体现
- app div 是一个视图层view
- vue 构造器是一个 模型层 model
- vm 层就是vm框架给我们写好的,把视图和模型结合起来,所以我们只要关心数据就可以了
- v m层联系的方法,使用vue api el 和使用
{{}}
插值语法把页面数据联系起来。
<div id="app">{{msg}}</div>
<script>
app = new Vue({
el: '#app',
data:{
msg:'Hello Vue'
}
})
插值语法
{{}}
Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进 DOM 的系统。这个插值语法,里面还可以使用js表达式。
这样就可以把标签直接的文本数据和模型、视图关联起来
基本指令
-
v-bind (:)
- 绑定标签属性
- 可以使用表达式
<div id="app" :test="test(keyName)"> {{msg}}</div> <script> app = new Vue({ el: '#app', data:{ msg:'Hello Vue', test: 'attrVue' } }) <div id="app" :test="test+'12344'"> {{msg}}</div>
-
v-on (@)
-
事件绑定 @事件名 = '函数名'
-
需要在模型层,的methods 属性中声明这个函数
-
修饰符
.stop
- 调用event.stopPropagation()
。.prevent
- 调用event.preventDefault()
。.capture
- 添加事件侦听器时使用 capture 模式。.self
- 只当事件是从侦听器绑定的元素本身触发时才触发回调。.{keyCode | keyAlias}
- 只当事件是从特定键触发时才触发回调。.native
- 监听组件根元素的原生事件。.once
- 只触发一次回调。.left
- (2.2.0) 只当点击鼠标左键时触发。.right
- (2.2.0) 只当点击鼠标右键时触发。.middle
- (2.2.0) 只当点击鼠标中键时触发。.passive
- (2.3.0) 以{ passive: true }
模式添加侦听器
-
-
v-if
- v-else-if
- v-else
-
v-show
- 根据表达式之真假值,切换元素的
display
CSS property。
- 根据表达式之真假值,切换元素的
-
v-for
- 我们可以用
v-for
指令基于一个数组来渲染一个列表。v-for
指令需要使用item in items
形式的特殊语法,其中items
是源数据数组,而item
则是被迭代的数组元素的别名。
- 我们可以用
-
v-model
-
v-slot
- 插槽使用
事件处理
事件绑定,在methods中声明这个事件。
参数:
- 在
@click ="clickBtn(..arg)"
中传递参数,那么clickBtn(e) 中的e会变成参数 - 获取事件对象
@click ="clickBtn(..arg,$event)"
根据它提供的$event 再次获取事件对象
<div id="app" :test="test"> {{msg}}
<button @click ="clickBtn">Click</button>
</div>
<script>
app = new Vue({
el: '#app',
data:{
msg:'Hello Vue',
test: 'attrVue'
},
methods: {
clickBtn(e){
console.log(e);
console.log("click button");
}
},
})
计算属性
Computed 方法。有时候我们在插值模板中需要写很长的一段表达式,但是这个表达式不易变更得。那么可以使用Computed。它是一个多对一的关系。
例如这样:
<div id="app" :test="test">{{msg.split("").reverse().join("")}}</div>
<script>
app = new Vue({
el: '#app',
data:{
msg:'Hello Vue',
test: 'attrVue'
}
})
那么插值得表达式可读性降低。要研究一段事件才知道是什么。改写
<div id="app" :test="test">{{msgReverse}}</div>
<script>
app = new Vue({
el: '#app',
data:{
msg:'Hello Vue',
test: 'attrVue'
},
computed: {
msgReverse() {
return this.msg.split("").reverse().join("");
}
},
})
methods + Computed 区别
那么使用methods也能实现这个效果,那么他们区别是一个缓存得问题。如果使用methods 他就会每次都会执行这个函数,性能会下降,如果是Computed 则他会把计算好的结果存到内存中,如果变量没有改变则不会触发函数。
侦听器 watch
Vue 提供了一种更通用的方式来观察和响应 Vue 实例上的数据变动:侦听属性。它可以是一个一对多得关系。即一个数据变更可以影响其他数据。
类型:{ [key: string]: string | Function | Object | Array }
参数:immediate || deep
// 该回调将会在侦听开始之后被立即调用
d: {
handler: 'someMethod',
immediate: true
},
// 该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深
c: {
handler: function (val, oldVal) { /* ... */ },
deep: true
},
<div id="app"> {{msg}}
<hr>
{{msg1}}
{{msg2}}
{{msg3}}
</div>
<script>
app = new Vue({
el: '#app',
data:{
msg:'Hello Vue',
msg1:'',
msg2:'',
msg3:'',
},
watch:{
msg(newV,oldV){
console.log(newV,oldV);
this.msg1 = newV + '1';
this.msg2 = newV + '2';
this.msg3 = newV + '3';
}
}
})
而且它可以监听data 里面属性里面某个key 的值得变化
// watch vm.e.f's value: {g: 5}
'e.f': function (val, oldVal) { /* ... */ }
}
条件渲染
- v-if
- v-else-if
- v-else
- v-show
- 控制 display
- v-if vs v-show
v-if
是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。- v-if
也是**惰性的**:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
v-show
就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。- 一般来说,
v-if
有更高的切换开销,而v-show
有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用v-show
较好;如果在运行时条件很少改变,则使用v-if
较好。
for列表循环
v-for
-
遍历数组
-
遍历对象
- 2个参数
- (Val,key)
- 3个参数
- (val,key,index)
- 2个参数
无论是遍历数组还是对象,都可以使用 in
就可以操作了
<div id="app"> {{msg}}
<div v-for = '(item,index) in list'>
{{index}} --> {{item.name}} --{{item.age}}
</div>
</div>
<script>
app = new Vue({
el: '#app',
data:{
msg:'Hello Vue',
list:[
{
name:'Ellen',
age:12
},
{
name:'Katty',
age:15
},
]
}
})
-
带有
v-for
的<template>
- 循环渲染一段包含多个元素的内容
-
v-for 和 v-if 一同使用
-
v-for优先于v-if
-
结合使用
<div v-for = '(item,index) in list' :key = "index" v-if = 'item.age >= 13'>
-
-
设置 key 属性,不绑定key值vue会出警告,而且不利于diff算法的执行
- 复用
- diff 算法优化
<div id="app"> {{msg}} //绑定Key值 <div v-for = '(item,index) in list' :key = "index"> {{index}} --> {{item.name}} --{{item.age}} </div> </div> <script> app = new Vue({ el: '#app', data:{ msg:'Hello Vue', list:[ { name:'Ellen', age:12 }, { name:'Katty', age:15 }, ] } }) </script>
默认情况下,在渲染
DOM
过程中使用 原地复用 ,这样一般情况下会比较高效,但是对于循环列表,特别是依赖某种状态的列表,会有一些问题,我们可以通过:key
属性,来给每个循环节点添加一个标识
Class 和Style
将 v-bind
用于 class
和 style
时,Vue.js 做了专门的增强。表达式结果的类型除了字符串之外,还可以是对象或数组。
Class
同样类名有数组及对象的写法:
数组写法:
<div :class="['box1', 'box2']"></div>
对象写法:
<div :class="{'box1': isActive, 'box2': isChecked}"></div>
data 里面声明的:
<div v-bind:class="classObject"></div>
data: {
classObject: {
active: true,
'text-danger': false
}
}
//或者
是一个数组加对象
<style>
body{
font-size: 12px;
}
.color{
color: red;
}
.size{
font-size: 20px;
}
</style>
</head>
<body>
<div id="app"> {{msg}}
<hr />
<span :class='classes'>class style</span>
</div>
<script>
app = new Vue({
el: '#app',
data:{
msg:'Hello Vue',
classes:[
'color',
{
size:false
}
]
}
})
style
v-bind 针对样式有不同的写法 ,可以写成对象形式:
<div :style=" {width: '100px', height: '100px', background: 'green' }"></div>
也可以写成数组形式 :
<div :style="[style1, style2]"></div>
不过在vue中样式要使用驼峰命名法
<div id="app"> {{msg}}
<hr />
<!-- :class='classes' -->
<span :style = 'styleInfo'>class style</span>
</div>
<script>
app = new Vue({
el: '#app',
data:{
msg:'Hello Vue',
// classes:[
// 'color',
// {
// size:false
// }
// ],
styleInfo:[
{
color:'blue'
},
{
fontSize: '36px'
}
]
}
})
表单数据双向绑定
-
v-model 双向数据绑定(语法糖)
<input type="text" v-model="title" /> {{title}}
原理
<input type="text" @input="handleInput" :value="content" />
app = new Vue({ el: "#app", data: { content: "test", isCheck: false, }, methods: { handleInput(e) { this.content = e.target.value; }, }, });
v-model
在内部为不同的输入元素使用不同的 property 并抛出不同的事件:- text 和 textarea 元素使用
value
property 和input
事件; - checkbox 和 radio 使用
checked
property 和change
事件; - select 字段将
value
作为 prop 并将change
作为事件。
- text 和 textarea 元素使用
第二层:组件化思想
随着代码越来越多考虑到
- 功能封装
- 代码复用
- 单一职责
- ...
等问题,vue提供了组件的系统。类似我们js中的函数一样的
命名规范
在注册组件时候名字使用的式大驼峰命名法,但是在视图中使用烤肉串命名法。为了解决这种不便。vue提供一个template属性,里面是视图。
全局注册
组件是全局注册的。也就是说它们在注册之后可以用在任何新创建的 Vue 根实例 (new Vue
) 的模板中。
<div id="app"></div>
<script>
// 全局注册
Vue.component("ComponentA", {
template: `<div>ComponentA</div>`,
});
app = new Vue({
el: "#app",
template: `
<div>
{{msg}}
<ComponentA></ComponentA>
</div>
`,
data: {
msg: "Hello Vue",
},
});
局部注册
局部注册的组件是一个对象形式,里面提供视图等信息。要使用的时候需要用
components:{
ComponentB
},
注册一下,否则会报错的。
<div id="app"></div>
<script>
const ComponentB ={
template : `<div>ComponentB</div>`
}
// 全局注册
Vue.component("ComponentA", {
components:{
ComponentB
},
template: `<div>ComponentA
<ComponentB></ComponentB>
</div>`,
});
app = new Vue({
el: "#app",
// 这里使用 <ComponentB></ComponentB> 就会报错,它没有在这里注册
template: `
<div>
{{msg}}
<ComponentA></ComponentA>
<ComponentB></ComponentB>
</div>
`,
data: {
msg: "Hello Vue",
},
});
</script>
data的问题
注意注册组件的时候 data 的 必须是一个函数。
一个组件的 data
选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。
如果不是单独一个函数的话,只是一个普通对象,那么在函数复用时候,同一个实例使用同一对象,就会有传址的问题。
data: function () {
return {
count: 0
}
}
必须要有一个根节点
组件得template 必须要有一个根节点
生命周期流程
生命周期函数
- beforeCreate
- created
- beforeMount
- mounted
- beforeUpdate
- updated
- beforeDestroy
- destroyed
子组件初始化的声明周期
先初始化父组件初始化到 beforeMount 父组件暂停初始化,子组件开始初始化直到子组件执行完mounted 函数后,再返回上一层父组件,父组件再执行mounted。
所以我们要拿到子组件的视图时候是再父组件的mounted 函数中获取的
props(组件间通信入口)
通过属性的值向子组件传递参数
-
数组类型
Vue.component("ComponentA", { props: ["title"], template: `<div>ComponentA {{title}} </div>`, }); app = new Vue({ el: "#app", template: ` <div> {{msg}} <ComponentA title='123'></ComponentA> </div> `, data: { msg: "Hello Vue", }, });
-
对象类型
-
对象类型的话里面可以设置类型 type
-
设置默认值 default
-
必须性 required
-
数据验证函数 validator (val){ return val ==='xxx'}
Vue.component("ComponentA", { // props: ["title"], props:{ title:{ type:String, required: true, default: 'ComponentA title' }, student:{ type:Object, validator(val){ return val.name === 'ellen' } } }, template: `<div>ComponentA {{title}} </div>`, }); app = new Vue({ el: "#app", // :student 绑定一下把它转成object template: ` <div> {{msg}} <ComponentA title='123' :student="{name:'ellen'}"></ComponentA> </div> `, data: { msg: "Hello Vue", }, });
-
emit(组件间通信出口)
利用给定的$emit
语法: this.$emit("EventName", "组件内的变量")
"组件内的变量" 也是父组件函数里面的参数
父组件接收
在父组件视图中的子组件标签中绑定一个方法 这个方法的名称对应EventName 等于这个方法,这个方法在 methods里面定义
//ComponentA
template: `<div>ComponentA
{{title}}
<hr />
{{count}}
<button @click="handClick">click</button>
</div>`,
methods: {
handClick(){
// console.log(this.count);
// this.count
this.$emit("change",this.count);
}
},
//父组件
template: `
<div>
{{msg}}
<ComponentA title='123' :student="{name:'ellen'}" @change='clickCount'></ComponentA>
</div>
`,
methods: {
clickCount(count){
console.log('father'+count);
}
},
v-model(v-model组件通信)
非自定义情况下
子组件
这里的value ,input都是定义好的,不能更改。
- props 接收一个value:''
- this.$emit("input",this.count);
父组件
- count 名字是随便取得
- 这里使用了count 所以data 里面要定义 count
<div id="app"></div>
<script>
// 全局注册
Vue.component("ComponentA", {
props: {
value: "",
},
data() {
return {
count: 0,
};
},
template: `<div>ComponentA
<hr />
{{count}}
<button @click="handClick">click</button>
</div>`,
methods: {
handClick() {
this.count++;
this.$emit("input", this.count);
},
},
});
app = new Vue({
el: "#app",
template: `
<div>
{{msg}}
{{countNum}}
<ComponentA v-model="countNum"></ComponentA>
</div>
`,
data: {
msg: "Hello Vue",
// count 这里要定义一下
countNum: 0,
},
});
自定义(model)
model 修改value 和 event 得名称
Vue.component("ComponentA", {
//重点
model:{
prop: 'countFn',
event: 'changeCount'
},
//接收参数
props: {
countFn: '',
value:''
},
data() {
return {
count: 0,
};
},
template: `<div>ComponentA
<hr />
//使用新名字
countFn: {{countFn}}
<br />
{{count}}
<button @click="handClick">click</button>
</div>`,
methods: {
handClick() {
this.count++;
//事件名称使用新名称
this.$emit("changeCount", this.count);
},
},
});
app = new Vue({
el: "#app",
template: `
<div>
{{msg}}
{{count}}
<ComponentA v-model="count"></ComponentA>
</div>
`,
data: {
msg: "Hello Vue",
// count 这里要定义一下
count: 0,
},
});
sync (组件间通信)
有时候要修改父组件传过来得属性值,然而vue是单向数据流,是不允许子组件修改父组件得值。那么可以使用:属性.sync
解决这个问题。
this.$emit('update:title(属性名称)')
<div id="app"></div>
<script>
Vue.component("ComponentA", {
props:['title'],
data() {
return {
count: 0
}
},
template: `<div>ComponentA
<hr/>
title: {{title}}
<br />
count: {{count}}
<br />
<button @click="handleClick">click</button>
</div>`,
methods: {
handleClick(){
this.count++;
//重点
this.$emit("update:title",this.title + this.count);
}
},
});
app = new Vue({
el: "#app",
template: `
<div>
{{msg}}
<br />
<ComponentA :title.sync="title"></ComponentA>
</div>
`,
data: {
msg: "App Vue",
title: "ComponentA title"
},
});
</script>
插槽
组件标签中要写入一些东西,但是我们直接写是不会显示。因为我们在子组件中没有给定slot插槽。形象得理解就是没有打一坑。而且这个坑还能命名,把指定得东西插入到对应名字得坑里面。
具名插槽
语法
-
子组件
-
父组件
-
v-slot:slotName
-
<script>
Vue.component("ComponentA", {
template: `<div>ComponentA
<br />
<p> <slot name="header"></slot> </p>
<p> <slot name="default"></slot> </p>
<p> <slot name="footer"></slot> </p>
</div>`,
});
app = new Vue({
el: "#app",
template: `
<div>
{{msg}}
<br />
<ComponentA>
<template v-slot:header>ComponentA header</template>
ComponentA main
<template v-slot:footer>ComponentA footer</template>
</ComponentA>
</div>
`,
data: {
msg: "Hello Vue",
},
});
</script>
作用域插槽
在组件标签中使用子组件得data 属性,但是使用得时候data 属性不在子组件得作用域,所以直接使用会导致报错未定义。
语法
-
子组件
- 插槽中 :title="title" .....名字随便取
- 可以绑定多个
-
父组件
- v-slot:header="data"
- 这个data是一个包含属性键值对得对象
<script> // 全局注册 Vue.component("ComponentA", { data() { return { count: 0, comTitle : 'ComponentA title' } }, template: `<div>ComponentA <br /> <p> <slot name="header" :count="count" :comTitle="comTitle"></slot> </p> <p> <slot name="default"></slot> </p> <p> <slot name="footer"></slot> </p> </div>`, }); app = new Vue({ el: "#app", template: ` <div> {{msg}} <br /> <ComponentA> <template v-slot:header="data">ComponentA header <br /> count-- {{data.count}} <br /> comTitle-- {{data.comTitle}} </template> ComponentA main <template v-slot:footer>ComponentA footer</template> </ComponentA> </div> `, data: { msg: "Hello Vue", }, });
-
作用域插槽的复用
既然我们可以在夫组件中使用子组件的data,可以把他当成函数一样。
例如:循环的逻辑可以子组件中,然后对数据判断的逻辑可以写在父组件中。
<script> // 全局注册 Vue.component("TodoList", { props:['list'], data() { return { } }, //循环逻辑 template: `<div> <ul> <li v-for="item in list" > <slot :item="item"></slot> </li> </ul> </div>`, }); app = new Vue({ el: "#app", //数据判断逻辑 template: ` <div> {{msg}} <br /> <TodoList :list='list'> <template v-slot="{item}"> <span v-if="item.age == 23">*</span> {{item.name}}--{{item.age}} </template> </TodoList> </div> `, data: { msg: "Hello Vue", list:[ {"name":'Ellen',"age":23}, {"name":'Cindy',"age":25} ] }, });
组件混用mixins
组件一和组件二都需要使用到functionA 那么可以把这个functionA 抽离到一个mixin对象里面
然后在组件使用命名一下:
mixins:[mixinName]
mixin也可以使用mixins:[mixinName]
,他们直接能相互嵌套。
但是在vue3中这个功能就被替代成组合式api。第一点它相互嵌套所以很难追踪到来源。第二点如果mixins 里面函数命名互相冲突,那么后面调用的函数会替代前面调用的函数。
所以一般谨慎使用mixins
指令进阶:自定义指令
除了基本指令,还能自定义指定来做特殊的事。例如下面的例子,我们想打开网页时候光标就定位到文本中。我们可以使用Vue.directive
方法定义自己的指令
Vue.directive('name',function)
函数里面有指令的声明周期钩子,但是vue3中已经废弃vue2的了,把他变成同名生命周期钩子。
函数参数:
-
el
:指令所绑定的元素,可以用来直接操作 DOM。 -
binding
:一个对象,包含以下 property:
name
:指令名,不包括v-
前缀。value
:指令的绑定值,例如:v-my-directive="1 + 1"
中,绑定值为2
。oldValue
:指令绑定的前一个值,仅在update
和componentUpdated
钩子中可用。无论值是否改变都可用。expression
:字符串形式的指令表达式。例如v-my-directive="1 + 1"
中,表达式为"1 + 1"
。arg
:传给指令的参数,可选。例如v-my-directive:foo
中,参数为"foo"
。modifiers
:一个包含修饰符的对象。例如:v-my-directive.foo.bar
中,修饰符对象为{ foo: true, bar: true }
。
-
vnode
:Vue 编译生成的虚拟节点。移步 VNode API 来了解更多详情。 -
oldVnode
:上一个虚拟节点,仅在update
和componentUpdated
钩子中可用。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app"></div>
<script>
// v-show
// v-focus
Vue.directive("focus", {
bind(el, binding) {
console.log(el);
console.log(binding);
el.focus();
console.log("bind");
},
inserted(el) {
console.log("inserted");
el.focus();
},
});
Vue.component("ComponentA", {
methods: {
handleClick() {
this.count++;
},
},
data() {
return {
count: 0,
};
},
template: `<div>ComponentA
<input type="text" v-focus.a.b="123 + 123"></input>
</div>`,
});
app = new Vue({
el: `#app`,
template: `
<div>{{msg}}
<ComponentA></ComponentA>
</div>`,
data: {
msg: "hello world",
},
});
</script>
</body>
</html>
思路梳理:初始化流程思路
vm.options.template 是视图层,下一步他会被解析为ast的语法树。经过compile(编译;编写(书、列表、报告等);编纂)变成一个vm.options.render函数。然后调用完vm.options.render函数后会返回一个虚拟节点树然后通过这个树转换成真实的dom视图。
然后定义的响应式的data里面有对应的getter setter 当他被设置和读取的时候会被watcher捕捉到。如果发生变更会再次调用vm.options.render函数。生成新的虚拟节点树,使用 diff算法进行优化再次渲染真实的dom视图。
第三层:vue-router
Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。包含的功能有:
- 嵌套的路由/视图表
- 模块化的、基于组件的路由配置
- 路由参数、查询、通配符
- 基于 Vue.js 过渡系统的视图过渡效果
- 细粒度的导航控制
- 带有自动激活的 CSS class 的链接
- HTML5 历史模式或 hash 模式,在 IE9 中自动降级
- 自定义的滚动条行为
为了实现spa 单页面的应用,不用更新页面也能更新视图,这样做到更好的交互效果。就引入我们的vue-router。
vue-router做成url 映射成我们的页面视图,根据不同的url显示不同的视图。
安装
- vue-cli
- vue add @vue/router
- 手动
- vue i vue-rotuer
最简模型
简单来说就是4步,进阶的内容就是补充日常开发所需。
作为一个插件,参考node使用中间件的方式。可以建立一个文件夹或者一个单文件 route.js / (router/ index.js)。
入口:插件使用的主体是Vue实例,所以要先引入vue 再引入插件 vue-router。
然后使用调用 vue-router :
Vue.use(VueRouter);
映射:引入使用之后,路由的作用就是做url 映射成我们的页面视图,所以我们要准备好path:url 和视图(我创建的是home.vue)。
怎么做映射,根据的文档的demo。在route.js中要使用到视图,先把它引入进来。创建Vue Router实例,里面传递定义好的路由,就是一个有path component的数组对象。
**挂载:**导出这个路由实例,挂在到主入口main.js的app vue实例
**渲染:**视图层上利用 标签显示路由
//router
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../views/home.vue";
Vue.use(VueRouter);
const routes = [
{
path: "/",
component: Home,
},
];
const router = new VueRouter({ routes });
export default router;
//main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
new Vue({
router,
render: h => h(App)
}).$mount('#app')
// app.vue 路由注入
<template>
<div id="app">
<p>app Page</p>
<!-- 路由注入 -->
<router-view></router-view>
</div>
</template>
<script>
export default {};
</script>
<style></style>
映射层:定义路由
-
构建参数
路由options对象里面可以设置的参数
interface RouteConfig = { path: string, component?: Component, name?: string, // 命名路由 components?: { [name: string]: Component }, // 命名视图组件 redirect?: string | Location | Function, props?: boolean | Object | Function, alias?: string | Array<string>, children?: Array<RouteConfig>, // 嵌套路由 beforeEnter?: (to: Route, from: Route, next: Function) => void, meta?: any, // 2.6.0+ caseSensitive?: boolean, // 匹配规则是否大小写敏感?(默认值:false) pathToRegexpOptions?: Object // 编译正则的选项 }
映射层:动态路由 (props)
-
传参
props
路由常见的需求url的传递参数 http://localhost:8080/#/class/123等。
定义路由时候,path 可以改成动态:
{
path: "/class/:id",
component: classPage,
},
怎么获取到这个id的值,首先要到这个class.vue组件中。vue提供了一个route。
但是这样每次都这样调用,单元测试不好进行,而且访问还比较复杂。
定义路由时候可以加
props:true
组件中通过 props:['id'] 就能简单获取到这个参数
// route.js
{
path: "/class/:id",
component: classPage,
props: true,
},
// class.vue
<script>
export default {
props: ["id"],
created() {
console.log(this.id);
},
};
</script>
-
匹配逻辑库
vue的底层的匹配逻辑是这个库: path-to-regexp。有空可以再了解一下
-
优先级
先定义的优先显示
-
404错误页
根据优先级,我们应该把404写在定义路由逻辑的最后,并且路径是个
*
表示前面的路由都走完了发现没有。显示这个错误页面Vue.use(VueRouter); const routes = [ { path: "/", component: Home, }, { path: "/class/:id", component: classPage, }, { path: "*", component: NotFound, }, ];
映射层:嵌套路由 (children)
多层嵌套的组件。路由里面还有下一层的路由
-
父子关系
配置定义路由参数 children , 然后再父组件进行渲染
{ path: "/class/:id", component: classPage, props: true, children: [ { // 等于 "/class/:id:name", path: ":name", component: subclass, props: true, }, ], },
映射层:命名视图(components)
一个页面有很多个路由,要区分他们,就可以参考slot插槽思想,给他命名。说明它是个什么路由
//route.js
{
path: "/",
components: {
default: Home,
one: oneView,
two: twoView,
},
},
//app.vue
<template>
<div id="app">
<p>app Page</p>
<!-- 路由注入 -->
<router-view></router-view>
<router-view name="one"></router-view>
<router-view name="two"></router-view>
</div>
</template>
映射层:重定向(redirect) 别名 (alias)
重定向也是通过 routes
配置来完成,定义这个参数时候,当访问对应地址时候重定向到redirect :'url' url的地址。
别名,这个path: 'url' 给这个url 定义一个名字,定义了之后, 访问alias 和访问path: 'url' 都是一样的效果。
const routes = [
{
path: "/homePage",
redirect: "/",
},
{
path: "/",
alias: "/home",
components: {
default: Home,
one: oneView,
two: twoView,
},
},
映射层:元信息(meta)
给这个路由挂载一些信息,例如路由守卫可以用。这个路由是否要鉴权。
meta:{
isRequired: true
}
进阶:导航组件(router-link)
<router-link to="地址"></router-link>
组件支持用户在具有路由功能的应用中 (点击) 导航。 通过 to
属性指定目标地址。
传递对象时候,需要再定义路由时候给这个路由定义一个名字
-
to required
- type: string | Location
<!-- 字符串 --> <router-link to="home">Home</router-link> <!-- 渲染结果 --> <a href="home">Home</a> <!-- 使用 v-bind 的 JS 表达式 --> <router-link v-bind:to="'home'">Home</router-link> <!-- 不写 v-bind 也可以,就像绑定别的属性一样 --> <router-link :to="'home'">Home</router-link> <!-- 同上 --> <router-link :to="{ path: 'home' }">Home</router-link> <!-- 命名的路由 --> <router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link> <!-- 带查询参数,下面的结果为 /register?plan=private --> <router-link :to="{ path: 'register', query: { plan: 'private' }}" >Register</router-link >
进阶:编程式导航(this.$router)
视图中实现了路由跳转,那么在js 中要怎么点击跳转路由,这就是编程式导航。例如我们点击一个按钮,跳转到对应得页面。
官方就给了this.$router 这个对象。
在 Vue 实例内部,你可以通过 $router
访问路由实例。因此你可以调用 this.$router.push
可以把访问得历史想像成一个栈得模式,每点击一个就push一个 地址进去,添加一个新得记录。例如
// 字符串
router.push('home')
// 对象
router.push({ path: 'home' })
// 命名的路由
router.push({ name: 'user', params: { userId: '123' }})
// 带查询参数,变成 /register?plan=private
router.push({ path: 'register', query: { plan: 'private' }})
-
方法
- push 新增地址
- replace 取代旧地址
- go 设置前进几步
- back 设置后退几步
router.push(location, onComplete?, onAbort?) router.push(location).then(onComplete).catch(onAbort) router.replace(location, onComplete?, onAbort?) router.replace(location).then(onComplete).catch(onAbort) router.go(n) router.back() router.forward()
例子:
<button @click="toClasspage">click to classPage page</button> <script> export default { methods: { toClasspage() { console.log(this.$router); this.$router.push({ name: "classPage" }); }, }, }; </script>
进阶:路由导航(hooks)
本质是一些钩子函数,可以理解成生命周期得钩子。
- 按照流程位置
- 前置守卫钩子
- 后置守卫钩子
- 按照模块划分
- 全局守卫钩子
- 路由守卫钩子
- 组件守卫钩子
它们相当于一个拦截器一样,一层层执行。如果执行到哪里出错了,就不会继续往下执行(不调用next())。那么可以使用到我们鉴权,用户登录等情况使用。
模式
他们两的区别就是 url是否有带 # 号,实现核心是更新视图不重新请求页面
- hash
- history history
思路梳理:组件间通信
第二层和第三层学习结束后,梳理一波组件间通信,这里涉及到了第二层组件和第三层路由的知识,还会补充一波新的方式去实现组件间的通信。
组件树直接直接要怎么正确通信呢?按照他们直接的关系分为下面三种情况
-
父子组件的通信
优先级: 优先使用**props/parent/parent/children是无法获取到它的信息的。
-
props/$emit
props
这种方式是父子之间中间没有隔断的通信。利用父级中属性传递参数,子级中props接收参数。
<template> <div> <h3>Parent Page</h3> <child title="Parent title transform to child"></child> </div> </template> <script> import child from "./child"; export default { components: { child }, }; </script> <style scoped></style> <-------------------------------------------------------------> <template> <div> <h4>Child Page</h4> {{ title }} </div> </template> <script> export default { props: ["title"], }; </script> <style scoped></style>
$emit
这个是子级向父级通信的方式。第二层组件化中的$emit
-
$refs/ref
-
ref : 给元素或子组件注册引用信息
-
$refs : 获取通过 ref 注册的引用
利用ref也可以获取子组件的信息。在引用的子组件中添加
ref='componentName'
然后在当前引用这个子组件的父组件的实例中利用this.$ref
查看这个子组件的信息。下面的例子:
<child title="Parent title transform to child" ref="childComp"></child> export default { components: { child }, mounted() { // console.log(this.$children); // console.log(this.$children[0].getTitle()) console.log("ref--", this.$refs.childComp); console.log(this.$refs.childComp.getTitle()); //Parent title transform to child }, };
此外这个属性ref 还能作用到普通组件上,例如input 的聚焦
<input type="text" name="" id="" ref="inputComp" /> <script> import child from "./child"; export default { components: { child }, mounted() { this.$refs.inputComp.focus(); }, }; </script>
-
-
children
-
$parent 获取当前组件的父组件实例
这个方式是子组件获取父组件实例的方法。在子组件中可以通过
this.$parent
来调用它的父组件的实例然后就调用父级的方法,属性等。 -
$children 获取当前组件的子组件实例
参数传递
在父组件中可以查看这个子组件实例,是一个对象数组 ,里面可以单个也可以有多个子组件实例。这里可以看到更多的信息,例如传递过去的参数等。
mounted() { console.log(this.$children); },
函数传递
除了参数以外,在子组件中的还可以定义函数在 methods然后在父组件中调用这个函数。
//child.vue export default { props: ["title"], methods: { getTitle() { return this.title; }, }, }; </script> //parent.vue import child from "./child"; export default { components: { child }, mounted() { console.log(this.$children); console.log(this.$children[0].getTitle()) }, }; </script>
-
-
-
多层级父子组件通信
隔代通信例如祖父组件和孙子组件通信,例如上图的 a组件 要通信 f组件 。注意昂,这些下面的方法,只能组父级通信子孙级。子孙级还没有办法向前通信。
-
provide/inject
这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。
provide
选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的 property。inject
选项应该是:- 一个字符串数组,或
- 一个对象,对象的 key 是本地的绑定名,value 是:
- 在可用的注入内容中搜索用的 key (字符串或 Symbol),或
- 一个对象,该对象的:
from
property 是在可用的注入内容中搜索用的 key (字符串或 Symbol)default
property 是降级情况下使用的 value
参数传递
//ComponentA <template> <div> ComponentA <hr /> <Comb></Comb> </div> </template> <script> import Comb from "./Comb"; export default { components: { Comb, }, // 依赖注入 provide: { title: "This is ComA title", }, }; </script> <style scoped></style> <----------------------------------------------------------------> //ComponentC <template> <div> ComponentC <div>{{ title }}</div> </div> </template> <script> export default { inject: ["title"], }; </script> <style scoped></style>
实例/函数传递
<template> <div> ComponentA <hr /> <Comb></Comb> </div> </template> <script> import Comb from "./Comb"; export default { components: { Comb, }, // // 依赖注入 // provide: { // title: "This is ComA title", // }, provide() { return { title: "This is ComA title", compA: this, }; }, methods: { sayName() { console.log("This is ComponentA"); }, }, }; </script> <style scoped></style> <----------------------------------------------------------------> <template> <div> ComponentC <div>{{ title }}</div> </div> </template> <script> export default { inject: ["title", "compA"], mounted() { console.log("comA", this.compA); this.compA.sayName(); }, }; </script> <style scoped></style>
-
listeners
$attrs,搭配 inheritAttrs: false 使用
包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (
class
和style
除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class
和style
除外),并且可以通过v-bind="$attrs"
传入内部组件——在创建高级别的组件时非常有用。意思就是当父组件中声明了一个属性 例如 title= 'text' ,在子组件中不能声明prop。那么通过 this.attrs" 就可以获取到 title= 'text' 并且孙级组件中也可以通过this.attrs" 获取到这个属性,并且可以省略 props.
那么我们开发组件时候,可以参考这个下面例子,在父组件用attrs 传递给子组件的原生组件中
// ComponentA <template> <div> ComponentA <hr /> <Comb></Comb> <p> <userButton disabled></userButton> </p> </div> </template> <script> import Comb from "./Comb"; import userButton from "./button"; export default { components: { Comb, userButton, }, <----------------------------------------------> // buttom component <template> <div> <button :disabled="$attrs.disabled">click userButton</button> </div> </template> <script> export default { inheritAttrs: false, }; </script> <style scoped></style>
$listeners 收集组件方法。
组件中定义的的方法都会存在这个$listeners对象里面。在后续的子组件和孙组件中通过
v-on=$listners
获取父组件和祖父组件的方法。它也是可以一层层传递的,只要在子组件中声明v-on=$listners
。就可以通过this.$listners
获取<template> <div> ComponentA <hr /> <Comb @hello="hello"></Comb> <p> <userButton disabled></userButton> </p> </div> </template> <script> import Comb from "./Comb"; import userButton from "./button"; export default { components: { Comb, userButton, }, // // 依赖注入 // provide: { // title: "This is ComA title", // }, provide() { return { title: "This is ComA title", compA: this, }; }, methods: { sayName() { console.log("This is ComponentA"); }, hello() { console.log("hello components"); }, }, }; <------------------------------> <template> <div> ComponentB <hr /> <ComC v-on="$listeners"></ComC> </div> </template> <script> import ComC from "./ComC"; export default { components: { ComC, }, mounted() { console.log(this.$listeners); }, }; </script> <style scoped></style>
非关系组件通信
EventBus ,本质就是一个订阅模式。在项目中创建一个EventBus js 里面导出去一个vue实例。
- $on 侦听事件,添加订阅,事件名称 + 一个回调函数
- $off 移除侦听的事件 ,这个事件要具名
- $emit 触发事件,事件名称 + 传递的参数
- $once 只触发一次的事件
import Vue from "vue"; const Eventbus = new Vue(); export default Eventbus; <---Eventbus.js ---> <template> <div> <h3>A</h3> <comb></comb> <comc></comc> </div> </template> <script> import comb from "./b"; import comc from "./c"; import eventbus from "../eventbus"; export default { components: { comb, comc }, mounted() { eventbus.$on("helloA", (msg) => { console.log(msg); }); }, }; </script> <style scoped></style> <--- A.vue ---> <template> <div> <h3>B</h3> <button @click="handleB">handleB</button> </div> </template> <script> import eventbus from "../eventbus"; export default { methods: { handleB() { eventbus.$emit("helloA", "我是组件b"); }, }, }; </script> <style scoped></style> <--- B.vue ---> <template> <div> <h3>C</h3> <button @click="handleC">handleC</button> </div> </template> <script> import eventbus from "../eventbus"; export default { methods: { handleC() { // eventbus.$emit("helloA", "我是组件C"); eventbus.$emit("helloA", "我是组件C"); }, }, }; </script> <style scoped></style> <--- C.vue --->
-
第四层:vueX
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式,采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
解决的问题:多个视图依赖于同一状态,来自不同视图的行为需要变更同一状态
vueX 思路:把组件的共享状态抽取出来,以一个全局单例模式管理
建议场景:
- 如果数据还有其他组件复用,建议放在vuex
- 如果需要跨多级组件传递数据,建议放在vuex
- 需要持久化的数据(如登录后用户的信息),建议放在vuex
- 跟当前业务组件强相关的数据,可以放在组件内
优点:
- 我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!
- 通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。
安装
npm install vuex --save
最简模型
-
创建store.js
import Vue from "vue"; import VueX from "vuex"; Vue.use(VueX); const store = new VueX.Store({ state: { msg: "hello, vuex!", }, }); export default store;
-
main.js 引用这个实例
import Vue from "vue"; import App from "./App.vue"; import store from "./store.js"; Vue.config.productionTip = false; new Vue({ store, render: (h) => h(App), }).$mount("#app");
-
通过
this.$store
获得这个实例mounted() { console.log(this.$store.state.msg); }, };
state
state是一个全局都可以获取的状态例如上面最简模型通过this.$store.state
可以获取到。
如果想修改它时候会发现修改不了。vuex要修改值,需要在computed计算属性中修改。当this.$store.state 的值修改的后,store会再次调用computed 重新获取到最新值。
this.$store.state
例子:
<template>
<div id="app">
{{ title }}
<p>
<button @click="handleMsg">change vuex</button>
</p>
</div>
</template>
<script>
export default {
name: "App",
components: {},
data() {
return {
// 无法响应式修改
// title: this.$store.state.msg,
};
},
mounted() {
console.log(this.$store.state.msg);
},
computed: {
title() {
return this.$store.state.msg;
},
},
methods: {
handleMsg() {
this.$store.state.msg = "change vuex msg";
},
},
};
</script>
但是这样就过于复杂了,如果很多属性,每个属性定义一个computed,就很麻烦。所以引入了一个mapState()
方法这个方法可以获取帮我们获取到store并且可以响应式修改。
这个方法里面可以是数组和对象的形式,两种方式可以同时存在,对象可以实现改名做一个映射
mapState()
例子:
<template>
<div id="app">
{{name}} -- {{age}} -- {{hobby}}
</div>
</template>
<script>
import { mapState } from "vuex";
export default {
name: "App",
mounted() {
console.log(this.$store.state);
},
computed: {
//数组
...mapState(["name", "age", "hobby"]),
//对象
...mapState({
myname: "name",
myage: "age",
myhobby: "hobby",
}),
},
};
</script>
getter
全局参数问题可以用State 解决。那么如果想对State 做一些计算和过滤。例如想知道这个人5年后的年龄。并且不止一个地方用到。很麻烦的是,需要每个组件中,拿到State 里面的年龄值,还有定义一个computed 定义5年后的年龄。所以就派生了getter
和对应辅助的 mapGetter
获取getter的计算属性方法。
这个mapGetter方法里面可以是数组和对象的形式,两种方式可以同时存在,对象可以实现改名做一个映射
看看这个例子:
//Store.js 定义getters
export default new Vuex.Store({
state: {
name: "katrina",
age: 18,
hobby: "read",
},
getters: {
ageplus: (state) => {
return state.age + 5;
},
},
// app.vue
<template>
<div id="app">
{{ myname }} -- {{ myage }} -- {{ myhobby }}
<p>root app ageplus {{ ageplus }}</p>
<componentA></componentA>
</div>
</template>
import { mapState, mapGetters } from "vuex";
export default {
components: { componentA },
name: "App",
computed: {
....
...mapGetters(["ageplus"]),
},
};
</script>
// componentA
<template>
<div>
componentA
<p>componentA get ageplus {{ ageplus }}</p>
</div>
</template>
<script>
import { mapGetters } from "vuex";
export default {
computed: {
...mapGetters(["ageplus"]),
},
};
</script>
<style scoped></style>
mutation(处理同步)
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数。
组件中调用这个mutation 是 this.$store.commit('name','argument')
为什么要通过mutation,之前的例子可以直接修改 this.$store里面值。因为如果直接修改值我们并不会知道,这个值被谁修改,也不知道修改的正确与否。所以修改store 状态的唯一方法是提交 mutation。它是一个中间层,可以收集那个方法修改这个值,并且跟踪方法来自哪里。
在我们使用devtools 时光机的时候就是利用这个原理追踪到结果的。
// store.js
mutations: {
ageCount: (state, payload) => {
console.log("payload", payload); //外部传入的参数
return state.age++;
},
},
//app.vue
methods: {
commitAge() {
this.$store.commit("ageCount", "我增加了一岁");
},
},
每个methods都要定义一个函数去触发mutation 方法就点繁琐。所以有mapMuations
直接在methods 里面解构就可以使用了
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
...mapMutations([
'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
// `mapMutations` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
]),
...mapMutations({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
})
}
}
...mapMutations([
'increment' // 将this.increment() 映射为 this.$store.commit('increment', payload)
// 在事件或方法中直接调用 this.increment({amount: 10 })即可
]),
<div @click="increment({amount: 10 })">Click me</div>
Actions(处理异步)
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit
提交一个 mutation,或者通过 context.state
和 context.getters
来获取 state 和 getters。
应用层: Action 通过 store.dispatch
方法触发
mutations: {
ageCount: (state, payload) => {
console.log("payload", payload);
state.age++;
},
changemsg: (state) => {
state.name = state.name + "Deng";
},
},
actions: {
getMsg({ commit }) {
console.log("run here");
setTimeout(() => {
commit("changemsg");
}, 1000);
},
},
组件层
<button @click="handlemsg">changemsg</button>
methods: {
...
handlemsg() {
this.$store.dispatch("getMsg");
},
},
module (模块化)
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
项目结构
Vuex 并不限制你的代码结构。但是,它规定了一些需要遵守的规则:
- 应用层级的状态应该集中到单个 store 对象中。
- 提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。
- 异步逻辑都应该封装到 action 里面。
只要你遵守以上规则,如何组织代码随你便。如果你的 store 文件太大,只需将 action、mutation 和 getter 分割到单独的文件。
对于大型应用,我们会希望把 Vuex 相关代码分割到模块中。下面是项目结构示例:
├── index.html
├── main.js
├── api
│ └── ... # 抽取出API请求
├── components
│ ├── App.vue
│ └── ...
└── store
├── index.js # 我们组装模块并导出 store 的地方
├── actions.js # 根级别的 action
├── mutations.js # 根级别的 mutation
└── modules
├── cart.js # 购物车模块
└── products.js # 产品模块
actions mutations .. 本质就是一个对象嘛,可以当成对象导出来就可以。
第五层:构建系统
- vue-cli
- element-ui
SFC 单文件组件
.vue后缀的文件, 优点
- 语法高亮
- 组件作用域css
- vscode - vetur
vue-cli
优点
-
功能丰富
- 集成了所有前端生态里最优秀的工具
-
易于扩展
- 插件系统
-
CLI 之上的图形化界面
- 可以使用可视化页面创建项目
-
即刻创建原型
- 方便快捷验证想法
-
面向未来
- 为现代浏览器轻松产出原生的 ES2015 代码,或将你的 Vue 组件构建为原生的 Web Components 组件
-
无需 eject
- Vue CLI 完全是可配置的,无需 eject。这样你的项目就可以长期保持更新了
安装
npm i @vue/cli -g
安装完成后cmd输入命令 vue会得到帮助文档
Usage: vue [options]
Options: -V, --version output the version number -h, --help output usage information
Commands: create [options] create a new project powered by vue-cli-service add [options] [pluginOptions] install a plugin and invoke its generator in an already created project invoke [options] [pluginOptions] invoke the generator of a plugin in an already created project inspect [options] [paths...] inspect the webpack config in a project with vue-cli-service serve [options] [entry] serve a .js or .vue file in development mode with zero config build [options] [entry] build a .js or .vue file in production mode with zero config ui [options] start and open the vue-cli ui init [options]