Utilisez Vite pour créer un projet SSR de rendu côté serveur hautement disponible

Au tout début du développement Web, les gens utilisaient encore l'ancienne syntaxe de modèle de JSP pour écrire des pages frontales, puis mettaient directement le fichier JSP sur le serveur, remplissaient les données sur le serveur et rendaient le contenu complet de la page. , vous pouvez On dit que la pratique de cette époque était le rendu naturel côté serveur. Cependant, avec la maturité de la technologie AJAX et l'essor de divers frameworks front-end (tels que Vue et React), le mode de développement de séparation front-end et back-end est progressivement devenu la norme. pour le développement de l'interface utilisateur et de la logique de la page, tandis que le serveur n'est responsable que de la fourniture des interfaces de données.Le rendu de la page dans le cadre de cette méthode de développement est également appelé rendu côté client (Client Side Render, appelé CSR).

Cependant, il existe également certains problèmes de rendu côté client, tels que le chargement lent du premier écran et peu convivial pour le référencement.. Par conséquent, SSR (Server Side Render), c'est-à-dire la technologie de rendu côté serveur, a émergé au fil du temps. Tout en conservant la pile technologique RSE, il peut également résoudre divers problèmes de RSE.

1. Concept de base de la RSS

Analysons d'abord le problème de la RSE, dont la structure produit HTML est généralement la suivante.

<!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>

Passons maintenant brièvement en revue le processus de rendu du navigateur.Ce qui suit est un diagramme schématique simple.
insérez la description de l'image ici

Lorsque le navigateur obtient le contenu HTML ci-dessus, il ne peut pas réellement restituer le contenu complet de la page, car il n'y a essentiellement qu'un nœud div vide dans le corps à ce moment-là, et aucun contenu de page réel n'est rempli. Ensuite, le navigateur commence à télécharger et à exécuter le code JS, et la page complète ne peut être rendue qu'après l'initialisation du cadre, la demande de données, l'insertion DOM et d'autres opérations. Autrement dit, le contenu complet de la page dans le CSR est essentiellement rendu après l'exécution du code JS. Cela pose des problèmes de deux manières :

  • La vitesse de chargement du premier écran est relativement lente . Le chargement du premier écran dépend de l'exécution de JS. Le téléchargement et l'exécution de JS peuvent prendre beaucoup de temps, en particulier dans certains scénarios avec un réseau médiocre ou des machines bas de gamme sensibles aux performances.
  • Non optimisé pour le référencement (optimisation des moteurs de recherche) . Le code HTML de la page n'a pas de contenu de page spécifique, de sorte que les robots des moteurs de recherche ne peuvent pas obtenir d'informations sur les mots clés, ce qui affecte le classement du site Web.

Alors, comment la SSR résout-elle ces problèmes ? Tout d'abord, dans le scénario SSR, le serveur génère un contenu HTML complet et le renvoie directement au navigateur. Le navigateur peut restituer le contenu complet du premier écran selon le code HTML sans compter sur le chargement JS, qui peut dans une certaine mesure réduit le temps de rendu du premier écran, et d'autre part, il peut également afficher le contenu complet de la page aux crawlers des moteurs de recherche, ce qui est propice au référencement.

Bien sûr, SSR ne peut que générer le contenu et la structure de la page, et ne peut pas terminer la liaison d'événements. Par conséquent, il est nécessaire d'exécuter le script JS de CSR dans le navigateur pour terminer la liaison d'événements et rendre la page interactive. Ce processus est appelé hydrater (traduit par injection d'eau ou activation). Dans le même temps, une application qui utilise le rendu côté serveur + l'hydrate côté client est également appelée une application isomorphe.

2. Cycle de vie du RSS

Nous avons dit que SSR rendrait le contenu HTML complet à l'avance côté serveur, alors comment cela fonctionne-t-il ?

