- 🌊 流式输出如何让AI像真人一样"打字"
- 🔗 Vue3响应式系统如何自动更新界面
- 🔐 API密钥如何在Vite项目中安全存储
- 🧩 组件化开发为什么是前端工程化的基石
- ⚛️
<script setup>语法糖如何简化组件开发
无论你是刚接触Vue的前端新手,还是想搞懂AI应用落地细节的后端大佬,这篇文章都能让你有所收获。
一、核心功能速览:这个Demo做了什么?

简单来说,这是一个调用DeepSeek大模型API的聊天界面,核心亮点是:
- 📝 双向数据绑定:输入框内容自动同步到JS变量
- 🎛️ 流式开关:一键切换"逐字输出"和"一次性返回"模式
- ⚡ 响应式更新:数据变化时,界面自动刷新(无需操作DOM)
- 🧩 组件化封装 :所有代码写在
.vue文件中,易于维护
生活类比:如果把获取AI回答比作"接水"------
- 非流式:等水龙头接满一桶水再端给你(等待时间长,但一次性拿到全部)
- 流式:直接接根管子到你杯子,水边流边喝(实时看到输出,体验更好)
二、解决什么痛点?为什么需要流式输出?
2.1 传统HTTP请求的"等待困境"
普通的API请求(如RESTful接口)通常是一次性返回完整数据。但对于LLM(大语言模型):
- 生成一个回答可能需要2-10秒(取决于问题复杂度和模型大小)
- 用户面对空白屏幕会焦虑(据统计,超过3秒的等待会让40%用户流失)
2.2 流式输出的"打字机"体验

