资讯动态

浏览器原生 Web Speech API 实战:React 语音转文字应用完整拆解

发布时间:2026/10/17 11:50:04 来源:尧图企业网站定制
简介这是一份面向前端开发者与React初学者的实战项目源码聚焦语音转文字场景通过调用浏览器内置API实现语音识别与文本输出适合想练习React组件化开发、熟悉脚手架工程配置的初中级开发者参考。压缩包共17个文件约117KB以JavaScript源码为主包含核心识别组件与入口文件辅以JSON配置、CSS样式、Markdown说明及SVG、HTML等静态资源结构紧凑、依赖清晰。项目基于引导式脚手架搭建涵盖自动格式化、代码分割、样式预处理、环境变量、代理配置与路由等常见工程化主题README中整理了从安装依赖到与后端集成的完整指引。目前已有672人学习下载读者可借此理解语音识别组件的实现思路、掌握React项目目录组织方式并对照文档完成本地运行与二次开发快速积累可复用的前端实践案例。1. 浏览器原生 API 做语音转文字一个 React 应用的完整拆解有个需求我最近在几个项目里反复碰到用户对着麦克风说话页面上实时把文字吐出来。第一反应可能是接第三方云服务但有个更轻的路子——直接用浏览器内置的 Web Speech API零后端、零费用、零密钥。这份资源就是一个用 React 搭的语音转文字应用核心逻辑全部跑在浏览器里不依赖任何外部服务。它适合两类人一类是想快速给自己的工具加语音输入的前端开发者另一类是正在学 React 状态管理和浏览器 API 配合的新手。整个项目体量不大但麻雀虽小从语音识别实例的创建、状态同步到错误处理该有的链路都齐了。下面我按“能干什么 → 怎么跑 → 怎么改”的顺序把这份源码拆开讲一遍。2. 技术选型与项目结构为什么是 Web Speech API 而不是云服务2.1 浏览器原生识别的能力边界Web Speech API 里的SpeechRecognition接口是浏览器直接调用系统级语音识别引擎的入口。Chrome 和 Edge 支持得比较好Safari 从 14.1 开始也跟上了Firefox 默认没开。它的工作方式是浏览器把麦克风采集的音频送到识别引擎引擎返回文本结果整个过程对前端来说就是一个事件驱动的对象。选它而不是云服务理由很直接不需要后端中转不需要申请 API Key没有网络请求延迟用户隐私数据不出浏览器。代价是识别精度依赖系统引擎中文识别在 Chrome 上走的是系统语音服务效果因设备而异。常见做法是把它当作“够用就好”的方案对精度要求极高的场景再考虑云端。这个项目里识别逻辑被封装在一个自定义 Hook 里组件层只负责渲染和交互。这种拆分方式让识别逻辑可以独立测试也方便后续替换成别的识别方案。2.2 目录结构与模块职责项目基于 Create React App 或 Vite 搭建目录结构大致如下src/ ├── App.jsx # 根组件组合 UI 和识别逻辑 ├── hooks/ │ └── useSpeechRecognition.js # 封装 SpeechRecognition 的核心 Hook ├── components/ │ ├── MicButton.jsx # 麦克风按钮控制开始/停止 │ └── Transcript.jsx # 展示识别结果 ├── App.css # 样式 └── index.js # 入口useSpeechRecognition.js是整个项目的心脏它管理识别实例的生命周期、监听识别事件、把结果同步到 React 状态。App.jsx负责把这些状态和回调串起来。MicButton和Transcript是纯展示组件不碰识别逻辑。这种结构的价值在于如果哪天要把 Web Speech API 换成别的方案只需要改 Hook 内部实现组件层完全不用动。我一般会在 Hook 里把isListening、transcript、error这几个状态暴露出去组件按需消费。2.3 核心依赖与运行环境项目依赖很干净除了 React 本身没有额外的语音处理库。package.json里主要就是react、react-dom、react-scripts或vite。这意味着npm install不会拉一堆东西装完就能跑。运行环境有两个硬性要求一是浏览器必须支持SpeechRecognition或webkitSpeechRecognition二是页面必须运行在 HTTPS 或 localhost 下否则浏览器不会授予麦克风权限。本地开发用localhost没问题部署到线上必须配 HTTPS。注意SpeechRecognition在 Chrome 里需要加webkit前缀代码里要做兼容判断不能直接new SpeechRecognition()。3. 核心 Hook 拆解从创建识别实例到状态同步3.1 识别实例的创建与配置Hook 的第一件事是拿到浏览器提供的构造函数并创建一个实例。这段代码是整个应用的起点// useSpeechRecognition.js import { useState, useRef, useCallback, useEffect } from react; const SpeechRecognition window.SpeechRecognition || window.webkitSpeechRecognition; export function useSpeechRecognition() { const [isListening, setIsListening] useState(false); const [transcript, setTranscript] useState(); const [error, setError] useState(null); const recognitionRef useRef(null); // 初始化识别实例只创建一次 useEffect(() { if (!SpeechRecognition) { setError(当前浏览器不支持语音识别); return; } const recognition new SpeechRecognition(); recognition.continuous true; // 持续识别不自动停止 recognition.interimResults true; // 返回临时结果实现实时显示 recognition.lang zh-CN; // 识别语言按需修改 recognitionRef.current recognition; }, []); }这里有几个参数值得说清楚。continuous设为true时识别不会在用户停顿后自动结束适合长时间听写设为false则每次停顿就触发onend。interimResults设为true会返回中间结果用户说话过程中就能看到文字在变体验更接近实时字幕设为false则只在识别出完整句子后才返回。lang决定识别引擎用哪种语言模型中文场景写zh-CN英文写en-US。useRef存实例而不是useState是因为实例不需要触发重渲染而且要在多次事件回调里保持同一个引用。这是 React 里管理非渲染状态的标准做法。3.2 事件监听与结果处理实例创建后要挂上onresult、onerror、onend这几个关键回调。onresult是核心它负责把识别结果拼成完整文本// 在 useEffect 里继续挂载事件 recognition.onresult (event) { let finalText ; let interimText ; // 从 resultIndex 开始遍历只处理新增结果 for (let i event.resultIndex; i event.results.length; i) { const result event.results[i]; if (result.isFinal) { finalText result[0].transcript; } else { interimText result[0].transcript; } } // 最终结果追加到 transcript临时结果单独展示 if (finalText) { setTranscript((prev) prev finalText); } setInterim(interimText); }; recognition.onerror (event) { setError(event.error); setIsListening(false); }; recognition.onend () { setIsListening(false); };event.results是一个类数组每个元素包含isFinal标志和识别文本。event.resultIndex告诉我们从哪个位置开始是本次新增的结果避免重复处理旧数据。最终结果追加到transcript临时结果放在interim里单独渲染这样用户能看到“正在识别”的灰色文字。onerror里event.error的常见值有no-speech没检测到语音、audio-capture麦克风不可用、not-allowed用户拒绝了权限。这些错误要分别处理不能笼统地显示“出错了”。3.3 开始与停止的控制逻辑Hook 对外暴露start和stop两个方法内部要处理状态切换和异常const start useCallback(() { const recognition recognitionRef.current; if (!recognition || isListening) return; try { recognition.start(); setIsListening(true); setError(null); } catch (err) { // 重复调用 start 会抛 InvalidStateError setError(err.message); } }, [isListening]); const stop useCallback(() { const recognition recognitionRef.current; if (!recognition || !isListening) return; recognition.stop(); setIsListening(false); }, [isListening]);start里用isListening做守卫防止用户连点按钮导致重复启动。recognition.start()在已经启动的状态下再调用会抛InvalidStateError所以 try-catch 不能省。stop调用后浏览器会触发onend状态在onend里也会被置为false两处都设是为了保证 UI 及时响应。组件卸载时还要记得调recognition.abort()否则麦克风可能一直占用。这个清理逻辑放在useEffect的返回函数里return () { if (recognitionRef.current) { recognitionRef.current.abort(); } };4. 组件层实现按钮、结果展示与状态联动4.1 麦克风按钮的交互设计MicButton是个受控组件它接收isListening和onToggle两个 props根据状态切换图标和样式function MicButton({ isListening, onToggle }) { return ( button className{mic-btn ${isListening ? active : }} onClick{onToggle} aria-label{isListening ? 停止录音 : 开始录音} {isListening ? 停止 : 开始} /button ); }按钮的aria-label根据状态变化这对无障碍访问很重要。样式上通过active类切换颜色给用户明确的视觉反馈。点击时调用父组件传下来的onToggle由父组件决定是 start 还是 stop。4.2 识别结果的实时渲染Transcript组件负责展示transcript和interim两部分文本function Transcript({ transcript, interim }) { return ( div classNametranscript-box p classNamefinal-text{transcript}/p {interim p classNameinterim-text{interim}/p} /div ); }最终结果用正常颜色临时结果用灰色斜体用户一眼就能区分哪些是已确认的、哪些还在识别中。这种视觉分层是实时听写类应用的标配少了它用户会困惑为什么文字在跳。4.3 App 组件里的状态串联App.jsx把 Hook 返回的状态和方法分发给子组件function App() { const { isListening, transcript, interim, error, start, stop } useSpeechRecognition(); const handleToggle () { isListening ? stop() : start(); }; return ( div classNameapp h1语音转文字/h1 MicButton isListening{isListening} onToggle{handleToggle} / {error p classNameerror-msg{error}/p} Transcript transcript{transcript} interim{interim} / /div ); }这里没有多余的状态所有识别相关的状态都来自 Hook。handleToggle根据当前是否在监听来决定调 start 还是 stop。错误信息单独渲染不混在结果里。整个组件树很浅数据流向清晰改起来不会牵一发动全身。5. 避坑与常见问题权限、兼容性与状态异常5.1 麦克风权限被拒绝后无法再次弹窗现象用户第一次点了“拒绝”之后再点开始按钮没有任何反应控制台报not-allowed。原因浏览器记住用户的拒绝决定不会主动再次弹窗。recognition.start()在这种情况下直接触发onerror不会重新请求权限。解决在 UI 上给出明确提示引导用户去浏览器地址栏的权限设置里手动改回“允许”。代码层面监听not-allowed错误展示对应的文案而不是只显示一个通用错误。5.2 Chrome 下必须加 webkit 前缀现象代码里写new SpeechRecognition()在 Chrome 里报SpeechRecognition is not defined。原因Chrome 至今仍然只暴露webkitSpeechRecognition标准名SpeechRecognition没挂到 window 上。解决创建实例前做兼容判断const SpeechRecognition window.SpeechRecognition || window.webkitSpeechRecognition;拿不到就提示浏览器不支持。5.3 连续识别模式下 onend 意外触发现象明明设了continuous true但用户停顿几秒后识别就停了按钮状态也变回未监听。原因部分浏览器在检测到较长静音后仍会触发onendcontinuous不是绝对保证。另外网络波动也可能导致识别引擎断开。解决在onend里判断如果用户仍处于“期望监听”状态自动调recognition.start()重启。用一个shouldListenRef记录用户意图避免用户主动停止后又被重启。5.4 临时结果与最终结果重复显示现象一句话说完后临时结果没有消失和最终结果同时出现在页面上。原因onresult里只更新了transcript没有清空interim。当isFinal为 true 时对应的临时文本应该被移除。解决每次onresult处理完后把interim设为当前批次里非最终结果拼接的文本。如果没有临时结果就设为空字符串。这样最终结果出现时临时文本自然消失。5.5 组件卸载后麦克风仍被占用现象切换路由或关闭页面后浏览器标签页上的录音指示器还亮着。原因SpeechRecognition实例没有在组件卸载时中止识别引擎还在运行。解决在 Hook 的useEffect清理函数里调recognition.abort()。abort()会立即停止识别并释放麦克风比stop()更彻底。6. 二次开发改造从能跑到好用这份源码跑通之后真正花时间的是把它改成符合自己业务的样子。我拿它做过几个小工具下面说几个改动方向。第一个是加“复制结果”和“清空”按钮。transcript就是个字符串复制用navigator.clipboard.writeText清空就是setTranscript()。但要注意清空时如果还在识别中interim也要一起清否则会出现“最终结果没了但临时结果还在”的怪状态。我一般会在 Hook 里额外暴露一个reset方法把两个状态一起重置。第二个是支持多语言切换。recognition.lang可以在运行时修改但修改后需要重新创建实例或者至少重新 start 才生效。我的做法是在 Hook 里把lang做成参数用useEffect监听它的变化变化时 abort 旧实例、创建新实例。这样切换语言时不会残留上一个语言的识别状态。第三个是加时间戳。如果要做会议记录类的应用每段最终结果需要带上时间。可以在onresult里finalText非空时记录new Date().toLocaleTimeString()把结果存成{ text, time }的数组而不是单个字符串。展示时按时间列表渲染。这个改动会影响transcript的数据结构组件层要跟着调整。第四个是错误重试。no-speech错误在安静环境下很常见用户没说话就触发了。我一般会忽略这个错误不打断监听状态让识别继续。其他错误才真正停止并提示。这个判断放在onerror里根据event.error的值分流处理。改造点涉及文件关键改动复制/清空Hook App暴露 reset加剪贴板调用多语言切换Hooklang 作为参数变化时重建实例时间戳记录Hook Transcripttranscript 改为对象数组错误重试Hookonerror 里区分 no-speech验证改动是否生效我有个习惯每次改完 Hook先在控制台手动调start()对着麦克风说三句话看transcript是否完整、interim是否及时消失、isListening是否在 stop 后变回 false。这三项都对了再去看 UI。从那以后我每次动识别相关的代码都强制走一遍这个手动验证流程比写单元测试还快。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