Tout d'abord, il faut s'assurer que le code du front-end peut s'exécuter normalement après avoir été compilé et placé sur le serveur.Deuxièmement, les composants du front-end sont rendus sur le serveur pour générer et assembler le HTML de l'application. Cela implique les deux cycles de vie majeurs des applications SSR : le temps de construction et le temps d'exécution, autant faire le tri avec soin.

temps de construction

La phase de construction de SSR fait principalement les choses suivantes :

  1. Correction du problème . En plus du processus de construction d'origine, il faut ajouter le processus de construction SSR, en particulier, nous devons générer un autre produit au format CommonJS afin qu'il puisse être chargé normalement dans Node.js. Bien sûr, comme le support de Node.js lui-même pour ESM devient de plus en plus mature, on peut aussi réutiliser le code au format ESM front-end, c'est aussi l'idée de la construction SSR de Vite en phase de développement.
    insérez la description de l'image ici

  2. Supprimez l'importation du code de style . Importer directement une ligne de css est en fait impossible côté serveur, car Node.js ne peut pas parser le contenu du CSS. L'exception est le cas des modules CSS, comme suit :

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


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

3. S'appuyer sur l'externalisation (externe) . Pour certaines dépendances tierces, nous n'avons pas besoin d'utiliser la version construite, mais de lire directement à partir de node_modules, comme react-dom, afin que ces dépendances ne soient pas construites pendant le processus de construction SSR, accélérant ainsi considérablement la construction de SSR.

Durée

Pour l'exécution de la SSR, elle peut généralement être divisée en étapes de cycle de vie relativement fixes. En termes simples, elle peut être organisée selon les étapes principales suivantes :
insérez la description de l'image ici

Ces étapes sont expliquées en détail ci-dessous :

  1. Chargez le module d'entrée SSR . À ce stade, nous devons déterminer l'entrée du produit de construction SSR, c'est-à-dire où se trouve l'entrée du composant, et charger le module correspondant.
  2. Effectuer une prélecture des données . À ce moment, le côté nœud interrogera la base de données ou la requête réseau pour obtenir les données requises par l'application.
  3. Rendre le composant . Cette étape est au cœur de SSR, qui restitue principalement les composants chargés à l'étape 1 dans des chaînes HTML ou des flux.
  4. Épissage HTML . Une fois le composant rendu, nous devons concaténer la chaîne HTML complète et la renvoyer au navigateur en tant que réponse.

On peut constater que SSR ne peut être réalisé que par la coopération entre la construction et l'exécution. En d'autres termes, les outils de construction seuls ne suffisent pas. Par conséquent, le développement d'un plug-in Vite ne peut pas strictement implémenter la capacité SSR. Nous devons apporter quelques modifications au processus de construction de Vite. Certains ajustements généraux et l'ajout d'une logique d'exécution côté serveur peuvent être réalisés.

3. Construire un projet SSR basé sur Vite

3.1 API de construction SSR

Comment Vite soutient-il la construction SSR en tant qu'outil de construction ? En d'autres termes, comment permet-il au code frontal de s'exécuter avec succès dans Node.js ?

Voici deux cas à expliquer. Dans l'environnement de développement, Vite adhère toujours au concept de chargement de module ESM à la demande, c'est-à-dire sans bundle, et fournit l'API ssrLoadModule en externe. Vous pouvez transmettre le chemin du fichier d'entrée à ssrLoadModule sans besoin de packager le projet :

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

Dans l'environnement de production, Vite empaquetera par défaut et sortira le produit au format CommonJS pour la construction SSR. Nous pouvons ajouter des instructions de construction similaires à package.json :

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

De cette façon, Vite emballera un produit de construction spécifiquement pour SSR. On peut voir que Vite nous a fourni des solutions prêtes à l'emploi pour la plupart des choses dans la construction SSR.

3.2 Construction du projet

Ensuite, démarrez officiellement la construction du projet SSR. Vous pouvez initialiser un projet react+ts via un échafaudage. La commande est la suivante.

