0%

Vue2.x进阶

前言

前面跟着官方文档学习了基础部分

下面深入学习Vue进阶内容

深入了解组件

组件注册

前面说到组件注册有两种:全局注册和局部注册,下面深入了解

组件名

  1. 组件名大小写
  • 全部使用kebab-case
Vue.component('my-component-name', { /* ... */ })

使用必须 <my-component-name>

  • 使用pascalCase
Vue.component('MyComponentName', { /* ... */ })

引用时两种方式都可以 <my-component-name><MyComponentName>

注意:DOM中(非模板)只有前者可以!!!

全局注册

前面基础篇中提到,全局注册可以在后面的任何Vue根实例中使用,所有子组件,各自组件内部都可以使用

Vue.component('component-a', { /* ... */ })
Vue.component('component-b', { /* ... */ })
Vue.component('component-c', { /* ... */ })

new Vue({ el: '#app' })
<div id="app">
  <component-a></component-a>
  <component-b></component-b>
  <component-c></component-c>
</div>

局部注册

大家可以发现,不管你是否使用全局注册的组件,他都会在你写的组件作用域中,webpack打包时会增加无意义的内容

这个时候,局部组件,通过Vue的componentsproperty来声明根实例需要使用到的组件。

对应组件通过js对象的形式声明

var ComponentA = { /* ... */ }
var ComponentB = { /* ... */ }
var ComponentC = { /* ... */ }
new Vue({
  el: '#app',
  components: {
    'component-a': ComponentA,
    'component-b': ComponentB
  }
})
子组件间不能相互调用

如果希望在ComponentA中使用ComponentB

var ComponentA = { /* ... */ }

var ComponentB = {
  components: {
    'component-a': ComponentA
  },
  // ...
}

或者webpack文件管理

import ComponentA from './ComponentA.vue'

export default {
  components: {
    ComponentA
  },
  // ...
}

模块系统

在模块系统中局部注册

推荐创建一个component目录,将每个组件放置在各自的文件中。

使用时就ES model 的方式 import相应模块

示例

import ComponentA from './ComponentA'
import ComponentC from './ComponentC'

export default {
  components: {
    ComponentA,
    ComponentC
  },
  // ...
}

现在 ComponentAComponentC 都可以在 ComponentB 的模板中使用了。

基础组件的自动化全局部署

对于一些相对通用的组件称为基础组件建议设置为全局组件

使用webpack或(Vue CLI3+)可以直接使用require.context 注册非常常用的基础组件

示例:应用在入口文件src/main.js

import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'

const requireComponent = require.context(
  // 其组件目录的相对路径
  './components',
  // 是否查询其子目录
  false,
  // 匹配基础组件文件名的正则表达式
  /Base[A-Z]\w+\.(vue|js)$/
)

requireComponent.keys().forEach(fileName => {
  // 获取组件配置
  const componentConfig = requireComponent(fileName)

  // 获取组件的 PascalCase 命名
  const componentName = upperFirst(
    camelCase(
      // 获取和目录深度无关的文件名
      fileName
        .split('/')
        .pop()
        .replace(/\.\w+$/, '')
    )
  )

  // 全局注册组件
  Vue.component(
    componentName,
    // 如果这个组件选项是通过 `export default` 导出的,
    // 那么就会优先使用 `.default`,
    // 否则回退到使用模块的根。
    componentConfig.default || componentConfig
  )
})

全局注册的行为必须在根 Vue 实例 (通过 new Vue) 创建之前发生

解析

require.context(“相对路径”, boolean, /正则/)

  • 传入三个参数,返回一个函数

返回的函数有三个属性

  • resolve {function}-接受一个参数request,request为”相对路径“下面匹配文件的相对路径,返回这个匹配文件相对整个工程的相对路径
  • keys{function}–返回匹配成功模块的名字组成的数据
  • id{string}–执行环境的id

示例

可以看出来

  • files(key) 自身作为函数,参数为匹配文件的相对路径,返回对应模块

  • id:就是返回了匹配的文件夹的相对于工程的相对路径,是否遍历子目录,匹配正则组成的字符串

  • keys():返回匹配的文件名数组

  • resolve():传参类似files,返回完整路径

