vue组件通信

Vue 组件通信

1 前言

在 Vue 开发中,组件化是核心理念之一。页面被拆分为一个个组件,组件之间必然需要传递数据、同步状态。而父子组件通信是最基础、最常见的通信场景------子组件挂载在父组件内部渲染,两者存在天然的层级关系:

复制代码
# 父组件
Father.vue
│ # 子组件
└─Child.vue

Vue 生态提供了多种父子通信方案,从最基础的 props/emits,到全局状态管理 Pinia,各有适用场景。本文将逐一介绍 8 种方案,给出实际代码示例,并对它们的优缺点进行对比,最后解释为什么需要这么多种方案


2 props / emits

这是 Vue 最基础、最标准的父子通信方式,Vue 官方推荐优先使用。

  • 父 → 子 :通过 props 向子组件传递数据。
  • 子 → 父 :通过 emits 触发父组件绑定的事件。

2.1 代码示例

vue 复制代码
<!-- Father.vue -->
<template>
  <div>
    <h2>父组件</h2>
    <p>当前计数:{{ count }}</p>
    <!-- 
      引入子组件并建立双向通信:
         :value="count"  (Props 向下传) 
           - 使用 v-bind 语法,将父组件的 count 数据绑定到子组件的 value 属性上。
           - 子组件可以通过 defineProps 接收并使用这个值。
         
         @increment="handleIncrement"  (Events 向上发)
           - 使用 v-on 语法,监听子组件触发的自定义事件 'increment'。
           - 当子组件触发该事件时,自动执行父组件的 handleIncrement 方法。
    -->
    <Child :value="count" @increment="handleIncrement" />
  </div>
</template>

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const count = ref(0)

/**
 * 定义事件处理函数,用于接收子组件传上来的数据并更新父组件状态
 * @param {Number} step - 子组件通过 $emit 传递过来的增量参数(如 1 或 5)
 */
function handleIncrement(step) {
  count.value += step
}
</script>
vue 复制代码
<!-- Child.vue -->
<template>
  <div style="border: 1px solid #ccc; padding: 10px; margin-top: 10px">
    <h3>子组件</h3>
    <!-- 
         渲染从父组件接收到的 props 数据 
         这里的 value 对应下方 defineProps 中声明的属性
    -->
    <p>从父组件接收的值:{{ value }}</p>
    <!-- 
      按钮点击事件:通过 $emit 向父组件发送自定义事件
         - 第一个参数:事件名称(必须与父组件 @ 后面监听的名称一致)
         - 第二个参数:传递给父组件的数据(可选,这里传递了数字 1)
    -->
    <button @click="$emit('increment', 1)">+1</button>
    <button @click="$emit('increment', 5)">+5</button>
  </div>
</template>

<script setup>
/**
 * defineProps 宏:用于声明当前组件期望接收的 props
 *    - 在 <script setup> 中无需 import,直接使用即可
 *    - 这里声明了 value 属性,并指定了类型校验规则
 */
defineProps({
  value: { type: Number, required: true }
})

/**
 *  defineEmits 宏:用于声明当前组件会向外触发哪些自定义事件
 *    - 声明后,在模板中使用 $emit 或在 JS 中使用 emit() 时会有更好的类型提示
 *    - 这里声明了 'increment' 事件,告诉父组件:"我会发出这个事件,你可以监听"
 */
defineEmits(['increment'])
</script>

2.2 优缺点

优点 缺点
数据流向清晰(单向数据流),易于调试 仅适用于直接父子关系
Vue 官方推荐,生态兼容最好 嵌套深时 props 逐层传递非常繁琐
组件解耦程度高,子组件不依赖父组件具体实现 兄弟组件通信需要借助共同父组件中转

2.3 单向数据流设计思想

Vue 官方明确规定:所有 props 都遵循单向数据流(One-Way Data Flow)。子组件绝对不能直接修改父组件传下来的 prop。

这种设计主要出于以下几个核心考虑:

  1. 保证数据流向的清晰与可预测性(核心原因)

