前端自定义Vue组件开发

视频下载

通读官方文档 - 组件基础

// Vue.component('组件名', {/* 定义 */})

// 组件名,推荐遵循 W3C 规范中的自定义组件名 (字母全小写且必须包含一个连字符)

定义一个名为 button-counter 的新组件

es5

Vue.component('button-counter', {
  data: function () {
    return {
      count: 0
    }
  },
  template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})

es6

Vue.component('button-counter', {
  data() {
    return {
      count: 0
    }
  },
  template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})

Webpack,单文件组件

  • @/components/button-counter.vue
<template>
    <button v-on:click="count++">You clicked me {{ count }} times.</button>
</template>

<script>
export default {
  data() {
    return {
      count: 0
    }
  },
}
</script>
  • @/main.js
//...

// 全局注册
import ButtonCounter from '@/components/button-counter.vue'
Vue.component('button-counter', ButtonCounter)

//...

使用

@/App.vue

<template>
    <div id="app">
      <button-counter></button-counter>
      <button-counter></button-counter>
      <button-counter></button-counter>
    </div>
</template>

<script>
// 局部注册
// import ButtonCounter from '@/components/button-counter.vue'

export default {
  // components: { ButtonCounter }, // 相当于 components: { ButtonCounter: ButtonCounter },<button-counter>
  // 或定义别名
  // components: { 'my-counter': ButtonCounter },
  data() {
    return {
      count: 0
    }
  },
}
</script>

vue.min.js完整版和vue.runtime.min.js运行时版的区别:

  • 完整版有complier,运行时版无complier,且完整版代码要多余运行时版40%;
  • 完整版视图写在HTML里或者template选项,运行时版视图写在render函数里用h来创建标签;
  • webpack引入,默认使用运行时版,完整版需配置alias;
  • @vue/li引入,默认使用运行时版,完整版需额外配置。

组件常用API的使用

  • props
  • 事件
  • 修饰符.prop、.sync、.camel(不常用)
  • v-model
  • slot
  • is(动态组件)

子组件 Child.vue

<template>
    <div class="child-comp">
        <header>
            <!-- 命名插槽 -->
            <slot name="header"></slot>
        </header>
        <!-- <span> {{ title }} </span> -->
        <!-- 尽量用v-text -->
        <span v-text="title"></span>
        <br>
        <span v-text="parseNum"></span>
        <br>
        <span v-if="disabled">disabled</span>
        <br>
        <button v-on:click="onClick">测试按钮</button>
        <br>
        <section>
            <!-- 默认插槽 -->
            <slot v-bind:value1="value1" :value2="value2"></slot>
        </section>
        <br>
        <footer>
            <!-- 命名插槽 -->
            <slot name="footer" :value1="value1"></slot>
        </footer>
    </div>
</template>

<script>
export default {
  // 定义v-model
  // v-model默认使用input事件,在自定义使用场景中可以使用其它事件名称,如change。
  model: {
      prop: 'value',
      event: 'change',
  },
  // 不推荐,不限定类型,也不提供默认值
  // props: ['title', 'num'],

  // 推荐
  props: {
    // 与model搭配使用
    value: {
        type: String,
        default: ''
    },
    // 
    syncValue: {
        type: String,
        default: ''
    },
    //
    title: {
        type: String,
        required: true, // 必填,一般是没有default值的时候使用
    },
    num: {
        type: [Number, String], // 限定多个类型
        // 不提供默认值,默认值即为undefined

        // 校验方法(组件实例创建之前进行验证,之后的无效),验证失败,控制台会出现一条warning
        validator(value) {
            return value > 10
        }
    },
    disabled: {
        type: Boolean,
        default: false
    }
  },
  data() {
    return {
        value1: '123',
        value2: 'abc'
    }
  },
  computed: {
      parseNum() {
          let num = parseInt(this.num)
          return isNaN(num)? 0 : num
      }
  },
  methods: {
      onClick() {
          // 事件,推荐你始终使用 kebab-case 的事件名
          this.$emit('on-test', 'abc')
          // 更新v-model的value值
          this.$emit('change', '123')
          // 更新syncValue的值
          this.$emit('update:syncValue', 'efg')
          // click事件
          this.$emit('click', '456')
      }
  },
  // 注入接收
  inject: ['value1', 'value2']
}
</script>

