其他
干货 | 新时代的 SSR 框架破局者:qwik
作者简介
19组清风,携程资深前端开发工程师,负责商旅前端公共基础平台建设,关注NodeJs、研发效能领域。首先会围绕对比 SSR 与 SPA 各自的优劣势,从而展开 SSR 的运行机制以及 SSR 相较于 SPA 究竟为了解决什么问题。 之后,会根据 NextJs 的运行机制思考针对目前主流 SSR 框架设计思路上存在的不足从而引出 qwik 为何会在众多成熟框架中脱颖而出。 最后,会针对于 qwik 提出自己的看法以及聊聊目前 qwik 存在的“问题”。
<html>
<head>
<title>携程商旅</title>
</head>
<body>
<div id="root"> </div>
<script src="./index.js"> </script>
</body>
</html>
初始加载时间长。
SEO(搜索引擎优化) 的负面影响。
首先服务器会根据对应的 URL 在服务端根据对应路径渲染对应的 HTML 模版。 注意这里渲染的 HTML 模版是具有该页面真正的内容。同时它并不具备任何交互逻辑(比如 DOM 元素的点击事件),这是一份完全的静态站点。
服务器会下发这份仅具有静态内容的 HTML 模版,同时这份模版中也会包含对应的 JavaScript 执行脚本。 第一时间会展示给用户对应的 HTML 页面,此时对于访问站点的用户来说首屏渲染相较于 SPA 应用来说会非常快。因为它并不需要在客户端浏览器上再次下载和执行 JavaScript 脚本来进行页面渲染。 其次,针对于 SEO 的优化也会非常良好,因为服务器上下发的 HTML 页面是包含当前站点的真实 HTML 结构,对于搜索引擎的爬虫来说会非常容易的匹配到当前关键字。
之后,浏览器会下载当前这份 HTML 的 JS 脚本。 因为首先呈现给用户的一份静态的 HTML 页面,并不具备任何交互效果。我们需要为页面上的元素增加对应交互,HTML 页面中的 JS 脚本中会包含网站的交互逻辑。
最后,当下载完 HTML 脚本中的 JS 脚本后,自然会执行这些 script 脚本。从而发生一种被称为 # hydrate(水合) 的方式,从而为页面上静态 HTML 元素再次添加对应的事件处理从而保证页面具有交互性。
当用户访问 时,服务端接收到请求调用 ReactDOMServer.renderToString() 生成当前页面的 HTML 静态结构。 服务器会下发这个 HTML 页面给客户端,同时这个 HTML 页面上也会携带一部分 JS 脚本 script 标签。 用户的浏览器中会立即展现到该 HTML 页面,同时也会下载对应 JS 脚本并执行。 当 JS 脚本执行完毕后,客户端会调用 ReactDOM.hydrate() 发生水合为当前页面的 HTML 页面添加事件交互处理,同时后续由 JS 接管页面的跳转渲染。
更好的搜索引擎优化 SEO 方式,HTML 模板是从服务端直接下发这也就导致搜索引擎爬虫中更多的关键字匹配。 更快的首屏渲染,因为相较于 SPA 它少了在 Client 中下载和执行 JS 脚本后渲染的过程。 页面不需要 JS 也可以正常渲染,虽然没有 JS 意味着页面失去了可交互性。但对于禁用 JS 的用户来说,展示一些静态内容总比 SPA 应用的白屏来的更加友好一些对吧。
强依赖于服务。 针对于 CSR 的方式它是一种纯静态资源。我们可以直接将它放在 CDN 上就可以良好的用户访问到,而 SSR 的方式必须依赖于一个服务器进行服务端预渲染。(当然纯 SSG 应用我们不在这个讨论范围之内) 同时,有服务的地方就存在并发压力。当你需要为你的应用考虑服务端渲染的方式时,一定不要忘记为你的服务器进行压测。 Time to Interactive 可交互时间 (TTI) 的增长,虽然说 SSR 的方式有效的缩短了首屏加载的方式,但是会增加所谓的TTI(可交互时间)。 所谓的 TTI 指标测量页面从开始加载到主要子资源完成渲染,并能够快速、可靠地响应用户输入所需的时间。 因为 SSR 的方式在用户访问时会下发当前页面中静态的 HTML 内容,也就是所谓的 First Contentful Paint 首次内容绘制 (FCP) 会非常快速,但是页面需要用户交互效果又需要下载和执行完成 JS 脚本发生 hydatrion 后才具有交互性。 这也就造成页面的 TTI 相较于 CSR 方式会有所差劲,因为 CSR 在渲染完成后就会立即具有交互性(不需要其他任何多余步骤)。
每一个事件处理程序中的内容,绝大多数框架中的状态都作为闭包函数保存在内容中。所以需要 hydration 的过程来重新获取状态。 其次,在搞清楚了每个事件处理函数的内容后。我们也需要将对应的事件处理函数附加到对应的 DOM 节点上,同时还要确保该监听器的正确事件类型。
APP_STATE:应用程序的状态。简单来说应用程序的状态就是 HTML 事件中的各个状态事件,如果不存在这些事件状态那么所有的内容都是没有任何交互效果的。 FRAMEWORK_STATE:框架内部状态。通常我们会利用诸如 React 或者 Vue 等框架进行接替渲染。如果没有 FRAMETER_STATE,框架内部就不知道应该更新哪些DOM节点,也不知道应该在什么时候更新它们。
export const Main = () => <>
<Greeter />
<Counter value={10}/>
</>
export const Greeter = () => {
return (
<button onClick={() => alert('Hello World!'))}>
Trip Biz
</button>
)
}
export const Counter = (props: { value: number }) => {
const store = useStore({ count: props.number || 0 });
return (
<button onClick={() => store.count++)}>
{store.count}
</button>
)
}
<button>Greet</button>
<button>10</button>
将所有必需的信息序列化为 HTML 的一部分。 qwik 将需要的状态以及事件序列化保存在 Server 端下发的 HTML 模版中,需要序列化信息需要包括WHAT(事件处理函数内容), WHERE(哪些节点需要哪些类型的事件处理函数), APP_STATE(应用状态), 和FRAMEWORK_STATE(框架状态)。 依赖于事件冒泡来拦截所有事件的全局事件处理程序。 qwik 中事件处理程序是在全局处理的,这样我们就不必在特定的 DOM 元素上单独注册所有事件。 qwki 内部存在一个可以延迟恢复事件处理程序的工厂函数。 该工厂函数主要用于处理 WHAT 阶段,也就是用来识别某个事件处理函数中应该存在什么脚本逻辑。
export const Main = () => <>
<Greeter />
<Counter value={10}/>
</>
export const Greeter = () => {
return (
<button onClick={() => alert('Hello World!'))}>
Trip Biz
</button>
)
}
export const Counter = (props: { value: number }) => {
const store = useStore({ count: props.number || 0 });
return (
<button onClick={() => store.count++)}>
{store.count}
</button>
)
}
<div q:host>
<div q:host>
<button on:click="./chunk-a.js#button">Trip Biz</button>
</div>
<div q:host>
<button q:obj="1" on:click="./chunk-b.js#count[0]">10</button>
</div>
</div>
<script id="qwikloader">/* qwik 中设置全局事件监听器的代码 */</script>
<script id="qwik/json">/* 用于反序列化的 JSON 相关信息 */</script>
需要开发人员自行去处理更加细粒度的代码分割,当然这并不是最主要的。因为目前我们可以利用 Magic Comments 配合 Dynaic Imports 来解决需要手动切入多个入口点的问题。
当存在非常多的延迟加载时,传统构建工具会从一个大 bundle 分割成为无数个小的 bundle 。
“携程技术”公众号
分享,交流,成长