openai SDK + dangerouslyAllowBrowser 免后端直连 DeepSeek,react-markdown 全家桶渲染流式回复
大模型如火如荼,作为公司的边角料、一个不起眼的小渣渣前端切图仔,我们如何在不启动后端服务的情况下快速入门调用大模型?本文就是来扫盲的,文章末尾有源码可以自行查看。
我制作的 demo 比较简陋,也就不到半天时间,基于 react@18、tailwindcss@3、vite、react-markdown 等来搞的。代码简单,主要是熟悉流程。
.env 文件的 VITE_DEEPSEEK_KEY 替换成你自己的;npm i
npm run dev
测试问答:你可以在问答框输入以下问题,看支持的情况如何:
你是什么模型 —— 测试 md 纯文本的;写一段 js 去重的代码 —— 测试代码块的;帮我生成一个三行三列的表格,用于计算姓名和成绩 —— 测试表格的。任何大模型的接口都兼容 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!)
});
};
采用 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>
借助 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>
pre 替换成 PreWithCopy,就能在所有代码块右上角加"复制"按钮(思路同系列文章的自定义节点分发)。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;
};
};
tree 的函数,递归遍历节点——文本节点合并空白、pre 节点跳过。想给 markdown 渲染加任何"后处理",都可以照这个套路写插件。以上就是 demo 版本的主要功能和技术实现细节,详情请查看代码(gitee 仓库地址见原文)。作者基于 react 写的,如果需要 vue 版本的,可以留言,抽时间做一套 vue 的。
| 术语 | 定义 |
|---|---|
| openai SDK | OpenAI 官方 JS SDK,各大模型接口兼容 OpenAI 规范,可换 baseURL 指向 DeepSeek 等 |
| dangerouslyAllowBrowser | 允许 SDK 在浏览器环境运行(暴露 key,仅 demo 用) |
| VITE_DEEPSEEK_KEY | Vite 环境变量,通过 import.meta.env 读取,存于 .env 文件 |
| remark-math | remark 插件,支持 $ 数学公式语法 |
| rehype-highlight | 基于 highlight.js 的代码高亮 rehype 插件 |
| removeExtraWhitespace | 自定义 rehype 插件:合并文本节点空白、删除纯空白节点、pre 跳过 |
// ========== 安装依赖 ==========
// 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>