从零跑通 Django:一个双非学生的第一个 Python 网站实战

王杰
2026-01-04 19:54
阅读 1607

去年冬天,我还在公司里吭哧吭哧地维护那套老掉牙的 Java 后端服务——没错,就是那种连日志格式都靠手动拼字符串、部署全靠运维大哥人肉拷贝 JAR 包的“经典”架构。作为一个在分布式系统方向摸爬滚打三年多的老油条(虽然学校是双非,但代码量绝对不输985),其实我对 Django 一直有点偏见:总觉得它“太重”、“不够底层”、“玩具框架”。直到上周五晚上,产品经理甩过来一句:“我们要做个内部工具原型,下周三演示,你用 Python 快速搭一下,前端简单点就行。”

那一刻,我盯着屏幕上的 Slack 消息,心里一万只草泥马奔腾而过。但转念一想:跳槽面试老被问“会不会 Django”,简历上写“熟悉 Python 生态”却连个 Hello World 都没跑过,确实有点虚。行吧,就当给自己补课了。


为什么选 Django?别杠,先跑起来

说实话,作为 Vim 党,我平时连 VS Code 都很少碰,更别说 PyCharm 这种“全家桶”了。但 Django 的魅力在于——开箱即用。不用配 Maven,不用搞 Webpack,甚至不用手动连数据库驱动。一条 pip install django + django-admin startproject,就能看到那个经典的火箭图标页面。对于被 Spring Boot 的 YAML 嵌套折磨到麻木的我来说,这简直是救赎。

而且,Django 自带 admin 后台、用户认证、ORM、路由……这些在 Java 里要集成一堆 Starter 才能搞定的功能,它原生就有。对我们这种既要写后端又要糊前端的“全栈苦力”来说,省下的时间够多改三版需求了(感谢产品经理的反复横跳)。


项目目标:做一个简单的任务看板

既然是入门,那就做最经典的 Todo List 吧,但加点料:支持多人协作、任务状态流转(待办/进行中/完成)、以及——必须有前端交互。别笑,很多教程到模板渲染就结束了,但现实项目里没有 JavaScript 的页面根本没法交差。

技术栈很朴素:

  • 后端:Django 4.2(最新 LTS)
  • 前端:原生 HTML + CSS + 一点点 JavaScript(拒绝 Vue/React,毕竟只是内部工具)
  • 数据库:SQLite(开发阶段够用,上线再换 PostgreSQL)

第一步:初始化项目 & App

# 创建虚拟环境(别偷懒!)
python -m venv .venv
source .venv/bin/activate  # Linux/Mac
# .venv\Scripts\activate   # Windows

pip install django

# 创建项目
django-admin startproject taskboard
cd taskboard

# 创建核心 App
python manage.py startapp tasks

然后在 taskboard/settings.py 里注册 App:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'tasks',  # ← 加上这行
]

第二步:设计数据库模型 —— 别小看 ORM

作为搞过分布式事务的人,我对数据一致性有执念。虽然只是个小项目,但模型设计不能糊弄。

# tasks/models.py
from django.db import models
from django.contrib.auth.models import User

class Task(models.Model):
    STATUS_CHOICES = [
        ('todo', '待办'),
        ('doing', '进行中'),
        ('done', '已完成'),
    ]
    
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='todo')
    assignee = models.ForeignKey(User, on_delete=models.CASCADE, related_name='assigned_tasks')
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.title

注意几个细节:

  • ForeignKey 关联 Django 内置的 User 模型,省去自己写登录逻辑
  • on_delete=models.CASCADE:用户删了,任务也跟着删(业务上合理)
  • 时间字段自动管理,避免手动赋值出错

然后生成并执行迁移:

python manage.py makemigrations
python manage.py migrate

第三步:写视图 —— 别只用函数视图!

很多教程只教 FBV(Function-Based View),但作为“有点经验”的人,我直接上 CBV(Class-Based View),因为:

  • 代码更结构化
  • 复用性强(比如分页、权限校验)
  • 更容易扩展成 API
