Skip to main content

12.点了「保存」,到底发生了什么?

我第一次做到「点击保存 → 数据进数据库」的时候,并没有想象中那么顺。

因为我心里一直有一个挥之不去的问题:

“这个保存,到底是表单在提交,还是 JS 在提交?”

如果你现在也有点迷糊,别慌,这太正常了。 这一节,我们就把**“保存这一步”彻底走一遍路径**。

1.为什么不再用默认的表单提交?

先回到一个最基础的事实:

  • <form> 默认就是 POST 提交
  • 提交后,页面会 整体刷新
  • 后端返回的是 HTML 页面

这套机制 没有错,但问题是—— 它不适合弹窗场景。

想象一下:

  • 你在 SweetAlert 弹窗里填学生信息
  • 一点保存
  • 页面整个刷新
  • 弹窗消失

体验直接崩掉。

所以我在这里做了一个选择:

用 Fetch 接管提交过程,让页面“看起来没动”,但数据已经保存。

2.怎么“拦住”表单?

很多人第一反应是:

“那我直接写 fetch 不就行了吗?”

但你会发现——表单还是会自己提交。

所以第一步不是提交,而是:

阻止它默认提交。

我们可以这样做:

1️⃣ 等页面加载完成 2️⃣ 找到 form 3️⃣ 监听 submit 4️⃣ preventDefault()

逻辑非常清楚:

你别自己跑,我让你跑,你再跑。

3.fetch 提交表单

用户填写完学生信息,点击“保存”按钮,需要提交到后端。

在 JavaScript 中使用 fetch 提交表单是最常见的现代前端发送数据的做法之一。代码如下:

# 代码位置:templates/students/student_form.html

<script src="{% static 'js/sweetalert2.js' %}" ></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.querySelector('form');
const url = actionUrl;
form.addEventListener('submit', async function (e) {
e.preventDefault(); // 阻止默认提交

const formData = new FormData(form);
const response = await fetch(url, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': formData.get('csrfmiddlewaretoken'),
},
credentials: 'same-origin' // 重要:携带 cookie
});
</script>

fetch是JavaScript的语法,很多同学一看到它就慌,其实它就做了三件事:

✅ 第一件:告诉它往哪提交

# 代码位置:templates/students/student_form.html
{% if student.pk %}
<script>
// 更新学生信息时,提交的URL
var actionUrl = "{% url 'student_update' student.pk %}";
</script>
{% else %}
<script>
// 创建学生信息时,提交的URL
var actionUrl = "{% url 'student_create' %}";
</script>
{% endif %}

actionUrl就是fetch提交的URL,就是你后端那个 CreateView

✅ 第二件:把表单数据带过去

这里我用的是 FormData,原因只有一个:

省事,而且不容易错。

const formData = new FormData(form)

它会自动帮你拿到:

  • 学生姓名
  • 学号
  • 班级
  • CSRF Token

✅ 第三件:把 CSRF 带上

这是 Django 新手必踩的坑。

我当时也是一脸问号,后来才意识到:

CSRF 就藏在表单里,你只要把它带过去就行。

headers: {
'X-CSRFToken': formData.get('csrfmiddlewaretoken'),
'X-Requested-With': 'XMLHttpRequest'
}

到这里,请求就能顺利到后台了。

4.后台返回JSON

这是第二个新手必卡点。

你会发现:

  • fetch 明明请求成功了
  • 但拿到的却是一整个 HTML 页面

原因只有一个:

CreateView 默认返回的是 render,不是 Json。

所以这一步,我们必须动后端。

在前端页面,点击“保存”按钮时,form表单数据提交到后端。

Django 就会按照正常的流程执行:

flowchart TB
A[POST 请求到达后端]
A --> B[实例化 Form<br/>get_form]

B --> C{表单是否通过校验}

C -->|通过| D[form_valid]
C -->|未通过| E[form_invalid]

那么我们可以重写form_valid()form_invalid()方法。这时不再返回HTML页面,而是返回JSON格式数据给前端。

StudentCreateView类中重新这个2个方法。代码如下:

# 代码位置:student/views.py
class StudentCreateView(CreateView):
model = Student
form_class = StudentForm
template_name = 'students/student_form.html'