<!-- 一般项目内的组件,都应该添加scoped,避免样式交叉冲突 -->
<!-- 但如果在实现一个可复用的组件库,则建议通过定义一个自定前缀,如el-xxx的方式管理组件库里组件的样式 -->
<style lang="less" scoped>
    .child-comp {
        // 使用scoped的时候,如果组件内含其它第三方组件,若需要修改、覆盖内含组件的样式,需要使用::v-deep,因为内部组件不受该组件的scoped影响
        ::v-deep .el-button{
            // ...
        }
    }
</style>

父组件 Parent.vue

<template>
    <div>
        <!-- :xxx="value1" 相当于 v-bind:xxx="value1" -->
        <!-- @xxx="func1" 相当于 v-on:xxx="func1"  -->
        <!-- xxx 后面不带等号和值,针对type为Boolean的prop,相当于:xxx="true" -->

        <!-- 针对没有定义prop的传入的值,作为dom property。
        <!-- 加修饰符.prop, 如 :title.prop="dom property" ,仅作为dom property。 -->

        <!-- 推荐使用kebab-case,不过使用字符串模板时,使用驼峰式也可以。 -->

        <!-- 传入class和style会自动合并,是vue提供的特殊处理。 -->

        <!-- 加修饰符.native后,如@click.native,则触发click的是基于原生的click事件,而非子组件的$emit('click') -->
        <child-comp 
           v-model="value1" 

           :sync-value.sync="value2" 
           @update:sync-value="onValue2Change"

           :title="title"
           num="123" 

           @on-test="onTest"
           @click.native="onNativeClick"

           disabled>
           <!-- slot-scope 已经被废弃,Vue3中不再支持。建议使用v-slot -->
           <!-- 默认插槽 -->
           <div slot-scope="{ value1 = '默认值', value2: myValue }">
               <span v-text="value1"></span>
               <br>
               <span v-text="myValue"></span>
           </div>
           <!-- 等效于 -->
           <template v-slot:default="{ value1 = '默认值', value2: myValue }">
               <div>
                   <span v-text="value1"></span>
                   <br>
                   <span v-text="myValue"></span>
              </div>
          </template>
           <!-- 只有默认插槽的时候,可以缩写为 -->
           <template v-slot="{ value1 = '默认值', value2: myValue }">
               <div>
                   <span v-text="value1"></span>
                   <br>
                   <span v-text="myValue"></span>
              </div>
          </template>

           <!-- 命名插槽 -->
           <div slot="header"></div>
           <!-- 等效于 -->
           <template v-slot:header>
            <div></div>
          </template>
           <!-- 假装动态插槽名 -->
           <template v-slot:['header']>
            <div></div>
          </template>

           <!-- 命名插槽 -->
           <div #footer="{ value1 }"></div>
          <!-- 等效于 -->
           <div slot="footer" slot-scope="{ value1 }"></div>
        </child-comp>
        <!-- 一次传入所有property,场景如使用第三方组件时,统一部分prop的配置值。 -->
        <child-comp v-bind="{ title: 'xxx', num: 123 }"></child-comp>
        <br>
        <span v-text="value1"></span>
        <br>
        <span v-text="value2"></span>
        <br>
        <!-- 动态组件 -->
        <!-- 绑定组件选项对象,而不是已注册组件名的示例 -->
        <component :is="currentComponent"></component>
    </div>
</template>

<script>
import ChildComp from './Child.vue'
// 假装有另外一个组件
import OtherChildComp from './OtherChildComp.vue'
export default {
  components: { ChildComp },
  data() {
    return {
      title: '我的标题',
      value1: '',
      value2: '',
      currentComponent: OtherChildComp
    }
  },
  methods: {
      onValue2Change() {},
      onTest(value) {
          // ...
      },
      onNativeClick() {}
  },
  // 依赖注入
  // 依赖注入还是有负面影响的。它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。
  // 同时所提供的 property 是非响应式的!!!
  provide() {
     // 返回对象
     return {
        value1: '1',
        value2: this.value2
     }
  }
}
</script>