# tasks/views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.views.generic import ListView, CreateView, UpdateView
from django.contrib.auth.mixins import LoginRequiredMixin
from .models import Task
from .forms import TaskForm

class TaskListView(LoginRequiredMixin, ListView):
    model = Task
    template_name = 'tasks/list.html'
    context_object_name = 'tasks'
    
    def get_queryset(self):
        # 只显示当前用户的任务
        return Task.objects.filter(assignee=self.request.user)

class TaskCreateView(LoginRequiredMixin, CreateView):
    model = Task
    form_class = TaskForm
    template_name = 'tasks/form.html'
    success_url = '/tasks/'

    def form_valid(self, form):
        form.instance.assignee = self.request.user
        return super().form_valid(form)

class TaskUpdateView(LoginRequiredMixin, UpdateView):
    model = Task
    form_class = TaskForm
    template_name = 'tasks/form.html'
    success_url = '/tasks/'

配套的表单:

# tasks/forms.py
from django import forms
from .models import Task

class TaskForm(forms.ModelForm):
    class Meta:
        model = Task
        fields = ['title', 'description', 'status']
        widgets = {
            'description': forms.Textarea(attrs={'rows': 3}),
        }

第四步:配置 URL 路由

# taskboard/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('tasks/', include('tasks.urls')),
    path('', lambda r: redirect('/tasks/')),  # 根路径跳转
]

# tasks/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.TaskListView.as_view(), name='task-list'),
    path('new/', views.TaskCreateView.as_view(), name='task-create'),
    path('<int:pk>/edit/', views.TaskUpdateView.as_view(), name='task-update'),
]

第五步:写前端模板 —— 别忘了 JavaScript!

终于到前端了!虽然我是个后端,但被产品经理骂过“页面点不动像卡死”之后,深知 交互体验的重要性

先写基础 HTML(用 Bootstrap 快速美化,别手写 CSS):

<!-- tasks/templates/tasks/list.html -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>我的任务看板</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
    <h1>我的任务</h1>
    <a href="{% url 'task-create' %}" class="btn btn-primary mb-3">新建任务</a>
    
    <div class="row">
        {% for task in tasks %}
        <div class="col-md-4 mb-3">
            <div class="card">
                <div class="card-body">
                    <h5 class="card-title">{{ task.title }}</h5>
                    <p class="card-text">{{ task.description|truncatewords:15 }}</p>
                    <small class="text-muted">状态: {{ task.get_status_display }}</small>
                    <div class="mt-2">
                        <a href="{% url 'task-update' task.pk %}" class="btn btn-sm btn-outline-secondary">编辑</a>
                        <!-- 重点:这里加 JS 交互 -->
                        <button class="btn btn-sm btn-success ms-1" 
                                onclick="updateTaskStatus({{ task.pk }}, 'done')">
                            完成
                        </button>
                    </div>
                </div>
            </div>
        </div>
        {% empty %}
        <p>暂无任务,快去创建吧!</p>
        {% endfor %}
    </div>
</div>

<!-- 引入 jQuery 和自己的 JS -->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
<script>
function updateTaskStatus(taskId, newStatus) {
    if (!confirm('确定要完成这个任务吗?')) return;
    
    $.ajax({
        url: `/api/tasks/${taskId}/`,
        type: 'PATCH',
        headers: {
            'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
        },
        data: JSON.stringify({status: newStatus}),
        contentType: 'application/json',
        success: function() {
            location.reload(); // 简单粗暴,实际可局部刷新
        },
        error: function(xhr) {
            alert('操作失败:' + xhr.responseText);
        }
    });
}
</script>
{% csrf_token %} <!-- 必须放在 body 里,供 JS 读取 -->
</body>
</html>

关键点:

  • onclick 绑定按钮,避免表单提交刷新页面
  • 通过 AJAX 发送 PATCH 请求更新状态
  • CSRF Token 处理:Django 默认开启 CSRF 保护,JS 请求必须带上 token(很多人在这里踩坑!)
  • 成功后 location.reload() —— 虽然糙,但快。真要优化可以只更新卡片状态

