Skip to content

Vue2 组件 ​

组件注册 ​

全局注册 ​

js
import Vue from 'vue'
import MyComponent from './MyComponent.vue'

Vue.component('my-component', MyComponent)
import Vue from 'vue'
import MyComponent from './MyComponent.vue'

Vue.component('my-component', MyComponent)

全局注册的组件在任何实例中都能使用。

局部注册 ​

vue
<template>
  <div>
    <child-component></child-component>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue'

export default {
  components: {
    ChildComponent
  }
}
</script>
<template>
  <div>
    <child-component></child-component>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue'

export default {
  components: {
    ChildComponent
  }
}
</script>

局部注册的组件只在当前组件中可用。

Props — 父传子 ​

基础用法 ​

vue
<!-- 父组件 -->
<template>
  <child-component
    :title="title"
    :count="count"
    :list="list"
    :user-info="userInfo"
  />
</template>

<!-- 子组件 ChildComponent.vue -->
<template>
  <div>
    <h3>{{ title }}</h3>
    <p>数量:{{ count }}</p>
    <p>用户:{{ userInfo.name }}</p>
  </div>
</template>

<script>
export default {
  // 数组声明(简单)
  props: ['title', 'count', 'list', 'userInfo']
}
</script>
<!-- 父组件 -->
<template>
  <child-component
    :title="title"
    :count="count"
    :list="list"
    :user-info="userInfo"
  />
</template>

<!-- 子组件 ChildComponent.vue -->
<template>
  <div>
    <h3>{{ title }}</h3>
    <p>数量:{{ count }}</p>
    <p>用户:{{ userInfo.name }}</p>
  </div>
</template>

<script>
export default {
  // 数组声明(简单)
  props: ['title', 'count', 'list', 'userInfo']
}
</script>

类型校验 ​

js
export default {
  props: {
    title: String,
    count: {
      type: Number,
      required: true,
      default: 0
    },
    userInfo: {
      type: Object,
      default() {
        return { name: '默认用户' }
      }
    },
    status: {
      type: String,
      validator(value) {
        return ['success', 'warning', 'error'].includes(value)
      }
    }
  }
}
export default {
  props: {
    title: String,
    count: {
      type: Number,
      required: true,
      default: 0
    },
    userInfo: {
      type: Object,
      default() {
        return { name: '默认用户' }
      }
    },
    status: {
      type: String,
      validator(value) {
        return ['success', 'warning', 'error'].includes(value)
      }
    }
  }
}

单向数据流: props 不可在子组件中直接修改,需要通过 $emit 通知父组件修改。

$emit — 子传父 ​

vue
<!-- 父组件 -->
<template>
  <child-component @update-count="handleUpdate" @submit="handleSubmit" />
</template>

<script>
export default {
  methods: {
    handleUpdate(val) {
      this.count = val
    },
    handleSubmit(data) {
      console.log('提交数据:', data)
    }
  }
}
</script>
<!-- 父组件 -->
<template>
  <child-component @update-count="handleUpdate" @submit="handleSubmit" />
</template>

<script>
export default {
  methods: {
    handleUpdate(val) {
      this.count = val
    },
    handleSubmit(data) {
      console.log('提交数据:', data)
    }
  }
}
</script>
vue
<!-- 子组件 ChildComponent.vue -->
<template>
  <div>
    <button @click="$emit('update-count', count + 1)">增加</button>
    <button @click="submitForm">提交</button>
  </div>
</template>

<script>
export default {
  data() {
    return { count: 0 }
  },
  methods: {
    submitForm() {
      this.$emit('submit', { name: 'test', age: 18 })
    }
  }
}
</script>
<!-- 子组件 ChildComponent.vue -->
<template>
  <div>
    <button @click="$emit('update-count', count + 1)">增加</button>
    <button @click="submitForm">提交</button>
  </div>
</template>

<script>
export default {
  data() {
    return { count: 0 }
  },
  methods: {
    submitForm() {
      this.$emit('submit', { name: 'test', age: 18 })
    }
  }
}
</script>

.sync 修饰符 ​

vue
<!-- 父组件 -->
<template>
  <child :title.sync="title" />
  <!-- 等价于:<child :title="title" @update:title="val => title = val" /> -->
