阅读视图

发现新文章,点击刷新页面。

Vue3-全局组件 && 递归组件

1.全局组件

全局组件是在 main.ts 中一次性注册,之后就可以在项目中的任何组件模板内直接使用,无需 import

组件本身就是一个标准的 SFC (单文件组件),使用 <script setup lang="ts"> 编写。

举个栗子,button组件

<template>
  <button class="base-button">
    <slot></slot>
  </button>
</template>

<script setup lang="ts">
// 这里可以定义 props, emits 等
// 例如:
// defineProps<{ type: 'primary' | 'secondary' }>()
</script>

<style scoped>
.base-button {
  padding: 8px 16px;
  border: 1px solid #ccc;
  border-radius: 4px;
  cursor: pointer;
}
.base-button:hover {
  background-color: #f0f0f0;
}
</style>

在创建 Vue 实例后、挂载 (.mount()) 之前来注册它。

app.component('组件名', 组件对象)

// main.ts
import { createApp } from 'vue'
import App from './App.vue'

// 1. 导入要全局注册的组件
import BaseCard from './components/BaseCard.vue'
import BaseButton from './components/BaseButton.vue'
import SvgIcon from './components/SvgIcon.vue'

const app = createApp(App)

// 2. 使用 app.component() 进行全局注册
// app.component('组件名', 组件对象)
app.component('BaseCard', BaseCard)
app.component('BaseButton', BaseButton)
app.component('SvgIcon', SvgIcon)

// 3. 挂载应用
app.mount('#app')

注册后,在任何其他组件中都可以直接使用 <BaseButton>,无需导入。

<template>
  <div>
    <h1>欢迎!</h1>
    <BaseButton>点我</BaseButton>
    <BaseCard>
      <SvgIcon name="user" />
      <p>一些内容</p>
    </BaseCard>
  </div>
</template>

<script setup lang="ts">
// 无需 import BaseButton, BaseCard, SvgIcon
</script>
使用场景
  1. 基础 UI 组件: 这是最常见的场景。项目中使用频率极高的组件,例如 Button, Icon, Modal, Card, Input 等。通常会以 Base-App- 作为前缀,以示区分。
  2. UI 库集成: 当使用像 Element Plus, Naive UI 或 Vuetify 这样的库时,它们通常会提供一个 app.use(Library) 的方式,这背后其实就是全局注册了它们所有的组件。
  3. 布局组件: AppHeader, AppFooter, Sidebar 等几乎每个页面都会用到的布局框架。

⚠️ 注意: 全局注册会轻微增加应用的初始加载体积,因为所有全局组件都会被打包到主 chunk 中。因此,请对那些真正常用的组件使用全局注册,避免滥用。


2.递归组件

递归组件是指在其模板中调用自身的组件。

举个栗子:树形菜单

首先,定义数据结构 ( types.ts)

// types.ts
export interface TreeNodeData {
  id: string;
  label: string;
  children?: TreeNodeData[]; // 关键:children 数组的类型是它自身
}

然后,创建递归组件 (TreeNode.vue)

<template>
  <div class="tree-node">
    <div class="node-label">{{ node.label }}</div>
    <!-- 可选链操作符?.:如果 node.children 存在且有值,则渲染子节点列表,否则返回undefined,隐式转换为false -->
    <ul v-if="node.children && node.children.length > 0" class="children-list">
      <li v-for="child in node.children" :key="child.id">
        <!-- 递归 -->
        <TreeNode :node="child" />
      </li>
    </ul>
  </div>
</template>

<script setup lang="ts">
// 导入我们定义的类型
import type { TreeNodeData } from './types'

// 1. 定义 Props,接收父组件传递的数据
interface Props {
  node: TreeNodeData;
}
defineProps<Props>()

</script>

<style scoped>
.tree-node {
  margin-left: 20px;
}
.node-label {
  font-weight: bold;
}
.children-list {
  list-style-type: none;
  padding-left: 15px;
  border-left: 1px dashed #ccc;
}
</style>

在父组件中导入并渲染“根节点”即可。

<template>
  <div>
    <h1>文件结构树</h1>
    <TreeNode :node="fileTree" />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import TreeNode from './components/TreeNode.vue'
import type { TreeNodeData } from './components/types' // 确保路径正确

// 准备一个符合 TreeNodeData 结构的 TS 数据
const fileTree = ref<TreeNodeData>({
  id: 'root',
  label: '项目根目录 (src)',
  children: [
    {
      id: 'c1',
      label: 'components',
      children: [
        { id: 'c1-1', label: 'TreeNode.vue' },
        { id: 'c1-2', label: 'BaseButton.vue' },
      ],
    },
    {
      id: 'c2',
      label: 'views',
      children: [
        { id: 'c2-1', label: 'HomePage.vue' },
      ],
    },
    {
      id: 'c3',
      label: 'App.vue',
      // 这个节点没有 children,递归将在此处停止
    },
  ],
})
</script>
使用场景
  1. 树形结构: 任何需要展示层级关系的数据。例如:文件浏览器、组织架构图、导航菜单(尤其是多级下拉菜单)。
  2. 嵌套评论: 社交媒体或论坛中的评论区,一条评论可以有“回复”(子评论),子评论又可以有回复。
  3. JSON 格式化器: 展示一个 JSON 对象,如果某个值是对象或数组,就递归地调用组件来展示其内部。

