vue3 基础概念 (含与 vue2.6 的对比)

vue3 特点

  1. vue3 支持 vue2 的大多数特性
  2. 性能提升:
    打包大小减少41%
    初次渲染快55%,更新快133%
    内存使用减少54%
  3. Composition API
    ref 和 reactive
    computed 和 watch
    新的生命周期函数
    自定义函数——Hooks 函数
  4. 其他新增特性
    Teleport——瞬移组建的位置
    Suspense——异步加载组件的新福音
    全局 API 的修改和优化
    更多的试验性特性
  5. 更好的 Typescript 支持

为什么要有 vue3

  1. vue2 遇到的难题:同一逻辑分类的代码分散,不利于维护。
  2. mixins 难点:命名冲突、不清楚暴露出来变量的作用、组件复用时会遇到问题
  3. vue2 对于 typescript 的支持非常有限

应用和组件

  • 创建应用:

    • vue3:
    const app = Vue.createApp({})
    app.component('SearchInput', SearchInputComponent)
    app.directive('focus', FocusDirective)
    app.use(LocalePlugin)
    app.mount('#app')
    
    // 或
    Vue.createApp({})
     .component('SearchInput', SearchInputComponent)
     .directive('focus', FocusDirective)
     .use(LocalePlugin)
     .mount('#app')
    
    • vue2:
    new Vue({
      el: '#app',
      data: obj
    })
    
  • 组件是可复用的 Vue 实例,且带有一个名字,如 <button-counter>。我们可以在一个通过 new Vue 创建的 Vue 根实例中,把这个组件作为自定义元素来使用。

生命周期钩子函数

vue3 Lifecycle Hooks
vue2 Lifecycle Hooks
// 主要区别在于销毁时
beforeCreate() { console.log('实例刚刚被创建') },
created() { console.log('实例创建完成') },
beforeMount() { console.log('实例挂载之前') },
// 请求数据,操作dom , 放在这个里面
mounted() { console.log('实例挂载完成') },
// 数据更新时,虚拟 DOM 变化之前调用,这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器。
beforeUpdate() { console.log('数据更新之前') },
// 数据更新和虚拟 DOM 变化之后调用。请不要在此函数中更改状态,否则会触发死循环。
updated() { console.log('数据更新完毕') },

// vue3:
// 实例销毁之前调用,在这一步,实例仍然完全可用。一般在这里移除事件监听器、定时器等,避免内存泄漏
beforeUnmount() { console.log('实例销毁之前') },
// Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑,所有的事件监听器会被移除,所有的子实例也会被销毁。
unmounted() { console.log('实例销毁完成') },

// vue2:
beforeDestroy() { console.log('实例销毁之前') },
destroyed() { console.log('实例销毁完成') },

不要在选项 property 、回调上或生命周期函数上使用箭头函数,比如 created: () => console.log(this.a)vm.$watch('a', newValue => this.myMethod())
因为箭头函数并没有 thisthis 会作为变量一直向上级词法作用域查找,直至找到为止。经常导致 Uncaught TypeError: Cannot read property of undefinedUncaught TypeError: this.myMethod is not a function 之类的错误。

不常用模板语法

  • v-once 指令:执行一次性地插值,当数据改变时,插值处的内容不会更新。但请留心这会影响到该节点上的其它数据绑定:

  • 动态参数(2.6.0 新增):
    <a v-bind:[attributeName]="url"> ... </a>
    <a v-on:[eventName]="doSomething"> ... </a>

    • 对动态参数表达式的约束:
      1、动态参数表达式有一些语法约束,因为某些字符,如空格和引号,放在 HTML attribute 名里是无效的。例如:

      <!-- 这会触发一个编译警告 -->
      <a v-bind:['foo' + bar]="value"> ... </a>
      

      变通的办法是使用没有空格或引号的表达式,或用计算属性替代这种复杂表达式。

      2、在 DOM 中使用模板时 (直接在一个 HTML 文件里撰写模板),还需要避免使用大写字符来命名键名,因为浏览器会把 attribute 名全部强制转为小写:

      <!-- 
      在 DOM 中使用模板时这段代码会被转换为 `v-bind:[someattr]`。
      除非在实例中有一个名为“someattr”的 property,否则代码不会工作。
      -->
      <a v-bind:[someAttr]="value"> ... </a>
      

      动态参数预期会求出一个字符串,异常情况下值为 null。这个特殊的 null 值可以被显性地用于移除绑定。任何其它非字符串类型的值都将会触发一个警告。