</template>
<!-- 父组件 -->
<template>
  <child :title.sync="title" />
  <!-- 等价于:<child :title="title" @update:title="val => title = val" /> -->
</template>
vue
<!-- 子组件 -->
<script>
export default {
  props: ['title'],
  methods: {
    updateTitle() {
      this.$emit('update:title', '新标题')
    }
  }
}
</script>
<!-- 子组件 -->
<script>
export default {
  props: ['title'],
  methods: {
    updateTitle() {
      this.$emit('update:title', '新标题')
    }
  }
}
</script>

组件通信 8 种方式 ​

1. props / $emit — 父子通信 ​

父子组件之间最常用的方式。

2. $refs — 父访问子 ​

vue
<template>
  <child-component ref="child" />
</template>

<script>
export default {
  mounted() {
    // 访问子组件实例
    this.$refs.child.focus()
    console.log(this.$refs.child.title)
  }
}
</script>
<template>
  <child-component ref="child" />
</template>

<script>
export default {
  mounted() {
    // 访问子组件实例
    this.$refs.child.focus()
    console.log(this.$refs.child.title)
  }
}
</script>

3. $parent / $children — 实例访问 ​

js
// 子组件访问父组件
this.$parent.xxx

// 父组件访问所有子组件
this.$children // 数组,不保证顺序
// 子组件访问父组件
this.$parent.xxx

// 父组件访问所有子组件
this.$children // 数组,不保证顺序

注意: $parent 和 $children 应节制使用,推荐 props 和 events 进行通信。

4. provide / inject — 跨级通信 ​

js
// 祖先组件
export default {
  provide: {
    theme: 'dark',
    user: { name: '张三' }
  }
}

// 后代组件
export default {
  inject: ['theme', 'user'],
  mounted() {
    console.log(this.theme) // 'dark'
  }
}
// 祖先组件
export default {
  provide: {
    theme: 'dark',
    user: { name: '张三' }
  }
}

// 后代组件
export default {
  inject: ['theme', 'user'],
  mounted() {
    console.log(this.theme) // 'dark'
  }
}

provide 默认不是响应式的,若要响应式需传对象或函数:

js
// 祖先组件 — 传整个实例以实现响应式
export default {
  data() {
    return { theme: 'dark' }
  },
  provide() {
    return {
      theme: this.theme,          // 非响应式
      updateTheme: this.updateTheme // 方法可以传递
    }
  },
  methods: {
    updateTheme(val) {
      this.theme = val
    }
  }
}
// 祖先组件 — 传整个实例以实现响应式
export default {
  data() {
    return { theme: 'dark' }
  },
  provide() {
    return {
      theme: this.theme,          // 非响应式
      updateTheme: this.updateTheme // 方法可以传递
    }
  },
  methods: {
    updateTheme(val) {
      this.theme = val
    }
  }
}

5. eventBus — 兄弟/任意组件通信 ​

js
// main.js
Vue.prototype.$bus = new Vue()
// main.js
Vue.prototype.$bus = new Vue()
js
// 组件 A — 发送
export default {
  methods: {
    sendMessage() {
      this.$bus.$emit('message', 'Hello from A')
    }
  }
}

// 组件 B — 接收
export default {
  created() {
    this.$bus.$on('message', (msg) => {
      console.log(msg)
    })
  },
  beforeDestroy() {
    // 必须销毁,防止内存泄漏
    this.$bus.$off('message')
  }
}
// 组件 A — 发送
export default {
  methods: {
    sendMessage() {
      this.$bus.$emit('message', 'Hello from A')
    }
  }
}

// 组件 B — 接收
export default {
  created() {
    this.$bus.$on('message', (msg) => {
      console.log(msg)
    })
  },
  beforeDestroy() {
    // 必须销毁,防止内存泄漏
    this.$bus.$off('message')
  }
}

注意: 项目较大时 eventBus 难以维护,推荐 Vuex。

6. $attrs / $listeners — 透传属性和事件 ​

主要用于二次封装组件时透传 props 和事件。

vue
<!-- 父组件 -->
<template>
  <base-input
    placeholder="请输入"
    type="text"
    @focus="onFocus"
    @blur="onBlur"
  />
