Рендеринг на стороне сервера React (изоморфизм внешней и внутренней маршрутизации)

React.js

Веб-приложение получает доступ к определенной HTML-странице через URL-адрес, и каждый URL-адрес соответствует ресурсу. В традиционных веб-приложениях браузер отправляет запрос на сервер через URL-адрес, а сервер считывает ресурс и отправляет обработанное содержимое страницы в браузер, тогда как в одностраничном приложении все изменения URL-адреса обрабатываются на стороне браузера. , При изменении URL-адреса браузер заменяет контент через js. Для серверных приложений, когда запрашивается URL-ресурс, сервер отправляет содержимое страницы, соответствующее URL-адресу, в браузер. Браузер загружает js, на который ссылается страница, и выполняет инициализацию маршрутизации на стороне клиента, а затем выполняются последующие переходы маршрутизации. на стороне браузера серверная сторона отвечает только за первый рендеринг запроса, отправленного из браузера

Первый в ранее построенном проектеsrcСоздайте 4 компонента страницы в каталоге

Затем установите веб-зависимость React react-router-dom

Примечание: react-router-dom версии 4.x

Предыдущий раздел:Строительство проекта

См. адрес исходного кода в конце статьи.

Код сервера в этом разделе был переписан, нажмите, чтобы узнать подробностиздесь

Внешняя маршрутизация

При написании маршрутизации React мы сначала используем самый простой подход, вApp.jsxиспользуется вBrowserRouterКомпонент оборачивает корневой узелNavLinkКомпонент оборачивает текст в тег li

import { 
  BrowserRouter as Router,
  Route,
  Switch,
  Redirect,
  NavLink
} from "react-router-dom";
import Bar from "./views/Bar";
import Baz from "./views/Baz";
import Foo from "./views/Foo";
import TopList from "./views/TopList";
render() {
  return (
    <Router>
      <div>
        <div className="title">This is a react ssr demo</div>
        <ul className="nav">
          <li><NavLink to="/bar">Bar</NavLink></li>
          <li><NavLink to="/baz">Baz</NavLink></li>
          <li><NavLink to="/foo">Foo</NavLink></li>
          <li><NavLink to="/top-list">TopList</NavLink></li>
        </ul>
        <div className="view">
          <Switch>
            <Route path="/bar" component={Bar} />
            <Route path="/baz" component={Baz} />
            <Route path="/foo" component={Foo} />
            <Route path="/top-list" component={TopList} />
            <Redirect from="/" to="/bar" exact />
          </Switch>
        </div>
      </div>
    </Router>
  );
}

Каждое представление маршрута в приведенном выше коде используетRouteЗаполнитель и компоненты, соответствующие представлению маршрутизации, необходимы в текущем компоненте.importЗаходи, если есть вложенность роутинга, то компоненты представления будут разбросаны по разным компонентам, которые будутimport, когда компоненты слишком вложены друг в друга, их будет сложно поддерживать

Затем измените вышеуказанные проблемы, все компоненты представления находятся в файле js.import, экспортировать список объектов конфигурации маршрутизации, соответственно использоватьpathуказать путь маршрутизации,componentУкажите компонент просмотра маршрута

src/router/index.js

import Bar from "../views/Bar";
import Baz from "../views/Baz";
import Foo from "../views/Foo";
import TopList from "../views/TopList";

const router = [
  {
    path: "/bar",
    component: Bar
  },
  {
    path: "/baz",
    component: Baz
  },
  {
    path: "/foo",
    component: Foo
  },
  {
    path: "/top-list",
    component: TopList,
    exact: true
  }
];

export default router;

существуетApp.jsxИмпорт настроенного объекта маршрутизации в , цикл назадRoute

<div className="view">
  <Switch>
    {
      router.map((route, i) => (
        <Route key={i} path={route.path} component={route.component} 
        exact={route.exact} />
      ))
    }
    <Redirect from="/" to="/bar" exact />
  </Switch>
</div>

