惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Hacker News: Ask HN
Hacker News: Ask HN
H
Heimdal Security Blog
S
Secure Thoughts
Help Net Security
Help Net Security
The Hacker News
The Hacker News
T
Threatpost
T
Troy Hunt's Blog
T
Threat Research - Cisco Blogs
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Simon Willison's Weblog
Simon Willison's Weblog
WordPress大学
WordPress大学
TaoSecurity Blog
TaoSecurity Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Cisco Talos Blog
Cisco Talos Blog
Microsoft Security Blog
Microsoft Security Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
阮一峰的网络日志
阮一峰的网络日志
Security Latest
Security Latest
Forbes - Security
Forbes - Security
The Last Watchdog
The Last Watchdog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
月光博客
月光博客
博客园 - 三生石上(FineUI控件)
A
About on SuperTechFans
C
Cybersecurity and Infrastructure Security Agency CISA
小众软件
小众软件
Microsoft Azure Blog
Microsoft Azure Blog
Google Online Security Blog
Google Online Security Blog
N
Netflix TechBlog - Medium
W
WeLiveSecurity
Google DeepMind News
Google DeepMind News
S
Securelist
Jina AI
Jina AI
酷 壳 – CoolShell
酷 壳 – CoolShell
G
GRAHAM CLULEY
G
Google Developers Blog
aimingoo的专栏
aimingoo的专栏
C
Cisco Blogs
量子位
有赞技术团队
有赞技术团队
雷峰网
雷峰网
Google DeepMind News
Google DeepMind News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
S
SegmentFault 最新的问题
T
The Exploit Database - CXSecurity.com
MyScale Blog
MyScale Blog
A
Arctic Wolf
T
Tenable Blog
P
Privacy & Cybersecurity Law Blog

Grtsinry43 的前端札记

我是怎样使用 AI Agent 写代码的 从头开始的勇气 列车上的生活 分享一点体测的小事 「手搓系列 02」深入浅出 Agent Skills:架构、实现与思考 路的尽头还是路 二十一岁,然后...... 我患上了 token 的瘾 Xiaomi 17 标准版刷机折腾记:解锁、官改ROM与必备模块 思考 在焦虑与代码中缓慢前行 从 v1 到 v2,谈谈这个简单博客背后的架构演进与实现 Go 语言初体验:Less is more,一种丑但可靠的工程美学 职规赛,一次特别的体验 2025 年终总结——从晨光到雾散,化经历为成长 2025 实习札记:关于一场未完成的迁徙与落地 新时代的 PHP:RSC 的边界错位与工程代价 从想法到实践:在无序的生活里,试图用代码敲出一点秩序 PureFlow 简析:用 Kotlin 跨平台构建 RSS 阅读器 命运开了个无情的玩笑 手把手带你玩转 Monorepo,拥抱现代前端开发新范式 好久不见,这是一份来自作者的独白
「手搓系列 01」 从零搭建 Vue 文档站,学习从静态生成到语法解析
grtinry43 · 2025-09-03 · via Grtsinry43 的前端札记

最近在实习,好久不更新了,休假回校之前,准备开一个新的合集 「手搓系列」,我们从头实现一些习以为常的前端轮子/技术栈/工具链,用手写的方式学习它的思路,同时得到些新的思考。

手搓的故事就从比较知名的文档网站 VitePress 开始吧,之后预计还有打包工具,ui 库,以及前端基建,干货多多,可以考虑长期追更(doge),自己写一遍既可以给大家讲原理,自己也能学习,两全其美,每一个部分都由“分析效果——规划实现——MVP 调通——难点解决——项目成品”组成

让我们开始吧

[!NOTE]

受限于精力,业余时间实在不多,这个 demo 项目的样式,交互还在设计,完成之后会开源

分析效果

VitePress 文档站,一个知名的静态文档站点,被无数项目和开发者所采用,简洁精美,配置简单,深受开发者喜爱。

分析它的效果,首先是 yaml 驱动的元信息,md 中直接书写 Vue 组件,智能生成 siderbar 和 toc,主页内容的自定义,markdown 扩展语法,代码着色高亮等等

规划实现

Vite 插件

我们将依托于 Vite 的强大能力,一步步手动实现这些特性

在 Vite 的构建中,我们可以编写插件来自定义 vite 处理文件的操作,严格来说,对于 vite 来说,它既不认识文件的扩展名,也看不懂里面的内容,之所以能够解析 vue/react 等等库,也就是因为插件的存在 Vite 插件(也就是 rollup)的类型定义是这样的

