vue 系列 -- 组件复用、传参、扩展

711 阅读4分钟

前言

一个页面里面有着不同的操作,每个操作对应打开一个弹窗,比如我之前实习做的一个项目,有 10 种操作对应 10 个不同的弹窗,所以基于代码复用原则,我们需要对弹窗进行复用

组件注册(局部注册)

基于 JavaScript 的模块化系统的 vue + Webpack 项目,组件注册方式:(或者说是自定义组件)

  • component-one.vue
<template>
  <div>...</div>
</template>
  • index.vue
<template>
  <component-one></component-one>
</template>
<script>
  import ComponentOne from './component-one.vue'
  export default{
    components: {
      ComponentOne
    }
  }
</script>

注意的是

  1. 组件是可复用的 Vue 实例,也有data、methods、computed、watch等选项,但没有像el这样的根实例特有的选项;

  2. 其中的data必须是函数,将写在new创建的实例的data选项,以return的方式传递给该组件创建的实例;因为每用一次组件,都会创建出新的实例;每次使用该组件,都独立维护一份data数据,不能因某一个实例修改data而影响其他实例维护的data

  3. 每个组件必须有且只有一个根元素,如用一个template包裹这些子元素

全局注册

有些组件可以在很多页面中被使用,我们称其为“基础组件”。像这样的“基础组件”我们应该对它进行全局注册,不用在每次使用的时候都要引入和在component注册:

  • dialog.vue
<div>
  <el-dialog>
    <div>
      <!-- 父组件中自定义标签 -->
      <slot></slot>
    </div>
  </el-dialog>
</div>
<script>
  export default {
    name: 'comDialog',  //name为组件名称,全局注册必备的
    props: {
      dialogList: Object
    },
  }
</script>
  • main.js
/**
 * require.context()参数的意义
 * 参数1.文件目录
 * 参数2.是否查找子集
 * 参数3.查找规则
 */
const requireComponent = require.context('./', true, /\.vue$/)
const install = (Vue) => {
  if (install.installed) return // 如果组件被注册就返回,没有就注册
  install.installed = true
  requireComponent.keys().forEach(filename => { // filename 文件
    const config = requireComponent(filename) // 第i个组件
    const componentName = config.default.name // 组件名
    Vue.component(componentName, config.default || config) // 循环注册组件
  })
}

Vue.use(install)
  • 在任何页面都可以直接使用:
<com-dialog></com-dialog>

父子组件传参

父传子——props

  • index.vue
<template>
  <component-one :prop1="value1" :prop2="value2"></component-one>
</template>
<script>
  import ComponentOne from './component-one.vue'
  export default{
    components: {
      ComponentOne
    },
    data: {
      value1: ...,
      value2: ...,
    }
  }
</script>
  • component-one.vue
<template>
  <div>{{prop1}}</div>
  <div>{{prop2}}</div>
</template>
<script>
  export default{
    props: {
      prop1: { type:..., default:... },
      prop2: { type:..., default:... },
    }
  }
</script>

子传父——emit

  • component-one.vue
<template>
  <div @click="goBacktoIndex">{{value1}}</div>
  <div>{{value2}}</div>
</template>
<script>
  export default{
    props: {
      prop1: { type:..., default:... },
      prop2: { type:..., default:... },
    },
    methods:{
      goBacktoIndex(){
        this.emit("fromComponentOne",params)
      }
    }
  }
</script>
  • index.vue
<script>
  export default{
    methods:{
      fromComponentOne(res){
        console.log(res) // res 的值就是 component-one.vue 里面的 params 的值
      }
    }
  }
</script>

子组件是否每次打开都需要刷新

  • 需要刷新 法一:
<component-one v-if="showComponentOne"></component-one>

法二:

<component-one v-show="showComponentOne" :key="key"></component-one>
key = Math.random()
或
key = new Date().getTime()
  • 不需要刷新
<component-one v-show="showComponentOne"></component-one>

是否刷新关乎是否会触发生命周期函数 created、mounted 等

在父组件里修改子组件的内容——插槽

例如从组件库里引入一个模板组件,想对模板组件里面的内容进行自定义,那就需要插槽

官方文章-插槽

base-layout.vue 子组件

<div class="container"> 
  <header> <!-- 想在这里自定义页头内容 --> </header>
  <main> <!-- 想在这里自定义主要内容 --> </main>
  <footer> <!-- 想在这里自定义页脚内容 --> </footer>
</div>

vue 3.0 的处理方案是:

vue3.0 提供了slot元素,含有name属性的叫具名插槽,没有含name属性的叫匿名插槽,用来指定父组件中自定义内容所放的位置

base-layout.vue 子组件加入slot元素

<div class="container">
  <header>
    <slot name="header"></slot> <!-- 具名插槽 -->
  </header>
  <main>
    <slot></slot> <!-- 匿名插槽 -->
  </main>
  <footer>
    <slot name="footer"></slot> <!-- 具名插槽 -->
  </footer>
</div>
  • 具名插槽看name的值锁定自定义内容位置
  • 匿名插槽的话就“接纳”来自所有没有指定name值的自定义内容

index.vue 父组件用<template v-slot:插槽名称>包裹自定义内容

<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:插槽名称>包裹,被匿名插槽接纳 -->

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

最终渲染结果(slot元素被自定义内容替换)

<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>

父组件能在插槽区域用子组件的数据——作用域插槽

current-user.vue 子组件

<span>
  <slot name="userName">{{ user.lastName }}</slot>
</span>

index.vue 父组件,我们想把user.lastName替换成user.firstName

<current-user>
  <template v-slot:userName>
    {{ user.firstName }}
  </template>
</current-user>

但是这样处理是行不通的,因为只能在子组件中访问到数据user,解决方案是:

  1. 在子组件中使用 v-bind 绑定 user 数据
<span>
  <slot :user="user">{{ user.lastName }}</slot>
</span>
  1. 在父组件中自定义一个名字slotProps,通过slotProps.user来访问子组件中user数据
<current-user>
  <template v-slot:userName="slotProps">
    {{ slotProps.user.firstName }}
  </template>
</current-user>

v-slot 可以被缩写为 #(v-bind缩写成:,v-on缩写成@

另外,还可以动态绑定插槽名:

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

vue 2.0 的处理方法:

  • slot 属性做具名插槽
<base-layout>
  <h1 slot="header">Here might be a page title</h1> <!-- 具名插槽 -->

  <p>A paragraph for the main content.</p> <!-- 匿名插槽 -->
  <p>And another one.</p>

  <p slot="footer">Here's some contact info</p> <!-- 具名插槽 -->
</base-layout>

这里和 vue3.0 的区别在于,自定义内容不需要被<template v-slot:插槽名称>包裹,而是在自定义内容组件上直接加slot属性

  • slot-scope 属性做作用域插槽
<slot-example>
  <span slot="default" slot-scope="slotProps">
    {{ slotProps.msg }}
  </span>
</slot-example>

这里和 vue3.0 的区别在于,自定义内容不是必须被<template>包裹,而是可以是任意元素,还可以用 ES5 解构

<slot-example>
  <span slot="default" slot-scope="{ msg }">
    {{ msg }}
  </span>
</slot-example>

参考文章