我们都知道函数的封装和重用,将具有相同功能的抽象成函数方法,在需要的时候直接传递不同的参数进行调用即可。我们同时应该也清楚一些无益的体力劳动我们可以避免,简单的封装可以使代码量更少,在有助于性能优化的同时还可以减少错误的发生。我们毋庸置疑:代码写得越多,出现bug的几率越大。
当然在我们开发页面的时候,具有类似功能的我们可以抽象成公共组件,例如一个小弹窗,ip输入......乃至整个页面逻辑的封装,这样我们就能大容量的减轻代码的耦合。
我理解的所谓公共组件其实就是这个组件在被引用的时候可能涉及到所有功能的并集,也就是所有的功能点,这样我们会联想到我们上学时候所学的交集和并集。接下来我们就拿一个简单的例子说明下,例如一个修改密码的弹框,有这样一个场景:我们在登录的时候我们利用默认密码或者安全不太强的密码登录他会提示我们修改默认密码,如图1,它默认有验证码输入:
还有一种情况我们登录成功,在登录的情况下,我们也有修改密码的入口,这时候如图2,他没有要求输入验证码
这个例子很小但也可以利用来体现整个设计过程图3
我们考虑这个功能的时候我们需要考虑这个功能的全面性,我们公共组件如果只是公共功能的话,整个组件我也就能实现类似于图2的状态,但我们需要验证码的时候,调用公共组件的时候,我无论如何也显示不出来,没有集成在公共组件里怎么实现?所以公共组件是包含了你分析了在不同地方引用可能出现的所有功能点,他是所有功能点的并集。就像一个商店卖东西,每个人去买东西买的可能不一样,但每个人买的时候,你尽可能保证有每个人需要的那个东西,公共组件也是这样,我只要用这个功能你一定得有。扯着扯着感觉好像再说框架一样,每个框架引用的时候你可能只用一部分API,但他有很多API,你不必担心,总有人能用到。
这样我们分析了这样一个小小的组件,接下来我们该考虑如何实现它:接下来我们用vue举例,其实react等其它框架是一样的:
验证码显示隐藏,我们肯定想到利用v-if或v-show去控制,这里我们因为没有频繁的显示隐藏,所以为了降低初始开销,我们用v-if
<div class="auth-code" v-if="authCodeShow">
<p class="form-label">
<span class="form-label-name">验证码:</span>
</p>
<input type="text" v-model="confirmCode">
<img id="img-rand-code" ref="codeImg" :src="codeUrl" @click="changeCode" alt="验证码">
</div>
通过变量authCodeShow去控制,这时候我们考虑这个参数应该由谁去控制?因为在父组件引用公共组件的时候我们才会考虑他的显隐,例如在登录页显示验证码,在登陆后不显示,这样我们明白他的显隐是由父组件控制的,所以这个应该是父组件利用props传进来的:
props: {
authCodeShow: {
type: Boolean,
default: false
}
}
接下来我们还应该考虑这个弹框的显示隐藏,我们肯定是通过父组件中的修改密码按钮触发的,所以我们控制整个组件显示隐藏的也在父组件我们用isShow去控制,
// 父组件中调用子组件
<changePassPop ref="changePass" v-if="popShow"></changePassPop>
我们根据图4可以看出我们可以通过弹框中的X 和取消,确定按钮去关闭弹窗,自己关闭自己但我们应该仔细想想,控制子组件的显示隐藏的参数在父组件,所以操作的时候我们需要同时改变父组件的值,所以同时我们还应通过props传进一个值isShow用来控制弹框的显示隐藏
<changePassPop ref="changePass" v-if="popShow" :isShow.sync="popShow"></changePassPop>
这里我用了sync修饰符,他的功能其实就是函数回调更改父组件的值,并不是数据的双向绑定(单项数据流也不允许),sync的用法这应该都知道,不知道的可以穿越 sync修饰符的讲解
因为在例子里我用了element-ui中的dialog,所以他还需要一个show控制dialog的显示隐藏,模板是这样
<template>
<div>
<div class="model">
<el-dialog :visible.sync="show" :modal="false" size="tiny" top="20%">
<div class="form">
<div>
<p class="form-label">
<span class="form-label-name">用户名:</span>
</p>{{username ? username : userName}}</div>
<div>
<p class="form-label">
<span class="form-label-name">旧密码:</span>
</p>
<input type="password" v-model="oldPass">
</div>
<div>
<p class="form-label">
<span class="form-label-name">新密码:</span>
</p>
<input type="password" v-model="newPass">
</div>
<div>
<p class="form-label">
<span class="form-label-name">确认新密码:</span>
</p>
<input type="password" v-model="confirmNewPass">
</div>
<div class="auth-code" v-if="authCodeShow">
<p class="form-label">
<span class="form-label-name">验证码:</span>
</p>
<input type="text" v-model="confirmCode">
<img id="img-rand-code" ref="codeImg" :src="codeUrl" @click="changeCode" alt="验证码">
</div>
<div slot="footer" class="foot clearFix">
<el-button @click="show=false">取 消</el-button>
<el-button type="primary" @click="changeDefaultPsw">确 定</el-button>
</div>
</div>
</el-dialog>
</div>
</div>
</template>
所以在控制show的时候同时改变父组件中的popShow的值,这里我们很容易联想到watch方法
watch: {
show() {
this.$emit('update:isShow', false)
}
},
这样我们子组件控制了自己的显示隐藏。
接下来我们继续看功能,用户名是智能添加不是自己输入那种,其他的旧密码和新密码等都是自己输入的,存在于公共组件的自己的data对象里面,接下来我们可能很快想到Vuex状态管理进行数据共享,这样是没问题的,初始化state,然后通过状态管理。这种方法可行,但是单纯的一个纯父子组件的数据传递我感觉没有必要用状态管理,虽然我当初也是利用vuex做的,但我们应该明白我们利用Vuex我们是为了更好的追踪共享数据的变化。讲到这里有必要讲讲vuex了。有的人用了vuex,感觉还不错,就好像全局变量存储库,我不用认真去考虑数据的流向props,这种想法是错的!我们不能将vuex当成localStorage和sessionStorage去使用,vuex是为了更好地去追踪共享state的变化,为了避免更繁杂的数据流向向下图这样:
当子组件1想想向组件2中进行传值,鉴于数据的单向流动,我们需要经过父组件(当然是通过函数回调传的),然后在传给组件2,当然感觉这样也还没什么,若果他们的父组件也是兄弟组件,他们有共同的爷爷,你需要传给他的爷爷,再通过他们爷爷往父组件二进行传递,如果再嵌套深一点,这TM还不把自己传乱
所以这时候vuex就能很好地解决我们的问题,共享state也能更好的进行追踪,同时更改state只能通过mutation进行更改,这样有了错误也能更的追踪,当项目大的时候我们可以好好利用vuex的优点,但是我们知道每次加载页面的时候都要初始化store,若果数据太多的时候,初始化的时间就比较长,这样就会给人造成一种加载速度慢的印象,所以我们说没有必要放在vuex的数据就不要放,千万不能把它当成本地存储,不要浪费了作者的设计灵魂。
跑偏了,赶紧回来。刚才那个用户名的我们其实存在父组件,接着我们再往下看,确定按钮肯定把你的表单提交给后台,但这时我们又有这样的顾虑,其实也真的是顾虑。我们提交的后台接口不一样,擦!后台这是肿么了,砍了他。哈哈!有时候这是一个假设,但没准也可能遇到,所以确定按钮绑定的方法我们不能写死,因为接口不一样嘛!所以我们可以这样给公共组件开放个方法,confirmChangePsw,我们让它绑定父组件的方法,提交的方法在父组件实现因为父组件是可控的,公共组件只是为了更大的兼容所有的功能要求,这样我们在公共组件中这样定义
// 公共组件
<el-button type="primary" @click="changeDefaultPsw">确 定</el-button>
// 修改默认密码
changeDefaultPsw() {
this.result = this.checkInput('oldPass') && this.checkInput('newPass') &&
this.checkInput('confirmNewPass')
if (this.authCodeShow) {
this.result = this.result && this.checkInput('confirmCode')
}
if (this.result) {
this.show = false;
this.$emit('confirmChangePsw')
}
}
// 父组件调用公共组件
<changePassPop ref="changePass" v-if="popShow" :isShow.sync="popShow" @confirmChangePsw="handleChangePsw"></changePassPop>
我们通过点击确定按钮触发了changeDefaultPsw函数,但是这个方法其实是为了触发子组件绑定的自定义方法confirmChangePsw,自定义confirmChangePsw方法其实调用的是父组件的handleChangePsw方法
可能这时我们开始怀疑为啥这样做?组件自定义方法什么鬼?接下来我们来说说组件的一些特质,请原谅我这个爱引申的毛病
组件与组件之间是相互独立的,组件中的方法只能在本组件调用,数据只能通过单项数据流传递公共组件在父组件调用的时候我们的公共组件访问的数据和方法都是父组件里面的
// 父组件调用公共组件
<changePassPop ref="changePass" v-if="popShow" :isShow.sync="popShow" @confirmChangePsw="handleChangePsw"></changePassPop>
其实就类似于函数的私有作用域
function father(params) {
function son1(params) {
var a = 1
}
function son2(params) {
console.log(a); // 调用的时候会报错
}
}
因为函数会形成一个私有作用域不允许其他无关函数访问,有人可能会说这样不就行了
function father(params) {
var a = 1
function son1(params) {
}
function son2(params) {
console.log(a); // 1
}
}
对呀!像上面这样是没问题的!所以刚才我说的是组件就类似于函数的私有作用域,类似,类似!!但不完全一样,组件有着更严格的权限,子组件也不能共享父组件的数据,如果你需要父组件的某个数据只能通过props传递过来
继续回来。。。。
我们相当于给确定按钮一个开放的权限,我给你暴露的方法,但如何定义由你自己控制。
当然还有一些ref拿值的方法!自己看代码去体会吧!
我贴下代码
<template>
<div>
<div class="model">
<el-dialog :visible.sync="show" :modal="false" size="tiny" top="20%">
<div class="form">
<div>
<p class="form-label">
<span class="form-label-name">用户名:</span>
</p>{{username ? username : userName}}</div>
<div>
<p class="form-label">
<span class="form-label-name">旧密码:</span>
</p>
<input type="password" v-model="oldPass">
</div>
<div>
<p class="form-label">
<span class="form-label-name">新密码:</span>
</p>
<input type="password" v-model="newPass">
</div>
<div>
<p class="form-label">
<span class="form-label-name">确认新密码:</span>
</p>
<input type="password" v-model="confirmNewPass">
</div>
<div class="auth-code" v-if="authCodeShow">
<p class="form-label">
<span class="form-label-name">验证码:</span>
</p>
<input type="text" v-model="confirmCode">
<img id="img-rand-code" ref="codeImg" :src="codeUrl" @click="changeCode" alt="验证码">
</div>
<div slot="footer" class="foot clearFix">
<el-button @click="show=false">取 消</el-button>
<el-button type="primary" @click="changeDefaultPsw">确 定</el-button>
</div>
</div>
</el-dialog>
</div>
</div>
</template>
<script>
import {
mapState
} from 'vuex'
import {
STI_AJAX_BASEURL
} from 'commons/constant'
const CODE_URL = `${STI_AJAX_BASEURL}/login/vcode`
export default {
props: {
isShow: {
type: Boolean,
default: false
},
authCodeShow: {
type: Boolean,
default: false
},
userName: {
type: String
}
},
computed: {
...mapState({
username: state => state.userInfo.username,
userId: state => state.userInfo.userId
})
},
watch: {
show() {
this.$emit('update:isShow', false)
}
},
mounted() {
if (this.authCodeShow) {
this.changeCode()
}
},
data() {
return {
result: null,
show: true,
oldPass: '',
newPass: '',
confirmNewPass: '',
confirmCode: '',
codeUrl: CODE_URL,
tips: {
oldPass: '请输入旧密码',
newPass: '新密码不能为空',
confirmNewPass: '两次输入的密码不一致',
authcode: '请输入验证码'
}
};
},
methods: {
// 检查输入内容
checkInput(param) {
if (this.$data[param]) {
if (param === 'confirmNewPass') {
if (this.confirmNewPass !== this.newPass) {
alert('两次输入的密码不一致')
}
}
return true
} else {
alert(this.tips[param])
return false
}
},
// 更换验证码
changeCode() {
this.codeUrl = CODE_URL + '?' + new Date().getTime()
},
// 修改默认密码
changeDefaultPsw() {
this.result = this.checkInput('oldPass') && this.checkInput('newPass') && this.checkInput('confirmNewPass')
if (this.authCodeShow) {
this.result = this.result && this.checkInput('confirmCode')
}
if (this.result) {
this.show = false;
this.$emit('confirmChangePsw')
}
}
}
}
</script>
<style lang="less" scoped>
.clearFix:after {
display: block;
height: 0;
content: '';
clear: both;
overflow: hidden;
}
.form {
>div {
height: 40px;
margin-bottom: 6px;
>input {
height: 36px;
width: 60%;
line-height: 36px;
padding-left: 8px;
}
.form-label {
float: left;
width: 100px;
height: 38px;
margin-right: 20px;
.form-label-name {
float: right;
}
}
}
.foot {
width: 100%;
margin-top: 20px;
padding-left: 56%;
}
}
</style>
通过上面的分析我们可以归纳到公共组件的抽象方法
- 先分析我可能涵盖的功能需求
- 需要传哪些值进来
- 需要开放哪些方法便于调用该组件的组件自己定义灵活控制