其中对我们来说最常用的就是 transform,id 就是文件名,code 就是代码内容,也就是相当于拿到每个文件,你告诉这个文件要怎么处理,处理完还给它就行 于是我们之前说到的特性,markdown 解析可以这里拿,元数据可以这里拿,甚至组件的渲染都可以这里自定义

当然别的特性也有大用,我们慢慢讲解

静态网站生成(SSG)

这名词听起来这么高大上,其实很好理解。

你应该了解过,也用过/实现过 SSR,也就是服务端渲染,这种方式利用 node runtime,在 node 中创建基础应用然后为客户端分发部分渲染好的字符串,由客户端加载 js 斌完成“水合”,进而提升了性能和 SEO 体验

但是这个时候又有新的问题出现了,SSR 对服务端的性能要求很高,或者是像一些静态的内容很少更新,无需或很少交互的内容/serverless,那么我们就让 node 渲染一次然后直接存起来不就好了,诶,于是你发明了 SSG,通过构建时候的一次渲染+客户端脚本水合,这使得你的应用极为轻量,因为你可以在构建阶段完成大部分的页面渲染。

技术选择

选择好了大概方向,那我们来决定用什么来写。

我们采用 Vite+Vue+Markdown-it+shiki+tailwind 手搓这个站点

其中如果是 react 的话其实绝大部分都采用 mdx,一步到位没有乐趣了

而 vue+markdown-it 刚好能发挥 vue sfc 的灵活性,以及自己掌控完全解析的高可玩性

shiki 没啥好说的,好用就是了,当然也可以 highlightjs。

从 MVP 开始

让我们从构建最小可行性产品开始

  1. 项目初始化:首先,我们用 Vite 创建一个基本的 Vue 项目作为我们的骨架。
  2. Markdown 解析:然后,我们要让 Vite 能“看懂” .md 文件,并把它转换成 Vue 组件。
  3. 路由系统:接着,我们会建立一个简单的文件路由,让不同的 URL 能访问到对应的 Markdown 页面。
  4. 静态站点生成 (SSG):最后,我们会编写一个构建脚本,把所有页面打包成最终的静态 HTML 文件。

先建项目,没啥可说的,Vue3+ts 模板,然后装好依赖

第一个插件

首先我们要让 Vite 认识 .md,让我们在 vite.config.js 写下第一个插件

是的,如你所见,就这么简单,使用 md.render 渲染成 html,组装成 sfc 之后直接给到下一步的 vue compiler 就可以了。

现在,我们直接创建一个 Markdown 文件,并在我们的 Vue 应用里使用它。

  1. src 文件夹下创建一个名为 hello.md 的文件,内容如下:

    Markdown

  2. 修改 src/App.vue 文件,清空原有内容,然后引入并使用这个 Markdown 文件:

    Code snippet

诶,这样就渲染出来了,原汁原味的 html

注:这里引入 ide 会报错,建一个 d.ts 就行

内容目录与路由系统

为了方便书写内容,我们创建 content/ 文件夹,现在开始构建路由

我们希望程序能自动扫描 content/ 目录下的所有 .md 文件,并为它们生成对应的页面路由。例如,content/hello.md 应该能通过 /hello 这个网址访问到。

这个过程可以分为两大部分:

  1. 扫描文件 (Node.js):我们需要在 Vite 的配置文件 (vite.config.ts) 里编写一段 Node.js 代码,用来读取 content/ 目录下的所有文件,并生成一个路由配置列表。
  2. 使用路由 (浏览器端):我们需要在 Vue 应用 (src/ 目录) 中安装和配置 vue-router,让它使用我们上一步生成的路由列表来展示不同的页面。

先装 router

好玩的来了,构建工具在构建中可以生成一些“虚拟模块 (Virtual Module)”。

听起来很高级,但原理很简单:我们将编写一个 Vite 插件,这个插件会创建一个 只存在于内存中 的“虚拟文件”。我们的 Vue 应用可以像导入普通文件一样导入这个虚拟文件,从而获取到我们动态生成的路由列表。

那就简单了,无外乎两步:

  1. 在 Vite 插件中:扫描 content/ 目录,生成路由配置,并通过一个特殊的虚拟 ID (咱们这利用 virtual:routes) 来提供这些配置。
  2. 在 Vue 应用中:导入 virtual:routes,并用它来初始化 vue-router

