Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SSR 原理 ,以及 SSG 的多种实现 #51

Open
chiyan-lin opened this issue Dec 29, 2024 · 0 comments
Open

SSR 原理 ,以及 SSG 的多种实现 #51

chiyan-lin opened this issue Dec 29, 2024 · 0 comments

Comments

@chiyan-lin
Copy link
Owner

chiyan-lin commented Dec 29, 2024

页面渲染流程

  1. 浏览器通过请求得到一个HTML文本
  2. 渲染进程解析HTML文本,构建DOM树
  3. 解析HTML的同时,如果遇到内联样式或者样式脚本,则下载并构建样式规则(stytle rules),若遇到JavaScript脚本,则会下载执行脚本。
  4. DOM树和样式规则构建完成之后,渲染进程将两者合并成渲染树(render tree)
  5. 渲染进程开始对渲染树进行布局,生成布局树(layout tree)
  6. 渲染进程对布局树进行绘制,生成绘制记录
  7. 渲染进程的对布局树进行分层,分别栅格化每一层,并得到合成帧
  8. 渲染进程将合成帧信息发送给GPU进程显示到页面中
    页面的渲染其实就是浏览器将HTML文本转化为页面帧的过程

服务端渲染

说到服务端渲染,会先想问客户端渲染是什么

客户端渲染是我们最常用的渲染方式了,我们用的 vue ,react 这些框架实现的页面,整个加载js脚本和执行js脚本最后再展示出来UI的过程就是客户端渲染

服务端渲染就是在浏览器请求页面URL的时候,服务端将我们需要的HTML文本组装好,并返回给浏览器,这个HTML文本被浏览器解析之后,不需要经过 JavaScript 脚本的执行,即可直接构建出希望的 DOM 树并展示到页面中

为什么要用服务端渲染

SEO

网页爬虫也分低级爬虫和高级爬虫。

  • 低级爬虫:只请求URL,URL返回的HTML是什么内容就爬什么内容。
  • 高级爬虫:请求URL,加载并执行JavaScript脚本渲染页面,爬JavaScript渲染后的内容。
    也就是说,低级爬虫对客户端渲染的页面来说,简直无能为力,因为返回的HTML是一个空壳,它需要执行 JavaScript 脚本之后才会渲染真正的页面。目前还是有一部分年代老旧的爬虫还属于低级爬虫,使用服务端渲染,对这些低级爬虫更加友好一些,SEO总体上也能更好地提升

白屏时间更短

相对于客户端渲染,服务端渲染在浏览器请求URL之后已经得到了一个带有数据的HTML文本,浏览器只需要解析HTML,直接构建DOM树就可以。而客户端渲染,需要先得到一个空的HTML页面,这个时候页面已经进入白屏,之后还需要经过加载并执行 JavaScript、请求后端服务器获取数据、JavaScript 渲染页面几个过程才可以看到最后的页面。特别是在复杂应用中,由于需要加载 JavaScript 脚本,越是复杂的应用,需要加载的 JavaScript 脚本就越多、越大,这会导致应用的首屏加载时间非常长,进而降低了体验感。

服务端渲染流程

  1. 浏览器请求URL,前端服务器接收到URL请求之后,根据不同的URL,前端服务器向后端服务器请求数据,
  2. 请求完成后,前端服务器会组装一个携带了具体数据的HTML文本,并且返回给浏览器,浏览器得到HTML之后开始渲染页面
  3. 浏览器加载并执行 JavaScript 脚本,给页面上的元素绑定事件,让页面变得可交互,当用户与浏览器页面进行交互(如跳转到下一个页面时,浏览器会执行 JavaScript 脚本,向后端服务器请求数据,获取完数据之后再次执行 JavaScript 代码动态渲染页面)

项目如何改造为SSR

理想的情况,我们肯定希望自己的项目不仅能保持原本的方式运行,又能支持SSR在服务端也正常运行,这时候就需要进行同构

假如现在有一个项目是使用 vue 开发的,一个常规的场景

当我们打开一个页面时,首先是打开这个页面的URL,这个URL,可以通过应用的路由匹配,找到具体的页面,不同的页面有不同的视图

在这个场景中,我们同构的重点就在将以下的几步能在服务端中能执行

1、路由模块的初始化

2、组件的render过程

3、data或者store的挂载

todo

Nuxt

SSR 的集大成框架