</template>
<!-- 父组件 -->
<template>
  <base-input
    placeholder="请输入"
    type="text"
    @focus="onFocus"
    @blur="onBlur"
  />
</template>
vue
<!-- BaseInput.vue — 二次封装 -->
<template>
  <div>
    <input
      v-bind="$attrs"      <!-- 透传所有未被 props 接收的属性 -->
      v-on="$listeners"    <!-- 透传所有事件 -->
    />
  </div>
</template>

<script>
export default {
  inheritAttrs: false // 阻止根元素继承未识别的属性
}
</script>
<!-- BaseInput.vue — 二次封装 -->
<template>
  <div>
    <input
      v-bind="$attrs"      <!-- 透传所有未被 props 接收的属性 -->
      v-on="$listeners"    <!-- 透传所有事件 -->
    />
  </div>
</template>

<script>
export default {
  inheritAttrs: false // 阻止根元素继承未识别的属性
}
</script>

7. Vuex — 全局状态管理 ​

详见 Vue-状态管理。

8. localStorage / sessionStorage ​

适合持久化非敏感数据。

js
// 存储
localStorage.setItem('token', 'xxx')

// 读取
const token = localStorage.getItem('token')

// 注意:数据变化需要手动更新视图
// 存储
localStorage.setItem('token', 'xxx')

// 读取
const token = localStorage.getItem('token')

// 注意:数据变化需要手动更新视图

插槽 (Slot) ​

默认插槽 ​

vue
<!-- 子组件 Child.vue -->
<template>
  <div class="card">
    <slot>默认内容(没传内容时显示)</slot>
  </div>
</template>

<!-- 父组件 -->
<template>
  <child>
    <p>这里替换插槽内容</p>
  </child>
</template>
<!-- 子组件 Child.vue -->
<template>
  <div class="card">
    <slot>默认内容(没传内容时显示)</slot>
  </div>
</template>

<!-- 父组件 -->
<template>
  <child>
    <p>这里替换插槽内容</p>
  </child>
</template>

具名插槽 ​

vue
<!-- 子组件 Layout.vue -->
<template>
  <div class="layout">
    <header>
      <slot name="header" />
    </header>
    <main>
      <slot />
    </main>
    <footer>
      <slot name="footer" />
    </footer>
  </div>
</template>

<!-- 父组件 -->
<template>
  <layout>
    <template #header>
      <h1>标题</h1>
    </template>

    <p>默认插槽内容</p>

    <template #footer>
      <p>底部信息</p>
    </template>
  </layout>
</template>
<!-- 子组件 Layout.vue -->
<template>
  <div class="layout">
    <header>
      <slot name="header" />
    </header>
    <main>
      <slot />
    </main>
    <footer>
      <slot name="footer" />
    </footer>
  </div>
</template>

<!-- 父组件 -->
<template>
  <layout>
    <template #header>
      <h1>标题</h1>
    </template>

    <p>默认插槽内容</p>

    <template #footer>
      <p>底部信息</p>
    </template>
  </layout>
</template>

作用域插槽 ​

子组件向父组件传递插槽数据。

vue
<!-- 子组件 List.vue -->
<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <slot :item="item" :index="index">
        {{ item.name }} <!-- 默认显示 -->
      </slot>
    </li>
  </ul>
</template>

<script>
export default {
  data() {
    return {
      items: [
        { id: 1, name: '张三', age: 18 },
        { id: 2, name: '李四', age: 20 }
      ]
    }
  }
}
</script>
<!-- 子组件 List.vue -->
<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <slot :item="item" :index="index">
        {{ item.name }} <!-- 默认显示 -->
      </slot>
    </li>
  </ul>
</template>

<script>
export default {
  data() {
    return {
      items: [
        { id: 1, name: '张三', age: 18 },
        { id: 2, name: '李四', age: 20 }
      ]
    }
  }
}
</script>
vue
<!-- 父组件 — 使用插槽定制渲染 -->
<template>
  <list>
    <template #default="{ item, index }">
      <span>{{ index }} - {{ item.name }}({{ item.age }}岁)</span>
    </template>
  </list>
</template>
<!-- 父组件 — 使用插槽定制渲染 -->
<template>
  <list>
    <template #default="{ item, index }">
      <span>{{ index }} - {{ item.name }}({{ item.age }}岁)</span>
    </template>
  </list>