我们需要一个新的 Vite 插件。这次,之前说的其他钩子就有用了:resolveIdload

  • resolveId 钩子:当 Vite 看到 import ... from 'virtual:routes' 这样的语句时,它会问:“这个 ‘virtual: routes’ 到底是什么东西?” 这个钩子就是用来回答这个问题的。我们会告诉 Vite:“是的,我认识这个 ID,你交给我来处理就行。”
  • load 钩子:在 resolveId 确认了 ID 之后,Vite 就会调用 load 钩子,问:“好了,那这个模块的内容是什么?” 在这里,我们就会动态地生成代码并返回。

利用这个功能,让我们创建新的插件:

同样为了 ts 更好推断,还要添加 d.ts

于是就可以直接引入

创建路由 use router-view 不再赘述,前端开发写过无数次了,于是你就有了简单路由。

但是这还不够呀,文档一定会有多级路由存在的,那也简单,递归处理一下就好了呗

递归很简单,基本功了,直接上代码

到这里,你的应用便有了雏形,可以在不同页面之间导航了。

从 SSR 到 SSG

相信熟悉前端的你一定写过 SSR 的手动实现,思路就是维护两个 entry,一个 server,一个 client,使用打包工具分别处理,收到请求首先内存 router 切换,状态库注入,然后渲染发给客户端,

这里还是实现一次吧,毕竟懂了 SSR 就简单了。

首先让 router 在服务端内存维护,客户端历史记录维护。

随后主应用使用这个额算是工厂函数

好,接下来是服务端入口,我们新建一个 entry-ssr.ts

Vite 的强大又来了,它提供一个 createServer 可以方便创建服务端环境,而 ssrLoadModule 就是为加载 ssr 入口而生,优化常用使用场景确实方便于 webpack 维护三套配置。

用我们之前扫描到的所有路由,依次完成渲染

正如我们之前的思路,我们把这些存起来就行了,存哪里呢,诶,存在客户端的打包刚好

为了渲染好的内容能够精准插入,我们不妨给它标记下,编辑根 index.html

我们添加了个注释,这样便于 SSR 拿到字符串之后替换 思路有了,相信对大家来说超级简单,直接最后的代码~

到这里,你的网站成功实现静态生成!(撒花)

在这里之后,你可以比如用 gray-matter 解析元数据,创建 meta 组件,这些思路差不多

难点解决

难题到这里开始了。

shiki 代码着色

核心挑战:同步 vs. 异步

在集成 shiki 时,我们会遇到一个非常经典且重要的问题:markdown-it 的渲染过程是 同步 的,但 shiki 的高亮过程是 异步 的(因为它需要异步加载不同语言的语法文件)。

要解决这个矛盾,我们只需要顶层 await 来初始化 shiki,然后把它作为高亮引擎提供给 markdown-it。其实也就是等他加载完再继续

现在我们只需要用这里的 md 对象替换 vite 插件里面的就好啦,很轻松,并且由于只有构建时引入一次,也不会影响客户端代码大小。

自定义组件解析

还记得之前我们说过 Vue 在这里的优势是 sfc 嘛,我们不妨将整篇文章作为 sfc 组件,利用 @vue/compiler-sfc 一把梭哈

当然也不是那么暴力,整体的思路是

原始文档 → 正则替换组件 → Markdown 渲染 → SFC 组件 → 最终渲染

我们就从简单的 <Alert/> 入手,首先是写好正则:

于是我们可以直接在 transform 钩子中替换与渲染:

我直接在注释中讲解吧

到这里,「手搓系列 01」就告一段落了。我们一起从零构建了一个完整的静态文档站,从最基础的 Vite 插件,到核心的 SSG 渲染,再到代码高亮和自定义组件解析。这个过程不仅是为了得到一个能用的轮子,更是为了深入理解这些习以为常的技术栈背后,每个环节是如何协同工作的。

最终的项目我还在慢慢开发,涉及到每个组件写一遍,还有 UI 和功能,精力真的不够,等到做完第一时间更新和开源。


手写一遍,才能真正掌握它的精髓。

如果你对这个系列感兴趣,可以长期关注 RSS。在接下来的文章里,我们还会继续探索更多有趣的前端轮子,比如打包工具、UI 库和前端基建等。让我们动手继续敲下去