本文首发于TalkingCoder,一个有逼格的程序员社区。转载请注明出处和作者。
写在前面
本文为系列文章,总共分四节,建议按顺序阅读:
《Vue+Webpack开发可复用的单页面富应用教程(配置篇)》
《Vue+Webpack开发可复用的单页面富应用教程(组件篇)》
《Vue+Webpack开发可复用的单页面富应用教程(技巧篇)》
在前两节中,我们介绍了在项目https://github.com/icarusion/vue-vueRouter-webpack中如何配置webpack以及使用vue的组件化开发SPA。通过前两节的介绍,现在应该可以轻松的写几个路由和组件了吧,并且在这个工程的基础上开发会省去很多事情,我们的重心应该在组件的开发上。随着项目的不断扩大和对vue的学习程度不同,多少会出现一些复杂的问题或者编程的最佳实践,本篇文章就来介绍一些笔者在几个项目中积累的一些经验谈。
山寨的Vuex
说到山寨,我们先看看正版的Vuex是个什么鬼。
当你的Vue项目比较大,或者说组件使用的比较深度(层级、复用性)的时候,状态、数据越来越共享化,如果把数据都存在组件中,会变的不太好维护,那么Vuex就是在这种情况下出现的。它借鉴于Flux和Redux的设计思想,用过React的话应该不会陌生,不过要比那些简单,而且转为Vue量身打造。
为什么需要山寨的Vuex
既然已经有正版的Vuex了,而且也不复杂,为什么需要山寨的vuex呢?我们先介绍一下什么是山寨的Vuex,其实这里有点概念化了,山寨的vuex并没有什么特殊,只是一个普通的js文件,为了达到类似的目的,我们将其命名为了vuex.js。它的结构大概是这样的:
exports.POILIST = {
data : {
list: []
},
clear : function() {
this.data = {
list: []
};
}
};
exports.MENU = {
data: {
list: [],
select: {}
},
clear: function() {
this.data = {
list: [],
select: {}
};
}
};
可以看到它对外导出了一些模块,模块的结构都有一个data,这个data里存储的就是我们需要共享的数据了,组件在通信时,不直接向上或向下传递数据,而是保存在vuex.js里,出于对象的引用关系,vue可以天然的实现共享数据的双向绑定,在使用时是这样的:
<template>
<div>
<li v-for="item in menu.list">{{ item }}</li>
<p v-for="item in poi.list">{{ item.lng }},{{ item.lat }}</p>
</div>
</template>
<script>
import { POILIST, MENU } from '../config/vuex.js';
export default {
data() {
return {
menu: MENU.data,
poi: POILIST.data
}
},
methods: {
set() {
MENU.list.push('首页');
POILIST.list.push({
lng: 124.1,
lat: 42.3
});
}
}
}
</script>
这种存储,可以理解为相当于存在全局,并且可以加一些事件来操作数据,比如我们demo的clear()用来清空数据,只不过这里是用函数来调用,也可以将其改为观察者模式,用事件派发的形式来修改数据,其实vuex正是这么玩的。
有了vuex.js,组件之间就可以直接保存到它里面,这对数据持久化、数据共享来说是一个简单粗暴的解决方案。它适用了不大的项目(当然在大型项目中它也没什么问题),或者不太想在当下接入vuex的架构,或者想简单而又优雅地维护一些全局状态和数据。
坑
我们知道,JavaScript的对象是引用关系,也正是这个原因我们可以设计vuex.js,但也正是这个原因,如果你的数据不是对象,只是简单数组、数字、字符串等,是不能直接共享的,JS会复制来使用,所以我们在组件的data里,return的都是像MENU.data
而不是MENU.data.list
,在遍历时,才循环menu.list
。
到底用prop还是事件
上一篇文章介绍过,组件之间数据交互,主要有prop和事件两种,在使用过程中发现,这两种方案有时候都可以达到同样的效果,比如下面的这个场景,父组件向子组件传递的一个数据id,子组件来判断这个id如果发生变化,就做一些事情。用prop是这么实现的:
<!-- 父组件 -->
<template>
<child :id="id"></child>
</template>
<script>
export default {
data() {
return {
id: 1
}
},
ready() {
setTimeout(() => {
this.id = 2;
}, 1000);
}
}
</script>
<!-- child组件 -->
<template>
</template>
<script>
export default {
props: {
id: {
type: Number,
default: 0
}
},
data() {
return {}
},
methods: {
handler() { }
},
watch: {
id() {
this.handler();
}
}
}
</script>
child组件通过watch来监听id的改变,如果用事件可以这样实现:
<!-- 父组件 -->
<template>
<child :id="id"></child>
</template>
<script>
export default {
data() {
return {
id: 1
}
},
ready() {
setTimeout(() => {
this.$broadcast('child-set-data', {
id: 2
});
}, 1000);
}
}
</script>
<!-- child组件 -->
<template>
</template>
<script>
export default {
props: {
id: {
type: Number,
default: 0
}
},
data() {
return {}
},
methods: {
handler() { }
},
events: {
id() {
this.handler();
}
}
}
</script>
这次是通过events来接收事件,效果是一样的。既然两者都可以,那如何优雅的选择呢,笔者觉得可以在数据功能上进行区分。比如上面的场景,是由子组件child在知道id改变后,在它自己的作用域里去执行handler()方法,handler可能是一个需要ajax获取数据并渲染到child组件上的函数,这时候就推荐用watch来监听id的改变了。如果handler所对应的ajax任务是在父组件完成的,父组件需要将拿到的一堆json数据传给child时,这时可以通过事件来传递。不过说到底,还是看使用者的习惯了,这两者都是可以的,只不过事件更多的是来触发外部环境的action。
管好组件的一亩三分地
用好prop的sync
上篇文章介绍过,组件间可以通过prop传递数据,而且使用sync还可以双向绑定:
<child :id.sync="id"></child>
这样id就是一个双向的了,子组件修改后,父组件也会修改,但这样做有时候会分不清到底是谁改的,也很可能会不小心就修改了父组件的值,所以在使用时要特别注意。
验证prop
有时为了省事,在写组件时就将props直接写一个数组了,但如果你写的组件是给别人用的,那还是建议对每个prop都进行严格的验证,确保他人正确使用。具体验证方法可查看文档。
子组件不该干涉父组件的状态
子组件可以通过$parent访问到父组件,也就是说它有权利去修改父组件的状态,但不建议这样做,虽然有时候是省事了,但是这让父子组件之间紧耦合,很难知道到底是谁来维护数据,而且你开发给别人用的组件,随意修改了父级的状态,这会让使用者不知所措。所以子组件应该只关心自己的数据,要修改父级状态时,通过$dispatch()
方法派发事件来通知父级,由父级自己修改。
总结
一口气写完这4篇文章,还真是有点辛苦啊。半年前还只是用Vue的一些简单功能,到现在TalkingData的多条产品线已逐步使用这整套架构,这个推动的过程还是很快的,也是真正意识到Vue是一个很出色的框架,对它充满了信心。TalkingData接下来陆续上线的几个重磅产品(Data Cloud、Marketing Cloud),也是证明了这一点。很快Vue2.0也要来了,对它更是充满了期待。前端就是这么神奇,总有玩不尽的框架,但一定要选对一个适合自己的技术栈。