Use Vite para crear un proyecto SSR de renderizado del lado del servidor de alta disponibilidad

En los primeros días del desarrollo web, la gente todavía usaba la antigua sintaxis de plantilla de JSP para escribir páginas frontales y luego colocar directamente el archivo JSP en el servidor, completar los datos en el servidor y representar el contenido completo de la página. , puedes Se dice que la práctica de esa época era la representación natural del lado del servidor. Sin embargo, con la madurez de la tecnología AJAX y el surgimiento de varios marcos front-end (como Vue y React), el modo de desarrollo de separación entre front-end y back-end se ha convertido gradualmente en la norma. para el desarrollo de la interfaz de usuario y la lógica de la página, mientras que el servidor solo es responsable de proporcionar interfaces de datos.La representación de la página bajo este método de desarrollo también se denomina representación del lado del cliente (Client Side Render, conocida como CSR).

Sin embargo, también existen ciertos problemas en el renderizado del lado del cliente, como la carga lenta de la primera pantalla y poco amigable con el SEO, por lo que SSR (Server Side Render), es decir, la tecnología de renderizado del lado del servidor, surgió con los tiempos. Requerir Mientras conserva la pila de tecnología CSR, también puede resolver varios problemas de CSR.

1. Concepto básico de RSS

Primero, analicemos el problema de la RSC, su estructura de producto HTML es generalmente la siguiente.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title></title>
  <link rel="stylesheet" href="xxx.css" />
</head>
<body>
  <!-- 一开始没有页面内容 -->
  <div id="root"></div>
  <!-- 通过 JS 执行来渲染页面 -->
  <script src="xxx.chunk.js"></script>
</body>
</html>

A continuación, repasemos brevemente el proceso de representación del navegador. El siguiente es un diagrama esquemático simple.
inserte la descripción de la imagen aquí

Cuando el navegador obtiene el contenido HTML anterior, en realidad no puede representar el contenido completo de la página, porque básicamente solo hay un nodo div vacío en el cuerpo en este momento, y no se completa el contenido real de la página. Luego, el navegador comienza a descargar y ejecutar el código JS, y la página completa solo se puede representar después de la inicialización del marco, la solicitud de datos, la inserción de DOM y otras operaciones. Es decir, el contenido completo de la página en la CSR se representa esencialmente después de la ejecución del código JS. Esto causa problemas de dos maneras:

  • La velocidad de carga de la primera pantalla es relativamente lenta . La carga de la primera pantalla depende de la ejecución de JS. La descarga y ejecución de JS pueden ser operaciones que consumen mucho tiempo, especialmente en algunos escenarios con una red deficiente o máquinas de gama baja sensibles al rendimiento.
  • No es compatible con SEO (optimización de motores de búsqueda) . El HTML de la página no tiene un contenido de página específico, por lo que los rastreadores de los motores de búsqueda no pueden obtener información de palabras clave, lo que afecta la clasificación del sitio web.

Entonces, ¿cómo resuelve SSR estos problemas? En primer lugar, en el escenario SSR, el servidor genera contenido HTML completo y lo devuelve directamente al navegador. El navegador puede representar el contenido completo de la primera pantalla de acuerdo con el HTML sin depender de la carga de JS, lo que puede, hasta cierto punto, reduce el tiempo de renderizado de la primera pantalla y, por otro lado, también puede mostrar el contenido completo de la página a los rastreadores de los motores de búsqueda, lo que favorece el SEO.

Por supuesto, SSR solo puede generar el contenido y la estructura de la página, y no puede completar el enlace de eventos. Por lo tanto, es necesario ejecutar el script JS de CSR en el navegador para completar el enlace de eventos y hacer que la página sea interactiva. Este proceso se llama hidratar (traducido como inyección de agua o activación). Al mismo tiempo, una aplicación que utiliza representación del lado del servidor + hidrato del lado del cliente también se denomina aplicación isomorfa.

2. Ciclo de vida de RSS

Dijimos que SSR generará el contenido HTML completo por adelantado en el lado del servidor, entonces, ¿cómo funciona esto?

En primer lugar, es necesario asegurarse de que el código del front-end se pueda ejecutar normalmente después de compilarlo y colocarlo en el servidor. En segundo lugar, los componentes del front-end se procesan en el servidor para generar y ensamblar el HTML de la aplicación. Esto involucra los dos ciclos de vida principales de las aplicaciones SSR: el tiempo de compilación y el tiempo de ejecución; es mejor que lo solucionemos con cuidado.

