Skip to content

服务器渲染

¥Server Rendering

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

¥This package implements generic support for server-side rendering in Meteor apps, by providing a mechanism for injecting fragments of HTML into the <head> and/or <body> of the application's initial HTML response.

用法

¥Usage

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

¥This package exports a function named onPageLoad which takes a callback function that will be called at page load (on the client) or whenever a new request happens (on the server).

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

¥The callback receives a sink object, which is an instance of either ClientSink or ServerSink depending on the environment. Both types of sink have the same methods, though the server version accepts only HTML strings as content, whereas the client version also accepts DOM nodes.

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

¥The current interface of {Client,Server}Sink objects is as follows:

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")。

¥The sink object may also expose additional properties depending on the environment. For example, on the server, sink.request provides access to the current request object, and sink.arch identifies the target architecture of the pending HTTP response (e.g. "web.browser").

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

¥Here is a basic example of onPageLoad usage on the server:

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

客户端上同样如此:

¥Likewise on the client:

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 函数实现(如上面的客户端情况)。

¥Note that the onPageLoad callback function is allowed to return a Promise if it needs to do any asynchronous work, and thus may be implemented by an async function (as in the client case above).

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

¥Note also that the client example does not end up calling any methods of the sink object, because ReactDOM.hydrate has its own similar API. In fact, you are not even required to use the onPageLoad API on the client, if you have your own ideas about how the client should do its rendering.

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

¥Here is a more complicated example of onPageLoad usage on the server, involving the styled-components npm package:

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>

¥In this example, the callback not only renders the <App /> element into the element with id="app", but also appends any <style> tag(s) generated during rendering to the <head> of the response document.

虽然这些示例都涉及 React,但 onPageLoad API 被设计为对任何类型的服务器端渲染都通用。

¥Although these examples have all involved React, the onPageLoad API is designed to be generically useful for any kind of server-side rendering.

流式 HTML

¥Streaming HTML

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

¥React 16 introduced renderToNodeStream, which enables the reading of rendered HTML in chunks. This reduces the TTFB (time to first byte).

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

¥Here is a renderToNodeStream example using styled-components. Note the use of sheet.interleaveWithNodeStream instead of 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);
});

从请求中获取数据

¥Getting data from the request

在某些情况下,你希望根据请求的 URL 自定义元标记或其他响应内容,例如,如果你正在加载应用中特定产品的页面,则可能希望包含 社交预览 的图片和描述。

¥In some cases you want to customize meta tags or something else in your response based in the requested URL, for example, if your are loading a page with a specific product in your app maybe you want to include an image and a description for social previews.

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

¥You can extract information from the request using the sink object.

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