React服务端渲染(前后端路由同构)

栏目: 服务器 · 发布时间: 6年前

内容简介:Web应用是通过url访问某个具体的HTML页面,每个url都对应一个资源。传统的Web应用中,浏览器通过url向服务器发送请求,服务器读取资源并把处理好的页面内容发送给浏览器,而在单页面应用中,所有url变化的处理都在浏览器端完成,url发生变化时浏览器通过js将内容替换。对于服务端渲染的应用,当请求某个url资源,服务器要将该url对应的页面内容发送给浏览器,浏览器下载页面引用的js后执行客户端路由初始化,随后的路由跳转都是在浏览器端,服务端只负责从浏览器发送请求的第一次渲染首先在之前搭建的项目中然后

Web应用是通过url访问某个具体的HTML页面,每个url都对应一个资源。传统的Web应用中,浏览器通过url向服务器发送请求,服务器读取资源并把处理好的页面内容发送给浏览器,而在单页面应用中,所有url变化的处理都在浏览器端完成,url发生变化时浏览器通过js将内容替换。对于服务端渲染的应用,当请求某个url资源,服务器要将该url对应的页面内容发送给浏览器,浏览器下载页面引用的js后执行客户端路由初始化,随后的路由跳转都是在浏览器端,服务端只负责从浏览器发送请求的第一次渲染

首先在之前搭建的项目中 src 目录下创建4个页面组件

React服务端渲染(前后端路由同构)

然后安装React Web端依赖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>
复制代码

复杂的应用中免不了组件嵌套的情况, Routecomponent 属性不仅可以传递组件类型还可以传递回调函数,通过回调函把当前组件的子路由通过 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 传递url,调用 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 包裹根组件,传入上下文 contextlocation ,同时使用函数来创建一个新的组件

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 提供了一个props用来传递上下文对象 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 组件渲染的内容

React服务端渲染(前后端路由同构)
React服务端渲染(前后端路由同构)

Bar.jsxrender 方法如下

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;
}
复制代码
React服务端渲染(前后端路由同构)

此时访问 http://loclahost:3000 ,浏览器发送了两次请求,第一次请求 / ,第二次重定向到 /bar

Head管理

每一个页面都有对应的head信息如title、meta和link等,这里使用react-helmet插件来管理Head,它同时支持服务端渲染

先安装 react-helmet

npm install react-helmet

然后在 App.jsximport ,添加自定义head

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() 才能获取head相关信息,为了在 server.js 中使用 App.jsx 中的 Helmet ,需要在入口 entry-server.jsApp.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 组件传入一个props函数 setHead ,在 Root 组件初始化时调用 setHead 函数给新的 App 组件添加一个 head 属性

修改模板 index.html ,添加 <!--react-ssr-head--> 作为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.jsximport 进来的 Helmet 调用 renderStatic() 后才能获头部信息

访问 http://localhost:3000 时,头部信息已经被渲染出来了

React服务端渲染(前后端路由同构)

每一个路由对应一个视图,每一个视图都有各自的head信息,视图组件是嵌套在根组件中的,当组件发生嵌套使用react-helmet时会自动替换相同的信息

Bar.jsxBaz.jsxFoo.jsxTopList.jsx 中分别使用react-helmet自定义标题。如

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>

React服务端渲染(前后端路由同构)

输入 http://localhost:3000/baz 时标题渲染成 <title data-react-helmet="true">Baz</title>

React服务端渲染(前后端路由同构)

总结

本节对React基本路由进行配置化管理,使得维护起来更加简单,也为后续数据预取奠定了基础。在服务端路由渲染中使用了 StaticRouter 组件,这个组件有 contextlocation 两个props,渲染时可以自行给 context 赋予自定义属性,比如设置状态码, location 则用来匹配路由。服务端渲染中head信息必不可少,react-helmet插件提供了简单的用法来定义head信息,同时支持客户端和服务端

本章节源码

下一节:数据预取和同步(待更新)


以上所述就是小编给大家介绍的《React服务端渲染(前后端路由同构)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

降维打击

降维打击

杨 健 / 北京时代华文书局 / 2016-10 / 68

“降维打击”出自中国科幻作家刘慈欣的小说《三体》,而笔者在这本书中试图把“降维打击”的思维引入到企业经营管理的实战中,总结出一套“降维打击”的商业理论。 按照笔者的理解,企业竞争力可以体现在若干个维度的累加上,具有高维度思维的企业,主动将竞争对手的某一核心维度的竞争力降为零,并跟对手在自己更具竞争优势的维度内进行竞争,从而实现以小博大、以弱灭强的商业竞争结果,这就是企业竞争中的“降维打击”。......一起来看看 《降维打击》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具