探索ChatGPT背后的前端黑科技

由于图片和格式解析问题,可前往 阅读原文

在人工智能与互联网技术飞速发展的今天,像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来接收数据

编写客户端页面代码:

js 复制代码
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搭建服务器:

js 复制代码
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服务器,更简单强大:

js 复制代码
@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类似应用中用到的

客户端页面代码示例:

js 复制代码
// 连接到 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服务器:

js 复制代码
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编写示例:

js 复制代码
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的不断打印文字的效果很有感觉,做到这一点也并不难,下面是一个简单实现的🌰

js 复制代码
// 模拟 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标准来实现文字朗读的功能,基本各大主流浏览器都支持

来简单朗读一段文本:

js 复制代码
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,里面包含了请求的媒体类型的轨道,包括音频、视频

ts 复制代码
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还是比较简单的

js 复制代码
function transferAudioToText() {
  // 检查浏览器是否支持 Web Speech API
  if (!("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画出想要的图案即可

来看下怎么做

js 复制代码
// 创建音频上下文
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标签,而是使用了富文本技术

富文本技术随着技术的进步也发展了多个阶段的产物

  1. document.execCommand命令的最初的简单富文本
  2. contenteditable标签的可编辑dom,开发根据内容自行实现格式展示方式
  3. 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聊天应用虽然看起来非常简单,但背后的逻辑思维非常复杂,涉及到很多复杂的技术,没有一个团队是很难做好的

由于图片和格式解析问题,可前往 阅读原文

相关推荐
孜然卷k2 分钟前
前端导出word文件,并包含导出Echarts图表等
前端·javascript
家里有只小肥猫22 分钟前
uniApp小程序保存canvas图片
前端·小程序·uni-app
前端大全25 分钟前
Chrome 推出全新的 DOM API,彻底革新 DOM 操作!
前端·chrome
八角丶36 分钟前
元素尺寸的获取方式及区别
前端·javascript·html
冴羽44 分钟前
Svelte 最新中文文档教程(16)—— Context(上下文)
前端·javascript·svelte
前端小臻1 小时前
关于css中bfc的理解
前端·css·bfc
白嫖不白嫖1 小时前
网页版的俄罗斯方块
前端·javascript·css
HappyAcmen1 小时前
关于Flutter前端面试题及其答案解析
前端·flutter
顾比魁1 小时前
pikachu之CSRF防御:给你的请求加上“网络身份证”
前端·网络·网络安全·csrf
林的快手1 小时前
CSS文本属性
前端·javascript·css·chrome·node.js·css3·html5