prop

prop大小写

HTML中attribute 是大小写不敏感的

Vue.component('blog-post', {
  // 在 JavaScript 中是 camelCase 的
  props: ['postTitle'],
  template: '<h3>{{ postTitle }}</h3>'
})

camelCase 的 prop 名需要使用其等价的 kebab-case 命名

<!-- 在 HTML 中是 kebab-case 的 -->
<blog-post post-title="hello!"></blog-post>
  • 在字符串模板中没有限制

prop类型

props: {
  title: String,
  likes: Number,
  isPublished: Boolean,
  commentIds: Array,
  author: Object,
  callback: Function,
  contactsPromise: Promise // or any other constructor
}

当然验证信息也可以是需求的对象

Vue.component('my-component', {
  props: {
    // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
    propA: Number,
    // 多个可能的类型
    propB: [String, Number],
    // 必填的字符串
    propC: {
      type: String,
      required: true
    },
    // 带有默认值的数字
    propD: {
      type: Number,
      default: 100
    },
    // 带有默认值的对象
    propE: {
      type: Object,
      // 对象或数组默认值必须从一个工厂函数获取
      default: function () {
        return { message: 'hello' }
      }
    },
    // 自定义验证函数
    propF: {
      validator: function (value) {
        // 这个值必须匹配下列字符串中的一个
        return ['success', 'warning', 'danger'].indexOf(value) !== -1
      }
    }
  }
})

注意

  • 默认值为对象需要从一个工厂函数返回
  • 自定义验证函数,返回值boolean
  • 注意那些 prop 会在一个组件实例创建之前进行验证,所以实例的 property (如 datacomputed 等) 在 defaultvalidator 函数中是不可用的。

类型检查

type 可以是下列原生构造函数中的一个:

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol

也可以是自定义构造函数,并且通过原型链instanceof 查找方式确认

例如

function Person (firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
}

Vue.component('blog-post', {
  props: {
    author: Person
  }
})

来验证author prop值是否通过new Person创建

传递静态或动态prop

传递字符串

  • 静态
<blog-post title="My journey with Vue"></blog-post>
  • 动态
<!-- 动态赋予一个变量的值 -->
<blog-post v-bind:title="post.title"></blog-post>

<!-- 动态赋予一个复杂表达式的值 -->
<blog-post
  v-bind:title="post.title + ' by ' + post.author.name"
></blog-post>

传递数字

<!-- 即便 `42` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:likes="42"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:likes="post.likes"></blog-post>

注意:静态属性默认是string类型

传递boolean

<!-- 包含该 prop 没有值的情况在内,都意味着 `true`。-->
<blog-post is-published></blog-post>

<!-- 即便 `false` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:is-published="false"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:is-published="post.isPublished"></blog-post>

注意:静态属性有两种方式

传递数组

<!-- 即便数组是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:comment-ids="[234, 266, 273]"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:comment-ids="post.commentIds"></blog-post>

同样注意静态形式

传递对象

类似数组

单向数据流

最好不要在子组件中尝试更改prop

  1. prop用来传递一个初始值;子组件想把它作为本地数据使用—最好定义一个本地data
props: ['initialCounter'],
data: function () {
  return {
    counter: this.initialCounter
  }
}
  1. prop作为原始值传入但需要进行转换–使用计算属性
props: ['size'],
computed: {
  normalizedSize: function () {
    return this.size.trim().toLowerCase()
  }
}

Vue在数据管理上一定要清楚,子组件最好只是使用数据,UI组件;数据变更最好在根实例中实现

非Prop的Attribute

