从文档开始,重学Vue(上)

文章较长👉请先关注收藏👈如果一不小心解决了你在使用vue中的某个痛点记得点个赞哦🤒

闲扯一番

vue也有些年头了,不得不说vue确实是一个了不起的框架(不接受任何反驳😄)但在工作中有太多的前端开发者还只是停留在会用的圈圈中,有很多人觉得我没有看完官方文档也不妨我们做vue项目写vue代码啊?确实,这点不可否认

但是大哥,你一个vue文件写1000多行,是觉得自己的头发掉的不够快吗?

你们信不爱读文档的程序员能写出好代码吗?反正我是不信🙃

举个例子

我们知道prop是接受父组件参数,假如现在要接收一个对象,可能你会这样用

<!--父组件传过来的值 父组件的数据是异步请求回来的-->
item:{
    name:'刘小灰',
    age:18
}
<!--子组件接收-->
Vue.component('my-component', {
    props:['item']    
}

<!--页面上使用-->
<span> {{item.name}} </span>

如果粗心的程序员没有传这个item,控制台就会报错


这个时候,聪明的你会有两个选择

  • 索性不管,不影响正常逻辑
  • 大不了加个判断
 <span v-if="item">{{ item.name }}</span>


页面又一切正常好像什么都没发生,这个时候你可能心里犯迷糊, 这个bug大家都是这样解决的吗?

如果你看过vue的官方文档,了解prop的所有用法,当你第一眼看到这个bug时就会立马反应过来,prop应该这样写更为合理

Vue.component('my-component', {
    props:{ 
        item:{
            type:Object,
            defent:()=>{return:{}}
        }
    }
}

例子可能过于简单,主要想表达的思想就是 只有先了解框架具备的所有能力,才能写出更高质量的代码

从风格指南开始


既然是重学vue说明不是第一次学了,这个时候建议大家从 风格指南 开始重学,如果是新手还是建议大家从 教程 一步一步开始学

以下示例均在 vue-cli3 中完成

组件的命名规范

在开发中你可能会遇到 不知道给组件怎么取名 的尴尬情况,遵从vue规范,让你给组件起名即 顺畅规范

组件名为多个单词

组件名应该始终是多个单词的,根组件 App 以及 、 之类的 Vue 内置组件除外。

这样做可以避免跟现有的以及未来的 HTML 元素相冲突,因为所有的 HTML 元素名称都是单个单词的。 -官方文档

用多个单词定义组件不仅可以避免和原有的HTML元素相冲突,在外观上看来也更加的好看😃

采用PascalCasekebab-case命名规范

或的意思是我们在命名时即可以采用驼峰命名da也可以采用-命名,但建议大家在项目中统一风格只用一种,我本人习惯使用PascalCase格式

单词大写开头对于代码编辑器的自动补全最为友好,因为这使得我们在 JS(X) 和模板中引用组件的方式尽可能的一致。
然而,混用文件命名方式有的时候会导致大小写不敏感的文件系统的问题,这也是横线连接命名同样完全可取的原因 -官方文档

原因就是PascalCase更有利于 代码自动补全 ,至于导致大小写不敏感的系统文件问题我暂时还没遇到

基础组件用 Base | App | V 开头

推荐用Base开头,因为更加语义化如一个基础的按钮组件我们可以叫BaseBtn.vue

单例组件用 The开头

可能有的人不熟悉什么是单例组件,单例是一种设计模式不清楚这个概念的可以自己查阅资料(也可以关注公众号 码不停息 里面有介绍),比如我们常见的element-ui中通过js调用的弹窗组件就可以看做是一个单例组件

和父组件紧密耦合的子组件应该以父组件名作为前缀命名

如果一个公用组件比较复杂我们可以抽离出几个子组件,同时从命名上区别出组件之间的关系,如:

components/
|- TodoList.vue
|- TodoListItem.vue
|- TodoListItemButton.vue

根据以上规则,我们来规范下项目中组件的目录

1. 这里我把基础组件和单例组件单独拿出来放在了`common`文件夹中`components`文件里面放置项目公共组件
2. 每个组件建议放在一个单独的文件夹而不是用单独的文件,有利于后期的扩展及维护

组件实例书写顺序规范

在我们平常开发中一个组件会调用很多vue实例,由于开发人员的习惯不同这些实例书写顺序也不同,这样无形之中增加了我们的维护成本,下面我们来看看vue推荐的书写顺序

vue文件里面js,要按照vue的生命周期来写,最开始是mixins->porps->data->computed->mounted->watch->methods->components,用不到的可以忽略,统一顺序,养成习惯

1. name
2. components
4. directives
5. filters
6. extends
7. minins
8. props
9. data
10. computed
11. watch
12. beforeCreate
13. created
14. beforeMount
15. mounted
16. beforeUpdate
17. updated
18. activated`
19. deactivated
20. beforeDestroy
21. destroyed
22. methods

上面列的比较多,在我们实际开发中,没有用到的可以不写,保留这个顺序即可

组件父子通信规范

应该优先通过 prop 和事件进行父子组件之间的通信,而不是 this.$parent 或变更 prop

一个理想的 Vue应用是 prop 向下传递,事件向上传递的。遵循这一约定会让你的组件更易于理解。然而,在一些边界情况下 prop 的变更或 this.$parent 能够简化两个深度耦合的组件

记住这句话 一个理想的 Vue 应用是 prop 向下传递,事件向上传递的 可以让我们少写很多野路子代码

vue官方的风格规范有很多,我这里只是抛砖引玉,捡了我认为比较有用的给大家回顾下,更加详细的内容可以去官方文档瞅一瞅

事件名书写规范

直接上官方推荐

如:

<BaseBtn @click="btn-click"></BaseBtn>

总结

写在<template>里面的(组件的使用,事件)使用kebab-case命名规范,其他地方使用PascalCase命名规范

  • 可以在任何地方都使用PascalCase吗?

    不推荐因为有些时候可能会出现大小写不明白情况

  • 可以再任何地方都使用kebab-case吗?

    原则上可以这个看个人爱好,需要注意的是kebab-case对代码编辑器的自动补全不太友好

再看官方教程


相信大家最初学vue的时候都看过这个教程,下面我带着大家再回顾下比较重要且容易被遗忘的一些知识点

Vue的安装

目前使用vue最常用的就是通过npm引入或者直接script标签引入,下面是官方给出的vue构建的不同版本


我们来说说不同版本的使用场景

  • UMD UMD 版本可以通过 <script> 标签直接用在浏览器中
  • CommonJS CommonJS 版本用来配合老的打包工具比如 Browserifywebpack 1。这些打包工具的默认文件 (pkg.main) 是只包含运行时的 CommonJS 版本 (vue.runtime.common.js)
  • ESModule 从 2.6 开始 Vue 会提供两个 ES Modules (ESM) 构建文件:
    • 为打包工具提供的 ESM:为诸如 webpack 2Rollup 提供的现代打包工具。ESM 格式被设计为可以被静态分析,所以打包工具可以利用这一点来进行“tree-shaking”并将用不到的代码排除出最终的包。为这些打包 工具提供的默认文件 (pkg.module)是只有运行时的 ES Module 构建 (vue.runtime.esm.js)。
    • 为浏览器提供的 ESM (2.6+):用于在现代浏览器中通过 <script type="module">直接导入。

可以看出 vue 给出了很多种构建版本适用于UMD CommonJS ESModule,对这些规范不理解的可以看 这篇文章,而 我们通常使用的通过webpack构建出来的vue-cli遵循的是ESModule规范

完成版&编译器&运行时

不同构建版本中又分为 完整版只包含运行时版本 ,为了便于理解我们可以把vue代码大致分为负责运行时的代码负责编译的代码,他们之间的关系是编译器 + 运行时 ≈ 完整版

而编译器只是在编译开发环境下使用,也就是说生产环境中我们只需要使用 只包含运行时版本vue,而不是 完整版vue,如果你是使用vue-cli可以在vue.config.js中配置生成环境下不打包vue然后通过 CDN 的方式去引入 只包含运行时版本vue,代码如下:

index.html

  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
    <title>vue-app</title>
    <script
      src="https://cdn.bootcss.com/vue/2.6.10/vue.runtime.min.js"
      crossorigin="anonymous"
    ></script>
  </head>

module.exports = {
  configureWebpack: {
    externals: {
      vue: 'Vue'
    }
  }
}

下面是通过 npm使用vue通过cdn使用vue完成版通过cdn使用只包含运行时版打包后的性能对比图

通过cdn方式引入vue打出来的包要小

我们再来看vueruntime.js (只包含运行时)vue.main.js (完整版)大小的对比

这也验证了官方的数据

在看看你的项目vue引入对了吗?

Vue并不完全遵循MVVM 模型

  • 面试官 : 你知道vue是基于什么模型吗?
  • 面试者: 知道 MVVM
  • 面试官: 欣慰地点了点头

我们来看看官网

😂😂😂

要说清楚这点,我们先来看看学习几个典型的架构模型

MVC


MVC把软件分为三个层,分别是

视图(View):用户界面。
控制器(Controller):业务逻辑
模型(Model):数据保存

他们之间的通讯方式为

可以看出MVC模型数据都是单向的,流程可以简化为

用户行为改变(点击事件)Viwe -> View通知Contoller进行逻辑处理 -> 处理后Controller通知Model层数据改变
-> Model数据改变后交给View渲染(改变view层)

注:用户也可以直接改变Contoller
MVP


MVP可以看做是MVC的衍生物,在MVPModel不能直接操作View,且所有的通讯都是双向的

MVVM


MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。
唯一的区别是,它采用双向绑定(data-binding)View的变动,自动 反映在 ViewModel,反之亦然

为什么说Vue没有完全遵循MVVM

严格意义上在MVVMViewModel之间是不能通讯的,但Vue却提供了相应的Api $refs

我们可以在项目中这样使用

<template>
  <div>
    <input type="text" ref="dome" value="1" />
  </div>
</template>

<script>
export default {
  name: 'home',
  components: {},
  data() {
    return {}
  },
  mounted() {
    console.log(this.$refs.dome.value)
    this.$refs.dome.value = 2
  },
  methods: {}
}
</script>

可以看出我们可以直接通过Model去操作View

vue官方也对$refs进行说明


所以说Vue并不是正在意义上的MVVM架构,但是思想是借鉴了MVVM然后又进行了些本土化,不过问题不大,现在根据MVVM本土化出来的架构都统称MV*架构

你还知道vue的哪些设计没有遵循MVVM规范呢? 欢迎留言

关于Vue实例

Object.freeze()

当一个 Vue实例被创建时,它将 data 对象中的所有的 property 加入到 Vue 的响应式系统中。当这些 property 的值发生改变时,视图将会产生“响应”,即匹配更新为新的值。

但在项目开发中,有的信息我们不需要他是响应式的,这个时候我们可以用Object.freeze() 如:

<template>
  <div>
    <div>
      {{ user }}
    </div>
  </div>
</template>
<script>
export default {
  name: 'index',
  data() {
    return {
      number: 2,
      price: 10,
      user: Object.freeze({ age: 18 })
    }
  },
  mounted() {
    this.user.age = 20
  }
}
</script>
<style>
.totle {
  padding-top: 20px;
}
</style>

当我们给user数据加上Object.freeze()后,如果再更改user数据控制台就会报错

Object.freeze()只能用户对象|数组

生命周期

下图展示了实例的生命周期。你不需要立马弄明白所有的东西,不过随着你的不断学习和使用,它的参考价值会越来越高。

上面是官方给出的完整的生命周期流程图,可以说是应用的灵魂,下面我们在代码中实际运行顺序为

  beforeCreate: function() {
    console.log(this)
    console.log('创建vue实例前', this)
  },
  created: function() {
    console.log('创建vue实例后', this)
  },
  beforeMount: function() {
    console.log('挂载到dom前', this)
  },
  mounted: function() {
    console.log('挂载到dom后', this)
  },
  beforeUpdate: function() {
    console.log('数据变化更新前', this)
  },
  updated: function() {
    console.log('数据变化更新后', this)
  },
  beforeDestroy: function() {
    console.log('vue实例销毁前', this)
  },
  destroyed: function() {
    console.log('vue实例销毁后', this)
  }

挑几个重要的具体说明

  • beforeCreate 创建vue前调用,这个过程中进行了初始化事件、生命周期
  • created vue创建成功之后,dom渲染之前调用,通常请求数据会写在这个函数里面
  • mounted dom创建渲染完成时调用,这个时候页面已经渲染完毕,可以再这个函数里面进行dom操作
  • updated 数据更改且 时调用,他和watch不同,watch只有监听的数据变化就会触发,而 updated要求这个变更的数据必须在页面上使用了,且 只要页面的数据发生变化都会触发这个函数
  • beforeDestroy/destroyed vue 实例或者说组件销毁前后调用,如果页面中需要销毁定时器和释放内存,可以写在这个函数里

destroyedbeforeRouteLeave

destroyed 需要和 vue-routerbeforeRouteLeave api区别开

通常意义下路由发生变化也就意味上个组件被销毁,所以这两个函数都会触发
destroyed 只是个监听功能,不能阻止页面要不要销毁
beforeRouteLeave可以通过next()控制路由是否要变化

例如:当需要判断用户是否返回时使用beforeRouteLeave而不是destroyed

关于模板语法

v-html

双大括号会将数据解释为普通文本,而非 HTML 代码。为了输出真正的 HTML,你需要使用 v-html,比如用v-html渲染后端返回回来的富文本内容

值得注意的是:
站点上动态渲染的任意 HTML 可能会非常危险,因为它很容易导致 XSS 攻击。请只对可信内容使用 HTML 插值,绝不要对用户提供的内容使用插值。

动态参数

vue指令支持动态参数,比如:

<a v-on:[eventName]="doSomething">...</a>

eventName=clickdoSomething就是点击事件当eventName=focus时doSomething就是focus事件

同理,属性也支持动态形式,如:

<a v-bind:[attributeName]="url"> ... </a>

计算属性和侦听器

计算属性 VS 方法

两者最大的区别就是

  • 计算属性是计算的作用,也就是对数据进行处理
  • 计算属性是响应式的且可以基于响应式依赖进行缓存

代码说明:

<template>
  <div>
    <span>数量:</span> <input type="number" ref="dome" v-model="number" />
    <span>价格:</span> <input type="number" v-model="price" />
    <div class="totle">
      <span> 总价: </span>
      <div>
        {{ totle }}
      </div>
      <div>
        {{ totle }}
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: 'index',
  data() {
    return {
      number: 2,
      price: 10
    }
  },
  computed: {
    totle() {
      console.log(1)
      const totle = this.number * this.price
      return totle > 0 ? totle : 0
    }
  }
}
</script>
<style>
.totle {
  padding-top: 20px;
}
</style>

这是例子很简单,就是时时计算totle值,我们在页面上故意写两个{{totle}}


但控制台中只输出了一个1,说明计算属性totle只计算了一次,页面上第二个20 直接用了第一次计算的结果

我们把totle改成方法的形式看一看

<template>
  <div>
    <span>数量:</span> <input type="number" ref="dome" v-model="number" />
    <span>价格:</span> <input type="number" v-model="price" />
    <div class="totle">
      <span> 总价: </span>
      <div>
        {{ totle() }}
      </div>
      <div>
        {{ totle() }}
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: 'index',
  data() {
    return {
      number: 2,
      price: 10
    }
  },
  methods: {
    totle() {
      console.log(1)
      const totle = this.number * this.price
      return totle > 0 ? totle : 0
    }
  }
}
</script>
<style>
.totle {
  padding-top: 20px;
}
</style>

控制台打印结果

可以看出totle在页面上调用了两次而控制台就输出两次

显然如果有多个数据依赖totle,方法的性能开销是计算属性的n倍,下面是官方的解释

计算属性 VS watch

我们使用监听着watch实现上述功能

  <div>
    <span>数量:</span> <input type="number" ref="dome" v-model="number" />
    <span>价格:</span> <input type="number" v-model="price" />
    <div class="totle">
      <span> 总价: </span>
      <div>
        {{ totle }}
      </div>
      <div>
        {{ totle }}
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: 'index',
  data() {
    return {
      number: 2,
      price: 10,
      totle: 20
    }
  },
  watch: {
    price() {
      const totle = this.number * this.price
      this.totle = totle > 0 ? totle : 0
    },
    number() {
      const totle = this.number * this.price
      this.totle = totle > 0 ? totle : 0
    }
  }
}
</script>

显然没有计算属性来的优雅,所有项目中,让我们又动态计算需求时最应该使用计算属性,而不是watch

那什么时候使用watch呢?官方给出答案

也就是说,当我们处理函数中有异步请求(定时器,ajax)时应该使用watch,因为计算属性里面不支持写异步

可以看出,编辑器直接提示computed不支持异步的写法

总结
  • 有动态处理数据的时候优先使用计算属性
  • 如果在处理数据逻辑里面有异步需求,使用watch

事件

事件修饰符

Vue中给元素添加事件可谓是最常见的操作,Vue中也为我们提供了很多事件修饰符供我们使用

<!-- 阻止单击事件继续传播 -->
<a @click.stop="doThis"></a>

<!-- 提交事件不再重载页面 -->
<form @submit.prevent="onSubmit"></form>

<!-- 修饰符可以串联 -->
<a @click.stop.prevent="doThat"></a>

<!-- 只有修饰符 -->
<form @submit.prevent></form>

<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div @click.capture="doThis">...</div>

<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div @click.self="doThat">...</div>
使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 @click.prevent.self 会阻止所有的点击,而 @click.self.prevent 只会阻止对元素自身的点击。

有两个修饰符值得我们注意:

  • once 点击事件将只会触发一次
  • native 将原生事件绑定到组件
    如:
<item-list @click="clickHandle"></item-list> // 将会触发item-list内部的clickHandle
<item-list @click.native="clickHandle"></item-list> // 将会触发父组件内部的clickHandle

官网上给出了很多像上面一样的小技巧,大家可以自行查阅

关于组件

这应该是最重要的一节,组件是vue的灵魂 在实际开发中,好的组件可以让我们的开发效率及维护成本事倍功半,反之事倍功半

如何注册全局组件

在做项目中有些组件是我们经常用的,这样的组件我们可以注册为全局组件,如:注册一个全局的BaseBtn组件

  • BaseBtn 组件
<template>
  <div class="baseben">
    这是一个按钮组件
  </div>
</template>
  • main.js 中注册
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";

import BaseBtn from "@/common/BaseBtn";
Vue.component("base-btn", BaseBtn);
Vue.config.productionTip = false;
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app");
  • 在页面中使用
<template>
  <div class="about">
    <h1>This is an about page</h1>
    <base-btn></base-btn>
  </div>
</template>
  • 页面显示

    一切OK,但是如果我们要注册多个去全局组件呢?是不是要重复上面的步骤?

自动注册全局组件

关键性方法require.context
主要流程是:读取要注册为全局组件的文件路径->循环进行动态注册

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import upperFirst from "lodash/upperFirst";
import camelCase from "lodash/camelCase";
Vue.config.productionTip = false;
const requireComponent = require.context(
  // 其组件目录的相对路径
  "./common",
  // 是否查询其子目录
  false,
  // 匹配基础组件文件名的正则表达式
  /Base[A-Z]\w+\.(vue|js)$/
);
requireComponent.keys().forEach(fileName => {
  const componentConfig = requireComponent(fileName);
  const componentName = upperFirst(
    camelCase(
      fileName
        .split("/")
        .pop()
        .replace(/\.\w+$/, "")
    )
  );
  Vue.component(componentName, componentConfig.default || componentConfig);
});

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app");

页面还是正常显示

上文说过,基础组件使用Base开头进行命名,所以require.context的筛选正则才可以这样写 Base[A-Z]\w+\.(vue|js)$/
这样以后只要我们在common文件夹下以Base开头的文件都会自动注册为全局组件😎

自定义组件使用 v-model

比如我们要封装一个input组件,使用的时候这样使用

<base-input v-model="name"></base-input>

data(){
    return{
        name:'刘小灰'
    }
}

那我们BaseInput里面如何去接受name参数呢? 我们可以使用model选项,如:

<!--BaseInput.vue-->
<template>
  <div class="inputWarp">
    <input
      type="text"
      :value="value"
      @input="$emit('change', $event.target.value)"
    />
    {{ value }}
  </div>
</template>
<script>
export default {
  props: {
    value: {
      type: String || Number
    }
  },
  model: {
    prop: "value",
    event: "change"
  }
};
</script>

子组件model中需要定义propevent

  • prop 给参数重命名,新的变量名需要在props中定义
  • event 触发的事件名称

默认情况下,一个组件上的 v-model 会把 value 用作 prop 且把 input 用作 event。

上面的代码还可以这样简化

<template>
  <div class="inputWarp">
    <input
      type="text"
      :value="value"
      @input="$emit('input', $event.target.value)"
    />
    {{ value }}
  </div>
</template>
<script>
export default {
  props: {
    value: {
      type: String || Number
    }
  }
};
</script>

.sync 修饰符

可能有的人不理解.sync有什么用,其实它就是一种子组件改变父组件传过来的prop并让父组件数据更新的一种语法糖

那什么时候使用呢?我们来封装一个自定义弹窗组件BaseAlert.vue

<template>
  <div class="baseAlert">
    <div v-if="show" class="alert">
      <div>
        我是一个弹框
      </div>
      <button @click="close">关闭弹窗</button>
    </div>
  </div>
</template>
<script>
export default {
  props: {
    show: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {};
  },
  methods: {
    close() {
      this.show = false;
    }
  }
};
</script>
<!--样式可忽略-->

在父组件中使用

<template>
  <div class="about">
    <button @click="show = true">打开弹框</button>
    <base-alert :show="show"></base-alert>
  </div>
</template>
<script>
export default {
  data() {
    return {
      show: false
    };
  }
};
</script>

试验一下


这时候我们思考两个问题

  • 为什么可以关闭,但页面为什么会报错?

    因为我们在子组件中让show=false但是show是父组件传过来的,我们直接改变它的值vue会报错

  • 再次点击打开弹窗时,为什么没有反应?

    虽然在子组件中show的状态是false但是在父组件中show的状态还是true

上面的情况可以解决吗? 肯定可以, 我们只需要点击子组件的关闭按钮时通知父组件,让父组件把show的状态变为false即可:如

<!--子组件-->
methods: {
    close() {
      this.$emit('close',false)
    }
  }
<!--父组件-->
 <base-alert :show="show" @close="close"></base-alert>
 
methods: {
    close(status) {
        this.show=status
    }
  }

问题是 父组件为了关闭弹窗这个简单的功能还需要用一个函数close,实在是不太优雅,而.sync就是来解决这样类似的场景

我们现在使用.sync重构代码

<!--父组件-->
<template>
  <div class="about">
    <button @click="show = true">打开弹框</button>
    <base-alert :show.sync="show"></base-alert>  // 在 :show 改为:show.sync
  </div>
</template>
<script>
export default {
  data() {
    return {
      show: false
    };
  },
  methods: {
    close(status) {
      this.show = status;
    }
  }
};
</script>

BaseAlert组件改造

<!--BaseAlert-->
<template>
  <div class="baseAlert">
    <div v-if="show" class="alert">
      <div>
        我是一个弹框
      </div>
      <button @click="close">关闭弹窗</button>
    </div>
  </div>
</template>
<script>
export default {
  props: {
    show: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {};
  },
  methods: {
    close() {
      this.$emit("update:show", false); // 注意把show 改为 update:show
    }
  }
};
</script>



这个时候页面功能正常,且没有报错,完美!

用插槽封装组件

公用逻辑的抽离和组合一直是项目中难题,同样也是框架设计者的难题,在Vue2.0中也提供了抽离公共逻辑的方法Mixins,但Mixins有着明显的缺陷 无法清楚得知数据来源 特别是在一个页面有多个Mixins时,页面维护起来简直是种灾难

上面是尤雨溪在VueConf演讲中提到的有关Mixins问题

上面是尤雨溪在VueConf演讲中提到插槽有关的问题,因为插槽是以组件为载体,所以有额外的组件实例性能消化,但也正是因为以组件为载体,所以也可以封装些样式相关的东西

可以看出在Vue2.0插槽是逻辑复用的最优解决方案,当然在Vue3.0中有更好的解决方案composition-api,现在你应该了解到为什么Vue3.0要出composition-api了,主要解决的问题就是 逻辑的分封装与复用

插槽的分类

  • 匿名插槽 没有名字的插槽
  • 具名插槽 有名字的插槽
  • 作用域插槽 可以通信的插槽
  • 动态插槽 就是动态插槽

下面我们再来改造下上面的BaseAlert.vue组件来学习下各种插槽之间的使用

需求:

  • 可以自定义头部
  • 弹窗中可以展示当前日期

假设我们在父组件中这样使用

    <base-alert :show.sync="show">
      <template v-slot:title>
        重大提示
      </template>
      <template v-slot="{ time }">
        这是一个弹窗
        <div class="time">{{ time }}</div>
      </template>
    </base-alert>

我们定义了一个具名插槽 v-slot:title 和作用域插槽接受子组件的time

BaseAlert.vue封装

<template>
  <div class="baseAlert">
    <div v-if="show" class="alert">
      <div class="header">
        <slot name="title"></slot>
      </div>
      <div class="contents">
        <slot :time="time"> </slot>
      </div>
      <button @click="close">关闭弹窗</button>
    </div>
  </div>
</template>
<script>
export default {
  props: {
    show: {
      type: Boolean,
      default: false
    }
  },
  computed: {
    time() {
      const d = new Date();
      return d.getFullYear() + "-" + (d.getMonth() - 1) + "-" + d.getDate();
    }
  },
  data() {
    return {};
  },
  methods: {
    close() {
      this.$emit("update:show", false);
    }
  }
};
</script>

作用域插槽的使用就是在slot中可以添加自定义属性,在父组件用v-slot接收即可

页面效果如下所示(请忽略样式)

关于匿名插槽动态插槽理解起来比较简单,就不举例子说明了

动态组件

我们可以通过is关键字动态加载组件,同时使用keep-alive可对组件进行缓存,在tab切换场景中使用较多,如:

<!-- 失活的组件将会被缓存!-->
<keep-alive>
  <component v-bind:is="currentTabComponent"></component>
</keep-alive>

keep-alive也可和路由搭配使用可以把项目的整体体验提升一个段,后续写重学vue-Router的时候会讲到

异步组件

当一个组件比较大的时候,为了不影响整个页面的加载速度,我们需要使用异步去加载整个组件,和异步路由写法一样,异步组件使用如下:

new Vue({
  components: {
    'my-component': () => import('./my-async-component')
  }
})

值得注意的是,官方还支持对异步组件加载状态进行配置


但是我在vue-cli测试的时候delay属性一直无效,不知道你们如何配置异步组件加载状态的呢?欢迎留言谈论

组件之间通信

组件之间的通讯一直是vue的高频考点,也是在项目开发中比较重要的一个知识点,下面我就带领大家总结下,vue组件通讯都有哪些,且分别适用于哪些场景

常规通讯方式1(推荐使用)
  • 父组件给子组件传递数据

    父组件调用子组件用 :(v-bind) 绑定数据

    <!--父组件-->
    <item-list :item="data"></item-list>
    

    子组件用 props 接收

    <!--子组件-->
    export default {
        props:{
            item:{
                type:Object
            }
        }
    }
    
  • 子组件给父组件传递数据

    子组件通过触发事件的方式 $emit 给父组件传递数据

    <!--子组件-->
    <template>
      <div>
        我是子组件
        <button @click="btnClick">点击传递数据</button>
      </div>
    </template>
    <script>
    export default {
      methods: {
        btnClick() {
          this.$emit("childFn", "数据");
        }
      }
    };
    </script>
    

    父组件用对应的事件去接收

    <button @click="btnClick"  @childFn="childFn">点击传递数据</button>
    
     <script>
        export default {
          methods: {
              childFn(val) {
                 console.log(val); //数据
                }
          }
        };
    </script>
    
  • 父组件触发子组件方法

    父组件通过ref的方式调用子组件方法

    <button @click="btnClick"  ref="hello">点击传递数据</button>
    
     <script>
        export default {
          methods: {
              btnClick(val) {
                this.$refs['hellow'].子组件方法
                }
          }
        };
    </script>
    
  • 子组件触发父组件方法

    通过$emit触发父组件方法,和上面的 子组件给父组件传递数据 一样

常规通讯方式2(不推荐使用)

在父组件里想拿到子组的实例很简单this.$children 就可以拿到全部子组件的实例,只要拿到组件的实例,那事情都变的简单了

  • 通过实例改变子组件的数据及调用子组件的方法
this.$children['组件名称'].xxx  //改变子组件数据
this.$children['组件名称'].xxx()  // 调用子组件方法

子组件调用父组件的道理也一样,用this.$parent即可

这种父子组件通讯的方式这么简单,为什么不推荐使用呢?刚开始学vue的时候我也有这样的疑问,但是通过做项目的时候发现,这样通讯最要命的弊端就是 数据状态改变不明了, 特别是一个父组件里面有很多子组件,当父组件数据改变时你并不知道是哪个子组件所为, 就和使用mixins所带来的尴尬一样

下面是官方给出的解释

值得注意的是,官方并没给出父子组件隔代通讯及兄弟组件之间通讯的相关API,如果业务里面有这样的需求我们只能用vuex这种第三方状态管理器,但如果我们是封装项目的基础组件,或者自己做个组件库,这个时候并不能依赖vuex,那我们应该怎么样方便快捷的去实现呢?

下面所说的方式推荐在开发独立组件的时候使用,不推荐在项目组件中直接使用

独立组件之间的通信方式
  • provide / inject

    主要用于子组件获取父组件的数据/方法,如:

    <!--父组件-->
    export default {
      name: "Home",
      provide: {
        name: "公众号码不停息" // 传数据
      },
    }
    
    <!--子组件-->
    export default {
      inject: ["name"], // 接受数据
      mounted() {
        console.log(this.name); //公众号码不停息
      }
    };
    

并且provide / inject还支持隔代传递

官方不推荐provide/inject用于普通应用程序代码中,但如果你充分的了解了它的特性,有时候provide/inject在某种应用场景下可以代替vuex
需要满足什么场景呢?

  • 应用不能太复杂,最好是简单的单页面
  • 没有权限判断,因为provide/inject只能用于vue组件,不能用于js文件中,而权限判断一般会写在路由拦截等js文件里面,这时用provide/inject就会显得比较乏力

那我们怎么使用provide/inject 来取代vuex呢?

vuex主要的作用就是管理应用中的数据,那按道说只要我们把provide写在一个应用 最大的父组件 里面,那应用里面所有的组件都可以使用provide所暴露出来的数据/方法了,显然这个 最大的父组件 就是app.vue组件

// app.vue

<script>
export default {
  provide() {
    return {
      app: this   //把整个app实例暴露出去
    };
  },
  data() {
    return {
      userInfo: {
        name: "码不停息",
        age: 18
      }
    };
  },
  methods: {
    getUserInfo() {
      console.log("请求接口");
    }
  }
};
</script>

下面我们在任意一个组件中去拿app.vueuserInfo和调用getUserInfo方法

<script>
import BaseLoading from "@/common/BaseLoading.vue";

export default {
  inject: ["app"], // 把app实例导入
  mounted() {
    console.log(this.app.userInfo);
    this.app.getUserInfo();
  }
};
</script>

可以看到数据可以拿到,方法也调用成功了

使用provide/inject可以满足我们搭建小而美的应用

  • 自定义通讯方式

所谓的自定义,也就是自己封装一个通用的函数,来实现复杂情况下的数据传递,原理就是根据组件的name去遍历查找自己需要的组件,下面我们先弄清楚这个方法应给具备怎样的功能

向上找到最近的指定组件
向上找到所有的指定组件
向下找到最近的指定组件
向下找到所有指定的组件
找到指定组件的兄弟组件

代码如下:

// 由一个组件,向上找到最近的指定组件
function findComponentUpward(context, componentName) {
  let parent = context.$parent;
  let name = parent.$options.name;

  while (parent && (!name || [componentName].indexOf(name) < 0)) {
    parent = parent.$parent;
    if (parent) name = parent.$options.name;
  }
  return parent;
}
// 由一个组件,向上找到所有的指定组件
function findComponentsUpward(context, componentName) {
  let parents = [];
  const parent = context.$parent;

  if (parent) {
    if (parent.$options.name === componentName) parents.push(parent);
    return parents.concat(findComponentsUpward(parent, componentName));
  } else {
    return [];
  }
}
// 由一个组件,向下找到最近的指定组件
function findComponentDownward(context, componentName) {
  const childrens = context.$children;
  let children = null;

  if (childrens.length) {
    for (const child of childrens) {
      const name = child.$options.name;

      if (name === componentName) {
        children = child;
        break;
      } else {
        children = findComponentDownward(child, componentName);
        if (children) break;
      }
    }
  }
  return children;
}
// 由一个组件,向下找到所有指定的组件
function findComponentsDownward(context, componentName) {
  return context.$children.reduce((components, child) => {
    if (child.$options.name === componentName) components.push(child);
    const foundChilds = findComponentsDownward(child, componentName);
    return components.concat(foundChilds);
  }, []);
}
// 由一个组件,找到指定组件的兄弟组件
function findBrothersComponents(context, componentName, exceptMe = true) {
  let res = context.$parent.$children.filter(item => {
    return item.$options.name === componentName;
  });
  let index = res.findIndex(item => item._uid === context._uid);
  if (exceptMe) res.splice(index, 1);
  return res;
}
export {
  findComponentUpward,
  findComponentsUpward,
  findComponentDownward,
  findComponentsDownward,
  findBrothersComponents
};

此代码copy Aresn大神(iView 作者) 写的 Vue.js组件精讲小册,不是打广告,看完这个小册你的vue水平可以提升一个段

下面我们简单的使用下其中的一个方法findComponentUpward向上查找最近的父组件

<script>
import { findComponentUpward } from "@/units/findComponents";
export default {
  methods: {
    btnClick() {
      console.log(this.$parent);
      this.$parent.aaaaa();
    },
    ceshi() {}
  },
  mounted() {
    console.log(findComponentUpward(this, "Home"));
  }
};
</script>

可以看出父组件实例已经打印出来,有了父组件实例就可以为所欲为啦

注意:使用该方法时组件必须有name属性

总结

本文主要从官方文档出发,梳理了vue比较实用且容易被忽略的知识点,不是大神,如有错误请多多指教,该篇是__从文档开始重学vue__的上册,下册将带领大家回顾vue一些比较重要的api

希望大家持续关注哦!😮

最后

交个朋友吧

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值