先说结论,除非是博客一类的静态网站项目搭建或者你在这个领域有很深入的研究,否则普通的前后端开发者还是能不要用服务端渲染就不要用渲染。 再叠个甲,本文主要讲的是以 Jquery 手动管理 DOM 的陈年项目,采用最传统的模板引擎托起的 SSR。而非目前现代化的 Astro、Remix 方案,要是能上 Vue、React 也不会有这篇文章了 XD。
缘起
对于一名体验过前后端的开发的 Coder 来说,基于模板引擎渲染的 Web 网站,我是怎么也想不到能在离开学校以后还能接触到这方面的开发。 回想起模板引擎,最开始接触好像是大一初学 Golang 基础的时候顺带过了一遍 template 标准库。 当时觉得模板这类东西用来做脚手架还挺好,但是用来写前端的话还是差点意思(在我学习 Golang 的时候已经对 Vue 略有耳闻)。
现在回忆起来,专业的事情还是要用专业的工具做, Nodejs、NPM、Webpack 等工具链的诞生对 Web 开发模式的改变真是革命性的。 大三的时候,学校开设了 JSP 开发相关的课程,这可能是我第一次比较完整的开发服务端渲染的网站。 记忆有点模糊了,当时好像是用他做了一个物流管理系统的课设。虽然只有千行代码,但是开发体验是真的非常差。 对于一个早早体验过 Springboot 全家桶的 Gopher 来说,Servlet、Tomcat、JavaBean 等概念真的有一种返祖的感觉。 加之落后的 Eclipse 编辑器、没有包管理工具需要手动导入的 JDBC 依赖库、Tomcat 托起的页面... 这套组合拳下来,使得我对 JSP 开发的好感度降至冰点。 更不用说,Java 代码和 HTML 模板混合编写,启动和渲染的生命周期限制。 因此可以得出结论,模板引擎的开发真的需要很大的耐心。
都说无巧不成书,由于最近项目的需要盯上一个看起来不错的网站,正好他还是开源的,就萌生了基于他二次开发的想法...
运行项目
二次开发的第一步就是将原项目跑起来。不幸的是,原项目因为历史遗留问题,编写的环境非常古老, 充斥着 Python 2.x、JQuery 等上古时期遗留的产物。其中,甚至还有 npm 仓库找不到的 Bootstrap 版本。 在消耗了 AI Agent 一些额度后,勉强将项目从 Python 2.X 迁移到 3.X 并顺利运行。 随后,便开始了慢慢又曲折的迁移之路。
模板迁移
由于每个语言的模板引擎规则都不太相同,虽然用法都大相径庭。但是语法的差异还是需要进行一些微调。 作为一名身处 AI 时代的 Coder,怎么可能会劳累自己手动迁移呢。 于是,我又花费了一些 Agent 额度,让 AI 成功将 Python 的模板语法迁移到了 Golang 的规则下, 并用 Go 语言知名 Web 框架 —— Gin 托起了项目。目前看起来,一切还算顺利。
类型提示
对于习惯编写静态类型语言以及 TypeScript 的开发者来说,看到 js 代码中满屏的 any 不禁会感到反胃和不适。 于是乎,我在不加思索的,安装了 pnpm 和 vite,虽然我知道基于模板编写和前后端分离的开发方式肯定有很多差异。 因为,模板的渲染需要服务端解析,而 vite 等一类打包工具直接编译出能够运行的源文件,他是没法解析模板语法的。 这就意味安装了 vite 除了占用磁盘外,没有一点用武之地。但是,利用 pnpm 安装的带 @types 的库,将 js 文件改名成 ts 后 有了一些类型提示,至少还能作为安慰。
模板纠错和代码提示
当我尝试将原项目中的模板扩展名 .html 替换成 Go语言模板扩展文件名后缀 .tmpl 后,
本来的高亮和代码提示消失了,只剩下悬停查看字段类型的插件还起作用。
之后,万能的 AI 替我想了一个黑魔法 —— 将 .vue 翻译成 .tmpl 以实现高亮和代码提示。
输入:
<li v-for="item in .Items">{{ item }}</li>
输出:
{{range .Items}}
<li>{{.item}}</li>
{{end}}
于是,在与 AI 一顿友好交流后, 它为我设计了看起来比较合理的语法。
并利用 @vue/compiler-dom 库操作 Vue 的 AST 根据语法树生成模板代码,看起来一切都走上了正轨。
但是,很快我就意识这样处理的问题,模板实在是太灵活了,他的插值在任何地方都能插入,
Vue 作为结构化的描述框架,肯定不能适用任何场景。
后来,我一想,为什么不用一开始的 HTML 代码提示就好。于是,对于代码提示和语法检测一切又回到了原点。
但是我并不甘心这来回的倒腾,也许是因为想上点不一样的吧。 于是,以《服务端渲染慢慢踩坑路》为主题的大型连续剧就出现了,并让我水了一期博客 XD
迁移 JQuery
由于原网站的核心逻辑是采用 JQuery 操作 DOM 计算路径,并用 d3.js 绘制线条实现的。
一开始看到文件夹中醒目的 jquery.js,就萌生了将它替换成更加现代化的解决方案,例如 React 或 Vue。
但是当看到核心逻辑写了整整 1200 行并且大部分逻辑还是操作 DOM 和路径计算后,便果断放弃了这个想法。
1200 行代码虽然不算太多,但也不少。尤其是很多逻辑涉及了计算的,重新理解它的实现不如推倒重写。
不过,吸取了以前开发的教训 —— 完成比完美更加重要的原则,能跑为什么要改呢?
于是,善于妥协的我决定对他进行一点点小小的升级,就是将充斥着大量 any 的类型签名打上具体的类型定义。
是的,我采取了更加折中的方案,将 js 代码换成 ts 代码。
当 html 需要引入 .js 文件的时候,调用 tsc 编译 .ts 成 .js 供其调用。
在技术上,虽然没啥提升,就是打了类型,调了下命令行。
但是,这个过程对于半吊子前端的我,也是涨了不少见识。
模拟类
在早期 js 没有 class 关键字的时候,可以使用函数模拟,然后为该函数的原型链扩展以实现方法。
而该函数可以视作构造函数,可以调用它返回 “对象”。
这办法现在看起来虽然有点 dirty work 的味道,但是放当年想必应该非常优雅。
单文件编译
如果将单文件的源码拆分成多文件,并利用 import 引入,调用 tsc 编译,它是没法打包成单文件的。 问了 AI,也没有相关的编译期选项,先拼接代码再编译成单文件的操作。 但是,喜欢折腾的我怎么会就这么放弃?不行的话,自己手撸语法树实现拼接也要上啊(划掉,专业的事情还是给专业的人干吧 XD
于是,我怀着好奇的心态询问了 GPT 一些单文件编译的方法和工具链。也是了解到了 esbuild 的作用。 之前都是听闻 vite 就是基于 esbuild 实现打包的,还没自己亲自实践过。 再者,我的项目也有 vite,之前没啥作用,但是现在终于有了用武之地。心想,这或许就是命运的安排吧。 从脑袋空空的安装 pnpm + vite,到误打误撞让我有接触 esbuild 的机会,这下谁都别想拦我用它编译单文件上马的想法。
不过,很快现实就给我当头一棒。esbuild 编译自带压缩,编译出来少了一大半东西,输出虽然是单文件了,代码也少了一大半。 这下还不如 tsc 编译凑合着过算了。碍于好胜心作祟,这我今天非得把它弄出来不可。 ......(十年后) 经过我一顿酷酷输出,问题终于还是没能解决。 我猜测是 esbuild 的代码压缩机制作怪,也可能是代码存在很多未定义的库没有引入。 导致 esbuild 无法正确推导,就将其舍弃。它的容错率应该比 tsc 要小,遂止。 是的,没错,我又放弃了。有时候就是这样,退一步海阔天空。我又再次说服了自己 hhh。 能跑就不要瞎折腾,完成比完美更重要。
处理上古依赖
在过去的开发中,很多人习惯于将 xxx.min.js 或者完整实现存放在 static 中一并上传 Github,这对于现在我的看来无疑是相当丑陋的做法。 无异于将 node_modules 上传到远程仓库中。 因此,处理这些文件就是当务之急。我 首先想到的解决方案就是从 import 引入,让 vite 打包出他们。但是现在的工具链好像和链接器一样,只会链接上被引用的定义,而不能导出完整的实现。 因此,我又又又做出了一个妥协。决定通过运行前下载的方式引入他们,但是下载也分好几种。 一开始我选择从 node_modules 提取想要的文件进入 static。但我扒下来的这个项目比较久远, 很多库如 Bootstrap、FontAwesome、d3.js、JQuery 离现在最新的版本少说差了10年的代差。 因此,NodeJS 的仓库有些都无法找到具体的源文件。 接着,我转向从他们的开源仓库扒拉,通过 gitmodule 这种看起来最高大上的方式引入,然后提取出想要的。 结果当我尝试引入 Bootstrap 的时候就给我当头一棒。由于它是一个经营了几十年的老项目了,光 clone 就要花大量时间。 如果加上网络波动,这仓库压根没法全部 clone 下来,再切换分支,遂止。 后面又陆陆续续做了一些尝试,GPT 来得以吐出可以通过 CDN 直接下载的方式。我才恍然大悟,还能从中提取出源文件。 之前,我知道生产环境大部分都是通过 CDN 引入的,但是我一直没想到开发环境也可以如法炮制。 果然,大道至简。
实现热更新
上面的方案充斥着各种妥协和委屈,总算是有一个让我还算满意的功能。 那就是,同现代化 Web 开发框架一样,当网页源码编辑后立刻刷新浏览器。 实现热更的方案可以说五花八门,我采取了一个最简单也最直观的方法。 利用 Go 语言的 fsnotify 库监听指定目录,当目录下的文件变化的时候, 操作系统就会推送事件给框架,然后我在利用 Websocket 连接告知前端刷新浏览器即可。 我是如此想的,GPT 也是如此建议的,代码也是如此写的,一切都非常顺利。 我很轻易的就实现了网站的热更,这个功能还是相当推荐没实现过的开发者尝试实现的。 当然,上述的方案可能不是最优雅的方案,但是是我能想到最暴力,最容易的方案。 毕竟,完成比完美更重要 :)