主题
02-4 JavaScript / TypeScript 接入
为什么前端也需要 Ollama?
你可能觉得"Ollama 是后端工具,前端管它干嘛?"——但现代 AI 应用的架构趋势是智能向前端下沉。想想看:
- ChatGPT 的网页版:纯前端应用,AI 能力全在浏览器里
- Cursor / Windsurf:编辑器里的 AI 助手
- Notion / Obsidian 插件:笔记软件中的 AI 对话
- 桌面端工具(TypingM):原生应用的 AI 集成
这些场景的共同点是:用户不想打开终端、不想部署后端服务、只想在当前使用的工具里直接获得 AI 能力。 而 Ollama 恰好提供了 HTTP API,让这一切成为可能。
Node.js 官方库
bash
npm install ollamajavascript
// ===== 基础用法 =====
import ollama from "ollama";
// 同步调用
const response = await ollama.chat({
model: 'qwen2.5:7b',
messages: [{ role: 'user', content: '什么是闭包?' }],
});
console.log(response.message.content);
// → "闭包是一种编程技术..."
// 流式输出(逐 token 推送)
const stream = await ollama.chat({
model: 'qwen2.5:7b',
messages: [{ role: 'user', content: '写一首关于春天的诗' }],
stream: true,
});
process.stdout.write("模型回复: ");
for await const chunk of stream) {
if (chunk.message?.content) {
process.stdout.write(chunk.message.content);
}
}
console.log("\n");
// Embedding
const embed = await ollama.embeddings({
model: 'nomic-embed-text',
input: 'Hello world',
});
console.log(`Embedding dim=${embed.embeddings[0].length}`);
// → Embedding dim=768
// Generate(底层接口)
const gen = await ollama.generate({
model: 'qwen2.5:7b',
prompt: 'What is 2+2?',
});
console.log(gen.response);官方库的 API 设计和 Python 版本高度一致,只是语言从 Python 变成了 JavaScript。核心接口 chat / generate / embeddings 三件套全部支持,流式和非流式也都覆盖。
浏览器端直接调用(无构建)
对于不想安装 Node.js 的场景,可以直接在浏览器中用 fetch 调用 Ollama API。这在以下场景特别有用:
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Ollama in Browser</title>
<style>
body { font-family: -apple-system, sans-serif; max-width: 800px; margin: 40px auto; }
#chat-container { border: 1px solid #ddd; border-radius: 8px; padding: 16px; min-height: 400px; }
.message { margin: 12px 0; padding: 10px 14px; border-radius: 8px; }
.user { background: #e3f2fd; color: #1a56db; }
.assistant { background: #f0f0f0; }
textarea { width: 100%; height: 80px; padding: 8px; font-size: 14px; resize: vertical; }
button { background: #1a56db; color: white; border: none; padding: 10px 24px; border-radius: 6px; cursor: pointer; font-size: 14px; }
button:hover { background: #155eae; }
#status { font-size: 13px; color: #666; margin-top: 8px; min-height: 20px; }
</style>
</head>
<body>
<h1>🦙 Ollama Chat (Browser Edition)</h1>
<div id="chat-container"></div>
<textarea id="input" placeholder="输入你的问题..." rows="3"></textarea>
<button onclick="sendMessage()">发送</button>
<div id="status"></div>
<script>
const API = "http://localhost:11434";
let messages = [];
// 注意: 浏览器有 CORS 限制,默认 localhost 不受影响,
// 但如果从其他端口或域名访问,需要配置 Ollama 的 CORS
async function sendMessage() {
const input = document.getElementById('input');
const msg = input.value.trim();
if (!msg) return;
// 添加用户消息到界面
addMessage(msg, 'user');
input.value = '';
// 禁用按钮,显示状态
document.querySelector('button').disabled = true;
document.getElementById('status').textContent = '🔄 思考中...';
try {
const resp = await fetch(`${API}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'qwen2.5:7b',
messages: [...messages, { role: 'user', content: msg }],
stream: true,
}),
});
// 流式读取响应
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let assistantMsg = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
// SSE 格式: data: {...}
for (const line of chunk.split('\n')) {
if (line.startsWith('data: ') && line !== 'data: [DONE]') {
const data = JSON.parse(line.slice(6));
if (data.message?.content) {
assistantMsg += data.message.content;
// 实时更新显示(不等待完成)
updateAssistantMessage(assistantMsg);
}
}
}
}
// 完成
addMessage(assistantMsg, 'assistant');
messages.push({ role: 'user', content: msg }, { role: 'assistant', content: assistantMsg });
document.getElementById('status').textContent = `✅ 完成 (${assistantMsg.length} 字符)`;
} catch (err) {
document.getElementById('status').textContent = `❌ 错误: ${err.message}`;
} finally {
document.querySelector('button').disabled = false;
document.getElementById('input').focus();
}
}
function addMessage(content, role) {
const container = document.getElementById('chat-container');
const div = document.createElement('div');
div.className = `message ${role}`;
div.textContent = content;
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
function updateAssistantMessage(text) {
const msgs = document.querySelectorAll('.message.assistant');
const last = msgs[msgs.length - 1];
if (last) last.textContent = text;
const container = document.getElementById('chat-container');
container.scrollTop = container.scrollHeight;
}
// 回车键发送
document.getElementById('input').addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
</script>
</body>
</html>把这个 HTML 文件保存为 ollama-chat.html,然后在浏览器中直接打开——一个完全不需要任何构建步骤的 AI 聊天界面就完成了。这就是 Ollama + 前端的魅力所在:零依赖、零构建、双击即用。
React 集成:生产级组件
对于更复杂的应用(比如企业内部工具平台),我们需要把 Ollama 调用封装成可复用的 React 组件:
tsx
// components/OllamaChat.tsx
import { useState, useRef, useCallback } from 'react';
interface Message {
role: 'user' | 'assistant';
content: string;
timestamp: number;
}
interface UseOllamaReturn {
response: string;
stats: {
inputTokens: number;
outputTokens: number;
durationMs: number;
tokensPerSecond: number;
};
}
export function useOllama(
defaultModel: string = 'qwen2.5:7b',
apiUrl: string = 'http://localhost:11434',
): UseOllamaReturn & {
const [response, setResponse] = useState('');
const [stats, setStats] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const abortController = useRef<AbortController | null>(null);
const chat = useCallback(async (
message: string,
options?: { model?: string; temperature?: number },
): Promise<string> => {
setIsLoading(true);
setStats(null);
// 取消上一次请求
abortController.current?.abort();
abortController.current = new AbortController();
try {
const controller = abortController.current;
const startTime = performance.now();
const resp = await fetch(`${apiUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: options?.model || defaultModel,
messages: [{ role: 'user', content: message }],
stream: False,
signal: controller.signal,
}),
});
const data = await resp.json();
const endTime = performance.now();
const result: UseOllamaReturn = {
response: data.message.content,
stats: {
inputTokens: data.prompt_eval_count || 0,
outputTokens: data.eval_count || 0,
durationMs: Math.round(endTime - startTime),
tokensPerSecond:
data.eval_count && (endTime - startTime) > 0
? Math.round(data.eval_count / ((endTime - startTime) / 1000))
: 0,
},
};
setResponse(result.response);
setStats(result.stats);
return result.response;
} catch (err: any) {
if (err.name === 'AbortError') return '[已取消]';
throw err;
} finally {
setIsLoading(false);
}
}, [defaultModel, apiUrl]);
const chatStream = useCallback(async (
message: string,
onChunk: (chunk: string) => void,
options?: { model?: string; temperature?: number },
): Promise<void> => {
setIsLoading(true);
const resp = await fetch(`${apiUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: options?.model || defaultModel,
messages: [{ role: 'user', content: message }],
stream: true,
}),
});
const reader = resp.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
for (const line of decoder.decode(value).split('\n')) {
if (line.startsWith('data: ') && line !== 'data: [DONE]') {
const data = JSON.parse(line.slice(6));
if (data.message?.content) {
onChunk(data.message.content);
}
}
}
}
setIsLoading(false);
}, [defaultModel, apiUrl]);
return {
response,
stats,
isLoading,
chat,
chatStream,
};
}
// ===== 使用示例 =====
function ChatPage() {
const { response, isLoading, chat } = useOllama('qwen2.5:7b');
const [messages, setMessages] = useState<Message[]>([]);
const handleSend = async () => {
const input = /* 从输入框获取 */;
const answer = await chat(input);
setMessages(prev => [
...prev,
{ role: 'user' as const, content: input, timestamp: Date.now() },
{ role: 'assistant' as const, content: answer, timestamp: Date.now() },
]);
};
return (
<div className="chat-interface">
{/* 输入区域 */}
<div className="messages">
{messages.map(m => (
<div key={m.timestamp} className={`msg ${m.role}`}>{m.content}</div>
))}
</div>
{/* 状态栏 */}
{isLoading ? <Spinner /> : null}
{stats && (
<div className="stats">
{stats.tokensPerSecond.toFixed(1)} t/s |
{stats.durationMs}ms
</div>
)}
</div>
);
}这个 useOllama Hook 封装了:
- 状态管理:loading / response / stats
- 取消机制:AbortController 支持请求取消
- 双模式:同步
chat()和流式chatStream() - 类型安全:完整的 TypeScript 类型定义
Next.js / Nuxt SSR 集成
如果你的项目是 Next.js(特别是 App Router),服务端调用 Ollama 有一些特殊考虑:
typescript
// app/api/chat/route.ts
import { NextRequest } from 'next/server';
import ZAI from '@/lib/zai'; // 或直接 fetch
export async function POST(request: NextRequest) {
const { message, model = 'qwen2.5:7b' } = await request.json();
// 服务端调用 Ollama —— 注意是在 server component 中执行
const start = Date.now();
const resp = await fetch('http://localhost:11434/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
messages: [{ role: 'user', content: message }],
stream: false,
}),
});
const data = await resp.json();
const latency = Date.now() - start;
return Response.json({
answer: data.message.content,
model,
latency_ms: latency,
tokens: {
input: data.prompt_eval_count,
output: data.eval_count,
},
});
}Next.js App Router 的优势在于:API 调用在服务器端完成,客户端只接收最终结果。这意味着:
- 用户看不到 API 地址(安全性更好)
- 可以做中间处理(日志记录、缓存、RAG 注入)
- SEO 友好(搜索引擎能索引到结果)
Electron 桌面应用集成
Electron 让你能把 Web 技术打包成跨平台桌面应用。结合 Ollama,你可以做出真正"离线可用"的 AI 工具:
typescript
// main.ts(Electron 主进程)
import { app, BrowserWindow, ipcMain } from 'electron';
import { handleChat } from './ollama-service';
let mainWindow: BrowserWindow | null = null;
async function createWindow() {
mainWindow = new BrowserWindow({
width: 900,
height: 700,
webPreferences: {
nodeIntegration: true,
contextIsolation: true,
},
});
await mainWindow.loadFile('index.html');
}
app.whenReady().then(createWindow);
ipcMain.on('chat:message', (_event, msg) => {
// 收到渲染进程的消息,转发给主进程处理
handleChat(msg).then(reply => {
mainWindow?.webContents.send('chat:reply', reply);
});
});typescript
// ollama-service.ts(IPC 通信层)
import fetch from 'node-fetch';
const API = process.env.OLLAMA_URL || 'http://localhost:11434';
export async function handleChat(message: string): Promise<string> {
const resp = await fetch(`${API}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'qwen2.5:7b',
messages: [{ role: 'user', content: message }],
stream: false,
}),
});
const data = await resp.json();
return data.message.content;
}这个架构的核心思路是:Electron 主进程负责 UI 渲染和窗口管理,通过 IPC 把消息发送给后台 worker(或直接 HTTP 调用 Ollama),拿到结果后再更新 UI。这样即使 Ollama 在做耗时较长的推理,UI 也不会卡顿。
VS Code 扩展开发预览
VS Code 是开发者最常驻留的工具,在里面集成 Ollama 作为 AI 编程助手是非常自然的需求。VS Code 扩展可以用 TypeScript 开发:
json
// package.json
{
"name": "ollama-vscode",
"displayName": "Ollama Assistant",
"version": "1.0.0",
"engines": ["*"],
"categories": ["Other", "Programming Languages"],
"activationEvents": [],
"main": "./out/extension.js",
"contributes": {
"commands": [
{
"command": "ollama.ask",
"title": "Ask Ollama",
"command": "ollama.ask",
},
{
"command": "ollama.explain",
"title": "Explain Selected Code",
"command": "ollama.explain",
},
{
"command": "ollama.review",
"title": "Review Current File",
"command": "ollama.review",
},
],
"slashCommands": [
{
"name": "ollama.ask",
"description": "Ask Ollama a question",
"command": "ollama.ask ${input}",
},
],
},
}typescript
// src/extension.ts
import * as vscode from 'vscode';
import fetch from 'node-fetch';
const MODEL = 'qwen2.5:7b';
const API = 'http://localhost:11434';
async function askOllama(question: string): Promise<void> {
const editor = vscode.window.activeTextEditor;
if (!editor) {
void vscode.showErrorMessage('没有打开的文件');
return;
}
const selection = editor.selection;
const selectedText = selection.document.getText(); // 用户选中的代码
const prompt = selectedText || question || '请帮我解释这段代码';
const resp = await fetch(`${API}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: MODEL,
messages: [
{ role: 'system', content: '你是 VS Code 的 AI 助手。请简洁回答,直接给出答案,不要多余解释。' },
{ role: 'user', content: `${prompt}\n\`\`\`\n${selectedText}\n\`\`\``` },
],
stream: false,
}),
});
const data = await resp.json();
const reply = data.message.content;
// 在新建文档中展示回答
const doc = await vscode.workspace.openTextDocument({
content: `# Ollama 回复\n\n${reply}`,
language: 'plaintext',
});
await vscode.window.showTextDocument(doc.uri);
}这个扩展让开发者可以:
- 选中代码后按快捷键直接问"这段代码有什么问题"
- 用
/ollama.ask斜开侧边栏进行对话 - 用
/ollama.explain让 AI 解释选中代码 - 所有操作都在 VS Code 内完成,不需要切换窗口
常见前端集成陷阱
CORS 跨域问题
这是浏览器端调用 Ollama 最常遇到的问题:
bash
# 现象:你的 Web 应用运行在 http://localhost:3000
# Ollama 运行在 http://localhost:11434
# 浏览器向 11434 发起 fetch → CORS preflight 失败
# 解决方案一(推荐用于开发环境):
# Ollama 默认允许所有来源的请求(*),所以 localhost 不受限制
# 但如果你修改了 OLLAMA_HOST 为非 localhost 地址,就需要处理 CORS
# 解决方案二(生产环境):通过反向代理统一处理
# Nginx 配置:
location /v1/ {
proxy_pass http://127.0.0.1:11434;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
add_header Access-Control-Allow-Headers "Content-Type";
}大响应体阻塞 UI
javascript
// ❌ 问题:一次性返回完整响应导致 UI 卡住
const resp = await fetch(url, { ... });
const data = await resp.json(); // 如果模型思考了 30 秒,这里就卡 30 秒
// ✅ 正确:始终使用流式
const resp = fetch(url, { ..., stream: true });
const reader = resp.body.getReader();
// 逐 token 更新 UI,用户立即看到内容开始出现连接状态管理
javascript
// ❌ 问题:每次请求都创建新连接(HTTP/1.1 无状态)
// 对于高频场景(如 IDE 自动补全,每秒可能触发多次),连接开销很大
// ✅ 正确:保持长连接(HTTP/2 或 WebSocket)
// Ollama 本身支持 keep-alive
// 前端可以通过 connection pooling 复用连接
// 或者使用 SSE 的自动重连机制本章我们涵盖了从浏览器原生 fetch 到 React/Vue/Next.js/Electron/VS Code 全栈的前端接入方案。下一章将快速覆盖 Go/Java/Rust 等其他语言的接入方法。