В сложных приложениях вложение компонентов неизбежно.RouteизcomponentАтрибуты могут передавать не только тип компонента, но и функцию обратного вызова, через которую через функцию обратного вызова передается подмаршрут текущего компонента.propsпройти, затем продолжить цикл

Для поддержки вложенности компонентов мы используемRouteинкапсулироватьNestedRouteкомпоненты

src/router/NestedRoute.jsx

import React from "react";
import { Route } from "react-router-dom";

const NestedRoute = (route) => (
  <Route path={route.path} exact={route.exact}
    /*渲染路由对应的视图组件,将路由组件的props传递给视图组件*/
    render={(props) => <route.component {...props} router={route.routes}/>}
  />
);

export default NestedRoute;

затем изsrc/router/index.jsэкспорт в

import NestedRoute from "./NestedRoute";
...
export {
  router,
  NestedRoute
}

App.jsx

import { router, NestedRoute } from "./router";
<div className="view">
  <Switch>
    {
      router.map((route, i) => (
        <NestedRoute key={i} {...route} />
      ))
    }
    <Redirect from="/" to="/bar" exact />
  </Switch>
</div>

Используйте вложенные маршруты, как показано ниже.

const router = [
  {
    path: "/a",
    component: A
  },
  {
    path: "/b",
    component: B
  },
  {
    path: "/parent",
    component: Parent,
    routes: [
      {
        path: "/child",
        component: Child,
      }
    ]
  }
];

Parent.jsx

this.props.router.map((route, i) => (
  <NestedRoute key={i} {...route} />
))

Внутренняя маршрутизация

В отличие от маршрутизации на стороне клиента, маршрутизация на стороне сервера не имеет состояния. React предоставляет компонент без состоянияStaticRouter,В направленииStaticRouterпередайте адрес, позвонитеReactDOMServer.renderToString()может соответствовать представлению маршрутизации

существуетApp.jsxразличать клиент и сервер, тогдаexportразличные корневые компоненты

let App;
if (process.env.REACT_ENV === "server") {
  // 服务端导出Root组件
  App = Root;
} else {
  App = () => {
    return (
      <Router>
        <Root />
      </Router>
    );
  };
}
export default App;

следующий заentry-server.jsмодифицировать, использоватьStaticRouterОберните корневой компонент, передайте в контекстеcontextиlocation, при использовании функции для создания нового компонента

import React from "react";
import { StaticRouter } from "react-router-dom";
import Root from "./App";

const createApp = (context, url) => {
  const App = () => {
    return (
      <StaticRouter context={context} location={url}>
        <Root/>  
      </StaticRouter>
    )
  }
  return <App />;
}

module.exports = {
  createApp
};

server.jsполучено вcreateAppфункция

let createApp;
let template;
let readyPromise;
if (isProd) {
  let serverEntry = require("../dist/entry-server");
  createApp = serverEntry.createApp;
  template = fs.readFileSync("./dist/index.html", "utf-8");
  // 静态资源映射到dist路径下
  app.use("/dist", express.static(path.join(__dirname, "../dist")));
} else {
  readyPromise = require("./setup-dev-server")(app, (serverEntry, htmlTemplate) => {
    createApp = serverEntry.createApp;
    template = htmlTemplate;
  });
}

Когда сервер обрабатывает запрос, передается текущий URL-адрес, и сервер будет сопоставлять компонент представления, соответствующий текущему URL-адресу.

const render = (req, res) => {
  console.log("======enter server======");
  console.log("visit url: " + req.url);

  let context = {};
  let component = createApp(context, req.url);
  let html = ReactDOMServer.renderToString(component);
  let htmlStr = template.replace("<!--react-ssr-outlet-->", `<div id='app'>${html}</div>`);
  // 将渲染后的html字符串发送给客户端
  res.send(htmlStr);
}

404 и редиректы

