ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Vue3漫画站从零开发:组件化改造到部署实战

Vue3漫画站从零开发:组件化改造到部署实战 简介这是一款基于Vue框架开发的漫画网站前端源码面向漫画爱好者、前端学习者及想要快速搭建内容展示型站点的开发者。项目采用组件化开发结构清晰适合用来理解Vue单页应用的组织方式也可作为二次开发的起点。资源共39个文件包含20个Vue组件、9个JavaScript脚本、4个JSON配置以及HTML、ICO、EditorConfig等基础文件压缩包约290KB。组件负责页面模块划分脚本处理交互与数据请求JSON用于路由、构建等配置整体规模适中便于逐文件研读。目前已有136人学习下载。源码中可以看到书架、榜单、分类、详情、搜索、用户等页面的实现覆盖常见漫画网站核心模块readme与注释也有助于快速上手。通过学习这份源码可掌握Vue组件通信、状态管理、路由配置和工程化工具搭配等实用技能适合作为前端实战参考模板。1. 从静态 HTML 到 Vue漫画站的组件化改造没那么玄很多人最开始做漫画网站第一反应是拿 HTML JavaScript 拼一个静态页面封面图横排、点击跳详情、内容靠 JS 手写innerHTML塞进模板。这套方案在只有十几部漫画时完全没有问题但一旦加上章节列表、阅读历史、自动续读、书架收藏这些功能全局变量和 DOM 操作就会迅速失控。换 Vue 不是赶时髦而是把“页面状态”这件事从“手动操作 DOM”变成“声明式绑定”。这个标题里的“设计源码”四个字很容易被误解——它不是什么视觉设计稿而是用 Vue 组织 HTML 结构和 JavaScript 逻辑之后的代码工程。换句话说你要交付的是一套别人 clone 下来就能npm install之后直接跑起来的项目。对 Vue 2 熟但对 Vue 3 的script setup还不太上手的开发者也能从这套方案里找到把静态稿改造成组件树的完整路径。2. Vue 组件化把漫画首页拆成可维护的 HTML JavaScript 单元2.1 先理清组件边界再写第一行代码用 Vue 写漫画站最容易犯的错是“页面级组件一把梭”把整个首页写成一个巨型.vue文件里面塞十几个ref()和一大段模板。正确做法是按业务区块拆组件。漫画站通常包含这些区块顶部导航栏、轮播 Banner、漫画分类标签、漫画卡片网格、底部推荐位。每个区块对应一个组件组件内只保留自己的状态和渲染逻辑。比如MangaCard.vue只负责接收manga对象并渲染封面、标题、评分和最近更新章节不关心这个卡片出现在首页还是搜索页。这样做的直接收益是详情页、书架页、搜索页都能复用同一个MangaCard组件而不是靠复制粘贴 HTML。2.2script setup下最常用的组件结构和参数传递以 Vue 3 为例一个标准的MangaCard.vue组件写法如下template div classmanga-card clickgoDetail div classmanga-cover img :srcmanga.cover :altmanga.title loadinglazy / span classmanga-badge{{ manga.category }}/span /div div classmanga-info h3{{ manga.title }}/h3 p classmanga-meta更新至第 {{ manga.latestChapter }} 话/p div classmanga-score span classscore-star★/span{{ manga.score }} /div /div /div /template script setup import { useRouter } from vue-router const props defineProps({ manga: { type: Object, required: true } }) const router useRouter() const goDetail () { router.push({ name: MangaDetail, params: { id: props.manga.id } }) } /script style scoped .manga-card { cursor: pointer; border-radius: 8px; overflow: hidden; background: #fff; transition: transform 0.2s ease, box-shadow 0.2s ease; } .manga-card:hover { transform: translateY(-4px); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12); } .manga-cover { position: relative; aspect-ratio: 3 / 4; } .manga-cover img { width: 100%; height: 100%; object-fit: cover; } /styledefineProps声明当前组件接收的参数父组件传什么字段、字段类型是什么一目了然。useRouter来自vue-router组件内部不需要知道自己在哪个页面被渲染统一通过路由跳转。scoped样式保证每个组件的 CSS 只作用于自身不会污染其他区块这是做组件化改造的一个底线。父组件MangaGrid.vue里用v-for渲染卡片这就是一个最小的“数据驱动视图”闭环template div classmanga-grid MangaCard v-formanga in mangaList :keymanga.id :mangamanga / /div /template script setup import { ref, onMounted } from vue import MangaCard from ./MangaCard.vue import { getHomeMangaList } from /api/manga const mangaList ref([]) onMounted(async () { mangaList.value await getHomeMangaList() }) /script这里有个参数细节:key必须用manga.id而不是循环索引index。漫画数据从接口拉取后会按评分排序当排序条件变化时用索引做 key 会导致 Vue 错误复用 DOM造成封面图错位和滚动位置异常。用唯一 ID 做 key框架才能正确识别哪些组件需要移动、哪些需要重建。2.3 样式方案从style scoped到全站主题变量漫画站视觉上最需要统一的是封面圆角、阴影层级、卡片间距和加载状态。推荐在src/assets/styles/下建一个variables.css定义全局主题变量然后在每个组件的样式中使用这些变量:root { --manga-card-radius: 10px; --manga-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); --manga-primary: #ff6b35; --manga-bg: #f6f7fb; }组件里直接写border-radius: var(--manga-card-radius)。后续如果要做暗黑模式只需要给html加一个>// router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /, name: Home, component: () import(/views/HomeView.vue) }, { path: /detail/:id, name: MangaDetail, component: () import(/views/MangaDetailView.vue), props: true }, { path: /reader/:id/:chapter, name: MangaReader, component: () import(/views/MangaReaderView.vue), props: true } ] const router createRouter({ history: createWebHistory(), routes })关键点在于props: true。开启后路由参数id和chapter会自动作为 props 传入组件在组件里直接用defineProps接收即可不需要再通过route.params去读。这样测试组件时可以直接传 props 进去渲染不依赖路由环境。3.2 阅读器页的 JavaScript 数据流设计漫画阅读器和普通页面最大的差异在于它的核心交互是连续翻页每一页的图片加载需要预加载下一张且用户随时可能退出再进来要能记住位置。这个场景下用ref一个个声明状态会非常分散更好的做法是用一个readerState对象集中管理// views/MangaReaderView.vue script setup import { ref, reactive, computed, watch, onMounted } from vue import { useRoute } from vue-router import { getChapterImages } from /api/manga import { saveReadingProgress } from /utils/storage const props defineProps({ id: { type: String, required: true }, chapter: { type: String, required: true } }) // 阅读器核心状态 const readerState reactive({ images: [], currentPage: 1, totalPages: 0, loading: true, error: }) // 当前页码对应的图片 URL const currentImage computed(() { if (!readerState.images.length) return return readerState.images[readerState.currentPage - 1] }) // 章节变化时重新拉取图片 watch( () props.chapter, async (newChapter) { readerState.loading true readerState.currentPage 1 await loadChapterImages(newChapter) }, { immediate: true } ) async function loadChapterImages(chapterId) { try { const { images } await getChapterImages(props.id, chapterId) readerState.images images readerState.totalPages images.length // 尝试恢复阅读进度 const saved localStorage.getItem(progress_${props.id}_${chapterId}) if (saved) { readerState.currentPage parseInt(saved, 10) } } catch (e) { readerState.error 章节图片加载失败 } finally { readerState.loading false } } function nextPage() { if (readerState.currentPage readerState.totalPages) { readerState.currentPage handleProgressSave() } } function prevPage() { if (readerState.currentPage 1) { readerState.currentPage-- handleProgressSave() } } function handleProgressSave() { saveReadingProgress(props.id, props.chapter, readerState.currentPage) } onMounted(() { // 监听键盘左右键翻页 const keyHandler (e) { if (e.key ArrowRight) nextPage() if (e.key ArrowLeft) prevPage() } window.addEventListener(keydown, keyHandler) }) /script这段代码里watch监听props.chapter是关键。用户在详情页点击“下一话”时Vue Router 会直接把 URL 中的chapter参数替换掉组件实例不会重新创建只有watch能捕获到这个变化并触发图片重新拉取。如果不写这个watch会发现点击下一话后页面数据没刷新这是漫画站开发里面最常见的“跳转不更新”问题。进度保存用localStorage是合理的因为阅读进度属于单机偏好。如果要做跨设备同步才需要把进度上报到后端接口。这里要注意computed返回的currentImage在图片列表为空时要返回空字符串避免模板里拿到undefined导致图片路径拼接错误。图片预加载建议放在nextPage之外的watch中watch( () readerState.currentPage, (page) { // 预加载下一页 if (page readerState.totalPages) { const img new Image() img.src readerState.images[page] } } )这类预加载的细节对下载图片较慢的源站尤其有用用户连续翻页时不会被下一张图的加载时间卡住。3.3 图片懒加载策略与loadinglazy的边界漫画列表页的懒加载直接用浏览器原生属性就够了。但阅读器页要小心——阅读器内连续翻页的图片如果也用loadinglazy在快速翻页时浏览器可能来不及加载已经滚动到视口的图片导致短暂白屏。阅读器内建议不用懒加载而是靠预加载机制列表页和详情页则放心使用。漫画详情页的封面图可以包一层错误处理组件用 JavaScript 监听图片加载失败img :srcmanga.cover :altmanga.title loadinglazy errorhandleCoverError / script setup const handleCoverError (e) { e.target.src https://via.placeholder.com/300x400?textCover } /script这个兜底逻辑对 CDN 图片失效、外链图片防盗链的场景非常实用。防盗链图片在浏览器端会爆 403onerror捕获后替换为占位图避免视觉上出现裂开的图标。4. 书架与搜索Vue 状态管理 HBuilder 环境配置下的小型应用实战4.1 用 Pinia 管理书架的收藏状态和阅读历史漫画站的数据不多但状态来源分散书架收藏存本地、阅读历史存localStorage、当前正在追的漫画需要跨页面展示。如果你的目标尺寸是一个单日几千上万的漫画站可以不用 Pinia但如果要维护收藏状态在首页卡片、详情页按钮、书架页三个地方同步就用 Pinia 统一管理。// stores/manga.js import { defineStore } from pinia export const useMangaStore defineStore(manga, { state: () ({ favorites: JSON.parse(localStorage.getItem(favorites) || []), history: JSON.parse(localStorage.getItem(history) || []) }), getters: { favoriteCount: (state) state.favorites.length, isFavorite: (state) (id) state.favorites.some((item) item.id id) }, actions: { toggleFavorite(manga) { const index this.favorites.findIndex((item) item.id manga.id) if (index -1) { this.favorites.splice(index, 1) } else { this.favorites.unshift(manga) } localStorage.setItem(favorites, JSON.stringify(this.favorites)) }, addHistory(record) { const index this.history.findIndex((item) item.id record.id) if (index -1) { this.history.splice(index, 1) } this.history.unshift(record) // 只保留最近 50 条 if (this.history.length 50) { this.history.pop() } localStorage.setItem(history, JSON.stringify(this.history)) } } })用 Pinia 管理本地存储状态有一个明显的好处多个组件可以通过同一个store实例上的isFavorite方法同步判断收藏状态。如果不用 store首页卡片和详情页按钮各自读取localStorage点击详情页收藏按钮后返回首页首页卡片的状态不会自动更新——除非你用自定义事件通知但那会非常绕。script setup import { useMangaStore } from /stores/manga import { useRoute } from vue-router const mangaStore useMangaStore() const route useRoute() const toggleFavorite () { const manga { id: route.params.id, title: detail.value.title, cover: detail.value.cover // ...其他字段 } mangaStore.toggleFavorite(manga) } /script这段代码体现的是“组件不直接操作 localStorage而是通过 store 的 action 做变更”。后续如果要把收藏同步到后端只需要改toggleFavorite一个方法所有调用它的组件不需要动。4.2 搜索功能防抖与取消过期请求漫画搜索是典型的“边输入边出结果”的场景。不要每敲一个字符就发一次请求用watch加setTimeout做防抖const keyword ref() const searchResults ref([]) let debounceTimer null watch(keyword, (newVal) { clearTimeout(debounceTimer) if (!newVal.trim()) { searchResults.value [] return } debounceTimer setTimeout(async () { const results await searchManga(newVal.trim()) searchResults.value results }, 300) })有一个搜索场景的坑用户输入“海贼”后快速改为“火影”第一次请求还没返回第二次请求已经发出先返回的结果可能覆盖后返回的结果。解决办法是用一个递增的请求序号let requestSeq 0 watch(keyword, (newVal) { const seq requestSeq // ... const results await searchManga(newVal) if (seq requestSeq) { searchResults.value results } })这比AbortController更简单直接在漫画站这种小型应用中足够可靠。4.3 HBuilder 创建 Vue 项目与在浏览器中调试如果你的环境偏爱 HBuilder它的 Vue 3 项目创建流程是新建项目 → 选择Vue 3 项目模板 → 生成后npm install安装依赖。一个容易踩的坑是 HBuilder 内置终端默认的 npm 源比较慢拉取依赖卡住。建议在项目根目录建立.npmrc文件registryhttps://registry.npmmirror.com配置完成后在 HBuilder 的终端里运行npm install或者直接打开内置终端执行npm run dev启动开发服务器。HBuilder 对 HTML、CSS、JavaScript 的自动补全和语法检查做得不错但你也要知道它默认打开的内置浏览器可能是自带的 webview这种环境下 Vue 开发服务器能正常跑但这个浏览器和 Chrome DevTools 的调试能力有差距。要做完整的 Vue 调试建议在npm run dev启动后手动复制地址到 Chrome 中打开装好 Vue.js devtools 扩展。这样看到的组件层级、props 传参、Pinia 状态变更都会直接可视化定位问题比在 HBuilder 内置浏览器里高效得多。5. 打包与部署Vue 漫画站的构建配置、vue 打包后布局异常排查与 CDN 路径修正5.1vite.config.js里最需要改的 3 个参数Vue 3 项目默认用 Vite 构建。漫画站打包后最容易遇到两类问题图片引不到、路由刷新 404。两者都可以通过vite.config.js解决import { defineConfig } from vite import vue from vitejs/plugin-vue import { fileURLToPath, URL } from node:url export default defineConfig({ plugins: [vue()], base: ./, resolve: { alias: { : fileURLToPath(new URL(./src, import.meta.url)) } }, build: { chunkSizeWarningLimit: 1500, rollupOptions: { output: { manualChunks: { vue: [vue, vue-router, pinia], reader: [/views/MangaReaderView.vue] } } } }, server: { port: 8080, host: 0.0.0.0 } })base: ./是相对路径的关键——打包后的index.html里引用的 JS 和 CSS 路径会变成相对路径这样无论把 dist 目录部署到服务器根目录还是subdir下都不会出现资源 404。manualChunks把 Vue 全家桶独立打成一个大包读者阅读器单独拆包这样用户访问首页时不需要加载阅读器的全部代码优化首屏加载时间。路由刷新 404 的问题在部署阶段异常常见尤其是本地开发正常、部署到 Nginx 后刷新/detail/123直接白屏。这需要 Nginx 配置回退location / { try_files $uri $uri/ /index.html; }原理是开发时 Vue Router 用createWebHistory()路由是浏览器负责的所以不经过服务器。部署后服务器接收到/detail/123的请求先在文件系统里找是否有这个路径对应的文件找不到就回退到index.html由 Vue Router 接管并渲染匹配的组件。5.2 静态资源 CDN 部署的路径分层漫画站的图片通常不是打包在项目里的而是放在图床或对象存储上。此时要注意域名分离页面静态资源JS/CSS放一个 CDN漫画图片放另一个 CDN。原因是浏览器对同一域名有并发连接数限制页面脚本和图片如果共用域名首屏图片加载会拖慢脚本执行。在代码里把图片域名抽成常量// src/config/index.js export const IMG_CDN_BASE https://media.manga-site.dev export const API_BASE https://api.manga-site.dev漫画封面路径一律拼接前缀后续换 CDN 只需要改一个文件。如果用对象存储建议给每个图片加版本参数const coverUrl ${IMG_CDN_BASE}/covers/${manga.id}.jpg?v${manga.cover_version}漫画封面更新后cover_version变化浏览器会重新拉取图片而不是命中旧缓存。这是漫画站运营中一个真实存在且容易被忽视的细节。5.3 观感优化和加载速度的平衡打包后的动漫图片资源占体积大头。漫画图片本身是 JPG 或 WebP不需要通过vite做 base64 内联那样只会让 HTML 文件膨胀。正确方式是保持图片为外部 URL只对项目的 HTML、CSS、JavaScript 做压缩。一个适合漫画站的加载优化手段是请求分级首屏只渲染封面和标题详情描述、相关推荐等模块在onMounted之后延迟 200ms 再加载。这样核心内容先展示次要内容后填充用户感知上的速度明显变快。实现起来就是在组件里加一个reactive状态并手动控制const displayState reactive({ loaded: false }) onMounted(() { setTimeout(() { displayState.loaded true }, 200) })模板里用v-ifdisplayState.loaded包裹次要模块配合transition做淡入效果既不影响首屏速度也不会有明显的“内容后补”突兀感。6. 漫画站排队效应用体验闭环提升阅读完成率漫画网站的核心指标不是访客数而是阅读完成率。一个用户打开第一话、看了两页就关掉和连续把整部漫画追到最新话运营价值完全不同。在技术实现上有几个级别可以明显提升这个指标。章节接力的自动跳转。阅读器最后一页翻完时停留在“已完结”页会让用户流失。正确的做法是在最后一页的翻页事件里判断是否有下一话如果有就自动跳转弹窗让用户确认。这是 JavaScript 逻辑不依赖后端function nextPage() { if (readerState.currentPage readerState.totalPages) { readerState.currentPage handleProgressSave() } else if (currentChapterInfo.next) { router.push(/reader/${props.id}/${currentChapterInfo.next}) } }阅读进度回跳。在详情页显示“继续阅读第 12 话”这个入口依赖前面保存的历史数据。组件挂载时读取store中的历史记录找到当前漫画的最新记录把按钮的文本和跳转地址动态化。这个功能看似简单但会让老用户的回访路径缩短一到两次点击留存提升明显。图片加载失败的静默重试。漫画阅读器里如果某张图片失败直接显示“加载失败”四个字用户会关闭页面。更专业的方案是自动重试两到三次。可以在全局拦截图片error事件对同一张图重试最多两次const retryCount new Map() function retryImage(img) { const src img.src const count retryCount.get(src) || 0 if (count 2) { retryCount.set(src, count 1) setTimeout(() { img.src src (src.includes(?) ? : ?) retry count }, 500) } }重试时给 URL 加上retry参数绕过浏览器对相同 URL 的缓存失败结果这是一线做漫画站的工程师常用的处理方式。把这段代码放到阅读器组件外层的onMounted中用事件委托监听所有图片的error事件即可不需要为每一张图片单独绑定处理器。最后完成一套基于 Vue 的漫画站源码后你最需要测试的不是桌面浏览器而是手机端的滚动手感、翻页响应、以及图片在弱网下的加载表现。Vue 框架并不会自动优化性能它只是把渲染逻辑变得清晰可控。真正的提升来自于你对阅读场景的预判和细节的打磨这些代码量不大但每一项都会直接反映在用户的留存和回访上。本文还有配套的精品资源点击获取
返回列表