计算属性和侦听器

计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要 message 还没有发生改变,多次访问 reversedMessage 计算属性会立即返回之前的计算结果,而不必再次执行函数。
相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。
通常更好的做法是使用计算属性而不是命令式的 watch 回调。

Class 与 Style 绑定

  • 自动添加前缀
    v-bind:style 使用需要添加浏览器引擎前缀的 CSS property 时,如 transformVue.js 会自动侦测并添加相应的前缀。

  • 多重值
    2.3.0 起你可以为 style 绑定中的 property 提供一个包含多个值的数组,常用于提供多个带前缀的值,如:<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
    这样写只会渲染数组中最后一个被浏览器支持的值。在本例中,如果浏览器支持不带浏览器前缀的 flexbox,那么就只会渲染 display: flex

条件渲染

  • key 管理可复用的元素
    Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。
    这样也不总是符合实际需求,所以 Vue 为你提供了一种方式来表达“这两个元素是完全独立的,不要复用它们”。只需添加一个具有唯一值的 key 即可。

  • v-if vs v-show
    1、v-if 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。
    2、v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
    3、相比之下,v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。
    4、一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。

    注意,v-show 不支持 <template> 元素,也不支持 v-else

  • v-ifv-for 一起使用
    v-ifv-for 一起使用时,v-for 具有比 v-if 更高的优先级,这意味着 v-if 将分别重复运行于每个 v-for 循环中。不推荐同时使用 v-ifv-for。若使用,eslint 会报错~~

数组更新检测

Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:push()pop()shift()unshift()splice()sort()reverse()

事件绑定

同时绑定多个事件

<button @click="one($event), two($event)">Submit</button>

methods: {
  one(event) {
    // first handler logic...
  },
  two(event) {
    // second handler logic...
  }
}

修饰符

  • 事件修饰符
    .prevent:等同于 JavaScript 中的 event.preventDefault(),用于取消默认事件
    .stop:等同于 JavaScript 中的 event.stopPropagation(),防止事件冒泡(由内到外)
    .self:只会触发自己范围内的事件,不包含子元素
    .capture:与事件冒泡的方向相反,事件捕获由外到内
    .once:事件将只会触发一次
    .passive:设置 {passive: true},表示处理事件函数中不会调用 preventDefault 函数,减少了额外的监听,从而提高了性能;所以不能和 .prevent 修饰符一同使用,否则浏览器会报错。尤其能够提升移动端的性能
    .native:把一个 vue 组件转化为一个普通的 HTML 标签,并且该修饰符对普通 HTML 标签是没有任何作用的。

  • 按键修饰符
    .enter.tab.esc.space.up.down.left.right.delete: 捕获“删除”和“退格”键

  • 系统修饰键
    .ctrl.alt.shift.meta

  • 鼠标按钮修饰符
    .left.right.middle

  • 精确修饰符
    .exact:允许你控制由精确的系统修饰符组合触发的事件。

    <!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
    <button v-on:click.ctrl="onClick">A</button>
    
    <!-- 有且只有 Ctrl 被按下的时候才触发 -->
    <button v-on:click.ctrl.exact="onCtrlClick">A</button>
    
    <!-- 没有任何系统修饰符被按下的时候才触发 -->
    <button v-on:click.exact="onClick">A</button>
    
  • 表单修饰符
    .lazy:在默认情况下,v-model 在每次 input 事件触发后将输入框的值与数据进行同步 (除了上述输入法组合文字时)。你可以添加 lazy 修饰符,从而转为在 change 事件之后进行同步。
    .number:自动将用户的输入值转为数值类型。
    .trim:自动过滤用户输入的首尾空白字符。

    修饰符可以串联,也可以只有修饰符。
    使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self 会阻止所有的点击,而 v-on:click.self.prevent 只会阻止对元素自身的点击。

