关于 vue 全家桶的四个 “最佳实践”

前言

在读这篇文章以前,我想先安利你们一个东西: html

Vue.js 组件编码规范

看到这副黑框眼镜,你是否是想到了什么?前端

对,就是它:Vue.js 组件编码规范。读过的同窗忽略,没读过的同窗有时间的话请花 20 分钟认真看看,文章的内容都是在承认这篇规范的基础上展开的。vue

另外,本文中的“最佳实践”(注意引号),全都是一家之言,不必定对,欢迎各路大佬讨论拍砖。ios

实践一:如何分类组件

组件(component)是 vue 最核心的概念之一,可是正由于这一律念太过宽泛,咱们会在实际开发中看到各类各样的组件,对开发和维护的同窗带来了很大的困惑和混乱。这里我把组件分红四类:git

view

顾名思义,view 指的是页面,你也能够把它叫作 page。它的定义是:和具体的某一条路由对应,在 vue-router 配置中指定。view 是页面的容器,是其余组件的入口。它能够和 vuex store 通讯,再把数据分发给普通组件。github

global component

全局组件,做为小工具而存在。例如 toast、alert 等。他的特色是具有全局性,直接嵌套在 root 下,而不从属于哪一个 view。global component 也和 vuex store 通讯,它单独地使用 state 中的一个 module,这个 state 中的数据专门用来控制 gloabl component 的显隐和展现,不和其余业务实体用到的 state 混淆。
其余组件想修改它,能够直接派发相应的 mutation。而要监听它的变化(好比一个全局的confirm,确认以后在不一样的组件中触发不一样的操做),则使用全局事件总线(event bus)。vue-router

simple component

简单组件。这种组件对应的是 vue 中最传统的组件概念。它的交互和数据都很少,基本上就是起到一个简单展现,拆分父组件的做用。这种组件和父组件之间经过最传统的方式进行通信:父组件将 props 传入它,而它经过 $emit 触发事件到父组件。
简单组件内部是不写什么业务逻辑的,它能够说是生活不能自理,要展现什么就等着父组件传入,要干什么就 $emit 事件出去让父组件干,父组件够操心的。vuex

complex component

复杂组件。这种组件的特色是,内部包含有不少交互逻辑,经常须要访问接口。另外,展现的数据也每每比较多。以下图。vue-cli

图中红框内部的就是一个复杂组件的实例。它是一个大列表的列表项,展现的数据不少,并且点击左下角的几个 button,还会弹出相应的弹窗,弹窗内有复杂的表单须要填写提交···逻辑能够说是至关复杂了。若是这时咱们还拘泥于简单组件的那种通讯方式,衣来伸手饭来张口,啥事儿不干,那么:
1.全部的 props 都由父组件一一传入,若是有十几个乃至几十个要展现的数据,那么父组件 <template> 内的代码可不得上天了?
2.全部的业务流程都要 $emit 出去要父组件处理,那么父组件 <script> 内的代码可不得上天了?
因此,对于这种复杂组件,咱们应该容许它有必定的“自主权”。能够跳过父组件,自行和 vuex 通讯,获取一下 state,派发一下 mutation 和 action,不是很开心么。

我画了一张图来讲明上面这四种 component 的关系,但愿能帮助你们更好理解。json

在区分了这四种 component 后,咱们在编码时就能作到内心有数,如今在写的组件,到底属于哪一类?每一类以特定的方式编写和交互,逻辑上就会清晰不少。 使用 vue-cli 构建的项目中都会有一个目录叫作 component,之前是一股脑往里塞,如今能够在此基础上再设置几个子目录,放置不一样类型的组件。

实践二:如何优雅地修改 props

