给自己看的VUE3.0技巧

1,166 阅读5分钟

1. v-model

(1) 多重v-model绑定

<user-name
  v-model:first-name="firstName"
  v-model:last-name="lastName"
></user-name>
const app = Vue.createApp({})

app.component('user-name', {
  props: {
    firstName: String,
    lastName: String
  },
  template: `
    <input 
      type="text"
      :value="firstName"
      @input="$emit('update:firstName', $event.target.value)">

    <input
      type="text"
      :value="lastName"
      @input="$emit('update:lastName', $event.target.value)">
  `
})

(2) 自定义修饰符

我们可以创建一个示例自定义修饰符,.capitalizev-model绑定提供的字符串的首字母大写。

添加到组件的修饰符v-model将通过modelModifiersprop 提供给组件。在下面的示例中,我们创建了一个包含modelModifiers默认值为空对象的prop 的组件。

当组件的created生命周期挂钩触发时,modelModifiersprop包含capitalize且其值为true,,因为它是在v-modelbinding 上设置的v-model.capitalize,如果不添加.capitalizemodelModifiers为默认的空对象

<modelComponent v-model.capitalize="modelValue"></modelComponent>
app.component('my-component', {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  methods: {
    emitValue(e) {
      let value = e.target.value
      if (this.modelModifiers.capitalize) {
        value = value.charAt(0).toUpperCase() + value.slice(1)
      }
      this.$emit('update:modelValue', value)
    }
  },
  template: `<input
    type="text"
    :value="modelValue"
    @input="emitValue">`
})

2. 非prop属性继承

在默认情况下属性(如class id)将自动继承到根节点的属性中,例如:

app.component('date-picker', {
  template: `
    <div class="date-picker">
      <input type="datetime" />
    </div>
  `
})
<!-- Date-picker component with a non-prop attribute -->
<date-picker data-status="activated"></date-picker>

<!-- Rendered date-picker component -->
<div class="date-picker" data-status="activated">
  <!-- Rendered date-picker component -->
<div class="date-picker" data-status="activated">
  <input type="datetime" />
</div>

如果你想属性继承到别的节点而不是根节点,则可以inheritAttrs: false在组件的选项中进行设置。

app.component('date-picker', {
  inheritAttrs: false,
  template: `
    <div class="date-picker">
      <input type="datetime" v-bind="$attrs" />
    </div>
  `
})

3. 插槽

(1) Scoped Slots

有时我们需要在父组件插槽中获取到子组件内容数据。例如,我们有一个组件,其中包含待办事项列表。

app.component('todo-list', {
  data() {
    return {
      items: ['Feed a cat', 'Buy milk']
    }
  },
  template: `
    <ul>
      <li v-for="(item, index) in items">
        {{ item }}
      </li>
    </ul>
  `
})

我们可能要在父组件用插槽来替换默认内容来对其内容的自定义:

<todo-list>
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}</span>
</todo-list>

但是,这是行不通的,因为只有<todo-list>组件可以访问item,我们在其父级提供slot内容是获取不到的。

为了使item在父级提供的slot可用,我们可以添加一个元素并将其绑定为属性:

<ul>
  <li v-for="( item, index ) in items">
    <slot v-bind:item="item"></slot>
  </li>
</ul>

父组件:

<todo-list>
  <template v-slot:default="slotProps">
    <i class="fas fa-check"></i>
    <span class="green">{{ slotProps.item }}</span>
  </template>
</todo-list>

(2) 简写以及接受JavaScript表达式

如果是默认插槽则可以简写,并且因为作用域插槽是通过将插槽内容包装在传递了单个参数的函数中来工作,所以v-slot可以接受可以出现在函数定义的参数位置的任何有效JavaScript表达式:

<todo-list v-slot="{ item = 'Placeholder' }">
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}</span>
</todo-list>

(3)动态插槽

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

(4) 语法糖

可以用#来替代v-slot,例如v-slot:header可以改写为#header:

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

  <template #default="{ item }">
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>

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

4. provide/inject

(1) 默认使用下为非响应数据传递

父组件:

<template>
  <modelComponent></modelComponent>
</template>
import modelComponent from '../components/model-component.vue'
export default {
  name: 'home',
  provide: {
    user: 'John Doe'
  }
}

子组件:

<template>
  获得的user为{{user}}
</template>
export default {
  name: 'model-component',
  inject: ['user'],
}