核心优势:
- ⏱️ 首字节时间(TTFB)极短:用户几乎立即看到第一个字
- 😌 心理预期管理:看到AI在"思考并输出",减少焦虑
- 🔄 可中断性:如果回答不满意,用户可提前停止(未来可扩展)
三、重难点剖析(核心干货)
🔥 难点1:流式数据解析------ReadableStream完整揭秘
3.1 可选链操作符 ?. 的妙用
javascript
ini
const reader = response.body?.getReader();
这行代码看似简单,实则包含了两层关键操作:
第一步:?. 可选链操作符(Optional Chaining)
javascript
javascript
// ❌ 传统写法(危险)
const reader = response.body.getReader();
// 如果response.body是null或undefined,报错:
// "Cannot read properties of undefined (reading 'getReader')"
// ✅ 使用可选链(安全)
const reader = response.body?.getReader();
// 如果response.body存在 → 调用getReader(),返回读取器
// 如果response.body不存在 → 返回undefined,不会报错
// 等价于:
const reader = (response.body !== null && response.body !== undefined)
? response.body.getReader()
: undefined;
核心价值:在不确定对象属性是否存在时,安全地访问深层嵌套属性,避免程序崩溃。
第二步:getReader() 锁定流的含义
getReader() 是ReadableStream原型上的方法,作用是创建一个流读取器(Reader)并锁定该流。
javascript
ini
// 获取读取器后,流被锁定
const reader = response.body.getReader();
// 此时其他代码无法再获取读取器
const reader2 = response.body.getReader();
// ❌ 报错:TypeError: ReadableStream is locked
为什么需要锁定?
- 独占访问:保证数据按顺序被消费,不会出现多个读取器竞争
- 数据完整性:防止同一个chunk被多次读取
- 资源管理:流读取完毕后,自动释放资源
返回值 :ReadableStreamDefaultReader对象,提供了read()、cancel()等方法。
3.2 response.body 与 ReadableStream 的本质
当fetch收到stream:true的响应时,response.body是一个ReadableStream对象,它代表一个可读的字节流。这个流的特点是:
- 数据是分块(chunk)到达的,不是一次性全部到齐
- 每个chunk是Uint8Array(二进制数组),不是文本
- 数据到达顺序是确定的,但到达时间是未知的
javascript
arduino
console.log(response.body)
// 输出: ReadableStream {
// locked: false, // 当前未被锁定
// ...
// }
// 它不是一个普通对象,而是一个流式数据源
3.3 reader.read() 的返回值详解
每次调用await reader.read()都会返回一个Promise,解析后的对象包含:
javascript
javascript
const result = await reader.read();
console.log(result);
// 输出示例:
// {
// value: Uint8Array [100, 97, 116, 97, 58, 32, 123, ...],
// done: false
// }
返回值解析:
| 字段 | 类型 | 说明 | |
|---|---|---|---|
done |
boolean | true表示流已结束,false表示还有更多数据 |
|
value |
Uint8Array | undefined | 当前chunk的二进制数据。如果done为true则value为undefined |
关键理解:
每个chunk可能包含多条消息 ,也可能一条消息被拆成多个chunk。这就是为什么我们需要buffer机制!
3.4 数据转换全流程:从二进制到文字
详细数据转换过程:
text
css
┌─────────────────────────────────────────────────────────┐
│ 1. 网络传输(二进制字节流) │
│ [100, 97, 116, 97, 58, 32, 123, 34, 105, 100, ...] │
│ ↑ Uint8Array(二进制数组) │
└────────────────┬────────────────────────────────────────┘
│
│ decoder.decode(value, { stream: true })
▼
┌─────────────────────────────────────────────────────────┐
│ 2. 解码后(字符串) │
│ 'data: {"id":"1","delta":{"content":"你"}}' │
│ ↑ string(字符串) │
└────────────────┬────────────────────────────────────────┘
│
│ 字符串操作: trimmed.slice(6)
▼
┌─────────────────────────────────────────────────────────┐
│ 3. 纯 JSON 字符串 │
│ '{"id":"1","delta":{"content":"你"}}' │
│ ↑ 仍然是 string │
└────────────────┬────────────────────────────────────────┘
│
│ JSON.parse()(解析成对象)
▼
┌─────────────────────────────────────────────────────────┐
│ 4. JavaScript 对象 │
│ { │
│ id: "1", │
│ choices: [{ │
│ delta: { │
│ content: "你" │
│ } │
│ }] │
│ } │
│ ↑ Object(对象) │
└────────────────┬────────────────────────────────────────┘
│
│ 访问属性:data.choices[0].delta.content
▼
┌─────────────────────────────────────────────────────────┐
│ 5. 最终内容片段 │
│ '你' │
│ ↑ string(字符串) │
└─────────────────────────────────────────────────────────┘
3.5 Buffer机制:为什么它如此重要?
Buffer的核心作用 :处理跨chunk的不完整数据。
场景演示:
javascript
ini
// 假设服务器发送了两个chunk
// Chunk 1(部分): 'data: {"id":"1","delta":{'
// Chunk 2(剩余): '"content":"你"}}'
// ❌ 错误做法:直接解析每个chunk
const wrong = (chunk) => {
const line = chunk.trim();
if (line.startsWith('data: ')) {
const json = JSON.parse(line.slice(6)); // 报错!因为JSON不完整
}
};
// ✅ 正确做法:使用buffer缓存不完整数据
let buffer = '';
const correct = (chunk) => {
buffer += chunk; // 追加到buffer
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // 最后一行可能不完整,保留到下次
for (const line of lines) {
// 只有完整行才处理
const trimmed = line.trim();
if (trimmed.startsWith('data: ')) {
try {
const json = JSON.parse(trimmed.slice(6));
// 处理完整数据
} catch (e) {
// 万一解析失败,放回buffer
buffer = trimmed + '\n' + buffer;
}
}
}
};
Buffer的三大职责:
- 拼接跨chunk的数据:确保拿到完整的行
- 暂存不完整的JSON:等待下一个chunk补全
- 处理多字节字符:如中文UTF-8编码可能跨越多个chunk
3.6 TextDecoder的stream参数
javascript
arduino
const decoder = new TextDecoder();
buffer += decoder.decode(value, { stream: true });
stream: false(默认) :假设当前chunk是完整数据,直接解码stream: true:保留内部状态,正确处理跨chunk的多字节字符
举例 :中文字符"你"的UTF-8编码是[228, 189, 160],如果第一个chunk只包含[228, 189],第二个chunk包含[160]:
javascript
css
// ❌ 错误:stream:false 会乱码
decoder.decode(new Uint8Array([228, 189]), { stream: false });
// 输出: '�'(乱码)
// ✅ 正确:stream:true 会等待后续字节
decoder.decode(new Uint8Array([228, 189]), { stream: true });
// 输出: ''(暂不输出,等待完成)
decoder.decode(new Uint8Array([160]), { stream: true });
// 输出: '你'(完整字符)
3.7 完整实现代码
javascript
ini
// 流式处理核心代码(完整注释版)
const reader = response.body?.getReader();
if (!reader) {
content.value = '⚠️ 无法读取响应流';
return;
}
const decoder = new TextDecoder(); // 创建文本解码器
let buffer = ''; // 缓存不完整的行
let done = false;
while (!done) {
// 读取一个数据块(chunk)
const { done: doneReading, value } = await reader.read();
// doneReading: true 表示流已结束,没有更多数据
// value: Uint8Array 或 undefined(当done为true时)
if (doneReading) break;
// 关键:将二进制数据转换为文本
// stream: true 表示可能跨chunk,保留解码器内部状态
buffer += decoder.decode(value, { stream: true });
// 按换行符分割成多行
const lines = buffer.split('\n');
// 最后一行可能不完整(如 'data: {"id":"1","delta":{' 被截断)
// 保留到下一次循环继续拼接
buffer = lines.pop() || '';
// 处理每一行完整数据
for (const line of lines) {
const trimmed = line.trim();
// SSE(Server-Sent Events)格式:每行以 'data: ' 开头
if (!trimmed.startsWith('data: ')) continue;
// 提取实际数据部分(去掉 'data: ' 前缀)
const payload = trimmed.slice(6);
// 检查是否结束标志
if (payload === '[DONE]') {
done = true;
break;
}
try {
// 解析 JSON 字符串为 JavaScript 对象
const data = JSON.parse(payload);
// 提取内容片段(根据DeepSeek API响应格式)
const delta = data.choices[0].delta.content;
if (delta) {
// 累加内容,Vue响应式自动更新界面
content.value += delta;
}
} catch (error) {
// 如果JSON不完整(如被截断),放回buffer等待下次拼接
console.warn('解析失败,等待更多数据:', error);
buffer = trimmed + '\n' + buffer;
}
}
}
🔥 难点2:Vue3的 <script setup> 语法糖
2.1 什么是 <script setup>?
<script setup> 是 Vue 3.2 引入的编译时语法糖,用于简化 Composition API 的写法。
对比传统写法:
javascript
xml
// ❌ 传统写法(Vue 3.0)
<script>
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
const increment = () => count.value++
return { count, increment } // 必须手动返回
}
}
</script>
// ✅ `<script setup>` 写法(Vue 3.2+)
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => count.value++
// 无需return,所有顶层绑定自动暴露给模板
</script>
2.2 <script setup> 的核心特点
| 特性 | 说明 | 示例 |
|---|---|---|
| 自动暴露 | 顶层声明的变量、函数自动暴露给模板 | const msg = ref('hello') → 模板直接用{{msg}} |
| 模块化 | 支持import导入其他模块 |
import { useRouter } from 'vue-router' |
| 宏函数 | 编译器提供的特殊函数,在编译时处理 | defineProps、defineEmits、defineExpose |
| 性能优化 | 编译时优化,减少运行时开销 | 比普通setup()更高效 |
| 类型推断 | 更好的TypeScript支持 | 自动推断类型,无需额外声明 |
2.3 为什么使用 <script setup>?
1. 简洁性
javascript
csharp
// ❌ 传统写法:冗余的return
export default {
setup() {
const question = ref('')
const stream = ref(false)
const content = ref('')
const updata = async () => { /* ... */ }
return { question, stream, content, updata } // 必须列出所有
}
}
// ✅ setup语法糖:自动暴露
<script setup>
const question = ref('')
const stream = ref(false)
const content = ref('')
const updata = async () => { /* ... */ }
// 无需return,所有顶层声明自动可用
</script>
2. 组件通信更直观
javascript
xml
<script setup>
// 定义props(宏函数,无需导入)
const props = defineProps({
title: String,
count: Number
})
// 定义emits
const emit = defineEmits(['update', 'delete'])
// 暴露方法给父组件
defineExpose({
reset: () => { /* ... */ }
})
</script>
3. 编译时优化
<script setup>在编译时会被转换为标准的setup()函数- 编译器会进行静态提升 、缓存内联事件等优化
- 最终生成的代码比手写
setup()更高效
2.4 与普通 <script> 的区别