Tiempo de construcción

La fase de construcción de SSR hace principalmente lo siguiente:

  1. Solucionar problemas . Además del proceso de construcción original, es necesario agregar el proceso de construcción de SSR, específicamente, necesitamos generar otro producto en formato CommonJS para que pueda cargarse normalmente en Node.js. Por supuesto, a medida que el soporte de Node.js para ESM se vuelve más y más maduro, también podemos reutilizar el código en el formato ESM front-end Esta es también la idea de la construcción de SSR de Vite en la etapa de desarrollo.
    inserte la descripción de la imagen aquí

  2. Eliminar la importación de código de estilo . La importación directa de una línea de CSS es realmente imposible en el lado del servidor, porque Node.js no puede analizar el contenido de CSS. La excepción es el caso de los Módulos CSS, así:

import styles from './index.module.css'


//styles 是一个对象,如{ "container": "xxx" },而不是 CSS 代码
console.log(styles)

3. Confiar en la externalización (externa) . Para algunas dependencias de terceros, no necesitamos usar la versión compilada, sino leer directamente desde node_modules, como react-dom, para que estas dependencias no se creen durante el proceso de compilación de SSR, lo que acelera enormemente la construcción de SSR.

tiempo de ejecución

Para el tiempo de ejecución de SSR, generalmente se puede dividir en etapas de ciclo de vida relativamente fijas. En pocas palabras, se puede organizar en las siguientes etapas principales:
inserte la descripción de la imagen aquí

Estas etapas se explican en detalle a continuación:

  1. Cargue el módulo de entrada SSR . En esta etapa, debemos determinar la entrada del producto de compilación SSR, es decir, dónde está la entrada del componente y cargar el módulo correspondiente.
  2. Realizar precarga de datos . En este momento, el lado del Nodo consultará la base de datos o la solicitud de red para obtener los datos requeridos por la aplicación.
  3. Renderice el componente . Esta etapa es el núcleo de SSR, que principalmente representa los componentes cargados en el paso 1 en secuencias o secuencias HTML.
  4. Empalme de HTML . Después de representar el componente, debemos concatenar la cadena HTML completa y devolverla al navegador como respuesta.

Se puede encontrar que SSR solo se puede realizar mediante la cooperación entre la compilación y el tiempo de ejecución. En otras palabras, las herramientas de compilación por sí solas no son suficientes. Por lo tanto, desarrollar un complemento de Vite no puede implementar estrictamente la capacidad de SSR. Necesitamos hacer algunos cambios. al proceso de compilación de Vite. Se pueden realizar algunos ajustes generales y agregar algo de lógica de tiempo de ejecución del lado del servidor.

3. Construyendo un proyecto SSR basado en Vite

3.1 API de compilación de SSR

¿Cómo apoya Vite la construcción SSR como herramienta de construcción? En otras palabras, ¿cómo permite que el código front-end se ejecute correctamente en Node.js?

Aquí hay dos casos para explicar. En el entorno de desarrollo, Vite aún se adhiere al concepto de módulo ESM que se carga a pedido, es decir, sin paquete, y proporciona la API ssrLoadModule externamente. Puede pasar la ruta del archivo de entrada a ssrLoadModule sin necesidad de empaquetar el proyecto:

// 加载服务端入口模块
const xxx = await vite.ssrLoadModule('/src/entry-server.tsx')

En el entorno de producción, Vite empaquetará de forma predeterminada y generará el producto en formato CommonJS para la construcción de SSR. Podemos agregar instrucciones de compilación similares a package.json:

{
  "build:ssr": "vite build --ssr 服务端入口路径"
}

De esta forma, Vite empaquetará un producto de compilación específicamente para SSR. Se puede ver que Vite nos ha brindado soluciones listas para usar para la mayoría de las cosas en la construcción de SSR.

3.2 Construcción del Proyecto

A continuación, comience oficialmente la construcción del proyecto SSR. Puede inicializar un proyecto de react+ts a través de scaffolding. El comando es el siguiente.

npm init vite
npm i

