Django+Vue前后端分离开发:实战构建网址导航系统
2026/9/15 22:23:48 网站建设 项目流程

简介:基于Django+Vue前后端分离架构的网址导航系统毕业设计项目,面向软件工程、计算机科学等专业的学生与开发者,可直接用于毕设、课程设计或初期项目演示。资源共108个文件,其中25个Python源码文件、22个Vue前端组件、21个pyc编译文件以及20张运行截图,另含XML配置、JS脚本和JSON数据文件,压缩包仅1.1MB,目录划分清晰,便于按模块查阅。目前已有84人学习下载,适合希望快速上手前后端分离开发或需要导航类网站完整案例的读者。代码经过测试可正常运行,整体涵盖了用户管理、网址分类、收藏维护、搜索跳转等典型功能,工程配置与使用说明均已打包在内。读者可在现有基础上替换数据、调整界面或增加新模块,既能支撑毕业设计答辩,也能作为Django与Vue交互的实战学习资料。

1. 这个网址导航项目,先把前后端分离的账算清楚

一个网页里排满了分类、链接、搜索框,点一下就走,这种网址导航站看起来简单,真正动手做起来却绕不开三个问题:数据从哪来、谁来维护、前端怎么展示。用 Django + Vue 做前后端分离,本质上是把“网址数据的管理”和“页面的渲染交互”拆成两个独立工程,一个跑在 8000 端口提供 API,一个跑在 5173 端口做界面。这套组合在毕业设计里常见,在企业内部的信息门户、团队收藏夹、个人书签同步这类场景里同样能用。

适合什么人看:已经学过 Python 基础、想用 Django 做后端,但对 Vue 前端和前后端联调不熟的人;或者反过来,前端能写页面,但不知道怎么用 Django 把数据稳定地吐出来。这份标题里的项目涉及用户登录、分类管理、链接增删改查、前台分类展示,是典型的管理后台加门户首页结构。文章会先讲清楚数据模型怎么设计,再给出后端 API 的完整实现思路和前端对接 token 的具体做法,最后落在一个可直接照抄的部署验证方案上。

2. 网址导航的数据模型与 Django 项目骨架搭建

2.1 先分清楚:User、Category、Link 三个模型够不够用

绝大多数网址导航系统只需要三个核心模型:用户、分类、链接。用户模型直接复用 Django 自带的auth.User,不要自己去建用户表,省去注册、密码加密、会话管理的一堆重复工作。分类模型存导航栏里的分组名称和排序权重,链接模型存具体的网址、标题、图标和所属分类。

先抛一个常见设计误区:很多同学一上来就给 Link 加很多冗余字段,比如点击量、标签、备注、缩略图、是否推荐,结果写页面的时候发现一半字段用不上,还拖慢了表单提交。我一般会按“最小可用”原则先只保留必要字段,后面真需要再做迁移加字段。Django 的 migration 机制支撑后期加字段很容易,但一开始就把模型堆满,前端联调和测试数据的成本会明显上升。

下面是我建议的模型结构。分类和链接之间用外键关联,分类删除时级联删除下面的链接,这在管理后台里省事,但要注意一点:真实生产环境如果链接数据很值钱,应该改成PROTECT,阻止误删。

from django.db import models from django.contrib.auth.models import User class Category(models.Model): name = models.CharField("分类名称", max_length=50, unique=True) icon = models.CharField("图标类名", max_length=100, blank=True, default="") sort = models.IntegerField("排序", default=0, help_text="数字越小越靠前") created_at = models.DateTimeField("创建时间", auto_now_add=True) class Meta: ordering = ["sort", "id"] verbose_name = "导航分类" verbose_name_plural = verbose_name def __str__(self): return self.name class Link(models.Model): title = models.CharField("网站名称", max_length=100) url = models.URLField("网址", max_length=500) desc = models.CharField("描述", max_length=200, blank=True, default="") icon = models.CharField("图标", max_length=500, blank=True, default="") category = models.ForeignKey( Category, on_delete=models.CASCADE, related_name="links", verbose_name="所属分类", ) creator = models.ForeignKey( User, on_delete=models.SET_NULL, null=True, blank=True, related_name="links", verbose_name="创建人", ) is_active = models.BooleanField("是否显示", default=True) created_at = models.DateTimeField("创建时间", auto_now_add=True) class Meta: ordering = ["id"] verbose_name = "网址链接" verbose_name_plural = verbose_name def __str__(self): return self.title

