免费不用抽的ssr,不进来看看么

1,172 阅读10分钟

服务端渲染大揭秘

前言

还在替弟弟学业操心的我听到一声:SSR!姐我 SSR 了!

我:???

???

小学生都会 SSR 了?让我清醒一下。

哦不,是这个啊~~

不知火

忙于 coding 的你一定听说过 SSGSSR 这些名词,究竟是啥一起来揭秘。

那什么是 SSR?Super Super Rare?

SSG 全名 Static-Site Generator,静态站点生成,听着名字就晓得是静态的、在构建的时候就生成了,那么想要更新网站内容就要重新构建,这适合企业官网 or 个人博客等,没有频繁更新的诉求。优点想想就很明显,速度快(连 api 都没肯定快了),部署方便(就静态文件丢上去就好使),安全(纯静态也没有 sql 注入).. SSR SSR 全名 Server-side rendering (SSR),服务端渲染,

CSR 的优点很明显,那缺陷也很明显(这不是废话),即使你做了 dynamic importsplit chunk, 但也免不了 bundle,一个做了 3 年的大应用 bundle 大小 1m 是很常见的,浏览器首次构建 HTML 拿到的是空的,要等 js 执行完了再开始动态渲染改变 DOM 树,这个期间还会请求 api,据获取数据将数据渲染到页面,完成显示,想想时间就很长。 由于服务器(针对任何页面)提供的初始 HTML 不包含任何特定于应用程序的 HTML,搜索引擎将该网站视为空白,没有任何内容。因此,尽管你的网站有巨大的流量或相关内容,但它可能不会出现在搜索结果的顶端。

大致脑海里知道 SSR 快,究竟什么快? 如果在服务器上执行 jsHTML 在服务端就可以装配好了,返回给浏览器渲染,页面就可以有初步的展示,但是在服务端是没有 Window 没法进行绑定的,因此在客户端还要执行一遍脚本,执行生命周期方法,对事件就行绑定,对 DOM 进行 diff,在这段事件完成之前, 服务端和客户端要执行一套代码,就是同构,以 react 为例,hydrate 不会再重新渲染 HTML.

要解决的问题

同构(Isomorphic rendering),就是服务端和客户端一套代码,服务端去渲染,客户端来负责交互。

路由的同构与数据预取

客户端使用 BrowserRouter,服务端使用 staticRouter,在 node 端没有 history 对象,只是根据请求的路由返回匹配的 React.createElementmatchRoutes 方法实现路由匹配 数据的预取 声明路由的时候把数据请求方法关联到路由中,比如定一个 loadData 方法,然后在查找到路由后就可以判断是否存在 loadData 这个方法。

// routes.ts
const routes = [
  {
    path: "/",
    component: loadable(() => import("./Com")),
    loadData: () => getData(),
  },
];
const loadData = () => {
  const promises: Promise<unknown>[] = [];
  routes.some((route) => {
    const match = matchPath(ctx.request.path, route);
    // 调用定义的获取数据的方法
    if (match && route.loadData) promises.push(route.loadData());
    return match;
  });
  return Promise.all(promises).then(() => {
    return Promise.resolve(
      <StaticRouter>
        <App />
      </StaticRouter>
    );
  });
};

// 预取的数据写入HTML(ejs)

动态加载以及资源获取

使用 loadable 库 主要原因是获取资源映射,当路由匹配到 key 获取 value 资源 塞到 HTML 返回字符串给客户端。并且 dynamic import 在客户端可以用 React.lazy,但在 18 之前不能用,如果用 React.lazyjs 加载并执行之后才能加载对应页面的 bundle,增加了 TTI(首次交互)的时间,即使 react18 可以了资源映射还是需要自己来获取( loadable 逃不掉了),@loadable/webpack-plugin 可以打出来资源映射的 map,交给 ChunkExtractor,思路是首先匹配路由,根据匹配到的路由取相应的映射资源,加载资源

// webpack配置
const LoadablePlugin = require('@loadable/webpack-plugin');

module.exports = {
  module: {
    rules: [],
  },
  plugins: [
    new LoadablePlugin(),
    ...
  ],
};
// 资源映射
const statsFile = path.resolve(__dirname, '../dist/asset/loadable-stats.json'); // 上面的loadaer默认打出来这个名字
...
const extractor = new ChunkExtractor({
  statsFile,
  publicPath: '/',
});
// extractor.getLinkTags(), extractor.getStyleTags(), ...

渲染同构

React.hydrate 水合 这个 api ,对节点进行对比,客户端执行生命周期方法,不会再重新渲染 HTML,比对客户端和服务端的 HTML 节点做 diff,比对结果不一致的时候,HTML 上的属性不会被替换,会把不一样的子节点替换,会抛出指向出错节点 warning,需要手动处理。