Abra el proyecto, elimine src/main.ts que viene con el proyecto y cree dos archivos de entrada entry-client.tsx y entry-server.tsx en el directorio src. Entre ellos, el código de entry-client.tsx es el siguiente:

// entry-client.ts
// 客户端入口文件
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'


ReactDOM.hydrate(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
)

El código de entry-server.ts es el siguiente:

// entry-server.ts
// 导出 SSR 组件入口
import App from "./App";
import './index.css'


function ServerEntry(props: any) {
  return (
    <App/>
  );
}


export { ServerEntry };

A continuación, tomamos el marco Express como ejemplo para implementar el servicio de backend Node, y la lógica SSR posterior se conectará a este servicio. Por supuesto, necesita instalar las siguientes dependencias:

npm i express -S
npm i @types/express -D

A continuación, cree un nuevo archivo ssr-server/index.ts en el directorio src, el código es el siguiente:

// src/ssr-server/index.ts
// 后端服务
import express from 'express';


async function createServer() {
  const app = express();
  
  app.listen(3000, () => {
    console.log('Node 服务器已启动~')
    console.log('http://localhost:3000');
  });
}


createServer();

Luego, agregue la siguiente secuencia de comandos en package.json:

{
  "scripts": {
    // 开发阶段启动 SSR 的后端服务
    "dev": "nodemon --watch src/ssr-server --exec 'esno src/ssr-server/index.ts'",
    // 打包客户端产物和 SSR 产物
    "build": "npm run build:client && npm run build:server",
    "build:client": "vite build --outDir dist/client",
    "build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server",
    // 生产环境预览 SSR 效果
    "preview": "NODE_ENV=production esno src/ssr-server/index.ts"
  },
}

Entre ellas, se necesitan dos herramientas adicionales en el proyecto, déjame explicarte:

  • nodemon: una herramienta que monitorea los cambios de archivos y reinicia automáticamente los servicios de Node.
  • esno: una herramienta similar a ts-node, utilizada para ejecutar archivos ts, la capa inferior se basa en Esbuild.

Instalemos estos dos complementos primero:

npm i esno nodemon -D

Ahora, se ha construido el esqueleto básico del proyecto, y luego solo debemos centrarnos en la lógica de implementación del tiempo de ejecución de SSR.

3.3 Implementación del tiempo de ejecución de SSR

Como un servicio back-end especial, SSR se puede encapsular en un formato de middleware, que es mucho más conveniente para usar más adelante. El código es el siguiente:

import express, { RequestHandler, Express } from 'express';
import { ViteDevServer } from 'vite';


const isProd = process.env.NODE_ENV === 'production';
const cwd = process.cwd();


async function createSsrMiddleware(app: Express): Promise<RequestHandler> {
  let vite: ViteDevServer | null = null;
  if (!isProd) { 
    vite = await (await import('vite')).createServer({
      root: process.cwd(),
      server: {
        middlewareMode: 'ssr',
      }
    })
    // 注册 Vite Middlewares
    // 主要用来处理客户端资源
    app.use(vite.middlewares);
  }
  return async (req, res, next) => {
    // SSR 的逻辑
    // 1. 加载服务端入口模块
    // 2. 数据预取
    // 3. 「核心」渲染组件
    // 4. 拼接 HTML,返回响应
  };
}


async function createServer() {
  const app = express();
  // 加入 Vite SSR 中间件
  app.use(await createSsrMiddleware(app));


  app.listen(3000, () => {
    console.log('Node 服务器已启动~')
    console.log('http://localhost:3000');
  });
}


createServer();

A continuación, nos enfocamos en la implementación lógica del SSR en el middleware, primero, el primer paso es cargar el módulo de entrada del servidor, el código es el siguiente:

async function loadSsrEntryModule(vite: ViteDevServer | null) {
  // 生产模式下直接 require 打包后的产物
  if (isProd) {
    const entryPath = path.join(cwd, 'dist/server/entry-server.js');
    return require(entryPath);
  } 
  // 开发环境下通过 no-bundle 方式加载
  else {
    const entryPath = path.join(cwd, 'src/entry-server.tsx');
    return vite!.ssrLoadModule(entryPath);
  }
}

Entre ellos, la lógica en el middleware es la siguiente:

async function createSsrMiddleware(app: Express): Promise<RequestHandler> {
  // 省略前面的代码
  return async (req, res, next) => {
    const url = req.originalUrl;
    // 1. 服务端入口加载
    const { ServerEntry } = await loadSsrEntryModule(vite);
    // ...
  }
}

A continuación, implementemos la operación de obtención previa de datos en el lado del servidor. Puede agregar una función simple para obtener datos en entry-server.tsx. El código es el siguiente:

export async function fetchData() {
  return { user: 'xxx' }
}

A continuación, la operación de obtención previa de datos se puede completar en el middleware SSR.

// src/ssr-server/index.ts
async function createSsrMiddleware(app: Express): Promise<RequestHandler> {
  // 省略前面的代码
  return async (req, res, next) => {
    const url = req.originalUrl;
    // 1. 服务端入口加载
    const { ServerEntry, fetchData } = await loadSsrEntryModule(vite);
    // 2. 预取数据
    const data = await fetchData();
  }
}

A continuación, ingresamos a la etapa de renderizado del componente central:

// src/ssr-server/index.ts
import { renderToString } from 'react-dom/server';
import React from 'react';


async function createSsrMiddleware(app: Express): Promise<RequestHandler> {
  // 省略前面的代码
  return async (req, res, next) => {
    const url = req.originalUrl;
    // 1. 服务端入口加载
    const { ServerEntry, fetchData } = await loadSsrEntryModule(vite);
    // 2. 预取数据
    const data = await fetchData();
    // 3. 组件渲染 -> 字符串
    const appHtml = renderToString(React.createElement(ServerEntry, { data }));
  }
}

Dado que obtuvimos el componente de entrada después del primer paso, ahora podemos llamar a renderToStringAPI del marco frontal para representar el componente como una cadena, y así se genera el contenido específico del componente. A continuación, también debemos proporcionar las ranuras correspondientes en el HTML bajo el directorio raíz para facilitar el reemplazo de contenido.

// index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="root"><!-- SSR_APP --></div>
    <script type="module" src="/src/entry-client.tsx"></script>
    <!-- SSR_DATA -->
  </body>
</html>

A continuación, complementamos la lógica de empalme de HTML en el middleware SSR.

// src/ssr-server/index.ts
function resolveTemplatePath() {
  return isProd ?
    path.join(cwd, 'dist/client/index.html') :
    path.join(cwd, 'index.html');
}


async function createSsrMiddleware(app: Express): Promise<RequestHandler> {
  // 省略之前的代码
  return async (req, res, next) => {
    const url = req.originalUrl;
    // 省略前面的步骤
    // 4. 拼接完整 HTML 字符串,返回客户端
    const templatePath = resolveTemplatePath();
    let template = await fs.readFileSync(templatePath, 'utf-8');
    // 开发模式下需要注入 HMR、环境变量相关的代码,因此需要调用 vite.transformIndexHtml
    if (!isProd && vite) {
      template = await vite.transformIndexHtml(url, template);
    }
    const html = template
      .replace('<!-- SSR_APP -->', appHtml)
      // 注入数据标签,用于客户端 hydrate
      .replace(
        '<!-- SSR_DATA -->',
        `<script>window.__SSR_DATA__=${JSON.stringify(data)}</script>`
      );
    res.status(200).setHeader('Content-Type', 'text/html').end(html);
  }
}

En la lógica de empalmar HTML, además de agregar el contenido específico de la página, también inyectamos una etiqueta de script que monta datos globales.¿Para qué sirve esto?

Mencionamos en el concepto básico de SSR que para activar la función interactiva de la página, necesitamos ejecutar el código JavaScript de la CSR para realizar la operación de hidratación, y cuando el cliente se hidrata, necesita sincronizar los datos precargados con el servidor para garantizar que la página El resultado representado sea consistente con la representación del lado del servidor, por lo que la etiqueta de secuencia de comandos de datos que acabamos de inyectar es útil. Dado que los datos precargados por el servidor se montan en la ventana global, podemos obtener estos datos en entry-client.tsx, que es la entrada de representación del cliente, e hidratarlos.

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'


// @ts-ignore
const data = window.__SSR_DATA__;


ReactDOM.hydrate(
  <React.StrictMode>
    <App data={data}/>
  </React.StrictMode>,
  document.getElementById('root')
)

