让万物皆可 SSR
什么是 SSR
SSR, Server-side rendering 顾名思义,是服务端渲染。早期网页开发都是 SSR,SSR 的概念是来自于 SPA 普及之后。
SPA, Single-page application 看起来和 SSR 没什么关系。但其实大多数 SPA 都是 CSR, Client-side rendering(至少我没见过其他的)。这个名词是不是就和 SSR 相对了呢?
SSR vs CSR
介于我想让所有人都能读懂我的文章,所以我简单介绍一下 SSR 和 CSR 的区别。如果你很熟这些可以直接往下滑到「掘地求生」那一章节。
| 功能 | SSR | CSR |
|---|---|---|
| 交互 | 难做,基本上都靠表单,按一下按钮刷新一下页面 | 信手拈来 |
| 速度 | 每一个页面基本上都差不多速度 | 首页加载慢,其他页面快,跳转页面没有白屏全部重载 |
| 数据获取 | 服务器直接生成 HTML 给浏览器,一个请求呈现所有数据 | JavaScript 发起额外请求向 API 拉取数据 |
| SEO / 机器人友好度 | 非常友好,机器人看到的就是人类看到的 | 不友好,早期机器人直接不运行 JavaScript,CSR 页面都呈现不出来;现在机器人也不会等待数据加载,索引页面不完整 |
可以看到上面 SSR 和 CSR 的对比,各有优劣。为了取长补短,所以出现了 Hybird 混合模式;就是服务端跑一遍 JavaScript 代码,返回最初的 HTML 给客户端。客户端再跑一遍 JavaScript 代码,渲染用户交互之后的页面。
现有 SSR 的问题
看起来这就是最佳实践了,对吧?Well, it works. But not good enough.
目前来看,在服务端上跑 JavaScript 有两个问题,一个问题是效率低下,NodeJS 吃超级多内存,解释型语言 CPU 利用率也不够高。第二个问题是容易遭到歧视。
那如果不用 JavaScript 呢?问这个问题之前我们要知道为什么 Hybird 这种模式要用 JS 写服务端。
上文提到,Hybird 总共要跑两边代码;一遍在服务器,一遍在客户端。但是实际上我们只写一遍代码。如果换语言客户端就跑不了了。但是如果你想用 WASM 也不是不行,但是我只能祝你好运。你是信客户端加载一个 MB 起跳的 WASM 非常快还是信我是秦始皇?
客户端控制不了,至少服务端能扩容。而且 JavaScript 的全栈框架可以吸引原本的前端开发者,学习语言的成本少,这样生态庞大。就没有别的人想用非 JavaScript 的语言来进行 Hybird 的开发了。
其实还是有的,但是很少;我目前知道两种其他的,一种是 PHP 的 Laravel,因为历史遗留问题所以做了 SPA 的兼容方案。另一种是 Rust 的系列,没有什么指定的框架, 但是因为他们那群邪教徒用 WASM 把 Rust 搬到前端去了,所以理论上也行。
掘地求生
自从 Solar Network 换到 v3 的服务器 Dyson Network 之后。脱离的 HyperNet 的束缚让我有点想法想给网页端做点心肺复苏。利用 ASP.NET 在 Good Old Days 积累的技术优势(SSR)Razor Pages 之类的写了点网页。但是无奈现在的现代前端技术栈都是给 React, Vue 之类框架做的,用 Razor Pages 感觉就像穿越回到了古代。让自己很不舒服。
像 v2 HyperNet 的时代,我是单独开一个 Hybird 项目来做搜索引擎优化等网页功能。但是这让很多人误解我的官网是 Solian 网页版。而且我也想做出一点改变。所以我便做出了 AnySSR。
AnySSR
AnySSR 不是什么具体的框架,而是一种思路。目前我在 Dyson Network 做了一版实现,等完善之后有空之后可能独立做出一套框架。
一般来说传统的 SPA 和 CSR 都是分开部署前后端的。这样也不是说不行,就是有点麻烦。所以很早我就是让后端也负责托管前端的静态资源。
想着既然访问前端都要走一遭后端,为什么我不在这里就把数据给前端呢?省的后来又后端要数据,即节省开销又可以让搜索引擎工作(现代搜索引擎是可以运行 JavaScript,但是运行 JS 不会等待你的数据获取完成再截取页面建立拿去索引)一举两得。
如果你开过一个 SPA 项目并且有追根的心态去探索一下你会发现里面有一个 index.html. SPA 的本质就是让所有路径都访问这一个 index.html,然后让 JavaScript 脚本根据你的路径和其他环境信息渲染出不同的页面出来。既然用户要访问的是这个 index.html,我们便可以在这里做点手脚。
古代有一个种后端给前端传数据的方式,用于规避 CORS,也就是浏览器的跨域限制。原理是将 JSON 数据套个壳变成 JS 脚本,由于浏览器并没有给脚本加上 CORS,所以就可以偷渡数据过来。这种方法叫做 JSONP
我借用一下这个思想,再加上模版的利用,在原本的 index.html 里面加一个提示标签 <app-data /> 然后让服务器根据用户返回的请求准备数据,生成一个 JSONP 替换这个特殊的标签。比如将它替换成
<script>window.DyPrefetch = /*DATA*/</script>
这样 SPA 代码就可以在渲染的时候访问到 window.DyPrefetch 里的数据,不用发送额外的请求啦,至此万事大吉…… 了吗?
Dev Server
好了,生产环境是没问题了,但是谁好人改一点代码就编译一遍然后只访问后端来看前端的改变。这部纯给自己找事吗?为了弥补开发体验上的这个缺点,我有以下办法。
Dev Server Proxy,让后端给前端开发服务器跑反向代理,后端拿到前端传来的数据继续上述操作,嵌入数据,最后传给浏览器。
Mixed,开发的时候依旧使用 API 获取数据,只在线上用嵌入数据。
我最终选择了第二种,一是避免嵌入数据要是被我弄坏了有备用方案。二是我有完善的 API,在一些场景不需要预先加载或者 SEO 的话直接用 API 可以节省开发成本。
至此,万事大吉。
你知道吗?NuxtJS 中后端给前端同步各个 Ref / Reactive 数据状态的方法也是 JSONP,你可以看到在 Hybird 页面中有 NUXT_DATA 的身影在一个