深入了解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
对象定义子比赛将如何在不同的位置,如过渡atEnter
,atLeave
和atActive
。
上面已经提到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,如atEnter
,mapStyles
,atLeave
和atActive
,它的工作方式与使用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导入的react
,withRouter
也是从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
在上面的代码块,Link
用this.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服务器webpackDevMiddleware
和webpackHotMiddleware
。在文件的顶部,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,以便路由可以匹配,并且我们将在下文中讨论上下文属性。
只要客户端上有重定向,浏览器历史记录就会更改状态,我们会得到新的屏幕。在静态服务器环境中,我们无法更改应用程序状态。相反,我们使用context
props来找出渲染的结果。如果找到context.url
,则说明该应用已重定向。
那么,我们如何在服务器渲染的应用程序中实际定义路由和匹配组件?这是在src/router-config.js
和src/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