当prop传递给一个组件,但是这个组件没有在``props` 中定义。常见于组件库中

当然组件可以接受任意的attribute,这些attribute会被添加到这个组件的根元素上

替换/合并已有Attribute

实例

<bootstrap-date-input> 的模板是这样的:

<input type="date" class="form-control">

这时我们想给我们的日期选择器插件定值一个主题

<bootstrap-date-input
  data-date-picker="activated"
  class="date-picker-theme-dark"
></bootstrap-date-input>

这时有两个class attribute,,,

对于绝大多数来说是替换,外来的(date-picker-theme-dark),替换掉(form-control

如果传入 type="text" 就会替换掉 type="date"

但是:对于style和class会智能合并

最终的值:class="form-control date-picker-theme-dark"

禁用Attribute继承

如果你不想组件根元素继承attribute,设置inheritAttrs: false

Vue.component('my-component', {
  inheritAttrs: false,
  // ...
})

配合实例的 $attr property 使用,它包含了传递给组件的attribute 名和 attribute 值,例如:

{
  required: true,
  placeholder: 'Enter your username'
}

有了 inheritAttrs: false$attrs,你就可以手动决定这些 attribute 会被赋予哪个元素

Vue.component('base-input', {
  inheritAttrs: false,
  props: ['label', 'value'],
  template: `
    <label>
      {{ label }}
      <input
        v-bind="$attrs"
        v-bind:value="value"
        v-on:input="$emit('input', $event.target.value)"
      >
    </label>
  `
})
<base-input
  v-model="username"
  required
  placeholder="Enter your username"
></base-input>

注意

  • v-bind=”$attrs” 定向绑定

  • 双向绑定复习v-model

自定义事件

事件名

不同于组件和prop,事件名不存在大小写自动转换

触发事件名字===监听绑定事件名

this.$emit('myEvent')
<!-- 没有效果 -->
<my-component v-on:my-event="doSomething"></my-component>

还注意到v-on事件监听在DOM模板中—大小写转换

v-on:myEvent 将会变成 v-on:myevent——导致 myEvent 不可能被监听到。

推荐使用kebab-case 的事件名

自定义组件v-model

复习

组件上的v-model默认利用valueinput事件,使用model选项可以避免一些像单选(),复选()等输入控件value使用不同目的的冲突

Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
})
  • model:
  • 声明prop名字(并且需要在props中声明)
  • 声明事件名(触发事件名一致)
  • input元素中表明
  • type="checkbox"
  • v-bind:checked="checked"
  • v-on:change="$emit('change', $event.target.checked)"

示例

<base-checkbox v-model="lovingVue"></base-checkbox>

这里lovingVue值会传入checked

input change事件触发–>触发change,并返回值—>返回值更新lovingVue

将原生事件绑定到组件

可以使用 v-on.native 修饰符:

<base-input v-on:focus.native="onFocus"></base-input>

需要考虑一个前面prop一样的问题,根元素上绑定事件,,,如果你想绑定input事件,但根元素不是input元素,子元素才是。。。

想到定向绑定事件!

同样先禁止attribute继承

Vue提供 $listeners property,它是一个对象,里面包含了作用在这个组件上的所有监听器

{
  focus: function (event) { /* ... */ }
  input: function (value) { /* ... */ },
}

示例:配合v-model

创建一个计算属性融合一下更加方便

Vue.component('base-input', {
  inheritAttrs: false,
  props: ['label', 'value'],
  computed: {
    inputListeners: function () {
      var vm = this
      // `Object.assign` 将所有的对象合并为一个新对象
      return Object.assign({},
        // 我们从父级添加所有的监听器
        this.$listeners,
        // 然后我们添加自定义监听器,
        // 或覆写一些监听器的行为
        {
          // 这里确保组件配合 `v-model` 的工作
          input: function (event) {
            vm.$emit('input', event.target.value)
          }
        }
      )
    }
  },
  template: `
    <label>
      {{ label }}
      <input
        v-bind="$attrs"
        v-bind:value="value"
        v-on="inputListeners"
      >
    </label>
  `
})

现在<base-input> 组件是一个完全透明的包裹器了,也就是说它可以完全像一个普通的 <input> 元素一样使用了:所有跟它相同的 attribute 和监听器都可以工作,不必再使用 .native 监听器。

.sync 修饰符

2.3以后变成一个编译时的语法糖,它会被扩展为自动更新父组件属性的v-on 监听器

<comp :foo.sync="bar"></comp>

扩展为:

<comp :foo="bar" @update:foo="bar=$event"></comp>

当子组件要更新foo值时,需要显示触发一个更新事件

this.$emit('update:foo', newValue)

注意

  • .sync后面只能是要绑定的property名,而不能是表达式

示例:弹窗关闭

插槽

Vue实现的内容分配的api,<slot>承载分发内容的出口;类似react中prop.children

插槽内容

  1. 字符串
  2. HTML等模板代码
  3. 甚至其他组件
<navigation-link url="/profile">
  <!-- 添加一个图标的组件 -->
  <font-awesome-icon name="user"></font-awesome-icon>
  Your Profile
</navigation-link>

注意:

如果<navigation-link>template没有包含一个 <slot> 元素,则内容都会被抛弃

编译作用域

父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。

<navigation-link url="/profile">
  Clicking here will send you to: {{ url }}
  <!--这里的 `url` 会是 undefined-->
</navigation-link>
  • 当然可以使用props在组件内部接收url这个property

后备内容

也就是slot插槽的默认内容

<button type="submit">
  <slot>Submit</slot>
</button>

在父级组件中使用 <submit-button> 并且不提供任何插槽内容时:

<button type="submit">
  Submit
</button>

将渲染后备内容

当然提供内容时,覆盖后备内容

具名插槽

废弃了slot属性和slot-scope属性,代替的是v-slot

示例

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>
  • 不带name的会有隐含name="default"
  • 同样,任何没有被带有v-slottemplate包裹的内容都视为默认插槽内容
<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

渲染为:

<div class="container">
  <header>
    <h1>Here might be a page title</h1>
  </header>
  <main>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </main>
  <footer>
    <p>Here's some contact info</p>
  </footer>
</div>

注意v-slot一般只能添加到<template>上,一个例外下节分析

缩写

把参数之前的所有内容 (v-slot:) 替换为字符 #。例如 v-slot:header 可以被重写为 #header

<base-layout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

注意 你希望使用缩写的话,你必须始终以明确插槽名取而代之:

<current-user #default="{ user }">
  {{ user.firstName }}
</current-user>

作用域插槽

当我们想让插槽内容可以访问子组件中才有的数据。

例如有如下模板的 <current-user> 组件:

<span>
  <slot>{{ user.lastName }}</slot>
</span>

想换掉备用内容

<current-user>
  {{ user.firstName }}
</current-user>

但是会报错,因为user是子作用域中的东西,<current-user>组件内容是父组件作用域

引入插槽prop:在<slot>元素上绑定user属性;并且在v-slot属性赋值内容全局prop名

<span>
  <slot v-bind:user="user">
    {{ user.lastName }}
  </slot>
</span>
<current-user>
  <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </template>
</current-user>

独占默认插槽的缩写语法

当提供的内容只有默认插槽时,组件的标签可以当做插槽模板使用;**v-slot 直接用在组件上**

<current-user v-slot:default="slotProps">
  {{ slotProps.user.firstName }}
</current-user>

default可以省略

<current-user v-slot="slotProps">
  {{ slotProps.user.firstName }}
</current-user>

当然缩写语法不可以和具名插槽混用,同时出现需要使用完整的基于 <template> 的语法:

<current-user>
  <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </template>

  <template v-slot:other="otherSlotProps">
    ...
  </template>
</current-user>

解构插槽prop

作用域插槽内部工作原理是将插槽内容,包裹在一个拥有单一参数的函数里

function (slotProps) {
  // 插槽内容
}

表示prop名可以是js表达式,可以使用对象结构的语法

<current-user v-slot="{ user }">
  {{ user.firstName }}
</current-user>

重命名

<current-user v-slot="{ user: person }">
  {{ person.firstName }}
</current-user>

自定义prop的后备内容

<current-user v-slot="{ user = { firstName: 'Guest' } }">
  {{ user.firstName }}
</current-user>

动态插槽名

动态指令参数可以用在 v-slot 上,来定义动态的插槽名:

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>
</base-layout>

动态组件&异步组件

动态组件上使用keep-alive

前面我们使用is 属性来切换不同的组件

<component v-bind:is="currentTabComponent"></component>

当然切换组件后,不能保存原来组件的状态,会触发重复渲染

给动态标签添加keep-alive实现状态缓存

示例:多标签界面

  1. 视图结构
  • tab-button
  • border上的细节注意
  • cursor,hover,active细节注意
  • tab大盒子
  • flex布局
  • 左sidebar
    • 单行文本
    • max-width设置
    • cursor,hover,active细节
  • 右container
    • <h>
    • <p>:复习v-html技巧
  1. 逻辑层
  • 根实例
  • 数据
    • currentTab:用于active和动态组件切换
    • tabs:用于循环渲染
    • 计算属性:动态组件切换
  • 子组件
    • tab-posts
    • tab-archive
  • tab-posts
  • 数据
    • posts数组
      • id:key
      • title
      • content:<p>:v-html
    • selectedPost:active,右container显示
  • template:注意单一原则
  • tab-archive
  • template

注意

  • ul li使用时
  1. 一定要样式中margin,padding自定义
  2. list-style-type: none;取消::marker
  • 单行文字css实现
>white-space: nowrap;
>text-overflow: ellipsis;
>overflow: hidden;
  • h标签上内外边距丑
>.selected-post > :first-child {
 margin-top: 0;
 padding-top: 0;
>}

异步组件

Vue允许以一个工厂函数的方式定义组件,工厂函数只有当组件需要被渲染的时候才会触发,并且把结果缓存供未来重渲染

例子

Vue.component('async-example', function (resolve, reject) {
  setTimeout(function () {
    // 向 `resolve` 回调传递组件定义
    resolve({
      template: '<div>I am async!</div>'
    })
  }, 1000)
})

上面示例,工厂函数调用,会收到resolve回调,这个回调函数从服务器得到组件定义(这里用setTimeout演示)

  • 将异步组件和webpack code-splitting结合
Vue.component('async-webpack-example', function (resolve) {
  // 这个特殊的 `require` 语法将会告诉 webpack
  // 自动将你的构建代码切割成多个包,这些包
  // 会通过 Ajax 请求加载
  require(['./my-async-component'], resolve)
})

require

  • 也可以返回一个Promise
Vue.component(
  'async-webpack-example',
  // 这个动态导入会返回一个 `Promise` 对象。
  () => import('./my-async-component')
)

局部组件也可以直接提供一个返回Promise的函数

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

import动态导入

处理加载状态

异步组件工厂函数也可以返回一个如下格式的对象

const AsyncComponent = () => ({
  // 需要加载的组件 (应该是一个 `Promise` 对象)
  component: import('./MyComponent.vue'),
  // 异步组件加载时使用的组件
  loading: LoadingComponent,
  // 加载失败时使用的组件
  error: ErrorComponent,
  // 展示加载时组件的延时时间。默认值是 200 (毫秒)
  delay: 200,
  // 如果提供了超时时间且组件加载也超时了,
  // 则使用加载失败时使用的组件。默认值是:`Infinity`
  timeout: 3000
})

处理边界问题

访问元素&组件

大多数情况,最好不要触达另一个组件实例内部或手动操作DOM元素。

访问根实例

每一个new Vue实例的子组件,都可以通过$root 来访问根实例

所有子组件都可以将这个实例作为全局store访问使用

// Vue 根实例
new Vue({
  data: {
    foo: 1
  },
  computed: {
    bar: function () { /* ... */ }
  },
  methods: {
    baz: function () { /* ... */ }
  }
})

// 获取根组件的数据
this.$root.foo

// 写入根组件的数据
this.$root.foo = 2

// 访问根组件的计算属性
this.$root.bar

// 调用根组件的方法
this.$root.baz()

注意

对于一些小应用,可以这行管理数据

大型应用:Vuex管理

访问父级组件实例

$root类似,$parent 可以用来从子组件中访问父组件的实例