如果允许子组件直接修改父组件的数据,那么当你的应用变得复杂时,数据修改的源头就会变得极其难以追踪

  • 场景假设 :假设你有 5 个子组件都接收了父组件的 count。如果它们都能直接修改 count,当 count 突然发生变化时,你需要去这 5 个子组件里逐一排查到底是谁改的。
  • 单向数据流的好处:现在的数据流向是:父组件 -> 子组件(通过 props),子组件 -> 父组件(通过 emit)。数据永远只在父组件内部被修改,你只需要看父组件的代码,就能完全掌控数据是怎么变化的。
  1. 防止意外的"副作用"(避免牵一发而动全身)

子组件通常是作为"通用组件"存在的,它不应该知道父组件的具体业务逻辑。

  • 场景假设 :如果子组件内部直接修改了父组件的 count,而这个 count 同时也被父组件的其他子组件使用,或者触发了父组件的某个复杂计算属性。那么子组件的这一个小小改动,可能会在父组件中引发一连串意想不到的连锁反应(Bug)。
  • 通过 emit 的好处 :子组件只是发出一个"意图"(我想加 1),而**"是否真的加 1"以及"加 1 之后还要做什么"**,完全由父组件来决定。父组件可以在 handleIncrement 里加上各种校验逻辑,比如:"如果 count 超过 100,就不允许再加了"。
  • 子组件只负责展示和交互,它不关心父组件的数据叫什么名字,也不关心父组件拿到事件后怎么处理。这使得子组件变得高度纯粹和可复用。

3 v-model / emits

Vue 3 中 v-model 是对 props + emits 的语法糖,适用于表单类组件的双向绑定场景。

  • 父 → 子 :与 props 相同,Vue 3 支持多个 v-model 绑定。
  • 子 → 父 :通过 update:modelValue 事件更新父组件数据。

3.1 代码示例

vue 复制代码
<!-- Father.vue -->
<template>
  <div>
    <h2>父组件</h2>
    <p>姓名:{{ name }}</p>
    <p>年龄:{{ age }}</p>
     <!-- 
      1. 默认 v-model 语法糖:
         Vue 编译器会将其自动展开为:
         <NameInput :modelValue="name" @update:modelValue="name = $event" />
         - 向下传:把 name 的值传给子组件的 modelValue 属性
         - 向上绑:监听子组件发出的 update:modelValue 事件,并用新值更新 name
    -->
    <NameInput v-model="name" />
    <!-- 
      2. Vue 3 带参数的 v-model(多 v-model 绑定):
         Vue 编译器会将其自动展开为:
         <AgeInput :age="age" @update:age="age = $event" />
         - 向下传:把 age 的值传给子组件的 age 属性(不再是默认的 modelValue)
         - 向上绑:监听子组件发出的 update:age 事件,并用新值更新 age
    -->
    <AgeInput v-model:age="age" />
  </div>
</template>

<script setup>
import { ref } from 'vue'
import NameInput from './NameInput.vue'
import AgeInput from './AgeInput.vue'

const name = ref('张三')
const age = ref(25)
</script>
vue 复制代码
<!-- NameInput.vue -->
<template>
  <div>
    <h3>姓名输入</h3>
    <!-- 
      6. 手动实现双向绑定:
         - :value="modelValue":将父组件传来的 prop 绑定到 input 的显示值上
         - @input:监听用户的输入事件
         - $emit('update:modelValue', ...):触发父组件监听的更新事件,
           并将输入框的最新值 ($event.target.value) 传递给父组件
    -->
    <input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
      placeholder="请输入姓名"
    />
  </div>
</template>

<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
vue 复制代码
<!-- AgeInput.vue -->
<template>
  <div>
    <h3>年龄输入</h3>
    <!-- 
      9. 手动实现带参数的双向绑定:
         - :value="age":因为父组件用的是 v-model:age,所以这里的 prop 名称是 age
         - @input:监听输入事件
         - $emit('update:age', ...):触发 update:age 事件,
           注意这里使用了 Number() 将字符串强转为数字,保证数据类型与父组件一致
    -->
    <input
      type="number"
      :value="age"
      @input="$emit('update:age', Number($event.target.value))"
    />
  </div>
</template>

<script setup>
// 10. 声明接收的 prop 为 age(对应父组件 v-model:age 的参数名)
defineProps(['age'])
// 11. 声明触发的更新事件为 update:age(对应父组件 v-model:age 的约定事件名)
defineEmits(['update:age'])
</script>

