在读这篇文章以前,我想先安利你们一个东西: html
看到这副黑框眼镜,你是否是想到了什么?前端
对,就是它:Vue.js 组件编码规范。读过的同窗忽略,没读过的同窗有时间的话请花 20 分钟认真看看,文章的内容都是在承认这篇规范的基础上展开的。vue
另外,本文中的“最佳实践”(注意引号),全都是一家之言,不必定对,欢迎各路大佬讨论拍砖。ios
组件(component)是 vue 最核心的概念之一,可是正由于这一律念太过宽泛,咱们会在实际开发中看到各类各样的组件,对开发和维护的同窗带来了很大的困惑和混乱。这里我把组件分红四类:git
顾名思义,view 指的是页面,你也能够把它叫作 page。它的定义是:和具体的某一条路由对应,在 vue-router 配置中指定。view 是页面的容器,是其余组件的入口。它能够和 vuex store 通讯,再把数据分发给普通组件。github
全局组件,做为小工具而存在。例如 toast、alert 等。他的特色是具有全局性,直接嵌套在 root 下,而不从属于哪一个 view。global component 也和 vuex store 通讯,它单独地使用 state 中的一个 module,这个 state 中的数据专门用来控制 gloabl component 的显隐和展现,不和其余业务实体用到的 state 混淆。
其余组件想修改它,能够直接派发相应的 mutation。而要监听它的变化(好比一个全局的confirm,确认以后在不一样的组件中触发不一样的操做),则使用全局事件总线(event bus)。vue-router
简单组件。这种组件对应的是 vue 中最传统的组件概念。它的交互和数据都很少,基本上就是起到一个简单展现,拆分父组件的做用。这种组件和父组件之间经过最传统的方式进行通信:父组件将 props 传入它,而它经过 $emit 触发事件到父组件。
简单组件内部是不写什么业务逻辑的,它能够说是生活不能自理,要展现什么就等着父组件传入,要干什么就 $emit 事件出去让父组件干,父组件够操心的。vuex
复杂组件。这种组件的特色是,内部包含有不少交互逻辑,经常须要访问接口。另外,展现的数据也每每比较多。以下图。vue-cli
<template>
内的代码可不得上天了?
<script>
内的代码可不得上天了?
我画了一张图来讲明上面这四种 component 的关系,但愿能帮助你们更好理解。json
在区分了这四种 component 后,咱们在编码时就能作到内心有数,如今在写的组件,到底属于哪一类?每一类以特定的方式编写和交互,逻辑上就会清晰不少。 使用 vue-cli 构建的项目中都会有一个目录叫作 component,之前是一股脑往里塞,如今能够在此基础上再设置几个子目录,放置不一样类型的组件。
先来看一个栗子🌰
假设有一个模态对话框的组件。父组件为了可以打开模态框,给模态框传入了一个控制其显隐的 props,命名为 visible,type 为 Boolean,绑定模态框外层的 v-if 指令。那么,问题来了,若是咱们点击了模态框内部的关闭按钮,关闭自身,应该怎么写?
固然,最传统的方式天然仍是模态框抛出事件,父组件中设置监听,而后修改值。但这种方式无疑有很强的侵入性,无故增长了不少的代码量。关闭按钮在模态框内部,关闭本身是我本身的事儿,能不能不让父组件管这些?
有同窗说了,直接在模态框内部修改 visible 啊。this.visible = false
,不行吗?
还真不行。若是这么干,你会看到如下一堆报错:
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value.
vue 很明确地告诉你了,做为子组件,你要安分守己,不准随便修改老爹传给你的 props。
那么咱们应该怎么办?
咱们思考一下,若是不容许修改 props 的值,那咱们修改 porps 的······属性如何?
事实证实,是能够的。
咱们能够把上面 visible 的 type 设为 Object,模态框的显隐决定于 visible.value。当模态框想要关闭自身时,只需 this.visible.value = false
便可。 这种方式看起来至关方便,但实际是一种投机取巧的方法。上面安利的 Vue.js 组件编码规范中明确有一条规范,就是 props 原子化,也就是说,props 里的字段必须是简单的 String,Number 或 Boolean。这么作的缘由是:
因此,咱们把 visible 改成 Object,原本就是违反规范的。
vue 中有种已经存在的机制,和现有需求很像,这就是 v-model。在表单中,每个 input,就像一个子组件。在外层经过 v-model 绑定的值能够在 input 中回显,而 input 自己的值也能改变。
事实上,v-model 仅仅是一个语法糖,v-model="xxx"
,就至关于 :value="xxx" @input="val=>xxx=val"
。那么,咱们就能够利用 v-model 的这种特性来实现咱们的需求。咱们只须要在模态框内部抛出一个 input 事件 this.$emit('input', false)
,就能关闭自身了。
这种方式比较简洁,也不违反规范,可是容易让人困惑,觉得这里是要进行什么表单操做。
咱们还有没有什么更好的方式呢?
若是你是从大版本为 1 时就开始接触 vue,那你能够知道一个修饰符,叫作.sync。若是你是从 2.0 开始接触的,则极可能不熟悉它。这是由于,vue 在 2.0 版本时把它删除了,不过好在, 2.3 版本以后,它又回来了。
这个修饰符简直就是为咱们这个需求量身定制的。它自己是一个和 v-model 相似的语法糖,咱们要作的,仅仅是在组件内部须要改动值的地方,抛出一个 update 事件。this.$emit('update:foo', newValue)
。既不违反规范,也足够清晰,能够说是最佳的解决方案了。惟一的不足之处,就是对版本有一点要求。
数据是 SPA 的核心,而数据的来源都是接口。如何优雅、高效地经过接口请求数据,是开发者必需要关心的问题。在实践中,我是这样封装接口的:
success: false
。对于这两种错误,咱们都要捕获并处理。下面是示例代码,可供参考。
if (opt.method === 'post') {
axiosOpt.data = opt.payload
} else if (opt.method === 'get') {
axiosOpt.params = opt.payload
}
if (opt.withFile) {
Object.assign(axiosOpt, { headers: {
'Content-Type': 'multipart/form-data'
}})
}
// 全局请求的 loading,当请求 300 ms 后还没返回,才会出现 loading
const timer = setTimeout(() => {
store.dispatch('showLoading', {
text: '加载数据中'
})
}, 300)
try {
// 开始请求
const result = await axios(axiosOpt)
// 若是 300 ms 还没到,就取消定时器
clearTimeout(timer)
store.dispatch('closeLoading')
if (result.status === 200 && result.statusText === 'OK') {
if (result.data.success) {
return result.data.results || true
} else {
// 请求失败的 toast
store.dispatch('showAlert', {
type: 'error',
text: `请求失败${result.data.message ? `,信息:${result.data.message}`: ''}`
})
return false
}
} else {
return false
}
} catch(e) {
clearInterval(timer)
// 请求失败的 toast
store.dispatch('closeLoading')
store.dispatch('showAlert', {
type: 'error',
text: '请求失败'
})
return false
}
复制代码
SPA中,每个 view 中的都有不少数据是须要经过接口请求得到的,若是没有得到,页面中就会有不少空白。上面,咱们讨论了如何封装好接口请求,下一步就是决定何时请求初始化数据,即,代码在哪里写的问题。实践下来,有两个时机是比较理想的。
vue-router 提供了以上两个生命周期钩子,分别会在进入路由和路由改变时触发。这两个钩子是写的 view 中的。
vue-router还提供了一个全局性的 beforeEach 方法,任何一个路由改变时,都会被这个方法拦截,咱们能够在这个方法中加入咱们本身的代码,作统一处理。好比,对于全部 view 初始化请求的 action,咱们能够以特定的名称命名,如以 _init 做为后缀等。在 beforeEach 方法内,咱们对当前 view 对应的 store 进行监听,查找到其中以 _init 命名的 action 并派发。
以上两种方式各有特色。
对于前者,优势是数据获取的代码和具体的 view 是绑定在一块儿的,咱们能够在 view 内部就清晰地看到数据获取的流程。缺点是,每增长一个页面,都要在其内部写一堆初始化代码,增长了代码量。 对于后者。优势是,代码统一且规整,使用了配置的方式,写一次便可,不须要每次增长额外的代码。缺点是比较隐晦,且初始化代码和 view 自己割裂了。
对于以上两种方式如何取舍的问题,我倾向于,大型项目用后者,小型项目用前者。
做者:丁香园前端团队-㍿社长