React Router V4 精讲

1、前端路由和后端路由

1)后端路由

多页应用中,一个URL对应一个HTML页面,一个Web应用包含不少HTML页面,在多页应用中,页面路由控制由服务器端负责,这种路由方式称为后端路由。css

多页应用中,每次页面切换都须要向服务器发送一次请求,页面使用到的静态资源也须要从新加载,存在必定的浪费。并且,页面的总体刷新对用户体验也有影响,由于不一样页面间每每存在共同的部分,例如导航栏、侧边栏等,页面总体刷新也会致使共用部分的刷新。前端

2)前端路由

在单面应用中,URL发生并不会向服务器发送新的请求,因此“逻辑页面”的路由只能由前端负责,这种路由方式称为前端路由。react

目前,国内的搜索引擎大多对单页应用的SEO支持的很差,所以,对于 SEO 很是看重的 Web
应用(例如,企业官方网站,电商网站等),通常仍是会选择采用多页面应用。React 也并不是只能用于开发单页面应用。

2、React Router 安装

这里使用的 React Router 的大版本号是 v4, 这也是目前最新版本。webpack

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应用,使用web

npm install react-router-dom

建立 navtive 应用,使用npm

npm install react-router-native

3、路由器

React Router 经过 Router 和 Route 两个组件完成路由功能。Router 能够理解成路由器,一个应用中须要一个 Router 实例,全部跌幅配置组件 Route 都定义为 Router 的子组件。在 Web应用中,咱们通常会使用对 Router 进行包装的 BrowserRouter 或 HashRouter 两个组件 BrowserRouter使用 HTML5 的 history API(pushState、replaceState等)实现应用的 UI 和 URL 的同步。HashRouter 使用 URL 的 hash 实现应用的 UI 和 URL 同步。后端

BrowserRouter 建立的 URL 形式以下:

http://example.com/some/path

HashRouter 建立的 URL 形式以下:

http://example.com/#/some/path

使用 BrowserRouter 时,通常还须要对服务器进行配置,让服务器能正确地处理全部可能的URL。例如,当浏览器发生 http://example.com/some/pathhttp://example.com/some/path2 两个请求时,服务器须要能返回正确的 HTML 页面(也就是单页面应用中惟一的 HTML 页面)react-native

HashRouter 则不存在这个问题,由于 hash 部分的内容会被服务器自动忽略,真正有效的信息是 hash 前端的部分,而对于单页应用来讲,这部分是固定的。浏览器

Router 会建立一个 history 对象,history 用来跟踪 URL, 当URL 发生变化时, Router,的后代组件会从新渲染。React Router 中提供的其余组件能够经过 context 获取 history 对象,这也隐含说明了 React Router 中其余组件必须做为 Router 组件后代使用。但 Router 中只能惟一的一个子元素,例如:服务器

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

4、路由器

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

1) path

每一个 Route 都须要定义一个 path 属性,当使用 BrowserRouter 时,path 用来描述这个Router匹配的 URL 的pathname;当使用 HashRouter时,path 用来描述这个 Route 匹配的 URL 的 hash。例如,使用 BrowserRouter 时,<Route path=''foo' /> 会匹配一个 pathname 以 foo 开始的 URL (如: http://example.com/foo)。当 URL 匹配一个 Route 时,这个 Route 中定义的组件就会被渲染出来。

2)match

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

(1)params: Route的 path 能够包含参数,例如 <Route path="/foo/:id" 包含一个参数 id。params就是用于从匹配的 URL 中解析出 path 中的参数,例如,当 URL = 'http://example.ocm/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 的匹配的方式

3)Route 渲染组件的方式

(1)component

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

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

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

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

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

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

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

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

若是 Route 匹配当前 URL,待渲染元素的根节点 div 的 class 将设置成 active.

4)Switch 和 exact

当URL 和多个 Route 匹配时,这些 Route 都会执行渲染操做。若是只想让第一个匹配的 Route 沉浸,那么能够把这些 Route 包到一个 Switch 组件中。若是想让 URL 和 Route 彻底匹配时,Route才渲染,那么可使用 Route 的 exact 属性。Switch 和 exact 经常联合使用,用于应用首页的导航。例如:

<Router>
 <Switch>
    <Route exact path='/' component={Home}/>
    <Route exact path='/posts' component={Posts} />
    <Route exact 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才会匹配。

5)嵌套路由

嵌套路由是指在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>
  )
}

5、连接

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>

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

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

6、路由设计

路由设计的过程能够分为两步:

  1. 为每个页面定义有语义的路由名称(path)
  2. 组织 Route 结构层次

1)定义路由名称

咱们有三个页面,按照页面功能不难定义出以下的路由名称:

  • 登陆页: /login
  • 帖子列表页: /posts
  • 帖子详情页: /posts/:id(id表明帖子的ID)

可是这些还不够,还须要考虑打开应用时的默认页面,也就是根路径"/"对应的页面。结合业务场景,帖子列表做为应用的默认页面为合适,所以,帖子列表对应两个路由名称: '/posts'和 '/'

2)组织 Route 结构层次

React Router 4并不须要在一个地方集中声明应用须要的全部 Route, Route实际上也是一个普通的 React 组件,能够在任意地方使用它(前提是,Route必须是 Router 的子节点)。固然,这样的灵活性也必定程度上增长了组织 Route 结构层次的难度。
咱们先考虑第一层级的路由。登陆页和帖子列表页(首页)应该属于第一层级:

<Router>
  <Switch>
    <Route exact path="/" component={Home}></Route>
    <Route exact path="/login" component={Login}></Route>
    <Route exact path="/posts" component={Home}></Route>
  </Switch>