这个模型文件解决了三件事:一是通过ordering控制列表默认排序,分类按 sort 升序、链接按 id 升序,新加的链接自动排在后面;二是用related_name="links"让分类对象可以直接category.links.all()取出该分类下所有链接,反向查询少写一长串filter(category=xxx);三是is_active字段可以在不删除数据的前提下临时下架某个链接,适用于审核场景。登录用户通过creator记录,后续要做“我的添加记录”就不用额外设计表了。

2.2 新建 Django 项目时的目录规划

搭建项目时建议按前后端分离的思路建两个顶层目录:backend/放 Django 工程,frontend/放 Vue 工程。很多教程把前后端代码混在一个项目里,目录一深就分不清哪段是 Python、哪段是 Node,联调的时候两边依赖还容易互相干扰。

# 在项目根目录下操作 mkdir nav-navigation && cd nav-navigation python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install django djangorestframework django-cors-headers pip install mysqlclient # 如果使用 MySQL,见下方说明 django-admin startproject config . python manage.py startapp nav

重点说下数据库选型。默认 SQLite 在开发阶段完全够用,零配置、单文件、克隆就能跑。但如果项目要求必须用 MySQL,mysqlclient在 Windows 上经常安装失败,建议直接装pymysql,然后在config/__init__.py里加入这几行,注意首字母大小写要写对,install_as_MySQLdb()是固定写法。

import pymysql pymysql.install_as_MySQLdb()

settings.py里把新应用注册进INSTALLED_APPS,再配置好数据库连接和跨域白名单。跨域这里先不展开,第 5 章有完整说明。

2.3 建好模型先不写业务,直接把表建出来

这一步很多人只执行 makemigrations 就急着写代码,我建议先迁移、再进 Django 自带 admin 把分类数据录几条,这样后端 API 写完调试时就有现成数据可以看。

python manage.py makemigrations nav python manage.py migrate python manage.py createsuperuser python manage.py runserver

然后打开/admin/登录,把 Navigation 里的分类和链接各录几个。录入时注意分类名不要用“常用”“热门”这种语义模糊的词,最好用“开发工具”“学习资料”“设计素材”这种一眼能看懂用途的分类,方便后面验证前端分组渲染效果。admin 后台默认样式比较朴素,不影响功能,等所有接口调通了再考虑美化。

3. 用 DRF 把导航站后端 API 一次性写明白

3.1 为什么导航站这类项目更推荐 DRF 而不是手写 JsonResponse

导航站的核心操作是分类和链接的增删改查,用 Django 原生JsonResponse也能做,但要做分页、过滤、权限校验、序列化嵌套这些功能时,手写代码量会成倍增加。Django REST Framework(DRF)的价值不在于少写几行,而在于把“请求解析、参数校验、权限判定、响应序列化”这四件事标准化了。

以“新增一个链接”为例,手写方案需要自己取request.bodyjson.loads()解析、手动校验url格式、判断用户是否登录、最后序列化返回。DRF 里这些分别由序列化器字段校验、认证类、权限类解决,而且校验失败时自动返回 400 状态码和错误信息,前后端联调时前端能直接拿到字段级别的错误提示。

你在网上的搜索记录里应该见过大量的“vue 前后端分离请求 token 处理”“django 前后端分离”关键词,其实核心痛点就在这一层:前端发请求要带 token,后端接口要识别 token 对应哪个用户,还得返回统一的 JSON 结构。DRF 配合SimpleJWT是目前最主流的组合,不需要自己实现 token 签发。

3.2 序列化器与视图集:增删改查的完整代码

先写序列化器。分类和链接各建一个 Serializer,链接序列化器里把分类的 id 和名称都展示出来,这样前端拿到数据后直接用category_name渲染分组标题,不用再发一个请求去查分类表。

from rest_framework import serializers from .models import Category, Link class CategorySerializer(serializers.ModelSerializer): class Meta: model = Category fields = ["id", "name", "icon", "sort"] class LinkSerializer(serializers.ModelSerializer): category_name = serializers.CharField(source="category.name", read_only=True) class Meta: model = Link fields = [ "id", "title", "url", "desc", "icon", "category", "category_name", "creator", "is_active" ] extra_kwargs = { "creator": {"read_only": True}, }

逻辑说明:category_name是只读字段,用source指定来源是关联分类对象的 name 属性,这样 GET 请求返回的数据里自动带上分类名。creator设为只读,防止用户通过请求体伪造创建人,创建人必须由后端从登录态中取。