npm init vite
npm i

Ouvrez le projet, supprimez le fichier src/main.ts fourni avec le projet et créez deux fichiers d'entrée entry-client.tsx et entry-server.tsx dans le répertoire src. Parmi eux, le code d'entry-client.tsx est le suivant :

// 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')
)

Le code de entry-server.ts est le suivant :

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


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


export { ServerEntry };

Ensuite, nous prenons le framework Express comme exemple pour implémenter le service backend Node, et la logique SSR ultérieure sera connectée à ce service. Bien sûr, vous devez installer les dépendances suivantes :

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

Créez ensuite un nouveau fichier ssr-server/index.ts dans le répertoire src, le code est le suivant :

// 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();

Ensuite, ajoutez le script de scripts suivant dans 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"
  },
}

Parmi eux, deux outils supplémentaires sont nécessaires dans le projet, laissez-moi vous expliquer :

  • nodemon : un outil qui surveille les modifications de fichiers et redémarre automatiquement les services Node.
  • esno : un outil similaire à ts-node, utilisé pour exécuter des fichiers ts, la couche inférieure est basée sur Esbuild.

Installons d'abord ces deux plugins :

npm i esno nodemon -D

Maintenant, le squelette de base du projet a été construit, et nous n'avons plus qu'à nous concentrer sur la logique d'implémentation du runtime SSR.

3.3 Implémentation de l'environnement d'exécution SSR

En tant que service back-end spécial, SSR peut être encapsulé dans une forme middleware, ce qui est beaucoup plus pratique à utiliser par la suite.Le code est le suivant :

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();

Ensuite, nous nous concentrons sur l'implémentation logique du SSR dans le middleware. Tout d'abord, la première étape consiste à charger le module d'entrée du serveur. Le code est le suivant :

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);
  }
}

Parmi eux, la logique dans le middleware est la suivante :

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

Ensuite, implémentons l'opération de préchargement des données côté serveur. Vous pouvez ajouter une fonction simple pour obtenir des données dans entry-server.tsx. Le code est le suivant :

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

Ensuite, l'opération de prélecture des données peut être effectuée dans le 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();
  }
}

Ensuite, nous entrons dans l'étape de rendu du composant principal :

// 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 }));
  }
}

Puisque nous avons obtenu le composant d'entrée après la première étape, nous pouvons maintenant appeler le renderToStringAPI du framework frontal pour restituer le composant sous forme de chaîne, et le contenu spécifique du composant est ainsi généré. Ensuite, nous devons également fournir des emplacements correspondants dans le HTML sous le répertoire racine pour faciliter le remplacement du contenu.

// 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>

Ensuite, nous complétons la logique d'épissage HTML dans le 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);
  }
}

Dans la logique du splicing HTML, en plus d'ajouter le contenu spécifique de la page, on injecte également une balise script qui monte les données globales.

Nous avons mentionné dans le concept de base de SSR que pour activer la fonction interactive de la page, nous devons exécuter le code JavaScript du CSR pour effectuer l'opération d'hydratation, et lorsque le client hydrate, il doit synchroniser les données prérécupérées avec le serveur pour s'assurer que la page Le résultat rendu est cohérent avec le rendu côté serveur, de sorte que la balise de script de données que nous venons d'injecter est très pratique. Étant donné que les données prérécupérées par le serveur sont montées sur la fenêtre globale, nous pouvons obtenir ces données dans l'entrée-client.tsx, qui est l'entrée de rendu du client, et les hydrater.

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')
)

Maintenant, nous avons essentiellement développé la logique du noyau SSR, puis exécuté la commande npm run dev pour démarrer le projet.
insérez la description de l'image ici

Après avoir ouvert le navigateur et affiché le code source de la page, vous pouvez constater que le code HTML généré par SSR a été renvoyé avec succès, comme indiqué dans la figure ci-dessous.
insérez la description de l'image ici