Ahora, básicamente hemos desarrollado la lógica del núcleo SSR y luego ejecutamos el comando npm run dev para iniciar el proyecto.
inserte la descripción de la imagen aquí

Después de abrir el navegador y ver el código fuente de la página, puede encontrar que el HTML generado por SSR se ha devuelto correctamente, como se muestra en la figura a continuación.
inserte la descripción de la imagen aquí

3.4 Procesamiento de recursos de RSE en el entorno de producción

Si ahora ejecutamos npm run build y npm run preview para obtener una vista previa del entorno de producción, encontraremos que SSR puede devolver contenido normalmente, pero todos los recursos estáticos y códigos CSR no son válidos.
inserte la descripción de la imagen aquí

Sin embargo, no existe tal problema en la etapa de desarrollo. Esto se debe a que el middleware de Vite Dev Server ya nos ha ayudado a manejar los recursos estáticos en la etapa de desarrollo, y todos los recursos en el entorno de producción han sido empaquetados. Necesitamos habilitar un servicio de recursos estáticos independiente para alojar estos recursos.

Para este tipo de problema, podemos usar middleware de servicio estático para completar este servicio. Primero, sobresalte e instale el paquete de terceros correspondiente:

npm i serve-static -S

A continuación, vamos al servidor para registrarnos.

// 过滤出页面请求
function matchPageUrl(url: string) {
  if (url === '/') {
    return true;
  }
  return false;
}


async function createSsrMiddleware(app: Express): Promise<RequestHandler> {
  return async (req, res, next) => {
    try {
      const url = req.originalUrl;
      if (!matchPageUrl(url)) {
        // 走静态资源的处理
        return await next();
      }
      // SSR 的逻辑省略
    } catch(e: any) {
      vite?.ssrFixStacktrace(e);
      console.error(e);
      res.status(500).end(e.message);
    }
  }
}


async function createServer() {
  const app = express();
  // 加入 Vite SSR 中间件
  app.use(await createSsrMiddleware(app));


  // 注册中间件,生产环境端处理客户端资源
  if (isProd) {
    app.use(serve(path.join(cwd, 'dist/client')))
  }
  // 省略其它代码
}

De esta manera, hemos resuelto el problema de la falla de recursos estáticos en el entorno de producción. Sin embargo, en circunstancias normales, cargaremos los recursos estáticos en la CDN y configuraremos la base de Vite como un prefijo de nombre de dominio, de modo que podamos acceder directamente a los recursos estáticos a través de la CDN sin agregar procesamiento del lado del servidor.

4. Problemas de ingeniería

Anteriormente, básicamente nos hemos dado cuenta de las funciones de construcción y tiempo de ejecución del núcleo SSR, e inicialmente podemos ejecutar un proyecto SSR basado en Vite, pero todavía hay muchos problemas de ingeniería que requieren nuestra atención en escenarios reales.

4.1 Gestión de enrutamiento

En el escenario SPA, generalmente existen diferentes soluciones de administración de enrutamiento para diferentes marcos front-end, como vue-router en Vue y react-router en React. Pero en el análisis final, las funciones realizadas por el esquema de enrutamiento en el proceso SSR son similares.

  • Le dice al marco qué rutas renderizar en este momento. En Vue, podemos usar router.push para determinar la ruta a representar, y en React, usar StaticRouter con el parámetro de ubicación para completar.
  • Establezca el prefijo base. Especifica el prefijo de la ruta, como el parámetro base en vue-router y el nombre base del componente StaticRouter en react-router.

4.2 Gestión del Estado

Para la gestión del estado global, existen diferentes ecologías y soluciones para diferentes marcos, como Vuex y Pinia en Vue, Redux y Recoil en React. El uso de cada herramienta de administración de estado no es el enfoque de este artículo. La idea de conectarse a SSR es relativamente simple. En la etapa de recuperación previa de datos, inicialice la tienda en el lado del servidor, almacene los datos obtenidos de forma asíncrona en la tienda, y luego transfiera los datos de la tienda a la etapa HTML. Sáquelo y colóquelo en la etiqueta del script de datos, y finalmente acceda a los datos precargados a través de la ventana cuando el cliente esté hidratado.

4.3 Rebaja de RSE

