基本用法

单页面应用和前端路由

在传统的 Web 应用中,浏览器根据地址栏的 URL 向服务器发送一个 HTTP 请求,服务器根据 URL 返回一个 HTML 页面。这种情况下,一个 URL 对应一个 HTML 页面,一个 Web 应用包含很多 HTML 页面,这样的应用就是多页面应用;在多页面应用中,页面路由的控制由服务器端负责,这种路由方式称为后端路由。

在多页面应用中,每次页面切换都需要向服务器发送一次请求,页面使用到的静态资源也需要重新请求加载,存在一定的浪费。而且,页面的整体刷新对用户体验也有影响,因为不同页面间往往存在共同的部分,例如导航栏、侧边栏等,页面整体刷新也会导致共用部分的刷新。

有没有一种方式让 Web 应用只是看起来像多页面应用,实际 URL 的变化可以引起页面内容的变化,但不会向服务器发送新的请求呢?实际上,满足这种要求的 Web 应用就是单页面应用(Single Page Application,简称SPA)。单页面应用虽然名为 “单页”,但视觉上的感受仍然是多页面,因为 URL 发生变化,页面的内容也会发生变化,但这只是逻辑上的多页面,实际上无论 URL 如何变化,对应的 HTML 文件都是同一个,这也是单页面应用名字的由来。在单页面应用中,URL 发生变化并不会向服务器发送新的请求,所以 “逻辑页面”(这个名称用来和真实的 HTML 页面区分)的路由只能由前端负责,这种路由方式称为前端路由。

React Router 就是一种前端路由的实现方式。通过使用 React Router 可以让 Web 应用根据不同的 URL 渲染不同的组件,这样的组件渲染方式可以解决更加复杂的业务场景。例如,当 URL 的 pathname 为 /list 时,页面会渲染一个列表组件,当点击列表中的一项时,pathname 更改为 /item/:id(id 为参数),旧的列表组件会被卸载,取而代之的是一个新的单一项的详情组件。

目前,国内的搜索引擎大多对单页面应用的 SEO 支持的不好,因此,对于 SEO 非常看重的 Web 应用(例如,企业官方网站、电商网站等),一般还是会选择采用多页面应用。React 也并非只能用于开发单页面应用。

React Router的安装

本书使用的 React Router 的大版本号是 v4,这也是写作本书时的最新版本。

React Router 包含 3 个库:react-router、react-router-dom 和 react-router-native。react-router 提供最基本的路由功能,实际使用时,我们不会直接安装 react-router,而是根据应用运行的环境选择安装 react-router-dom(在浏览器中使用)或 react-router-native(在 react-native 中使用)。react-router-dom 和 react-router-native 都依赖于 react-router,所以在安装时,react-router 也会自动安装。因为本书介绍的是创建 Web 应用,所以这里需要安装 react-router-dom:

npm install react-router-dom

React Router v4 是对 React Router 的一次彻底重构,采用动态路由,遵循 React 中一切皆组件的思想,每一个 Route(路由)都是一个普通的 React 组件,这一点也导致 v4 版本较之前的版本在 API 和使用方式上都有了巨大变化,也就是说,v4 版本并不兼容之前的 React Router 版本,请读者务必注意。

路由器

React Router 通过 Router 和 Route 两个组件完成路由功能。Router 可以理解成路由器,一个应用中只需要一个 Router 实例,所有的路由配置组件 Route 都定义为 Router 的子组件。在 Web 应用中,我们一般会使用对 Router 进行包装的 BrowserRouter 或 HashRouter 两个组件。BrowserRouter 使用 HTML 5 的 history API(pushState、replaceState 等)实现应用的 UI 和 URL 的同步。HashRouter 使用 URL 的 hash 实现应用的 UI 和 URL 的同步。

BrowserRouter 创建的 URL 形式如下:

HashRouter 创建的 URL 形式如下:

使用 BrowserRouter 时,一般还需要对服务器进行配置,让服务器能正确地处理所有可能的 URL。例如,当浏览器发送 http://example.com/some/pathhttp://example.com/some/path2 两个请求时,服务器需要能返回正确的 HTML 页面(也就是单页面应用中唯一的 HTML 页面)。使用 HashRouter 则不存在这个问题,因为 hash 部分的内容会被服务器自动忽略,真正有效的信息是 hash 前面的部分,而对于单页面应用来说,这部分内容是固定的。

Router 会创建一个 history 对象,history 用来跟踪 URL,当 URL 发生变化时,Router 的后代组件会重新渲染。React Router 中提供的其他组件可以通过 context 获取 history 对象(在4.3节组件通信中已经详细介绍过 context),这也隐含说明了 React Router 中的其他组件必须作为 Router 组件的后代组件使用。但 Router 中只能有唯一的一个子元素,例如:

// 正确
ReactDOM.render((
    <BrowserRouter>
        <App />
    </BrowserRouter>
), document.getElementById('root'))

// 错误,Router中包含两个子元素
ReactDOM.render((
    <BrowserRouter>
        <App1 />
        <App2 />
    </BrowserRouter>
), document.getElementById('root'))

路由配置

Route 是 React Router 中用于配置路由信息的组件,也是 React Router 中使用频率最高的组件。每当有一个组件需要根据 URL 决定是否渲染时,就需要创建一个 Route。

path