3.4 Traitement des ressources CSR dans l'environnement de production

Si nous exécutons maintenant npm run build et npm run preview pour prévisualiser l'environnement de production, nous constaterons que SSR peut renvoyer le contenu normalement, mais toutes les ressources statiques et les codes CSR ne sont pas valides.
insérez la description de l'image ici

Cependant, il n'y a pas de problème de ce type dans la phase de développement, car le middleware de Vite Dev Server nous a déjà aidés à gérer les ressources statiques dans la phase de développement, et toutes les ressources de l'environnement de production ont été packagées. un service de ressources statiques distinct pour héberger ces ressources.

Pour ce genre de problème, nous pouvons utiliser le middleware serve-static pour compléter ce service. Commencez par sursauter et installez le package tiers correspondant :

npm i serve-static -S

Ensuite, nous allons sur le serveur pour nous inscrire.

// 过滤出页面请求
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 cette manière, nous avons résolu le problème de la défaillance des ressources statiques dans l'environnement de production. Cependant, dans des circonstances normales, nous téléchargeons les ressources statiques sur le CDN et configurons la base de Vite en tant que préfixe de nom de domaine, afin que nous puissions accéder directement aux ressources statiques via le CDN sans ajouter de traitement côté serveur.

4. Problèmes d'ingénierie

Ci-dessus, nous avons essentiellement réalisé les fonctions de construction et d'exécution du noyau SSR, et pouvons initialement exécuter un projet SSR basé sur Vite, mais il reste encore de nombreux problèmes d'ingénierie qui nécessitent notre attention dans les scénarios réels.

4.1 Gestion du routage

Dans le scénario SPA, il existe généralement différentes solutions de gestion du routage pour différents frameworks frontaux, tels que vue-router dans Vue et react-router dans React. Mais en dernière analyse, les fonctions exécutées par le schéma de routage dans le processus SSR sont similaires.

  • Indique au framework les itinéraires à rendre pour le moment. Dans Vue, nous pouvons utiliser router.push pour déterminer la route à rendre, et dans React, utiliser StaticRouter avec le paramètre location pour terminer.
  • Définissez le préfixe de base. Spécifie le préfixe du chemin, tel que le paramètre de base dans vue-router et le nom de base du composant StaticRouter dans react-router.

4.2 Gestion des états

Pour la gestion globale de l'état, il existe différentes écologies et solutions pour différents frameworks, tels que Vuex et Pinia dans Vue, Redux et Recoil dans React. L'utilisation de chaque outil de gestion d'état n'est pas l'objet de cet article. L'idée de se connecter au SSR est relativement simple. Dans l'étape de prélecture des données, initialiser le magasin côté serveur, stocker les données obtenues de manière asynchrone dans le magasin, puis transférez les données du magasin vers l'étape HTML. Sortez-les et mettez-les dans la balise de script de données, et enfin accédez aux données prérécupérées par la fenêtre lorsque le client est hydraté.

4.3 Déclassement RSE

Dans certains cas extrêmes, nous devons recourir à la RSE, qui est le rendu côté client. De manière générale, les scénarios de rétrogradation suivants sont inclus :

  • Le serveur n'a pas réussi à préextraire les données et doit rétrograder vers le client pour obtenir des données.
  • Le serveur a une exception et doit renvoyer le modèle CSR ascendant et le rétrograder complètement en CSR.
  • Pour le développement local et le débogage, il est parfois nécessaire d'ignorer le SSR et d'effectuer uniquement le CSR.

Pour le premier cas, il faut qu'il y ait une logique pour réacquérir les données dans le fichier d'entrée des clients, et nous pouvons faire les ajouts suivants.

// 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')
  )
}

Pour le deuxième scénario, c'est-à-dire que le serveur exécute une erreur, nous pouvons ajouter une logique try/catch à la logique middleware SSR précédente.

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