关于keep-alive

主要用于保留组件状态或避免重新渲染,要求被切换到的组件都有自己的名字,不论是通过组件的 name 选项还是局部/全局注册。

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

<keep-alive>
  <component-a v-if="a > 1"></component-a>
  <component-b v-else></component-b>
</keep-alive>
  • 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们
  • 当组件在 <keep-alive> 内被切换,它的 activateddeactivated 这两个生命周期钩子函数将会被对应执行。

Props

  • include:字符串或正则表达式。只有名称匹配的组件会被缓存。
  • exclude:字符串或正则表达式。任何名称匹配的组件都不会被缓存。
  • max:数字。最多可以缓存多少组件实例。

异步组件

只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。

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

一个推荐的做法是将异步组件和 webpack 的 code-splitting 功能一起配合使用:

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

// 或

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

当使用局部注册的时候,你也可以直接提供一个返回 Promise 的函数:

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

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

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

以上用法,可以配置Vue Router使用。

处理边界情况

  • 访问根实例 - this.$root
  • 访问父级组件实例 - this.$parent
  • 访问子组件实例或子元素 - this.$refs.xxx
<template>
    <div>
        <child-comp ref="xxx">
        </child-comp>
    </div>
</template>

<script>
import ChildComp from './Child.vue'
export default {
  components: { ChildComp },
  data() {
    return {
    }
  },
  methods: {
  },
  mounted() {
      // 访问子组件实例或子元素
      console.log(this.$refs.xxx)
      // 访问子组件实例所挂载的dom element
      console.log(this.$refs.xxx.$el)
  }
}
</script>

程序化的事件侦听器

<template>
    <div>
        <child-comp ref="xxx">
        </child-comp>
    </div>
</template>

<script>
import ChildComp from './Child.vue'
export default {
  components: { ChildComp },
  data() {
    return {
    }
  },
  methods: {
      onTest(value) {}
  },
  mounted() {
      // 侦听一个事件
      this.$refs.xxx.$on('on-test', this.onTest)

      // 一次性侦听一个事件
      this.$refs.xxx.$once('hook:beforeDestroy', this.onTest)

      // 停止侦听一个事件
      this.$refs.xxx.$off('on-test', this.onTest)
  }
}
</script>

递归组件

  • 确保递归调用的条件性,否则会导致“max stack size exceeded”错误。

组件之间的循环引用

  • 全局注册的组件,组件之间的循环引用,是可以的。不过同样要确保调用的条件性。
  • 通过webpack,组件之间存在循环引用会出错,构建过程中的相互依赖关系会导致构建出错。解决办法是通过异步组件的方式实现。

通过 v-once 创建低开销的静态组件

渲染普通的 HTML 元素在 Vue 中是非常快速的,但有的时候你可能有一个组件,这个组件包含了大量静态内容。在这种情况下,你可以在根元素上添加 v-once attribute 以确保这些内容只计算一次然后缓存起来,就像这样:

Vue.component('terms-of-service', {
  template: `
    <div v-once>
      <h1>Terms of Service</h1>
      ... a lot of static content ...
    </div>
  `
})

再说一次,试着不要过度使用这个模式。当你需要渲染大量静态内容时,极少数的情况下它会给你带来便利,除非你非常留意渲染变慢了,不然它完全是没有必要的——再加上它在后期会带来很多困惑。

Vue.use

一般来说,一组相关的组件的初始化,可以通过插件的方式安装。

以webpack下为例:

  • 定义(@/my/index.js)
import xxx from '@/my/components/xxx/index.vue'
import yyy from '@/my/components/yyy/index.vue'
import zzz from '@/my/components/zzz/index.vue'

// other
// 全局资源、全局方法、指令等等,均可以在这里引入,并在install处初始化。

export default {
    install(Vue) {
      // do something maybe
      // 假设'my-'为前缀
      Vue.component('my-xxx', xxx)
      Vue.component('my-yyy', yyy)
      Vue.component('my-zzz', zzz)
    }
}
  • 安装
import Vue from 'vue'

import My from '@/my/index.js'

Vue.use(My)

// ...
文档更新时间: 2024-04-25 14:58   作者:管理员