Vue.js、Clojure Hiccup SSR、Asp.Net的Web方案比较

柏舟   新冠4年 06-05

这个博客迭代了三轮了,从最开始的Vue.js到Clojure+Java,到现在的Asp.Net。

flowchart LR; Vue.js --> Clojure+Java --> Asp.Net;

Vue.js

由于使用了Element-UI,打包后接近1MB,阿里云有带宽限制,首次加载时间5秒左右,而且Vue.js不方便SEO。虽然可以改成SSR+CSR复合方案,但是我实在是不喜欢Javascript,尤其是各种Webpack,Vite,Esbuild,Babel这些搞不清楚的东西。

优点也非常明显:

只是我的博客只需要渲染页面,不存在状态,Vue.js就是杀鸡用牛刀。

Clojure hiccup+Java

对于博客来说功能上是完备的,而且可以逻辑和页面可以混着写,比如Html就没有办法嵌入for循环,但是Clojure就可以使用map嵌入循环,并且不用像Vue.js那样需要编译,非常简洁。

(defn sitemap [{articles :articlePageView t :tagsAll article-latest :articleLatest} url]
  (str
   #"<?xml version='1.0' encoding='UTF-8'?>"
   (html
    [:urlset {:xmlns "http://www.sitemaps.org/schemas/sitemap/0.9"}
     (map
      #(path-uri url % (get-in article-latest [:cur :datePublished]))
      ["/" (router-articles-list)])
     (map (partial article-uri url) articles) ; map 第二个参数是函数,第三个参数是参数(list)
     (map (partial topic-uri url) t)])))

;; [:url [:loc "http://127.0.0.1/topics/2"]
;;   [:priority 0.1]]
(defn topic-uri [uri topic]
  [:url [:loc (str uri (router-topics (:id topic)))]
   [:priority 0.1]])

编译出来的部分结果:

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>http://127.0.0.1/topics/2</loc>
    <priority>0.1</priority>
  </url>
  <url>
    <loc>http://127.0.0.1/topics/3</loc>
    <priority>0.1</priority>
  </url>
  <url>
    <loc>http://127.0.0.1/articles/22</loc>
    <lastmod>06/01/2023 03:14:44</lastmod>
  </url>
  <url>
    <loc>http://127.0.0.1/articles/21</loc>
    <lastmod>05/25/2023 10:13:37</lastmod>
  </url>
</urlset>

但缺点是需要同时写Java和Clojure。引入Java的原因是Clojure这种静态语言完全没有类似静态语言结构体的字段提示,导致写起来非常痛苦。设想一下,写一个函数不知道参数的类型是什么,也不知道里面有什么字段。我在开发过程中发现出错了,就只有打log观察数据结构是什么样子,而且作为一个Lisp注定没有办法打断点Debug。

所以在确定性强的部分,我引入Java从后端获取数据,在动态的部分,使用Clojure渲染。数据传递使用了Vert.x,可能的问题就是架构太复杂了。Vert.x是一个事件驱动模型,我使用了其中的Actor模型,每一个获取数据、渲染都单独使用了Actor,事实上完全没有必要。

flowchart TD; Request-->A; subgraph A[Java class: ViewHandler]; B[Java Actor: 从后端获取数据]--> C{成功?}; C --Yes--> D[Clojure Actor: 渲染模板]; C --No--> E[Clojure Actor: 渲染失败页面]; end; F[Vert.x EventBus]; B -.-> F; F -.-> D & E ;

如果再进行重构的话,能不使用Actor就不使用Actor。因为从后端获取数据本质上就是一个异步操作,注入Vert.x返回一个Future就可以了。Actor更多的是用于状态管理,抽象成Actor反而引入了Actor内部通信失败和序列化反序列化等等复杂度,函数调用根本不会存在这个问题。

flowchart TD; Request-->ViewHandler; ViewHandler <-.-> F[Vert.x EventBus]; F <-.-> A; subgraph A[Cojure Actor]; B[Java class: 从后端获取数据]--> C{成功?}; C --Yes--> D[Clojure function: 渲染模板]; C --No--> E[Clojure function: 渲染失败页面]; end; B -.- F;

这样就从原来的一个View三个Actor变为现在的一个,减少很多通信失败的错误处理。

Asp.Net

Asp.Net在我看来与Clojure+Java方案逻辑是一样的,使用C#获取后端数据,然后在cshtml文件中嵌入C#代码编写Html模板,但是不需要使用Actor进行通信。

flowchart TD; Request-->A; subgraph A[C#: ViewHandler]; B[C#: 从后端获取数据]--> C{成功?}; C --Yes--> D[cshtml: 渲染模板]; C --No--> Throw-Exception; end; subgraph Middleware; E[cshtml: 失败页面]; F[已知的Exception Filter: 返回设定的错误码] --> E; G[Internal Exception Filter: 500] --> E; end; Throw-Exception--> Middleware;

由于有巨硬(微软)的支持,调试方面非常好。Clojure+Java每次重载需要重新编译运行,每次10秒左右,但是Asp.Net可以热加载,编译速度非常快,并且可以支持断点Debug。

此外,C#的模板比Python的Django和Go的Template强多了,可以在模板内部嵌入组件,我觉得开发体验和功能跟写Vue的SSR相同(因为Vue的SSR不能有生命周期,那么状态的管理和绑定就没用)。这还是我还没学Blazor,我觉得完全版的Asp.Net跟前端工具链是没有区别的。

C#的模板还有一些独特的地方,它有全局共享的数据结构ViewData,你可以在Layout(写head等通过模板等地方)使用ViewData,在组件内部对ViewData进行赋值。

@* layout.cshtml *@
<head>
    <title>@ViewData["Title"]|柏舟的博客</title>
    @if (ViewData["Description"] is not null)
    {
        var desp = ViewData["Description"];
        <meta name="description" content="@desp" />
    }
</head>
<body>
    <main role="main">
        @RenderBody()
    </main>
</body>

@* article *@
@{
    Layout = "_Layout";
    ViewData["Title"] = "首页";
}

也就是说,它的渲染顺序不完全是从外到内的,内部的逻辑也会影响外层。这个渲染逻辑顺序还真是挺神奇的。