组件

  • 全局组件:使用 app.component('search-input', SearchInputComponent) 定义。只要定义了,处处可以使用,性能不高,但是使用起来简单。建议小写字母开头中划线间隔命名。

  • 局部组件:使用 components 注册。定义了,要注册之后才能使用,性能较高,使用起来麻烦。建议大写字母开头驼峰命名。

  • Prop
    1、Prop 的大小写
    HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名:

    const app = Vue.createApp({})
    
    app.component('blog-post', {
      // camelCase in JavaScript
      props: ['postTitle'],
      template: '<h3>{{ postTitle }}</h3>'
    })
    
    <!-- 在 HTML 中是 kebab-case 的 -->
    <blog-post post-title="hello!"></blog-post>
    

    2、传入一个对象的所有 property
    如果你想要将一个对象的所有 property 都作为 prop 传入,你可以使用不带参数的 v-bind (取代 v-bind:prop-name)。例如,对于一个给定的对象 post

    post: {
      id: 1,
      title: 'My Journey with Vue'
    }
    

    下面的模板:

    <blog-post v-bind="post"></blog-post>
    

    等价于:

    <blog-post :id="post.id" :title="post.title"></blog-post>
    

    3、单项数据流
    所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。

    4、Prop 验证

    app.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
          }
        }
      }
    })
    

    5、非 prop 的 attribute
    组件可以接受任意的 attribute,而这些 attribute 会被添加到这个组件的根元素上。
    如果你不希望组件的根元素继承 attribute,你可以在组件的选项中设置 inheritAttrs: false。(不会影响 styleclass 的绑定)

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

    当组件有多个根节点时:

    <custom-layout id="custom-layout" @click="changeValue"></custom-layout>
    
    // This will raise a warning
    app.component('custom-layout', {
      template: `
        <header>...</header>
        <main>...</main>
        <footer>...</footer>
      `
    })
    
    // No warnings, $attrs are passed to <main> element
    app.component('custom-layout', {
      template: `
        <header>...</header>
        <main v-bind="$attrs">...</main>
        <footer>...</footer>
      `
    })
    

自定义事件

不同于组件和 prop,事件名不存在任何自动化的大小写转换。因为 HTML 是大小写不敏感的,因此推荐你始终使用 kebab-case 的事件名。

  • 验证发出的事件
app.component('custom-form', {
  emits: {
    // No validation
    click: null,

    // Validate submit event
    submit: ({ email, password }) => {
      if (email && password) {
        return true
      } else {
        console.warn('Invalid submit event payload!')
        return false
      }
    }
  },
  methods: {
    submitForm() {
      this.$emit('submit', { email, password })
    }
  }
})
  • 自定义组件的 v-model
<my-component v-model:title="bookTitle"></my-component>
app.component('my-component', {
  props: {
    title: String
  },
  emits: ['update:title'],
  template: `
    <input 
      type="text"
      :value="title"
      @input="$emit('update:title', $event.target.value)">
  `
})
  • 同时绑定多个 v-model
<user-name
  v-model:first-name="firstName"
  v-model:last-name="lastName"