Когда запрошенный ресурс сервера не существует, сервер должен отправить ответ 404, маршрут перенаправляется, и серверу также необходимо перенаправить на указанный URL-адрес.StaticRouterПредоставляет реквизит для передачи объекта контекстаcontext, передается при рендеринге маршрутизируемых компонентовstaticContextПолучите и установите код состояния. Когда сервер выполняет рендеринг, код состояния оценивается для обработки ответа. Если перенаправление происходит при отображении маршрута на стороне сервера, передайтеcontextАтрибуты с информацией, связанной с перенаправлением, добавляются автоматически, напримерurl

Для обработки статуса 404 мы инкапсулируем компонент статусаStatusRoute

src/router/StatusRoute.jsx

import React from "react";
import { Route } from "react-router-dom";

const StatusRoute = (props) => (
  <Route render={({staticContext}) => {
    // 客户端无staticContext对象
    if (staticContext) {
      // 设置状态码
      staticContext.status = props.code;
    }
    return props.children;
  }} />
);

export default StatusRoute;

отsrc/router/index.jsэкспорт в

import StatusRoute from "./StatusRoute";
...

export {
  router,
  NestedRoute,
  StatusRoute
}

существуетApp.jsxиспользуется вStatusRouteкомпоненты

<div className="view">
  <Switch>
    {
      router.map((route, i) => (
        <NestedRoute key={i} {...route} />
      ))
    }
    <Redirect from="/" to="/bar" exact />
    <StatusRoute code={404}>
      <div>
        <h1>Not Found</h1>
      </div>
    </StatusRoute>
  </Switch>
</div>

renderФункция изменена следующим образом

let context = {};
let component = createApp(context, req.url);
let html = ReactDOMServer.renderToString(component);

if (!context.status) {  // 无status字段表示路由匹配成功
  let htmlStr = template.replace("<!--react-ssr-outlet-->", `<div id='app'>${html}</div>`);
  // 将渲染后的html字符串发送给客户端
  res.send(htmlStr);
} else {
  res.status(context.status).send("error code:" + context.status);
}

Рендеринг на стороне сервераcontext.status,не существуетstatusАтрибут указывает, что маршрут совпал, если он существует, установить код состояния и отреагировать на результат.

App.jsxМаршрут перенаправления используется в<Redirect from="/" to="/bar" exact />,доступhttp://localhost:3000перенаправит наhttp://localhost:3000/bar, пока вStaticRouterМаршрут в середине не имеет состояния и не может быть перенаправлен.http://localhost:3000Сервер возвращаетсяApp.jsxФрагмент html, отображаемый в формате , за исключениемBar.jsxчто делает компонент

Bar.jsxизrenderМетоды, как показано ниже

render() {
  return (
    <div>
      <div>Bar</div>
    </div>
  );
}

Из-за роутинга клиента адресная строка браузера сталаhttp://localhost:3000/bar, и выводитBar.jsx, но рендеринг на стороне клиента и на стороне сервера несовместимы

существуетserver.jsxдобавить строку кода вconsole.log(context)

let context = {};
let component = createApp(context, req.url);
let html = ReactDOMServer.renderToString(component);

console.log(context);
...

затем посетитеhttp://loclahost:3000, вы можете увидеть следующую информацию о выводе в терминале

======enter server======
visit url: /
{ action: 'REPLACE',
  location: { pathname: '/bar', search: '', hash: '', state: undefined },
  url: '/bar' }

пройти черезcontextПолучатьurlВыполнение обработки перенаправления на стороне сервера

if (context.url) {  // 当发生重定向时,静态路由会设置url
  res.redirect(context.url);
  return;
}

посетить в это времяhttp://loclahost:3000, браузер отправляет два запроса, первый запрос/, второе перенаправление на/bar

Главное управление

Каждая страница имеет соответствующую информацию о заголовке, такую ​​как заголовок, метаданные и ссылка, которые используются здесь.react-helmetПлагин для управления головой, он также поддерживает рендеринг на стороне сервера

Установить первымreact-helmet

npm install react-helmet

затем вApp.jsxсерединаimport, добавить пользовательский заголовок

import { Helmet } from "react-helmet";
<div>
  <Helmet>
    <title>This is App page</title>
    <meta name="keywords" content="React SSR"></meta>
  </Helmet>
  <div className="title">This is a react ssr demo</div>
  ...