先来看一个栗子🌰
假设有一个模态对话框的组件。父组件为了可以打开模态框,给模态框传入了一个控制其显隐的 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。这么作的缘由是:

  • 使得组件 API 清晰直观。
  • 只使用原始类型和函数做为 props 使得组件的 API 更接近于 HTML(5) 原生元素。
  • 其它开发者更好的理解每个 prop 的含义、做用。
  • 传递过于复杂的对象使得咱们不可以清楚的知道哪些属性或方法被自定义组件使用,这使得代码难以重构和维护。

因此,咱们把 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 的核心,而数据的来源都是接口。如何优雅、高效地经过接口请求数据,是开发者必需要关心的问题。在实践中,我是这样封装接口的:

从高层到底层,依次说明。
第一层就是组件。
第二层则是 vuex 中的 action,咱们在组件中调用 action,基本操做。
第三层是 api。在这里,咱们预先定义了每个接口。包括接口的 url、type、content-type,以及写死的请求参数。在 action 中,咱们调用 api 请求接口。
第四层是 request,这是咱们请求的公共方法,做用就是对特定的 http client。 进行封装,实现一套统一的接口请求——处理流程。
第五层则是以 axios 为表明的各类 http client。
咱们主要进行编码的是第三层和第四层,也就是 api 和 request。api 的编写没有什么难点,主要谈谈 request 的代码。这部分代码,咱们要关心如下几个方面。

  • loading 处理。当请求时间比较长时,要跳出全局的 loading 让用户知晓。
  • 错误处理。有两种错误,第一种是 http 请求直接返回错误码。第二种,虽然请求的返回值是 200,可是返回结果中提示错误。好比返回的 json 中 success: false。对于这两种错误,咱们都要捕获并处理。
  • api 一致性处理。http client 接受的参数是有讲究的,以 axios为例,get 请求的请求参数为 params,而 post 请求的参数则为 data。对于这种差别,request 这层须要将其抹平,api 层不须要在定义接口时关心这些。

下面是示例代码,可供参考。

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 中的都有不少数据是须要经过接口请求得到的,若是没有得到,页面中就会有不少空白。上面,咱们讨论了如何封装好接口请求,下一步就是决定何时请求初始化数据,即,代码在哪里写的问题。实践下来,有两个时机是比较理想的。

beforeRouteEnter/Update

vue-router 提供了以上两个生命周期钩子,分别会在进入路由和路由改变时触发。这两个钩子是写的 view 中的。

router.beforeEach

vue-router还提供了一个全局性的 beforeEach 方法,任何一个路由改变时,都会被这个方法拦截,咱们能够在这个方法中加入咱们本身的代码,作统一处理。好比,对于全部 view 初始化请求的 action,咱们能够以特定的名称命名,如以 _init 做为后缀等。在 beforeEach 方法内,咱们对当前 view 对应的 store 进行监听,查找到其中以 _init 命名的 action 并派发。
以上两种方式各有特色。
对于前者,优势是数据获取的代码和具体的 view 是绑定在一块儿的,咱们能够在 view 内部就清晰地看到数据获取的流程。缺点是,每增长一个页面,都要在其内部写一堆初始化代码,增长了代码量。 对于后者。优势是,代码统一且规整,使用了配置的方式,写一次便可,不须要每次增长额外的代码。缺点是比较隐晦,且初始化代码和 view 自己割裂了。
对于以上两种方式如何取舍的问题,我倾向于,大型项目用后者,小型项目用前者。

Other Tips

  • 多使用 mixing,可以在组件级别抽离公共部分,减小冗余,极好的机制。
  • 多使用常量,这点和 vue 自己没有关系,可是能极大地提高代码的健壮性。
  • 连接若是是在项目内部跳转,多使用 ,而不是去拼 a 标签的 href。
  • 不要用 dom 操做。但若是无可奈何,好比你要得到某个 dom 的 scrollTop 属性,用 $ref,而不是用选择器去取。
  • 能想到的就这些,欢迎大佬们讨论补充。

做者:丁香园前端团队-㍿社长

相关文章
相关标签/搜索