简介:这是一套基于Django后端与Vue2前端构建的股票数据可视化与新闻实时更新Web应用系统完整源码,适合想通过综合项目学习前后端分离、数据采集与可视化展示的开发者。项目中股票市场新闻抓取、交互式搜索、多维度数据展示及用户个人中心等模块均已编码实现,能帮助读者理清单页应用与后端接口协作的全流程。资源共243个文件,压缩包约20.42MB,文件以css、svg、vue、js及woff/ttf字体资源为主,包含完整前端工程、组件逻辑与样式文件,便于按模块对照学习界面布局与交互实现。已有57人学习下载,适合用于毕业设计参考、全栈实践或作为Django+Vue项目脚手架直接复用。
1. 基于Django与Vue2的股票数据可视化Web应用:为什么难点全在数据管道
做股票数据可视化系统,新手最容易误判的是“难点在画图”。实际上选好 ECharts 只是第一步,真正劝退人的是这条链路:新闻怎么抓、行情数据怎么清洗入库、Django 如何把数据吐成接口、Vue2 如何消费接口并渲染成交互页面。任意一环断掉,页面就是一片空白。基于 Django 后端与 Vue2 前端构建的股票数据可视化与新闻实时更新 Web 应用系统,解决的正是“数据到页面”这条完整管道,它既是毕业设计、简历项目的经典选题,也是团队内网数据看板的常见原型。适合的人有两类:想跑通前后端分离实战的开发者,以及需要用最小成本验证一个数据产品想法的从业者。
2. Django后端:先把数据管道建起来,再谈可视化
前后端分离的项目里,Django 的职责不是“返回页面”,而是把业务数据整理成接口。股票行情、新闻抓取、用户自选股是三个差异很大的领域,混在一个 app 里后期会越写越乱。建项目的第一步不是写代码,是先定边界。
2.1 先拆app:django创建app之前先想清楚三个领域模型
我一般习惯把系统拆成 market、news、accounts 三个 app。market 放股票代码表和日线行情,news 放新闻抓取模型与定时任务,accounts 放用户扩展信息与自选股。这样划分的好处是:抓新闻的爬虫与行情接口互不干扰,之后做单元测试、迁移数据、单独部署定时任务都容易。
# 创建虚拟环境并安装基础依赖,以 Django 4.2 LTS 为例 python -m venv venv source venv/bin/activate pip install django djangorestframework django-cors-headers requests beautifulsoup4 # 创建工程与 app django-admin startproject stock_visual cd stock_visual python manage.py startapp market python manage.py startapp news python manage.py startapp accounts命令里的 djangorestframework、django-cors-headers 是联调刚需:前者提供 DRF 接口框架,后者解决 Vue2 dev server 跨域请求。beautifulsoup4 配合 requests 做新闻列表页解析。开发期不需要装 Scrapy,单站点列表页用 requests 足够,等新闻源超过三个再考虑重架构。
创建完 app 后,记得在 stock_visual/settings.py 的 INSTALLED_APPS 里注册这三个 app,并在根 urls.py 里 include 对应路由。漏掉这一步,后面执行 makemigrations 时会提示 “No installed app with label”。
2.2 数据模型设计:行情、新闻、自选股三张核心表
模型设计决定了接口写得多顺。股票日线表必须用唯一约束防止重复导入;新闻表用 URL 做唯一键实现天然去重;自选股表用联合唯一约束保证“一个用户不会重复收藏同一只股票”。
# market/models.py from django.db import models class Stock(models.Model): symbol = models.CharField('代码', max_length=16, unique=True, db_index=True) name = models.CharField('名称', max_length=64) exchange = models.CharField('交易所', max_length=8, blank=True) updated_at = models.DateTimeField(auto_now=True) def __str__(self): return f'{self.symbol} {self.name}' class StockDaily(models.Model): stock = models.ForeignKey(Stock, on_delete=models.CASCADE, related_name='daily') trade_date = models.DateField('交易日期', db_index=True) open = models.DecimalField('开盘', max_digits=12, decimal_places=4) high = models.DecimalField('最高', max_digits=12, decimal_places=4) low = models.DecimalField('最低', max_digits=12, decimal_places=4) close = models.DecimalField('收盘', max_digits=12, decimal_places=4) volume = models.BigIntegerField('成交量', default=0) class Meta: constraints = [ models.UniqueConstraint(fields=['stock', 'trade_date'], name='uniq_stock_date') ] ordering = ['-trade_date']代码里有三个关键选择。第一,价格字段用 DecimalField 而不是 FloatField,金融数据对小数精度敏感,float 累积误差在计算涨跌幅时会被放大。第二,成交量用 BigIntegerField,A 股日成交量动辄上亿,IntegerField 会溢出。第三,外键用 related_name='daily',后面查某个股票的行情直接 stock.daily.all(),不用写额外 join。
新闻模型相对简单:title 用 CharField,url 用带 unique=True 的 URLField,published_at 用 DateTimeField。抓取入库时 update_or_create(url=href, defaults={...}),URL 唯一约束自动挡住重复抓取。自选股模型放在 accounts/models.py,字段只有 user 外键、stock 外键、created_at 时间戳,Meta 里加 UniqueConstraint(fields=['user', 'stock'])。
2.3 新闻抓取与定时更新:用management command实现可巡检的爬虫任务
把爬虫逻辑直接塞进 views.py 是新手最常犯的错:请求一多 Django 进程被爬虫阻塞,页面全部卡住。正确做法是把抓取写成 management command,命令行可以手动触发,cron 可以定时触发,逻辑还能独立调试。
# news/management/commands/fetch_news.py import time import requests from bs4 import BeautifulSoup from django.core.management.base import BaseCommand from news.models import NewsArticle NEWS_URL = 'https://your-news-site.example.com/finance' # 替换为允许抓取的公开站点 class Command(BaseCommand): help = '抓取公开财经新闻列表页并入库' def handle(self, *args, **options): session = requests.Session() session.headers.update({ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Accept': 'text/html,application/xhtml+xml', }) resp = session.get(NEWS_URL, timeout=10) resp.raise_for_status() resp.encoding = 'utf-8' soup = BeautifulSoup(resp.text, 'lxml') items = soup.select('div.news-list li a')[:20] # 按目标站实际结构调整 saved = 0 for a in items: title = a.get_text(strip=True) href = a.get('href') if not title or not href: continue _, created = NewsArticle.objects.update_or_create( url=href, defaults={'title': title, 'source': '示例财经站'}, ) saved += created time.sleep(0.3) # 降低请求频率 self.stdout.write(self.style.SUCCESS(f'新增 {saved} 条新闻'))这里有三处参数值得留意。timeout=10 是必须的,外部站点慢响应时不会让爬虫挂死。select 选择器用宽松的层级写法,避免站点加个 class 就抓不到。time.sleep(0.3) 是给抓取节奏留的缓冲,频率越低越不容易触发风控,也更符合抓取公开内容的合规习惯。
定时任务我用 cron 而不是 APScheduler,原因很实际:cron 由操作系统拉起独立进程,Django Web 服务崩了不影响抓取,日志走 system log 方便排查故障。开发环境想立即验证就手动执行 python manage.py fetch_news。
# 每天 9:00 到 21:00,每隔 30 分钟抓一次 */30 9-21 * * * cd /path/to/stock_visual && /path/to/venv/bin/python manage.py fetch_news >> /var/log/stock_news.log 2>&12.4 接口设计:用DRF把数据吐给Vue2
Vue2 端需要三类接口:股票列表与搜索、单只股票日线、新闻列表。DRF 的 ReadOnlyModelViewSet 搭配 SearchFilter,能在一百行内解决全部查询需求。
# market/serializers.py from rest_framework import serializers from market.models import Stock, StockDaily class StockDailySerializer(serializers.ModelSerializer): class Meta: model = StockDaily fields = ['trade_date', 'open', 'high', 'low', 'close', 'volume'] class StockSerializer(serializers.ModelSerializer): latest_close = serializers.DecimalField(max_digits=12, decimal_places=4, read_only=True) class Meta: model = Stock fields = ['id', 'symbol', 'name', 'exchange', 'latest_close'] # market/views.py from rest_framework import viewsets, filters from market.models import Stock, StockDaily from market.serializers import StockSerializer, StockDailySerializer class StockViewSet(viewsets.ReadOnlyModelViewSet): queryset = Stock.objects.all() serializer_class = StockSerializer filter_backends = [filters.SearchFilter, filters.OrderingFilter] search_fields = ['symbol', 'name'] ordering_fields = ['symbol'] class StockDailyViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = StockDailySerializer def get_queryset(self): symbol = self.request.query_params.get('symbol') if not symbol: return StockDaily.objects.none() return StockDaily.objects.filter(stock__symbol=symbol)SearchFilter 会自动把 ?search=600 转成 symbol 或 name 的 LIKE 查询,前端搜索联想直接调 /api/stocks/?search=600 即可。StockDaily 接口不返回嵌套股票对象,只按 symbol 过滤,payload 变小,ECharts 解析也更快。在根 urls.py 里用 router.register 注册这两个 ViewSet,DRF 会自动生成列表与详情路由。
前端拿到接口后就能开画了。但先别急,行情数据从哪来还没说——演示阶段可以把 CSV 静态样本放一个 django management command 里导入,生产阶段再接免费或商业数据源。重点是先把数据结构定稳,换数据源只是换一个导入脚本的事。
3. Vue2前端:从“能显示数据”到“交互式股票搜索与详情分析”
Vue2 这层要做的事很明确:请求封装、K 线联动、搜索防抖、新闻刷新。四个功能点按依赖关系排序,先有稳定的请求层,再谈可视化。
3.1 请求层封装:axios拦截器统一处理Token与错误
Vue2 项目用 vue-cli 5 初始化,目录结构常见做法是:src/api 放请求封装,src/views 放页面组件,src/router 放路由。所有接口请求统一走一个 axios 实例,好处是 Token 注入、401 跳转、错误提示只写一遍。
// src/api/http.js import axios from 'axios' import router from '../router' const http = axios.create({ baseURL: process.env.VUE_APP_API_BASE || 'http://127.0.0.1:8000/api', timeout: 15000 }) http.interceptors.request.use(config => { const token = localStorage.getItem('stock_token') if (token) { config.headers.Authorization = `Token ${token}` } return config }) http.interceptors.response.use( response => response.data, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('stock_token') router.push({ name: 'login' }) } return Promise.reject(error) } ) export default httpbaseURL 通过环境变量控制:开发环境指向 Django 的 8000 端口,生产环境交给 Nginx 反向代理,前端只请求同源 /api 路径。timeout 设 15 秒,行情接口响应慢时前端不会一直白屏等待。响应拦截器直接返回 response.data,业务代码里拿到的就是干净的 JSON,省掉每处写 response.data 的重复。
Token 放在 Authorization 头而不是 cookie,跨域配置会简单很多,后面第 5 章会展开讲。
// src/api/stock.js import http from './http' export function fetchStockList(params) { return http.get('/stocks/', { params }) } export function fetchDaily(symbol) { return http.get('/daily/', { params: { symbol }, }) }3.2 用ECharts做股票数据可视化:K线与成交量的双图联动配置
ECharts 是这个项目的主角。做股票数据可视化最常见也最实用的组合是上 K 线、下成交量,两个图共用一个 dataZoom,缩放时同步移动。ECharts 5.x 的配置项对 Vue2 完全兼容,通过 ref 拿到 DOM 节点后 init。
// src/views/StockDetail.vue 的图表配置部分 const option = { tooltip: { trigger: 'axis', axisPointer: { type: 'cross' } }, axisPointer: { link: [{ xAxisIndex: 'all' }] }, grid: [ { left: 60, right: 16, top: 20, height: '48%' }, { left: 60, right: 16, bottom: 40, height: '18%' } ], xAxis: [ { type: 'category', data: dates, gridIndex: 0, boundaryGap: true }, { type: 'category', data: dates, gridIndex: 1, axisLabel: { show: false } } ], yAxis: [ { type: 'value', scale: true, gridIndex: 0 }, { type: 'value', gridIndex: 1, splitNumber: 2 } ], dataZoom: [ { type: 'inside', xAxisIndex: [0, 1], start: 60, end: 100 }, { type: 'slider', xAxisIndex: [0, 1], top: '88%' } ], series: [ { name: 'K线', type: 'candlestick', data: kData, xAxisIndex: 0, yAxisIndex: 0, itemStyle: { color: '#ef232a', color0: '#14b143', borderColor: '#ef232a', borderColor0: '#14b143' } }, { name: '成交量', type: 'bar', data: volumes, xAxisIndex: 1, yAxisIndex: 1 } ] }这段配置有两个关键点。一是 grid 数组定义上下两块面板,K 线占 48% 高度、成交量占 18% 高度,中间留白。二是 dataZoom 的 xAxisIndex 同时指向两个 x 轴,拖动缩放条时 K 线和成交量保持同一可见区间。itemStyle 里 color 是阳线颜色,color0 是阴线颜色,按国内习惯红涨绿跌。
后端返回的日线数据需要先转换成 ECharts 认识的格式。candlestick 的 data 每一项是 [open, close, lowest, highest],注意顺序不是 [open, high, low, close],这里搞反图表会直接错乱。我会在组件里用一个 map 函数处理:
const kData = rawDaily.map(item => [ Number(item.open), Number(item.close), Number(item.low), Number(item.high) ]) const dates = rawDaily.map(item => item.trade_date) const volumes = rawDaily.map(item => item.volume)3.3 交互式股票搜索:防抖与候选下拉的最小实现
搜索框是用户最频繁碰到的交互。直接在每个 input 事件里发请求会把后端打崩溃,防抖是必须的。Vue2 里最顺手的写法是用 watch 监听关键字,结合 setTimeout 实现 350ms 防抖。
// src/components/SearchBar.vue export default { data() { return { keyword: '', candidates: [], timer: null } }, watch: { keyword(newVal) { if (!newVal || newVal.length < 2) { this.candidates = [] return } clearTimeout(this.timer) this.timer = setTimeout(() => { this.fetchSuggest(newVal) }, 350) } }, methods: { async fetchSuggest(keyword) { const data = await http.get('/stocks/', { params: { search: keyword, page_size: 8 } }) this.candidates = data.results || [] } } }两个参数值得注意:min_length=2,输入一个字符时不做请求,避免候选列表全是无关股票;防抖 350ms 是一个平衡点,低于 200ms 防抖效果不明显,高于 500ms 用户会觉得搜索“肉”。点击候选条目后把 symbol 传给详情页路由,同时清空关键字。
这个搜索接口就是第 2 章里的 SearchFilter 在 Vue2 侧的直接落地,前端传 search 参数,后端不用写任何额外逻辑。
3.4 新闻实时更新:轮询先跑通,再考虑Channels WebSocket
标题里的“新闻实时更新”有两种实现路线。演示项目我最推荐轮询:前端每 30 秒拉一次新闻列表,后端加缓存,改动最小、最容易排查。等用户量上来再升级到 WebSocket,Django Channels 是常见选择。
// src/views/NewsPanel.vue export default { data() { return { newsList: [], timer: null } }, created() { this.loadNews() this.timer = setInterval(this.loadNews, 30000) }, beforeDestroy() { clearInterval(this.timer) }, methods: { async loadNews() { const data = await http.get('/news/', { params: { limit: 20 } }) this.newsList = data.results || data } } }30 秒是新闻场景的合理间隔,太短浪费请求,太长用户感知不到“实时”。这段代码里最容易翻车的是 beforeDestroy 里忘记 clearInterval,组件切走之后定时器还在跑,接口请求持续发到后端。Vue2 里页面切换不会自动销毁定时器,这个坑我在联调阶段踩过不止一次。
如果一定要上 WebSocket,Django Channels 的典型结构是这样:前端连 ws://host/ws/news/,后端 NewsConsumer 维护连接,抓取命令往 group 里广播新消息。
# news/consumers.py(Django Channels 方案) from channels.generic.websocket import AsyncJsonWebsocketConsumer class NewsConsumer(AsyncJsonWebsocketConsumer): async def connect(self): await self.accept() await self.channel_layer.group_add('news', self.channel_name) async def news_updated(self, event): await self.send_json(event['payload'])抓取任务入库后调用 channel_layer.group_send('news', {...}),所有在线客户端立刻收到推送。这个方案对部署有额外要求,需要 ASGI 服务器和 Redis channel layer,项目初期用轮询足够,WebSocket 等确实有推送需求再升级不迟。
4. 用户个人中心与联调:登录鉴权、自选股同步与偏好记忆
标题里“用户个人中心”落到功能上就是:注册登录、收藏自选股、查看收藏列表、记住用户偏好。这部分串起了 Django auth、DRF Token、Vue2 路由守卫,是前后端联调的主战场。
4.1 登录鉴权选型:Session还是Token
前后端分离的 Vue2 项目里,Session 方案要让 axios 带 cookie,跨域配置会更麻烦。常见做法是用 Token 认证:登录成功后后端返回一个随机 Token,前端存 localStorage,之后每个请求在 Authorization 头带上。DRF 内置了 TokenAuthentication,写登录接口非常快。
# accounts/views.py from django.contrib.auth import authenticate from rest_framework.authtoken.models import Token from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import AllowAny from rest_framework.response import Response @api_view(['POST']) @permission_classes([AllowAny]) def login(request): username = request.data.get('username') password = request.data.get('password') user = authenticate(username=username, password=password) if not user: return Response({'detail': '用户名或密码错误'}, status=400) token, _ = Token.objects.get_or_create(user=user) return Response({'token': token.key, 'username': user.username})Token.get_or_create 会让同一用户多次登录拿到同一个 Token,方便后续踢人下线。注册接口用 User.objects.create_user(username=..., password=...),create_user 会自动加密密码,绝对不能直接 create。
生产环境想更灵活可以换 SimpleJWT 的 access/refresh 双 Token,但演示项目里 DRF Token 的代码量最少,逻辑最直观,足够支撑个人中心全流程。
4.2 自选股增删:django执行查询-删除对象的正确姿势
自选股接口的核心操作是“收藏”和“取消收藏”。收藏用 get_or_create 防重复;取消收藏用 filter().delete() 而不是 get().delete(),原因是前者在记录不存在时返回 0,后者直接抛 DoesNotExist 异常。
# accounts/views.py from rest_framework.permissions import IsAuthenticated from market.models import Stock from accounts.models import UserFavorite @api_view(['POST', 'DELETE']) @permission_classes([IsAuthenticated]) def favorite(request, symbol): stock = Stock.objects.filter(symbol=symbol).first() if not stock: return Response({'detail': '股票不存在'}, status=404) if request.method == 'POST': obj, created = UserFavorite.objects.get_or_create(user=request.user, stock=stock) return Response({'created': created}) deleted, _ = UserFavorite.objects.filter(user=request.user, stock=stock).delete() return Response({'deleted': deleted})delete() 返回的是 (删除数量, 明细字典) 二元组,这里只取数量。个人中心的收藏列表接口用 select_related 加载 stock,避免 N+1 查询。
# accounts/serializers.py class UserFavoriteSerializer(serializers.ModelSerializer): symbol = serializers.CharField(source='stock.symbol', read_only=True) name = serializers.CharField(source='stock.name', read_only=True) class Meta: model = UserFavorite fields = ['id', 'symbol', 'name', 'created_at']前端个人中心页循环渲染这个列表,点“移除”按钮调 DELETE 接口,本地数组 filter 掉对应项即可,不用整页刷新。
4.3 前端路由守卫:未登录只能看行情,不能进个人中心
路由守卫是个人中心的前端闭环。Vue2 Router 的 beforeEach 钩子里检查 localStorage 是否有 Token,没有就跳登录页,并带上 redirect 参数让登录后可以回到原页面。
// src/router/index.js router.beforeEach((to, from, next) => { const token = localStorage.getItem('stock_token') if (to.meta.requiresAuth && !token) { next({ name: 'login', query: { redirect: to.fullPath } }) } else { next() } })登录页提交表单成功后把 Token 写入 localStorage,然后 router.push(route.query.redirect || '/')。这里有个小细节:Token 写入用 localStorage 而不是 sessionStorage,否则用户关掉标签页再打开又要重新登录,个人中心体验会差很多。行情页和搜索页不需要登录,K 线展示对游客开放,这更符合股票类网站的实际习惯。
4.4 用户偏好与可视化的联动:自选股以外的记忆能力
个人中心如果只有自选股列表会显得单薄。常见做法是给用户加一张偏好表,存 K 线周期、默认排序、主题色这类界面选项。实现上不复杂,Django 的 JSONField 就能搞定。
# accounts/models.py class UserProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) preferences = models.JSONField('界面偏好', default=dict)前端个人中心页面提供“默认 K 线周期”“列表排序方式”几个选项,保存时整体提交这个 JSON。详情页加载时先请求偏好接口,再根据偏好设置 ECharts 的 dataZoom 初始范围。这个功能能让系统看起来“懂得用户”,开发成本却只有一张表和两个接口,性价比很高。
5. 避坑与常见问题排查:五处踩过才懂的问题
这一章是我认为全文最值得反复看的部分。前四章把功能推起来了,但真正决定项目能不能演示、能不能交付的,是下面这些联调阶段的坑。
5.1 跨域被拦:Vue2页面请求Django接口一片飘红
现象:前端页面能打开,但所有接口请求在浏览器 Console 报 CORS error,network 面板显示请求被拦截。
原因:Vue2 dev server 跑在 8080 端口,Django 跑在 8000 端口,浏览器同源策略禁止跨端口 Ajax。Django 默认不会在响应头里带 Access-Control-Allow-Origin。
解决:安装 django-cors-headers,在 settings.py 里配置允许的来源列表。
# stock_visual/settings.py INSTALLED_APPS = [ # ... 'corsheaders', ] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', # ... ] CORS_ALLOWED_ORIGINS = [ 'http://localhost:8080', ]注意 CORS_ALLOW_CREDENTIALS 保持 False。我们的 Token 放在 Authorization 头里,不依赖 cookie,没必要开 credentials。如果误设成 True,浏览器会强制要求 Access-Control-Allow-Origin 不能是 * ,反而多一层限制。
5.2 时区与trade_date错位:K线最后一根日期少一天
现象:前端展示日线,最新一根 K 线的日期比实际交易日晚一天,历史数据日期也整体偏移。
原因:Django 设置了 USE_TZ=True,DateTimeField 会按 UTC 存储。如果 trade_date 用了 DateTimeField 而不是 DateField,接口返回的是 2024-03-08T00:00:00+08:00,前端 new Date() 解析后在 UTC 环境转字符串就变成了前一天。
解决:日期序列字段用 DateField,接口序列化时直接输出 YYYY-MM-DD 字符串,前端不要转 Date 对象。我在 StockDaily 模型里把 trade_date 定义成 DateField 就是为了避开这个坑。如果必须用 DateTimeField,序列化时加 format='%Y-%m-%d' 强制成字符串。
5.3 新闻爬虫突然抓不到数据:列表页改版与请求频率叠加
现象:爬虫每天正常运行,某天开始新增数量为 0,或者直接抛 HTTP 403 / 504。
原因:目标站点改了页面结构,CSS 选择器失效;或者请求频率太高被风控,返回了验证码页面。两者表现都是“能访问但解析不到内容”,不打印日志很难发现。
解决:选择器不要写太深的层级,比如用 div.news-list li a 而不是 div.body > div.main > ul > li:nth-child(2) > a。抓取结束后做个判断,本次解析条数为 0 时在终端输出告警而不是静默通过。time.sleep 加上随机抖动,避免固定间隔请求特征太明显。
提示:只抓取公开列表页摘要信息,不抓个人隐私、不做批量下载。抓取行为要与目标站点 robots 协议和当地法律要求保持一致。
还有一条:生产环境下爬虫要独立进程跑,出问题只影响抓取模块,不会把行情接口拖挂。
5.4 Vue2图表空白:K线不渲染或canvas宽度为0
现象:有时进入详情页图表正常,刷新页面后空白;有时从列表页跳详情,图表区域一片灰。
原因:ECharts 在容器还没有真实宽高时 init,canvas 初始尺寸为 0。常见触发场景是父容器用了 v-if 控制显示,或者组件 mounted 里异步请求还没回来就执行 init。
解决:mounted 里用 this.$nextTick 等 DOM 渲染完成再 init;图表 init 后设置窗口 resize 监听,调用 chart.resize() 自适应。如果组件被 keep-alive 缓存,把 resize 逻辑放到 activated 而不是 mounted。
mounted() { this.$nextTick(() => { this.chart = echarts.init(this.$refs.chartRef) this.renderChart() }) window.addEventListener('resize', this.handleResize) }想用 Vue2 的 updated 生命周期做自适应时,务必先判断图表数据或容器尺寸真的变化了再 resize,否则 updated 频繁触发会引起图表死循环刷新,这是 Vue2 里 echarts 相关的另一个经典翻车点。
5.5 新闻“实时更新”把接口打爆:轮询没有做缓存
现象:多人同时打开新闻面板,后端日志里 /api/news/ 请求量暴涨,Django 进程 CPU 占用升高,其他接口跟着变慢。
原因:每个客户端每 30 秒拉一次新闻,10 个用户就是每秒 0.3 个请求,虽然绝对数量不大,但新闻数据本身变化频率很低,大部分请求返回的是相同内容。
解决:新闻接口加缓存,60 秒内直接返回缓存。DRF 里用 cache_page 装饰器最省事。
# news/urls.py from django.views.decorators.cache import cache_page from news.views import NewsListView urlpatterns = [ path('api/news/', cache_page(60)(NewsListView.as_view())), ]加上缓存之后,新闻接口实际打到数据库的请求量降为原来的几十分之一。前端轮询间隔也可以做个自适应:两次返回内容完全一致时,把间隔拉长到 60 秒,有变化再缩短,这是比固定 30 秒更体面的实时策略。
6. 进阶与验证:从“能跑”到“真有用”的三件事
项目跑通之后,真正决定它能不能用的不是代码量,而是数据质量、承载能力和演进空间。
6.1 行情数据源切换的校验清单
演示阶段用 CSV 导入没问题,切真实数据源前先检查三件事:交易日历是否缺失,停牌日成交量是否为 0 且价格不变,前复权与不复权口径是否在页面上标注。我一般会在导入脚本里加断言,连续 5 个交易日缺失就中止导入并告警,宁可数据不更新也不能图表出现断崖。
6.2 生产部署兜底配置
Vue2 build 产物交给 Nginx 静态托管,Django 用 Gunicorn 跑,这是最成熟的前后端分离部署形态。Gunicorn 的 worker 数按 2*CPU 核数 +1 设置,timeout 至少 60 秒,因为行情接口偶尔会慢。Django 后台管理界面想做得现代一点可以换 django-unfold 这类后台主题,但核心还是先把静态文件收集、collectstatic 和 /api 反向代理配好。
gunicorn stock_visual.wsgi:application \ --bind 127.0.0.1:8000 \ --workers 5 \ --timeout 606.3 Vue2项目迁移Vue3前的提前规划
如果规划中要把这个项目迁到 Vue3,现在就要注意两点:把图表初始化逻辑封装成独立 composable,而不是散落在各组件 mounted 里;把 axios 请求层保持纯函数导出,避免依赖 this 上下文。这两件事在 Vue2 阶段做好,迁移时 ECharts 封装和 API 层几乎可以原样搬,真正要动的只有响应式写法和路由模块引入方式。我现在的习惯是:新项目直接用 Vue3,但维护这类 Vue2 系统时,仍然严格按照“数据逻辑不进组件、请求封装不进视图”的规则写,这样两代框架之间切换成本最低。
希望这些落地细节能帮你把这个项目跑顺,少走几段弯路。
本文还有配套的精品资源,点击获取