关键区别:
| 特性 | 普通 <script> |
<script setup> |
|---|---|---|
| 组件实例 | 完整的组件选项对象 | 编译后的setup函数 |
| 数据暴露 | 必须手动return | 自动暴露所有顶层声明 |
| 组件注册 | 需要在components中注册 | 导入即注册 |
| 生命周期 | 所有生命周期钩子 | 同Composition API |
| 适用场景 | 需要多个script块、options API | Composition API优先 |
🔥 难点3:Vue3响应式系统------ref vs reactive
3.1 响应式数据绑定原理
数据绑定(Data Binding) 是Vue的核心机制,确保数据状态 与界面显示 保持同步。关键区别:
| 特性 | 普通 <script> |
<script setup> |
|---|---|---|
| 组件实例 | 完整的组件选项对象 | 编译后的setup函数 |
| 数据暴露 | 必须手动return | 自动暴露所有顶层声明 |
| 组件注册 | 需要在components中注册 | 导入即注册 |
| 生命周期 | 所有生命周期钩子 | 同Composition API |
| 适用场景 | 需要多个script块、options API | Composition API优先 |
🔥 难点3:Vue3响应式系统------ref vs reactive
3.1 响应式数据绑定原理
数据绑定(Data Binding) 是Vue的核心机制,确保数据状态 与界面显示保持同步。

