所有源代码、文档和图片都在 github 的仓库里,点击进入仓库

相关阅读

1. 路由

  • 在页面上有了内容之后,我们还需要进行路由的跳转
  • 下载路由依赖包,react-router-dom,npm i react-router-dom -S
  • 这里暂时先不介绍多级路由,多级路由放在后边介绍,现在我们只介绍一级路由

1.1 路由介绍

  • 我们在使用客户端渲染的时候,页面跳转路由是前端控制的,主要有两种模式,一种是 history 模式,另外一种是 hash 模式。
  • 无论是哪种模式,都可以进行路由跳转操作,唯一的不同是 history 模式需要后端控制 404 页面,而 hash 模式不需要
  • 在服务端渲染,我们需要的是静态路由(static router),与客户端的路由模式是不一样的
  • 路由的控制,不仅仅是在服务端,客户端也需要进行同样的路由规则,那么我们就可以写一份路由,供客户端和服务端一起使用

1.2 路由页面组件

  • 路由跳转,至少需要两个页面级组件,所以我们可以先创建一个 News 页面组件,containers/News/index.js
  • 之前我们已经有了一个 Home 页面级组件,现在有两个组件,现在页面我们已经准备好了
  • containers/News/index.js
// containers/News/index.js
import React, { Component } from 'react';

class News extends Component {
  render() {
    return (
      <div>
        <h1>News Page</h1>
      </div>
    );
  }
}

export default News;

1.3 路由文件

  • 我们需要导出一个路由组件,组件内部是 Route
  • 导出的路由组件,需要作为子组件,才能在页面上渲染
  • 现在我们先采用这种模式,后期我们会用另外一种模式,但是不论是哪种模式,实质上是一样的
  • 注: <></> 标签和 <Fragment><Fragment/> 标签是一样的,都是一个聚合子元素的一个标签,不增加真实的 DOM 节点
  • src/routes.js
// src/routes.js
import React from 'react';
import { Route } from 'react-router-dom';
import Home from './containers/Home';
import News from './containers/News';

export default (
  <>
    <Route path='/' exact component={Home}/>
    <Route path='/news' component={News}/>
  </>
);

1.4 客户端路由

  • 客户端路由比较简单,直接把路由组件作为子组件传递就可以
  • 在这里直接把路由放在这里,实际上是把路由文件里的多个 Route 放在这里,如果我们把路由里的 Route 放在这里,我们会发现实际上是一样的,没有什么区别
import React from 'react'
import { hydrate } from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import routes from '../routes';

hydrate(
  <BrowserRouter>
    {routes}
  </BrowserRouter>, window.root);

1.5 服务端路由

  • 服务端路由相对复杂一些,需要使用无状态路由,也就是静态路由 StaticRouter

,因为服务端并不知道请求是通过什么模式,而且服务端也无需知道,服务端只需要根据客户端发送的请求,做相应的处理即可

  • StaticRouter 里需要传递两个属性,一个是 context,一个是 location
  • context 主要是用来给组件传递数据信息,这个属性可以在客户端和服务端互相传递参数,比如 css 样式的参数
  • location 主要是用来接收请求路径信息,比如 pathname,search,hash 等
  • 修改 src/server/index.js
import routes from '../routes';

app.get('*', (req, res) => {
  let context = {};

  let domContent = renderToString(
    <StaticRouter context={context} location={req.path}>
      {routes}
    </StaticRouter>
  );

  // html 的内容不改变
  res.send(html);
});
  • 从代码中我们可以看到,我们移除了 Home 组件,引入了 routes 组件,因为 routes 已经可以匹配到 Home 和 News 组件
  • 添加了 StaticRouter 组件,传入了一个 context 值,把 express 的 req.path 传递给了 location 属性,这样服务端就可以知道客户端的路由地址
  • 修改了 app.get('/')app.get('*'),因为我们服务端不仅仅是接收同一个路由地址,我们还有 /news 路由,所以我们要把根路径匹配修改为全局匹配。如果不修改为全局匹配呢,也可以,只不过我们如果有 100 个路由,我们就要写 100 个 app.get('/xxx'),如果你愿意的话,也是可以的
  • 原理就是

    • 客户端通过路由修改了请求的地址,服务端接收相应,在 StaticRouter 中我们获取到了客户端请求的路由地址
    • routes 就开始进行匹配,如果匹配到了,那么就把匹配到的路由组件进行渲染成 HTML 字符串,发送给客户端。
    • 如果没有匹配到呢,假如我们访问了 http://localhost:3000/hello 路由,那么我们的 routes 是匹配不到的,既然匹配不到,那么 renderToString 渲染的结果就是空,domContent 作为空值插入 HTML 模板,得到的就是一个空白页面
  • 此时,我们就可以在通过不同的 url,访问到不同的页面,查看网页源代码,可以看到不同 url 对应的源代码就是对应组件里的代码

1.5 页面效果与页面源代码

  • 首页面效果

  • 首页面源代码

  • news 页面效果

  • news 页面源代码

2. 路由跳转

  • 我们已经实现了路由的切换,但是我们是通过输入 url 来完成路由切换的,显然我们应该通过点击实现跳转,所以我们使用 Link 实现路由跳转
  • 我们创建一个 Header 导航,在 Header 导航中进行点击路由,实现页面的切换
  • 为了样式更加美观,我们引入 [email protected],这里是采用了 又拍云(bootcdn) 的 bootstrap 资源,我们也可以把 bootstrap 放入 public 文件夹下,像引入 client.js 一样引入 bootstrap.css,效果是一样的

2.1 Header 组件

  • Header 组件,/components/Header/index.js
  • 使用 bootstrap 的导航样式
  • /components/Header/index.js
// /components/Header/index.js
import React, { Component } from 'react';
import { Link } from 'react-router-dom';

class Header extends Component {
  render() {
    return (
      <nav className="navbar navbar-inverse navbar-fixed-top">
        <div className="container-fluid">
          <div className="navbar-header">
            <a className="navbar-brand">REACT-SSR</a>
          </div>
          <div>
            <ul className="nav navbar-nav">
              <li><Link to="/">首页</Link></li>
              <li><Link to="/news">新闻</Link></li>
            </ul>
          </div>
        </div>
      </nav>
    );
  }
}

export default Header;

2.2 把 Header 组件进行同构

  • 这里的同构就很简单,直接把 Header 组件放入 BrowserRouter 和 StaticRouter 中,然后稍微进行一下样式的调整,就可以了
  • 客户端同构,修改 src/client/index.js
// src/client/index.js
import React from 'react'
import { hydrate } from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import routes from '../routes';
import Header from '../components/Header';

hydrate(
  <BrowserRouter>
    <>
      <Header/>
      <div className="container" style={{marginTop: 70}}>
        {routes}
      </div>
    </>
  </BrowserRouter>, window.root);
  • 服务端同构,修改 src/server/index.js
// src/server/index.js
import Header from '../compoents/Header';

let domContent = renderToString(
  <StaticRouter context={context} location={req.path}>
    <>
      <Header/>
      <div className="container" style={{marginTop: 70}}>
        {routes}
      </div>
    </>
  </StaticRouter>
);

相关阅读

03-05 20:04