视图集用 ModelViewSet,它把 list、create、retrieve、update、destroy 五个操作一次性生成,对于导航站这种标准 CRUD 项目非常合适。重写perform_create的目的只有一个:让当前登录用户自动成为creator

from rest_framework import viewsets, permissions from rest_framework.response import Response from .models import Category, Link from .serializers import CategorySerializer, LinkSerializer class CategoryViewSet(viewsets.ModelViewSet): queryset = Category.objects.all() serializer_class = CategorySerializer class LinkViewSet(viewsets.ModelViewSet): queryset = Link.objects.filter(is_active=True) serializer_class = LinkSerializer permission_classes = [permissions.IsAuthenticatedOrReadOnly] def perform_create(self, serializer): serializer.save(creator=self.request.user)

IsAuthenticatedOrReadOnly的含义是:GET 请求任何人可访问,POST、PUT、PATCH、DELETE 要求登录。导航站这类系统的访问特点就是“前台公开读,后台登录写”,这个权限类正好匹配。查询集里直接 filter 掉is_active=False的链接,前端拿到的永远是可展示数据。

3.3 路由注册与 API 端点对照表

路由用 DRF 的 DefaultRouter 注册,register之后所有标准端点都自动生成,不用手动写as_view()

from django.urls import path, include from rest_framework.routers import DefaultRouter from . import views router = DefaultRouter() router.register("categories", views.CategoryViewSet) router.register("links", views.LinkViewSet) urlpatterns = [ path("api/", include(router.urls)), path("api/auth/", include("rest_framework.urls")), ]

注意rest_framework.urls提供的是 DRF 自带的登录登出页面,它依赖 Session 认证,适合开发调试。生产环境一般用 JWT 做认证,需要单独配 token 的获取和刷新地址。

API 端点对照表:

方法路径功能是否需要登录
GET/api/categories/获取分类列表
POST/api/categories/新建分类
GET/api/links/获取链接列表
POST/api/links/新增链接
GET/api/links/1/获取链接详情
PUT/PATCH/api/links/1/修改链接
DELETE/api/links/1/删除链接

3.4 用 SimpleJWT 搞定登录态

安装djangorestframework-simplejwt后,在 settings 里配置认证类和默认权限:

REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": [ "rest_framework_simplejwt.authentication.JWTAuthentication", ], "DEFAULT_PERMISSION_CLASSES": [ "rest_framework.permissions.IsAuthenticatedOrReadOnly", ], }

然后在项目的 urls.py 里加 token 获取和刷新的路由:

from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView urlpatterns += [ path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), ]

到这里,后端的核心逻辑已经完整了。用 Postman 测试一下:未登录访问/api/links/能拿到 JSON 列表;登录后 POST 一个新链接,响应里带idcategory_name。确认这两点通过,再进前端开发。

4. Vue 3 + Element Plus 前端:从装依赖到对接 token

4.1 前端项目结构与 axios 实例封装

前端用 Vite 创建 Vue 3 项目,配合 Element Plus 做管理界面,vue-router负责路由跳转,piniavuex管理用户状态。不管用什么脚手架,第一步一定是做 axios 的二次封装,把 baseURL、超时时间、请求拦截器、响应拦截器集中在一个文件里,避免每个组件里重复写axios.get的完整路径。

// src/utils/request.js import axios from "axios"; import { ElMessage } from "element-plus"; import { useUserStore } from "@/stores/user"; import router from "@/router"; const request = axios.create({ baseURL: "http://127.0.0.1:8000/api", timeout: 10000, }); // 请求拦截器:自动附带 token request.interceptors.request.use( (config) => { const userStore = useUserStore(); if (userStore.token) { config.headers.Authorization = `Bearer ${userStore.token}`; } return config; }, (error) => Promise.reject(error) ); // 响应拦截器:统一处理 401 和业务错误 request.interceptors.response.use( (response) => response.data, (error) => { if (error.response?.status === 401) { const userStore = useUserStore(); userStore.logout(); router.push("/login"); } ElMessage.error(error.response?.data?.detail || "请求失败"); return Promise.reject(error); } ); export default request;

这段代码有三个关键设计。第一,baseURL直接指向 Django 的/api前缀,和路由注册保持一致,改动时只改这一处。第二,请求拦截器从 pinia 的 userStore 里拿 token 放进Authorization头,注意Bearer后面有一个空格,这是 JWT 的标准格式,漏掉空格后端解析会报Authentication credentials were not provided。第三,响应拦截器里把response直接解包成response.data,这样页面里调接口拿到的就是纯数据,不用到处写.data.data

