服务器端渲染
此包通过提供一种将 HTML 片段注入应用程序初始 HTML 响应的<head>
和/或<body>
中的机制,实现了对 Meteor 应用程序中服务器端渲染的通用支持。
用法
此包导出一个名为onPageLoad
的函数,该函数接受一个回调函数,该回调函数将在页面加载时(在客户端)或每次发生新请求时(在服务器上)被调用。
回调接收一个sink
对象,该对象是ClientSink
或ServerSink
的实例,具体取决于环境。两种类型的sink
具有相同的方法,尽管服务器版本仅接受 HTML 字符串作为内容,而客户端版本还接受 DOM 节点。
{Client,Server}Sink
对象的当前接口如下所示
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
的基本示例
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} />
));
});
同样在客户端
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 包
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-components 的renderToNodeStream
示例。请注意,使用sheet.interleaveWithNodeStream
而不是sink.appendToHead(sheet.getStyleTags());
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
对象从请求中提取信息。
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`
);
}
}
});