</template>

作用域插槽解构 ​

vue
<template>
  <!-- 解构 prop -->
  <list v-slot="{ item }">
    <span>{{ item.name }}</span>
  </list>

  <!-- 解构 + 重命名 -->
  <list v-slot="{ item: user }">
    <span>{{ user.name }}</span>
  </list>

  <!-- 具名 + 作用域 -->
  <template #header="{ title }">
    <h1>{{ title }}</h1>
  </template>
</template>
<template>
  <!-- 解构 prop -->
  <list v-slot="{ item }">
    <span>{{ item.name }}</span>
  </list>

  <!-- 解构 + 重命名 -->
  <list v-slot="{ item: user }">
    <span>{{ user.name }}</span>
  </list>

  <!-- 具名 + 作用域 -->
  <template #header="{ title }">
    <h1>{{ title }}</h1>
  </template>
</template>

生命周期 ​

生命周期图示 ​

new Vue()
  │
beforeCreate    data 和 methods 还未初始化
  │
created         data 和 methods 已初始化,可访问,但未挂载 DOM
  │
beforeMount     模板已编译,虚拟 DOM 已创建,尚未渲染
  │
mounted         DOM 已挂载,可操作 DOM,适合发请求
  │
  │   (数据变化时)
  │
beforeUpdate   数据已变化,DOM 尚未更新
  │
updated         DOM 已更新
  │
  │   (组件销毁时)
  │
beforeDestroy   组件销毁前,适合清理定时器/事件监听
  │
destroyed       组件已销毁
new Vue()
  │
beforeCreate    data 和 methods 还未初始化
  │
created         data 和 methods 已初始化,可访问,但未挂载 DOM
  │
beforeMount     模板已编译,虚拟 DOM 已创建,尚未渲染
  │
mounted         DOM 已挂载,可操作 DOM,适合发请求
  │
  │   (数据变化时)
  │
beforeUpdate   数据已变化,DOM 尚未更新
  │
updated         DOM 已更新
  │
  │   (组件销毁时)
  │
beforeDestroy   组件销毁前,适合清理定时器/事件监听
  │
destroyed       组件已销毁

生命周期用途总结 ​

钩子用途
beforeCreate极少使用,实例初始化前
created可访问 data/methods,适合初始化数据
beforeMount极少使用,编译完成未渲染
mounted最常用,发请求、操作 DOM、加事件监听
beforeUpdate数据更新前,可获取旧 DOM
updated数据更新后,避免在此改数据(死循环)
beforeDestroy清理:清除定时器、移除事件监听、取消订阅
destroyed实例已销毁
activatedkeep-alive 组件激活时
deactivatedkeep-alive 组件停用时
errorCaptured捕获后代组件错误
js
export default {
  // 创建
  created() {
    this.fetchInitData()
  },
  // 挂载
  mounted() {
    window.addEventListener('resize', this.handleResize)
    this.getList()
  },
  // 更新
  beforeUpdate() {
    console.log('数据变化,DOM 未更新')
  },
  updated() {
    console.log('DOM 已更新')
  },
  // 销毁
  beforeDestroy() {
    window.removeEventListener('resize', this.handleResize)
    clearInterval(this.timer)
  },
  // keep-alive
  activated() {
    console.log('组件激活')
  },
  deactivated() {
    console.log('组件停用')
  },
  // 错误捕获
  errorCaptured(err, vm, info) {
    console.error('捕获错误:', err, info)
    return false // 阻止错误向上传播
  }
}
export default {
  // 创建
  created() {
    this.fetchInitData()
  },
  // 挂载
  mounted() {
    window.addEventListener('resize', this.handleResize)
    this.getList()
  },
  // 更新
  beforeUpdate() {
    console.log('数据变化,DOM 未更新')
  },
  updated() {
    console.log('DOM 已更新')
  },
  // 销毁
  beforeDestroy() {
    window.removeEventListener('resize', this.handleResize)
    clearInterval(this.timer)
  },
  // keep-alive
  activated() {
    console.log('组件激活')
  },
  deactivated() {
    console.log('组件停用')
  },
  // 错误捕获
  errorCaptured(err, vm, info) {
    console.error('捕获错误:', err, info)
    return false // 阻止错误向上传播
  }
}

