当我们在讨论 react、vue、angualr 时,大多数时候,讨论的都是客户端渲染时的表现,其实很少会考虑他们在服务端渲染这一块做的到底如何了。
但是事实上。服务端渲染方案一直是一个巨大的刚需。 他虽然没有被更普遍的提及,但是在大多数团队中,他从来都没有消失。只是很多时候他并不被初级前端工程师所关注,很多后来入行的前端对这些服务端渲染方案变得逐渐陌生。以至于有的人认为,服务端渲染只停留在了一个提升首屏渲染速度的八股文概念。
!事实上,真要深究,如果只是简单的使用服务端渲染,有可能不仅无法提升首屏渲染速度,还会在用户量变大时访问速度更慢
或者有的团队,干脆就将这样的需求,交给后端来处理。比如用 python web 的框架来做这个事情。甚至有的后端语言在服务端渲染上的方案层出不穷,比前端发展的更快更好。
前后端分离在一定程度上,降低了前端/后端的准入门槛。 这种环境下,滋生出来的前端工程师,天生就注定在技术方案的积累上是残缺的,不懂服务端,不擅长服务端渲染是许多前端工程师进阶的主要壁垒和障碍。
另外一方面,随着客户端方案的持续发展,我们也会发现,客户端方案始终存在一些的痛点。例如,功能复杂之后,项目过重、打包体积过大,无法支持 SEO,首屏渲染速度偏慢等,并且现在的项目工程也越来越庞杂。
当我接触过越来越多的项目之后,我越发有一种非常深刻的感受,那就是早期的 JSP/PHP JS 脚本语言,有可能才是项目架构的最佳实践的雏形。
几个场景
第一个场景是iOS、Android 等客户端开发。
发展了这么多年,客户端开发始终没有办法完整的取代网页开发。这里有几个重要痛点他们无法解决。
一个是,发版审核速度不确定的问题。当我们想要快速修改某一个细节内容或者 bug 时,客户端由于每次发版本都无法轻松的绕开审核机制,因此为了热更新方案和平台斗智斗勇,最终只有 webview 是最轻松的方案。webview 可以轻松的做到更改内容并发布上线,还不需要审核。因此一个成熟的 App 内部,一定会有大量内嵌 webview 的场景。
另外一个问题就是打包体积问题。 如果你想要在客户端方案中,集成大量复杂的交互和渲染结果,那么必然会导致更加庞大的打包体积,这一点在宣传和应用上是非常不利的,因此,很多时候,客户端开发的复杂场景都会尽可能的优先考虑是否需要使用 webview。
除此之外,webview 还具备非常轻松的跨平台能力。这一点也是在方案选择上无法忽视的巨大优势。基于这些点,在手机 app 的开发中,webview 始终都是一个无法被忽视的重要组成部分。
在这个场景之下,往往我们的网页他不需要是一个完整的 SPA 项目,而是某一个嵌入客户端功能点的碎片页面。此时如果引入整个 SPA 项目包,就可能会导致包体积过大而页面出现速度不够快,因此,在客户端开发的混合模式中,服务端渲染是最合适的方案。
另外一个场景就是 SEO,在面向用户的许多场景中,电商、博客、新闻、官网资讯类等网站,都需要 SEO,服务端渲染方案在 SEO 这一块有必然的优势。
由于服务端渲染优化得非常好之后,可以做到页面内容小、直出快,在许多的宣传营销活动中也被大量的采用。
也是因为由于服务端渲染可以做到打包体积极小,因此在一些团队的后台管理系统,居然会采用服务端渲染方案,以更方便和轻量的集成到 B 端 App 中,而不是在客户端搞一个重型项目使用。
这个时候,一个恐怖的事情来了,一通分析下来,居然发现服务端渲染项目其实是可以覆盖网页应用的全场景的。
问题出在哪?
即然服务端渲染这么牛逼,为什么客户端在过去十年中,客户端方案成为了主流并且大行其道?问题出在哪?
这里就会涉及到两个非常重要的问题,一个是开发难度,另外一个是开发体验。
我们以 PHP 开发举例。一个完整的应用,在服务端我们只能负责字符串的拼接工作。因此在后端我们会使用模板语法去拼接网页内容
代码语言:javascript复制<ul>
<?php foreach($names as $key=>$name): ?>
<li><?=$name?></li>
<?php endforeach; ?>
</ul>
这样处理之后,如果我们还需要额外的客户端交互,就可以直接利用 jQuery 在服务端的网页模板中嵌入一段 script
脚本语言来编写逻辑,例如下面这段代码
<!doctype html>
<html>
<head>
<script src="https://code.jquery.com/jquery-3.3.1.js" integrity="sha256-2Kok7MbOyxpgUVvAk/HJ2jigOSYS2auK4Pfzbm7uH60=" crossorigin="anonymous"></script>
</head>
<body>
<form id="loginform" method="post">
<div>
Username:
<input type="text" name="username" id="username" />
Password:
<input type="password" name="password" id="password" />
<input type="submit" name="loginBtn" id="loginBtn" value="Login" />
</div>
</form>
<script type="text/javascript">
$(document).ready(function() {
$('#loginform').submit(function(e) {
e.preventDefault();
$.ajax({
type: "POST",
url: 'login.php',
data: $(this).serialize(),
success: function(response) {
var jsonData = JSON.parse(response);
// user is logged in successfully in the back-end
// let's redirect
if (jsonData.success == "1") {
location.href = 'my_profile.php';
}
else {
alert('Invalid Credentials!');
}
}
});
});
});
</script>
</body>
</html>
方案上本来没啥问题,但是随着前端页面的交互变得越来越复杂,交互部分的代码也越来越多,开发难度变得越来越大,这种在项目中嵌入部分片段脚本语言来获得交互能力的做法,成本就越来越高。搞到后面,发现有的项目已经复杂到搞不了了。
除此之外,在开发难度上,对于前端部分的开发能力的要求也变得越来越高。早期的程序员都是前后端一起学的,但是后来发现大多数程序员搞不定这么复杂的前端交互。然后慢慢演变出前后端分离的方案。
同构
虽然如此,但是技术大佬们并没有放弃对服务端渲染更优秀解决方案的探索,在纯客户端方案发展得如火如荼的期间,服务端渲染方案中,提出了一个很有意义的概念,叫做同构组件。
在过去很长一段时间,同构这个概念是高级程序员必须要接触的一个知识点,但是许多纯粹的前端程序员可能还是第一次听说它。
以 React 组件为例,同构的意思是,我们编写的一个组件,即可以在服务端被渲染成字符串,也可以在客户端被渲染成 DOM 节点。这是在开发体验上进展了一大步。同构组件组合而出的应用,我们称之为同构应用
但是同构应用的掌握可不是这么简单。他包含了更多晦涩难懂的概念。例如,你是否能准确的区分前端环境与服务端环境?
你是否能准确区分前端路由与服务端路由?
你是否明白什么是水合(hydrate)?什么是脱水(dehydrate)?
你是否能在这个过程中正确的处理服务端内容和客户端样式的整合过程?
你是否能合理利用缓存解决最重要的性能问题?
!性能问题非常重要,因为要把原本放在客户端去做的渲染任务,集中放在服务器来做,这给服务端带来的压力可想而知,尤其是涉及到大型数据、图表、markdown 解析等高计算量的场景,算力成本花费更高。
next.js 解决了什么问题?
过去普通的同构应用带来了一些开发体验的提升,在开成本上也大大降低。但是它也有明显的短板,那就是服务端压力可能会有点大,以及水合所占用的时间可能会有点长。从而导致 TTI 的数据很难看,并且包体积也不见得会减少多少
✓TTI:页面从渲染到可交互所经历的时间
同构应用的概念提出得很早,但是由于各种条件的不成熟,例如有的方案连 ts 的支持都做不到,因此许多的技术方案也湮灭在了历史的发展过程中,例如早期的 rendr、Lazo、beidou 等。
next.js 在充分吸收了过去的发展历史,放弃了同构组件的概念,采用了另外一种同构方案,来解决服务端渲染的问题,这就是 RSC:React Server Components
同构组件说的是,一个组件,即可以是服务端组件,也可以是客户端组件。但是 RSC 则明确的将服务端组件和客户端组件区分开。一个组件,要么只能用于客户端渲染,要么只能用于服务端渲染
但是他解决的一个重要问题是,客户端组件和服务端组件可以混合在一起编写。例如,我们可以在服务端渲染的组件中,引入一个客户端组件。
如下所示,我在服务端组件中,引入一个客户端组件 <ThemeToggleButton/>
<Flex>
<ChatButton />
<ActiveCodeButton />
<ThemeToggleButton/>
</Flex>
<ThemeToggleButton/>
代码如下
'use client';
import Button from "@/components/Button";
import { useEffect } from "react";
import { useTheme } from 'next-themes'
export default function ThemeToggleButton() {
const {theme, setTheme} = useTheme()
function __themeToggle() {
setTheme(theme === 'light' ? 'dark' : 'light')
}
return (
<Button onClick={__themeToggle}>Switch</Button>
)
}
我们可以称这种组件的组合方式为一种新型的同构方式。在服务端组件中,客户端组件相关的内容依然会渲染出来,但是他不具备响应能力。
这样的组合方式,为部分水合奠定了重要的理论基础。在此基础之上,nextjs 提出并践行了一些重要的理论:
1、 按需水合。我们只需要在需要交互的地方声明客户端组件,此时开发者可以控制水合内容的体积。从而让水合成本变得更小
2、 更多的使用原生 DOM 的能力,让部分交互即使不水合也能够直接交互。
3、 SSR、SSG、ISR 三种渲染模式混合运用以解决服务端性能的问题。
在这个理论基础之上,next.js 还解决了开发体验不好的问题。比如他支持 HMR,自动代码拆分,内置路由系统、良好的 TS 集成等,最关键的是,他内置了 tailwindcss,无痛解决了服务端渲染项目中处理起来很麻烦的样式问题.
关于 RSC 解决的性能问题其实并不是我最关心的问题,因为一个比较强的架构师,或多或少都能够通过其他的方式在原有同构架构的基础之上解决性能问题,但是对我来说,最重要的就是开发体验带来的提升这个是非常有吸引力的。
用好之后,目前 next.js 有不输于客户端方案的开发体验。
总结与趋势
当我接触过越来越多的项目之后,我越发有一种非常深刻的感受,那就是早期的 JSP/PHP JS 脚本语言,有可能才是网页应用项目架构的最佳实践的雏形。
但是受限于开发难度与开发体验,这种方案到如今已经没落,而 next.js 的成熟则在这种思想上奠定了更为坚实的落地基础,这种全栈思维正在慢慢回归主流。
next.js 和 react 不同的地方在于,它提供了一整套完整的解决方案。这一整套解决方案非常便利的弱化了开发与部署的难度。
因此,我的结论就是,next.js 在前端框架上,换了一个赛道卷,它有效的整合了客户端渲染和服务端渲染的共同优点,并且这个赛道正在被越来越多的团队所接受。不管你接受与否,它都会发展成为未来网页开发的主流方案。
毕竟,全场景覆盖真的太香了!