☰
Django+Vue前后端分离实战:前端环境搭建与接口联调指南
2026/9/29 19:21:03 网站建设 项目流程

写网络设备运维系统那段时间,我对Django写后端接口没什么障碍,卡壳全卡在前端环境上。Node版本换来换去,依赖装到一半报错,Vite代理怎么配都连不上Django的8000端口,token带上了依然被401弹回来。当时就一个念头——要是有篇教程把这些坑挨个说清楚该多好。这篇文章就是用实战项目的角度,把Django前后端分离里"前端环境搭建"这件事从头到尾捋一遍,从Node版本选择、Vite创建工程、axios封装,到和Django接口真正连通、token处理、常见报错排查,适合正在做设备管理类系统、但被前端工程化流程卡住的后端同学,也适合想用Django+Vue做点正经项目的人参考。

1. 系统选型与整体设计思路

1.1 网络设备运维系统到底需要怎样的前后端架构

网络设备运维这个场景,多发生在机房、弱电工程、ISP或者企业内部IT团队。要管理的对象包括交换机、路由器、防火墙、无线AP这些实体设备,核心诉求通常绕不开三件事:设备台账要清晰、在线状态要实时、配置变更要有痕迹。

这类系统的后端天然适合Django,因为它自带Admin、ORM和一套成熟的数据模型管理方式。往深了说,设备列表要分页、筛选,状态要轮询刷新,配置备份要上传下载,这些动作都可以抽象成REST接口。

但前端如果继续用Django模板渲染,体验会越来越吃力。设备列表页需要在5秒内给出筛选结果,状态指示灯要自动刷新,配置对比要在一个页面里并排展示——这些交互用模板引擎写,后期维护成本相当高。所以项目采用了前后端分离的方式:Django只负责输出JSON接口,前端独立工程负责页面渲染和交互。这样一来,后端逻辑和前端展示解耦,换皮肤、加交互、做移动端适配都不用碰后端代码。

1.2 为什么前端选择Vue这套组合

前端框架可选的不算少,React、Vue、Angular,甚至Svelte。放在运维场景里,我选了Vue。理由很实际:Vue的学习曲线更平缓,模板语法对后端出身的人友好,生态里的Element Plus组件库直接提供了表格、表单、弹窗、消息提示这些后台系统高频组件。网络设备运维系统的界面90%都是表格加表单,ElTable配上ElForm能省掉大半页面开发时间。

工程层面选择了Vite而不是Vue CLI。Vite启动项目几乎不用等,保存代码热更新也是秒级反应,开发体验比Webpack时代的Vue CLI舒服很多。Vue CLI目前已经进入维护模式,新项目用Vite是社区共识。这套组合基本是当前Vue3项目的标准答案:Vite负责构建、Vue Router负责路由、Pinia负责状态存储、axios负责接口请求、Element Plus提供UI组件。

1.3 最终项目结构要达成什么目标

动手之前先把目标定清楚。我当时脑子里列了一个清单:开发者在本地执行npm run dev就能起前端,访问5173端口;请求带/api/前缀的接口会自动转发到Django的8000端口;登录后拿到token存在本地,之后每个请求自动带上;Django那边不用做额外的跨域处理,因为生产环境会由Nginx统一收口。整条链路在后面会拆开一一验证。

2. 前端环境准备与基础工具链

2.1 Node.js版本选择和安装

前端工程跑起来,第一件事就是Node.js。这一步看起来简单,踩坑的人却特别多。我见过不少同事直接去官网下载了最新的奇数版本,结果Vite启动报错,折腾半天才发现是Node版本不兼容。Vite5要求Node版本18以上,最好用LTS版本——目前推荐20.x,更稳妥的可以等Node22稳定后再切。记住一条原则:前端工程看LTS,不为追新装奇数版本。

版本管理推荐用nvm,这样不同项目可以随时切换Node版本。Windows用户去下载nvm-setup.exe安装包,macOS和Linux用户用命令安装。装好后执行两个命令切到20版本:

nvm install 20 nvm use 20

验证是否装对:

node -v npm -v

