
简介这是一套基于Django后端与Vue前端协同开发的小说阅读网站完整源码面向Web全栈初学者及Python/JavaScript进阶学习者旨在帮助开发者掌握前后端分离架构下的项目搭建、用户交互与内容展示全流程。资源共1785个文件主体为1554个SVG图标资源支撑界面矢量图形渲染、63个Python文件含Django模型、视图、路由及管理逻辑、31个JS与33个CSS文件实现Vue组件交互与样式定制辅以HTML模板、SCSS/LESS预处理器样式及字体文件EOT/TTF/WOFF等整体压缩包仅5.62MB轻量但结构完整。已有81人学习下载资源包含可直接运行的工程骨架、响应式UI组件库集成如Bootstrap、Font Awesome及SVG图标系统、多层级静态资源组织方式以及清晰的前后端目录划分便于理解MVC与MVVM混合模式下的协作机制与工程化实践。1. 这不是一个“前后端分离”的样板工程而是一套可上线的小说阅读网站落地路径你下载了一个叫“基于Python的Django框架和Vue实现的小说阅读网站.zip”的压缩包解压后看到backend/和frontend/两个目录心里可能立刻冒出三个问题Django真要配Vue静态资源怎么托管小说章节的分页、搜索、阅读记录这些业务逻辑到底该放哪边这不是教科书式的“DjangoVue模板渲染”或“纯API接口Vue单页应用”的二选一题——真实项目里小说类站点对SEO友好性、首屏加载速度、章节文本渲染性能、用户阅读行为埋点都有强约束。Django负责小说元数据管理、用户权限、章节内容存储含富文本/Markdown解析、搜索索引构建Vue负责前端交互层但必须支持服务端预渲染SSR或至少静态生成SSG关键页面如小说列表页、详情页否则百度蜘蛛爬不到内容新书根本推不出去。本文不讲“如何安装Vue”而是聚焦于Django如何暴露结构化小说数据接口、Vue如何安全高效地消费这些接口、两者在Nginx反向代理下的资源协同策略、以及针对小说场景特有的缓存与分页优化。适合已能独立搭建Django Admin和Vue CLI项目的开发者目标是让这个zip包里的代码真正跑起来、接上真实数据库、扛住千人并发阅读。2. Django后端小说模型设计与RESTful API分层实现小说阅读网站的核心不是UI动效而是数据结构的合理性与查询效率。一个粗糙的Book模型只存书名和封面很快会在“按分类筛选按更新时间排序全文搜索章节跳转”时崩盘。必须从领域建模出发拆解出可复用、可扩展的实体关系。2.1 小说领域模型为什么需要Category、Author、Chapter三张独立表常见错误是把所有字段塞进一张Novel表title,author,category,cover,intro,last_update,status……这会导致三类问题分类变更成本高若将“玄幻”改为“东方玄幻”需全表UPDATE作者重名难区分两个“辰东”无法关联其全部作品章节顺序依赖字符串排序第1章、第10章、第2章在数据库中按字典序排错。正确做法是建立外键关联# backend/novel/models.py from django.db import models from django.contrib.auth.models import User class Category(models.Model): name models.CharField(max_length50, uniqueTrue, db_indexTrue) # 添加索引加速筛选 slug models.SlugField(max_length60, uniqueTrue) # 用于URL如 /category/xuanhuan/ def __str__(self): return self.name class Author(models.Model): name models.CharField(max_length100, db_indexTrue) bio models.TextField(blankTrue) def __str__(self): return self.name class Book(models.Model): title models.CharField(max_length200, db_indexTrue) author models.ForeignKey(Author, on_deletemodels.CASCADE, related_namebooks) category models.ForeignKey(Category, on_deletemodels.SET_NULL, nullTrue, related_namebooks) cover models.ImageField(upload_tocovers/, blankTrue) # 需配置MEDIA_ROOT intro models.TextField() status models.CharField(max_length20, choices[(serial, 连载中), (end, 已完结)]) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) class Meta: ordering [-updated_at] # 默认按更新时间倒序 class Chapter(models.Model): book models.ForeignKey(Book, on_deletemodels.CASCADE, related_namechapters) title models.CharField(max_length200) content models.TextField() # 存储纯文本或HTML避免富文本编辑器引入XSS风险 order models.PositiveIntegerField(db_indexTrue) # 显式序号替代字符串排序 created_at models.DateTimeField(auto_now_addTrue) class Meta: ordering [order] # 章节默认按order升序 unique_together [book, order] # 防止同一本书出现重复序号提示db_indexTrue对name、order、updated_at等高频查询字段加索引是小说站QPS过千的前提。未加索引的ORDER BY updated_at在万级数据下会触发 filesort拖慢首页加载。2.2 REST API设计用Django REST Framework暴露分层接口前端Vue需要的不是Django模板而是结构清晰、版本可控的JSON接口。使用django-rest-frameworkDRF而非裸写JsonResponse原因在于自动序列化/反序列化内置分页、过滤、权限控制Swagger文档自动生成drf-yasg与Django Admin权限体系复用。安装并注册pip install djangorestframework django-filter drf-yasg# backend/novel/serializers.py from rest_framework import serializers from .models import Book, Chapter, Category, Author class CategorySerializer(serializers.ModelSerializer): class Meta: model Category fields [id, name, slug] class AuthorSerializer(serializers.ModelSerializer): class Meta: model Author fields [id, name] class BookListSerializer(serializers.ModelSerializer): category CategorySerializer(read_onlyTrue) author AuthorSerializer(read_onlyTrue) latest_chapter serializers.SerializerMethodField() class Meta: model Book fields [id, title, cover, intro, status, created_at, updated_at, category, author, latest_chapter] def get_latest_chapter(self, obj): # 预加载最新章节标题避免N1查询 latest obj.chapters.order_by(-created_at).first() return latest.title if latest else None class ChapterSerializer(serializers.ModelSerializer): class Meta: model Chapter fields [id, title, content, order, created_at]# backend/novel/views.py from rest_framework import generics, filters from django_filters.rest_framework import DjangoFilterBackend from .models import Book, Chapter, Category from .serializers import BookListSerializer, ChapterSerializer, CategorySerializer class BookListView(generics.ListAPIView): queryset Book.objects.select_related(category, author).prefetch_related(chapters) serializer_class BookListSerializer filter_backends [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] filterset_fields [category__slug, status] # 支持 /api/books/?category__slugxuanhuan search_fields [title, intro] # 全文搜索 ordering_fields [updated_at, created_at] ordering [-updated_at] class BookDetailView(generics.RetrieveAPIView): queryset Book.objects.select_related(category, author) serializer_class BookListSerializer lookup_field id class ChapterListView(generics.ListAPIView): serializer_class ChapterSerializer lookup_url_kwarg book_id def get_queryset(self): return Chapter.objects.filter(book_idself.kwargs[book_id]).order_by(order) class CategoryListView(generics.ListAPIView): queryset Category.objects.all() serializer_class CategorySerializer# backend/backend/urls.py from django.urls import path, include from rest_framework.routers import DefaultRouter from novel.views import BookListView, BookDetailView, ChapterListView, CategoryListView urlpatterns [ path(api/categories/, CategoryListView.as_view(), namecategory-list), path(api/books/, BookListView.as_view(), namebook-list), path(api/books/int:pk/, BookDetailView.as_view(), namebook-detail), path(api/books/int:book_id/chapters/, ChapterListView.as_view(), namechapter-list), ]注意select_related解决外键关联的N1问题如category、authorprefetch_related解决多对多/反向外键如chapters。未做此优化的列表页在100本书时会发起200次SQL查询响应时间超2s。2.3 小说内容安全防止XSS与敏感词过滤的双重校验小说正文常含用户提交的HTML如加粗、换行、图片直接{{ chapter.content|safe }}极危险。必须在Django层做净化# backend/novel/utils.py import re from bs4 import BeautifulSoup from django.conf import settings def clean_novel_content(html_content): 移除script、iframe等危险标签保留p、br、strong、em soup BeautifulSoup(html_content, html.parser) # 移除危险标签 for tag in soup([script, iframe, object, embed]): tag.decompose() # 移除on*事件属性 for tag in soup.find_all(True): attrs_to_remove [attr for attr in tag.attrs if attr.startswith(on)] for attr in attrs_to_remove: del tag[attr] # 敏感词替换从settings读取词库 if hasattr(settings, SENSITIVE_WORDS) and settings.SENSITIVE_WORDS: for word in settings.SENSITIVE_WORDS: html_content html_content.replace(word, * * len(word)) return str(soup) # 在Chapter模型save时调用 # backend/novel/models.py def save(self, *args, **kwargs): self.content clean_novel_content(self.content) super().save(*args, **kwargs)# backend/backend/settings.py SENSITIVE_WORDS [违禁词1, 违禁词2] # 生产环境应从Redis或DB动态加载3. Vue前端路由、状态管理与小说阅读核心组件实现Vue部分不能只写npm run serve就完事。小说网站的用户体验瓶颈不在打包体积而在章节切换时的白屏时间、滚动位置记忆、离线缓存策略。必须放弃默认的vue-router历史模式直连改用hash模式 服务端静态资源托管并用vuex-persistedstate持久化阅读进度。3.1 Vue Router配置适配小说阅读场景的路由守卫与懒加载小说站URL需兼顾SEO与用户体验列表页/category/xuanhuan→ 对应Django的/api/categories/接口详情页/book/123→ 对应/api/books/123/章节页/book/123/chapter/456→ 对应/api/books/123/chapters/中第456章。但Vue Router的history模式要求Nginx配置try_files而宝塔面板用户常忽略这点导致404。更稳妥的是hash模式且对章节页做特殊处理// frontend/src/router/index.js import { createRouter, createWebHashHistory } from vue-router const routes [ { path: /, name: Home, component: () import(/views/Home.vue) }, { path: /category/:slug, name: Category, component: () import(/views/Category.vue), props: true }, { path: /book/:id, name: BookDetail, component: () import(/views/BookDetail.vue), props: true, children: [ { path: chapter/:chapterId, name: ChapterRead, component: () import(/views/ChapterRead.vue), props: true } ] } ] const router createRouter({ history: createWebHashHistory(), routes, scrollBehavior(to, from, savedPosition) { // 保持章节页滚动位置其他页回到顶部 if (to.name ChapterRead savedPosition) { return savedPosition } else { return { top: 0 } } } }) export default router3.2 Vuex状态管理用持久化插件保存用户阅读进度用户关闭浏览器再打开应自动回到上次阅读的章节。vuex-persistedstate是标准解法但需注意不持久化整个chapter.content太大只存bookId,chapterId,scrollY读取时触发API请求而非从store直接取内容。npm install vuex4 vuex-persistedstate4// frontend/src/store/index.js import { createStore } from vuex import createPersistedState from vuex-persistedstate export default createStore({ state: () ({ readingProgress: { bookId: null, chapterId: null, scrollY: 0 } }), mutations: { SET_PROGRESS(state, { bookId, chapterId, scrollY }) { state.readingProgress { bookId, chapterId, scrollY } } }, actions: { saveProgress({ commit }, { bookId, chapterId, scrollY }) { commit(SET_PROGRESS, { bookId, chapterId, scrollY }) } }, plugins: [ createPersistedState({ key: novel-reader, paths: [readingProgress] // 仅持久化此字段 }) ] })3.3 核心阅读组件防抖滚动监听与章节内容渲染优化ChapterRead.vue是性能关键点。直接v-html渲染万字HTML会阻塞主线程。必须使用v-html前做DOM片段化每500字一个div滚动时防抖更新scrollY离开页面前保存进度。!-- frontend/src/views/ChapterRead.vue -- template div classchapter-container scrollhandleScroll div v-for(chunk, index) in chunks :keyindex v-htmlchunk/div /div /template script import { onBeforeUnmount, onMounted, ref } from vue import { useRoute, useRouter } from vue-router import { useStore } from /store import { getChapter } from /api/novel export default { setup() { const route useRoute() const router useRouter() const store useStore() const container ref(null) const chunks ref([]) const loadChapter async () { try { const res await getChapter(route.params.bookId, route.params.chapterId) // 将长文本分块避免长任务阻塞渲染 const text res.data.content const chunkSize 500 chunks.value [] for (let i 0; i text.length; i chunkSize) { chunks.value.push(text.substring(i, i chunkSize)) } } catch (err) { console.error(加载章节失败, err) router.push(/404) } } const handleScroll () { if (!container.value) return const scrollY container.value.scrollTop // 防抖500ms内只存一次 clearTimeout(window.scrollTimer) window.scrollTimer setTimeout(() { store.dispatch(saveProgress, { bookId: route.params.bookId, chapterId: route.params.chapterId, scrollY }) }, 500) } onMounted(() { loadChapter() // 恢复滚动位置 const progress store.state.readingProgress if (progress.bookId route.params.bookId progress.chapterId route.params.chapterId container.value) { container.value.scrollTop progress.scrollY } }) onBeforeUnmount(() { clearTimeout(window.scrollTimer) }) return { container, chunks, handleScroll } } } /script提示v-html渲染前不做分块Chrome DevTools 的 Performance 面板会显示超过100ms的长任务导致滚动卡顿。分块后每个div的渲染时间控制在16ms内60fps。4. 部署与性能调优Nginx反向代理、静态资源分离与缓存策略本地开发能跑通不等于线上可用。小说站90%流量来自首页和热门书详情页必须用CDN边缘缓存降低源站压力。宝塔面板用户常犯的错误是把Django和Vue打包放同一目录导致Nginx既处理API又托管静态文件CPU飙升。4.1 Nginx配置严格分离Django API与Vue静态资源假设Django运行在http://127.0.0.1:8000GunicornVuedist/目录放在/www/wwwroot/novel-frontend/API请求路径以/api/开头。正确配置宝塔→网站→配置文件server { listen 80; server_name your-domain.com; # Vue静态资源直接由Nginx返回不走Django location / { root /www/wwwroot/novel-frontend; try_files $uri $uri/ /index.html; index index.html; # 启用gzip压缩 gzip on; gzip_types text/plain application/javascript text/css; } # Django API接口反向代理到Gunicorn 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; proxy_set_header X-Forwarded-Proto $scheme; # 缓存API响应仅GET proxy_cache my_cache; proxy_cache_valid 200 302 10m; proxy_cache_valid 404 1m; proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; } # 图片等静态资源Django MEDIA_ROOT location /media/ { alias /www/wwwroot/novel-backend/media/; expires 1h; add_header Cache-Control public, immutable; } } # 定义缓存区 proxy_cache_path /www/wwwroot/cache levels1:2 keys_zonemy_cache:10m max_size1g inactive60m use_temp_pathoff;注意proxy_cache对/api/books/这类列表接口非常有效但必须确保Django视图中Cache-Control头未被覆盖。在DRF视图中添加# backend/novel/views.py from django.utils.cache import add_never_cache_headers class BookListView(generics.ListAPIView): # ... 其他代码 def finalize_response(self, request, response, *args, **kwargs): # 对列表页启用缓存 if request.method GET: response[Cache-Control] public, max-age600 # 10分钟 return super().finalize_response(request, response, *args, **kwargs)4.2 Django生产环境关键配置数据库连接池与查询优化默认SQLite无法支撑并发阅读。必须切换为PostgreSQL或MySQL并配置连接池。以django-db-geventpool为例适配Gunicorngeventpip install django-db-geventpool psycopg2-binary # PostgreSQL # 或 pip install django-db-geventpool mysqlclient # MySQL# backend/backend/settings.py DATABASES { default: { ENGINE: django_db_geventpool.backends.postgresql_psycopg2, NAME: novel_db, USER: novel_user, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 5432, MAX_CONNS: 20, # 连接池最大连接数 MIN_CONNS: 5, # 最小连接数 } }同时禁用Django Debug Toolbar生产环境并开启查询日志定位慢SQL# backend/backend/settings.py LOGGING { version: 1, disable_existing_loggers: False, handlers: { console: { level: DEBUG, class: logging.StreamHandler, }, }, loggers: { django.db.backends: { handlers: [console], level: DEBUG, # 生产环境设为WARNING propagate: False, }, }, }4.3 Vue生产构建提取公共chunk与CDN加速vue.config.js中配置externals将vue、axios等大依赖剥离通过CDN加载// frontend/vue.config.js const isProduction process.env.NODE_ENV production module.exports { configureWebpack: config { if (isProduction) { config.externals { vue: Vue, axios: axios } } }, chainWebpack: config { if (isProduction) { // 提取公共chunk config.optimization.splitChunks({ chunks: all, cacheGroups: { vendor: { name: chunk-vendors, test: /[\\/]node_modules[\\/]/, priority: 10, chunks: initial } } }) } } }!-- frontend/public/index.html -- head % if (process.env.NODE_ENV production) { % script srchttps://cdn.jsdelivr.net/npm/vue3.2.47/dist/vue.global.prod.js/script script srchttps://cdn.jsdelivr.net/npm/axios1.3.4/dist/axios.min.js/script % } % /head5. 小说阅读专项优化章节分页、搜索高亮与离线阅读支持小说站区别于普通CMS的核心体验在于“沉浸式阅读”。这要求超越基础CRUD实现章节内容分页非整章滚动而是按屏幕高度分页搜索关键词高亮用户搜“主角名字”结果页需标红离线缓存用户地铁断网仍可读已加载章节。本节提供可直接集成的代码方案。5.1 基于IntersectionObserver的章节分页组件传统分页需用户点击“下一页”打断阅读流。现代做法是监听章节内容区域的可视区域当底部元素进入视口时自动加载下一章。!-- frontend/src/components/AutoLoadNextChapter.vue -- template div classauto-loader refloaderRef/div /template script import { onMounted, ref, watch } from vue import { useRoute, useRouter } from vue-router import { getBookChapters } from /api/novel export default { props: { bookId: { type: [String, Number], required: true }, currentChapterId: { type: [String, Number], required: true } }, emits: [nextChapter], setup(props, { emit }) { const route useRoute() const router useRouter() const loaderRef ref(null) let observer null const loadNextChapter async () { try { const chapters await getBookChapters(props.bookId) const currentIndex chapters.data.findIndex(c c.id props.currentChapterId) if (currentIndex 0 currentIndex chapters.data.length - 1) { const nextChapter chapters.data[currentIndex 1] emit(nextChapter, nextChapter) } } catch (err) { console.warn(自动加载下一章失败, err) } } onMounted(() { if (!loaderRef.value) return observer new IntersectionObserver( (entries) { if (entries[0].isIntersecting) { loadNextChapter() } }, { threshold: 0.1 } ) observer.observe(loaderRef.value) }) return { loaderRef } } } /script style scoped .auto-loader { height: 1px; width: 100%; } /style在ChapterRead.vue中使用AutoLoadNextChapter :book-idroute.params.bookId :current-chapter-idroute.params.chapterId next-chapterhandleNextChapter /5.2 搜索关键词高亮服务端分词与前端渲染Django侧用django-haystackwhoosh实现全文检索但高亮需前端完成。接收API返回的highlighted_content字段# backend/novel/views.py from haystack.query import SearchQuerySet class SearchView(generics.ListAPIView): def list(self, request, *args, **kwargs): query request.query_params.get(q, ) if not query: return Response([]) # 使用Haystack搜索返回带高亮的content sqs SearchQuerySet().filter(contentquery).highlight() results [] for result in sqs: results.append({ id: result.object.id, title: result.object.title, highlighted_content: result.highlighted.get(content, [])[0] if result.highlighted else result.object.content[:200] ... }) return Response(results)Vue端用正则高亮注意转义特殊字符// frontend/src/utils/highlight.js export function highlightText(text, keyword) { if (!keyword || !text) return text // 转义正则特殊字符 const escapedKeyword keyword.replace(/[.*?^${}()|[\]\\]/g, \\$) const regex new RegExp((${escapedKeyword}), gi) return text.replace(regex, mark classhighlight$1/mark) } // 在组件中使用 const highlighted highlightText(chapter.content, this.searchKeyword)/* frontend/src/assets/main.css */ .highlight { background-color: #ffeb3b; padding: 0 2px; }5.3 Service Worker离线缓存仅缓存已读章节PWA不是噱头对小说站有实际价值。但全站缓存会浪费用户流量应只缓存用户主动阅读过的章节。// frontend/public/sw.js const CACHE_NAME novel-chapters-v1 const urlsToCache [] self.addEventListener(install, event { event.waitUntil( caches.open(CACHE_NAME) .then(cache cache.addAll(urlsToCache)) ) }) self.addEventListener(fetch, event { // 只拦截 /api/books/*/chapters/ 请求 if (event.request.url.includes(/api/books/) event.request.url.includes(/chapters/)) { event.respondWith( fetch(event.request) .then(response { // 缓存成功响应 if (response.ok) { const responseToCache response.clone() caches.open(CACHE_NAME) .then(cache cache.put(event.request, responseToCache)) } return response }) .catch(() caches.match(event.request)) // 网络失败时返回缓存 ) } })在main.js中注册if (serviceWorker in navigator) { window.addEventListener(load, () { navigator.serviceWorker.register(/sw.js) }) }提示Service Worker 缓存需HTTPS本地开发用localhost也支持。测试离线效果Chrome DevTools → Application → Service Workers → Check “Offline”。本文还有配套的精品资源点击获取