每个 Route 都需要定义一个 path 属性,当使用 BrowserRouter 时,path 用来描述这个 Route 匹配的 URL 的 pathname;当使用 HashRouter 时,path 用来描述这个 Route 匹配的 URL 的 hash。例如,使用 BrowserRouter 时,<Route path='/foo'/> 会匹配一个 pathname 以 foo 开始的 URL(如 http://example.com/foo )。当 URL 匹配一个 Route 时,这个 Route 中定义的组件就会被渲染出来;反之,Route 不进行渲染(Route 使用 children 属性渲染组件除外,可参考下文)。

本章中的示例,如无特殊说明,使用的都是 BrowserRouter。

match

当 URL 和 Route 匹配时,Route 会创建一个 match 对象作为 props 中的一个属性传递给被渲染的组件。这个对象包含以下 4 个属性。

  1. params:Route 的 path 可以包含参数,例如 <Route path='/foo/:id'> 包含一个参数 id。params 就是用于从匹配的 URL 中解析出 path 中的参数,例如,当 URL="http://example.com/foo/1" 时,params= {id: 1} 。

  2. isExact:是一个布尔值,当 URL 完全匹配时,值为 true;当 URL 部分匹配时,值为 false。例如,当 path="/foo"、URL="http://example.com/foo" 时,是完全匹配;当 URL="http://example.com/foo/1" 时,是部分匹配。

  3. path:Route 的 path 属性,构建嵌套路由时会使用到。

  4. url:URL 的匹配部分。

Route渲染组件的方式

Route 如何决定渲染的内容呢?Route 提供了 3 个属性,用于定义待渲染的组件:

  • component

component 的值是一个组件,当 URL 和 Route 匹配时,component 属性定义的组件就会被渲染。例如:

<Route path="/foo" component={Foo} />

当 URL="http://example.com/foo" 时,Foo 组件会被渲染。

  • render

render 的值是一个函数,这个函数返回一个 React 元素。这种方式可以方便地为待渲染的组件传递额外的属性。例如:

<Route path="/foo" render={ (props) => (
    <Foo {...props} data={extraProps} />
)} />

Foo 组件接收了一个额外的 data 属性。

  • children

children 的值也是一个函数,函数返回要渲染的 React 元素。与前两种方式不同之处是,无论是否匹配成功,children 返回的组件都会被渲染。但是,当匹配不成功时,match 属性为 null。例如:

<Route path='/foo' children={(props) => (
    <div className={props.match ? 'active': ''}>
        <Foo />
    </div>
)} />

如果 Route 匹配当前 URL,待渲染元素的根节点 div 的 class 将被设置成 active。

Switch和exact

当 URL 和多个 Route 匹配时,这些 Route 都会执行渲染操作。如果只想让第一个匹配的 Route 渲染,那么可以把这些 Route 包到一个 Switch 组件中。如果想让 URL 和 Route 完全匹配时,Route 才渲染,那么可以使用 Route 的 exact 属性。Switch 和 exact 常常联合使用,用于应用首页的导航。例如:

<Router>
    <Switch>
        <Route exact path="/" component={Home} />
        <Route path='/posts' component={Posts} />
        <Route path='/:user' component={User} />
    </Switch>
</Router>

如果不使用 Switch,当 URL 的 pathname 为 "/posts" 时,<Route path='/posts'/> 和 <Route path='/:user' /> 都会被匹配,但显然我们并不希望 <Route path='/:user' /> 被匹配,实际上也没有用户名为 posts 的用户。如果不使用 exact,"/" "/posts" "/user1" 等几乎所有 URL 都会匹配第一个 Route,又因为 Switch 的存在,后面的两个 Route 永远也不会被匹配。使用 exact,保证只有当 URL 的 pathname 为 "/" 时,第一个 Route 才会被匹配。

嵌套路由

嵌套路由是指在 Route 渲染的组件内部定义新的 Route。例如,在上一个例子中,在 Posts 组件内再定义两个 Route:

const Posts = ({ match }) => {
    return (
      <div>
        {/* 这里 match.url 等于 /posts */}
        <Route path={`${match.url}/:id`} component={PostDetail} />
        <Route exact path={match.url} component={PostList} />
      </div>
    );
}

当 URL 的 pathname 为 "/posts/react" 时,PostDetail 组件会被渲染;当 URL 的 pathname 为 "/posts" 时,PostList 组件会被渲染。Route 的嵌套使用让应用可以更加灵活地使用路由。

链接

Link 是 React Router 提供的链接组件,一个 Link 组件定义了当点击该 Link 时,页面应该如何路由。例如:

const Navigation = () => (
    <header>
        <nav>
            <ul>
                <li><Link to='/'>Home</Link></li>
                <li><Link to='/posts'>Posts</Link></li>
            </ul>
        </nav>
    </header>
)

Link 使用 to 属性声明要导航到的 URL 地址。to 可以是 string 或 object 类型,当 to 为 object 类型时,可以包含 pathname、search、hash、state 四个属性,例如:

<Link to={{
    pathname: '/posts',
    search: '?sort=name',
    hash: '#the-hash',
    state: { fromHome: true }
}}/>

除了使用 Link 外,我们还可以使用 history 对象手动实现导航。history 中最常用的两个方法是 push(path, [state]) 和 replace(path, [state]),push 会向浏览历史记录中新增一条记录,replace 会用新记录替换当前记录。例如:

history.push('/posts')
history.replace('/posts')