></user-name>
app.component('user-name', {
  props: {
    firstName: String,
    lastName: String
  },
  emits: ['update:firstName', 'update:lastName'],
  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)">
  `
})
  • v-model 增加自定义修饰符
<my-component v-model.capitalize="myText"></my-component>
app.component('my-component', {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  template: `
    <input type="text" 
      :value="modelValue"
      @input="emitValue">
  `,
  created() {
    console.log(this.modelModifiers) // { capitalize: true }
  },
  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)
    }
  },
})

插槽

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

  • 后备内容:

// slot 标签内为后备内容,父组件提供了插槽内容,则展示;否则展示后备内容
<button type="submit">
  <slot>Submit</slot>
</button>
  • 具名插槽:v-slot 只能添加在 <template> 上(独占默认插槽的缩写语法除外)
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <!-- 一个不带 name 的 <slot> 出口会带有隐含的名字“default” -->
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>
<base-layout>
  <!-- 可简写为 <template #header> -->
  <!-- 还可使用动态插槽名:<template v-slot:[dynamicSlotName]> -->
  <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>
  • 作用域插槽
    为了让插槽内容能够访问子组件中才有的数据,可以将 data 作为 <slot> 元素的一个 attribute 绑定上去。
<!-- 绑定在 <slot> 元素上的 attribute 被称为插槽 prop -->
<span>
 <slot :user="user">
   {{ user.lastName }}
 </slot>
</span>

<!-- 将包含所有插槽 prop 的对象命名为 slotProps,也可以使用任意你喜欢的名字 -->
<current-user>
 <template v-slot:default="slotProps">
   {{ slotProps.user.firstName }}
 </template>
</current-user>
  • 独占默认插槽的缩写语法
    被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用
<!--
解构插槽 Prop:
<current-user v-slot="{ user: person }"> or <current-user #default="{ user }">
-->
<current-user v-slot="slotProps">
  {{ slotProps.user.firstName }}
</current-user>

provide / inject

使用场景:由于 vue$parent 属性可以让子组件访问父组件。但孙组件想要访问祖先组件就比较困难。通过 provide / inject 可以轻松实现跨级访问祖先组件的数据。
provide:Object | () => Object
inject:Array<string> | { [key: string]: string | Symbol | Object }

// TodoList -> TodoListFooter -> TodoListStatistics
app.component('todo-list', {
  // ...
  provide() {
    return {
      todoLength: Vue.computed(() => this.todos.length)
    }
  }
})

app.component('todo-list-statistics', {
  inject: ['todoLength'],
  created() {
    console.log(`Injected property: ${this.todoLength.value}`) // > Injected property: 5
  }
})

提示:provideinject 绑定并不是可响应的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。

动态组件与异步组件

动态组件:

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

注意这个 <keep-alive> 要求被切换到的组件都有自己的名字,不论是通过组件的 name 选项还是局部 / 全局注册。

异步组件:

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/AsyncComponent.vue')
)

app.component('async-component', AsyncComp)

// or
import { createApp, defineAsyncComponent } from 'vue'

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

mixin

混入 mixin 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。

  • 例子
// 定义一个混入对象
const myMixin = {
  created() {
    this.hello()
  },
  methods: {
    hello() {
      console.log('hello from mixin!')
    }
  }
}

// 定义一个使用混入对象的组件
const app = Vue.createApp({
  mixins: [myMixin]
})

app.mount('#app') // => "hello from mixin!"
  • 选项合并
    当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。

    1. 数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。
    2. 同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。
    3. 值为对象的选项,例如 methodscomponentsdirectives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
  • 全局混入
    混入也可以进行全局注册。一旦使用全局混入,它将影响每一个之后创建的 Vue 实例 (包括第三方组件)。

const app = Vue.createApp({
  // 自定义属性
  myOption: 'hello!'
})

// 为自定义的选项 'myOption' 注入一个处理器。
app.mixin({
  created() {
    // 获取自定义属性 this.$options.XXX
    const myOption = this.$options.myOption
    if (myOption) {
      console.log(myOption)
    }
  }
})

// add myOption also to child component
app.component('test-component', {
  myOption: 'hello from component!'
})

app.mount('#app')

// => "hello!"
// => "hello from component!"
  • 自定义选项合并策略
    自定义选项将使用默认策略,即简单地覆盖已有值。如果想让自定义选项以自定义逻辑合并,可以向 app.config.optionMergeStrategies 添加一个函数:
const app = Vue.createApp({})
// 返回合并后的值
app.config.optionMergeStrategies.customOption = (mixinVal, appVal) => mixinVal || appVal

自定义指令

  • 注册一个全局自定义指令 v-focus
const app = Vue.createApp({})
app.directive('focus', {
  // 当被绑定的元素插入到 DOM 中时……
  mounted(el) {
    // 聚焦元素
    el.focus()
  }
})
  • 注册局部指令:
app.directive('focus', {
  mounted (el) {
    el.focus()
  }
})

// 若只有 mounted 和 updated,且逻辑一致,可简写
// binding.arg 获取参数;binding.value 获取值
app.directive('top', (el, binding) => {
   el.style.top = binding.value + 'px'
})
  • 钩子函数

    1. created: 在绑定元素的属性或事件侦听器被应用之前调用,只调用一次
    2. beforeMount: 当指令第一次绑定到元素上并且父组件挂载之前调用
    3. mounted: 当绑定元素的父组件挂载完成时调用
    4. beforeUpdate: 指令所在组件的 VNode 及其子 VNode 更新前调用
    5. updated: 指令所在组件的 VNode 及其子 VNode 更新后调用
    6. beforeUnmount: 当绑定元素的父组件销毁之前调用
    7. unmounted: 只调用一次,指令与元素解绑时调用
  • 动态指令参数

<div id="dynamicexample">
 <h2>Scroll down the page</h2>
 <input type="range" min="0" max="500" v-model="pinPadding">
 <p v-pin:[direction]="pinPadding">Stick me {{ pinPadding + 'px' }} from the {{ direction }} of the page</p>
</div>
const app = Vue.createApp({
  data() {
    return {
      direction: 'right',
      pinPadding: 200
    }
  }
})

app.directive('pin', {
  mounted(el, binding) {
    el.style.position = 'fixed'
    const s = binding.arg || 'top'
    el.style[s] = binding.value + 'px'
  },
  updated(el, binding) {
    const s = binding.arg || 'top'
    el.style[s] = binding.value + 'px'
  }
})

传送门 teleport

将插槽内容传送至指定位置,接受一个 to 的属性,它接受一个 css query selector 作为参数,这就是代表要把这个组件渲染到哪个 dom 元素中

<body>
    <div id="app"></div>
    <div id="childBox"></div>
    <div id="modals"></div>
    <div class="wrapper"></div>
    <div data-teleport></div>
</body>
const app = Vue.createApp({
        template: `
            <h1>Root</h1>
            <modal-button />
            <parent-component />
            <multiple-teleports />
        `
    })

    // 基础用法
    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">
                    I'm a teleported modal! (My parent is "body")
                    <button @click="modalOpen = false">Close</button>
                </div>
            </teleport>
        `,
        data() {
            return { 
                modalOpen: false
            }
        }
    })

    // 与Vue components一起使用,如果<teleport>包含Vue组件,则它仍将是<teleport>父组件的逻辑子组件
    app.component('parent-component', {
        template: `
            <h2>This is a parent component</h2>
            <teleport to="#childBox">
                <child-component name="John" />
            </teleport>
        `
    })

    app.component('child-component', {
        props: [ 'name' ],
        template: `<div>Hello, {{ name }}</div>`
    })

    // 在同一目标上使用多个teleport
    // disabled 可以用于禁用teleport组件的功能,这意味着它的插槽内容将不会被移动到任何位置,而是在周围父组件中指定<teleport>的地方渲染。
    app.component('multiple-teleports', {
        template: `
            <teleport to="#modals" disabled>
                <div>A</div>
            </teleport>
            <teleport to="#modals">
                <div>B</div>
            </teleport>
            <teleport to=".wrapper">
                <div>C</div>
            </teleport>
            <teleport to="[data-teleport]">
                <div>D</div>
            </teleport>
        `
    })

    app.mount('#app')

