
抛弃React和Vue,让前端工程返璞归真
本文探讨了前端开发的复杂性,提倡回归以后端为中心的架构,强调使用简单的工具和技术,如模板引擎、jQuery、alpine.js和htmx,来简化开发流程。作者认为,现代前端工具链的复杂性逐渐偏离了简化开发的初衷,建议在不引入过多复杂性的情况下,利用Web Components等标准技术来实现可复用组件,从而提高开发效率。
作者分享了个人博客框架迭代历程:从Hexo迁移到Astro的尝试,到发现Elog平台带来的启发,最终思考将Notion作为内容管理系统的新方案。文章重点描述了作者对博客系统的核心需求——解决创作同步问题,以及在探索NotionNext等解决方案过程中遇到的技术限制和思考。
还记得上次发布博客已经是24年3月1号了,写的是我在上一段实习结束时最后遗留的议题,然后就再也没有更新过。
其实,我从23年年底开始就对我已有的基于Hexo的博客方案不满已久,包括麻烦的导出、同步过程,时不时还会遇到语法不兼容的问题,Hexo的自定义性也不是很好,于是开始筹划要构建一个自己的新博客平台,同时做一些新功能,如个人主页、AI助手、项目集等。
24年初,我正好了解到了一个正在名声鹊起的前端新框架——Astro,其新颖的理念(无JS、岛屿模型)和极致的性能一下子就吸引了我,我开始在这个框架上捣鼓,基于OpenBlog建立了我的第一个Astro博客。
但是很快,我找到了一份比较有意思的兼职工作,时间马上就不够用了,规划中的AI知识库助手、炫酷个人主页等功能很快就搁浅,只留下一个非常粗糙的页面,现在还可以在 refactor_astro 分支上看到当时的代码。
现在回想起来,当时的Astro相比其他方案除了SSG的理念创新,其实在应用形态或者开发体验上并没有所谓绝对优于其他框架的「杀手锏」功能,包括客户端不运行JS的理念也很快被NextJS的RSC抢了过去。
但是,在这趟Astro之旅上,我也学习了很多新东西,包括ViewTransition API、Lighthouse性能评分、岛屿架构等。
这次旅途就像一次绕路,最后我又回到了Hexo的舒适圈内,毕竟他省心又好看,只是同步比较麻烦,这也导致我大半年虽然没有辍笔,但是也没有发布。
2024年中,我遇见了elog,这是一个Adapter平台,支持各种各样的数据源(如Notion、飞书等),利用这些应用的API导出为md文件,再将这些md用于Hexo这样的博客平台,很好地解决了创作「同步麻烦」的痛点。
这个项目打开了我的思路——Notion既然是我的创作中心,那么我就直接将Notion作为我的CMS(Content Management System)进行文章发布,并进一步将Notion打造为我的知识库中心,将收集、处理、创作和输出的所有信息都统一放在Notion中管理,而Notion强大的数据库功能和开放的API生态又给这一设想提供了可能性。
寻着这个思路,我继续往下找,找到了一个Star数8k+的优秀开源项目——NotionNext,这是一个直接利用Notion API读取Block数据,渲染成Notion页面的项目,比起elog先转换成markdown作为中间层,这种「没有中间商赚差价」的模式可以避免元数据的丢失,更好地保留源格式信息,避免排版错误,理论上可以做到完美复刻Notion的所有Block渲染。
可是我在尝试接入后,发现了新问题:
因此,接下来半年的时间我就断断续续地开始了下面的工作,但是因为本职工作繁忙,并没有那么多的时间来做完:
本来一切都很顺利,我的重构工作也基本实现了基础的博客渲染功能,具体代码都在下面的仓库,有志者可以继承我的衣钵接着开发。
好巧不巧,Astro此时推出了重量级更新,将我一把拽回了Astro的怀抱。
正值年底,我的NotionNext重构工作正在如火如荼地进行着,奈何平地一声惊雷,Astro 5.0横空出世,带来了如Server Islands(服务器岛屿)和Content Layer(内容层)API两个重量级功能。
Astro不忘初心,秉承其内容优先应用的框架理念,不断在易用性和性能上推陈出新,这次更是重量级:
我一拍屁股,想到这不对了吗,完全契合了我们的使用场景,我就是把Notion当作CMS来用的。因此说干就干,立刻开始尝试着手尝试迁移。
根据我一年多的调研,要使用Notion作为博客CMS,可以按中间层格式、转换工具、渲染层三个维度来划分渲染方案。
中间层格式 | 转换工具 | 渲染层 | 代表方案 |
Markdown | https://github.com/souvikinator/notion-to-md | https://astro.build/ | |
Markdown | https://github.com/LetTTGACO/elog | https://hexo.io/zh-cn/ | |
Notion Blocks | https://github.com/NotionX/react-notion-x | https://nextjs.org/ | https://github.com/tangly1024/NotionNext |
Notion Blocks | 自包含 | https://astro.build/ |
经过一番调研,我发现上面的开源方案虽然都很优秀,但我用起来或多或少都有些不太舒服,有这样那样的限制:
在Content Layer API推出的第一时间,Notion作为主流的数据源,就被社区添加了相关的Loader支持。
解析其实现如下:
children
块和处理图像资源总的来看,是一个非常简单且实用的设计,没有做多余的事情,还提供了一个线上博客例子。
只可惜,这个示例中只有标题含有图片,正文中并没有图片,这埋下了一个巨大的坑,足足占据了我大半个月的时间进行修复,这里先按下不表,等到后文再谈。
在经过了一段时间的技术调研后,我最终确定了我的技术选型,并于2025年初正式启动了开发:
于是,我兴致勃勃地创建了我的仓库:
本文只做简单的介绍,具体的技术细节可以参考我的另一篇文章 Astro Content Layer 处理图片的技术细节
我兴致勃勃地创建项目,创建内容集合,做简单的适配接入,一个Demo很快就运行了起来,我怀揣着激动的心,颤抖的手,在心里疯狂对Astro易用的开发体验竖起大拇指。
然而,好景不长,仅仅调试了一段时间,我就遇到了下面的错误:
我尝试点开一个链接
然而,刚解决了上面的问题,我写完了第一版的代码,准备将初版demo部署上网时,天又塌了,运行构建时刷刷的红色错误接踵而至:
我按照错误指示检查配置文件,却发现我早已将aws域名添加到了白名单里,那这是为何呢?我尝试点到报错的路径里,打印出了完整的Trace信息。
Error: There was an error loading the configured image service. Please see the stack trace for more information.
InvalidImageService: There was an error loading the configured image service. Please see the stack trace for more information.
at eval (/Users/bytedance/Code/personal/notion-astro-rev/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected][email protected][email protected][email protected]/node_modules/astro/dist/assets/internal.js:25:21)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async getConfiguredImageService (/Users/bytedance/Code/personal/notion-astro-rev/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected][email protected][email protected][email protected]/node_modules/astro/dist/assets/internal.js:21:34)
at async getImage (/Users/bytedance/Code/personal/notion-astro-rev/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected][email protected][email protected][email protected]/node_modules/astro/dist/assets/internal.js:55:19)
at async getImage (astro:assets:32:42)
at async #fetchImage (/Users/bytedance/Code/personal/notion-astro-rev/node_modules/.pnpm/[email protected][email protected]_@[email protected][email protected][email protected]_roll_2ibjhaoyvb5densitsoeosfrn4/node_modules/notion-astro-loader/dist/render.js:187:38)
at async listBlocks (/Users/bytedance/Code/personal/notion-astro-rev/node_modules/.pnpm/[email protected][email protected]_@[email protected][email protected][email protected]_roll_2ibjhaoyvb5densitsoeosfrn4/node_modules/notion-astro-loader/dist/render.js:79:25)
at async awaitAll (/Users/bytedance/Code/personal/notion-astro-rev/node_modules/.pnpm/[email protected][email protected]_@[email protected][email protected][email protected]_roll_2ibjhaoyvb5densitsoeosfrn4/node_modules/notion-astro-loader/dist/render.js:54:22)
at async NotionPageRenderer.render (/Users/bytedance/Code/personal/notion-astro-rev/node_modules/.pnpm/[email protected][email protected]_@[email protected][email protected][email protected]_roll_2ibjhaoyvb5densitsoeosfrn4/node_modules/notion-astro-loader/dist/render.js:163:28)
at async Promise.all (index 19) {
loc: undefined,
title: 'Error while loading image service.',
hint: undefined,
frame: undefined,
type: 'AstroError',
cause: Error: Vite module runner has been closed.
at SSRCompatModuleRunner.getModuleInformation (file:///Users/bytedance/Code/personal/notion-astro-rev/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected][email protected]/node_modules/vite/dist/node/module-runner.js:1196:13)
at SSRCompatModuleRunner.cachedModule (file:///Users/bytedance/Code/personal/notion-astro-rev/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected][email protected]/node_modules/vite/dist/node/module-runner.js:1186:21)
at request (file:///Users/bytedance/Code/personal/notion-astro-rev/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected][email protected]/node_modules/vite/dist/node/module-runner.js:1222:99)
at dynamicRequest (file:///Users/bytedance/Code/personal/notion-astro-rev/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected][email protected]/node_modules/vite/dist/node/module-runner.js:1224:124)
at getConfiguredImageService (/Users/bytedance/Code/personal/notion-astro-rev/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected][email protected][email protected][email protected]/node_modules/astro/dist/assets/internal.js:21:40)
at getImage (/Users/bytedance/Code/personal/notion-astro-rev/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected][email protected][email protected][email protected]/node_modules/astro/dist/assets/internal.js:55:25)
at getImage (astro:assets:32:82)
at fileToImageAsset (/Users/bytedance/Code/personal/notion-astro-rev/node_modules/.pnpm/[email protected][email protected]_@[email protected][email protected][email protected]_roll_2ibjhaoyvb5densitsoeosfrn4/node_modules/notion-astro-loader/dist/format.js:33:46)
at #fetchImage (/Users/bytedance/Code/personal/notion-astro-rev/node_modules/.pnpm/[email protected][email protected]_@[email protected][email protected][email protected]_roll_2ibjhaoyvb5densitsoeosfrn4/node_modules/notion-astro-loader/dist/render.js:187:86)
at listBlocks (/Users/bytedance/Code/personal/notion-astro-rev/node_modules/.pnpm/[email protected][email protected]_@[email protected][email protected][email protected]_roll_2ibjhaoyvb5densitsoeosfrn4/node_modules/notion-astro-loader/dist/render.js:79:31)
}
可以看出,这并非是没有加白,而是没有获取到图像服务导致的,我还查到了一个相关Issue:
我进一步溯源出问题的代码,发现在内容层进行 sync
操作时,Astro会先启动一个Vite服务器用于解析和编译配置代码,然后在内容层执行 loader.load()
进行同步之前,关闭并释放这个Vite服务器;而在开发环境中,Vite服务器一直都保持运行状态,因此没有这个问题。
当时,我想到了一个很愚蠢的解决方案,即把 dispose
删掉,让构建结束后Node来杀掉并释放Vite的内存即可,虽然造成了内存泄漏,但至少也不报错了不是吗?
在报错问题解决后,我越想越不对劲——这种写法怎么看也不是个事儿啊,就像一颗沙子一样硌着我,让我食不能安,夜不能寐。
于是,我做了一个和祖训完全一致的决定——研究一下内容层API的源码,看看官方到底是怎么写的,是Notion Loader写的不对,还是Content Layer API有问题,反正最后谁有问题都要接我的大板PR。
我从三个入口入手对Astro源码进行研究,核心目的是搞清楚Astro官方是如何在内容层中处理这种需要输出到产物中的图片的,这里不深究技术细节,仅供各位看个乐子,了解下大概思路,具体细节请移步 Astro Content Layer 处理图片的技术细节 。
cli/sync/index.ts
入手,研究内容层的同步机制content/runtime.ts:renderEntry()
函数入手,研究内容层的每个Entry是怎么被渲染成HTML产物的最终,我得到的结论是:Astro官方推荐的对本地缓存并输出到产物的图片的处理方法是在内容层中先「打标签」,在真正构建时再读取这些标签,调用服务进行处理,因此是Notion Loader用错了。
简单来说,glob loader是这样处理图像资源的:
Sync 周期:生成内容层
<img>
标签作特殊标记Build 周期:渲染页面 & 生成产物
render()
函数渲染时,会基于先前收集的元数据对 <img>
标签上的标记进行替换和处理assets
模块,使用先前存储在store中的信息,将图片转为ESM模块导入,以获取完整的元数据用于图像优化graph LR
subgraph Sync["Sync Period (Content Layer)"]
A[Read MD Files] --> B[Process with Remark]
B --> C[Render with Rehype]
C --> D[Generate Store File]
B -.->|Collect| E[Image Info]
E -->|Mark| C
D -->|Store| F[Resource Info]
D -->|Store| G[Module Info]
D -->|Store| H[Type Info]
end
subgraph Build["Build Period (Page Rendering)"]
I[Render Function] --> J[Process IMG Tags]
J --> K[ESM Import]
K --> L[Image Optimization]
F -.->|Use| J
G -.->|Use| K
L -->|Output| M[Final Assets]
end
Sync --> Build
结合考虑到Notion链接过期的问题,我最终选择将所有的图片都存在本地的 /src
目录内,这样做有几个好处:
/public
目录内,存放在 /src
目录内的图片可以转为ESM导入,从而获得资源优化Talk is cheap, let me show you the code!
我直接从原仓库中fork了一份代码,并仿照glob loader的处理方式实现了类似的图片处理机制,等功能稳定了就考虑PR合并到原项目中。
如果我的项目有帮到你,麻烦帮我点个star吧~
我的本意是解决自己的博客需求,让自己能用的爽,在Github上设为Public的原因也是我这点代码没啥好藏着掖着的,公开了便是,根本没有完善README、LICENSE等,也没有发包的打算。
没想到,我的小小贡献也意外帮助了几个人,Issue里有好几个人等我的方案,也因此收获了几个陌生人的star,npm第一天的下载量也有了小几百。
那么,不若就此试试做一个完整的开源项目吧,做成了自然好,做不成也可以自己用。
为了让别人用的舒心,也能方便地参与到开发中来,我需要尽可能地使自己的项目规范化:
目前的项目还处于非常初期的阶段,可以看作是一个仅仅实现了博客功能的MVP,还不能作为完整的个人网站使用。
将博客本体发布为模板
npm create
的模板生成器包WIP
作者: ChlorineC
创建于: 2025-02-18 00:00:00
更新于: 2025-02-17 18:35:00
版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
本文探讨了前端开发的复杂性,提倡回归以后端为中心的架构,强调使用简单的工具和技术,如模板引擎、jQuery、alpine.js和htmx,来简化开发流程。作者认为,现代前端工具链的复杂性逐渐偏离了简化开发的初衷,建议在不引入过多复杂性的情况下,利用Web Components等标准技术来实现可复用组件,从而提高开发效率。
本文总结了作者在前端开发领域的学习与成长历程,从大二的项目学习到2023年的实习经历,强调了通过项目实践和面试驱动学习的重要性。作者探讨了前端技术的广度与深度,认为前端并未“死亡”,而是面临着技术的两极分化,未来的发展方向包括拓展Web技术栈和探索全栈开发。文章还提出了大前端技术栈的横向与纵向拓展策略,鼓励前端工程师不断学习与适应新技术。
在React中,useRef和useEffect常用于管理组件的可变值和副作用,但在响应DOM元素挂载时,使用useCallback作为ref的回调函数更为有效。useCallback确保在元素挂载和卸载时被调用,避免了因useRef导致的生命周期回调未触发的问题。文中还介绍了其他几种优化Node操作的方法,如使用useState、useStateRef和useRefWithCallback,以提高性能和管理复杂性。
本文探讨了redux的发布-订阅模式的实现,强调其与原生useContext和useReducer的区别。redux通过subscription实例实现按需刷新,避免无关组件的更新。文章详细介绍了发布-订阅模式的基本概念、与观察者模式的对比,以及redux中的实现,包括Provider组件和Hook API的用法,如useSelector和useDispatch等,展示了redux如何解耦组件与状态更新的过程。