我见过很多人在这一步直接跳过了nvm,结果后面同时维护三四个项目时,每个项目要求的Node版本都不一样,上来就被折磨。如果前期觉得只有自己一个项目、没必要搞版本管理,多半会在项目中期吃到苦头。装nvm的成本不过两分钟,后面省下的时间远不止两分钟。

2.2 npm源配置与依赖安装常识

Node装好之后,npm默认源是官方源。在国内网络环境下,安装electron这类大型依赖经常卡到怀疑人生。我的做法是直接把registry切到国内镜像源:

npm config get registry npm config set registry https://registry.npmmirror.com

设置完后再查一次,确认已经生效。这一步不是必需品,但做过之后,后面npm install的体验会从"抽奖"变成"秒下"。

依赖安装还有个痛点值得一提:npm install失败的时候,很多人第一反应是重新装一遍,这是最低效的排障方式。应该先看报错信息,通常分三类——网络超时(换源或者重试)、node-gyp编译失败(本机缺少Python或C++构建工具)、依赖版本冲突(清空node_modules重新装)。如果遇到奇怪的诡异报错,可以优先尝试删除node_modules和package-lock.json,然后重新install,这个"三连"能解决大半玄学问题:

rm -rf node_modules package-lock.json npm install

2.3 包管理器选型

npm、yarn、pnpm三选一的话,我现在的习惯是pnpm。它是目前安装速度最快、磁盘占用最小的方案,通过硬链接机制让多个项目共享同一份依赖副本。用npm初始化过的项目,临时切成pnpm也不会出大问题:

npm install -g pnpm

不过这里不给读者强推,因为团队协作里最怕的就是三个人用了三种包管理器,导致lock文件互相覆盖。我的建议是:自己单独折腾随便选,团队项目统一一个。这篇文章后面的命令以npm为主,方便照着抄。

3. 创建前端工程与目录规划

3.1 用Vite初始化项目

进入项目工作目录,执行创建命令:

npm create vite@latest device-ops-web -- --template vue cd device-ops-web npm install

命令里的device-ops-web是项目名字,模板选用Vue。Vite创建过程会问你用JavaScript还是TypeScript,这个问题在团队场景下可以讨论,个人项目我看着办。给个参考:网络设备运维系统如果计划长期维护、多人参与,建议直接上TypeScript,字段类型定义能少很多低级错误;如果只是为了快速打通前后端流程,JavaScript更省心,少一层编译报错。我第一次做这个项目时选了JavaScript,因为核心目标是验证链路通不通,等系统长大了再迁移也来得及。

初始化完成后,先执行npm run dev启动一次,浏览器打开http://localhost:5173能看到Vite欢迎页,说明最基础的工程已经活了。注意不要跳过这一步直接改代码,先确认基线环境是好的,后面出了问题能减少排查范围。

3.2 安装项目运行所需的核心依赖

基础依赖一共五件套:

npm install vue-router@4 npm install pinia npm install axios npm install element-plus

vue-router负责前端路由,pinia负责全局状态管理,axios是HTTP请求库,element-plus是UI组件库。这里需要解释一下为什么用pinia而不是vuex——vuex的写法在Vue3组合式API的环境下手感很别扭,pinia删掉了mutations的概念,设计更贴合setup函数风格,TypeScript支持也更好。它现在就是Vue3官方推荐的正式状态管理方案。

装完后打开package.json看一眼dependencies,确认这些包都写进去了。npm install在Linux下偶尔会出现权限报错,不要直接用sudo去跑npm install——这是给未来埋坑。正确做法是修复npm目录权限,或者用nvm重新装一个当前用户目录下的Node。

3.3 前端目录结构规划

工程建好之后第一件事不是写页面,而是先把目录理清楚。没有一个好骨架,后面每加一个页面都要纠结文件放哪里。参考结构如下:

device-ops-web/ ├── public/ ├── src/ │ ├── api/ # 接口请求模块,按业务拆分 │ ├── assets/ # 静态资源 │ ├── components/ # 通用组件 │ ├── layouts/ # 整体布局,比如侧边栏+顶栏 │ ├── router/ # 路由配置 │ ├── stores/ # pinia状态 │ ├── utils/ # 工具函数,axios封装放这里 │ ├── views/ # 页面级组件 │ ├── App.vue │ └── main.js ├── .env.development # 开发环境变量 ├── .env.production # 生产环境变量 ├── index.html ├── package.json └── vite.config.js

