
1. 项目背景与方案选型1.1 为什么选择 Vite 来搭建博客平台如果你最近在关注前端圈子的动态应该能明显感受到 Vite 已经从一个“新玩具”变成了真正能扛起生产环境的主力构建工具。我在搭建这个现代化 Vue 技术博客平台之前其实也纠结过一段时间到底是继续用 Webpack还是切换到 Vite后来实际动手做了一遍答案其实已经很清楚了——对于 Vue 项目尤其是内容型的博客站点Vite 的开发体验和构建效率优势几乎是碾压级的。先说最直观的感受。博客这个场景有个特点你会在本地频繁地写文章、看效果、改样式一天下来可能要启动十几次开发服务器。以前用 Webpack 的时候每次冷启动少说也要等十几秒到几十秒改完代码热更新有时候还会卡顿特别影响写作节奏。换成 Vite 之后基于 ES Module 的 dev server 几乎是秒开浏览器只需要按需加载当前页面用到的模块根本不用像 Webpack 那样启动时就把整个项目打包一遍。这个开发体验的差异用过一次就回不去了。从技术层面讲Vite 的核心优势在于它利用了浏览器原生 ES Module 的能力。开发环境下Vite 直接把源码推给浏览器依赖模块用 esbuild 预构建源码模块按需编译所以无论项目有多大dev server 的启动速度都能保持在一个非常理想的范围。而到了生产构建阶段Vite 又用 Rollup 做打包兼顾了开发速度和产物质量。这套“开发用 esbuild、构建用 Rollup”的组合拳恰恰是它区别于 Webpack 单引擎方案的关键。当然选择 Vite 也不只是因为快。这个博客平台还涉及路由、Markdown 渲染、代码高亮、响应式布局等一系列需求Vue 3 的生态在这些方面已经非常成熟而 Vite 作为 Vue 官方推荐的构建工具对 Vue 单文件组件的支持也是最及时、最完整的。团队维护成本低社区资料丰富后续想扩展功能也基本没有踩坑的阻碍。1.2 博客平台的核心需求拆解在动手写代码之前我先把博客平台的需求理了一遍。说白了一个技术博客平台要解决的核心问题其实就三件事文章的编写和展示能不能用 Markdown 舒服地写作能不能对代码块做高亮能不能支持文章目录和代码复制内容的管理和组织文章列表怎么展示、标签体系怎么设计、文章详情页的路径结构是否友好性能和体验页面加载快不快首屏渲染行不行移动端阅读舒不舒服打包体积有没有失控。基于这些诉求我把技术栈定成了 Vue 3 Vite Vue Router Pinia内容渲染用 Markdown样式方案直接用 SCSS再配合 Vite 环境下天然好用的 import.meta.glob 做文章模块的批量导入。整个项目没有引入太重的内容管理系统文章以本地 Markdown 文件维护既符合程序员写博客的习惯也省去了数据库和后端接口的成本。另外值得一提的是这个项目的代码结构大概按照“页面组件 视图容器 组合式函数”的层次来组织工具函数和业务逻辑尽量抽到独立的模块里。这样做的目的很简单文章内容、组件逻辑、路由配置各司其职后续想加个归档页、标签页或者换一套主题改动范围都控制得住。1.3 关于技术选型的几点个人看法经常有人在 Vite、Webpack 和 Next.js 这些方案之间纠结这里顺便说几句我自己的体会。拿 Next.js 和 Vite React 比较如果你只做一个纯前端的博客平台Next.js 的 SSR 和静态生成能力确实有优势但相应的学习成本和部署复杂度也会高一些。而 Vite Vue 这套组合能在保持开发效率的前提下配合 vite-plugin-ssr 或者单纯的前端渲染方案满足绝大多数个人博客的需求。我个人觉得选型的关键不是哪个框架“更高级”而是你的场景到底需要什么。内容站对 SEO 有硬性要求就老老实实考虑 SSR 或者静态生成如果是偏内部使用、登录后浏览的技术笔记平台纯 SPA 完全没问题。至于 Vue 和 React 的对比这个聊起来三天三夜都说不完。简单说Vue 3 的 Composition API 配合script setup在组件逻辑复用和模板表达能力上非常顺手加上 Vite 的加持中小型项目里它的开发效率是很有竞争力的。React 生态更庞大、更灵活但需要你自己做更多的组装决策。做博客平台这种偏内容型的项目Vue 的模板语法单文件组件会让维护体验更轻松。2. Vite 环境配置与项目初始化全流程2.1 使用 Vite 创建 Vue 3 项目创建项目这个步骤其实没什么难度网上到处都能搜到命令但很多人只复制命令、不理解参数含义后面遇到问题就容易懵。这里我把关键步骤拆开说一下。npm create vitelatest tech-blog -- --template vue这条命令的含义是使用最新版的 Vite 脚手架创建一个名为tech-blog的项目模板指定为vue。--template vue这一步会给项目生成 Vue 3 的基础骨架包括src目录、index.html、vite.config.js等基本文件。如果你对 TypeScript 比较熟也可以把模板换成vue-tsnpm create vitelatest tech-blog -- --template vue-ts我在实际项目中直接用了 Vue 3 JavaScript 的组合理由很简单博客项目本身逻辑不复杂JS 的灵活度更高写起来也更快。当然如果团队有规范或者个人偏好TypeScript 自然也没问题Vite 对 TS 的支持同样是开箱即用的。项目创建好之后需要安装依赖并启动开发服务器cd tech-blog npm install npm run devnpm install 这一步如果要等很久建议检查一下 npm 仓库镜像源或者直接用 pnpm。Vite 对 pnpm 支持得很好依赖安装速度和磁盘占用都比 npm 有优势。我自己后期的项目基本都切换到了 pnpm体感差异确实明显。2.2 vite.config.js 关键配置解析Vite 项目的核心配置文件是vite.config.js。新建的项目里这个文件内容很少但完全够用。我把博客平台用到的几个关键配置项列出来给大家看import { defineConfig } from vite import vue from vitejs/plugin-vue import { fileURLToPath, URL } from node:url export default defineConfig({ plugins: [vue()], resolve: { alias: { : fileURLToPath(new URL(./src, import.meta.url)), blog: fileURLToPath(new URL(./src/blog, import.meta.url)) } }, server: { host: 0.0.0.0, port: 3000, open: true, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } }, build: { outDir: dist, sourcemap: false, chunkSizeWarningLimit: 1500, rollupOptions: { output: { manualChunks: { vue: [vue, vue-router, pinia], markdown: [marked, highlight.js] } } } } })这里有几个点需要特别解释一下第一路径别名。指向src目录这在 Vue 生态里几乎是标配。如果你文章目录比较深建议像blog这样再单独设一个别名避免出现一串../../..的相对路径维护起来非常痛苦。第二开发服务器代理。如果你的博客平台后期打算接入评论系统、搜索接口等后端服务server.proxy是避免跨域问题最省事的方案。把/api的请求代理到后端地址前端代码里统一写相对路径就行不用每处都配环境变量。第三手工分包manualChunks。这是经常被忽略但很实用的优化项。默认情况下 Vite 会把所有第三方依赖打进一个vendor块里但 Vue、Markdown 解析器这类体积较大的库把它们单独拆出来可以更好地利用浏览器缓存。用户再次访问博客时如果文章内容更新了只有业务代码部分的 chunk 会变化第三方库的缓存依然能用加载速度会快不少。2.3 环境变量与模式管理Vite 内置了一套环境变量系统通过.env文件来管理不同环境下的配置。最常见的三个文件是文件作用.env所有环境下都会加载.env.development开发环境下加载.env.production生产构建环境下加载另外你也可以自定义模式比如测试环境用vite build --mode testVite 就会去读取.env.test文件。需要注意的是只有以VITE_前缀开头的变量才会暴露给前端代码这是 Vite 刻意设计的命名约定目的是防止敏感信息泄漏到客户端。我在博客平台里就把站点名称、站点描述、文章列表分页条数、评论系统开关这些都放到了环境变量里# .env.development VITE_SITE_NAMEDevBlog VITE_SITE_DESC一个基于 Vite Vue 3 的技术博客平台 VITE_API_BASE/api VITE_COMMENT_ENABLEDtrue # .env.production VITE_SITE_NAMEDevBlog VITE_SITE_DESC一个基于 Vite Vue 3 的技术博客平台 VITE_API_BASEhttps://api.yourblog.com VITE_COMMENT_ENABLEDtrue在代码里的读取方式也非常简单const siteName import.meta.env.VITE_SITE_NAME用import.meta.env而不是传统 Vue CLI 项目的process.env这是 Vite 项目跟 Webpack 项目一个很重要的区别。如果从 Vue 2 Webpack 的项目迁移过来这个差异要注意。2.4 安装 Vue Router 和 Pinia路由和状态管理是一个 Vue 应用不可缺少的基础设施。对于博客平台Vue Router 用来管理页面跳转Pinia 用来管理站点级的状态比如主题模式、文章筛选条件等。npm install vue-router4 piniaVue Router 4 是专门为 Vue 3 设计的版本API 和 Vue 2 时代的 Router 3 有一些差别。核心的路由配置如下import { createRouter, createWebHistory } from vue-router const router createRouter({ history: createWebHistory(), routes: [ { path: /, name: home, component: () import(/views/HomeView.vue) }, { path: /post/:postId, name: post-detail, component: () import(/views/PostDetailView.vue) }, { path: /tags, name: tags, component: () import(/views/TagsView.vue) }, { path: /:pathMatch(.*)*, name: not-found, component: () import(/views/NotFoundView.vue) } ], scrollBehavior() { return { top: 0 } } }) export default router这个配置里有一个跟普通业务项目不太一样的点文章详情页的路由用了一个:postId参数后面我们在处理动态路由的时候会基于这个参数去加载对应的 Markdown 内容。另外scrollBehavior返回{ top: 0 }确保用户从一篇文章跳转到另一篇文章时页面能回到顶部而不是停留在上一次滚动的位置。这个细节不处理的话阅读体验会差不少。Pinia 的配置也非常简单直接在main.js里挂载即可import { createApp } from vue import { createPinia } from pinia import App from ./App.vue import router from ./router const app createApp(App) app.use(createPinia()) app.use(router) app.mount(#app)2.5 关于 Vuex 和 Pinia 的选择这里忍不住多说一点。很多人新学 Vue 3第一个遇到的困惑就是“状态管理到底用 Vuex 还是 Pinia”。我的建议很明确直接上 Pinia。Pinia 是 Vue 官方团队正式推荐的状态管理库从设计理念上就更贴合 Composition API 的直觉用法。它没有 Vuex 那些繁琐的 mutation 概念直接支持setup store的写法TypeScript 类型推断也更好。同层级做对比的话Vuex 像是社区历史的沉淀Pinia 则代表了新的方向。对于博客平台这种对状态管理要求不高的项目Pinia 的轻量和直观反而是一种优势。3. 核心功能实现文章列表、Markdown 渲染与动态路由3.1 用 import.meta.glob 批量加载文章博客平台最关键的问题是怎么把几十上百篇 Markdown 文章管理起来。相比于逐个手动 importVite 提供的import.meta.glob是一个接近完美的方案。import.meta.glob允许我们通过一个文件路径模式一次性引入匹配的所有模块。Vite 在预构建阶段会把这些文件全部转换成异步导入函数并且支持自定义导入方式。具体到博客场景我推荐使用query: ?raw这种写法来加载 Markdown 原文// src/blog/loadPosts.js const modules import.meta.glob(../posts/*.md, { query: ?raw, import: default }) export async function getAllPosts() { const posts [] for (const path in modules) { const content await modules[path]() const slug path.split(/).pop().replace(.md, ) const meta parseFrontmatter(content) posts.push({ slug, meta, content }) } return posts.sort((a, b) new Date(b.meta.date) - new Date(a.meta.date)) }这里面有几个关键细节query: ?raw表示直接把文件内容以字符串的形式导入而不是交给 Vite 的静态资源处理管线import: default是因为?raw导出的默认导出就是源码字符串打包时 Vite 会自动将这些文件变成独立的模块按需加载文章排序放在数据加载阶段做而不是渲染阶段避免每次渲染都重复排序。import.meta.glob还有一个兄弟写法import.meta.globEager在新版本 Vite 中改名为import.meta.glob(..., { eager: true })。两种方式的区别在于加载时机非 eager 模式是动态导入eager 模式是打包时同步导入。鉴于文章数量通常不大用 eager 模式反而更直观省去了写async/await的麻烦。3.2 Markdown 元数据解析与文章列表设计博客文章除了正文内容还需要标题、日期、标签、摘要等元信息。我采用的做法是直接在 Markdown 文件头部写 YAML 风格的 frontmatter--- title: 基于 Vite 构建的现代化 Vue 技术博客平台 date: 2024-11-15 tags: [Vite, Vue 3, 前端工程化] description: 这篇文章完整记录了我从零搭建一个基于 Vite 和 Vue 3 的技术博客平台的全部过程包括技术选型、配置方案、核心功能实现与构建优化。 --- 这里是文章正文……解析 frontmatter 不需要引入复杂的内容管理系统写一个简单的解析函数就够了。原理也非常直观找到文件开头---和第二个---之间的文本按行切分再用一个正则或简单的 key-value 拆分提取出字段。export function parseFrontmatter(content) { const match content.match(/^---\n([\s\S]*?)\n---\n/) if (!match) { return { meta: {}, content } } const meta {} match[1].split(\n).forEach(line { const idx line.indexOf(:) if (idx -1) { const key line.slice(0, idx).trim() const value line.slice(idx 1).trim() meta[key] value.replace(/^\[|\]$/g, ).split(,).map(item item.trim()) } }) return { meta, content: content.replace(match[0], ) } }有一点经验之谈要分享frontmatter 的tags用成[Vite, Vue 3, 前端工程化]这种数组格式比用逗号分隔的字符串可读性更好解析也更不容易出错。另外日期格式强烈建议统一使用YYYY-MM-DD这样在排序和后续做归档页时不需要额外的日期解析逻辑。有了解析好的元数据文章列表组件就可以用很干净的模板展示出来template div classpost-list article v-forpost in posts :keypost.slug classpost-card h2 RouterLink :to/post/${post.slug} {{ post.meta.title }} /RouterLink /h2 time :datetimepost.meta.date{{ formatDate(post.meta.date) }}/time p{{ post.meta.description }}/p div classpost-tags span v-fortag in post.meta.tags :keytag classtag {{ tag }} /span /div /article /div /template这里要注意的是通过post.slug而不是数字 ID 来做路由参数。slug 是文章文件名的拼音或英文缩写对 SEO 友好URL 语义化程度也更高。例如/post/vite-vue-tech-blog一眼就能看出是哪个主题而/post/123就完全做不到。3.3 Markdown 渲染与代码高亮方案文章的正文是 Markdown 文本渲染成 HTML 肯定要引入解析器。我用的是marked配合highlight.js做代码高亮。选择这两个库的原因很简单marked轻量、插件机制清晰highlight.js支持的语言丰富、跟marked配合成熟。安装依赖npm install marked highlight.js然后写一个组合式函数负责把 Markdown 源码转换成带高亮的 HTMLimport { marked } from marked import hljs from highlight.js import highlight.js/styles/github-dark.min.css marked.setOptions({ highlight(code, lang) { if (lang hljs.getLanguage(lang)) { return hljs.highlight(code, { language: lang }).value } return hljs.highlightAuto(code).value }, gfm: true, breaks: true }) export function renderMarkdown(source) { return marked.parse(source) }在实际使用之前我一直以为代码高亮是很复杂的事真正做下来发现只要在marked配置里挂了highlight钩子剩下的就是样式问题。不过这里有个刚踩过的坑必须提一下如果没有在前端代码里引入对应的 CSS 主题文件就算高亮逻辑写对了页面上的代码块也是一坨没有颜色的纯文本。这个import highlight.js/styles/github-dark.min.css很关键而且 GitHub 风格的主题在博客站里观感最自然。文章详情页在拿到 HTML 后出于安全考虑还需要注意 XSS 问题。虽然我们文章的来源是自己写的 Markdown 文件但凡是渲染用户可输入内容都要保持警觉。如果文章内容可能包含外部投稿建议做一次 DOMPurify 的清洗再加一道保险。3.4 动态路由与文章详情页的实现上一节我们创建了/post/:postId的路由接下来就需要让 Vue Router 在进入详情页时根据postId参数去加载对应的 Markdown 内容。一个相对优雅的做法是维护一个“slug 到组件模块”的映射表然后在路由守卫或者详情页onMounted的时候去动态加载。考虑到博客文章的数量通常不会多到一个映射表都管不住的程度这种方案在性能和可维护性上都是平衡得最好的。我的实现思路是这样的先在loadPosts.js里导出一个getPostBySlug函数const modules import.meta.glob(../posts/*.md, { query: ?raw, import: default }) export async function getPostBySlug(slug) { const path ../posts/${slug}.md const loader modules[path] if (!loader) { return null } const content await loader() const parsed parseFrontmatter(content) return { slug, meta: parsed.meta, content: renderMarkdown(parsed.content) } }然后在详情页组件里script setup import { ref, watch } from vue import { useRoute } from vue-router import { getPostBySlug } from blog/loadPosts const route useRoute() const post ref(null) const notFound ref(false) async function loadPost(slug) { const result await getPostBySlug(slug) if (result) { post.value result notFound.value false document.title ${result.meta.title} | DevBlog } else { notFound.value true } } watch( () route.params.postId, (newSlug) { loadPost(newSlug) }, { immediate: true } ) /script template div v-ifpost classpost-detail h1{{ post.meta.title }}/h1 div classpost-meta time{{ formatDate(post.meta.date) }}/time span v-fortag in post.meta.tags :keytag{{ tag }}/span /div div classpost-content v-htmlpost.content/div /div div v-else-ifnotFound classnot-found 文章不存在请检查地址是否正确 /div /template留意一个容易被忽略的细节我用了watch监听route.params.postId而不是只在onMounted里加载一次。这背后的原因很简单——如果用户在文章详情页里通过某种方式切换了另一篇文章的链接而 Vue Router 复用了同一个组件实例onMounted不会再次触发这时只有通过响应式监听才能拿到新的参数并重新加载数据。对博客平台来说文章详情页之间互跳是非常常见的操作这个坑不能不防。3.5 博客列表的分页与筛选文章多起来之后首页把所有文章一次性渲染出来显然不合理。我做了一个简单的分页机制核心逻辑放在组合式函数里import { ref, computed } from vue import { getAllPosts } from blog/loadPosts import { useRoute, useRouter } from vue-router const PAGE_SIZE Number(import.meta.env.VITE_PAGE_SIZE) || 10 export function usePagination() { const route useRoute() const router useRouter() const posts ref([]) const currentPage computed(() Number(route.query.page) || 1) const totalPages computed(() Math.ceil(posts.value.length / PAGE_SIZE)) const pagePosts computed(() { const start (currentPage.value - 1) * PAGE_SIZE return posts.value.slice(start, start PAGE_SIZE) }) async function loadPosts() { posts.value await getAllPosts() } function changePage(page) { router.push({ query: { ...route.query, page } }) } return { posts, currentPage, totalPages, pagePosts, loadPosts, changePage } }这里的设计有一个值得借鉴的地方页码通过route.query.page来维护而不是单纯用一个响应式 ref。这样做的优势是用户刷新页面、复制链接、甚至浏览器前进后退分页状态都会被 URL 记录下来不会因为刷新而导致数据丢失。很多博客项目的分页做出来一个 bug刷新之后页码变回第一页根源就在于没有把页码跟路由绑定。3.6 相关内容推荐与标签体系博客平台如果没有“上一篇/下一篇”或者相关文章推荐用户看完一篇文章就只能自己回列表页翻体验是断层的。我实现的方案不复杂核心逻辑是基于标签相似度做推荐export function getRelatedPosts(currentPost, allPosts, limit 3) { const currentTags new Set(currentPost.meta.tags) return allPosts .filter(post post.slug ! currentPost.slug) .map(post { const overlap post.meta.tags.filter(tag currentTags.has(tag)).length return { post, score: overlap } }) .filter(item item.score 0) .sort((a, b) b.score - a.score) .slice(0, limit) .map(item item.post) }这个实现非常直白统计当前文章标签跟其他文章标签的重合数量按重合数量排序取前三条。如果文章量不大这种写法已经完全够用。如果哪一天文章到了几百篇的规模需要做更精准的推荐可以再考虑引入 TF-IDF 或者简单的向量相似度但那就是另一个层面的优化了博客平台初期完全没必要背这个包袱。标签页的处理跟列表页类似根据route.query.tag参数过滤文章同时展示所有标签及对应文章数量const tagCountMap computed(() { const map {} posts.value.forEach(post { post.meta.tags.forEach(tag { map[tag] (map[tag] || 0) 1 }) }) return map })4. 页面布局、样式方案与移动端适配4.1 整体布局设计与组件拆分博客平台的布局我参考了很多优秀技术博客的做法最终定为三段式顶部导航栏、中间内容区、底部版权栏。顶部导航放站点名称、首页、标签、关于页等入口中间内容区是核心展示区域底部放版权信息和备案信息。组件的拆分上我把每个页面的公共部分抽成了通用组件SiteHeader.vue顶部导航包含站点标题、菜单、主题切换按钮PostCard.vue文章卡片在列表页和标签页复用PostPagination.vue分页控件SiteFooter.vue底部信息TagList.vue标签列表在标签页和文章详情页底部复用。这种组件拆分的原则是“一次编写、多处复用”。如果用 Vue 3 的script setup写法组件的属性和事件都非常直观比如PostCard的 props 定义script setup defineProps({ post: { type: Object, required: true } }) /script有了这样的组件抽象首页和标签页的模板就会干净很多减少大量重复 DOM 结构也让样式维护的范围更聚焦。4.2 SCSS 变量与主题切换样式方案我选了 SCSS配合 Vue 3 的style langscss写法。一套合适的变量体系能极大减少后期调样式的成本尤其是当你想做“暗色模式”这种功能时变量几乎就是必备方案。我定义了一个变量文件统一管理颜色、字体、间距、圆角这些基础设计 token// src/styles/variables.scss :root { --color-bg: #ffffff; --color-text: #24292f; --color-link: #0969da; --color-border: #d0d7de; --color-card-bg: #f6f8fa; --color-code-bg: #f6f8fa; --font-sans: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif; --font-mono: SF Mono, SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; --radius-md: 8px; --radius-lg: 12px; --content-max-width: 860px; } [data-themedark] { --color-bg: #0d1117; --color-text: #c9d1d9; --color-link: #58a6ff; --color-border: #30363d; --color-card-bg: #161b22; --color-code-bg: #161b22; }主题切换的实现不复杂默认在html标签上挂一个>// stores/theme.js import { defineStore } from pinia export const useThemeStore defineStore(theme, { state: () ({ theme: localStorage.getItem(blog-theme) || light }), actions: { toggleTheme() { this.theme this.theme light ? dark : light localStorage.setItem(blog-theme, this.theme) applyTheme(this.theme) }, initTheme() { const saved localStorage.getItem(blog-theme) if (!saved window.matchMedia((prefers-color-scheme: dark)).matches) { this.theme dark } applyTheme(this.theme) } } }) function applyTheme(theme) { document.documentElement.setAttribute(data-theme, theme) }4.3 响应式设计与移动端适配的经验现在访客用手机看博客的比例非常高移动端适配绝对不能事后补救。我用的是纯粹的 CSS 媒体查询方案没有引入 Tailwind CSS 或者 UI 框架原因很简单博客的布局复杂度不高为了一两个响应式断点引入一套完整的原子化 CSS 体系性价比太低。常见的断点设置如下media (max-width: 768px) { // 平板和手机 .container { padding: 0 16px; } .post-card h2 { font-size: 1.3rem; } } media (max-width: 480px) { // 小屏手机 .site-header { flex-direction: column; gap: 10px; } .post-content { font-size: 15px; } }一个实用的细节文章正文区域的代码块在手机上很容易溢出导致页面出现横向滚动条。解决方案是在pre和code上设置合适的overflow-x: auto而不是让代码块把整个页面撑宽。这是我开发过程中实测踩过的问题代码块默认超过屏幕宽度时页面布局直接崩了加上overflow-x之后才恢复正常。另外还有一个移动端阅读体验的关键点——正文的font-size和line-height必须单独调试。桌面端 16px/1.7 的阅读体验在手机上通常会显得偏小和紧凑适当的行高放大到 1.8 甚至 1.9阅读长文的时候眼睛不容易疲劳。5. 构建优化与性能调优实战5.1 生产构建的优化策略博客平台的构建目标是构建时间可接受、产物体积可控、首屏加载速度够快。围绕这三个目标我在vite.config.js里做了一系列针对性配置。除了前面提到的manualChunks手工分包之外还有两个方向值得实践第一个是消除无用的第三方代码Tree Shaking。确保项目引入第三方库时使用的是 ES Module 格式的入口。Vite 默认对node_modules下的依赖进行预构建生产构建时 Rollup 会做 Tree Shaking但前提是库本身支持 ESM。像marked、highlight.js这些主流库都支持不需要额外配置如果是冷门库可以在引入时留意一下发布包的module字段。第二个是代码分割。Vite 天然支持动态 import 的代码分割也就是路由懒加载。在路由配置里我全部用了() import(/views/...)的写法这意味着每个页面会被单独打成 chunk。用户在访问首页时只加载首页相关的代码文章详情页的代码在真正跳转过去时才会加载。对博客场景来说这种“按需加载”的收益非常明显体现在首屏资源体积上能减少三分之一以上。构建命令很简单npm run build构建完成之后最好预览一下产物npm run preview这个命令会在本地启动一个静态服务器模拟生产环境的部署效果检查路由、资源路径、页面样式是否都正常。5.2 构建时日志里的几个关键词Vite 构建完成后控制台会输出产物文件列表和体积信息。很多人看一眼就关了其实这些日志里藏着不少优化线索。下面是我整理的一个小技巧当某个 chunk 体积明显偏大超过 500KB时说明有比较大的依赖被打进了同一个包里这时候就需要检查是否应该单独分包或者换个更轻量的库。一个典型的场景是highlight.js默认会打包所有支持的语言体积高达几百 KB。解决方案是按需引入语言包import hljs from highlight.js/lib/core import javascript from highlight.js/lib/languages/javascript import typescript from highlight.js/lib/languages/typescript import xml from highlight.js/lib/languages/xml import css from highlight.js/lib/languages/css import bash from highlight.js/lib/languages/bash hljs.registerLanguage(javascript, javascript) hljs.registerLanguage(typescript, typescript) hljs.registerLanguage(xml, xml) hljs.registerLanguage(css, css) hljs.registerLanguage(bash, bash)只注册博客文章里最常用的几门语言体积瞬间降好几个量级。如果某天写了 Rust 或者 Go 的文章再补对应语言的注册即可。这种“按需注册”的思路在依赖大型生态库时几乎永远正确。5.3 构建报错内存溢出与布局异常热搜词里有两条非常典型的问题vite build时node_options不是内部或外部命令以及vue 打包后布局异常。这两个问题我在实践中都遇到过这里展开说一下排查思路。先说vite build --max-old-space-size报 node_options 不是内部或外部命令 这个问题。本质上是命令写错了正确的方式不是直接拼接成 shell 命令而是通过 Node.js 的环境变量来调整 V8 引擎的内存上限# 错误写法Windows CMD 下会报错 vite build --max-old-space-size4096 # 正确写法 set NODE_OPTIONS--max-old-space-size4096 vite build # 或者在 package.json 中配置 build: cross-env NODE_OPTIONS--max-old-space-size4096 vite build推荐用cross-env这个库它可以兼容 Windows 和 macOS/Linux 两种环境避免跨平台时 shell 语法不同的坑。内存溢出通常发生在项目依赖特别多或第三方库体积特别大的情况下调整这个参数属于治标之法治本还得靠分包和按需引入。再说vue 打包后布局异常。这类问题大多出在 CSS 处理上尤其常见于以下几种根源使用了postcss-px-to-viewport或类似插件时转换规则配置不当导致某些样式单位在构建后被错误转换某些字体或异步样式在开发环境加载正常生产环境因为静态资源路径配置错误导致样式丢失分包导致某些组件的 CSS 被延迟加载或者重复加载产生样式覆盖顺序异常。我的排查套路很简单先在本地跑npm run preview复现问题打开浏览器开发者工具看 Elements 面板里元素的实际计算样式和网络面板里的样式文件加载情况。如果是路径问题通常是base配置没设对如果是样式互相覆盖考虑引入 CSS 作用域或者检查样式引入顺序。这个排查思路能覆盖九成以上的“开发正常、打包异常”问题。5.4 部署与持续集成建议博客平台部署的常见方式有两种静态托管和自建服务器。如果选择静态托管平台比如 GitHub Pages 等流程非常简单构建产物dist目录直接上传或者通过 CI 自动部署。需要注意的一点是如果你的站点部署在子路径下比如https://username.github.io/blog/那么vite.config.js里要设置base: /blog/否则所有静态资源都会从根路径请求导致 404 白屏。这个坑我见过太多人踩了而且症状非常隐蔽——HTML 能正常打开但 JS、CSS 全部加载失败。如果选择自建服务器推荐用 Nginx 来做静态文件服务。一个基础的 Nginx 配置大致是这样的server { listen 80; server_name yourdomain.com; root /var/www/tech-blog/dist; index index.html; location / { try_files $uri $uri/ /index.html; } }这里try_files $uri $uri/ /index.html是最关键的一行它确保了 SPA 的路由在刷新时也能正确回退到index.html由前端路由接管后续的页面渲染。如果不写这一行直接访问/post/some-slug这种路径时Nginx 会尝试找对应文件找不到就返回 404这是很多新手部署 SPA 项目时遇到的典型问题。6. 常见问题与避坑指南6.1 开发阶段的高频问题速查做这个项目的过程中我在开发阶段收集到了一些高频问题的排查经验整理成表格方便查阅问题现象常见原因解决方案启动 dev server 端口被占用3000 端口已被其他进程使用修改server.port或加strictPort: trueimport.meta.glob匹配路径为 null文件命名或路径中大小写不一致检查../posts/*.md的模式是否与实际目录结构匹配修改环境变量后无效Vite dev server 缓存了旧的 env修改.env后重启 dev server图片资源路径在打包后 404base配置或图片放到了public外部静态资源放public目录或使用正确的导入方式路由刷新后 404服务器未配置 history fallbackNginx 配置try_files或在静态托管平台设置 SPA fallback第一行补充一个细节Vite 默认端口是5173不是 Vue CLI 常见的8080。如果你同时在跑后端项目很容易撞端口。我在项目里显式设置了port: 3000同时加上了open: true启动后自动打开浏览器省去手动输入的麻烦。6.2 打包体积优化操作实录做完功能之后我专门做了一轮打包体积优化记录一下当时的实测数据。优化前npm run build产出的dist目录总大小约 320KBgzip 后约 110KB主要瓶颈就两个highlight.js全量语言包和 Vue 全家桶打进了一个 chunk。优化动作可以总结为四条将highlight.js改为按需注册语言通过manualChunks把 Vue 相关依赖单独拆包对图片资源做必要的压缩优先使用 WebP 格式开启构建时的代码压缩Vite 默认用 Esbuild 来做 minify。优化完成后总产物体积降到约 240KB其中文章详情页相关的代码占比有了明显下降。对博客平台来说这个体积已经非常理想完全不必要再引入 SSR 或者微前端这类重型方案。6.3 关于 m3u8 播放等扩展功能的边界热搜词里出现过 “vue 播放 m3u8” 这样的搜索意图虽然这不是本项目落地的核心功能但确实属于技术博客常见的扩展场景。如果要在博客平台里嵌入视频模块比如直播回放或者录播课程可以考虑用hls.js来解析和播放m3u8格式的视频流。安装和基础使用如下npm install hls.jsimport Hls from hls.js function setupVideo(videoElement, src) { if (Hls.isSupported()) { const hls new Hls() hls.loadSource(src) hls.attachMedia(videoElement) } else if (videoElement.canPlayType(application/vnd.apple.mpegurl)) { // 部分 Safari 原生支持 m3u8 videoElement.src src } }不过要提醒一点hls.js的体积不小如果不是博客的核心功能建议通过动态 import 按需加载不要直接写进主入口。这也是 Vite 项目里处理按需加载的一个典型示例。6.4 实用开发技巧WebSocket、地图与图表组件的按需集成技术博客平台除了文章展示后期很可能还会有一些“炫技”模块比如实时在线访谈、数据大屏、交互式图表。这些都是 Vue 生态里非常有代表性的需求也经常上热搜我简单说几个实践结论。WebSocketVue 3 里使用 WebSocket 并不需要专门的插件。自己封装一个组合式函数管理连接和消息分发就够了export function useWebSocket(url) { const status ref(connecting) const message ref(null) let ws null function connect() { ws new WebSocket(url) ws.onopen () { status.value connected } ws.onmessage (event) { message.value JSON.parse(event.data) } ws.onclose () { status.value disconnected } } function send(data) { if (ws ws.readyState WebSocket.OPEN) { ws.send(JSON.stringify(data)) } } function close() { ws?.close() } onUnmounted(close) return { status, message, connect, send } }腾讯地图Vue 项目里接入地图最省事的方案是用官方提供的小程序版 SDK 或者 JS API再包一层组件。加载 SDK 时记得使用异步加载避免阻塞首屏渲染。图表如果只用一两个图表类型优先考虑按需引入 ECharts 的模块而不是全量引入import * as echarts from echarts/core import { BarChart, LineChart } from echarts/charts import { GridComponent, TooltipComponent } from echarts/components import { CanvasRenderer } from echarts/renderers echarts.use([BarChart, LineChart, GridComponent, TooltipComponent, CanvasRenderer])这样可以显著减小图表库的打进主包的体积跟前面highlight.js按需注册的思路完全一致。7. 项目后续扩展方向与个人经验总结这个博客平台做到现在基础功能已经非常完整无论是开发体验还是线上性能都令人满意。回顾整个搭建过程我最想强调的其实是“理解工具而不是背命令”。Vite 的命令只有那么几条真正决定项目体验的是你对它工作原理的理解——开发环境的按需编译、生产环境的 Rollup 打包、import.meta.glob的模块加载机制、环境变量的约定式管理这些才是让一个项目保持长期可维护性的底层能力。如果你正准备从零搭建自己的博客我的建议是不要一上来就堆一大堆依赖和花哨的功能。先把文章的加载和渲染跑通再把布局和样式打磨好最后根据实际需求逐步补充标签、推荐、主题切换等能力。这样你的每一项改动都建立在稳定的基础上排查问题也不会陷入“满盘皆输”的困境。最后再分享一个实操细节在开发阶段写文章时可以在npm run dev之外另开一个窗口跑npx vite build --watch这样可以同时获得开发服务器的热更新和构建产物的实时输出排查生产环境独有的问题时特别有用。这个小技巧是我在调样式路径问题时意外发现的实测下来很实用。