def form_valid(self, form):
# 验证成后,返回json数据
return JsonResponse({
'status': 'success',
'messages': '操作成功'
}, status=200)

def form_invalid(self, form):
# 验证失败,返回json数据
errors = form.errors.as_json()
return JsonResponse({
'status': 'error',
'messages': errors
}, status=400)

此时,从前端-->后端-->前端的完整流程如下:

flowchart TD
A[前端 fetch 提交] --> B[后端收到 POST 请求]

B --> C[StudentCreateView.dispatch]
C --> D[StudentCreateView.post]
D --> E[StudentCreateView.get_form]

E --> F[form = StudentForm<br/>data=request.POST,<br/>files=request.FILES]

F --> G{form.is_valid ?}

G -->|Yes| H[form_valid]
G -->|No| I[form_invalid]

H --> J[返回 JsonResponse]
I --> J

5.前端接住结果,只做一件事

现在 Fetch 拿到的是JSON 了。

那前端就非常简单了:

  • 如果 status === success
  • 用 SweetAlert 弹个提示
Swal.fire({
icon: 'success',
title: data.messages
})

student_form.html完整代码如下:

# 代码位置:templates/students/student_form.html
{% load static %}
<link rel="stylesheet" href="{% static 'css/form.css' %}">
<link rel="stylesheet" href="{% static 'css/sweetalert2.css' %}" >
<div class="container">
{% if student.pk %}
<h2>编辑学生信息</h2>
{% else %}
<h2>添加学生信息</h2>
{% endif %}
<form method="post">
{% csrf_token %}
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label }}:</label>
{{ field }}
{% if field.help_text %}
<small class="form-text text-muted"> {{ field.help_text}} </small>
{% endif %}
</div>
{% endfor %}
<div class="handleButton">
<button type="submit" id="saveButton">保存</button>
<button type="button" id="cancelButton" onclick="window.parent.Swal.close();">取消</button>
</div>
</form>
</div>

{% if student.pk %}
<script>
var actionUrl = "{% url 'student_update' student.pk %}";
</script>
{% else %}
<script>
var actionUrl = "{% url 'student_create' %}";
</script>
{% endif %}
<script src="{% static 'js/sweetalert2.js' %}" ></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.querySelector('form');
const url = actionUrl;
form.addEventListener('submit', async function (e) {
e.preventDefault(); // 阻止默认提交

const formData = new FormData(form);

try {
const response = await fetch(url, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': formData.get('csrfmiddlewaretoken'),
},
credentials: 'same-origin' // 重要:携带 cookie
});

const data = await response.json();

if (data.status === 'success') {
Swal.fire({
icon: 'success',
title: data.message || '提交成功',
text: '学生信息已成功保存',
timer: 2000,
showConfirmButton: false
});
} else {
Swal.fire({
icon: 'error',
title: '提交失败',
text: data.message || '请检查输入内容'
});
}

console.log('服务器返回:', data);

} catch (error) {
console.error('请求出错:', error);
Swal.fire({
icon: 'error',
title: '网络错误',
text: '请检查网络连接或稍后重试'
});
}
});


});


</script>

6.验证结果

访问学生列表页面,打开浏览器的调试模式,选择console, 填写一份正确字段格式的学生信息,点击“保存”,运行效果如下:

image-20260105223645109

填写一份错误字段类型格式的学生信息,点击“保存”,运行效果如下:

image-20260105223823519

走到这里,你已经完成了很多人卡很久的一件事:

  • ✅ 用 Fetch 接管表单提交
  • ✅ Django 后端返回 Json
  • ✅ SweetAlert 做用户反馈

这已经不是“写个教学 Demo”的水平了,而是真实项目在用的交互方式。

下一步你可以立刻做的事

别停,马上选一个做:

1️⃣ 故意不填字段,看看后台会发生什么 2️⃣ 打印 data,观察 Django 返回了什么 3️⃣ 想一个问题: 👉 如果保存失败,我怎么把错误也弹出来?

下一节,我们就专门解决这个问题: 字段校验失败,怎么优雅地告诉用户。

我们继续。