两种绑定方式:
vue
xml
<template>
<!-- 1. 单向数据绑定:数据 → 界面 -->
<div>{{ content }}</div>
<!-- 2. 双向数据绑定:数据 ↔ 界面 -->
<input v-model="question" />
<!-- 3. 属性绑定 -->
<input :value="question" @input="question = $event.target.value" />
</template>
3.2 ref 与 reactive 的区别
javascript
csharp
import { reactive, ref } from 'vue'
// ref:适用于基本类型
const question = ref('讲一个关于中国龙的故事') // string
const stream = ref(false) // boolean
const count = ref(0) // number
// reactive:适用于对象/数组
const state = reactive({
name: 'Vue',
version: 3,
features: ['响应式', '组件化']
})
// 访问方式不同
console.log(question.value) // ref需要.value
console.log(state.name) // reactive直接访问
对比表格:
| 特性 | ref |
reactive |
|---|---|---|
| 数据类型 | 任何类型(内部包装) | 对象、数组 |
| 访问方式 | .value访问 |
直接访问属性 |
| 模板解包 | 自动解包(无需.value) |
直接访问 |
| 重新赋值 | 支持整体替换 | 不支持整体替换 |
| 原理 | 通过RefImpl类包装 |
通过Proxy代理 |
3.3 响应式系统的底层原理

核心机制:
- 依赖收集 :模板中使用了
{{content}},Vue会记录"这个组件依赖content" - 派发更新 :当
content.value = '新值'时,Vue知道该组件需要重新渲染 - 虚拟DOM:通过diff算法最小化DOM操作
注意 :在<script setup>中,ref会自动解包,所以模板中直接写{{content}}而不是{{content.value}}。
3.4 为什么表单元素需要双向绑定?
表单元素(如<input>)是双向数据绑定的典型场景:

vue
xml
<template>
<!-- v-model = :value + @input -->
<input v-model="question" />
<!-- 等价于 -->
<input :value="question" @input="question = $event.target.value" />
</template>
<script setup>
const question = ref('') // 响应式数据
// 用户输入 → 自动更新question → 界面同步更新
</script>
单向绑定 vs 双向绑定:
| 特性 | 单向绑定 | 双向绑定 |
|---|---|---|
| 数据流向 | 数据→界面 | 数据↔界面 |
| 适用场景 | 纯展示 | 表单输入 |
| Vue语法 | {{}}、:prop |
v-model |
| 数据更新 | 手动触发 | 自动同步 |
🔥 难点4:环境变量安全------Vite的.env机制
设计者为什么这么写?
javascript
javascript
'Authorization': `Bearer ${import.meta.env.VITE_DEEPSEEK_API_KEY}`
安全问题:API密钥绝对不能硬编码在代码中(否则提交到GitHub就泄露了)。
Vite解决方案:
- 创建
.env.local文件(已被.gitignore忽略):
text
ini
VITE_DEEPSEEK_API_KEY=sk-xxxxxx
- 代码中通过
import.meta.env.VITE_XXX访问 3. Vite构建时会将环境变量编译进静态资源 (注意:仅以VITE_开头的变量会被暴露)
避坑指南
❌ 错误做法:
javascript
arduino
// 硬编码密钥(危险!)
const API_KEY = 'sk-1234567890abcdef'
❌ 另一个常见错误:
javascript
arduino
// 在服务端渲染(SSR)中误用
console.log(process.env.VITE_XXX) // Node环境没有import.meta.env
✅ 正确做法:
javascript
go
// 使用Vite提供的环境变量
const key = import.meta.env.VITE_DEEPSEEK_API_KEY
if (!key) {
console.error('请检查.env.local文件中的VITE_DEEPSEEK_API_KEY')
}
四、最佳实践:新手避坑指南
🚫 坑1:忘记处理空值
javascript
ini
// ❌ 错误:如果response.body为null,getReader()会报错
const reader = response.body.getReader();
// ✅ 正确:使用可选链
const reader = response.body?.getReader();
if (!reader) {
content.value = '请求失败,请重试';
return;
}
🚫 坑2:流式输出时内容被覆盖
javascript
ini
// ❌ 错误:每次更新都重新赋值,会丢失之前的内容
content.value = delta;
// ✅ 正确:追加内容
content.value += delta;
🚫 坑3:忘记处理DONE标志
如果没有检测到[DONE],while循环会一直执行,导致界面卡死。
javascript
ini
// ✅ 正确:遇到[DONE]立即退出
if (payload === '[DONE]') {
done = true;
break;
}
🚫 坑4:忽略buffer机制
javascript
javascript
// ❌ 错误:直接处理每个chunk,可能拿到不完整的数据
for (const line of chunk.split('\n')) {
JSON.parse(line); // 可能报错!
}
// ✅ 正确:使用buffer缓存不完整数据
let buffer = '';
// ... 参考上面的完整实现
🚫 坑5:API密钥提交到Git
bash
bash
# ✅ 正确:确保.gitignore包含
.env.local
.env.*.local
五、面试高频考点
Q1:Vue的响应式原理是什么?ref和reactive的区别?
回答要点:
- Vue2用
Object.defineProperty,Vue3用Proxy(更强大,支持数组、新增属性等) ref用于基本类型,通过.value访问;reactive用于对象- 两者都能触发视图更新,但
ref在模板中自动解包
Q2:response.body.getReader() 和 reader.read() 的原理?
回答要点:
-
response.body是ReadableStream对象,代表一个可读的字节流 -
getReader()创建流读取器,锁定流(只能一个读取器访问) -
reader.read()返回Promise,解析为{value, done}:value:Uint8Array(二进制数据块)done:boolean(流是否结束)
-
每次
read()返回一个chunk,需要循环调用直到done:true -
数据是二进制格式,需要用
TextDecoder解码为文本
Q3:为什么需要buffer机制?如何实现?
回答要点:
-
原因:网络传输中,一个完整的消息可能被拆分成多个chunk
-
buffer的作用:缓存不完整的数据,等待下次拼接完整后再处理
-
实现策略:
- 将每次解码后的文本追加到buffer
- 按
\n分割成行数组 - 最后一行可能不完整,放回buffer
- 只处理完整的行
-
异常处理 :如果
JSON.parse失败,说明数据仍不完整,放回buffer
Q4:TextDecoder的stream参数作用?
回答要点:
stream:true告诉解码器:数据可能不完整,保留内部状态- 正确处理多字节字符(如中文UTF-8编码可能是3个字节)
- 避免乱码(如"你"被拆成两个chunk时,
stream:true能正确组合)
Q5:<script setup> 和普通 <script> 的区别?
回答要点:
- 暴露方式 :
<script setup>自动暴露顶层声明,普通script需要手动return - 编译优化 :
<script setup>是编译时转换,性能更好 - 组件注册 :
<script setup>导入即注册,普通script需在components中注册 - 适用场景 :
<script setup>推荐用于Composition API,普通script用于Options API或特殊需求 - 宏函数 :
<script setup>支持defineProps、defineEmits等编译宏vue
xml
<template>
<!-- v-model = :value + @input -->
<input v-model="question" />
<!-- 等价于 -->
<input :value="question" @input="question = $event.target.value" />
</template>
<script setup>
const question = ref('') // 响应式数据
// 用户输入 → 自动更新question → 界面同步更新
</script>
单向绑定 vs 双向绑定:
| 特性 | 单向绑定 | 双向绑定 |
|---|---|---|
| 数据流向 | 数据→界面 | 数据↔界面 |
| 适用场景 | 纯展示 | 表单输入 |
| Vue语法 | {{}}、:prop |
v-model |
| 数据更新 | 手动触发 | 自动同步 |
🔥 难点4:环境变量安全------Vite的.env机制
设计者为什么这么写?
javascript
javascript
'Authorization': `Bearer ${import.meta.env.VITE_DEEPSEEK_API_KEY}`
安全问题:API密钥绝对不能硬编码在代码中(否则提交到GitHub就泄露了)。
Vite解决方案:
- 创建
.env.local文件(已被.gitignore忽略):
text
ini
VITE_DEEPSEEK_API_KEY=sk-xxxxxx
- 代码中通过
import.meta.env.VITE_XXX访问 3. Vite构建时会将环境变量编译进静态资源 (注意:仅以VITE_开头的变量会被暴露)
避坑指南
❌ 错误做法:
javascript
arduino
// 硬编码密钥(危险!)
const API_KEY = 'sk-1234567890abcdef'
❌ 另一个常见错误:
javascript
arduino
// 在服务端渲染(SSR)中误用
console.log(process.env.VITE_XXX) // Node环境没有import.meta.env
✅ 正确做法:
javascript
go
// 使用Vite提供的环境变量
const key = import.meta.env.VITE_DEEPSEEK_API_KEY
if (!key) {
console.error('请检查.env.local文件中的VITE_DEEPSEEK_API_KEY')
}
四、最佳实践:新手避坑指南
🚫 坑1:忘记处理空值
javascript
ini
// ❌ 错误:如果response.body为null,getReader()会报错
const reader = response.body.getReader();
// ✅ 正确:使用可选链
const reader = response.body?.getReader();
if (!reader) {
content.value = '请求失败,请重试';
return;
}
🚫 坑2:流式输出时内容被覆盖
javascript
ini
// ❌ 错误:每次更新都重新赋值,会丢失之前的内容
content.value = delta;
// ✅ 正确:追加内容
content.value += delta;
🚫 坑3:忘记处理DONE标志
如果没有检测到[DONE],while循环会一直执行,导致界面卡死。
javascript
ini
// ✅ 正确:遇到[DONE]立即退出
if (payload === '[DONE]') {
done = true;
break;
}
🚫 坑4:忽略buffer机制
javascript
javascript
// ❌ 错误:直接处理每个chunk,可能拿到不完整的数据
for (const line of chunk.split('\n')) {
JSON.parse(line); // 可能报错!
}
// ✅ 正确:使用buffer缓存不完整数据
let buffer = '';
// ... 参考上面的完整实现
🚫 坑5:API密钥提交到Git
bash
bash
# ✅ 正确:确保.gitignore包含
.env.local
.env.*.local
五、面试高频考点
Q1:Vue的响应式原理是什么?ref和reactive的区别?
回答要点:
- Vue2用
Object.defineProperty,Vue3用Proxy(更强大,支持数组、新增属性等) ref用于基本类型,通过.value访问;reactive用于对象- 两者都能触发视图更新,但
ref在模板中自动解包
Q2:response.body.getReader() 和 reader.read() 的原理?
回答要点:
-
response.body是ReadableStream对象,代表一个可读的字节流 -
getReader()创建流读取器,锁定流(只能一个读取器访问) -
reader.read()返回Promise,解析为{value, done}:value:Uint8Array(二进制数据块)done:boolean(流是否结束)
-
每次
read()返回一个chunk,需要循环调用直到done:true -
数据是二进制格式,需要用
TextDecoder解码为文本
Q3:为什么需要buffer机制?如何实现?
回答要点:
-
原因:网络传输中,一个完整的消息可能被拆分成多个chunk
-
buffer的作用:缓存不完整的数据,等待下次拼接完整后再处理
-
实现策略:
- 将每次解码后的文本追加到buffer
- 按
\n分割成行数组 - 最后一行可能不完整,放回buffer
- 只处理完整的行
-
异常处理 :如果
JSON.parse失败,说明数据仍不完整,放回buffer
Q4:TextDecoder的stream参数作用?
回答要点:
stream:true告诉解码器:数据可能不完整,保留内部状态- 正确处理多字节字符(如中文UTF-8编码可能是3个字节)
- 避免乱码(如"你"被拆成两个chunk时,
stream:true能正确组合)
Q5:<script setup> 和普通 <script> 的区别?
回答要点:
- 暴露方式 :
<script setup>自动暴露顶层声明,普通script需要手动return - 编译优化 :
<script setup>是编译时转换,性能更好 - 组件注册 :
<script setup>导入即注册,普通script需在components中注册 - 适用场景 :
<script setup>推荐用于Composition API,普通script用于Options API或特殊需求 - 宏函数 :
<script setup>支持defineProps、defineEmits等编译宏