第六步:加个简易 API —— 为前端解耦做准备

虽然现在用的是服务端渲染,但未来可能要拆前后端分离。提前暴露 API 是好习惯。

# tasks/views.py 新增
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
import json

@method_decorator(csrf_exempt, name='dispatch')
class TaskAPIUpdateView(View):
    def patch(self, request, pk):
        try:
            task = Task.objects.get(pk=pk, assignee=request.user)
            data = json.loads(request.body)
            task.status = data.get('status', task.status)
            task.save()
            return JsonResponse({'status': 'success'})
        except Task.DoesNotExist:
            return JsonResponse({'error': 'Task not found'}, status=404)
        except Exception as e:
            return JsonResponse({'error': str(e)}, status=400)

# urls.py 补充
path('api/tasks/<int:pk>/', TaskAPIUpdateView.as_view()),

吐槽csrf_exempt 虽然方便,但生产环境千万别这么干!应该用 django-cors-headers + 正确的 CSRF 策略。不过入门阶段,先跑通再说。


调试与踩坑记录

坑1:静态文件 404

开发时 CSS/JS 加载失败?记得在 settings.py 开启:

DEBUG = True  # 开发环境必须为 True
# 并确保 urls.py 包含:
from django.conf import settings
from django.conf.urls.static import static

if settings.DEBUG:
    urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

坑2:CSRF Failed

前端 AJAX 报 403?除了上面提到的 token,还要确认:

  • 模板里有 {% csrf_token %}
  • JS 读取的是 input[name=csrfmiddlewaretoken] 的 value
  • 不要用 csrf_exempt 除非你清楚后果

坑3:外键关联用户报错

如果创建任务时没传 assignee,会报 NOT NULL constraint failed。所以 CBV 里一定要重写 form_valid

def form_valid(self, form):
    form.instance.assignee = self.request.user  # ← 关键!
    return super().form_valid(form)

性能与生产考虑

虽然只是 demo,但作为老后端,我忍不住想提几点:

问题 开发阶段 生产建议
数据库 SQLite 换 PostgreSQL/MySQL
静态文件 直接 serve 用 Nginx 或 CDN
并发 单进程 Gunicorn + Nginx
安全 DEBUG=True 关闭 DEBUG,设 ALLOWED_HOSTS
缓存 加 Redis 缓存热点数据

特别是 ALLOWED_HOSTS,上线前必须配,否则 500 错误让你怀疑人生:

# settings.py
ALLOWED_HOSTS = ['your-domain.com', 'localhost']

最后:为什么我觉得 Django 值得学?

以前总觉得“大厂不用 Django”,但最近面试几家独角兽,发现不少内部工具、数据分析平台都在用它。原因很简单:开发效率碾压一切。对我这种想跳槽的人来说,掌握一个能快速产出 MVP 的框架,比死磕底层原理更实用。

而且,Django 的设计哲学(DRY、明确优于隐式)和我在分布式系统中学到的原则高度一致。ORM 虽然“魔法”,但理解其生成的 SQL 后,反而觉得比手写 MyBatis 更安全。


写在结尾

折腾了两天,终于把这套东西跑起来了。周一演示时,产品经理居然说“这个完成按钮的动效不错”(其实只是弹了个 confirm)……算了,能糊弄过去就行。

如果你也是双非出身、自学编程、在小公司挣扎求生的程序员,别被“必须精通源码”的焦虑绑架。先做出能跑的东西,再慢慢深挖。Django 不是银弹,但它能让你在 deadline 前保住头发。

代码已开源(假装有):github.com/yourname/taskboard-django
(其实还没 push,但先占个坑位)

下期预告:《用 Celery + Redis 给 Django 任务看板加异步通知》——毕竟,老板说“要有邮件提醒”,而我又不想阻塞主线程。


作者:某双非院校大二(心理年龄30+)自学者,Vim 党,分布式系统爱好者,正在逃离舒适区。
写作于 2024 年 6 月的一个加班夜,咖啡已凉。

评论 0

最热最新
暂无评论
王杰Lv.1
0
影响力
0
文章
0
粉丝