3.2 优缺点

优点 缺点
语法简洁,v-model 一行搞定双向绑定 本质还是 props + emits,仅多了语法糖
Vue 3 支持多个 v-model,一个子组件可同时绑定多个值 不适合非表单类的复杂数据传递
自定义组件也能像原生表单元素一样使用 仍然是父子一对一通信

3.3 和props的区别

Props(单向数据流):数据只能从父组件流向子组件。子组件接收到 props 后,只能"看"和"用",绝对不能直接修改它14。如果子组件想改变数据,必须通过 $emit 触发事件,由父组件去修改11。

v-model(双向绑定):数据在父组件和子组件之间双向同步。当父组件的数据改变时,子组件的视图会自动更新;反之,当用户在子组件(如表单输入框)中修改了值,父组件的数据也会自动同步更新

Props:适用于单向展示或配置场景。比如父组件向子组件传递标题、列表数据、布尔值(控制显隐)等。

v-model:专为表单类控件设计。常用于 、、 等需要频繁与用户交互、且需要将用户输入的值实时同步到数据模型中的场景

复制代码
<script setup>
import { ref } from 'vue'
const username = ref('')
</script>

<template>
  <!-- 用户输入 "  admin  ",username 的值会自动变成 "admin" -->
  <input v-model.trim="username" placeholder="请输入用户名" />
  <p>当前用户名:[{{ username }}]</p>
</template>

底层原理 :Vue 会在每次更新数据前,自动调用字符串的 .trim() 方法,把首尾的空格、换行符等空白字符剔除。

4 ref / emits

通过 ref 获取子组件实例,父组件可以直接调用子组件暴露的方法或访问其数据。

  • 父 → 子:通过模板 ref 拿到子组件实例,调用其暴露的方法。
  • 子 → 父:子组件仍通过 emits 向父组件通知变化(或用 expose 暴露回调)。

4.1 代码示例

vue 复制代码
<!-- Father.vue -->
<template>
  <div>
    <h2>父组件</h2>
    <button @click="resetChild">重置子组件</button>
    <button @click="getChildData">获取子组件数据</button>
    <p>子组件消息:{{ childMsg }}</p>
    <Child ref="childRef" @submit="handleSubmit" />
  </div>
</template>

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const childRef = ref(null)
const childMsg = ref('')

function resetChild() {
  childRef.value.reset()
}

function getChildData() {
  childMsg.value = childRef.value.getFormData()
}

