v-model、ref、stream…这些词不再可怕!一个 App.vue 让你全搞懂

52 阅读3分钟

📄 先整体看一下这个文件长啥样(结构)

<script setup>
  // 这里写逻辑(大脑)
</script>

<template>
  <!-- 这里写界面(脸) -->
</template>

<style scoped>
  /* 这里写样式(衣服) */
</style>

就像一个人:有脑子、有脸、有衣服。我们一个一个来!


1️⃣ <script setup> —— “大脑”部分

💡 <script setup> 是 Vue 3 的一种写法,意思是:“我这个组件的逻辑从这里开始”。

🔹 第一行:引入工具

import { ref } from 'vue'
  • 白话:我要用 Vue 提供的一个叫 ref 的小工具。
  • 作用:用来创建“会自动通知页面更新的小本本”。后面你会看到。

🔹 声明三个“小本本”(响应式变量)

// 用户输入的问题,默认已经写好一句话
const question = ref('讲一个喜洋洋和灰太狼的故事,20字')

// 控制是否“慢慢显示答案”,默认打开(true = 打开)
const stream = ref(true)

// AI 回答的内容,一开始是空的
const content = ref("")
  • ref() 是什么?
    想象你有一张便利贴:

    • 普通变量:你写了字,别人看不见你改了没。
    • ref 变量:你一改字,旁边的人(比如网页)立刻知道:“哇!他改了!快更新画面!”
  • 所以这三个变量,一变,页面就会自动刷新对应内容


🔹 核心功能:askLLM() —— “去问AI”

const askLLM = async () => {
  • 白话:定义一个叫 askLLM 的动作,意思是“去问大聪明(AI)”。
  • async 表示:这个动作要花时间(比如打电话、等回信),别卡住其他事。

✅ 步骤1:检查问题是不是空的
  if (!question.value) {
    console.log('question 不能为空');
    return
  }
  • 解释

    • question.value:因为 questionref,所以要用 .value 才能拿到真实内容。
    • 如果你啥也没写就点“提交”,它会说“问题不能为空”,然后不干了。

✅ 步骤2:先显示“思考中...”
  content.value = '思考中...';
  • 用户体验:让你知道它没卡死,正在努力干活!

✅ 步骤3:准备打电话给 AI
  const endpoint = 'https://api.deepseek.com/chat/completions';
  const headers = {
    'Authorization': `Bearer ${import.meta.env.VITE_DEEPSEEK_API_KEY}`,
    'Content-Type': 'application/json'
  }
  • endpoint:AI 的电话号码(API 地址)。

  • headers:打电话时要说的“暗号”:

    • Authorization:你的身份证明(API 密钥),藏在环境变量里。

      • VITE_DEEPSEEK_API_KEY:这是你在项目根目录 .env 文件里写的密钥,比如:

        VITE_DEEPSEEK_API_KEY=sk-xxxxxxxxxxxx
        
      • VITE_ 前缀,是 Vite 框架的安全规则——只有这样前端才能读到。

    • Content-Type:告诉对方:“我说的是 JSON 格式的话”。


✅ 步骤4:拨打电话(发送请求)
  const response = await fetch(endpoint, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      model: 'deepseek-chat',
      stream: stream.value,
      messages: [
        { role: 'user', content: question.value }
      ]
    })
  })
  • 白话翻译

    “喂,DeepSeek 吗?我是小盒子。
    我想用 deepseek-chat 这个模型,
    用户问的是:‘xxx’,
    要不要流式回答?(看 stream 开关)”

  • fetch:浏览器自带的“打电话”函数。

  • await:等电话打通、对方回完再继续。


🔹 步骤5:处理 AI 的回答(分两种情况)