父组件和子组件生命周期顺序 ​

父 beforeCreate → 父 created → 父 beforeMount
  → 子 beforeCreate → 子 created → 子 beforeMount → 子 mounted
→ 父 mounted
父 beforeCreate → 父 created → 父 beforeMount
  → 子 beforeCreate → 子 created → 子 beforeMount → 子 mounted
→ 父 mounted

监听子组件生命周期 ​

vue
<template>
  <!-- 在父组件中监听子组件的生命周期钩子 -->
  <child @hook:mounted="onChildMounted" @hook:updated="onChildUpdated" />
</template>

<script>
export default {
  methods: {
    onChildMounted() {
      console.log('子组件已挂载')
    }
  }
}
</script>
<template>
  <!-- 在父组件中监听子组件的生命周期钩子 -->
  <child @hook:mounted="onChildMounted" @hook:updated="onChildUpdated" />
</template>

<script>
export default {
  methods: {
    onChildMounted() {
      console.log('子组件已挂载')
    }
  }
}
</script>

动态组件 ​

基本用法 ​

vue
<template>
  <div>
    <button @click="currentComponent = 'ComponentA'">组件A</button>
    <button @click="currentComponent = 'ComponentB'">组件B</button>

    <component :is="currentComponent" />
  </div>
</template>

<script>
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'

export default {
  components: { ComponentA, ComponentB },
  data() {
    return { currentComponent: 'ComponentA' }
  }
}
</script>
<template>
  <div>
    <button @click="currentComponent = 'ComponentA'">组件A</button>
    <button @click="currentComponent = 'ComponentB'">组件B</button>

    <component :is="currentComponent" />
  </div>
</template>

<script>
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'

export default {
  components: { ComponentA, ComponentB },
  data() {
    return { currentComponent: 'ComponentA' }
  }
}
</script>

keep-alive 缓存组件 ​

vue
<template>
  <div>
    <!-- 动态组件切换时,默认会销毁/重建 -->
    <!-- 使用 keep-alive 可以缓存组件状态 -->
    <keep-alive>
      <component :is="currentComponent" />
    </keep-alive>

    <!-- 条件缓存:include/exclude 匹配组件 name -->
    <keep-alive :include="['ComponentA', 'ComponentC']">
      <component :is="currentComponent" />
    </keep-alive>

    <!-- 最大缓存数 -->
    <keep-alive :max="5">
      <component :is="currentComponent" />
    </keep-alive>
  </div>
</template>
<template>
  <div>
    <!-- 动态组件切换时,默认会销毁/重建 -->
    <!-- 使用 keep-alive 可以缓存组件状态 -->
    <keep-alive>
      <component :is="currentComponent" />
    </keep-alive>

    <!-- 条件缓存:include/exclude 匹配组件 name -->
    <keep-alive :include="['ComponentA', 'ComponentC']">
      <component :is="currentComponent" />
    </keep-alive>

    <!-- 最大缓存数 -->
    <keep-alive :max="5">
      <component :is="currentComponent" />
    </keep-alive>
  </div>
</template>

关于组件 name ​

js
export default {
  // name 的作用
  name: 'MyComponent'
  // 1. keep-alive 的 include/exclude 匹配
  // 2. Vue DevTools 显示
  // 3. 递归组件调用自身
}
export default {
  // name 的作用
  name: 'MyComponent'
  // 1. keep-alive 的 include/exclude 匹配
  // 2. Vue DevTools 显示
  // 3. 递归组件调用自身
}

函数式组件 ​

函数式组件无状态、无实例、无 this,渲染开销低。

js
// 全局注册
Vue.component('smart-list', {
  functional: true,
  props: {
    items: Array,
    isOrdered: Boolean
  },
  render(h, context) {
    const children = context.props.items.map(item => h('li', item.name))
    return context.props.isOrdered ? h('ol', children) : h('ul', children)
  }
})
// 全局注册
Vue.component('smart-list', {
  functional: true,
  props: {
    items: Array,
    isOrdered: Boolean
  },
  render(h, context) {
    const children = context.props.items.map(item => h('li', item.name))
    return context.props.isOrdered ? h('ol', children) : h('ul', children)
  }
})
vue
<!-- 单文件函数式组件 -->
<template functional>
  <div>
    <h3>{{ props.title }}</h3>
    <slot />
  </div>
