前端调用大模型实战
在 AI 原生应用开发中,前端直接调用大模型 API 已成为高频场景 —— 无需后端中转即可快速实现对话、生成等核心功能。本文结合工程化实践,从 Vite 全栈项目初始化到 Fetch 复杂请求封装,手把手教你搭建可落地的前端大模型调用方案,干货密集,新手也能直接复用!
一、项目初始化:Vite 全栈脚手架搭建
相比原生 HTML/CSS/JS 项目,Vite 的热更新、工程化支持、全栈适配能力更适合生产级项目。以下是完整初始化流程:
基础项目创建
# 初始化Vite项目(支持Vue/React/原生JS,这里以原生TS为例)
npm create vite@latest llm-frontend -- --template vanilla-ts
cd llm-frontend
npm install
# 安装可选依赖(请求工具增强、类型提示)
npm install axios # 可选,本文以原生Fetch为主,axios作为补充方案
npm install -D @types/node # 环境变量类型支持2
3
4
5
6
7
8
核心 Vite 配置(vite.config.ts)
重点解决跨域问题(前端直接调用第三方 LLM API 必踩坑)和环境变量暴露,配置如下:
import { defineConfig, loadEnv } from 'vite';
import path from 'path';
export default defineConfig(({ mode }) => {
// 加载环境变量(区分开发/生产环境)
const env = loadEnv(mode, process.cwd(), 'VITE_');
return {
// 项目基础配置
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'), // 路径别名,方便导入
},
},
// 开发服务器配置(解决跨域)
server: {
proxy: {
// 配置代理前缀,避免直接暴露第三方API地址
'/api/llm': {
target: env.VITE_LLM_API_BASE_URL, // 目标API地址(如https://api.deepseek.com)
changeOrigin: true, // 开启跨域代理
rewrite: (path) => path.replace(/^/api/llm/, ''), // 重写路径(去掉代理前缀)
},
},
},
// 生产环境构建配置
build: {
outDir: 'dist',
sourcemap: mode === 'development', // 开发环境生成sourcemap
},
};
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
目录结构设计(工程化核心)
llm-frontend/
├─ src/
│ ├─ api/ # API封装目录(工程化关键)
│ │ └─ llm.ts # 大模型请求封装
│ ├─ utils/ # 工具函数
│ │ └─ env.ts # 环境变量工具
│ ├─ types/ # 类型定义(TS必备)
│ │ └─ llm.ts # 大模型请求/响应类型
│ ├─ main.ts # 入口文件
│ └─ style.css # 样式文件
├─ .env.development # 开发环境变量
├─ .env.production # 生产环境变量
├─ .gitignore # 忽略文件(关键:排除.env)
└─ vite.config.ts # Vite配置2
3
4
5
6
7
8
9
10
11
12
13
14
二、核心实战:Fetch 调用 LLM API(复杂请求封装)
大模型 API 调用属于 POST 复杂请求,需严格配置请求行、请求头、请求体,且必须处理异步逻辑和错误捕获。
环境变量配置(.env 文件)
创建 .env.development(开发环境)和 .env.production(生产环境),API 密钥必须放在环境变量中,避免硬编码泄露:
# .env.development
VITE_LLM_API_BASE_URL=https://api.deepseek.com
VITE_LLM_API_KEY=your-deepseek-api-key # 替换为你的密钥
VITE_LLM_MODEL=deepseek-chat # 模型名称(根据API文档配置)2
3
4
⚠️ 关键提醒:在
.gitignore中添加.env*,禁止提交环境变量文件:
# .gitignore
node_modules/
dist/
.env
.env.*
!.env.example # 可选:保留示例文件(不含真实密钥)2
3
4
5
6
类型定义(types/llm.ts,TS 提升开发体验)
提前定义请求和响应类型,避免类型混乱:
/** 大模型请求参数类型 */
export interface LLMRequest {
model: string;
messages: Array<{
role: 'user' | 'assistant' | 'system'; // 角色(用户/助手/系统)
content: string; // 消息内容
}>;
temperature?: number; // 随机性(0-1)
max_tokens?: number; // 最大响应长度
}
/** 大模型响应类型 */
export interface LLMResponse {
id: string;
object: string;
created: number;
model: string;
choices: Array<{
message: {
role: 'assistant';
content: string;
};
finish_reason: string;
index: number;
}>;
usage: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
}
/** 错误响应类型 */
export interface LLMErrorResponse {
error: {
message: string;
code: string;
};
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Fetch 请求封装( api/llm.ts,工程化核心)
封装通用请求函数,处理请求配置、JSON 序列化、异步逻辑、错误捕获,避免重复代码:
import { LLMRequest, LLMResponse, LLMErrorResponse } from '@/types/llm';
// 从环境变量获取配置
const LLM_API_BASE_URL = import.meta.env.VITE_LLM_API_BASE_URL;
const LLM_API_KEY = import.meta.env.VITE_LLM_API_KEY;
const LLM_MODEL = import.meta.env.VITE_LLM_MODEL;
/**
* 调用大模型API(通用封装)
* @param messages 对话消息数组
* @param options 额外配置(temperature/max_tokens等)
*/
export async function callLLM(
messages: LLMRequest['messages'],
options?: Omit<LLMRequest, 'model' | 'messages'>
): Promise<LLMResponse['choices'][0]['message']['content']> {
// 1. 校验必要配置
if (!LLM_API_KEY || !LLM_API_BASE_URL) {
throw new Error('请配置LLM_API_KEY和LLM_API_BASE_URL环境变量');
}
// 2. 构造请求参数
const requestData: LLMRequest = {
model: LLM_MODEL,
messages,
temperature: options?.temperature || 0.7,
max_tokens: options?.max_tokens || 1024,
...options,
};
try {
// 3. 发送Fetch请求(复杂请求核心配置)
const response = await fetch('/api/llm/chat/completions', {
method: 'POST', // 必须POST
headers: {
'Authorization': `Bearer ${LLM_API_KEY}`, // 令牌固定前缀Bearer
'Content-Type': 'application/json', // 必须JSON格式
'Accept': 'application/json',
},
body: JSON.stringify(requestData), // 序列化JSON对象(关键:不能直接传对象)
});
// 4. 处理响应(区分成功/失败)
const data = await response.json() as LLMResponse | LLMErrorResponse;
if (!response.ok) {
// 解析错误信息
const errorMsg = (data as LLMErrorResponse).error?.message || '请求失败';
throw new Error(`LLM API错误: ${errorMsg}`);
}
// 5. 提取响应内容(简化返回结果)
return (data as LLMResponse).choices[0].message.content;
} catch (error) {
// 6. 统一错误捕获
console.error('LLM调用失败:', error);
throw error; // 抛出错误,让调用方处理
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
调用示例(main.ts)
使用 async/await 简化异步逻辑(比 .then 更清晰):
import { callLLM } from '@/api/llm';
// 立即执行函数(ES模块中无法直接写顶层await,需包裹)
(async () => {
try {
// 调用大模型(用户消息+系统提示)
const messages = [
{ role: 'system', content: '你是一个前端助手,回答简洁明了' },
{ role: 'user', content: '如何用前端调用大模型API?' },
];
console.log('正在请求大模型...');
const result = await callLLM(messages, { temperature: 0.5 });
console.log('大模型响应:', result);
// 渲染到页面
document.body.innerHTML = `
<div style="max-width: 800px; margin: 2rem auto; padding: 0 1rem;">
<h2>前端大模型调用示例</h2>
<div style="margin: 1rem 0; padding: 1rem; background: #f5f5f5; border-radius: 8px;">
<p><strong>用户:</strong> 如何用前端调用大模型API?</p>
<p><strong>助手:</strong> ${result}</p>
</div>
</div>
`;
} catch (error) {
document.body.innerHTML = `<div style="color: red; margin: 2rem;">调用失败: ${(error as Error).message}</div>`;
}
})();2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
三、工程化进阶:让代码更可维护
环境变量工具封装( utils/env.ts)
统一处理环境变量,避免重复写 import.meta.env:
/** 获取环境变量(带默认值) */
export function getEnv<T = string>(key: string, defaultValue?: T): T {
const value = import.meta.env[key] as unknown as T;
if (value === undefined && defaultValue === undefined) {
throw new Error(`环境变量${key}未配置`);
}
return value ?? defaultValue!;
}
// 常用环境变量快捷访问
export const llmEnv = {
apiBaseUrl: getEnv('VITE_LLM_API_BASE_URL'),
apiKey: getEnv('VITE_LLM_API_KEY'),
model: getEnv('VITE_LLM_MODEL', 'deepseek-chat'),
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
扩展:Axios 替代方案(可选)
如果项目中已使用 Axios,可替换 Fetch(优势:拦截器、取消请求):
import axios from 'axios';
import { llmEnv } from '@/utils/env';
import { LLMRequest } from '@/types/llm';
// 创建Axios实例
const llmAxios = axios.create({
baseURL: '/api/llm',
headers: {
'Authorization': `Bearer ${llmEnv.apiKey}`,
'Content-Type': 'application/json',
},
});
// 请求拦截器(统一添加配置)
llmAxios.interceptors.request.use((config) => {
config.data = config.data || {};
config.data.model = llmEnv.model;
return config;
});
// 响应拦截器(统一处理响应)
llmAxios.interceptors.response.use(
(response) => response.data.choices[0].message.content,
(error) => {
const errorMsg = error.response?.data?.error?.message || '请求失败';
console.error('LLM调用失败:', errorMsg);
throw new Error(errorMsg);
}
);
// 调用函数
export async function callLLMAxios(messages: LLMRequest['messages'], options?: Omit<LLMRequest, 'model' | 'messages'>) {
return llmAxios.post('/chat/completions', { messages, ...options });
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
安全最佳实践
- 密钥安全:前端环境变量仍可能被破解,生产环境建议通过后端中转请求(前端调用自己的后端,后端再调用 LLM API,密钥存储在后端)。
- 请求限制:添加请求频率限制,避免滥用 API。
- 输入过滤:对用户输入进行安全过滤,防止注入攻击。
四、避坑指南(高频问题解决)
- 跨域报错:确保 Vite 代理配置正确,
changeOrigin: true必须开启,且代理路径重写正确。 - 401 Unauthorized:检查 API 密钥是否正确,
Authorization头格式是否为Bearer + 空格 + 密钥。 - 请求体格式错误:必须用
JSON.stringify序列化请求体,且字段名与 API 文档一致(如messages不能写成message)。 - 环境变量未生效:Vite 环境变量必须以
VITE_为前缀,且修改.env后需重启开发服务器。
五、扩展方向
- 流式响应:大模型支持 SSE(Server-Sent Events),可实现打字机效果(需修改 Fetch 为
response.body.getReader())。 - 缓存策略:缓存重复请求,减少 API 调用成本(如使用
localStorage或sessionStorage)。 - 多模型支持:封装通用 API,支持切换 DeepSeek、OpenAI、Anthropic 等不同模型。
- UI 组件化:将调用逻辑封装为 Vue/React 组件(如
ChatInput、ChatMessage)。
总结
前端调用大模型的核心是**规范的 HTTP 请求配置+工程化的代码组织*:Vite 解决了跨域和环境变量问题,Fetch/Axios 负责请求封装,TypeScript 提升类型安全,环境变量和目录结构保证可维护性。按照本文方案,你可以快速搭建一个稳定、安全、可扩展的前端大模型调用方案,直接应用到生产项目中。
如何优雅终止 Fetch 请求:提升前端应用性能与用户体验
在现代前端开发中,fetch() 已成为发起网络请求的主流 API。然而,当用户切换页面、取消操作或请求超时时,如何终止正在进行的请求成为提升应用性能的关键技术。本文将深入探讨终止 Fetch 请求的多种方案与最佳实践。
一、为何需要主动终止 Fetch 请求?
必要性分析
- 避免资源浪费:未完成的请求持续占用网络带宽和服务器资源
- 提升用户体验:用户离开页面时立即停止无关请求,加速页面切换
- 防止竞态条件:解决快速连续操作导致的前后请求响应顺序错乱问题
- 优化性能指标:减少不必要的请求可显著降低内存占用和CPU消耗
典型应用场景
- 搜索框输入时的实时搜索建议
- 分页表格快速切换时的数据加载
- 大文件上传/下载的中途取消
- 页面跳转时中断未完成的请求
- 自定义请求超时控制
二、核心技术:AbortController API
AbortController 工作原理
// 创建控制器与信号对象
const controller = new AbortController();
const signal = controller.signal;
// 发起可终止的fetch请求
fetch('https://api.example.com/data', { signal })
.then(response => response.json())
.catch(err => {
if (err.name === 'AbortError') {
console.log('请求已被终止');
} else {
console.error('网络错误', err);
}
});
// 终止请求
controller.abort();2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
关键组件解析
- Controller:提供
abort()方法的控制对象 - Signal:传递终止状态的信号对象
- AbortError:请求终止时抛出的特定错误类型
实现请求超时自动终止
// 设置5秒超时自动终止
const timeout = 5000;
function fetchWithTimeout(url, options = {}) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
return fetch(url, {
...options,
signal: controller.signal
}).finally(() => clearTimeout(timeoutId));
}
// 使用示例
fetchWithTimeout('https://api.example.com/slow-data')
.catch(err => console.error(err)); // 超时将触发AbortError2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
三、实战应用场景解析
用户主动取消操作
// 获取DOM元素
const fetchButton = document.getElementById('fetch-btn');
const cancelButton = document.getElementById('cancel-btn');
let controller;
fetchButton.addEventListener('click', async () => {
controller = new AbortController();
try {
const response = await fetch('/api/data', {
signal: controller.signal
});
const data = await response.json();
renderData(data);
} catch (err) {
if (err.name !== 'AbortError') {
showError('请求失败: ' + err.message);
}
}
});
cancelButton.addEventListener('click', () => {
if (controller) controller.abort();
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
组件卸载时自动清理
// React组件中自动终止请求
useEffect(() => {
const controller = new AbortController();
const loadData = async () => {
try {
const res = await fetch('/api/data', {
signal: controller.signal
});
setData(await res.json());
} catch (err) {
if (!err.name === 'AbortError') {
setError(err.message);
}
}
};
loadData();
// 组件卸载时执行清理
return () => controller.abort();
}, []);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
批量请求统一终止
// 创建主控制器
const masterController = new AbortController();
// 发起多个关联请求
const requests = [
fetch('/api/users', { signal: masterController.signal }),
fetch('/api/products', { signal: masterController.signal }),
fetch('/api/orders', { signal: masterController.signal })
];
Promise.all(requests)
.then(responses => {/* 处理响应 */})
.catch(err => {
if (err.name === 'AbortError') {
console.log('所有请求已终止');
}
});
// 一键终止所有请求
document.getElementById('cancel-all').addEventListener('click', () => {
masterController.abort();
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
四、高级技巧与边界处理
中断后重试机制
async function fetchWithRetry(url, options, retries = 3) {
for (let i = 0; i < retries; i++) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
return response;
} catch (err) {
clearTimeout(timeoutId);
if (err.name !== 'AbortError' || i === retries - 1) {
throw err;
}
console.log(`第${i+1}次重试...`);
}
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
内存管理最佳实践
// 避免内存泄漏的模式
function fetchData() {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(handleResponse)
.catch(handleError)
.finally(() => {
// 清除引用以便垃圾回收
controller = null;
});
return {
abort: () => controller.abort(),
// 其他方法...
};
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
五、浏览器兼容性与替代方案
兼容性处理方案
// 检测AbortController支持情况
const supportsAbortController = typeof AbortController !== 'undefined';
// 兼容的请求终止函数
function abortableFetch(url, options) {
if (supportsAbortController) {
const controller = new AbortController();
const signal = controller.signal;
const promise = fetch(url, { ...options, signal });
return {
promise,
abort: () => controller.abort()
};
} else {
// 回退到XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open(options.method || 'GET', url);
return {
promise: new Promise((resolve, reject) => {
xhr.onload = () => resolve(new Response(xhr.response));
xhr.onerror = reject;
xhr.send(options.body);
}),
abort: () => xhr.abort()
};
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
主流框架的集成方案
Axios 取消令牌
const source = axios.CancelToken.source();
axios.get('/api/data', {
cancelToken: source.token
}).catch(err => {
if (axios.isCancel(err)) {
console.log('请求已取消');
}
});
// 取消请求
source.cancel();2
3
4
5
6
7
8
9
10
11
12
Vue Composition API 封装
import { ref } from 'vue';
export function useAbortableFetch() {
const controller = ref(null);
const fetchData = async (url, options = {}) => {
// 终止之前的请求
if (controller.value) {
controller.value.abort();
}
controller.value = new AbortController();
try {
const response = await fetch(url, {
...options,
signal: controller.value.signal
});
return await response.json();
} catch (err) {
if (err.name !== 'AbortError') throw err;
}
};
return { fetchData };
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
六、总结:构建健壮的请求管理策略
终止 Fetch 请求不仅是技术需求,更是优化用户体验的关键环节。通过合理应用 AbortController:
- 实现精准的请求生命周期控制
- 降低服务器负载与客户端资源消耗
- 避免无效渲染与状态更新
- 提升复杂交互场景下的应用稳定性
在 SPA 成为主流的今天,请求终止能力已成为前端工程师的必备技能。建议在项目中统一封装请求工具库,集成终止、重试、缓存等高级特性,为应用性能提供坚实保障。
技术扩展:关注新兴的
Navigator.sendBeacon()对于页面关闭时请求的特殊处理方案,以及 React 18 Suspense 对数据获取模式的革新,它们与请求终止技术共同构成了现代前端数据获取的完整解决方案。
