普通视图

发现新文章,点击刷新页面。
昨天以前首页

从零到一:用 Vue3 + Kimi 大模型打造「拍照记单词」AI 应用

2026年5月6日 17:52

从零到一:用 Vue3 + Kimi 大模型打造「拍照记单词」AI 应用

本文适合有一定 Vue3 基础、想了解如何将大模型 API 集成到前端项目的开发者。完整项目已开源,文末附链接。

前言

在 AI 时代,"一个人的公司"(OPC)正在成为可能。本文将带你从零搭建一个 拍照记单词 的前端 AI 应用——用户拍一张照片,AI 自动识别图片内容并生成一个英文单词、例句和发音。

这个项目的核心价值在于:它不是一个 Demo,而是一个可以落地的产品原型。你会学到:

  • 如何用 Vue3 Composition API 组织复杂业务逻辑
  • 如何调用多模态大模型(Kimi Vision)解析图片
  • 如何集成 TTS 语音合成
  • 如何设计一个对用户友好的 Prompt

一、项目架构总览

vue3-ts-cameraword/
├── src/
│   ├── App.vue                 # 主页面,核心业务逻辑
│   ├── components/
│   │   └── PictureCard.vue     # 拍照卡片组件
│   ├── lib/
│   │   └── audio.ts            # TTS 语音合成模块
│   └── main.ts                 # 入口文件
├── .env.local                  # 环境变量(API Key 等)
└── vite.config.ts              # Vite 配置

技术栈:Vue3 + TypeScript + Vite + Kimi Vision API + 火山引擎 TTS


二、核心功能实现

2.1 图片上传:FileReader 的妙用

传统文件上传需要后端配合,但多模态大模型可以直接接收 Base64 编码的图片。我们用 FileReader 在前端完成图片转码:

// PictureCard.vue
const updateImageData = async (e: Event): Promise<any> => {
    const file = (e.target as HTMLInputElement).files?.[0];
    if (!file) return;

    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.readAsDataURL(file); // 转为 Base64
        reader.onload = () => {
            const data = reader.result as string;
            imgPreview.value = data;        // 本地预览
            emit('updateImage', data);      // 传给父组件
            resolve(data);
        };
        reader.onerror = (error) => reject(error);
    });
};

关键点:

  • readAsDataURL() 将文件转为 data:image/png;base64,... 格式的字符串
  • 这个字符串可以直接作为 <img>src 实现预览
  • 同时可以直接传给大模型的 image_url 字段

2.2 调用 Kimi Vision:多模态 API 实战

这是整个项目的核心。Kimi 的 moonshot-v1-8k-vision-preview 模型支持图片+文字的混合输入:

// App.vue
const update = async (imageDate: string) => {
    const endpoint = import.meta.env.VITE_KIMI_API_ENDPOINT + '/chat/completions';
    const headers = {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${import.meta.env.VITE_KIMI_API_KEY}`
    };

    word.value = '分析中...';

    const response = await fetch(endpoint, {
        method: 'POST',
        headers,
        body: JSON.stringify({
            model: 'moonshot-v1-8k-vision-preview',
            messages: [{
                role: 'user',
                content: [
                    {
                        type: 'image_url',
                        image_url: { url: imageDate }  // Base64 图片
                    },
                    {
                        type: 'text',
                        text: userPrompt                // 文字指令
                    }
                ]
            }],
            stream: false
        })
    });

    const data = await response.json();
    const replyData = JSON.parse(data.choices[0].message.content);
    // 处理返回数据...
};

这里的 content 是一个数组,可以同时包含图片和文字。这是多模态 API 的标准用法。

2.3 Prompt 设计:决定产品质量的关键

Prompt 是 AI 产品的灵魂。一个好的 Prompt 需要:

  1. 清晰的指令:告诉模型你要什么
  2. 明确的输出格式:JSON 格式便于前端解析
  3. 约束条件:限制词汇难度、输出长度等
const userPrompt = `
  分析图片内容,找出最能描述图片的一个英文单词,尽量选择更简单的A1~A2的词汇。

  返回JSON 数据:
  {
    "image_discription": "图片描述",
    "representative_word": "图片代表的英文单词",
    "example_sentence": "结合英文单词和图片描述,给出一个简单的例句",
    "explaination": "结合图片解释英文单词,段落以Look at ...开头,
                    将段落分句,每一句单独一行,
                    解释的最后给一个日常生活有关的问句",
    "explanation_replys": ["根据explaination给出的回复1",
                          "根据explaination给出的回复2"]
  }
`;

设计要点:

  • A1~A2 级别:控制词汇难度,适合初学者
  • JSON 格式OutputParser 的思想,让返回数据结构化,便于业务处理
  • Look at ... 开头:引导模型用"看图说话"的方式解释,更生动
  • 问句结尾:制造对话感,增强学习互动性

2.4 TTS 语音合成:让单词"说出来"

学英语离不开发音。我们集成火山引擎的 TTS 服务,将例句转为语音:

// lib/audio.ts
export const generateAudio = async (text: string) => {
    const endpoint = '/tts/api/v1/tts';
    const headers = {
        'Content-Type': 'application/json',
        'Authorization': `Bearer;${token}`
    };

    const payload = {
        app: { appid: appId, token, cluster: clusterId },
        user: { uid: 'bearbobo' },
        audio: {
            voice_type: voiceName,    // 音色:en_female_anna_mars_bigtts
            encoding: 'ogg_opus',     // 音频编码格式
            speed_ratio: 1.0,         // 语速
            emotion: 'happy',         // 情绪
        },
        request: {
            reqid: Math.random().toString(36).substring(7),
            text,                     // 要合成的文本
            text_type: 'plain',
            operation: 'query',
        },
    };

    const res = await fetch(endpoint, {
        method: 'POST',
        headers,
        body: JSON.stringify(payload)
    });

    const data = await res.json();
    return createBlobURL(data.data);  // 转为可播放的 URL
};

Base64 转 Blob URL 的工具函数:

function createBlobURL(base64AudioData: string): string {
    const byteArrays: number[] = [];
    const byteCharacters = atob(base64AudioData);  // 解码 Base64

    for (let offset = 0; offset < byteCharacters.length; offset++) {
        byteArrays.push(byteCharacters.charCodeAt(offset));
    }

    const audioBlob = new Blob([new Uint8Array(byteArrays)], {
        type: 'audio/mp3'
    });

    return URL.createObjectURL(audioBlob);  // 生成临时播放 URL
}

播放逻辑很简单:

// PictureCard.vue
const playAudio = () => {
    const audio = new Audio(props.audio);
    audio.play();
};

三、Vite 代理配置:解决跨域问题

前端直接调用第三方 API 会遇到跨域。用 Vite 的 server.proxy 解决:

// vite.config.ts
export default defineConfig({
    plugins: [vue()],
    server: {
        host: '0.0.0.0',           // 允许局域网访问
        proxy: {
            '/tts': {
                target: 'https://openspeech.bytedance.com',
                changeOrigin: true,
                rewrite: path => path.replace(/^\/tts/, ''),
            }
        },
    },
});
  • host: '0.0.0.0':让手机等设备也能访问开发服务器
  • /tts 代理:将 /tts/api/v1/tts 转发到火山引擎的 API

四、无障碍设计:被忽略的细节

这个项目有一个亮点:支持读屏器的无障碍访问

传统的 <input type="file"> 样式很难控制。我们的做法是:

<!-- 隐藏原生 input,用 label 触发 -->
<input type="file" id="selecteImage" class="input"
       accept="image/*" @change="updateImageData">
<label for="selecteImage" class="upload">
    <img :src="imgPreview" alt="camera" class="img"/>
</label>
.input {
    display: none;  /* 隐藏原生控件 */
}
  • for="selecteImage" 关联 id,点击 label 等同于点击 input
  • accept="image/*" 限制只能选择图片
  • 读屏器可以通过 label 的文本识别按钮用途

效果

image.png

image.png

五、项目总结与思考

学到了什么

  1. 多模态 API 的调用方式content 字段是数组,图片用 Base64 编码传入
  2. Prompt 工程:JSON 输出格式、难度约束、引导性描述
  3. 前端音频处理:Base64 → Blob → ObjectURL 的完整链路
  4. Vite 代理:一行配置解决跨域

可以改进的方向

  • 加入流式输出stream: true),让分析过程可视化
  • 增加单词本功能,收藏学过的单词
  • 接入语音识别,支持跟读打分
  • IndexedDB 本地存储学习记录

六、环境配置

创建 .env.local 文件:

VITE_KIMI_API_KEY=sk-xxxxx          # Kimi API Key
VITE_KIMI_API_ENDPOINT=https://api.moonshot.cn/v1

VITE_AUDIO_APP_ID=xxxxx             # 火山引擎 TTS 配置
VITE_AUDIO_ACCESS_TOKEN=xxxxx
VITE_AUDIO_CLUSTER_ID=volcano_tts
VITE_AUDIO_VOICE_NAME=en_female_anna_mars_bigtts

启动项目:

npm install
npm run dev

写在最后

这个项目虽然代码量不大,但覆盖了前端 AI 应用的核心链路:图片输入 → 多模态理解 → 结构化输出 → 语音合成

AI 时代,前端工程师的价值不只是写页面,更是用 AI 能力重新定义产品体验。希望这篇文章能给你一些启发。

项目地址:[project/capture_word /lesson_zp - 码云 - 开源中国] 欢迎 Star 和 PR!

TCP/IP 与前端性能:从数据包到首次渲染的底层逻辑

2026年5月4日 21:23

TCP/IP 与前端性能:从数据包到首次渲染的底层逻辑

这是“从 URL 到页面展示”系列第三篇。前面两篇我们聊完了浏览器导航DNS 与传输层细节,今天我们钻进 TCP/IP 协议栈的核心,看看一个数据包究竟怎么跑完全程,以及它为什么直接影响前端最关心的性能指标 FP(首次渲染时间)


一、前端性能的起点:FP 与 TTFB

面试官问“怎么优化页面加载速度”,你可以先说两个关键时间点:

  • TTFB(Time To First Byte):从发起请求到收到服务器第一个字节的耗时 = DNS 解析 + TCP/TLS 连接 + 服务器处理 + 响应传输的第一个字节到达。
  • FP(First Paint):从页面加载到浏览器首次绘制出像素的耗时 = TTFB + HTML 解析 + CSSOM 构建 + 渲染树构建 + 布局 + 首次绘制。

从这个公式可以看出:TTFB 里有一大块时间花在网络传输上,而网络传输的根基就是 TCP/IP。前端做性能优化,不能只盯着 JS 和 CSS,还得懂底层。


二、数据包的旅程:互联网的“快递系统”

互联网本质是一套理念和协议组成的体系架构,数据不是一整块丢进网线的,而是拆成一个一个数据包传输。

为什么拆包?

  • 单个文件可能几十 MB,一次性发送会长时间占用整条链路,其它请求就得排队。
  • 拆成小包后,可以利用带宽并发传输,提升传输效率和容错率。某个包丢了,只需要重发这一个,不用重发全部。

这些数据包最终会变成二进制数据帧,在物理介质上流动。


三、IP 层:只负责“送到”,不负责“送到位”

IP(Internet Protocol)是网络层的协议,职责非常单纯:根据 IP 地址,把数据包从源主机送到目标主机

它做的是“尽力而为”的服务:

  • 可能丢包
  • 可能出错
  • 可能不按顺序到达
  • 不提供任何纠正机制

所以 IP 本身是一个“不可靠”的协议。前端请求的 HTML 文件结构严密,一个字节错位都可能导致渲染异常,怎么办?

答案就在传输层。


四、UDP vs TCP:两种“快递模式”

传输层运行在 IP 之上,负责将数据包交付到目标主机上的具体应用(通过端口号)。主要有两位选手:

UDP(User Datagram Protocol):只管快

  • 不建立连接,直接发包。
  • 不保证顺序,不重传丢失的包。
  • 头部开销小,速度快。

适用场景:对实时性要求极高、能容忍少量数据丢失的音视频直播、视频通话、在线游戏。

TCP(Transmission Control Protocol):保证到位

对于 HTML、CSS、JS、图片这类 Web 资源,哪怕一个包出错都可能导致页面渲染异常。TCP 专门解决两个核心问题:

问题 TCP 的解法
数据包在传输过程中丢失 超时重传机制 — 每发一个包,启动一个计时器,过期未收到确认就重发
数据包到达接收端顺序错乱 序号机制 — 每个包都带有序号,接收端按序号重新组装

因为要保证可靠性,TCP 比 UDP 慢 —— 但这恰恰是 Web 页面需要的可靠传输


五、三次握手:建立可靠连接

在真正发送 HTTP 请求之前,TCP 需要通过三次握手建立连接。核心目的:同步初始序号,验证双方收发能力

简化过程:

  1. 客户端 → 服务器SYN,带上初始序号 J
    “我想和你建立连接,我发送的包从 J 开始编号,你听得到吗?”

  2. 服务器 → 客户端SYN + ACK,确认号 J+1,同时带上自己的初始序号 K
    “听到了,你下一个从 J+1 开始发。我也想和你建立连接,我的包从 K 开始编号,你听得到吗?”

  3. 客户端 → 服务器ACK,确认号 K+1
    “听到了,你下一个从 K+1 开始发。咱们可以正式开始传数据了。”

高频追问:为什么是三次,不是两次或四次?

  • 两次不够:服务器无法确认客户端能收到自己的消息(无法确认客户端有接收能力)。
  • 四次没必要:第二次握手时,服务器把 “响应客户端的 SYN”“发出自己的 SYN” 合并成一条消息,效率最大化。
    原则是:每一方的发送和接收能力都需要两次验证,但因为服务器把两个动作合并了,总共只需三次。

过程图

image.png

六、四次挥手:优雅地断开连接

数据传输完毕(比如 HTML 下载完成、图片加载结束),需要断开 TCP 连接释放资源。这个过程需要四次挥手

  1. A → BFIN,带上序号 M
    “我没有数据要发了,想断开连接。”

  2. B → AACK,确认号 M+1
    “知道你没数据了,但我可能还有数据没发完,你再等等。”

  3. B → AFIN,带上序号 N
    “我的数据也发完了,可以断开了。”

  4. A → BACK,确认号 N+1
    “好的,我知道你也没数据了。再见。”

为什么比握手多一次?因为 TCP 是全双工的,双方都可以独立发送和接收数据。一端说“我发完了”,另一端可能还有数据要传,所以 FIN 和 ACK 不能合并,必须分开发送,正好四次。

过程图

image.png


七、回到前端:这些对性能优化意味着什么?

理解了 TCP,就能看懂很多性能优化的底层逻辑:

优化手段 背后的 TCP 原理
减少 HTTP 请求数(雪碧图、合并文件) 每个 TCP 连接都有三次握手开销,请求数越少,握手成本越低
使用 HTTP/2 多路复用 单个 TCP 连接上并发传输多个请求和响应,避免重复握手
启用 TCP Fast Open 在握手阶段就开始传数据,将握手和数据传输部分重叠,降低 TTFB
使用 CDN 缩短物理距离 → 减少 RTT(往返时延)→ 丢包概率降低 → 重传少 → 更快
资源预连接(preconnect) 提前完成 DNS + TCP + TLS 握手,请求时直接使用已建立的连接

八、总结:一条链路串起知识体系

至此,我们串联起了整个前端性能链条的网络部分:

DNS 解析(IP 找到主机)
  → TCP 三次握手(建立可靠连接)
  → TLS 握手(加密安全)
  → HTTP 请求/响应(应用层数据)
  → TCP 四次挥手(断开连接)

每一个环节的耗时,都叠加进了 TTFB,进而影响 FP。下次面试问到性能优化,你完全可以从这个底层视角切入,展示你对网络协议栈的真懂,而不是只背“减少请求数”的表面答案。


从 URL 到页面展示,还有哪些你忽略的底层细节?(DNS 与传输篇)

2026年4月29日 21:43

从 URL 到页面展示,还有哪些你忽略的底层细节?(DNS 与传输篇)

上一篇文章我们用「浏览器多进程架构」为主线,拆解了从输入 URL 到页面渲染的完整导航过程。很多读者反馈:DNS 和传输层部分还值得再挖深一点 —— 面试时能不能多讲一些「IP 地址背后的事」?数据到底是怎么从服务器跑到浏览器的?

今天我们就顺着这条链路,把 DNS 的兜底逻辑负载均衡OSI 模型下的数据封包一次讲透,让你在面试中再往底层迈一步。


一、DNS 返回的 IP,并不是最终服务器的 IP

很多人以为 DNS 解析后直接拿到的是真实 Web 服务器的 IP,其实不是。

你可以亲自在 Chrome 地址栏输入 chrome://net-internals/#dns,就能看到浏览器缓存的 DNS 记录。查询一个大型网站,返回的经常是一个 IP 数组(多个地址),而不是单个 IP。

这就是分布式服务器集群所带来的现象。真正和浏览器通信的通常是一台 反向代理服务器(比如 Nginx),它充当“媒婆”的角色:

  • 请求先到这台 Nginx 代理
  • 代理背后有成百上千台真实业务服务器
  • 代理根据负载均衡策略,选择一台把请求转发过去

负载均衡怎么选服务器?

常见的策略有:

  • 轮询:按顺序一台一台分配
  • 加权轮询:配置高的机器多承担一些请求
  • 最少连接数:谁当前任务少就发给谁
  • IP 哈希:让同一用户的请求落到同一台服务器(便于 session 保持)

这样一来,即使某台服务器宕机,代理也能把流量导到健康节点,用户几乎无感知。


二、离你最近的 IP:CDN 的地域性调度

DNS 解析还有一个高级技能:根据你的地理位置,返回离你最近的节点 IP

很多大厂在全国(甚至全球)部署机房,DNS 解析服务会通过用户的 Local DNS 出口 IP 判断你的城市,然后返回附近机房的 Nginx 代理 IP。这就是 CDN 就近接入的底层逻辑。

比如在北京访问 douyin.com,DNS 可能解析到北京的边缘节点;到了上海出差,解析结果会变成上海的节点。不仅降低了延迟,也分散了源站压力。

小技巧:你可以用 nslookupping 测试域名在不同网络下的 IP,能看到 CDN 调度的效果。


三、本地 DNS 的“后门”:hosts 文件

在 DNS 查询链路中,有一个优先级很高却常被开发者忽略的环节:操作系统 hosts 文件

Windows 路径:

C:\Windows\System32\drivers\etc\hosts

macOS / Linux 路径:

/etc/hosts

它可以手动定义域名到 IP 的映射,比如:

127.0.0.1  douyin.com

这样访问 douyin.com 时,浏览器就直接走本地回环地址,完全跳过 DNS 解析

实际开发中的妙用

  • 本地开发时,将测试域名指向 127.0.0.1,就能用真实域名测试 cookie、token 等域名相关逻辑。
  • 线上故障应急时,有时会临时修改 hosts 跳过故障 DNS 或直奔某台服务器。

注意,localhost 这类特殊域名甚至不需解析,操作系统就直接识别成环回地址。


四、数据如何上路:OSI 七层模型形象理解

DNS 拿到 IP 之后,浏览器开始和服务器建立连接,真正传输数据。
这就进入到了经典的 OSI 七层模型(实际互联网更多用 TCP/IP 四层),从下往上看数据的变化:

  1. 物理层
    网线、光纤、无线电波。传输的最底层是 0 和 1 的电信号或光信号。

  2. 数据链路层
    数据被加上 MAC 地址(每台上网设备的唯一硬件标识),组成数据帧。

    目标 MAC + 源 MAC + 数据
    
  3. 网络层
    加上 IP 地址,让数据能够跨网络到达目标主机。

    目标 IP + 源 IP + MAC + 数据
    
  4. 传输层
    再加上 TCP 或 UDP 协议头。TCP 头部包含序号、确认号、窗口大小等,保证可靠性。

    TCP 头(序号…)+ IP + MAC + 数据
    

这就像寄快递:

  • 物理层是公路/飞机
  • 链路层是小区收发室(MAC)
  • 网络层是城市和街道(IP)
  • 传输层是快递公司的签收规则(保证不丢件、不乱序)

五、TCP:可靠传输的规矩

HTTP 协议基于 TCP,而 TCP 为了保证数据完整有序,制定了一套规矩:

1. 拆包与并发传输

服务器要返回一个 HTML 文件,可能几十 KB 甚至几百 KB。TCP 不会一次性全部扔到网上,而会切分成固定大小(MSS)的数据包,分批次、多通道并发发送。

这样即使某个包卡住了,其他包也能继续前进,提高效率。

2. 序号与排序

每个包在 TCP 头部都带着序号,接收端按序号重新拼装数据。
即使包到达的顺序是乱的(因为网络路由不同),也能重新排好。

3. 丢包重发

如果发送方一段时间没收到某个包的确认(ACK),就认为丢包,触发自动重传。这就是 TCP 可靠性的保障。

对比 UDP:UDP 不建立连接、不保证顺序、不重传,但速度快,适合直播、视频会议等对实时性要求高的场景。

4. 三次握手本质

我们在上一篇文章提过三次握手,其实它的核心就是同步双方的初始序号,确认彼此收发能力正常。只有握手完成后,浏览器才会发送真正的 HTTP 请求。


六、落地到面试:你能这样说

当面试官问起“DNS 过程中做了什么”,你可以这样组织语言:

  1. 浏览器先查本地缓存(含 chrome://net-internals/#dns 记录)和操作系统 hosts 文件。
  2. 没有命中,则逐级向上递归查询,直到拿到 IP(很可能是一个反向代理 IP)。
  3. 这个 IP 背后通常是 Nginx 等代理,代理根据负载均衡策略将请求转发到内部某台真实服务器。
  4. 同时 CDN 会通过 DNS 智能解析,返回离用户最近的边缘节点 IP。

问到数据传输,可以补充:

  • 物理层、链路层、网络层、传输层的逐层封包过程
  • TCP 通过拆包、序号、重传来保证可靠性,而 UDP 牺牲可靠性换取速度
  • 三次握手是为了同步序号、验证双方收发能力

这样,不仅讲清了前端视角的请求全过程,还向下扎到了网络架构和传输原理,能充分展示你的计算机基础。


掌握这些细节,你再回答那道经典面试题时,就不再是“表面流程复读机”,而是一个能讲出“为什么”和“底层发生了什么”的开发者。下一期我们可以继续聊聊 HTTPS 的 TLS 握手,敬请期待。

❌
❌