</Router>

第一个Route 使用了 exact 属性,保证只有当访问根路径时,第一个 Route 才会匹配成功。Home 是首页对应组件,能够经过 "/posts" 和 “/” 两个路径访问首页。注意,这里并无直接渲染帖子列表组件,真正渲染帖子列表组件的地方在 Home 组件内,经过第二层级的路由处理帖子列表组件和帖子详情组件渲染,components/Home.js 的主要代码以下:

class Home extends Component {
  /**省略其他代码 */
  render() {
    const {match, location } = this.props;
    const { username } = this.state;
    return(
      <div>
        <Header
          username = {username}
          onLogout={this.handleLogout}
          location = {location}
        >
        </Header>
        {/* 帖子列表路由配置 */}
        <Route
          path = {match.url}
          exact
          render={props => <PostList username={username} {...this.props}></PostList>}
        ></Route>
      </div>
    )
  }
}

Home的render内定义了两个 Route,分别用于渲染帖子列表和帖子详情。PostList 是帖子列表组件,Post是帖子详情组件,代码使用Router 的render属性渲染这两个组件,由于它们须要接收额外的 username 属性。另外,不管访问是帖子列表页面仍是帖子详情页面,都会共用相同 Header 组件。

7、代码分片

默认状况下,当在项目根路径下执行 npm run build 时 ,create-react-app内部使用 webpack将 src路径下的全部代码打包成一个 JS 文件和一个 Css 文件。

当项目代码量很少时,把全部代码打包到一个文件的作法并不会有什么影响。可是,对于一个大型应用,若是还把全部的代码都打包到一个文件中,显然就不合适了。

create-react-app 支持经过动态 import() 的方式实现代码分片。import()接收一个模块的路径做为参数,而后返回一个 Promise 对象, Promise 对象的值就是待导入的模块对象。例如

// moduleA.js

const moduleA = 'Hello'
export { moduleA };

// App.js

import React, { Component } from 'react';

class App extends Component {
  handleClick = () => {
    // 使用import 动态导入 moduleA.js
    import('./moduleA')
      .then(({moduleA}) => {
        // 使用moduleA
      })
      .catch(err=> {
        //处理错误
      })
  };
  render() {
    return(
      <div>
        <button onClick={this.handleClick}>加载 moduleA</button>
      </div>
    )
  }
}

export default App;

上面代码会将 moduleA.js 和它全部依赖的其余模块单独打包到一个chunk文件中,只有当用户点击加载按钮,才开始加载这个 chunk 文件。
当项目中使用 React Router 是,通常会根据路由信息将项目代码分片,每一个路由依赖的代码单独打包成一个chunk文件。咱们建立一个函数统一处理这个逻辑:

import React, { Component } from 'react';
// importComponent 是使用 import()的函数
export default function asyncComponent(importComponent) {
  class AsyncComponent extends Component {
    constructor(props) {
      super(props);
      this.state = {
        component:  null //动态加载的组件
      }
    }
    componentDidMount() {
      importComponent().then((mod) => {
        this.setState({
          // 同时兼容 ES6 和 CommonJS 的模块
          component: mod.default ? mod.default : mod;
        });
      })
    }
    render() {
      // 渲染动态加载组件
      const C = this.state.component;
      return C ? <C {...this.props}></C> : null
    }
  }

  return AsyncComponent;
}

asyncComponent接收一个函数参数 importComponent, importComponent 内经过import()语法动态导入模块。在AsyncComponent被挂载后,importComponent就会阴调用,进而触发动态导入模块的动做。
下面利用 asyncComponent 对上面的例子进行改造,代码以下:

import React, { Component } from 'react';
import { ReactDOM, BrowserRouter as Router, Switch, Route } from 'react-dom';
import asyncComponent from './asyncComponent'
//经过asyncComponent 导入组件,建立代码分片点
const AsyncHome = asyncComponent(() => import("./components/Home"))
const AsyncLogin = asyncComponent(() => import("./components/Login"))

class App extends component {
  render() {
    return(
      <Router>
        <Switch>
          <Route exact path="/" component={AsyncHome}></Route>
          <Route exact path="/login" component={AsyncLogin}></Route>
          <Route exact path="/posts" component={AsyncHome}></Route>
        </Switch>
      </Router>
    )
  }
}

export default App;

这样,只有当路由匹配时,对应的组件才会被导入,实现按需加载的效果。

这里还有一个须要注意的地方,打包后没有单独的CSS文件了。这是由于 CSS样子被打包到各个 chunk 文件中,当 chunk文件被加载执行时,会有动态把 CSS 样式插入页面中。若是但愿把 chunk 中的 css打包到一个单独的文件中,就须要修改 webpack 使用的 ExtractTextPlugin 插件的配置,但 create-react-app 并无直接把 webpack 的配置文件暴露给用户,为了修改相应配置
,须要将 create-react-app 管理的配置文件“弹射”出来,在项目根路径下执行:

npm run eject

项目中会多出两个文件夹:config和 scripts,scrips中包含项目启动、编译和测试的脚本,config 中包含项目使用的配置文件,
webpack配置文件 就在这个路径下,打包 webpack.config.prod.js 找到配置 ExtractTextPlugin 的地方,添加 allChunks:true 这项配置:

new ExtractTextPlugin({
  filename: cssFilename,
  allChunks: true
})

而后从新编译项目,各个chunk 文件 使用的 CSS 样式 又会统一打包到 main.css 中。

以上主要参考 《React 进阶之路》这本书

愿你成为终身学习者

想了解更多生活鲜为人知的一面,能够关注个人大迁世界噢

图片描述

相关文章
相关标签/搜索