</div>

При рендеринге на сервере вызовитеReactDOMServer.renderToString()нужно позвонить послеHelmet.renderStatic()Чтобы получить информацию, относящуюся к голове, чтобыserver.jsиспользуется вApp.jsxсерединаHelmet, нужно быть у входаentry-server.jsиApp.jsxвнести некоторые изменения

entry-server.js

const createApp = (context, url) => {
  const App = () => {
    return (
      <StaticRouter context={context} location={url}>
        <Root setHead={(head) => App.head = head}/>  
      </StaticRouter>
    )
  }
  return <App />;
}

App.jsx

class Root extends React.Component {
  constructor(props) {
    super(props);

    if (process.env.REACT_ENV === "server") {
      // 当前如果是服务端渲染时将Helmet设置给外层组件的head属性中
      this.props.setHead(Helmet);
    }
  }
  ...
}

даватьRootКомпонент передается в функцию реквизитаsetHead,существуетRootВызывается при инициализации компонентаsetHeadфункция к новомуAppдобавить компонентheadАтрибуты

Изменить шаблонindex.html,Добавить к<!--react-ssr-head-->Заполнитель для информации о голове

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <link rel="shortcut icon" href="/public/favicon.ico">
    <title>React SSR</title>
    <!--react-ssr-head-->
</head>

существуетserver.jsзаменить в

if (!context.status) {  // 无status字段表示路由匹配成功
  // 获取组件内的head对象,必须在组件renderToString后获取
  let head = component.type.head.renderStatic();
  // 替换注释节点为渲染后的html字符串
  let htmlStr = template
  .replace(/<title>.*<\/title>/, `${head.title.toString()}`)
  .replace("<!--react-ssr-head-->", `${head.meta.toString()}\n${head.link.toString()})`)
  .replace("<!--react-ssr-outlet-->", `<div id='app'>${html}</div>`);
  // 将渲染后的html字符串发送给客户端
  res.send(htmlStr);
} else {
  res.status(context.status).send("error code:" + context.status);
}

componentда<App />Объект, преобразованный синтаксисом jsx,component.typeтип компонента, который получает объект, вотentry-server.jsсерединаApp

Примечание: необходимо пройти здесьApp.jsxсерединаimportВойдитеHelmetперечислитьrenderStatic()информация заголовка

доступhttp://localhost:3000Когда информация заголовка была отображена

Каждый маршрут соответствует представлению, и каждое представление имеет свою собственную информацию о голове. Компонент представления вложен в корневой компонент. Когда компонент вложен и используется реактивный шлем, та же информация будет автоматически заменена

существуетBar.jsx,Baz.jsx,Foo.jsxиTopList.jsxИспользуйте реактивный шлем, чтобы настроить заголовок соответственно. как

class Bar extends React.Component {
  render() {
    return (
      <div>
        <Helmet>
          <title>Bar</title>
        </Helmet>
        <div>Bar</div>
      </div>
    );
  }
}

ввод в браузереhttp://localhost:3000/barкогда заголовок отображается как<title data-react-helmet="true">Bar</title>

входитьhttp://localhost:3000/bazкогда заголовок отображается как<title data-react-helmet="true">Baz</title>

Суммировать

Этот раздел настраивает и управляет базовой маршрутизацией React, что упрощает обслуживание и закладывает основу для последующей предварительной выборки данных. Используется при рендеринге маршрутизации на стороне сервера.StaticRouterкомпонент, этот компонент имеетcontextиlocationДва реквизита, вы можете дать их сами при рендерингеcontextДайте настраиваемые свойства, такие как установка кода состояния,locationиспользуется для сопоставления маршрутов. Информация о голове важна для рендеринга на стороне сервера.Плагин react-helmet обеспечивает простое использование для определения информации о голове и поддерживает как клиент, так и сервер.

Исходный код этой главы

Следующий раздел:Разделение кода и предварительная выборка данных