三大前端框架之一,数据驱动的MVVM单页面应用,第一篇介绍框架的基础内容。
# 安装
预安装node
安装vue
npm i -g @vue/cli
添加element-UI
vue add element
添加路由
vue add router
使用Scss(super css)
npm i -D sass sass-loader
vue创建项目
vue create [项目名]
# 构建路由
创建router.js或者在router文件夹下创建index.js,添加代码
import Vue from 'vue'
import VueRouter from 'vue-router'
import home from './views/home.vue'
Vue.use(VueRouter)
const router = new VueRouter[{
routes[
{
path:'/',
name'home',//命名路由方便进行操作
component:home,
}
]
}]
export default router
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
创建对应组件home.vue
<template>
</template>
<script>
export default{
}
</script>
2
3
4
5
6
7
在app.js中引用
import router from './routes.js'
new Vue({
router
})
2
3
4
5
在组件里调用
<div>
<router-view/>
</div>
<router-link to="/login"></router-link>//字符串跳转
<router-link to="{path:'login'}"></router-link>//对象跳转
<router-link to="{name:'login'}"></router-link>//命名跳转
2
3
4
5
6
7
<router-link>
导航组件,与a标签类似,用to定目标地址
router-link可以赋值active-class和exact-active-class,指定模糊匹配和精确匹配。模糊匹配只要router-link中有对应的name就可以激活,精确匹配则必须匹配到
<router-view>
路由的出口,路由匹配到的组件将在这里进行渲染
不同的路由可以渲染到一个<router-view>
注意:vue-router的路径自带#号,为hash模式,使用history模式没有#号,但直接刷新时会导致找不到页面,vue-router的路径是虚拟路径,使用历史模式直接发布需要重新设置,
路由元信息
配置路由信息可以提供更多路由操作,定义meta信息
# 路由嵌套
子路由
{
path:'/about',
component:About,
children:[
{
path:'/',
name:'express',
component:Express
},
{
path:'/about/contact',
name:'contact',
component:contact
},
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 动态路由
路由携带参数
this.$router.push(name:'',params:{})
this方法指向当前的vue实例
# 路由懒加载
component: resolve=>require(['@/components/home'],resolve)
# 路由钩子函数
在定义路由时添加路由钩子函数
全局钩子
Router.beforeEach((to,from,next)=>{
next()
})//to表示即将进入的路由,from表示将要离开的路由,next表示函数
Router.afterEach((to,from,next)=>{
next()
})
2
3
4
5
6
某个路由的钩子函数
beforeEnter:(to,from,next)=>{
},
beforeleave:(to,from,next)=>{
}
2
3
4
5
6
路由组件内的钩子函数
beforeRouteLeave(to,from,next){
},
beforeRouteEnter(to,from,next){
},
beforeRouteUpdate(to,from,next){
}
2
3
4
5
6
7
8
9
# 路由实例
//直接添加一个路由,表现为切换路由,往历史纪录里添加一个历史记录
this.$router.push({'path:'home'})
//替换路由,历史纪录里没有添加记录
this.$router.replace({path:'news'})
2
3
4
# 生命周期函数
vue的生命周期函数如下图所示
beforeCreate(){},//生命周期,创建之前
created(){},//生命周期,创建之后
beforeMount(){},//生命周期,挂载之前
mounted(){},//生命周期,挂载之后
beforeupdate(){},//生命周期,更新之前
updated(){},//生命周期,更新之后
beforeDestroy(){},//生命周期,销毁之前
destroyed(){},//生命周期,销毁完成
activated(){},//如果这个页面有keep-alive缓存功能,这个函数会触发
2
3
4
5
6
7
8
9
这些函数引用时与data、methods、computed等并列使用
在beforecreate函数中修改data中的属性
//showmodal为data中数据,checkdevice为引入的方法
beforeCreate(){
this.$nextTick(function(){
if(checkdevice() == 'ios'){
this.showModal = false;
}
else if(checkdevice() == 'anzhuo'){
this.showModal = true;
}
})
},
2
3
4
5
6
7
8
9
10
11
# 组件化
# 组件分类
按组件注册方式分为全局组件和局部组件
全局注册
对于一些比较基础的组件,在各个组件中会比较被频繁地用到,那么在每个组件中都引入就会导致一个包含基础组件的长列表,此时在入口文件中引入这些组件,全局导入基础组件
局部注册
只在需要引用的组件中注册
<script>
import A from './componentA'
import B from './componentA'
export default {
components: {
A,B
}
}
</script>
2
3
4
5
6
7
8
9
按组件的功能分类,分为展示组件、业务组件、独立组件(难度逐渐递增):
展示组件:也就是平常业务开发还原设计稿,将信息展示在页面上,是呀route切换
业务组件:将当前公司的业务封装抽取出来的组件,不具有很强的通用性,
独立组件:不针对具体的业务,例如日期、表单,也就是标准组件库里的那些,通用性强
# 动态加载组件
点击不同的按钮加载不同的组件
通过 Vue 的 <component>
元素加一个特殊的 is
attribute 来实现:
<div id="dynamic-component-demo" class="demo">
<button
v-for="tab in tabs"
v-bind:key="tab"
v-bind:class="['tab-button', { active: currentTab === tab }]"
v-on:click="currentTab = tab"
>
{{ tab }}
</button>
<component v-bind:is="currentTabComponent" class="tab"></component>
<script>
Vue.component("tab-home", {
template: "<div>Home component</div>"
});
Vue.component("tab-posts", {
template: "<div>Posts component</div>"
});
Vue.component("tab-archive", {
template: "<div>Archive component</div>"
});
new Vue({
el: "#dynamic-component-demo",
data: {
currentTab: "Home",
tabs: ["Home", "Posts", "Archive"]
},
computed: {
currentTabComponent: function() {
return "tab-" + this.currentTab.toLowerCase();
}
}
});
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
动态加载组件时每次切换都会重新渲染组件,如果需要缓存组件的状态,可以使用keep-alive
<keep-alive>
<component v-bind:is="currentTabComponent"></component>
</keep-alive>
2
3
# 异步组件
使用factory工厂函数创建异步组件,只有在子组件需要渲染时再加载。
# 抽象组件
vue框架中存在内置组件(transition、keep-alive、),这两个内置组件均为抽象组件,抽象了通用的功能:动画过渡 和 缓存未活动的组件。
抽象组件和普通的组件类似,只是他们添加额外的行为,不向DOM呈现任何内容。有点像react中的高阶组件
实现:
首先实现抽象组件,不用设置template,否则Vue会优先渲染tempplate里面的东西,就不能额外添加行为了.
防抖抽象组件
<script>
import {get,set,debounce} from 'lodash';
export default {
name:'debounce',
abstract:true,
render(){
//从$slots中获取子组件
let vnode = this.$slots.default[0]
console.log(vnode)
if(vnode){
let event = get(vnode,'data.on.click')
if(typeof event === 'function'){
set(vnode,"data.on.click",debounce(event,1000))
}
//如果有DOM的操作,必须是在$nextTick中操作,因为在$nextTick中真是的DOM才能获取到
this.$nextTick(function(){
console.log(vnode.elm)
set(vnode,'elm.style.backgroundColor','red')
})
}
return vnode;
}
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
使用
<template>
<Debounce >
<button @click-"click">Debounce</button>
</Debounce>
</template>
<script>
import Debounce from "../Debounce";
export default{
name:"HelloWorld";
components:{
Debounce
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
权限控制抽象组件
场景:
作者及其团队经常需要实现新feature,且为了避免新feature的可能存在bug或让用户不舒服,会依靠权限配置先将新开发的feature隐藏起来,只对administrators可见。待administrators认为新功能可以向用户开发时,通过配置权限向用户开放功能。
<script>
export default {
name: 'permission-control',
props: ['permission-name'],
render() {
// EDITABLE case. Don't change anything and just render control as is.
if (userHasPermission(this.permissionName, EDITABLE)) {
return this.$slots.default[0];
}
// If user has no EDITABLE permission, but he has 'VISIBLE',
// let's change "disabled" prop value to the true
if (userHasPermission(this.permissionName, VISIBLE)) {
this.$slots.default[0].componentOptions.propsData.disabled = true;
return this.$slots.default[0];
}
// User has permission for nothing! Let's don't show him anything!
return null;
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
使用
<template>
<div>
<permission-control permission-name="team.trainingTime">
<my-time-input label="Next Training Time" :disabled="!hasNextTraining" />
</permission-control>
</div>
</template>
<script>
export default {
name: 'football-team-form',
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
https://www.yuque.com/loway/zh3lby/tv2zcq
事实上,Vue的文档中没有对抽象组件和关键字abstract: true做出解释。实际上,abstract: true仅是一个标记的作用,当组件实例在建立父子关系的过程中,会忽略掉抽象组件,此过程发生在initLifecycle。具体可见源码:
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
2
3
4
5
6
7
8
9
# 高阶组件
高阶组件(HOC)英文全称是“high-order-component”, 出自react生态,主要作用是“重用组件逻辑”,但这种高阶组件的思想不限制于框架和api,它只是一种最佳实践。React文档中简单明了的描述了HOC的概念:高阶组件就是一个(纯)函数,且该函数接受一个组件作为参数,并返回一个新的组件
使用高阶组件实现拦截props
export function EnhancePropsComponent (WrappedComponent, description) {
return {
props: WrappedComponent.props,
render (h) {
// 包装props
self = this
Object.keys(description)
.forEach(key => {
const property = Object.getOwnPropertyDescriptor(this._props, key)
const getter = property && property.get
const setter = property && property.set
Object.defineProperty(this._props, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter.call(self._props)
return description[key](value)
},
set: setter
})
})
const slots = Object.keys(this.$slots)
.reduce((arr, key) => arr.concat(this.$slots[key]), [])
.map(vnode => {
vnode.context = this._self
return vnode
})
return h(WrappedComponent, {
on: this.$listeners,
attrs: this.$attrs,
props: this.$props,
scopedSlots: this.$scopedSlots
}, slots)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
https://www.yuque.com/loway/zh3lby/wzgzti
# 组件间通信
# 父子组件、eventbus、provide/inject
父子组件通信:props/$emit
父组件向子组件传递值:子组件中定义props,父组件调用子组件时通过使用props的值传递到自组件,每当父组件中的值更新时子组件中的props也会自动更新。同时你也可以为props提供一些类型检查、设置默认值等,验证不满足时会发出警告。子组件的非父组件传递数据(自有数据)定义在data中
子组件向父组件传递值:子组件使用this.$emit(param)调用父组件的方法。在父组件调用中$on 事件进行监听,自组件传回值则调用对应方法。
实例
//父组件
<sss
:isEdit = "isEdit"
:TimeArray = "TimeArray"
:update="updatedata"
/>
<script>
export default{
name:"",
data:{
},
methods:{
updatedata(){
}
}
}
</script>
//子组件
<template>
</template>
<script>
export default{
name: "",
props: {
isEdit:{
type: Boolean,
default:false,
},
TimeArray:{
type: Array,
default => [],//默认为空数组
}
},
data(){
return {
}
},
methods:{
this.$emit(update)
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
需要注意的是,在vue2中,如果prop是数组或者对象这种引用数据类型,可以在子组件中直接修改props,如果是字符串、布尔值等基本数据类型则需要在父组件中修改,使用子组件提交到父组件的方法更新props的数据
兄弟组件通信:
方式1:可以先把需要改变的值放到父组件中,子组件通过props来获取父组件的值
方式2:通过eventbus 来实现兄弟组件之间的传值,其原理还是通过$on和$emit来时实现的,区别是现在全局建立一个空的vue对象,然后将事件绑定到该空对象上,最后通过该空对象来触发$on监听的事件
几乎所有的模块通信都是基于类似events的模式,包括安卓开发中的Event Bus
,Node.js中的Event
模块(Node中几乎所有的模块都依赖于Event,包括不限于http、stream、buffer、fs
等).
可以全局注册eventbus,也可以部分使用。
使用实例
//A组件
//B组件
2
3
4
跨级通信:provide/inject
这是vue@2.2
版本添加的一对需要一起使用的API
,它允许父级组件向它之后的所有子孙组件提供依赖,让子孙组件无论嵌套多深都可以访问到
provide
和 inject
主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。
provide/inject会让组件数据层级关系变的混乱的缘故,但在开发组件库时会很好使。
<!--父组件 提供-->
{
provide() {
return {
parent: this
}
}
}
<!--子组件 注入-->
{
// 写法一
inject: ['parent']
// 写法二
inject: { parent: 'parent' }
// 写法三
inject: {
parent: {
from: 'parent',
default: 222
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复杂系统使用vuex,相当于单独维护的数据
# 插槽
<template>
<div>
<!--默认插槽-->
<slot></slot>
<!--另一种默认插槽的写法-->
<slot name="default"></slot>
<!--具名插槽-->
<slot name="footer"></slot>
<!--传参插槽-->
<slot v-bind:user="user" name="header"></slot>
</div>
</template>
<!--使用-->
<children>
<!--跑到默认插槽中去-->
<div>123</div>
<!--另一种默认插槽的写法-->
<template v-slot:default></template>
<!--跑到具名插槽 footer 中去-->
<template v-slot:footer></template>
<!--缩写形式-->
<template #footer></template>
<!--获取子组件的值-->
<template v-slot:header="slot">{{slot.user}}</template>
<!--结构插槽值-->
<template v-slot:header="{user: person}">{{person}}</template>
<!--老式写法,可以写到具体的标签上面-->
<template slot="footer" slot-scope="scope"></template>
</children>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
具名插槽和普通插槽最大的区别在于,给普通插槽赋予了名字,这就是最大的一点区别,这就有点类似ES6
中新出的特性,解构赋值
。现在我们来改写上面的案例,新增加一个明明为header
的插槽,并且,用v-slot:header
的方式给传入的html代码
进行命名,可以看到成功地将名字为header的插槽替换了,所以输出结果为Header defaultContent
# 作用域插槽
父组件拿子组件中的信息。可以看到我们在插槽上动态绑定了data
,值为user对象
,那么,在父组件引用中的v-slot值.data
就可以访问到子组件中的user对象
的值了。
//Parent.vue
<Child>
<template v-slot="scope">
{{scope.data.name}}
{{scope.data.age}}
</template>
</Child>
//Child.vue
<template>
<div>
<slot :data="user">default</slot>
</div>
</template>
<script>
export default {
name: "Child",
data(){
return{
user:{
name:'jack',
age:18
}
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 动态名称插槽
通过[ ]
将变量名称括起来,可以实现动态名称的插槽,配合条件渲染,选择相应的具名插槽,能让组件的封装更加地灵活,因为args=other
,所以,输出结果为default jack
//Parent.vue
<template>
<div>
<Child>
<template v-slot:[args]="scope">
{{scope.data.name}}
</template>
</Child>
</div>
</template>
<script>
import Child from "../components/Child";
export default {
name: "Parent",
components:{
Child
},
data(){
return{
args:'other'
}
}
}
</script>
//Child.vue
<template>
<div>
<slot :data="user">default</slot>
<slot name="other" :data="user"></slot>
</div>
</template>
<script>
export default {
name: "Child",
data(){
return{
user:{
name:'jack',
age:18
},
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 驼峰命名法
如果注册组件时使用驼峰命名法,在使用时需要转换成短横线分隔命名。
父组件向子组件传递数据时使用短横线分隔命名法,则自组件接收时采用驼峰命名法。
传递方法时双方只能用短横线命名法,都不能使用驼峰命名法
# 模板语法
<script>
export default{
components:{ },
data(){
return{
}
},
methods:{
},
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
data:定义以及初始化数据。一般定义视图上的数据,否则定义在外部或者vm对象内
props用来接收来自父组件的数据,可以是数组或者使用对象替代
props是单向数据流,父组件prop的更新会向下流动到子组件中,反之不行
computed是计算属性,提供相对简单的数据计算,与methods相比缓存输出,只有当依赖的值发生改变时才会重新求值,且不需要绑定方法,只要依赖值有变化就会执行,称作响应式
场景:如果同一函数被大量重复调用,会消耗大量资源,此时就可以用computed。
每个计算属性都包含get和set属性,默认是get,set不常用,了解即可。
method:提供相对复杂的数据计算,methods是非响应式的,只有在调用时才执行而不会自动执行
watch:监听属性,与computed都是监听器,都起到监听/依赖数据并进行输出的作用,一般情况下使用computed,只有在数据变化需要执行异步是或者开销比较大时选用watch。
需要注意的是
1.computed只是单纯的计算,依赖某个值得到某个值,赋值、修改dom等操作只能在watch中完成
2.使用watch有两点需要注意:
(1)watch默认在最初绑定的时候是不会执行的,要等到数据在当前页面改变时才会监听。可以改变写法使得一开始最初绑定的时候就执行。
(2)watch监听引用变量类型,如数组和对象,只能在语句中执行赋值语句后才能监听,直接进行修改、添加或者删除属性无法监听,采用deep深度监听,监听器会向下一层一层遍历,给所有的属性都加上监听器。这样做的缺点时性能开销会加大。
也可以直接监听对象的某个属性
<script>
export default(){
data(){
a:1,
b:{
c:1;
}
d:""
}
watch:{
a(val,oldVal){
console.log("a",val,oldVal)
},
b:{
handler(val,oldVal){
console.log("b,c" val.c,oldVal.c)
},
deep:true // true 深度监听
},
d:{
handler(newval,oldval){
console.log()
},
immediate: true;
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 执行机制
在new vue时,index.js初始化各项功能:
function Vue(options){
if(process.env.NODE_ENV !== 'production' && !(this instanceof Vue)){
warn ('Vue is a. constructor and should be called with the new keyword')
}
this._init(options)
}
2
3
4
5
6
this._init的执行顺序为:
initInjections(vm)
initState(vm);
initProvide(vm)
callHook(vm,'created')
2
3
4
在initState中做了这些事情
if(opts.props) initProps(vm,opts.props)//初始化props
if(opts.methods) initMethods(vm,opts.methods)//初始化methods
if(opts.data) {
initData(vm)}
else{
observe(vm._data = {},true /* asRootData */)
}
//初始化data
if(opts.computed) initComputed(vm,opts.computed)//初始化computed
2
3
4
5
6
7
8
9
computed、props、data和computed的初始化都是在beforecreated和created之间完成的。
deep、immediate、handler
# 局部样式、多个样式
当 <style>
标签有 scoped
属性时,它的 CSS 只作用于当前组件中的元素。
<style scoped>
</style>
2
没有scoped时为全局
:class
# v-bind、v-on、v-for
v-bind属性绑定,class、style、href等属性
实例
<template>
<a v-bind:href="link">link</a>
</template>
<script>
export default{
data() {
return{
link:''
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
v-on事件绑定,监听dom事件,接受需要调用的方法
<a v-on:click=""></a>
<a @click=""></a>
2
v-on事件修饰符
<!-- 点击事件将只会触发一次 -->
<a v-on:click.once="doThis"></a>
<!-- 阻止单击事件继续传播 -->
<a v-on:click.stop="doThis"></a>
<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>
2
3
4
5
6
v-on支持监听绑定键盘事件和鼠标事件
//键盘
<input v-on:keyup.enter="submit">//按下enter键触发submit事件
.enter
.tab
.delete (捕获“删除”和“退格”键)
.esc
.space//空格键
.up//方向上键
.down//方向下键
.left//方向左键
.right//方向右键
//鼠标
.left //鼠标左键
.right //鼠标右键
.middle //中间滚轮
2
3
4
5
6
7
8
9
10
11
12
13
14
15
v-for使用于列表渲染
v-for是基于一个数组来渲染列表。循环指令为item in items。其中items是源数据数组,item是被迭代的数组元素的别名。in可以使用of代替。
<ul id="example">
<li v-for"item in items" :key="item.message">
{{ item.message}}
</li>
</ul>
<script>
var example = new Vue({
el:'example',
data:{
items:[
{message:'Foo'},
{message:'Bar'},
]
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
v-for还支持第二个参数,数组的索引,可以作为选项的key或者对选项中引用别的数组进行逐项操作
<ul id="example">
<li v-for"(item,index in items" :key="index">
{{ item.message}}--{{ index }}
</li>
</ul>
<script>
var example = new Vue({
el:'example',
data:{
items:[
{message:'Foo'},
{message:'Bar'},
]
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
v-for比v-if 的优先级更高,性能优化将v-for中的一些元素过滤,再进行v-if判断
v-for中一般带有key,为了高效更新虚拟DOM而不出错,在新旧辨识时辨别组件,如果不使用vue会使用一种最大限度减少动态元素并且尽可能尝试就地修改、复用相同的元素的算法。使用key后就会基于key重新排列元素,排除key不存在的元素
使用v-for遍历部分数据时,可以使用v-show,不能使用v-if,用v-if会报错
<tr v-for="(item,index) in showList" v-show="index < 4" :key="index">
</tr>
2
# v-if、v-show、v-model
v-if 用于条件性地渲染内容,可以跟v-else或者v-else-if搭配使用
<div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else="type === 'D'">
D
</div>
2
3
4
5
6
7
8
9
10
11
12
v-show与v-if都是条件展示,v-if有组件的销毁和重建,v-show只切换css
v-model用在表单、
← rust(六) Vue.js前端框架(二) →