如果要提供一些组件实例属性,则需要这么写:

import modelComponent from '../components/model-component.vue'
export default {
  name: 'home',
  data() {
    return {
      todos: ['Feed a cat', 'Buy tickets']
    }
  },
  provide() {
    return {
      todoLength: this.todos.length
    }
  }
}

子组件:

<template>
  todos的长度为{{todoLength}}
</template>
export default {
  name: 'model-component',
  inject: ['todoLength'],
}

(2) 使传递的数据变为响应式

父组件:

<template>
  <h3>改变provide的值</h3>
  <input type="text" v-model="provideVal" />
  <modelComponent></modelComponent>
  <h3>改变user的值 user的值为{{user}}</h3>
  <button @click="user.push('3')">添加user数量</button>
  <proComponent></proComponent>
</template>
import { provide,ref,reactive } from 'vue'
import modelComponent from '../components/model-component.vue'
import proComponent from '../components/provide-componet.vue'

export default {
  name: 'home',
  components: {
    modelComponent,
    proComponent
  },
  setup() { // setup函数被执行时,该组件实例尚未建立,在created之前。所以能访问的属性只有 (props, { attrs, slots, emit })
    const provideVal = ref(''); // ref使独立的原始类型变为响应式,变成通过引用来进行传递数值。(但经过试验也可使object,array变为响应式,内部变化也会响应)
    const user = reactive(['2']); // reactive将对象、数组等变为响应式,是“深度”转换,它会影响所有嵌套属性
    provide('provideVal', provideVal);
    provide('user', user);
    return {
      provideVal,
      user
    }
  }
}

子组件:

modelComponent:

<template>
  <h2>provide</h2>
  <h4>provide的值为{{provideVal}}</h4>
  <input type="text" v-model="provideVal">
</template>
import { inject } from 'vue'
export default {
  name: 'model-component',
  setup() {
    const provideVal = inject('provideVal');
    return {
      provideVal
    }
  }
}

proComponent:

<template>
  <h2>子组件内user的值为{{user}}</h2>
</template>
import { inject } from 'vue'
export default {
  name: 'provide-component',
  setup() {
    const user = inject('user');
    return {
      user
    }
  }
}

5. 异步加载组件

Vue提供了一种defineAsyncComponent方法可以使我们能够异步加载组件。

(1)全局注册

import { createApp,defineAsyncComponent } from 'vue'
import App from './App.vue'

const modelComponent = defineAsyncComponent(() =>
  import('./components/model-component.vue')
);

const app = createApp(App);
app.component('modelComponent', modelComponent);
app.mount('#app');

(2)局部注册组件

import { createApp, defineAsyncComponent } from 'vue'

createApp({
  // ...
  components: {
    modelComponent: defineAsyncComponent(() =>
      import('./components/modelComponent.vue')
    )
  }
})

(3)可选变量

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent({
  loader: () => import('./Foo.vue')
  // 组件加载时显示的公用加载组件
  loadingComponent: LoadingComponent,
  // 组件加载失败时显示的公用加载失败组件
  errorComponent: ErrorComponent,
  // 显示公用加载组件之前的延迟. Default: 200ms.
  delay: 200,
  // 加载超时显示加载错误组件. Default: Infinity.
  timeout: 3000,
  // 定义是否使用父组件的<Suspense>. Default: true.
  suspensible: false,
  /**
   *
   * @param {*} error Error message object
   * @param {*} retry 重试函数
   * @param {*} fail  加载错误结束
   * @param {*} attempts 重试的次数
   */
  onError(error, retry, fail, attempts) {
    if (error.message.match(/fetch/) && attempts <= 3) {
      // retry on fetch errors, 3 max attempts
      retry()
    } else {
      // Note that retry/fail are like resolve/reject of a promise:
      // one of them must be called for the error handling to continue.
      fail()
    }
  },
})

<Suspense>组件用于在等待某个异步组件解析时显示后备内容,在父组件中进行设置。用法如下:

<template> 
  <div v-if="errMsg"> {{ errMsg }} </div> 
  <Suspense v-else> 
    <template #default> 
      <articleInfo/> // 需要异步加载的组件
    </template> 
    <template #fallback> 
      <div>正在拼了命的加载…</div> // loading状态
    </template> 
  </Suspense> 