Pour le troisième cas, nous pouvons forcer le saut SSR en passant le paramètre de requête d'url de ?csr, et ajouter la logique suivante dans le middleware 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 Compatibilité de l'API du navigateur

Étant donné que les API telles que window et document dans le navigateur ne peuvent pas être utilisées dans Node.js, une fois ces API exécutées côté serveur, l'erreur suivante sera signalée :

image.png

Pour ce problème, nous pouvons d'abord utiliser la variable d'environnement intégrée Vite import.meta.env.SSR pour déterminer si elle se trouve dans l'environnement SSR, afin d'éviter que l'API du navigateur n'apparaisse du côté serveur du code métier.

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

Bien sûr, nous pouvons également injecter des API de navigateur dans Node via polyfill, afin que ces API puissent fonctionner normalement et résoudre les problèmes ci-dessus. Je recommande d'utiliser une bibliothèque polyfill relativement mature jsdom, qui est utilisée comme suit :

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 Tête personnalisée

Dans le processus de SSR, bien que nous puissions décider du contenu du composant, c'est-à-dire

Le contenu dans le conteneur div, mais pour le contenu de la tête dans le HTML, nous ne pouvons pas décider en fonction de l'état interne du composant. Cependant, le casque de réaction de l'écosystème React et la bibliothèque vue-meta de l'écosystème Vue sont conçus pour résoudre de tels problèmes, nous permettant d'écrire directement des balises Head dans les composants, puis d'obtenir l'état interne des composants sur le du côté serveur.

Prenons l'exemple du casque de réaction pour illustrer :

// 前端组件逻辑
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())

Après avoir démarré le service et visité la page, nous pouvons constater que le terminal peut imprimer les informations souhaitées. De cette manière, nous pouvons déterminer le contenu Head en fonction de l'état du composant, puis insérer le contenu dans le modèle lors de l'étape de fusion HTML.

4.6 Rendu en continu

La couche inférieure de différents frameworks frontaux a réalisé la capacité de rendu en continu, c'est-à-dire de répondre pendant le rendu, au lieu d'attendre que l'intégralité de l'arborescence des composants soit rendue avant de répondre.Cela peut faire en sorte que la réponse atteigne le navigateur à l'avance et améliore les performances de chargement du premier écran. Le renderToNodeStream dans Vue et le renderToNodeStream dans React réalisent tous deux la capacité de rendu en continu, et l'utilisation générale est la suivante :

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 => {
  // 错误处理
})

Cependant, si le rendu en streaming améliore les performances du premier écran, il nous apporte également quelques restrictions : Si nous devons renseigner du contenu lié à l'état du composant en HTML, le rendu en streaming ne peut pas être utilisé. Par exemple, pour le contenu d'en-tête personnalisé dans react-helmet, même si les informations d'en-tête sont collectées lors du rendu du composant, dans le rendu en continu, la partie d'en-tête du HTML a été envoyée au navigateur à ce moment-là, et cette partie de le contenu de la réponse ne peut pas être modifié, donc le casque de réaction échouera pendant le SSR.

4.7 Cache SSR

Le SSR est une opération typique gourmande en CPU.Afin de réduire au maximum la charge de la machine en ligne, la configuration du cache est un maillon très important. Lorsque SSR est en cours d'exécution, le contenu mis en cache peut être divisé en plusieurs parties :

  • Cache de lecture de fichiers . Évitez autant que possible les opérations de lecture de disque répétées et réutilisez autant que possible les résultats mis en cache pour chaque E/S de disque. Comme indiqué dans le code suivant :
     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');
  • Prérécupérer le cache de données . Pour certaines données d'interface avec de faibles performances en temps réel, nous pouvons adopter une stratégie de mise en cache pour réutiliser les résultats de la prélecture des données lorsque la même requête arrive la prochaine fois, de sorte que diverses consommations d'E/S dans le processus de prélecture des données peuvent également être réduites à un dans une certaine mesure, réduire le temps d'accès au premier écran.
  • Cache de rendu HTML . Le contenu HTML épissé est au centre de la mise en cache. Si cette partie peut être mise en cache, après le prochain accès au cache, une série de consommations telles que renderToString et l'épissage HTML peuvent être enregistrées, et l'avantage en termes de performances du serveur sera plus évident. .