Suspense

Suspense 组件用于在等待某个异步组件解析时显示后备内容。

AsyncShow.vue

<template>
    <div v-for="(item, index) in list" :key="index">{{item}}</div>
</template>

<script lang="ts">
import axios from 'axios'
import { defineComponent, PropType } from 'vue'

export default defineComponent({
    name: 'AsyncShow',
    async setup () {
        const result = await axios.get('https://XXX')

        return {
            list: result.data,
        }
    },
})
</script>

index.vue

<template>
    <Suspense>
        <template #default>
            <async-show />
        </template>
        <template #fallback>
            <h1>Loading !...</h1>
        </template>
    </Suspense>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import AsyncShow from '@/components/AsyncShow.vue'

export default defineComponent({
    name: 'Index',
    components: {
        AsyncShow,
    },
})
</script>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • Vue.js(读音/vjuː/, 类似于 view)是一个构建数据驱动的web 界面的渐进式框架。Vue.js的目...
    xingyunfuhao阅读 499评论 0 0
  • 指令用在我们的html中。 指令 (Directives) 是带有v-前缀的特殊特性。指令特性的值预期是单个 Ja...
    Rising_life阅读 412评论 0 3
  • 构造器 实例化vue时,需传入一个选项对象,它可以包括 数据、模板、挂载元素、方法和生命周期钩子属性与方法 每个v...
    饥人谷_阿银阅读 424评论 0 0
  • 下载安装搭建环境 可以选npm安装,或者简单下载一个开发版的vue.js文件 浏览器打开加载有vue的文档时,控制...
    冥冥2017阅读 5,976评论 0 42
  • 每个 Vue 应用都是通过用 Vue 函数创建一个新的 Vue 实例开始的: 实例生命周期钩子 每个 Vue 实例...
    Timmy小石匠阅读 1,340评论 0 11