WechatIMG106120-tuya

数据同构

吐槽一下比较蛋疼的解释(国内大多数文章会出现的概念),从数据层面,把数据放到 Window 上叫注水,把数据从 Window 取出来叫脱水,属实比较难理解,简单理解就是服务端将数据写到 ejs 的模版里,作为全局变量,服务端就从 Window 上取这个变量,实现数据的同构。

///rendux的数据
// ejs模版
<body>
  <div id="root"><%- html %></div>
  <script type="text/javascript">
    window.REDUX_PRELOAD_DATA = <%- preloadState %>
  </script>
  <%- reload -%>
  <%- scriptTags %>
</body>

// server/app.ts
ejs.renderFile(
    template,
    {
      ...
      // 将preloadState变量写入ejs模版
      preloadState: JSON.stringify(ctx.store.getState()),
      ...
    },
    {},
    (err, str) => {
      ...
    },
  );
});

取到路径,key(路由路径) value(页面所需的资源)

loadable 如何知道的页面路径?

const jsx = extractor.collectChunks(reactApp);其实这块是创建 provider,然后下面的 loadable(() => import(''))相当于 consumer

image-20210809230313872

纸巾一擦,咱继续,继续,接着 wu,接着 tiao。

性能监控

监控 nodejs v8 堆内存,内存超出 80%进行服务降级,以及在时间范围内(可能半小时检查一次,试业务情况而定),将不健康的容器部署到其他实例。 process.memoryUsage() 返回一个对象,描述 Node.js 进程的内存使用量(以字节为单位)。

import { memoryUsage } from "process";

console.log(memoryUsage());
// 打印:
// {
//  rss: 4935680,
//  heapTotal: 1826816,
//  heapUsed: 650472,
//  external: 49879,
//  arrayBuffers: 9386
// }
  • heapTotalheapUsed 指的是 V8 的内存使用情况。
  • external 指的是绑定到 V8 管理的 JavaScript 对象的 C++ 对象的内存使用。
  • rssResident Set Size,是进程在主内存设备(即总分配内存的一个子集)中占用的空间量,包括所有 C++JavaScript对象和代码。
  • arrayBuffers 是指为 ArrayBufferSharedArrayBuffer 分配的内存,包括所有 Node.js Buffer。 这也包含在 external 值中。 当 Node.js 用作嵌入式库时,此值可能是 0,因为在这种情况下可能不会跟踪 ArrayBuffer 的分配。

抛开一切,我们用 Next.js 吧

上面提出的问题,Next.js 都可以完美解决,通用级 SSR 解决方案,虽然上面说了一堆原理,但作为企业级解决方案依然不够,Next.js 大量的代码在处理各种兼容性的问题,作为体量较大的react项目,选取通用性方案更为推荐。

混合渲染

我们在实际业务中常常是部分页面需要 SSR,其余的依旧 CSR。 对于我们自己搭的简易 SSR 可以在配置白名单,做请求匹配的时候, 位于白名单的吐空的 HTML 字符串(仅有 css、js 资源的);或者在 nginx 一层做拦截,白名单转发到 CSR 的地址。 Next.js 为这种混合渲染提供了更为简单的方式,提供 getStaticProps 静态生成的 api,在这里面的请求会被在构建的时候请求好,写入数据到Window。如果没有 export getServerSideProps 方法,就会默认走 SSG 渲染,getServerSideProps 是只会在服务端执行的 api,因此在静态生成的时候不会导出getServerSideProps,下面是静态生成的代码。

export async function getStaticProps() {
  // Call an external API endpoint to get posts.
  // You can use any data fetching library
  const res = await fetch("https://.../posts");
  const posts = await res.json();

  // By returning { props: { posts } }, the Blog component
  // will receive `posts` as a prop at build time
  return {
    props: {
      posts,
    },
  };
}

服务降级

node 服务器不健康的时候,达到毫秒级 CSR,即不需要依赖 对于我们自己搭的框架,可采取 serverclient 分开打包,给 client 打包产出加上 HTML 文件,就可以单独托管了。下图是 nginx 配置样例及优雅降级的原理图,解释下就是用户请求到 nginx,如果服务器正常就会转发到 node 渲染服务器,如果异常返回异常状态码,拦截异常状态码,并重写成 200,转发到 HTML 静态文件服务器。 服务降级

自然而然优雅的服务降级