function handleSubmit(data) {
  console.log('子组件提交:', data)
}
</script>
vue 复制代码
<!-- Child.vue -->
<template>
  <div style="border: 1px solid #ccc; padding: 10px; margin-top: 10px">
    <h3>子组件</h3>
    <input v-model="inputVal" placeholder="说点什么..." />
    <button @click="submit">提交</button>
    <p>内部值:{{ inputVal }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const inputVal = ref('')
const emit = defineEmits(['submit'])

// 通过 defineExpose 暴露给父组件的方法和数据
function reset() {
  inputVal.value = ''
}

function getFormData() {
  return inputVal.value
}

function submit() {
  emit('submit', inputVal.value)
}

defineExpose({ reset, getFormData })
</script>

4.2 优缺点

优点 缺点
父组件可以精确控制子组件的行为(调用方法) 父组件与子组件强耦合,需要知道子组件的内部方法名
适合表单重置、聚焦等命令式操作 破坏了数据驱动的理念,可维护性下降
无需在父组件维护额外状态来同步 子组件对父组件不透明,重构成本高

5 provide / inject

适用于跨层级 的数据传递,祖辈组件通过 provide 提供数据,任意深度的后代组件通过 inject 获取,中间组件无需中转。

  • 父(祖)→ 子(孙) :通过 provide 下发数据。
  • 子(孙)→ 父(祖) :通过 provide 下发一个修改函数,后代调用该函数修改数据。

5.1 代码示例

vue 复制代码
<!-- Father.vue -->
<template>
  <div>
    <h2>父组件(祖先)</h2>
    <p>主题颜色:{{ theme }}</p>
    <p>用户信息:{{ user.name }},{{ user.age }}岁</p>
    <button @click="toggleTheme">切换主题</button>
    <Child />
  </div>
</template>

<script setup>
import { ref, provide, readonly } from 'vue'
import Child from './Child.vue'

const theme = ref('light')
const user = ref({ name: '李四', age: 30 })

function toggleTheme() {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}

function updateUser(data) {
  user.value = { ...user.value, ...data }
}

// provide 可传静态值、ref、响应式对象
provide('theme', readonly(theme))        // readonly 防止后代直接修改
provide('user', user)
provide('updateUser', updateUser)        // 提供修改函数,实现孙→祖通信
</script>
vue 复制代码
<!-- Child.vue(中间组件 - 不需要接收和传递任何 props) -->
<template>
  <div style="border: 1px solid #ccc; padding: 10px; margin-top: 10px">
    <h3>子组件(中间层)</h3>
    <p>主题:{{ theme }}</p>
    <GrandChild />
  </div>
</template>

<script setup>
import { inject } from 'vue'
import GrandChild from './GrandChild.vue'

const theme = inject('theme')
</script>
vue 复制代码
<!-- GrandChild.vue -->
<template>
  <div style="border: 1px dashed #ccc; padding: 10px; margin-top: 10px">
    <h3>孙子组件</h3>
    <p>主题:{{ theme }}</p>
    <p>用户:{{ user.name }},{{ user.age }}岁</p>
    <button @click="changeAge">修改年龄(孙→祖)</button>
  </div>
</template>

<script setup>
import { inject } from 'vue'

const theme = inject('theme')
const user = inject('user')
const updateUser = inject('updateUser')

function changeAge() {
  updateUser({ age: 35 })   // 孙子直接调用祖先提供的函数
}
</script>

5.2 优缺点

优点 缺点
跨任意层级传递,消除 prop drilling 数据来源不直观,调试困难(不知道谁 provide 的)
中间组件无需感知和转发 类型安全差,inject 的 key 是字符串,易拼写错误
适合全局配置(主题、语言、权限) 响应式数据被多处注入时,追踪变更来源困难
配合 Symbol 做 key 可避免命名冲突 滥用会使组件间的数据流变得隐晦

6 EventBus

EventBus 是一种发布-订阅模式 ,通过一个中央事件总线来实现任意组件间的通信。Vue 3 中官方已移除了 $on/$off/$emit 实例方法 ,需要借助第三方库(如 mitt)实现。

  • 任何组件都可以 emit 事件到总线上,也可以 on 监听总线上的事件。

6.1 代码示例

js 复制代码
// eventBus.js ------ 用 mitt 创建事件总线
import mitt from 'mitt'
export const bus = mitt()
vue 复制代码
<!-- Father.vue -->
<template>
  <div>
    <h2>父组件</h2>
    <p>最新通知:{{ notice }}</p>
    <ChildA />
    <ChildB />
  </div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { bus } from './eventBus'
import ChildA from './ChildA.vue'
import ChildB from './ChildB.vue'

const notice = ref('暂无')

// 通过回调方式传递参数
function handleNotice(text) {
  notice.value = text
}

onMounted(() => {
  bus.on('notice', handleNotice)
})

onBeforeUnmount(() => {
  bus.off('notice', handleNotice)  // 必须手动解绑,避免内存泄漏
})
</script>
vue 复制代码
<!-- ChildA.vue -->
<template>
  <div style="border: 1px solid #ccc; padding: 10px; margin-top: 10px">
    <h3>子组件 A</h3>
    <button @click="sendNotice('A 发来消息:任务完成')">发送通知</button>
  </div>
</template>

<script setup>
import { bus } from './eventBus'

function sendNotice(text) {
  bus.emit('notice', text)
}
</script>
vue 复制代码
<!-- ChildB.vue ------ 同样可以 emit 或 on -->
<template>
  <div style="border: 1px solid #ccc; padding: 10px; margin-top: 10px">
    <h3>子组件 B</h3>
    <button @click="sendNotice('B 发来消息:数据已更新')">发送通知</button>
  </div>
</template>

<script setup>
import { bus } from './eventBus'

function sendNotice(text) {
  bus.emit('notice', text)
}
</script>

6.2 优缺点

优点 缺点
任意组件之间直接通信,无需层级关系 事件名是字符串,容易拼写错误且无编译检查
使用简单,少量事件时非常灵活 大量使用后事件流向难以追踪,调试噩梦
组件完全解耦,不需要互相引用 必须手动在 onBeforeUnmount 中解绑,否则内存泄漏
适合跨兄弟组件、跨路由的简单通信 Vue 3 不再内置支持,需要额外依赖

7 Pinia

Pinia 是 Vue 3 官方推荐的新一代状态管理库,也是 Vuex 的精神续作。它放弃了 Mutation 概念,API 更简洁,对 TypeScript 支持极好。

  • 组件通过 useXxxStore() 获取 store 实例,直接读写状态或调用 actions。

7.1 代码示例

js 复制代码
// stores/counter.js ------ 使用 Pinia 定义的全局状态仓库
// 1. 从 pinia 中引入 defineStore 宏,从 vue 中引入响应式 API
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

/**
 * 2. 【核心】定义并导出 Store
 *    - 第一个参数 'counter':Store 的唯一 ID(类似数据库的主键)
 *    - 第二个参数:一个 Setup 函数(与 Vue 组件的 <script setup> 写法完全一致)
 */
export const useCounterStore = defineStore('counter', () => {
  
  // ========== State(状态) ==========
  // 3. 直接使用 ref 定义响应式变量,Pinia 会自动将其识别为全局状态
  const count = ref(0)

  // ========== Getter(获取器/派生状态) ==========
  // 4. 直接使用 computed 定义计算属性,当 count 变化时,double 会自动重新计算
  const double = computed(() => count.value * 2)

  // ========== Action(动作) ==========
  // 5. 定义普通的同步方法,【可以直接修改 state】,无需像 Vuex 那样提交 Mutation
  function increment(n = 1) { 
    count.value += n 
  }
  
  function decrement(n = 1) { 
    count.value -= n 
  }

  // 6. 定义异步方法(如请求接口获取数据)
  // 在 Pinia 中,异步操作和同步修改都放在 Action 中,逻辑更加内聚
  async function fetchAndSet() {
    const res = await fetch('/api/count').then(r => r.json())
    count.value = res.value // 直接赋值,Pinia 会自动追踪并触发视图更新
  }

  // 7. 【必须】将状态、计算属性和方法统一 return 出去,供外部组件使用
  return { count, double, increment, decrement, fetchAndSet }
})
vue 复制代码
<!-- Father.vue (父组件 - 布局与数据读取) -->
<template>
  <div>
    <h2>父组件</h2>
    <!-- 
      8. 直接在模板中通过 counter.xxx 读取全局状态和计算属性
         因为 Pinia 的状态是完全响应式的,所以这里的展示会自动更新
    -->
    <p>计数:{{ counter.count }} | 翻倍:{{ counter.double }}</p>
    
    <!-- 引入子组件,无需传递任何 props -->
    <CounterControl />
    <CounterDisplay />
  </div>
</template>

<script setup>
// 9. 导入刚才定义的 Store Hook 函数
import { useCounterStore } from './stores/counter'
import CounterControl from './CounterControl.vue'
import CounterDisplay from './CounterDisplay.vue'

// 10. 【核心】调用 Hook 获取全局 Store 实例
// 任何组件只要调用这个函数,拿到的都是同一个单例对象
const counter = useCounterStore()
</script>
vue 复制代码
<!-- CounterControl.vue(子组件 - 数据的修改者) -->
<template>
  <div style="border: 1px solid #ccc; padding: 10px; margin-top: 10px">
    <h3>计数器控制(子组件)</h3>
    
    <!-- 
      11. 直接调用 Store 暴露的方法来修改全局数据
    -->
    <button @click="counter.increment()">+1</button>
    <button @click="counter.decrement()">-1</button>
    <button @click="counter.increment(5)">+5</button>
  </div>
</template>

<script setup>
// 12. 导入并实例化 Store
import { useCounterStore } from './stores/counter'
const counter = useCounterStore()
</script>
vue 复制代码
<!-- CounterDisplay.vue(子组件 - 数据的消费者) -->
<template>
  <div style="border: 1px solid #ccc; padding: 10px; margin-top: 10px">
    <h3>计数器展示(子组件)</h3>
    
    <!-- 
      13. 读取全局状态
          当 CounterControl 组件中调用了 increment 方法后,
          因为底层是 Vue 的响应式系统,这里的视图会自动、实时地刷新
    -->
    <p>当前值:{{ counter.count }}</p>
    <p>翻倍值:{{ counter.double }}</p>
  </div>
</template>

<script setup>
// 14. 导入并实例化 Store(与父组件和控制组件拿到的是同一个实例)
import { useCounterStore } from './stores/counter'
const counter = useCounterStore()
</script>

7.2 优缺点

  • 选择 Pinia :当你需要共享和管理数据时(例如:购物车数据、用户登录状态、全局主题配置、复杂的表单状态)。你需要知道数据"现在是什么"以及"它是怎么变成这样的"。
  • 选择 EventBus :当你只需要发送一个动作或通知,且不需要关心数据状态时(例如:全局消息提示、触发某个不相关的组件刷新、关闭全局弹窗)。你只关心"发生了一件事",不关心"数据变成了什么"。

一句话比喻

Pinia 就像是公司的中央数据库和规章制度 ,所有数据的存取都要留痕;

EventBus 就像是公司里的大喇叭广播,喊一嗓子通知大家有事发生,但广播完就散了,没人记录。


8 为什么会有这么多种方案?

看到这里,你可能会困惑:不就是父子传个数据吗,为什么要搞出 8 种方案?

答案是:不同的场景需要不同的复杂度。没有银弹,只有最合适的选择。

8.1 各方案的定位

方案 适用场景
props / emits 直接父子关系,数据流简单明确
v-model 表单类组件,需要双向绑定
ref / emits 父组件需要命令式调用子组件方法
provide / inject 跨层级传递(主题、语言、用户信息)
EventBus 任意组件间的松耦合通信
Pinia 新项目首选状态管理

8.2 选择原则

  1. 能用 props/emits 解决的,绝不上状态管理库。 这是 Vue 的核心设计,简单、直观、可预测。
  2. 跨层级不超过 2 层,仍然用 props 透传。 如果只是爷爷传孙子,透传一次完全可以接受。
  3. 跨 3 层以上,或数据被多处消费,考虑 provide/inject。 比如全局主题、国际化、用户权限。
  4. 需要共享的状态是"应用级别"的(如购物车、登录态),上 Pinia。 它给你 devtools、持久化、模块化,这些不是"多余"的,而是大项目必需的保障。

8.3 核心矛盾:灵活 vs 可控

这 8 种方案本质上是在灵活性可控性之间做权衡:

  • props/emits:灵活性最低,但数据流最清晰,谁传的、谁改的一目了然。
  • **EventBus **:灵活性最高,任意组件都能通信,但数据流完全失控,调试困难。
  • Pinia:在灵活和可控之间找到了工程化的平衡,有规范约束,但不至于繁琐。
相关推荐
mCell1 小时前
AI 时代 SVG 画图的潜力
前端·agent·svg
我命由我123457 小时前
CesiumJS 笔记 - 获取容器中心点、Cartesian3 clone 方法、修改 Cartesian3 对象的高度
前端·javascript·css·前端框架·html·html5·js
Hopebearer_8 小时前
页面突然只剩 DOM?一次静态资源版本错配排查
前端·部署
抱抱宝8 小时前
Agent-study项目教程(03):手写 Mini-ReAct Agent(不依赖框架)
javascript·人工智能·gpt·react.js·prompt·agent
东风破_8 小时前
ESLint 是什么?为什么你的项目需要它?
前端·后端·代码规范
用户938515635078 小时前
Next.js 笔记系统(二):Redis 数据服务与侧边栏组件拆分实战
javascript·全栈
java1234_小锋10 小时前
Vue3专题 - 组件基础
vue.js·vite
圣殿骑士-Khtangc10 小时前
Go字符串高效拼接性能对比与底层原理分析
服务器·前端·golang
BigTopOne11 小时前
【ijkplayer】 硬解码流程
前端
kyriewen11 小时前
DeepSeek Harness开源第一天我就上手了——和Claude Code的差距比想象中大
前端·ai编程·deepseek