</template> 
<script> 
import { onErrorCaptured,defineAsyncComponent } from 'vue'
export default {
  name: 'home',
  components: {
    articleInfo: defineAsyncComponent(() =>
      import('./components/articleInfo.vue')
    )
  },
  setup() {
    setup () { 
    const errMsg = ref(null) 
    onErrorCaptured(e => { // 捕获加载组件错误信息
      errMsg = '呃,出了点问题!' 
      return true 
    })} 
    return { errMsg }
  }
} 
</script> 

6. transition组件

(1) 指定过渡时间

<transition :duration="1000">...</transition>
// 或
<transition :duration="{ enter: 500, leave: 800 }">...</transition>

(2) 定义JavaScript挂钩

<transition
  @before-enter="beforeEnter"
  @enter="enter"
  @after-enter="afterEnter"
  @enter-cancelled="enterCancelled"
  @before-leave="beforeLeave"
  @leave="leave"
  @after-leave="afterLeave"
  @leave-cancelled="leaveCancelled"
  :css="false" <!-- 添加:css="false"是为了跳过CSS检测 -->
>
  <!-- ... -->
</transition>
// ...
methods: {
  // --------
  // ENTERING
  // --------

  beforeEnter(el) {
    // ...
  },
  // 回调函数done是必须的,否则挂钩函数会被同步调用并且过渡会立刻完成
  enter(el, done) {
    // ...
    done()
  },
  afterEnter(el) {
    // ...
  },
  enterCancelled(el) {
    // ...
  },

  // --------
  // LEAVING
  // --------

  beforeLeave(el) {
    // ...
  },
  // 回调函数done是必须的,否则挂钩函数会被同步调用并且过渡会立刻完成
  leave(el, done) {
    // ...
    done()
  },
  afterLeave(el) {
    // ...
  },
  // leaveCancelled 只能用于 v-show
  leaveCancelled(el) {
    // ...
  }
}

(3)初始渲染使用过渡效果

<transition appear>
  <!-- ... -->
</transition>

(4)过渡模式

transition可选择额外的两种过渡模式:

  • default:新旧元素同时进行过渡。

  • in-out:先完成新元素移入的过渡,后进行旧元素移出的过渡。

  • out-in:当前旧元素先进行移出的过渡,再进行新元素移入的过渡。

这些模式通常用于两个元素的互相切换。例如:

<transition name="mode-fade" mode="out-in">
  <button v-if="on" key="on" @click="on = false">
    on
  </button>
  <button v-else key="off" @click="on = true">
    off
  </button>
</transition>
#demo {
  position: relative;
}

button {
  position: absolute;
}

.mode-fade-enter-active, .mode-fade-leave-active {
  transition: opacity .5s ease
}

.mode-fade-enter-from, .mode-fade-leave-to {
  opacity: 0
}

button {
  background: #05ae7f;
  border-radius: 4px;
  display: inline-block;
  border: none;
  padding: 0.5rem 0.75rem;
  text-decoration: none;
  color: #ffffff;
  font-family: sans-serif;
  font-size: 1rem;
  cursor: pointer;
  text-align: center;
  -webkit-appearance: none;
  -moz-appearance: none;
}

7. Mixins

Mixins可以使Vue任何组件选项都成为可重复使用的。当组件使用mixins,会将选项混合入组件自己的选项中,如果有重复的选项,则以组件自身的选项优先,具有相同挂钩函数时,会将函数内容合并到一起,Mixin挂钩将在组件自己的挂钩之前被调用。 例子如下:

const myMixin = {
  data() {
    return {
      message: 'hello',
      foo: 'abc'
    }
  },
  created() {
    console.log('mixin hook called')
  }
}

const app = Vue.createApp({
  mixins: [myMixin],
  data() {
    return {
      message: 'goodbye',
      bar: 'def'
    }
  },
  created() {
    console.log(this.$data)
  }
})
// => "mixin hook called"
// => { message: "goodbye", foo: "abc", bar: "def" }

8. teleport

teleport组件可以使我们将目标组件渲染在指定位置。

app.component('modal-button', {
  template: `
    <button @click="modalOpen = true">
        Open full screen modal! (With teleport!)
    </button>

    <teleport to="body">
      <div v-if="modalOpen" class="modal">
        <div>
          I'm a teleported modal! 
          (My parent is "body")
          <button @click="modalOpen = false">
            Close
          </button>
        </div>
      </div>
    </teleport>
  `,
  data() {
    return { 
      modalOpen: false
    }
  }
})

modal组件会准确的渲染为body的子代。