4.2 token 存储与登录流程

token 不能只存在内存里,页面刷新后 pinia 状态会重置,所以必须同步到 localStorage。登录流程用户输入账号密码,后端返回accessrefresh两个 token,前端都存下来,access用于每次请求的身份验证,refresh用于 access 过期后刷新。

// src/stores/user.js import { defineStore } from "pinia"; import request from "@/utils/request"; export const useUserStore = defineStore("user", { state: () => ({ token: localStorage.getItem("access_token") || "", refreshToken: localStorage.getItem("refresh_token") || "", userInfo: JSON.parse(localStorage.getItem("user_info") || "null"), }), actions: { async login(username, password) { const data = await request.post("/token/", { username, password }); this.token = data.access; this.refreshToken = data.refresh; localStorage.setItem("access_token", data.access); localStorage.setItem("refresh_token", data.refresh); }, logout() { this.token = ""; this.refreshToken = ""; this.userInfo = null; localStorage.removeItem("access_token"); localStorage.removeItem("refresh_token"); localStorage.removeItem("user_info"); }, }, });

刷新 token 的调用时机:axios 响应拦截器里判断 access 过期后,用 refreshToken 调/token/refresh/接口拿新的 access,然后把原来的请求重新发一次。这个逻辑有点绕,但对于 5 年以上经验的开发者来说,直接写一个refreshToken函数并配合队列防止并发刷新是比较完善的做法。导航站这类低并发管理后台,简单实现即可,不追复杂方案。

4.3 导航首页的数据加载与分类分组渲染

导航首页的最终效果是一排分类,每个分类下面是一堆链接卡片。前端拿到/links/的全量列表后,在前端按category_name分组,还是后端按分类嵌套返回?两种都能做,我建议前端分组。理由很直接:导航站链接数量一般几百条以内,一次取回全量数据后reduce一下,代码清晰、接口耗时短。数据库压力不是这个规模的项目该优先考虑的问题。

<!-- src/views/Home.vue 核心逻辑 --> <script setup> import { ref, onMounted, computed } from "vue"; import request from "@/utils/request"; const links = ref([]); const groupedLinks = computed(() => { const map = {}; links.value.forEach((item) => { if (!map[item.category_name]) { map[item.category_name] = []; } map[item.category_name].push(item); }); return map; }); onMounted(async () => { links.value = await request.get("/links/"); }); </script>

这里有几个 Vue 使用细节。computed里重新构建的groupedLinks会在links改变时自动重算,这是 Vue 3 响应式系统的特点,不要在forEach循环里手动 push 到reactive数组后再手动触发更新。模板里用Object.keys(groupedLinks)遍历分类渲染,注意computed返回的对象在模板中直接访问即可。

图标字段icon可以存图片 URL、字体图标类名、或者留空。前端显示时优先用 favicon 服务,https://域名/favicon.ico这种方式获取率不高,有很多公共的 favicon 接口可以把完整域名传进去返回图标,这些服务在搜索关键词“vue 网址导航”里能翻到不少现成方案。对于前后端分离项目,我一般建议在desc里让用户填写,或者在录入链接时自动抓取,避免前端写死逻辑。

4.4 路由守卫与页面缓存

管理后台通常需要登录才能访问,前端不能只靠隐藏入口,必须在路由跳转时做拦截。

// src/router/index.js import { createRouter, createWebHistory } from "vue-router"; const router = createRouter({ history: createWebHistory(), routes: [ { path: "/", component: () => import("@/views/Home.vue") }, { path: "/login", component: () => import("@/views/Login.vue") }, { path: "/admin", component: () => import("@/views/Admin.vue"), meta: { requiresAuth: true }, }, ], }); router.beforeEach((to, from, next) => { const token = localStorage.getItem("access_token"); if (to.meta.requiresAuth && !token) { next("/login"); } else { next(); } });

路由守卫的粒度控制:meta.requiresAuth标记需要登录的页面,判断依据是 localStorage 里有没有 token。缺点是在 token 已过期的情况下,前端仍然放行进入页面,等接口调用失败后在响应拦截器里被踢回登录页。导航站后台是低敏感场景,这样做体验更好,用户不会刚登录进去几分钟就被莫名跳转。