</template>
<!-- 单文件函数式组件 -->
<template functional>
  <div>
    <h3>{{ props.title }}</h3>
    <slot />
  </div>
</template>

Mixins — 混入 ​

基础用法 ​

js
// mixins/tableMixin.js
export const tableMixin = {
  data() {
    return {
      loading: false,
      list: [],
      pagination: {
        page: 1,
        size: 10,
        total: 0
      }
    }
  },
  methods: {
    async fetchData() {
      this.loading = true
      try {
        const res = await this.getList()
        this.list = res.data.list
        this.pagination.total = res.data.total
      } finally {
        this.loading = false
      }
    }
  }
}
// mixins/tableMixin.js
export const tableMixin = {
  data() {
    return {
      loading: false,
      list: [],
      pagination: {
        page: 1,
        size: 10,
        total: 0
      }
    }
  },
  methods: {
    async fetchData() {
      this.loading = true
      try {
        const res = await this.getList()
        this.list = res.data.list
        this.pagination.total = res.data.total
      } finally {
        this.loading = false
      }
    }
  }
}
js
// 组件中使用
import { tableMixin } from '@/mixins/tableMixin'

export default {
  mixins: [tableMixin],
  data() {
    return { otherData: [] }
  },
  mounted() {
    this.fetchData()
  }
}
// 组件中使用
import { tableMixin } from '@/mixins/tableMixin'

export default {
  mixins: [tableMixin],
  data() {
    return { otherData: [] }
  },
  mounted() {
    this.fetchData()
  }
}

选项合并规则 ​

  • data:递归合并,组件 data 优先级高于 mixin
  • 生命周期钩子:合并为数组,mixin 钩子先执行,组件钩子后执行
  • methods / computed:同名时组件选项覆盖 mixin

全局混入 ​

js
// main.js — 谨慎使用
Vue.mixin({
  created() {
    console.log('全局 created')
  }
})
// main.js — 谨慎使用
Vue.mixin({
  created() {
    console.log('全局 created')
  }
})

全局 mixin 会影响所有组件,通常只用于插件开发。

Extends — 组件继承 ​

js
// BaseComponent.js
export default {
  data() {
    return { baseData: 'base' }
  },
  methods: {
    baseMethod() {
      console.log('base method')
    }
  }
}
// BaseComponent.js
export default {
  data() {
    return { baseData: 'base' }
  },
  methods: {
    baseMethod() {
      console.log('base method')
    }
  }
}
js
// 组件继承
import BaseComponent from './BaseComponent'

export default {
  extends: BaseComponent,
  data() {
    return { childData: 'child' }
  },
  mounted() {
    console.log(this.baseData) // 'base'
    this.baseMethod()          // 'base method'
  }
}
// 组件继承
import BaseComponent from './BaseComponent'

export default {
  extends: BaseComponent,
  data() {
    return { childData: 'child' }
  },
  mounted() {
    console.log(this.baseData) // 'base'
    this.baseMethod()          // 'base method'
  }
}

extends 的合并策略与 mixins 相同。

边界情况 ​

$root ​

js
// 访问根组件实例
this.$root.count
this.$root.$emit('global-event')
// 访问根组件实例
this.$root.count
this.$root.$emit('global-event')

不建议在子组件中大量依赖 $root,会导致组件难以测试和维护。

内联模板 ​

vue
<my-component inline-template>
  <div>
    <p>内联模板内容,直接绑定了组件的数据和方法</p>
    <button @click="sayHello">点击</button>
  </div>
</my-component>
<my-component inline-template>
  <div>
    <p>内联模板内容,直接绑定了组件的数据和方法</p>
    <button @click="sayHello">点击</button>
  </div>
</my-component>

X-Template ​

html
<script type="text/x-template" id="hello-template">
  <p>Hello {{ name }}</p>
</script>
<script type="text/x-template" id="hello-template">
  <p>Hello {{ name }}</p>
</script>
js
Vue.component('hello', {
  template: '#hello-template',
  data() {
    return { name: 'Vue' }
  }
})
Vue.component('hello', {
  template: '#hello-template',
  data() {
    return { name: 'Vue' }
  }
})