
1. 项目概述一个开发者个人门户的诞生最近在整理自己的技术栈和项目履历时总感觉缺一个统一的“门面”。简历是静态的领英是社交化的而GitHub仓库又过于分散。我需要一个地方既能展示我的技术能力、项目作品又能承载我的技术思考甚至还能作为一个实验性的技术沙盒。这就是我决定动手搭建zack-dev-cm/zack-dev-cm.github.io这个项目的初衷。本质上它是一个部署在 GitHub Pages 上的个人开发者网站但它的意义远不止一个静态页面那么简单。对于任何一位开发者无论是刚入行的新手还是希望建立个人品牌的老手拥有一个自托管的、完全可控的个人技术门户都是一个极具价值的投资。它不仅是你的在线名片更是你技术成长的动态档案馆和实验场。这个项目名为zack-dev-cm.github.io遵循了 GitHub Pages 的仓库命名规范{username}.github.io。这意味着一旦你将代码推送到这个仓库GitHub 会自动将其构建并发布为一个可通过https://zack-dev-cm.github.io访问的公开网站。整个过程几乎是零成本的你无需购买服务器无需配置复杂的 Nginx只需要关注内容本身。这个项目适合所有希望建立个人技术品牌的开发者无论你前端技术是强是弱都可以通过现有的静态站点生成器快速上手。接下来我将从设计思路、技术选型、实现细节到部署运维完整拆解我是如何一步步构建这个个人门户的并分享其中踩过的坑和收获的经验。2. 整体架构设计与技术选型背后的思考搭建一个静态网站听起来技术选项很多从纯手写 HTML/CSS/JS到使用 VuePress、Hugo、Gatsby再到直接套用现成的主题。我的核心诉求有几个第一是极简维护我希望写作和更新的体验接近写 Markdown而不是去折腾前端构建第二是高度定制化我不希望我的网站和成千上万个 Jekyll 主题网站长得一样第三是性能与现代化网站应该加载迅速并且能利用一些现代前端特性提升体验第四是良好的 SEO 和可访问性毕竟这是一个对外展示的门户。基于这些考量我最终选择了Vite Vue 3 自研主题的技术栈并采用Markdown 驱动内容的模式。下面详细解释为什么这么选。2.1 为什么放弃主流静态站点生成器SSG像 Hugo、Jekyll、Hexo 这类 SSG 非常成熟生态丰富主题海量。但对于一个开发者门户我遇到了几个痛点主题同质化严重好看的主题大家都用缺乏独特性。修改主题通常需要深入其模板语言如 Liquid、Go Template学习成本不低。开发体验割裂写作时是 Markdown但想添加一个复杂的交互组件时就需要在主题的框架下“打补丁”不够灵活。构建流程黑盒虽然它们很快但当你想深度优化构建输出如资源哈希、代码分割策略时往往需要翻阅晦涩的文档或插件。Vite Vue 3 的组合实际上是将一个现代前端 SPA 应用的开发体验用于构建静态站点。Vite 提供闪电般的开发服务器和基于 Rollup 的优化构建Vue 3 的组件化开发模式让网站的每个部分导航栏、项目卡片、博客文章都成为可复用、易维护的模块。更重要的是通过vite-plugin-md或mdit-vue/plugin这样的插件可以完美地将 Markdown 文件转换为 Vue 组件实现“在 Markdown 中使用 Vue 组件”这给了我极大的内容创作自由度。2.2 核心架构内容与表现分离我的项目结构遵循了清晰的分层原则zack-dev-cm.github.io/ ├── src/ │ ├── assets/ # 静态资源图片、字体 │ ├── components/ # 可复用的 Vue 组件 │ ├── layouts/ # 页面布局组件 │ ├── pages/ # 由路由对应的页面组件 │ ├── content/ # **核心所有的 Markdown 内容文件** │ │ ├── blog/ # 博客文章 │ │ ├── projects/ # 项目介绍 │ │ └── about.md # 关于我 │ ├── styles/ # 全局样式 │ └── main.js # 应用入口 ├── public/ # 无需构建的静态文件 ├── vite.config.js # Vite 配置 └── package.jsoncontent/目录是网站的内容核心。所有文章、项目介绍都用 Markdown 书写。构建时一个自定义的脚本或使用vite-plugin-pages的动态路由会读取这个目录为每个.md文件生成对应的路由和页面数据。这样我更新网站内容只需要在content/下新增或修改 Markdown 文件即可完全不需要触碰 Vue 组件代码。2.3 关键工具链选型解析Vite: 选择它而非 Vue CLI主要是看中其极致的开发体验和更快的构建速度。对于静态站点生产构建的优化如资源压缩、代码分割Vite 也做得非常好。Vue 3 script setup: 使用 Composition API 和script setup语法糖让组件逻辑更清晰、代码更简洁。这对于维护一个逐渐增长的功能集合至关重要。Pinia: 用于管理少量的全局状态比如主题模式亮色/暗色、移动端菜单的展开状态等。虽然对于纯静态内容站点状态管理需求不大但 Pinia 的轻量和 TypeScript 友好性让它成为不二之选。UnoCSS: 我选择了原子化 CSS 框架 UnoCSS而不是 Tailwind CSS。主要原因在于 UnoCSS 的按需生成特性极其强大它只生成你真正在代码中用到的工具类最终的 CSS 文件体积可以做到非常小。这对于追求首屏性能的静态站点来说是一个巨大的优势。VitePress 的权衡: 有人会问为什么不直接用 VitePressVitePress 是 Vue 官方的静态站点生成器非常优秀。但我认为它更偏向于文档站其主题定制虽然比 VuePress 灵活但依然有一套预设的约定。我希望我的网站有更独特的视觉设计和交互逻辑因此选择了从更底层开始搭建这牺牲了一些开箱即用的便利换来了百分百的控制权。注意技术选型没有绝对的对错只有是否适合。如果你的首要目标是快速搭建一个博客那么 Hexo 或 Hugo 搭配一个心仪的主题可能是更优解。但如果你希望将这个网站作为一个长期的技术产品来迭代并深度介入其技术实现那么从 Vite Vue 开始会给你带来更大的灵活性和学习价值。3. 核心功能模块的细节实现与难点攻克一个开发者门户通常包含几个标准模块首页展示、博客系统、项目作品集、关于页面。下面我拆解其中两个最具技术代表性的模块博客系统和动态项目展示墙的实现。3.1 基于文件系统的博客系统实现我不打算引入后端数据库因此博客数据全部来自文件系统。我在src/content/blog/目录下存放所有文章例如my-first-post.md。每篇文章的 Markdown 文件顶部有一个 Front Matter 区域用于定义元数据。--- title: “深入理解 Vue 3 响应式原理” date: 2023-10-27 tags: [“Vue”, “前端”, “原理”] excerpt: 本文将从 Reactive/Ref 源码入手剖析 Vue 3 响应式系统的核心实现。 ---构建时数据处理在vite.config.js中我配置了vite-plugin-md将.md文件转换为 Vue 组件。同时我编写了一个 Node.js 脚本例如scripts/generate-blog-data.js在开发服务器启动前或构建前执行。这个脚本会使用fs模块读取src/content/blog/下的所有.md文件。用gray-matter库解析每个文件的 Front Matter。将解析出的元数据标题、日期、标签等和文件路径汇总生成一个blog-list.json文件存放在public/或作为虚拟模块注入。这个 JSON 文件就是博客列表页的数据源。博客列表页在src/pages/blog.vue组件中直接导入这个blog-list.json遍历生成文章卡片列表。卡片上显示标题、日期、标签和摘要。点击卡片跳转到对应的文章详情页。文章详情页与路由我使用vite-plugin-pages插件它支持基于文件系统的路由。我将src/content/blog/目录配置为路由源那么my-first-post.md就会自动映射为路由/blog/my-first-post。详情页组件会接收到当前路由参数从而知道要渲染哪篇文章。插件会自动将 Markdown 内容转换为一个 Vue 组件我只需要在页面中放置一个Content组件占位符即可。难点与解决方案代码高亮在 Markdown 中写代码块很常见。我使用了prismjs库并在构建后处理阶段通过 UnoCSS 的transformer功能自动为代码块添加 Prism 的 CSS 类名并引入一个主题 CSS 文件实现了服务端渲染友好的语法高亮无需客户端 JS 激活。图片处理Markdown 中的图片路径需要正确处理。我配置了 Vite将src/content/下的图片资源也作为构建依赖。在 Markdown 中我使用相对路径引用图片Vite 会处理这些资源并进行压缩和哈希。标签过滤与搜索博客列表页需要支持按标签筛选。这完全在客户端完成。我利用 Vue 的响应式特性将blog-list.json数据加载到 Pinia store 或组件状态中然后使用计算属性根据选中的标签动态过滤列表。对于简单的全文搜索可以使用lunr.js或flexsearch在客户端建立索引但这会增加包体积。我目前采用了标签过滤为主未来如果文章量巨大再考虑引入轻量级搜索。3.2 动态项目展示墙的设计项目展示墙 (/projects) 不仅仅是静态图片和文字的罗列。我希望它能动态展示一些“活”的数据比如 GitHub 仓库的星标数、最后更新时间甚至是最新提交信息。这能让访客直观地感受到项目的活跃度。实现方案数据源分离与博客类似每个项目有一个 Markdown 文件 (src/content/projects/)描述项目背景、技术栈、我的角色等。同时我会在 Front Matter 中定义一个repo字段填写 GitHub 仓库地址如“zack-dev-cm/awesome-tool“。构建时获取动态数据这是关键一步。我不能在用户浏览器里直接调用 GitHub API因为有速率限制和暴露令牌的风险。我的做法是在构建阶段获取这些数据。在generate-blog-data.js的脚本中我扩展了它的功能。在读取项目 Markdown 文件后对于有repo字段的项目脚本会使用octokit/rest.js(GitHub官方 SDK) 调用 GitHub API获取该仓库的stargazers_count、pushed_at等信息。为了不触发 API 限流脚本中会加入简单的延时并且可以将获取到的数据缓存到本地一个 JSON 文件中例如project-stats.json下次构建时如果缓存未过期比如设置24小时就直接读取缓存避免重复请求。静态化注入获取到的动态数据会和项目的 Markdown 元数据合并一起写入到最终供前端使用的project-list.json中。这样在构建生成的静态网站里项目卡片上显示的星标数已经是“冻结”在构建时刻的数据了。虽然它不是实时的但对于个人项目展示来说每天或每次部署更新一次完全足够并且保证了网站的纯静态特性无需任何运行时后端。前端交互项目卡片本身是一个 Vue 组件。它接收合并后的项目数据作为 prop。卡片设计上除了基础信息会突出显示星标数和“最近更新”标签。点击卡片可以跳转到项目详情页Markdown渲染或者外部链接如 GitHub 仓库、在线演示。实操心得利用“构建时数据获取”这一模式是增强静态站点动态性的利器。除了 GitHub 星标你还可以在构建时获取 RSS 订阅最新内容、获取天气 API 数据如果你做的是一个仪表盘等等。核心思想是将动态数据获取从浏览器运行时挪到构建时生成包含最新数据的静态页面。这既保持了静态站点的速度与安全优势又提供了动态内容的感觉。工具上除了自己写脚本也可以考虑使用专门为 SSG 设计的工具如axios配合 Node 环境或者更集成的方案如Nuxt.js的asyncData功能。4. 样式、性能优化与自动化部署4.1 基于 UnoCSS 的原子化样式实践我放弃了传统的编写.css文件或style scoped的方式全面拥抱 UnoCSS。在vite.config.js中引入 UnoCSS 插件并创建一个uno.config.ts配置文件。优势体验极致的自由与速度在 Vue 模板中我可以直接写div class“flex items-center justify-between p-4”UnoCSS 会在开发时即时生成对应的 CSS体验流畅。它支持所有主流的原子化 CSS 工具类语法学习成本几乎为零。超小的生产体积这是最让我惊喜的一点。由于 UnoCSS 只扫描你的源码中实际用到的类名并只生成这些 CSS 规则我的整个网站最终的 CSS 文件体积被压缩到了10KB 以下。这对于首屏加载性能是质的提升。自定义设计系统我可以在uno.config.ts中轻松定义自己的主题色、间距尺度、断点。例如// uno.config.ts import { defineConfig } from ‘unocss‘ export default defineConfig({ theme: { colors: { primary: ‘#3b82f6‘, // 自定义主色 ‘bg-canvas‘: ‘var(--color-bg-canvas)‘, // 支持 CSS 变量 }, breakpoints: { ‘xs‘: ‘320px‘, ‘sm‘: ‘640px‘, // ... } } })然后我就可以在代码中使用bg-primary、text-primary等类实现了设计的一致性。注意事项原子化 CSS 需要改变你的样式编写思维。它不适合处理极其复杂、独一无二的动画或图形效果。对于这类需求我仍然会使用style scoped编写一小段传统的 CSS。两者可以完美共存。4.2 性能优化关键点静态站点的性能优化主要围绕“加载速度”和“交互流畅度”。资源压缩与哈希Vite 生产构建默认会对 JS、CSS 进行压缩 (terser)并对资源文件名添加哈希利于长期缓存。确保build.rollupOptions.output.assetFileNames等配置合理。图片优化我使用了vite-plugin-imagemin插件在构建时自动对public/和构建产物中的 PNG、JPG、SVG 图片进行无损或有损压缩通常能减少 30%-70% 的图片体积。对于背景图等优先使用 WebP 格式可以通过插件自动生成。代码分割与懒加载Vite 默认支持基于动态import()的代码分割。我将博客详情页、项目详情页这些非首屏必需的组件配置为异步懒加载。这样用户访问首页时不会加载这些详情页的代码。预渲染与预加载由于是 SPA我使用vue/router的router.isReady()来确保路由解析完成后再挂载应用避免闪屏。对于重要的首屏图片可以使用link rel“preload”进行预加载。字体优化我使用了 Google Fonts 的字体。为了减少渲染阻塞我通过preconnect提前建立连接并使用displayswap参数让字体异步加载避免文字不可见时间过长。4.3 自动化部署流水线使用 GitHub Pages 部署最优雅的方式是利用GitHub Actions。我在项目根目录创建了.github/workflows/deploy.yml文件。name: Deploy to GitHub Pages on: push: branches: [ main ] # 只在 main 分支推送时触发 workflow_dispatch: # 允许手动触发 jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkoutv3 - name: Setup Node.js uses: actions/setup-nodev3 with: node-version: ‘18‘ cache: ‘npm‘ - name: Install Dependencies run: npm ci # 使用 ci 命令确保依赖锁一致 - name: Build run: npm run build env: # 如果有需要可以在这里注入构建环境变量 VITE_APP_TITLE: ‘My Dev Site‘ - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pagesv3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./dist # Vite 默认的输出目录这个工作流实现了完全自动化的 CI/CD。每当我向main分支推送代码比如写了一篇新博客GitHub Actions 就会自动在一个干净的 Ubuntu 环境中拉取代码、安装依赖、执行构建然后将生成的dist目录推送到仓库的gh-pages分支。GitHub Pages 服务会自动从gh-pages分支更新网站内容。整个过程无需我手动干预真正实现了“写作即发布”。5. 开发中遇到的典型问题与解决方案在实际搭建过程中我遇到了不少坑。这里记录几个有代表性的问题及其解决方法希望能帮你绕开它们。5.1 问题一Markdown 中的 Vue 组件不生效现象在博客 Markdown 里我写了一个自定义的Warning组件但构建后只显示成了原始的 HTML 标签文本没有被渲染。排查首先检查vite-plugin-md的配置。我需要在插件配置中明确声明哪些 Vue 组件可以在 Markdown 中使用。解决方案在vite.config.js中import { createMarkdownPlugin } from ‘vite-plugin-md‘ import Warning from ‘./src/components/Warning.vue‘ export default defineConfig({ plugins: [ createMarkdownPlugin({ markdownItOptions: { html: true, linkify: true, typographer: true }, // 关键在这里注册全局组件 markdownItSetup(md) { md.use(/* ... */) }, // 或者在 Vue 组件层面全局注册 }), vue({ include: [/\.vue$/, /\.md$/], // 必须包含 .md 文件 }) ], })更可靠的做法是在用于渲染 Markdown 的布局组件中使用 Vue 的component动态组件功能或者确保你的自定义组件在应用级别被全局注册。5.2 问题二部署后页面路由刷新 404现象在本地开发环境一切正常但部署到 GitHub Pages 后从首页点击导航到/blog可以正常跳转但如果直接在浏览器地址栏输入https://zack-dev-cm.github.io/blog或刷新该页面就会返回 404。原因GitHub Pages 是一个静态文件服务器。当你访问/blog时服务器会去寻找blog.html文件。但在 SPA 中/blog是一个由前端路由管理的虚拟路径对应的文件其实是index.html。服务器找不到blog.html自然返回 404。解决方案需要为 GitHub Pages 配置一个404.html页面并将其重定向到index.html由前端路由接管。最简单的方法是在构建脚本中复制一份index.html并重命名为404.html。在 Vite 中可以通过修改build.rollupOptions来实现// vite.config.js export default defineConfig({ build: { rollupOptions: { input: { main: resolve(__dirname, ‘index.html‘), 404: resolve(__dirname, ‘404.html‘), // 生成 404.html }, }, }, })同时确保你的index.html和404.html内容一致。这样当服务器返回 404 时实际上会提供404.html文件也就是你的 SPA 入口前端路由就能正确匹配并显示/blog页面了。5.3 问题三构建时获取 GitHub API 数据失败或超慢现象构建脚本在获取项目星标数时偶尔失败或者因为请求太多导致构建时间很长。排查GitHub API 有严格的速率限制未认证每小时 60 次认证后每小时 5000 次。如果项目多或者频繁重新构建很容易触发限制。解决方案认证在 GitHub Actions 工作流或本地构建脚本中使用 Personal Access Token (PAT) 进行认证。将 Token 存储在 GitHub Repository Secrets 中通过环境变量传递给脚本。缓存这是最重要的优化。在构建脚本中实现一个简单的文件缓存逻辑。每次请求 API 后将结果以{repo: ‘owner/name‘, data: {...}, timestamp: Date.now()}的格式存储到一个本地 JSON 文件如.cache/github-stats.json中。下次构建时先检查缓存文件是否存在以及对应数据的时间戳是否在有效期内例如 24 小时内。如果是则直接使用缓存数据跳过 API 请求。延迟在循环请求多个仓库信息的代码中在每次请求之间加入setTimeout或await new Promise(resolve setTimeout(resolve, delay))添加一个几百毫秒的延迟避免短时间内发出大量请求。5.4 问题四暗色/亮色主题切换的闪屏问题现象实现了基于 CSS 变量和 UnoCSS 的主题切换但页面加载时在 JS 执行前会短暂显示默认的亮色主题然后才切换到用户保存的暗色偏好出现“闪屏”。解决方案关键在于将主题的判断和应用于尽可能早的阶段最好是在 HTML 解析时。我采用了以下策略内联关键脚本在index.html的head中放置一小段内联的 JavaScript。这段脚本在任何外部 CSS 或 JS 加载之前执行。读取本地存储这段内联脚本读取localStorage中保存的主题偏好例如theme: ‘dark‘。设置 HTML 类名根据读取到的偏好直接给html或body标签添加class“dark”。CSS 变量依赖类名我的所有主题 CSS 变量都定义在.dark类名下。例如:root { --color-bg: white; } .dark { --color-bg: #1a1a1a; } body { background-color: var(--color-bg); }这样在浏览器开始渲染页面内容时正确的主题类名已经存在从而完全避免了闪屏。之后Vue 应用启动再接管主题切换的交互逻辑如按钮点击并负责将用户的新选择写回localStorage。搭建zack-dev-cm.github.io的过程是一个将想法逐步打磨成产品的过程。它不仅仅是一个网站更是一个持续演进的个人项目。从技术选型的权衡到一个个功能模块的实现再到性能调优和自动化部署每一步都加深了我对现代前端开发生态的理解。最重要的是它完全属于我我可以随时添加新的实验性功能比如用 Three.js 做个3D背景或者集成一个小的代码编辑器而不受任何平台限制。如果你也想打造自己的技术名片不妨就从 fork 一个模板或者像我从零开始动手做起来。