关于“vue 路由参数”和“vue 打包后布局异常”这两个搜索热词,前者对应的是导航链接跳转时传参的问题,比如router.push({ name: "detail", params: { id: 1 } }),注意 Vue Router 4 中params不能与path混用;后者通常是资源路径问题,会在第 5 章部署部分说明。

5. 跨域、打包与部署前的验证技巧

5.1 CORS 配置的两个阶段

前后端分离开发时前端跑在 5173 端口,后端跑在 8000 端口,端口不同就是跨域,必须处理。开发环境用django-cors-headers简单粗暴全放开,生产环境则要把白名单收紧。实例代码在 settings.py 中的位置不同,务必保证corsheaders.middleware.CorsMiddleware放在CommonMiddleware之前,否则某些情况下 CORS 响应头不会生效。

INSTALLED_APPS = [ # ... "corsheaders", "rest_framework", ] MIDDLEWARE = [ "corsheaders.middleware.CorsMiddleware", # ... ] CORS_ALLOWED_ORIGINS = [ "http://localhost:5173", "http://127.0.0.1:5173", ] CORS_ALLOW_CREDENTIALS = True

生产环境把CORS_ALLOWED_ORIGINS换成 Nginx 里的前端域名,比如https://nav.example.com,不要用CORS_ALLOW_ALL_ORIGINS = True,那会暴露所有 API 给任意网站调用,有些教程图省事这样写,安全上是明显的疏漏。

5.2 Nginx 部署:history 路由与静态文件

Vue 打包后默认是hash模式,URL 里带#,丑且不利于分享。换成history模式后,非首页刷新时会请求一个真实的路径,Django 后端没有这个路由,返回 404,所以 Nginx 要配置 try_files 把所有请求回退到 index.html。

server { listen 80; server_name nav.example.com; root /var/www/nav-frontend/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /static/ { alias /var/www/nav-backend/static/; } }

三个 location 的职责不同:/负责前端页面路由回退;/api/反向代理到 Django,同时带上真实 IP 头;/static/指向 Django 的静态文件目录,admin 后台样式和 DRF 的页面依赖这部分。不要忘了在 Django 的 settings.py 里设DEBUG = False后执行python manage.py collectstatic,否则 admin 后台会没有样式。collectstatic 时会提示选择是否覆盖同名文件,确认即可,注意STATIC_ROOTSTATICFILES_DIRS不要指向同一个目录,否则会一直报错。

5.3 上线前必查的几个验证点

部署完成后,打开浏览器按顺序检查这几个地方:

第一,访问前端域名,看到首页且没有跨域报错。开发者工具 Network 面板里任意一个/api/请求的响应头应包含Access-Control-Allow-Origin,且值正是当前域名。如果没有,检查CORS_ALLOWED_ORIGINS是否包含https://前缀和端口号。

第二,登录后刷新页面,导航菜单保持登录态。正常情况刷新后 pinia 从 localStorage 读 token,路由守卫放行,接口请求带Bearer token。如果刷新后跳回登录页,检查响应拦截器的 401 处理逻辑是否误判——401 也分“token 无效”和“token 过期”,过期场景应先尝试刷新再决定是否登出。

第三,新增链接时图标能正常加载。图标是外部 URL 时存在图片防盗链的问题,前端页面如果加了 Referrer Policy 策略,部分网站的图标会加载失败。常见做法是在index.html里加<meta name="referrer" content="no-referrer">,或者直接不使用外站图标,改用本地上传。

另外提一个搜索热词里反复出现的“宝塔部署 Django”。宝塔面板部署 Django 的原理和 Nginx 手动配置是一样的,把项目文件传到服务器、建好 Python 虚拟环境、安装依赖、用 uwsgi 或 gunicorn 启动后端、设置反向代理到前端静态目录即可。唯一的坑是宝塔默认的 Python 版本可能较旧,进入终端后先执行python3 -V确认版本,不要直接pip install装到系统环境里,务必先激活虚拟环境,否则包管理器会把依赖装到全局导致ModuleNotFoundError

对于 5 年以上经验的开发者,有一个值得留意的地方:网址导航系统虽然业务简单,但它的 API 设计模式可以复用到企业内部的工具导航、云资源导航、内部系统收录这类场景。区别只是将Link换成ServiceCategory换成DepartmentBusinessLine,前端的分类分组渲染逻辑可以原样保留。数据模型不变、接口风格不变、前端组件复用,这就是项目能快速落地为其他应用的原因。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询