既然自带 SSG,那么我们的服务降级便可采取他的静态生成,官方提供 next export 命令,可以直接生成 SSG 产出,将每个路由都打出一个 HTML 文件,里面会引入所需要的 cssjs,需要注意的是想要一套代码就需要种植环境变量,因为 SSG 要求是不能暴露getServerSideProps,兼容处理代码如下

// .sh
export NEXT_SSG = SSG
// .tsx
let getServerSideProps =
  process.env.NEXT_SSG === "SSG"
    ? undefined
    : async () => {
        const res = await fetch(`url`);
        const post = await res.json();
        return { props: { name: post?.data?.token } };
      };

export { getServerSideProps };

将打出来的产出丢到 nginx,如下配置

server {
root /www/data;

    location / {
        try_files $uri ;
    }

}

如果取到了返回路径下的 HTML,客户端拿到 HTML 字符串之后再 hydrate,实际上还是 CSR,实现了不走 node 服务器的优雅降级。

WeChat2a1bf8dbab6316ddef9eeb79e1b15ecd-tuya

自带的性能分析

对于 web 应用的性能分析总是绕不开Web Vitals的几大指标,Next.js Analytics 为我们提供了非常方便的 api 来获取这些指标数据。 对于部署在托管在Vercel的项目,在其Analytics tab 页签中就可以看到可视化的指标数据。 对于自托管的项目可以通过也是可以进行 web 性能分析的。 仅仅只需要创建一个_app.js 在其中暴露一个名为reportWebVitals的方法。

Next.js 会在完成任何一个指标计算的时候调用该函数。

//_app.js
export function reportWebVitals(metric) {
  console.log(metric);
}
// 打印
// {
//   id: "1628518848412-9295257969280",
//   label: "web-vital",
//   name: "TTFB",
//   startTime: 0,
//   value: 815.5,
// }
  • id:指标唯一的标识符;
  • label: 是指标类型,分别是web-vitalscustom
  • name:指标名称;
  • startTime: 以毫秒为单位,所有记录该指标的时间戳;
  • value: 以毫秒为单位,指标的值或者持续的时间。

web-Vitals

是谷歌提出的用来统一衡量web页面用户体验和质量的指标。Next.js为我们提供了一下五种指标数据:

  • 首字节时间TTFB
  • 首次内容绘制FCP
  • 衡量加载性能LCP
  • 衡量可交互性FID
  • 衡量视觉稳定性CLS

custom

这是Next.js提供的独有的指标,用来衡量 hydraterender 时间

  • Next.js-hydration:页面开始和完成hydrate所需的时间(以毫秒为单位)
  • Next.js-route-change-to-render:页面在路由改变后到开始渲染的时间(以毫秒为单位)
  • Next.js-render: 路由更改后到页面完成渲染的时间(以毫秒为单位)

通过这个函数我们就可以创建自己的性能分析报告,这还不香么!

src=http___i0.hdslb.com_bfs_article_2229244e224ca19d6753fe495d37055b48b47d72.jpg&refer=http___i0.hdslb-tuya

其他

动态加载、动态路由匹配等 next10 均已经支持,需要的可以移步文档哦~ www.nextjs.cn/docs/gettin…

试情况选择 serverless

什么是 serverless 呢,广义来说,自动扩容 按需计费 noops 无需运维符合这些就算是了,本来 serverless 就是很抽象的概念,大致步骤是 SSR 应用放在函数中,serverless 有触发器,选用 http 触发器,触发器中可以添加路由,接收到的路由传递给 Next.js,再返回 HTML 给客户端。

Serverless 能解决什么问题?

Serverless 可以使应用在服务端免运维。在没有流量的时候缩容为 0,节省流量。可以节省不少开支。是性价比高的方案。 对于落地页可能在特定情况流量增多,以及边缘服务等,使用 Serverless+SSR 可谓是完美配合~。 啥叫免运维呢,将一个服务 部署在服务商给我们提供的 运行环境中,不需要关心运维相关的东西,只需要关心业务代码,我们也不需要维护物理机 虚拟机 之类的 Linux。 各大云服务厂商有封装好的 Next.js 服务,可以简单操作快速部署 Next.js

有需要可以自行搜索哦,就不贴了~

总结

介绍了 ssr、ssg 是啥 解决了什么问题、原理以及性能监控,通用级别 ssr 框架 nextjs 如何做优雅服务降级,(是不是已经跃跃欲试想实操),nextjs 依旧在持续更新中并在几天前发布了 11,支持 module federation,微前端也可以用 nextjs(手动狗头)。

番外

我是萱酱,是个 lo 娘 FE(鼓励师划掉),路过的朋友给个三连叭~你的支持是萱酱创作的动力(说的我都感动了),我会持续更新~