跳至内容

服务器端渲染

此包通过提供一种将 HTML 片段注入应用程序初始 HTML 响应的<head> 和/或<body> 中的机制,实现了对 Meteor 应用程序中服务器端渲染的通用支持。

用法

此包导出一个名为onPageLoad 的函数,该函数接受一个回调函数,该回调函数将在页面加载时(在客户端)或每次发生新请求时(在服务器上)被调用。

回调接收一个sink 对象,该对象是ClientSinkServerSink 的实例,具体取决于环境。两种类型的sink 具有相同的方法,尽管服务器版本仅接受 HTML 字符串作为内容,而客户端版本还接受 DOM 节点。

{Client,Server}Sink 对象的当前接口如下所示

js
class Sink {
  // Appends content to the <head>.
  appendToHead(content)

  // Appends content to the <body>.
  appendToBody(content)

  // Appends content to the identified element.
  appendToElementById(id, content)

  // Replaces the content of the identified element.
  renderIntoElementById(id, content)

  // Redirects request to new location.
  redirect(location, code)


  // server only methods

  // sets the status code of the response.
  setStatusCode(code)

  // sets a header of the response.
  setHeader(key, value)

  // gets request headers
  getHeaders()

  // gets request cookies
  getCookies()
}

根据环境,sink 对象还可以公开其他属性。例如,在服务器上,sink.request 提供对当前request 对象的访问权限,而sink.arch 标识挂起 HTTP 响应的目标架构(例如,“web.browser”)。

以下是在服务器上使用onPageLoad 的基本示例

js
import from "react";
import { renderToString } from "react-dom/server";
import { onPageLoad } from "meteor/server-render";

import App from "/imports/Server.js";

onPageLoad(sink => {
  sink.renderIntoElementById("app", renderToString(
    <App location={sink.request.url} />
  ));
});

同样在客户端

js
import React from "react";
import ReactDOM from "react-dom";
import { onPageLoad } from "meteor/server-render";

onPageLoad(async (sink) => {
  const App = (await import("/imports/Client.js")).default;
  ReactDOM.hydrate(<App />, document.getElementById("app"));
});

请注意,如果onPageLoad 回调函数需要执行任何异步工作,则允许它返回一个Promise,因此可以通过async 函数实现(如上面的客户端案例)。

还要注意,客户端示例最终没有调用sink 对象的任何方法,因为ReactDOM.hydrate 具有其自己的类似 API。事实上,如果您对客户端如何进行渲染有自己的想法,则甚至不需要在客户端上使用onPageLoad API。

以下是在服务器上使用onPageLoad 的更复杂的示例,涉及styled-components npm 包

js
import React from "react";
import { onPageLoad } from "meteor/server-render";
import { renderToString } from "react-dom/server";
import { ServerStyleSheet } from "styled-components";
import App from "/imports/Server";

onPageLoad((sink) => {
  const sheet = new ServerStyleSheet();
  const html = renderToString(
    sheet.collectStyles(<App location={sink.request.url} />)
  );

  sink.renderIntoElementById("app", html);
  sink.appendToHead(sheet.getStyleTags());
});

在此示例中,回调不仅将<App /> 元素渲染到具有id="app" 的元素中,而且还将渲染过程中生成的任何<style> 标签附加到响应文档的<head> 中。

尽管这些示例都涉及 React,但onPageLoad API 旨在普遍适用于任何类型的服务器端渲染。

流式 HTML

React 16 引入了renderToNodeStream,它允许分块读取渲染的 HTML。这减少了TTFB(第一个字节的时间)。

以下是一个使用styled-componentsrenderToNodeStream 示例。请注意,使用sheet.interleaveWithNodeStream 而不是sink.appendToHead(sheet.getStyleTags());

js
import React from "react";
import { onPageLoad } from "meteor/server-render";
import { renderToNodeStream } from "react-dom/server";
import { ServerStyleSheet } from "styled-components";
import App from "/imports/Server";

onPageLoad((sink) => {
  const sheet = new ServerStyleSheet();
  const appJSX = sheet.collectStyles(<App location={sink.request.url} />);
  const htmlStream = sheet.interleaveWithNodeStream(renderToNodeStream(appJSX));
  sink.renderIntoElementById("app", htmlStream);
});

从请求中获取数据

在某些情况下,您希望根据请求的 URL 自定义元标签或响应中的其他内容,例如,如果您正在加载应用程序中具有特定产品的页面,则可能希望包含一个图像和一个描述,用于社交预览

您可以使用sink 对象从请求中提取信息。

js
import { onPageLoad } from "meteor/server-render";

const getBaseUrlFromHeaders = (headers) => {
  const protocol = headers["x-forwarded-proto"];
  const { host } = headers;
  // we need to have '//' to findOneByHost work as expected
  return `${protocol ? `${protocol}:` : ""}//${host}`;
};

const getContext = (sink) => {
  // more details about this implementation here
  // https://github.com/meteor/meteor/issues/9765
  const { headers, url, browser } = sink.request;
  // no useful data will be found for galaxybot requests
  if (browser && browser.name === "galaxybot") {
    return null;
  }

  // when we are running inside cordova we don't want to resolve meta tags
  if (url && url.pathname && url.pathname.includes("cordova/")) {
    return null;
  }

  const baseUrl = getBaseUrlFromHeaders(headers);
  const fullUrl = `${baseUrl}${url.pathname || ""}`;

  return { baseUrl, fullUrl };
};

onPageLoad((sink) => {
  const { baseUrl, fullUrl } = getContext(sink);

  // product URL contains /product on it
  const urlParseArray = fullUrl.split("/");

  const productPosition = urlParseArray.indexOf("product");
  const productId =
    productPosition !== -1 &&
    urlParseArray[productPosition + 1].replace("?", "");
  const product = productId && ProductsCollection.findOne(productId);

  const productTitle = product && `Buy now ${product.name}, ${product.price}`;
  if (productTitle) {
    sink.appendToHead(`<title>${productTitle}</title>\n`);
    sink.appendToHead(`<meta property="og:title" content="${productTitle}">\n`);
    if (product.imageUrl) {
      sink.appendToHead(
        `<meta property="og:image" content="${product.imageUrl}">\n`
      );
    }
  }
});