由于图片和格式解析问题,可前往 阅读原文
在人工智能与互联网技术飞速发展的今天,像ChatGPT这样的智能对话系统已经成为科技领域的焦点。它不仅能够进行自然流畅的对话,还能以多种格式展示内容,为用户带来高效且丰富的交互体验。然而,这些令人惊叹的功能背后,离不开前端技术的支持与实现
本文将深入探索ChatGPT背后的前端黑科技,希望能为开发者提供有价值的参考,帮助他们在实际项目中更高效地实现类似的功能
以下内容仅是本人的一次思考,如有更好方案的可在评论区留言
:::warning 小贴士
文章中涉及到的示例代码你都可以从 这里查看 ,若对你有用还望点赞支持
:::
单页面应用(SPA)
单页面应用是指在整个使用过程中,只有一个HTML页面被加载。所有的导航和交互操作都是在前端通过JavaScript完成,无需重新加载整个页面。这使得用户可以在同一个页面内无缝浏览不同的内容
虽然在开发过程中可能会遇到一些挑战(比如SEO优化),但SPA凭借其优势已成为现代Web开发的主流趋势。相信大家都对React/Vue/Angular/Svelte
等都不陌生,就不多做介绍了
实时通讯
实时通讯的总要性以及使用场景就不多说了,来看下常用的几种通讯技术
Server-Sent Events(SSE)
如果你仔细查看了ChatGPT的对话请求过程就会看到,服务器在不断的推送数据
这里用到的就是SSE技术,基于HTTP协议单向推送技术。它使得服务器能够在数据生成时实时推送给客户端,而无需客户端频繁轮询服务器。这非常适合需要实时更新的应用场景,如新闻推送、股票价格监控和日志跟踪
使用SSE注意事项
- 在响应头中添加
Content-Type: text/event-stream
,以通知客户端这是一个SSE连接 - 返回的数据要包含
data: xxx
,并以data: xxx\n\n
双换行符格式,这样客户端才能正确解析,可以指定id、type
等数据;默认情况下事件都是message
,也可以自定义事件 - 建立好后的sse链接就会不断发数据,直到
EventSource
调用close事件
- 客户端使用EventSource来接收数据
编写客户端页面代码:
const eventSource = new EventSource('http://localhost:3000/sse');eventSource.addEventListener("message", (e) => {console.log(e);if (e.data >> 0 > 4) eventSource.close(); // 客户端根据数据判断然后主动断开连接
});// 服务器如果判断数据发完了可以发送type为done的数据包,客户端就可以监听到此类事件
eventSource.addEventListener("done", (e) => {});eventSource.addEventListener('error', (e) => {console.log('Connection failed:', e);
});
使用express搭建服务器:
app.get("/sse", (req, res) => {// 设置响应头为SSE格式(必填的)res.setHeader("Content-Type", "text/event-stream");res.setHeader("Cache-Control", "no-cache");res.setHeader("Connection", "keep-alive");let count = 0; let timer;const sendEvent = () => {res.write(`id: ${count}\n\n`);res.write(`data: ${count}\n\n`);count++;timer = setTimeout(sendEvent, 1000);};sendEvent();req.on("close", () => {console.log("Client disconnected");clearTimeout(timer);});
});
推荐使用Nest框架搭建sse服务器,更简单强大:
@Controller("/api/sse")
export class SseController {private subject = new Subject<void>();@Sse("msgs")sse(@Query("msg") msg: string): Observable<MessageEvent> {const eventStream = Array.from({ length: 5 }, (v, i) => i).map((message, index) =>of({data: `from sse events: ${index}, 你发送了 【${msg}】`,id: randomUUID(),retry: 0,type: index === 4 ? "done" : "message",} as MessageEvent).pipe(delay(1000)),takeUntil(this.subject),);return concat(...eventStream);}// 主动停止@Post("/stop")stop() {return this.subject.next();}
}
Websocket
WebSocket是一种在单个TCP连接上进行全双工通信的协议。它允许客户端和服务端之间建立持久连接,实现双向实时数据传输
相比sse支持双向通讯、实现更加复杂,适合在线客服、实时消息通知、在线文档编辑、白板共享等等,不过也有在ChatGPT类似应用中用到的
客户端页面代码示例:
// 连接到 WebSocket 服务器
const ws = new WebSocket('ws://localhost:8080');ws.onopen = () => { console.log('Connected to server'); };
ws.onmessage = (event) => { console.log(`Received message: ${event.data}`); };
ws.onclose = () => { console.log('Connection closed'); };
node创建Websocket服务器:
const WebSocket = require('ws');// 创建一个 HTTP 服务器(可选)
const http = require('http');
const server = http.createServer((req, res) => {res.writeHead(404, {'Content-Type': 'text/plain'});res.end('Not Found');
});// 使用 ws 库创建 WebSocket 服务器
const wss = new WebSocket.Server({ server });wss.on('connection', (ws) => {console.log('New client connected');// 发送消息到客户端ws.send('Hello from server!');// 接收客户端消息ws.on('message', (message) => {console.log(`Received message: ${message}`);// 回复客户端ws.send(`Echo back: ${message}`);});// 处理客户端断开连接ws.on('close', () => {console.log('Client disconnected');});
});
Chunked Transfer
HTTP 分块传输(HTTP Chunked Transfer)是一种将大数据量分解为多个较小的数据块进行传输的技术。每个数据块被称为“chunk”,这些chunk独立地通过网络传输,并在接收端重新组装成原始数据
这种方法特别适用于需要逐步处理数据的场景,例如视频流媒体和大文件下载,使用看起来和sse很像,也是可以用在这种gpt这种场景的
使用express编写示例:
app.use("/chunked", (req, res) => {// 必须要设置Transfer-Encoding头信息res.setHeader("Transfer-Encoding", "chunked");let timer, i = 1;// 1s返回一次 总共返回9次timer = setInterval(() => {res.write(`${i}`);if (i >= 10) {clearInterval(timer);res.end();}i++;}, 1000);
});
现在请求这个接口看下效果:
模拟打印
是不是看到ChatGPT的不断打印文字的效果很有感觉,做到这一点也并不难,下面是一个简单实现的🌰
// 模拟 ChatGPT 打印文字效果
function simulateChatGPTTyping(outputElement, text) {let index = 0;outputElement.innerHTML = '';function typeText() {if (index < text.length * 2) {// 随机延迟,模拟思考时间setTimeout(() => {// 随机选择一个字符来打字const randomIndex = Math.floor(Math.random() * text.length);const char = text[randomIndex];// 更新输出内容outputElement.innerHTML += char;// 滚动到最新位置outputElement.scrollTop = outputElement.scrollHeight;index++;typeText();}, 10 + Math.random() * 200); // 延迟范围:100-300ms} else {// 打印完成,添加换行符setTimeout(() => {outputElement.innerHTML += '<br>';}, 500);}}typeText();
}// 示例文本
const text = `我是人工智能助手ChatGPT。我可以帮助你回答问题、提供信息和进行对话。你可以问我任何你感兴趣的问题,我会尽力为你提供详细的解答。例如,你可以问我关于科技、历史、文化、科学、数学、编程等方面的知识。我还可以帮助你完成一些任务,比如编写代码片段、解释技术概念或者提供建议。请告诉我你需要什么帮助!
`;
// 初始化输出容器
const output = document.getElementById('output');
// 开始模拟打字效果
simulateChatGPTTyping(output, text);
模块化组件
ChatGPT能生成很多种类的结果,包括:文字、列表、表格、代码等等,那么前端如何对应展示呢❓这里只讲下实现思路
其中最简单的一种方案就是把后端返回的markdown格式的数据直接喂给页面上的markdown组件,只需要丰富markdown组件功能就行,对格式的解析直接交给其内部
这种方式比较简单,容易大众化无法做好定制功能,要实现界面的定制功能,就要设计到对数据的解析了;需要自研如何解析匹配数据,然后根据不同的类型数据调用不同的组件,这样就可以满足定制功能了,相对来说比较复杂点
实现定制功能需要做很多种的组件,那么就涉及到了前端组件库的设计了,比如:按需加载等等
语音技术
随着互联网技术的发展,页面上也出现了各种各样的富媒体内容,如:音频、视频等等,工作压力下可能有很多读者无法持续关注这方便内容,下面就来看下ChatGPT用到的功能
文字朗读
文字朗读看上去很高大上,很多都支持这个功能,如:攻粽号朗读,ChatGPT也支持
作者博客网站也是加上了文本朗读的功能
实现它非常简单,浏览器提供了speechSynthesis标准来实现文字朗读的功能,基本各大主流浏览器都支持
来简单朗读一段文本:
const synth = window.speechSynthesis;
const text = "我是一段文本,请朗读";
const utterance = new SpeechSynthesisUtterance(text);const voices = synth.getVoices();
const chineseVoice = voices.find((voice) => voice.lang === "zh-CN");
if (chineseVoice) {utterance.voice = chineseVoice; // 设置语言
}
// 朗读
synth.speak(utterance);
除此之外还支持调整音色、声音以及监听各种朗读事件等等
对话
对话相对文本朗读来说更复杂一点,涉及到录音、播放声音等逻辑,整体流程就是录音、识别、播放声音。先来看如何录音
H5提供了MediaRecorder标准API来进行媒体的轻松录音,需要通过调用 MediaRecorder()
构造方法进行实例化。使用之前需要调用MediaDevices.getUserMedia()给予使用媒体输入的许可权限,媒体输入会产生一个MediaStream,里面包含了请求的媒体类型的轨道,包括音频、视频
let mediaRecorder: MediaRecorder;
const recordDataChunks: Blob[] = [];function startRecording() {navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {try {mediaRecorder = new MediaRecorder(stream);mediaRecorder.ondataavailable = e => {recordDataChunks.push(e.data);};mediaRecorder.start(1000);state.isRecording = true;mediaRecorder.addEventListener("stop", () => {console.log("录制完成");const blob = new Blob(recordDataChunks, {type: "audio/ogg; codecs=opus",});const url = URL.createObjectURL(blob);console.log(url);});} catch (error) {state.isRecording = false;}});
}
拿到用户的声音后就需要开始识别,然后思考了,最后将内容播放出来就可以了
语音识别
语音识别即音频转文字的功能,ChatGPT在说话时也会将语音实时转化为文字
这个功能使用js还是比较简单的
function transferAudioToText() {// 检查浏览器是否支持 Web Speech APIif (!("webkitSpeechRecognition" in window)) {message.error("你的浏览器不支持语音识别");return;}const recognition = new webkitSpeechRecognition();recognition.continuous = false;recognition.lang = "zh-CN";recognition.interimResults = false;recognition.start();recognition.onresult = (e: any) => {console.log(e.results[0][0].transcript);};
}
当开始识别时就会调用麦克风讲话,然后实时识别语音
音频可视化
ChatGPT在说话时由用图案动画反馈说话状态
H5提供了 AudioContext 接口提供了音频节点的创建和音频处理或解码的执行操作,使用起来也不是很麻烦,但相对前面2者稍微复杂点
其主要逻辑就是拿到音频后通过audiocontext获取音频节点,最后通过canvas画出想要的图案即可
来看下怎么做
// 创建音频上下文
const audioCtx = new AudioContext();
const audioSource = audioCtx.createMediaStreamSource(mediaStream); // 这里把音频媒体流传入
const analyser = audioCtx.createAnalyser(); // 创建音频分析器
audioSource.connect(analyser); // 将音频连接到分析器
analyser.fftSize = 2048;
audioDataBuffer = new Uint8Array(analyser.frequencyBinCount);// 最后将其渲染到canvas上就可以了
const ctx = canvasRef.value!.getContext("2d")!;
const canvasW = (canvasRef.value!.width = canvasRef.value!.parentElement!.offsetWidth - 48);
const canvasH = (canvasRef.value!.height = 120);
ctx.fillStyle = "#4646fc";function drawAudioTrackBarGraphic() {ctx.clearRect(0, 0, canvasW, canvasH);// 然后通过analyser拿到节点信息analyser!.getByteFrequencyData(audioDataBuffer!);const barLen = audioDataBuffer!.length / 10;const barWidth = canvasW / barLen;for (let i = 0; i < barLen; i++) {const data = audioDataBuffer![i];const barHeight = (data / 255) * canvasH;const x1 = i * barWidth;const y = canvasH - barHeight;ctx.fillRect(x1, y, barWidth, barHeight);}rFId = requestAnimationFrame(drawAudioTrackBarGraphic);
}requestAnimationFrame(drawAudioTrackBarGraphic);
好了,到这里基本上实现了录音、语音转文字、讲话动态反馈效果:
富文本与光标
可以看到ChatGPT的对话框不是简单的textarea
标签,而是使用了富文本技术
富文本技术随着技术的进步也发展了多个阶段的产物
- 以
document.execCommand
命令的最初的简单富文本 - 以
contenteditable
标签的可编辑dom,开发根据内容自行实现格式展示方式 - 以
canvas
为主要的自研光标系统,代表为google docs
而ChatGPT这里简单的对话框则使用了contenteditable=true
标签,然后通过内容根据浏览器光标API getSelection 来实现内容的富文本化,其实现还是有一点点复杂的,关键还是内容解析和标签处理
安全防范
好的产品和应用一定少不了安全方面的防范,对于web 应用基本上和我之前的文章 HTTP协议及安全防范 中讲的安全知识大差不差
来看看ChatGPT怎么做的❓
首先就是CSP
防范XSS攻击
还有禁止客户端的 MIME 类型嗅探行为,通知浏览器应该只通过 HTTPS 访问该站点等等
总之,万变不离其宗。读者可以翻阅往期文章
性能优化
性能方面的手段也值得读者学习
缓存
常见的HTTP缓存,以及indexDB数据库使用等等,这里不再讲了
Server Push
利用HTTP2的服务器主动推送功能,加快资源的加载,这在HTTP文中也讲过了
TailwindCSS/UnoCSS
Tailwind CSS和UnoCSS都是用于快速构建用户界面的CSS工具,还有读者不了解的需要抓紧看看了
总结
可以看出一个ChatGPT聊天应用虽然看起来非常简单,但背后的逻辑思维非常复杂,涉及到很多复杂的技术,没有一个团队是很难做好的
由于图片和格式解析问题,可前往 阅读原文