掘金文章归档 · 第 6 篇 AI 流式系列 1/4 React / DeepSeek

纯前端调用 deepseek v3 模型,流式返回,支持 md 文本、table、代码等

openai SDK + dangerouslyAllowBrowser 免后端直连 DeepSeek,react-markdown 全家桶渲染流式回复

原文作者:随意_(掘金) | juejin.cn/post/7493125575072514057 | 发布于 2025-04-15 | 阅读 1,455 · 约 3 分钟

0背景 & 技术栈

大模型如火如荼,作为公司的边角料、一个不起眼的小渣渣前端切图仔,我们如何在不启动后端服务的情况下快速入门调用大模型?本文就是来扫盲的,文章末尾有源码可以自行查看。

我制作的 demo 比较简陋,也就不到半天时间,基于 react@18tailwindcss@3vitereact-markdown 等来搞的。代码简单,主要是熟悉流程。

1demo 功能介绍

截图位置请对照原文:demo 主页面(左侧会话列表 + 右侧问答区,AI 回复支持 md/表格/代码,代码块右上角有复制按钮)。

2实现步骤:注册 key 到跑起来

  1. 注册 deepseek api key:前往 deepseek API 开放平台,充值至少十块钱;
  2. 写入 api key:clone 代码后,将 .env 文件的 VITE_DEEPSEEK_KEY 替换成你自己的;
  3. 项目启动
npm i
npm run dev

测试问答:你可以在问答框输入以下问题,看支持的情况如何:

⚠️ deepseek 有提示:请创建后很好地保存你的 key,不要外泄。纯前端调用会暴露 key,仅作 demo!

3核心代码:openai SDK 免后端直连

任何大模型的接口都兼容 openai 的规范,我们可以借助 openai SDK 来实现 deepseek 模型的请求。打开 dangerouslyAllowBrowser 属性,可以在浏览器中调用:

import OpenAI from "openai";

const initOpenAI = () => {
    openaiRef.current = new OpenAI({
        baseURL: "https://api.deepseek.com",
        apiKey: GlobalAPI.config?.VITE_DEEPSEEK_KEY,
        dangerouslyAllowBrowser: true,   // 浏览器中调用(仅 demo!)
    });
};
⚠️ 这种做法仅作 demo,后期实际项目需要后端去调用——纯前端调用会把 api key 暴露在浏览器里。

4核心代码:markdown-ai 渲染组件

采用 react-markdown 及配套实现了样式、md 文本、table、html 标签等支持,详情请看具体代码:

import ReactMarkdown from "react-markdown";
import rehypeRaw from "rehype-raw";
import remarkGfm from "remark-gfm";
import rehypeHighlight from 'rehype-highlight'; // 代码高亮
import 'highlight.js/styles/github.css';

<ReactMarkdown
    remarkPlugins={[remarkGfm, remarkMath]}
    rehypePlugins={[rehypeRaw, rehypeHighlight, removeExtraWhitespace]}
    components={components as any}
>
    {markdownstr}
</ReactMarkdown>
📌 插件组合:remark-gfm 表格/任务列表、remark-math 数学公式、rehype-raw 原生 HTML、rehype-highlight 代码高亮、removeExtraWhitespace 自定义空白处理。

5核心代码:代码 copy 组件

借助 react-markdown 自定义标签的方式,自定义了 <pre> 标签,借助 rehype-highlight + copy-to-clipboard 来实现代码的高亮及复制:

import PreWithCopy from "./copycode.tsx";

// 自定义渲染器
const components = {
    pre: PreWithCopy,
};

<ReactMarkdown
    remarkPlugins={[remarkGfm, remarkMath]}
    rehypePlugins={[rehypeRaw, rehypeHighlight, removeExtraWhitespace]}
    components={components as any}
>
    {markdownstr}
</ReactMarkdown>
✅ 通过 components 把 pre 替换成 PreWithCopy,就能在所有代码块右上角加"复制"按钮(思路同系列文章的自定义节点分发)。

6核心代码:html 渲染空白行处理

react-markdown 开启 rehypePlugins rehypeRaw 的时候,大多数情况由于模型返回的 \n 太多,页面会间距过大,尤其是表格等,我们需要单独做空白行处理:

// 消除空格,解决 html 渲染的问题
const removeExtraWhitespace = () => {
    return (tree: any) => {
        const removeWhitespace = (node: any) => {
            if (node.tagName === "pre") {
                return;                    // 代码块跳过,保留原样
            }
            if (node.type === "text") {
                node.value = node.value.replace(/\s+/g, " ");  // 多余空白合并为单空格
            }
            if (node.children) {
                node.children = node.children.filter((child: any) => {
                    if (child.type === "text") {
                        return child.value.trim() !== "";      // 去掉纯空白文本节点
                    }
                    removeWhitespace(child);
                    return true;
                });
            }
        };
        removeWhitespace(tree);
        return tree;
    };
};
📌 这是自定义 rehype 插件写法:返回一个接收 tree 的函数,递归遍历节点——文本节点合并空白、pre 节点跳过。想给 markdown 渲染加任何"后处理",都可以照这个套路写插件。

7总结 & 术语表 & 附录

7.1 总结

以上就是 demo 版本的主要功能和技术实现细节,详情请查看代码(gitee 仓库地址见原文)。作者基于 react 写的,如果需要 vue 版本的,可以留言,抽时间做一套 vue 的。

术语表

术语定义
openai SDKOpenAI 官方 JS SDK,各大模型接口兼容 OpenAI 规范,可换 baseURL 指向 DeepSeek 等
dangerouslyAllowBrowser允许 SDK 在浏览器环境运行(暴露 key,仅 demo 用)
VITE_DEEPSEEK_KEYVite 环境变量,通过 import.meta.env 读取,存于 .env 文件
remark-mathremark 插件,支持 $ 数学公式语法
rehype-highlight基于 highlight.js 的代码高亮 rehype 插件
removeExtraWhitespace自定义 rehype 插件:合并文本节点空白、删除纯空白节点、pre 跳过

附录 A:最小可运行骨架(纯前端调用 + 渲染)

// ========== 安装依赖 ==========
// npm i openai react-markdown remark-gfm remark-math rehype-raw rehype-highlight highlight.js -S
// 在 .env 里配置:VITE_DEEPSEEK_KEY=sk-xxxxxxxx

import OpenAI from "openai";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
import rehypeHighlight from "rehype-highlight";
import 'highlight.js/styles/github.css';

// 1. 初始化客户端(纯前端,仅 demo)
const client = new OpenAI({
    baseURL: "https://api.deepseek.com",
    apiKey: import.meta.env.VITE_DEEPSEEK_KEY,
    dangerouslyAllowBrowser: true,
});

// 2. 流式调用
async function chat(question, onDelta) {
    const stream = await client.chat.completions.create({
        model: "deepseek-chat",
        messages: [{ role: "user", content: question }],
        stream: true,                       // 开启流式
    });
    for await (const chunk of stream) {
        const delta = chunk.choices[0]?.delta?.content || "";
        if (delta) onDelta(delta);          // 每段增量交给渲染
    }
}

// 3. 渲染:react-markdown 全家桶
// <ReactMarkdown
//   remarkPlugins={[remarkGfm, remarkMath]}
//   rehypePlugins={[rehypeRaw, rehypeHighlight]}
//   components={{ pre: PreWithCopy }}
// >{markdownstr}</ReactMarkdown>