**基于文件的路由:**根据pages/目录的结构定义路由。这样可以更容易地组织应用程序,避免手动配置路由的需要。

**内置服务器端渲染:**Nuxt具备内置的服务器端渲染能力,因此你不需要自己设置单独的服务器。

**自动导入:**在各自的目录中编写Vue组件和可组合函数,并在使用时无需手动导入,享受树摇和优化JS捆绑包的好处。

**数据获取工具:**Nuxt提供了可用于处理与服务器端渲染兼容的数据获取的可组合函数,以及不同的策略。

零配置的TypeScript支持: 可以编写类型安全的代码,无需学习TypeScript,因为我们提供了自动生成的类型和tsconfig.json配置文件。

配置好的构建工具: 使用Vite来支持开发中的热模块替换(HMR),以及在生产中将代码打包成符合最佳实践的形式。

性能优化:

**代码分割:**Nuxt自动将代码拆分成较小的块,这有助于减少应用程序的初始加载时间。意味着只有访问特定页面时,才会加载该页面的脚本。这种自动代码分割减少了首屏加载时需要下载的资源量,加快了页面加载速度。

**图片优化:**使用 Nuxt.js 的 @nuxt/image 模块可以自动优化图片大小和格式,根据浏览器的支持情况选择最佳的图片格式,并提供懒加载等特性。图片优化可以显著减少页面体积和加载时间。

**支持vue异步组件:**异步加载某些不是立即需要的组件可以减少初始加载时间。Nuxt.js 支持 Vue.js 的异步组件,允许开发者根据需要动态加载组件。

**页面缓存和数据预取:**在服务端渲染期间预先获取页面所需数据,可以减少客户端请求和等待时间;使用服务器端缓存来存储渲染好的页面,以便于快速响应后续的请求。

流式渲染:传输来的 HTML 不需要等待整个html下载完成、吐出来的 html 流数据在浏览器上可以渐进地渲染出来,呈现给用户。

example

服务端渲染会带来更大的心智负担

  • 代码复杂度增加。为了实现服务端渲染,应用代码中需要兼容服务端和客户端两种运行情况,而一部分依赖的外部扩展库却只能在客户端运行,需要对其进行特殊处理,才能在服务器渲染应用程序中运行。
  • 需要更多的服务器负载均衡。由于服务器增加了渲染HTML的需求,使得原本只需要输出静态资源文件的nodejs服务,新增了数据获取的IO和渲染HTML的CPU占用,如果流量突然暴增,有可能导致服务器down机,因此需要使用响应的缓存策略和准备相应的服务器负载。
  • 涉及构建设置和部署的更多要求。与可以部署在任何静态文件服务器上的完全静态单页面应用程序 (SPA) 不同,服务器渲染应用程序,需要处于 Node.js server 运行环境。
    所以在使用服务端渲染SSR之前,需要开发者考虑投入产出比,比如大部分应用系统都不需要SEO,而且首屏时间并没有非常的慢,如果使用SSR反而小题大做了

SSG

静态站点生成 (Static-Site Generation,缩写为 SSG),也被称为预渲染。如果用服务端渲染一个页面所需的数据对每个用户来说都是相同的,那么我们可以只渲染一次,提前在构建过程中完成,而不是每次请求进来都重新渲染页面。预渲染的页面生成后作为静态 HTML 文件被服务器托管。

如何将自己的站点升级为 SSG

1、使用 nuxt ,执行****nuxt build --prerender

打包后的文件目录树

**2、**VitePress 静态生成

VitePress 是一个静态站点生成器 ,专为构建快速、以内容为中心的站点而设计。简而言之,VitePress 获取用 Markdown 编写的内容,对其应用主题,并生成可以轻松部署到任何地方的静态 HTML 页面。

3、使用****Puppeteer 。在项目构建成功之后,通过无头浏览器,在构建服务器中动态访问指定的路由,并将页面的 html 吐出来

// packages
{
  "name": "innereye_frontend",
  "version": "1.0.0",
  "prerender": ["page1", "page2"]
}