En algunos casos extremos, debemos recurrir a la CSR, que es la representación del lado del cliente. En términos generales, se incluyen los siguientes escenarios a la baja:

  • El servidor no pudo precargar los datos y necesita degradar al cliente para obtener datos.
  • El servidor tiene una excepción y necesita devolver la plantilla de CSR de abajo hacia arriba y degradarla por completo a una CSR.
  • Para el desarrollo local y la depuración, a veces es necesario omitir SSR y solo realizar CSR.

Para el primer caso, debe haber una lógica para volver a adquirir datos en el archivo de entrada del cliente, y podemos hacer las siguientes adiciones.

// entry-client.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'


async function fetchData() {
  // 客户端获取数据
}




async fucntion hydrate() {
  let data;
  if (window.__SSR_DATA__) {
    data = window.__SSR_DATA__;
  } else {
    // 降级逻辑 
    data = await fetchData();
  }
  // 也可简化为 const data = window.__SSR_DATA__ ?? await fetchData();
  ReactDOM.hydrate(
    <React.StrictMode>
      <App data={data}/>
    </React.StrictMode>,
    document.getElementById('root')
  )
}

Para el segundo escenario, es decir, el servidor ejecuta un error, podemos agregar la lógica de prueba/captura a la lógica del middleware SSR anterior.

async function createSsrMiddleware(app: Express): Promise<RequestHandler> {
  return async (req, res, next) => {
    try {
      // SSR 的逻辑省略
    } catch(e: any) {
      vite?.ssrFixStacktrace(e);
      console.error(e);
      // 在这里返回浏览器 CSR 模板内容
    }
  }
}

Para el tercer caso, podemos forzar la omisión de SSR pasando el parámetro de consulta de URL de ?csr y agregar la siguiente lógica en el middleware de SSR.

async function createSsrMiddleware(app: Express): Promise<RequestHandler> {
  return async (req, res, next) => {
    try {
      if (req.query?.csr) {
        // 响应 CSR 模板内容
        return;
      }
      // SSR 的逻辑省略
    } catch(e: any) {
      vite?.ssrFixStacktrace(e);
      console.error(e);
    }
  }
}

4.4 Compatibilidad con la API del navegador

Debido a que las API, como la ventana y el documento en el navegador, no se pueden usar en Node.js, una vez que dichas API se ejecuten en el lado del servidor, se informará el siguiente error:

imagen.png

Para este problema, primero podemos usar la variable de entorno integrada de Vite import.meta.env.SSR para determinar si está en el entorno SSR, para evitar que la API del navegador aparezca en el lado del servidor del código comercial.

if (import.meta.env.SSR) {
  // 服务端执行的逻辑
} else {
  // 在此可以访问浏览器的 API
}

Por supuesto, también podemos inyectar API de navegador en Node a través de polyfill, para que estas API puedan ejecutarse normalmente y resolver los problemas anteriores. Recomiendo usar una biblioteca jsdom de polyfill relativamente madura, que se usa de la siguiente manera:

const jsdom = require('jsdom');
const { window } = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`);
const { document } = window;
// 挂载到 node 全局
global.window = window;
global.document = document;

4.5 Cabeza personalizada

En el proceso de SSR, aunque podemos decidir el contenido del componente, es decir

El contenido en el contenedor div, pero para el contenido de la cabeza en el HTML no podemos decidir en función del estado interno del componente. Sin embargo, el casco de reacción en el ecosistema React y la biblioteca vue-meta en el ecosistema Vue están diseñados para resolver tales problemas, permitiéndonos escribir directamente algunas etiquetas Head en los componentes y luego obtener el estado interno de los componentes en el lado del servidor.

Tome el ejemplo de react-helmet para ilustrar:

// 前端组件逻辑
import { Helmet } from "react-helmet";


function App(props) {
  const { data } = props;
  return {
    <div>
       <Helmet>
        <title>{ data.user }的页面</title>
        <link rel="canonical" href="http://mysite.com/example" />
      </Helmet>
    </div>
  }
}
// 服务端逻辑
import Helmet from 'react-helmet';


// renderToString 执行之后
const helmet = Helmet.renderStatic();
console.log("title 内容: ", helmet.title.toString());
console.log("link 内容: ", helmet.link.toString())

Después de iniciar el servicio y visitar la página, podemos encontrar que el terminal puede imprimir la información que queremos. De esta forma, podemos determinar el contenido de Head según el estado del componente y luego insertar el contenido en la plantilla durante la etapa de empalme de HTML.

4.6 Representación de transmisión

La capa inferior de diferentes marcos front-end se ha dado cuenta de la capacidad de transmisión de procesamiento, es decir, responder mientras se procesa, en lugar de esperar a que se procese todo el árbol de componentes antes de responder. Esto puede hacer que la respuesta llegue al navegador con anticipación y mejore el rendimiento de carga de la primera pantalla. El renderToNodeStream en Vue y el renderToNodeStream en React se dan cuenta de la capacidad de renderizado de transmisión, y el uso general es el siguiente:

import { renderToNodeStream } from 'react-dom/server';


// 返回一个 Nodejs 的 Stream 对象
const stream = renderToNodeStream(element);
let html = ''


stream.on('data', data => {
  html += data.toString()
  // 发送响应
})


stream.on('end', () => {
  console.log(html) // 渲染完成
  // 发送响应
})


stream.on('error', err => {
  // 错误处理
})

Sin embargo, si bien el renderizado en streaming mejora el rendimiento de la primera pantalla, también nos trae algunas limitaciones: Si necesitamos rellenar algún contenido relacionado con el estado del componente en HTML, no se puede utilizar el renderizado en streaming. Por ejemplo, para el contenido de encabezado personalizado en react-helmet, incluso si la información de encabezado se recopila cuando se procesa el componente, en el procesamiento de transmisión, la parte de encabezado del HTML se ha enviado al navegador en este momento, y esta parte de el contenido de la respuesta no se puede cambiar, por lo que react-helmet fallará durante SSR.

4.7 caché SSR

SSR es una operación típica de uso intensivo de la CPU.Para reducir la carga de la máquina en línea tanto como sea posible, configurar el caché es un vínculo muy importante. Cuando SSR se está ejecutando, el contenido almacenado en caché se puede dividir en varias partes:

  • Caché de lectura de archivos . Evite las operaciones de lectura de disco repetidas tanto como sea posible y reutilice los resultados almacenados en caché tanto como sea posible para cada E/S de disco. Como se muestra en el siguiente código:
     function createMemoryFsRead() {
  const fileContentMap = new Map();
  return async (filePath) => {
    const cacheResult = fileContentMap.get(filePath);
    if (cacheResult) {
      return cacheResult;
    }
    const fileContent = await fs.readFile(filePath);
    fileContentMap.set(filePath, fileContent);
    return fileContent;
  }
}


const memoryFsRead = createMemoryFsRead();
memoryFsRead('file1');
// 直接复用缓存
memoryFsRead('file1');
  • Caché de datos de captación previa . Para algunos datos de interfaz con bajo rendimiento en tiempo real, podemos adoptar una estrategia de almacenamiento en caché para reutilizar los resultados de la obtención previa de datos cuando llegue la misma solicitud la próxima vez, de modo que varios consumos de E/S en el proceso de obtención previa de datos también se puedan reducir a un hasta cierto punto Reduzca el tiempo hasta la primera pantalla.
  • Caché de representación HTML . El contenido HTML empalmado es el foco del almacenamiento en caché. Si esta parte se puede almacenar en caché, después del siguiente golpe de caché, se puede guardar una serie de consumo como renderToString y empalme HTML, y el beneficio de rendimiento del servidor será más obvio. .

Para el contenido del caché anterior, la ubicación específica del caché puede ser:

  • memoria del servidor . Si se coloca en la memoria, es necesario considerar el mecanismo de eliminación de caché para evitar el tiempo de inactividad del servicio causado por el exceso de memoria.Una solución típica de eliminación de caché es lru-cache (basada en el algoritmo LRU).
  • Base de datos Redis. Es equivalente al procesamiento de caché con la idea de diseño de un servidor back-end tradicional.
  • servicio CDN . Podemos almacenar en caché el contenido de la página en el servicio CDN y, cuando llegue la misma solicitud la próxima vez, usar el contenido almacenado en caché en la CDN en lugar de consumir los recursos del servidor de origen.

4.8 Supervisión del rendimiento

En los proyectos reales de SSR, a menudo nos encontramos con algunos problemas de rendimiento en línea de SSR.Sin un mecanismo completo de supervisión del rendimiento, será difícil encontrar y solucionar problemas. Para los datos de rendimiento de SSR, hay algunos indicadores comunes:

  • Tiempo de carga del producto SSR
  • Tiempo de precarga de datos
  • Cuando el componente se renderiza
  • El tiempo completo desde que el servidor recibe la solicitud hasta la respuesta.
  • Acierto de caché de SSR
  • Tasa de éxito de SSR, registro de errores

Podemos usar la herramienta perf_hooks para completar la recopilación de datos, como se muestra en el siguiente código:

import { performance, PerformanceObserver } from 'perf_hooks';


// 初始化监听器逻辑
const perfObserver = new PerformanceObserver((items) => {
  items.getEntries().forEach(entry => { 
    console.log('[performance]', entry.name, entry.duration.toFixed(2), 'ms');
  });
  performance.clearMarks();
});


perfObserver.observe({ entryTypes: ["measure"] })


// 接下来我们在 SSR 进行打点
// 以 renderToString  为例
performance.mark('render-start');
// renderToString 代码省略
performance.mark('render-end');
performance.measure('renderToString', 'render-start', 'render-end');

Luego iniciamos el servicio y la visita, y podemos ver la información de registro de RBI. De manera similar, podemos recopilar los indicadores de otras etapas a través de los métodos anteriores como registros de rendimiento, por otro lado, en el entorno de producción, generalmente necesitamos combinar plataformas de monitoreo de rendimiento específicas para administrar y reportar los indicadores anteriores. servicio de vigilancia

4.9 GSS/ISR/SPR

A veces, para algunos sitios estáticos (como blogs, documentos), no hay datos que cambien dinámicamente, por lo que no es necesario utilizar la representación del lado del servidor. En este punto, solo necesita generar HTML completo para la implementación durante la fase de construcción.Este método de generar HTML durante la fase de construcción también se denomina SSG (Static Site Generation, generación de sitios estáticos).

La mayor diferencia entre SSG y SSR es que el tiempo para generar HTML ha cambiado desde el tiempo de ejecución de SSR hasta el tiempo de compilación, pero el proceso central del ciclo de vida no ha cambiado:

imagen.png

El siguiente es un código de implementación simple:

// scripts/ssg.ts
// 以下的工具函数均可以从 SSR 流程复用
async function ssg() {
  // 1. 加载服务端入口
  const { ServerEntry, fetchData } = await loadSsrEntryModule(null);
  // 2. 数据预取
  const data = await fetchData();
  // 3. 组件渲染
  const appHtml = renderToString(React.createElement(ServerEntry, { data }));
  // 4. HTML 拼接
  const template = await resolveTemplatePath();
  const templateHtml = await fs.readFileSync(template, 'utf-8');
  const html = templateHtml
  .replace('<!-- SSR_APP -->', appHtml)
  .replace(
    '<!-- SSR_DATA -->',
    `<script>window.__SSR_DATA__=${JSON.stringify(data)}</script>`
  ); 
  // 最后,我们需要将 HTML 的内容写到磁盘中,将其作为构建产物
  fs.mkdirSync('./dist/client', { recursive: true });
  fs.writeFileSync('./dist/client/index.html', html);
}


ssg();

Luego, agregue una secuencia de comandos npm de este tipo a package.json para usarla.

{
  "scripts": {
    "build:ssg": "npm run build && NODE_ENV=production esno scripts/ssg.ts"  
  }
}

De esta manera, inicialmente nos dimos cuenta de la lógica de SSG. Por supuesto, además de SSG, hay algunos otros modos de renderizado que circulan en la industria, como SPR e ISR, que suenan más altos, pero en realidad son solo funciones nuevas derivadas de SSR y SSG. Aquí hay una breve explicación para ti. :

  • SPR significa Serverless Pre Render, que implementa servicios SSR en un entorno Serverless (FaaS) para realizar la expansión y contracción automáticas de instancias de servidor y reducir el costo de operación y mantenimiento del servidor.
  • ISR significa Representación incremental del sitio, que mueve parte de la lógica de SSG del tiempo de construcción al tiempo de ejecución de SSR, y resuelve el problema de la construcción de SSG que consume mucho tiempo para una gran cantidad de páginas.

Supongo que te gusta

Origin blog.csdn.net/xiangzhihong8/article/details/131426226
Recomendado
Clasificación