核心是api目录和utils目录。api目录下一份文件对应一个业务模块,比如device.js管理所有设备接口,auth.js管理登录认证接口;utils目录里放封装的request.js实例。这样的好处是页面组件里不会散落着裸露的axios调用,接口地址改动只需要改api目录对应文件。

4. 打通前后端的核心配置

4.1 Django侧CORS配置

前后端分离的首要障碍是跨域。浏览器默认同源策略会拦截不同源之间的请求,可以把它理解成小区门卫只认本栋楼的工牌,外面的访客进门前都要登记。前端跑在5173端口,后端跑在8000端口,端口不同就是跨域。

处理方案有两种:一种是后端开启CORS,另一种是使用前端代理。开发阶段我推荐用Vite代理,但Django侧还是建议顺手把CORS装上——有些场景比如临时调试、移动端内嵌WebView会直接访问API,有CORS兜底会方便很多。

安装和配置:

pip install django-cors-headers

settings.py里注册应用并加中间件:

INSTALLED_APPS = [ ... 'corsheaders', ] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', # 放在最前面 ... ] CORS_ALLOWED_ORIGINS = [ "http://localhost:5173", "http://127.0.0.1:5173", ]

重点说一下CorsMiddleware的位置,必须放在CommonMiddleware之前,否则不生效。开发阶段如果嫌配置麻烦,可以临时设置CORS_ALLOW_ALL_ORIGINS = True,但上线之前一定要收紧到具体域名,否则等于向全互联网开放了API访问权限。这个坑我没有亲眼见过,但从安全审计的角度看属于必改项。

4.2 Vite开发服务器代理配置

跨域的第二个解法是代理。用代理的核心理义是:浏览器始终只访问5173这一个源,Vite开发服务器收到请求后转发给后端8000端口,转发是服务器到服务器的通信,不经过浏览器,所以不存在同源策略约束。

打开vite.config.js,在defineConfig里加上server配置:

import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { host: '0.0.0.0', port: 5173, open: true, proxy: { '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true } } } })

配置里的/api意思是:前端发出的所有以/api开头的请求,都会被转发到http://127.0.0.1:8000,并且保留/api路径前缀。比如前端请求的是/api/devices/,Django实际收到的是http://127.0.0.1:8000/api/devices/。如果Django的路由里没有/api前缀,可以在proxy里加rewrite把前缀去掉,但建议后端路由统一挂上/api,前后端都清爽:

proxy: { '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } }

改完vite.config.js后必须重启dev server才会生效,这个细节总是有人忘。我自己的习惯是改完配置就顺手Ctrl+C重启一下,不要等页面报错才想起来。

4.3 axios实例封装与请求拦截器

跨域解决之后,接着要解决的是请求统一携带token。网络设备运维系统的接口肯定需要鉴权,不能裸奔。我封装了一个统一请求实例,放在src/utils/request.js:

import axios from 'axios' const service = axios.create({ baseURL: '/api', timeout: 15000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('access_token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }, error => { return Promise.reject(error) }) service.interceptors.response.use( response => { return response.data }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('access_token') window.location.href = '/login' } return Promise.reject(error) } ) export default service

baseURL设成/api意味着api/device.js里写url时只需要写业务路径,比如'/devices/'。请求拦截器是灵魂——每次发请求前自动从localStorage里取token,拼到Authorization头里。响应拦截器统一处理401,一旦token失效直接清空并跳回登录页。

这里的Authorization格式记得是Bearer加空格加token,没有空格后端解析就会失败。这个细节我在联调时盯过两小时,最后发现漏了个空格。

4.4 token的存储与刷新策略

token存哪里是个容易被忽略的问题。localStorage的优点是刷新页面不丢,缺点是XSS攻击可以把脚本注入页面直接读走token。放sessionStorage会随浏览器标签关闭而清空,但仍在同一会话内保持。放Pinia内存里最安全,刷新页面就丢了,需要配合持久化插件重新拉取。

设备运维系统一般建议至少做到:登录成功把token写入localStorage,刷新时通过token换取用户信息并写入Pinia。如果要更进一步,一套完整的刷新机制包括定时刷新token、在请求/响应拦截器里自动处理token过期、后端提供刷新接口。前端这边最简化但有效的方案是响应401后尝试用refresh_token换新的access_token,拿不到再跳登录页。这个逻辑要把异步请求排队处理好,避免多个请求同时触发刷新。

5. 第一个接口联调实战:设备列表

5.1 Django端设备接口示例

后端接口用Django自带的JsonResponse可以快速写出一个最简单的接口。用一个设备列表做例子,假定模型里已有Device表,字段包括hostname(设备名)、management_ip(管理IP)、device_type(设备类型)、vendor(厂商)、model(型号)、status(运行状态)、software_version(软件版本):

from django.http import JsonResponse from django.contrib.auth.decorators import login_required @login_required def device_list(request): devices = Device.objects.all()[:100] data = [ { "id": d.id, "hostname": d.hostname, "management_ip": d.management_ip, "device_type": d.device_type, "vendor": d.vendor, "model": d.model, "status": d.status, "software_version": d.software_version, } for d in devices ] return JsonResponse({"code": 0, "message": "ok", "data": data})

实际生产环境建议用Django REST Framework,使用ModelSerializer和ViewSet可以省掉大量手写序列化和状态码处理的重复工作。这里的代码目的是先跑通链路,简单够用。

5.2 前端API模块封装

接口模块放在src/api/device.js:

import request from '@/utils/request' export function getDeviceList(params) { return request({ url: '/devices/', method: 'get', params }) }

函数接收的params会拼到URL后面成为查询参数,翻页、搜索、过滤都可以通过这个对象传入。封装成这样,页面组件里调接口就一句话:

import { getDeviceList } from '@/api/device'

5.3 页面组件对接数据流

在views目录建一个DeviceList.vue。先做最简版本:页面加载后调后端接口,拿到的数据渲染成表格,加载过程中有loading效果,请求失败有错误提示。

<template> <div> <el-table v-loading="loading" :data="devices" border stripe> <el-table-column prop="hostname" label="设备名" min-width="120" /> <el-table-column prop="management_ip" label="管理IP" min-width="130" /> <el-table-column prop="device_type" label="设备类型" width="110" /> <el-table-column prop="vendor" label="厂商" width="110" /> <el-table-column prop="model" label="型号" width="130" /> <el-table-column prop="status" label="状态" width="90" /> <el-table-column prop="software_version" label="软件版本" min-width="140" /> </el-table> </div> </template> <script setup> import { ref, onMounted } from 'vue' import { ElMessage } from 'element-plus' import { getDeviceList } from '@/api/device' const loading = ref(false) const devices = ref([]) async function fetchDevices() { loading.value = true try { const res = await getDeviceList() devices.value = res.data } catch (err) { ElMessage.error('设备列表加载失败,请检查后端服务') console.error(err) } finally { loading.value = false } } onMounted(fetchDevices) </script>

这段代码里有一个容易被忽略的细节:因为我们封装的响应拦截器已经返回了response.data,所以在这里可以直接拿到res.data这样的业务数据格式,不会再套一层axios的default结构。联调时一旦发现数据格式不对,先回去检查拦截器的return语句。

数据流全路径是这样的:DeviceList.vue调用getDeviceList,api/device.js通过封装的request实例发起请求,axios请求被拦截器加上token,Vite dev server收到/api/devices/后转发给Django的8000端口,Django路由匹配后返回JSON,响应经过拦截器解包,最终data落到devices变量上渲染成表格。这条链路通了,后面所有业务模块都是复制这个套路。

6. 联调中的常见问题与实战排查

6.1 接口请求失败速查表

实际开发中出问题最多的不是业务代码,而是环境联调这一层。我整理了一张表,把高频问题按现象、原因、解决思路列开,方便照方抓药:

现象可能原因排查方向
浏览器报proxy error后端服务没启动或端口不对先curl http://127.0.0.1:8000验证后端是否可用
Access-Control-Allow-Origin报错请求直接打到了8000端口而不是5173代理打开Network面板看请求URL,确认是否走了/api代理
Django报DisallowedHostALLOWED_HOSTS没有包含请求主机名settings.py里ALLOWED_HOSTS加上localhost、127.0.0.1
请求返回401token缺失、Header名不对、token过期查看请求头里Authorization是否存在,格式是否正确
页面白屏控制台报错组件语法错误或依赖未安装先清除node_modules重新安装,再查具体报错堆栈
element-plus样式没生效main.js没引入样式文件确认引入element-plus/dist/index.css

6.2 端口占用与后端不可达的处理

前端启动时提示端口被占用,执行以下命令找到占用进程:

macOS/Linux:

lsof -i :5173 kill -9 <PID>

Windows:

netstat -ano | findstr 5173 taskkill /F /PID <PID>

如果dev server正常起起来了但接口请求还是超时,先别急着怀疑前端配置,直接在终端里测后端:

curl http://127.0.0.1:8000/api/devices/

能返回JSON说明后端正常,问题在前端;返回连接拒绝说明后端服务没起来,去Django那边的终端看runserver是不是挂了。我见过太多人遇到502先干瞪眼,一看后端进程根本没跑。

6.3 401鉴权几大坑

401是前端联调时出现频率最高的状态码,各种奇怪的成因都遇到过。最常见的几个:

  • token根本没存进localStorage,登录接口返回token后遗漏了存储逻辑;
  • 请求拦截器写错,把localStorage.getItem('access_token')写成了setItem;
  • 后端要求Header名是X-Token而不是Authorization;
  • token过期时间太短,开发环境后端签发1小时有效期,前端改完代码回来请求就过期了;
  • 手动在浏览器里敲URL访问接口,这种GET请求不会经过页面里的拦截器,自然没有token。

排查401的通用思路是:打开浏览器开发者工具,切到Network,点开请求查看Request Headers,确认Authorization头是否存在、格式是否正确。再用后端日志确认token解析是否报错。前后端各看一段,快速定位到底是在哪一端出问题。

6.4 生产环境部署时的配置切换

开发时靠Vite代理解决的问题,生产环境要换成Nginx处理。前端构建产物是纯静态文件,dist目录里是编译后的html、js、css。Nginx配置要点是:前端静态文件直接托管,接口请求转发到Django,历史路由模式还要做try_files回退。

server { listen 80; server_name ops.example.com; root /opt/device-ops-web/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; } }

这样生产环境浏览器和前端同源,不存在跨域,也省去了CORS配置隐患。构建命令是npm run build,产物在dist目录,把dist目录推到服务器上就完成了前端部署。

6.5 网络设备运维系统特有的两个坑

设备管理还有一个普通后台系统不会遇到的问题:部分状态接口响应很慢。比如从设备上拉取当前运行配置、批量ping探测在线状态,这些操作动辄几十秒,HTTP请求超时设置15秒完全不够用。处理办法有两种——后端把耗时操作做成异步任务,前端用setInterval轮询结果;或者把axios的timeout按接口单独调大。我倾向于前者,因为运维系统里"操作即任务"的模式更适合异步化,用户提交任务后先看到"执行中",完成后状态自动刷新,体验也更自然。

另外,设备状态列表如果要做实时刷新,简单方案是setInterval每30秒拉一次。但要注意在组件卸载时清掉定时器,否则页面切走之后定时器还在跑,控制台全是网络请求,严重的还会内存泄漏。用Vue的onUnmounted清理:

let timer = null onMounted(() => { fetchDevices() timer = setInterval(fetchDevices, 30000) }) onUnmounted(() => { clearInterval(timer) })

这个小细节在设备监控场景几乎是必踩的。

7. 几点个人心得

整套环境搭下来,最深刻的体会是"环境问题不要死磕,先用最小路径跑通"。我习惯先只用一条接口验证全链路——不搞权限、不做交互,就一个表格页拉一条数据。链路通了再逐步往上加东西,排查范围就小得多。

另外,把环境版本信息写进项目README是个好习惯。我之前在一个项目里被Node版本坑过一次,后来把node版本、npm镜像源、后端Python版本、依赖清单全部写进文档,新同事加入时照着配,半小时就能跑起开发环境。这种看起来不起眼的记录,节省的时间比写代码还要多。

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

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

立即咨询