// webpack 插件
PrerenderSPAPlugin.prototype.apply = function (compiler) {
  const { prerender } = getPkgConfig();
  const afterEmit = (compilation, done) => {
    routes.map(
      // 限制 promise 数量的一个小方法,防止太多页面导致内存爆炸
      (route, index) =>
        limiter(async () => {
          const page = await this._puppeteer.newPage();
          // 注入 window 全局对象,标记当前处于预渲染环境
          if (options.inject) {
            await page.evaluateOnNewDocument(
              `(function () { window['${
                options.injectProperty
              }'] = ${JSON.stringify(options.inject)}; })();`
            );
          }
          const baseURL = `http://localhost:${rootOptions.server.port}`;
          // 设置视窗大小
          if (options.viewport) await page.setViewport(options.viewport);
          // 由使用方配置 renderAfterDocumentEvent 并在合适的时候 new Event(renderAfterDocumentEvent)
          if (options.renderAfterDocumentEvent) {
            page.evaluateOnNewDocument(function (options) {
              window["__PRERENDER_STATUS"] = {};
              // https://developer.mozilla.org/zh-CN/docs/Web/API/Event
              // 使用浏览器的原生事件方式,
              document.addEventListener(
                options.renderAfterDocumentEvent,
                () => {
                  // 可以准备把内容导出了
                  window["__PRERENDER_STATUS"].__DOCUMENT_EVENT_RESOLVED = true;
                }
              );
            }, this._rendererOptions);
          }
          // 设置一下网络配置
          const navigationOptions = options.navigationOptions
            ? { waituntil: "networkidle0", ...options.navigationOptions }
            : { waituntil: "networkidle0" };
          // 打开页面
          await page.goto(`${baseURL}${route}`, navigationOptions);
          // Wait for some specific element exists
          const { renderAfterElementExists } = this._rendererOptions;
          if (
            renderAfterElementExists &&
            typeof renderAfterElementExists === "string"
          ) {
            await page.waitForSelector(renderAfterElementExists);
          }
          // 在页面注入一个方法,因为在前面注入了一个
          // 当触发生成的时候,window['__PRERENDER_STATUS'].__DOCUMENT_EVENT_RESOLVED
          // 会被标记为 true ,然后会触发下面这个 promise ,在这个页面执行完成,就有页面内容了
          await page.evaluate(function (options) {
            options = options || {};
            return new Promise((resolve, reject) => {
              if (options.renderAfterDocumentEvent) {
                // 如果很快就产生了文件,就可以直接 resolve 了
                if (
                  window["__PRERENDER_STATUS"] &&
                  window["__PRERENDER_STATUS"].__DOCUMENT_EVENT_RESOLVED
                ) {
                  resolve();
                }
                // 不然就自己再监听一遍
                document.addEventListener(
                  options.renderAfterDocumentEvent,
                  () => resolve()
                );
              }
            });
          }, this._rendererOptions);
          // 产出页面的基本内容
          const rpath = await page.evaluate("window.location.pathname")
          const result = {
            originalRoute: route,
            route: rpath,
            html: await page.content(),
          };
          await page.close();
          compilerFS.writeFile('prerender/' + rpath + '.html', res.html)
        })
    );
  };
  if (compiler.hooks) {
    const plugin = { name: "PrerenderSPAPlugin" };
    compiler.hooks.afterEmit.tapAsync(plugin, afterEmit);
  } else {
    compiler.plugin("after-emit", afterEmit);
  }
};

上述生成之后的文件列表 dist

修改 nginx 的代理配置

// nginx 配置
server {
    listen 80;
    server_name www.damin.com;
    location ~ ^/project_name/(.+)$ {
        # 定义本地文件的路径
        root dist;  # 替换为 pages 文件夹所在的父目录路径
        # 将 URL 的最后一部分映射为文件名
        rewrite ^/project_name/(.+)$ /prerender/$1.html break;
        # 尝试访问文件
        try_files $uri =404;
    }
    # 错误处理(可选)
    error_page 404 /404.html;  # 自定义 404 页面,如果需要
}

如果项目用了 vue-router ,有 about ,home 等 path
在访问到 www.damin.com/xxxxx/home 的时候,就可以被转发到 prerender 返回里面的 html 文件内容了,如果仅仅针对部分路由处理,那就是对部分路由的 ssg


参考资料:

书写友好的 ssr:https://cn.vuejs.org/guide/scaling-up/ssr#writing-ssr-friendly-code

nuxt文档:https://nuxt.com.cn/docs/getting-started/introduction

nuxt3重构实践:https://blog.csdn.net/qq_38974163/article/details/131420984

nuxt3加载优化:https://inx.fun/article/876

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant