深入了解React Router:递归路径,代码拆分等

在深入研究之前,首先让我们在基本知识上达成一致。React Router提供:

  • 将路由功能内置在React中的单页应用程序
  • React应用的声明式路由

在本教程中,我将重点介绍一些高级React Router概念,例如代码拆分(code splitting),动画过渡(animated transitions),滚动还原(scroll restoration),递归路径(recursive path)和服务器端渲染(server-side rendering)。

最后,我将演示如何在React应用程序中使用这些概念。

本教程的Github例子在这里。每个高级概念都有不同的分支。随时浏览它们,并让我知道您的想法。

代码拆分 Code splitting

有效地代码拆分是为用户增量下载应用程序的过程。这样,可以将捆绑在一起的大型JavaScript文件分成较小的块,并仅在需要时使用。通过代码拆分,您可以将较小的应用程序包交付给用户,并且仅在用户访问SPA的特定“页面”时才下载其他JS代码。

在React应用程序中,可以使用import()语法和webpack来实现代码拆分。

更好的是,您可以使用react-loadable,它是用于加载具有动态导入的组件的高阶组件。React Loadable是一个小型库,在React中使以组件为中心的代码拆分变得异常容易。

让我们看看如何在上面创建的React应用程序中实现代码拆分。

检出code-splitting分支并导航到/src/routes/index.js文件夹中的index.js文件。或者,您可以在此处在线查看文件。

在文件的开头,您将看到一些import语句。它们基本上是要导入以在代码中使用的模块。

import React, { Component } from 'react'
import {
  BrowserRouter as Router,
  Route,
  Switch,
  Link
} from 'react-router-dom'
import Loadable from 'react-loadable'
import LoadingPage from '../components/LoadingPage/LoadingPage'                                                 

正如您在上面看到的,Loadable是从react-loadable导入的,它将用于执行代码拆分。该LoadingPage组件呈现一个将用作加载程序的视图。

Loadable是一个高阶组件(一个创建组件的函数),它使您可以在将任何模块呈现到应用程序之前动态加载任何模块。在下面的代码块中,loader使用import函数动态import加载要加载的特定组件,并将该LoadingPage组件用于加载状态。delay是传递props.pastDelay到加载组件之前要等待的时间(以毫秒为单位)。默认为200

const AsyncHome = Loadable({
  loader: () => import('../components/Home/Home'),
  loading: LoadingPage
})
const AsyncAbout = Loadable({
  loader: () => import('../components/About/About'),
  loading: LoadingPage,
  delay: 300
})
const AsyncNotFound = Loadable({
  loader: () => import('../components/NotFound/NotFound'),
  loading: LoadingPage
})    

您可以通过构建用于生产的应用程序并观察JavaScript代码的捆绑方式来检查是否确实发生了代码拆分。运行npm run build命令以构建用于生产的应用程序。

如您所见,由于代码拆分,包含组件的JavaScript代码现在被划分为不同的块。

动画过渡 Animated transitions

动画过渡有助于提供轻松的网站导航流程。在React中有很多React插件可以帮助解决此问题,但我们将考虑为该应用程序使用react-router-transition插件。

这是我们将要构建的效果:

请查看“ animation-transitions”分支,然后导航到/src/routes/index.js文件夹中的index.js文件,或者您可以在此处在线查看文件。如上所示,我将只重点介绍有助于动画过渡的代码的重要部分。

import { AnimatedSwitch, spring } from 'react-router-transition';      

AnimatedSwitch模块从react-router-transition中导入,React Motion的spring helper函数也已导入,用于为动画指定弹簧配置。AnimatedSwitch基于<Switch />,但是在子路由更改时会带有过渡。

const bounceTransition = {
  // start in a transparent, upscaled state
  atEnter: {
    opacity: 0,
    scale: 1.2,
  },
  // leave in a transparent, downscaled state
  atLeave: {
    opacity: bounce(0),
    scale: bounce(0.8),
  },
  // and rest at an opaque, normally-scaled state
  atActive: {
    opacity: bounce(1),
    scale: bounce(1),
  },
};  

mapStyles()函数使用样式的参数返回不透明度和变换的值。稍后将在配置过渡时使用它。

bounce()的功能从缠绕运动做出反应,得到弹性的配置和弹簧辅助bounceTransition对象定义子比赛将如何在不同的位置,如过渡atEnteratLeaveatActive

上面已经提到AnimatedSwitch在路由中取代了Switch,所以让我们看看如何。

class Routes extends Component {
  render () {
    return (
      <Router history={history}>
        <div>
          <header className="header container">
            <nav className="navbar">
              <div className="navbar-brand">
                <Link to="/">
                  <span className="navbar-item">Home</span>
                </Link>
              </div>
            </nav>
          </header>
          <AnimatedSwitch
            atEnter={bounceTransition.atEnter}
            atLeave={bounceTransition.atLeave}
            atActive={bounceTransition.atActive}
            mapStyles={mapStyles}
            className="route-wrapper"
          >
            <Route exact path="/" component={Home} />
            <Route path="/p/1" component={One} />
            <Route path="/p/2" component={Two} />
            <Route path="*" component={NotFound} />
          </AnimatedSwitch>
        </div>
      </Router>
    )
  }
}

尽管带有一些其他props,如atEntermapStylesatLeaveatActive,它的工作方式与使用Switch的方式相同。

要查看实际的动画过渡,请在终端中运行命令npm start,以在开发模式下运行该应用程序。一旦应用程序启动并运行,请浏览应用程序的路由。

滚动恢复

当您尝试确保用户在切换路由或导航到另一个页面时返回页面顶部时,滚动恢复很有用。它有助于向上滚动导航,因此您无需启动滚动到底部的新屏幕。

另一个重要的用例是,当用户在其他地方导航后返回到您的应用中的长页面时,您可以将其放回相同的滚动位置,以便他们可以从上次停止的地方继续。

这是查看滚动恢复实际操作的链接

让我们看看如何在上面创建的React应用程序中实现滚动恢复。

检出到scroll-restoration分支并导航到routes文件夹/src/routes/index.js中的index.js文件,或者您可以在此处在线查看文件。

import ScrollToTop from '../components/ScrollToTop/ScrollToTop'

class Routes extends Component {
  render () {
    return (
      <Router history={history}>
        <ScrollToTop>
          <div>
            <header className="header container">
              <nav className="navbar">
                <div className="navbar-brand">
                  <Link to="/">
                    <span className="navbar-item">Home</span>
                  </Link>
                </div>
                <div className="navbar-end">
                  <Link to="/about">
                    <span className="navbar-item">About</span>
                  </Link>
                  <Link to="/somepage">
                    <span className="navbar-item">404 page</span>
                  </Link>
                </div>
              </nav>
            </header>
            <Switch>
              <Route exact path="/" component={Home} />
              <Route path="/about" component={About} />
              <Route path="*" component={NotFound} />
            </Switch>
          </div>
        </ScrollToTop>
      </Router>
    )
  }
}        

该文件的重要位显示在上面的代码块中。ScrollToTop当实现滚动恢复时,该组件会承担所有繁重的工作,并且在render()中,它在Router下用于包含Routes。

让我们打开该ScrollToTop组件以查看用于滚动还原的代码。浏览src/components/ScrollToTop并打开ScrollToTop.js或在此处在线查看文件。

import { Component } from 'react'
import { withRouter } from 'react-router-dom'

class ScrollToTop extends Component {
    componentDidUpdate(prevProps) {
        if (this.props.location !== prevProps.location) {
            window.scrollTo(0, 0)
        }
    }

    render() {
        return this.props.children
    }
}

export default withRouter(ScrollToTop)  

在上面的代码块中,组件模块是从react-router-dom导入的reactwithRouter也是从react-router-dom导入的。

接下来的事情是命名为ES6的类ScrollToTop,该类扩展了组件模块的react功能。在componentDidUpdate生命周期检查自己的一个新的页面,并使用该window.scroll函数返回页面顶部。

ScrollToTop然后将该组件包装在导出文件中,withRouter以使其能够访问路由器的props。

要查看实际的滚动还原,请npm start在终端中运行命令以在开发模式下运行该应用程序。一旦应用程序启动并运行,请导航至“关于”页面并向下滚动,直到到达页面底部,然后单击“转到主页” 链接以查看正在执行的滚动还原。

递归路径

递归路径是使用嵌套路由通过调用同一组件来显示嵌套视图的路径。递归路径的一个示例可能是网站上通常使用面包屑。“面包屑”是一种辅助导航方案,可显示用户在网站或Web应用程序中的位置。

面包屑为用户提供了一种即使经过多条路由也可以将路径追溯到其原始着陆点的方法,并且可以使用React Router的功能(特别是match对象)来实现,它提供了为嵌套子组件编写递归路由的功能。

检出recursive-paths到分支并导航到About文件夹/src/components/About/About.js中的文件About.js,或者您可以在此处在线查看文件。

import React, { Component } from 'react'
import './About.css'
import { Link, Route } from 'react-router-dom'

class About extends Component {

    componentDidMount () {
        console.log(this.props.match.url)
    }

    render () {
        return (
            <div className="container">
                <h1>Recursive paths</h1>
                <p>Keep clicking the links below for a recursive pattern.</p>
                <div>
                    <ul>
                        <li><Link className="active" to={this.props.match.url + "/1"}>Link 1</Link></li>
                        <li><Link className="active" to={this.props.match.url + "/2"}>Link 2</Link></li>
                        <li><Link className="active" to={this.props.match.url + "/3"}>Link 3</Link></li>
                    </ul>
                </div>
                <div>
                    <p className="recursive-links">New recursive content appears here</p>
                    <Route path={`${this.props.match.url}/:level`} component={About} />
                </div>
            </div>
        )
    }
}

export default About

在上面的代码块,Linkthis.props.match.url跳转到当前的URL,然后用一个拼接/1/2或者/3。递归实际上发生在Route内,其中将this.props.match.url设置为当前路径并添加了/:level参数,并且该路径所使用的组件就是该About组件。

要查看实际的递归路径,请npm start在终端中运行命令以在开发模式下运行该应用程序。应用启动并运行后,导航至“关于”页面,并继续单击那里的任何链接以查看递归模式。

服务器端渲染

使用像React,Angular或Vue这样的JavaScript框架的缺点之一是,在浏览器执行应用程序的JavaScript包之前,页面基本上是空的。此过程称为客户端渲染。如果用户的互联网连接不畅,可能会导致更长的等待时间。

客户端渲染的另一个缺点是,网络爬虫不会在乎您的页面是否仍在加载或等待JavaScript请求。如果搜寻器什么都看不到,那么显然对SEO不利。

服务器端呈现(SSR)通过在初始请求中加载所有HTML,CSS和JavaScript来帮助解决此问题。这意味着所有内容均已加载并转储到Web爬网程序可以爬网的最终HTML中。

可以使用Node.js在服务器上呈现React应用,并且React Router库可用于在应用中导航。让我们看看如何实现它。

SSR React应用程序位于GitHub仓库中,您可以检出SSR分支,也可以在此处查看该仓库。我将仅强调应用程序中最重要的部分,涉及SSR。

webpack.development.config.js文件包含React应用程序所需的webpack配置,该文件的内容可以在下面或在GitHub上看到。

var path = require('path')
var webpack = require('webpack')
var ExtractTextPlugin = require("extract-text-webpack-plugin")

var config = {

  devtool: 'eval',

  entry: [
    './src/App',
    'webpack-hot-middleware/client'
  ],

  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    publicPath: '/dist/'
  },

  resolve: {
    extensions: ['*', '.js']
  },

  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoEmitOnErrorsPlugin(),
    new webpack.DefinePlugin({
      "process.env": {
        BROWSER: JSON.stringify(true)
      }
    }),
    new ExtractTextPlugin("[name].css")
  ],

  module: {
    loaders: [
      {
        test: /\.js$/,
        loaders: ['react-hot-loader', 'babel-loader'],
        include: [path.join(__dirname, 'src')]
      },
      {
        test: /\.css$/,
        loader: ExtractTextPlugin.extract('style-loader','css-loader')
      }
    ]
  }
}

module.exports = config

应用程序的入口点是server.js在服务器上运行该应用程序所需的Node.js后端。该文件的内容可以在下面或在GitHub上看到。

require('babel-core/register')({});

//Adding a Development Server
let webpack = require('webpack')
let webpackDevMiddleware = require('webpack-dev-middleware')
let webpackHotMiddleware = require('webpack-hot-middleware')
let config = require('./webpack.development.config')
let path = require('path')
let Express = require('express')
let requestHandler = require('./requestHandler')

let app = new Express()
let port = 9000

let compiler = webpack(config)

app.use(webpackDevMiddleware(compiler, {
  noInfo: true,
  publicPath: config.output.publicPath,
  historyApiFallback: true
}))

app.use(webpackHotMiddleware(compiler))

delete process.env.BROWSER;


app.get('/dist/main.css', (req, res) => {
  res.sendFile(path.join(__dirname, '/public/main.css'))
});

app.use(requestHandler);

app.listen(port, (error) => {
  if (error) {
    console.error(error)
  } else {
    console.info('==> Listening on port %s. Open up http://localhost:%s/ in your browser.', port, port)
  }
})

在上面的代码块,我们基本上建立在其中的应用将运行,并且还成立了一个开发服务器与快速的Web服务器webpackDevMiddlewarewebpackHotMiddleware。在文件的顶部,requestHandler.js导入了该文件,以后可通过将该app.use(requestHandler)文件用于构建应用程序的视图。让我们看看该JavaScript文件的内容。您也可以在这里查看


import React from 'react'
import { renderToString } from 'react-dom/server'
import { StaticRouter } from 'react-router'
import { App } from './src/Components'

function handleRender(req,res) {
    // first create a context for <StaticRouter>, it's where we keep the
  // results of rendering for the second pass if necessary
  const context = {}
  // render the first time
  let markup = renderToString(
    <StaticRouter
      location={req.url}
      context={context}
    >
      <html>
        <head>
          <title>Advanced React Router Usage</title>
          <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.0/css/bulma.min.css" />
          <link href="dist/main.css" media="all" rel="stylesheet" />
        </head>
        <body>
          <div id="main">
            <App/>
          </div>
          <script src="dist/bundle.js"></script>
        </body>
      </html>
    </StaticRouter>
  )

  // the result will tell you if it redirected, if so, we ignore
  // the markup and send a proper redirect.
  if (context.url) {
    res.writeHead(301, {
      Location: context.url
    })
    res.end()
  } else {
    res.write(markup)
    res.end()
  }
}
module.exports = handleRender

在服务器上渲染React应用程序要求您将组件渲染到静态标记,这就是为什么renderToString从文件react-dom/server顶部导入的原因。还有其他要突出显示StaticRouter的导入,使用导入是因为服务器上的呈现都有些不同,因为它们都是无状态的。

基本思想是,我们将应用包装在无状态的StaticRouter而不是有状态的BrowserRouter。然后,我们从服务器传入请求的URL,以便路由可以匹配,并且我们将在下文中讨论上下文属性。

只要客户端上有重定向,浏览器历史记录就会更改状态,我们会得到新的屏幕。在静态服务器环境中,我们无法更改应用程序状态。相反,我们使用contextprops来找出渲染的结果。如果找到context.url,则说明该应用已重定向。

那么,我们如何在服务器渲染的应用程序中实际定义路由和匹配组件?这是在src/router-config.jssrc/components/App.js中发生的

import { Home, About, NotFound } from './Components'

export const routes = [
   {
      'path':'/',
      'component': Home,
      'exact': true
   },
   {
      'path':'/about',
      'component': About
   },
   {
      'path':'*',
      'component': NotFound
   }
]                                      

在上面的代码块中,导出的routes数组包含不同的对象,其中包含不同的路由及其随附的组件。然后,将在下面的src/components/App.js文件中使用它。

import React, { Component } from 'react'
import { Switch, Route, NavLink } from 'react-router-dom'
// The exported routes array from the router-config.js file is imported here to be used for the routes below
import { routes } from '../router-config'
import { NotFound } from '../Components'

export default class App extends Component {
  render() {
    return (
      <div>
          <header className="header container">
            <nav className="navbar">
                <div className="navbar-brand">
                    <NavLink to="/" activeClassName="active">
                        <span className="navbar-item">Home</span>
                    </NavLink>
                </div>

                <div className="navbar-end">
                    <NavLink to="/about" activeClassName="active">
                        <span className="navbar-item">About</span>
                    </NavLink>
                    <NavLink to="/somepage" activeClassName="active">
                        <span className="navbar-item">404 Page</span>
                    </NavLink>
                </div>

            </nav>
          </header>

          <div className="container">
              <Switch>
                  {/*The routes array is used here and is iterated through to build the different routes needed for the app*/}
                  {routes.map((route,index) => (
                      <Route key={index} path={route.path} component={route.component} exact={route.exact} />
                  ))}
                  <Route component={NotFound}/>
              </Switch>
          </div>
      </div>
    )
  }
}              

在上面的代码块中,将routes从前一个文件导出的数组导入以供使用,然后在Switch组件内部routes迭代该数组以构建应用程序所需的不同路由。

要查看实际的服务器端渲染,请在终端中运行命令node server.js,以在开发模式下运行该应用程序。一旦启动并运行该应用程序,请导航至http://localhost:9000该应用程序运行所在的端口或任何端口,该应用程序应能正常加载,并且类似于下面的屏幕快照。

要检查该应用程序是否真正在服务器端呈现,请右键单击该页面,然后单击“查看页面源” ,您将看到页面的内容完全呈现,而不是从JavaScript文件呈现。

参考

Advanced React Router concepts: Recursive path, code splitting, and more

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容