🟢 情况A:流式回答(Streaming 打勾 ✅)
  if (stream.value) {
    // 先清空之前的答案
    content.value = "";

    // 准备“收音机”:用来一段一段听 AI 说话
    const reader = response.body?.getReader();   // 获取数据流读取器
    const decoder = new TextDecoder();           // 把二进制转成文字
    let done = false;                            // 标记是否说完
    let buffer = '';                             // 临时拼图板(防断包)

⚠️ 网络传输可能把一句话拆成几段,比如:

  • 第一段收到:data: {"delta": "灰太"
  • 第二段才收到:狼追羊"} 所以需要 buffer 先存着不完整的部分。
🔁 循环收听 AI 说话:
    while(!done) {
      // 读一段数据(可能是几个字,也可能是一整句)
      const { value, done: doneReading } = await reader?.read()
      done = doneReading;

      // 把二进制转成字符串,并和上一次没拼完的接起来
      const chunkValue = buffer + decoder.decode(value);
      buffer = ''; // 清空临时板

      // 按换行符拆开,只保留以 "data: " 开头的行(SSE 格式)
      const lines = chunkValue.split('\n').filter(line => line.startsWith('data: '))

      // 逐行处理
      for (const line of lines) {
        const incoming = line.slice(6); // 去掉开头的 "data: "

        // 如果看到 [DONE],说明说完了
        if (incoming === '[DONE]') {
          done = true;
          break;
        }

        try {
          // 尝试把这一行变成“真正的数据”
          const data = JSON.parse(incoming);
          const delta = data.choices[0].delta.content; // AI 这次说了啥

          if (delta) {
            content.value += delta; // 把新字加到答案板上(页面自动更新!)
          }
        } catch(err) {
          // 如果这行不完整(比如只收到一半),先存回 buffer,等下一段
          buffer += `data: ${incoming}`;
        }
      }
    }
  }

关键点

  • content.value += delta:每收到一个字,就贴到答案板上 → 页面实时显示!
  • buffer 防止网络断包导致乱码。

🔴 情况B:非流式(Streaming 不打勾 ❌)
  else {
    const data = await response.json();
    console.log(data);
    content.value = data.choices[0].message.content;
  }
  • 白话:等 AI 想好整句话,一次性拿回来,直接贴到答案板。
  • 简单粗暴,适合短回答。

2️⃣ <template> —— “脸”部分(你看到的界面)

<template>
  <div class="container">
    <!-- 输入区域 -->
    <div>
      <label>输入:</label>
      <input class="input" v-model="question"/>
      <button @click="askLLM">提交</button>
    </div>

    <!-- 输出区域 -->
    <div class="output">
      <div>
        <label>Streaming</label>
        <input type="checkbox" v-model="stream" />
        <div>{{content}}</div>
      </div>
    </div>
  </div>
</template>

🔍 关键指令解释:

代码白话解释
v-model="question"输入框和 question 小本本双向绑定——你打字,它自动记;它改值,输入框也变
@click="askLLM"点按钮就执行 askLLM 这个动作(去问AI)
v-model="stream"checkbox 和 stream 开关同步——打勾 = true,不打 = false
{{content}}content 小本本的内容显示在这里(自动更新!)

3️⃣ <style scoped> —— “衣服”部分(样式)

<style scoped>
* { margin: 0; padding: 0; }

.container {
  display: flex;
  flex-direction: column;
  align-items: start;
  justify-content: start;
  height: 100vh;
  font-size: 0.85rem;
}

.input { width: 200px; }

button {
  padding: 0 10px;
  margin-left: 6px;
}

.output {
  margin-top: 10px;
  min-height: 300px;
  width: 100%;
  text-align: left;
}
</style>
  • scoped:这些样式只在这个组件生效,不会影响别的页面。

  • 简单布局

    • 整体垂直排列(flex-direction: column
    • 输入框宽 200 像素
    • 按钮左边留点空隙
    • 输出区至少高 300 像素,方便看长答案

✅ 最后总结:整个流程串起来

  1. 你打开页面 → question 默认有字,stream 打开,content 为空

  2. 你改输入框 → question 自动更新(v-model

  3. 你点“提交” → 执行 askLLM()

  4. 它检查问题 → 显示“思考中...” → 给 DeepSeek 打电话

  5. 根据 stream 开关:

    • ✅ 打勾:AI 一个字一个字回,网页实时拼出来
    • ❌ 不打勾:AI 想好后一次性回,网页直接显示
  6. content 一变 → 页面自动更新(Vue 响应式魔法)


🎯 你现在可以做到

  • 看懂每一行代码是干啥的
  • 知道怎么改默认问题
  • 知道怎么开关流式模式
  • 知道 API 密钥放哪安全

不惧机器聪明,只怕人心懈怠~~~~