参考文章

小满zs 学习Vue3 第十五章(全局组件,局部组件,递归组件)xiaoman.blog.csdn.net/article/det…

Vue3-父子组件通信

在 Vue 3 中,尤其是在使用 `` 并结合 TypeScript 时,父子组件之间的通信变得非常清晰和类型安全。

核心思想是:

  1. 父组件向子组件 (Prop down): 通过 Props 传递数据。
  2. 子组件向父组件 (Event up): 通过 Events (自定义事件) 发送消息。

1.父传子

  • 定义一些本地数据 (响应式引用 ref)。

  • 将这些数据通过 props 传递给子组件。

  • 监听子组件发出的 emits 事件,并定义一个处理函数来更新本地数据。


  <div class="parent-container">
    <h2>👋 我是父组件</h2>
    
    更新给子组件的消息: 
    

    <p>
      从子组件收到的消息: <strong>{{ messageFromChild }}</strong>
    </p>

    <hr/>

    
  </div>



// 1. 导入 ref 用于创建响应式数据
import { ref } from 'vue';
// 2. 导入子组件
import ChildComponent from './ChildComponent.vue';

// 3. 定义父组件的本地状态
const messageForChild = ref('你好,子组件!这是来自父组件的消息。');
const messageFromChild = ref('...等待子组件的消息...'); // 存放从子组件收到的消息

// 4. 定义一个函数,用于处理子组件触发的 'updateMessage' 事件
// TypeScript 在这里可以根据子组件的 emit 定义自动推断 'newMessage' 的类型为 string
const handleChildMessage = (newMessage: string) => {
  messageFromChild.value = newMessage;
}



.parent-container {
  border: 2px solid #34495e;
  background-color: #f0f8ff;
  padding: 20px;
  border-radius: 8px;
}
input {
  margin-left: 10px;
}

2.子接收数据,传递数据给父组件


  <div class="child-container">
    <h3>👶 我是子组件</h3>
    
    <p>父组件传来的消息 (mainMessage):</p>
    <blockquote>{{ props.mainMessage }}</blockquote>
    
    <p>父组件传来的可选数字 (optionalCount):</p>
    <blockquote>{{ props.optionalCount || '未提供' }}</blockquote>

    
      向父组件发送消息
    
  </div>



// 1. 接收 Props (父 -> 子)
// 使用泛型参数为 props 定义类型。
// 这是最推荐的 TS 语法。
const props = defineProps<{
  mainMessage: string;      // 必需的 prop
  optionalCount?: number;   // 可选的 prop (注意 '?' 符号)
}>()

// 2. 定义 Emits (子 -> 父)
// 使用泛型参数为 emit 定义事件和载荷类型。
const emit = defineEmits<{
  // (e: '事件名', 载荷1: 类型, 载荷2: 类型, ...): void
  (e: 'updateMessage', message: string): void;
}>()

// 3. 定义一个函数来触发 emit
const sendMessageToParent = () => {
  const dataToSend = `你好父组件,我更新了!(时间戳: ${Date.now()})`;
  
  // 4. 触发事件,将数据 (dataToSend) 发送回父组件
  // Vue 会查找父组件上绑定的 @updateMessage 监听器
  emit('updateMessage', dataToSend);
}



.child-container {
  border: 2px dashed #42b983;
  background-color: #f0fff4;
  padding: 15px;
  margin-top: 15px;
  border-radius: 8px;
}
blockquote {
  margin: 0;
  padding: 10px;
  background: #fff;
  border-left: 5px solid #ccc;
}

3.关键点总结

  • defineProps (子组件用于接收)

    • 在 `` 中,它是一个宏,无需导入。
    • 使用泛型 defineProps<{...}>() 来提供最严格的类型检查。
    • props 是单向数据流。子组件不应该直接修改 props 对象中的值。
  • defineEmits (子组件用于发送)

    • 同样是一个宏,无需导入。
    • 使用泛型 defineEmits<{...}>() 来定义事件签名。
    • 语法 (e: 'eventName', payload: Type): void 确保了当你调用 emit('eventName', ...) 时,payload 必须匹配定义的 Type
  • 模板中的绑定

    • 父组件使用冒号 : (即 v-bind) 来传递 props
    • 父组件使用艾特 @ (即 v-on) 来监听 emits 事件。
❌