🕐 预计用时:2-3 小时 | 🎯 目标:用 fetch 调用 Django API,实现前后端数据交互
# 传统模式(Django 模板渲染):
# 浏览器 → Django(路由+视图+模板)→ 完整 HTML → 浏览器
# 每次操作都刷新整个页面
# 前后端分离模式:
# 前端:HTML/CSS/JS(独立运行,可能在另一台服务器)
# 后端:Django REST API(只返回 JSON 数据)
# 浏览器 → 前端页面 → fetch 请求 API → JSON 数据 → JS 动态更新页面
# 不刷新页面,体验更好# 后端(Django REST Framework)
# GET /api/posts/ → 文章列表 JSON
# POST /api/posts/ → 创建文章
# GET /api/posts/1/ → 文章详情 JSON
# PUT /api/posts/1/ → 更新文章
# DELETE /api/posts/1/ → 删除文章
# 响应格式示例
{
"count": 42,
"next": "http://example.com/api/posts/?page=2",
"previous": null,
"results": [
{
"id": 1,
"title": "Python 学习笔记",
"content": "...",
"author_name": "Alice",
"created_at": "2026-05-12T13:00:00Z"
}
]
}// 基础 GET 请求
fetch('/api/posts/')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.then(data => {
console.log(data.results); // 文章数组
})
.catch(error => {
console.error('请求失败:', error);
});
// async/await 写法(推荐)
async function loadPosts() {
try {
let response = await fetch('/api/posts/');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
let data = await response.json();
return data.results;
} catch (error) {
console.error('加载失败:', error);
return [];
}
}
// 带查询参数
async function searchPosts(keyword) {
let response = await fetch(`/api/posts/?search=${encodeURIComponent(keyword)}`);
let data = await response.json();
return data.results;
}
// 带分页
async function loadPage(page) {
let response = await fetch(`/api/posts/?page=${page}`);
let data = await response.json();
return data;
}// 创建文章
async function createPost(title, content) {
let response = await fetch('/api/posts/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken'), // Django CSRF
},
body: JSON.stringify({
title: title,
content: content,
}),
});
if (response.status === 201) {
let data = await response.json();
console.log('创建成功:', data);
return data;
} else {
let errors = await response.json();
console.error('创建失败:', errors);
return null;
}
}
// 获取 CSRF Token(Django 需要)
function getCookie(name) {
let value = document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)');
return value ? value.pop() : '';
}💡 Django CSRF 处理:
Django 的 POST 请求默认需要 CSRF token。两种解决方案:
1. 前后端同域:从 Cookie 读取 csrftoken,放在请求头 X-CSRFToken
2. 前后端跨域:用 Token 认证(DRF 的 TokenAuthentication),不需要 CSRF
// 更新文章
async function updatePost(id, data) {
let response = await fetch(`/api/posts/${id}/`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken'),
},
body: JSON.stringify(data),
});
return await response.json();
}
// 删除文章
async function deletePost(id) {
let response = await fetch(`/api/posts/${id}/`, {
method: 'DELETE',
headers: {
'X-CSRFToken': getCookie('csrftoken'),
},
});
return response.status === 204; // 204 = 删除成功,无返回体
}// 完整的加载流程
async function loadPosts() {
let container = document.querySelector('#posts');
// 1. 显示加载中
container.innerHTML = '<div class="loading">加载中...</div>';
try {
// 2. 发起请求
let response = await fetch('/api/posts/');
// 3. 检查状态码
if (response.status === 404) {
container.innerHTML = '<p>暂无文章</p>';
return;
}
if (response.status === 403) {
container.innerHTML = '<p>请先登录</p>';
return;
}
if (!response.ok) {
throw new Error(`服务器错误:${response.status}`);
}
// 4. 解析 JSON
let data = await response.json();
// 5. 渲染数据
if (data.results.length === 0) {
container.innerHTML = '<p>暂无文章</p>';
return;
}
container.innerHTML = data.results.map(post => `
<div class="post-card">
<h3><a href="/posts/${post.id}/">${post.title}</a></h3>
<p>${post.author_name} · ${new Date(post.created_at).toLocaleDateString()}</p>
</div>
`).join('');
} catch (error) {
// 6. 错误处理
container.innerHTML = `
<div class="error">
<p>⚠️ 加载失败:${error.message}</p>
<button onclick="loadPosts()">重试</button>
</div>
`;
}
}// 封装通用的 API 请求函数
async function apiRequest(url, options = {}) {
const defaults = {
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken'),
},
};
const config = {
...defaults,
...options,
headers: { ...defaults.headers, ...options.headers },
};
try {
let response = await fetch(url, config);
if (response.status === 401) {
window.location.href = '/login/';
return null;
}
if (!response.ok) {
let error = await response.json().catch(() => ({}));
throw new Error(error.detail || `请求失败:${response.status}`);
}
if (response.status === 204) return true; // DELETE 成功
return await response.json();
} catch (error) {
showToast(error.message, 'error');
throw error;
}
}
// 使用
let posts = await apiRequest('/api/posts/');
await apiRequest('/api/posts/', {
method: 'POST',
body: JSON.stringify({ title: 'Hello', content: '...' }),
});
await apiRequest('/api/posts/1/', { method: 'DELETE' });// Token 认证流程
// 1. 用户登录 → 后端返回 Token
// 2. 前端存储 Token(localStorage 或 Cookie)
// 3. 后续请求带 Token
// 登录获取 Token
async function login(username, password) {
let response = await fetch('/api-token-auth/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
if (response.ok) {
let data = await response.json();
localStorage.setItem('token', data.token); // 存储 Token
return data.token;
}
return null;
}
// 带 Token 的请求
async function authFetch(url, options = {}) {
let token = localStorage.getItem('token');
return fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
'Authorization': `Token ${token}`,
...options.headers,
},
});
}
// 使用
let data = await authFetch('/api/posts/').then(r => r.json());<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>商品列表</title>
<style>
.products { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; }
.card { border: 1px solid #eee; border-radius: 8px; padding: 16px; }
.card img { width: 100%; border-radius: 4px; }
.price { color: #e74c3c; font-size: 20px; font-weight: bold; }
.loading { text-align: center; padding: 40px; color: #888; }
.error { text-align: center; padding: 40px; color: red; }
.search-bar { margin: 20px 0; }
.search-bar input { padding: 10px; width: 300px; }
.search-bar button { padding: 10px 20px; }
.pagination { margin: 20px 0; text-align: center; }
.pagination button { margin: 0 5px; padding: 8px 16px; }
</style>
</head>
<body>
<h1>🛒 商品列表</h1>
<div class="search-bar">
<input type="text" id="searchInput" placeholder="搜索商品...">
<button onclick="search()">搜索</button>
</div>
<div id="products" class="products">
<div class="loading">加载中...</div>
</div>
<div id="pagination" class="pagination"></div>
<script>
let currentPage = 1;
async function loadProducts(page = 1, search = '') {
let container = document.querySelector('#products');
container.innerHTML = '<div class="loading">加载中...</div>';
try {
let url = `/api/products/?page=${page}`;
if (search) url += `&search=${encodeURIComponent(search)}`;
let response = await fetch(url);
let data = await response.json();
if (data.results.length === 0) {
container.innerHTML = '<p>暂无商品</p>';
return;
}
container.innerHTML = data.results.map(product => `
<div class="card">
<img src="${product.image || '/static/images/default.png'}" alt="${product.name}">
<h3>${product.name}</h3>
<p class="price">¥${product.price}</p>
<p>已售 ${product.sales}</p>
<button onclick="addToCart(${product.id})">加入购物车</button>
</div>
`).join('');
// 分页
renderPagination(data.count, page);
currentPage = page;
} catch (error) {
container.innerHTML = `
<div class="error">
⚠️ 加载失败
<button onclick="loadProducts(${page})">重试</button>
</div>
`;
}
}
function renderPagination(total, current) {
let pages = Math.ceil(total / 12);
let html = '';
if (current > 1) html += `<button onclick="loadProducts(${current-1})">上一页</button>`;
for (let i = 1; i <= pages; i++) {
html += `<button onclick="loadProducts(${i})" ${i === current ? 'disabled' : ''}>${i}</button>`;
}
if (current < pages) html += `<button onclick="loadProducts(${current+1})">下一页</button>`;
document.querySelector('#pagination').innerHTML = html;
}
function search() {
let keyword = document.querySelector('#searchInput').value;
loadProducts(1, keyword);
}
async function addToCart(productId) {
let response = await fetch(`/api/cart/add/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken'),
},
body: JSON.stringify({ product_id: productId, quantity: 1 }),
});
if (response.ok) {
alert('已加入购物车!');
} else {
alert('添加失败');
}
}
function getCookie(name) {
let v = document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)');
return v ? v.pop() : '';
}
// 页面加载完成后执行
document.addEventListener('DOMContentLoaded', () => loadProducts());
</script>
</body>
</html># 创建一个完整的文章管理页面:
# - 文章列表(fetch 加载)
# - 创建文章(表单 + fetch POST)
# - 编辑文章(表单预填充 + fetch PUT)
# - 删除文章(确认 + fetch DELETE)
# - 每个操作都有加载状态和错误提示# 用 JS 实现购物车页面:
# - 从 Session API 加载购物车数据
# - 修改数量(实时更新)
# - 删除商品
# - 实时计算总价
# - 结算按钮# 实现实时搜索:
# - 输入框输入时实时搜索(防抖 debounce 300ms)
# - 分类下拉筛选
# - 排序选择
# - 搜索结果高亮关键词fetch(url).then(r => r.json()) | |
fetch(url, {method:'POST', body:JSON.stringify(data)}) | |
🎉 前端基础阶段完结!
Day 67-70 你掌握了前端三件套:
✅ HTML(骨架)· CSS(样式)· JavaScript(交互)
✅ 前后端分离 · fetch API · Token 认证
接下来进入数据科学阶段(Day 71-78)—— NumPy、Pandas、Matplotlib。Python 在数据分析领域的威力即将展现!