Pour le contenu du cache ci-dessus, l'emplacement spécifique du cache peut être :

  • mémoire du serveur . S'il est placé dans la mémoire, il est nécessaire de considérer le mécanisme d'élimination du cache pour éviter les temps d'arrêt du service causés par une mémoire excessive.Une solution typique d'élimination du cache est lru-cache (basé sur l'algorithme LRU).
  • Base de données Redis. Cela équivaut à traiter le cache avec l'idée de conception d'un serveur principal traditionnel.
  • Service CDN . Nous pouvons mettre en cache le contenu de la page sur le service CDN, et lorsque la même demande arrive la prochaine fois, utiliser le contenu mis en cache sur le CDN au lieu de consommer les ressources du serveur source.

4.8 Surveillance des performances

Dans les projets SSR réels, nous rencontrons souvent des problèmes de performances en ligne SSR. Sans un mécanisme complet de surveillance des performances, il sera difficile de trouver et de résoudre les problèmes. Pour les données de performance SSR, il existe des indicateurs communs :

  • Temps de chargement du produit SSR
  • Temps de prélecture des données
  • Lorsque le composant s'affiche
  • Le temps complet entre le serveur recevant la demande et la réponse
  • Accès au cache SSR
  • Taux de réussite SSR, journal des erreurs

Nous pouvons utiliser l'outil perf_hooks pour terminer la collecte de données, comme indiqué dans le code suivant :

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');

Ensuite, nous démarrons le service et visitons, et nous pouvons voir les informations du journal RBI. De même, nous pouvons collecter les indicateurs d'autres étapes via les méthodes ci-dessus sous forme de journaux de performances ; d'autre part, dans l'environnement de production, nous devons généralement combiner des plates-formes de suivi des performances spécifiques pour gérer et rapporter les indicateurs ci-dessus. service de surveillance.

4.9 SSG/ISR/SPR

Parfois, pour certains sites statiques (tels que les blogs, les documents), aucune donnée à modification dynamique n'est impliquée, nous n'avons donc pas besoin d'utiliser le rendu côté serveur. À ce stade, il vous suffit de générer du HTML complet pour le déploiement lors de la phase de construction.Cette méthode de génération de HTML lors de la phase de construction est également appelée SSG (Static Site Generation, génération de site statique).

La plus grande différence entre SSG et SSR est que le temps de génération de HTML a changé entre l'exécution de SSR et le temps de construction, mais le processus de cycle de vie principal n'a pas changé :

image.png

Voici un code d'implémentation 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();

Ensuite, ajoutez un tel morceau de scripts npm à package.json pour l'utiliser.

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

De cette façon, nous avons initialement réalisé la logique de SSG. Bien sûr, en plus de SSG, il existe d'autres modes de rendu qui circulent dans l'industrie, tels que SPR et ISR, qui sonnent plus haut, mais en fait ce ne sont que de nouvelles fonctions dérivées de SSR et SSG. Voici une brève explication pour vous :

  • SPR signifie Serverless Pre Render, qui déploie des services SSR dans un environnement Serverless (FaaS) pour réaliser une expansion et une contraction automatiques des instances de serveur et réduire le coût d'exploitation et de maintenance du serveur.
  • ISR signifie Incremental Site Rendering, qui déplace une partie de la logique SSG du temps de construction au temps d'exécution SSR, et résout le problème de la construction fastidieuse du SSG pour un grand nombre de pages.

Je suppose que tu aimes

Origine blog.csdn.net/xiangzhihong8/article/details/131426226
conseillé
Classement