客户端路由(使用 react-router)和服务端路由
Client Routing (using react-router) and Server-Side Routing
我一直在想,我对Client和Server之间的路由感到困惑。假设我在将请求发送回网络浏览器之前使用 ReactJS 进行服务器端渲染,并使用 react-router 作为客户端路由在页面之间切换而不刷新为 SPA。
想到的是:
- 如何解释路线?例如,从主页 (
/home
) 到帖子页面 (/posts
) 的请求
- 路由到哪里去,服务器端还是客户端?
- 它怎么知道它是如何处理的?
请注意,此答案涵盖 React Router 版本 0。13.x - upcoming version 1.0 看起来会有明显不同的实现细节
服务器
这是一个最小的server.js
react-router:
var express = require('express')
var React = require('react')
var Router = require('react-router')
var routes = require('./routes')
var app = express()
// ...express config...
app.use(function(req, res, next) {
var router = Router.create({location: req.url, routes: routes})
router.run(function(Handler, state) {
var html = React.renderToString(<Handler/>)
return res.render('react_page', {html: html})
})
})
其中 routes
模块导出路由列表:
var React = require('react')
var {DefaultRoute, NotFoundRoute, Route} = require('react-router')
module.exports = [
<Route path="/" handler={require('./components/App')}>
{/* ... */}
</Route>
]
每次向服务器发出请求时,您都会创建一个一次性 Router
实例,该实例配置了传入的 URL 作为其静态位置,该实例针对路由树进行解析设置适当的匹配路由,使用要呈现的顶级路由处理程序回调,并记录每个级别匹配的子路由。这是当您在路由处理组件中使用 <RouteHandler>
组件来呈现匹配的子路由时所参考的内容。
如果用户关闭了 JavaScript,或者加载速度很慢,他们点击的任何 link 都会再次访问服务器,如上再次解决。
客户
这是带有 react-router 的最小 client.js
(重新使用相同的路由模块):
var React = require('react')
var Router = require('react-router')
var routes = require('./routes')
Router.run(routes, Router.HistoryLocation, function(Handler, state) {
React.render(<Handler/>, document.body)
})
当您调用 Router.run()
时,它会在后台为您创建一个 Router 实例,每次您在应用程序中导航时都会重新使用该实例,因为 URL 可以是动态的客户端,而不是在单个请求具有固定 URL.
的服务器上
在本例中,我们使用 HistoryLocation
,它使用 History
API to make sure the right thing happens when you hit the back/forward button. There's also a HashLocation
which changes the URL hash
to make history entries and listens to the window.onhashchange
事件触发导航。
当你使用 react-router 的 <Link>
组件时,你给它一个 to
属性,它是路由的名称,加上任何 params
和 query
数据路线需要。此组件呈现的 <a>
有一个 onClick
处理程序,它最终使用您提供给 link 的道具在路由器实例上调用 router.transitionTo()
,看起来像这样:
/**
* Transitions to the URL specified in the arguments by pushing
* a new URL onto the history stack.
*/
transitionTo: function (to, params, query) {
var path = this.makePath(to, params, query);
if (pendingTransition) {
// Replace so pending location does not stay in history.
location.replace(path);
} else {
location.push(path);
}
},
对于常规 link 这最终会在您使用的任何位置类型上调用 location.push()
,它会处理设置历史记录的细节,以便使用后退和前进按钮进行导航,然后回调 router.handleLocationChange()
让路由器知道它可以继续转换到新的 URL 路径。
路由器然后使用新的 URL 调用自己的 router.dispatch()
方法,该方法处理确定哪些已配置路由与 URL 匹配的细节,然后调用任何 transition hooks 存在于匹配的路由中。您可以在任何路由处理程序上实现这些转换挂钩,以便在路由即将离开或导航到时采取一些行动,如果事情不符合您的喜好,则可以中止转换。
如果转换没有中止,最后一步是使用顶级处理程序组件和状态对象调用您提供给 Router.run()
的回调,其中包含 URL 和匹配的路线。顶级处理程序组件实际上是 Router
实例本身,它处理渲染匹配的最顶级路由处理程序。
每次您在客户端导航到新的 URL 时,上述过程都会重新 运行。
示例项目
在 1.0 中,React-Router 依赖 history 模块作为 peerDependency。该模块处理浏览器中的路由。默认情况下,React-Router 使用 HTML5 History API (pushState
, replaceState
),但您可以将其配置为使用基于哈希的路由(见下文)
路由处理现在在幕后完成,当路由发生变化时,ReactRouter 会向 Route 处理程序发送新的 props。每当路由发生变化时,路由器都会有一个新的 onUpdate
道具回调,用于页面浏览跟踪或更新 <title>
,例如。
客户端(HTML5路由)
import {Router} from 'react-router'
import routes from './routes'
var el = document.getElementById('root')
function track(){
// ...
}
// routes can be children
render(<Router onUpdate={track}>{routes}</Router>, el)
客户端(基于哈希的路由)
import {Router} from 'react-router'
import {createHashHistory} from 'history'
import routes from './routes'
var el = document.getElementById('root')
var history = createHashHistory()
// or routes can be a prop
render(<Router routes={routes} history={history}></Router>, el)
服务器
在服务器上,我们可以使用ReactRouter.match
,这个取自server rendering guide
import { renderToString } from 'react-dom/server'
import { match, RoutingContext } from 'react-router'
import routes from './routes'
app.get('*', function(req, res) {
// Note that req.url here should be the full URL path from
// the original request, including the query string.
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
if (error) {
res.status(500).send(error.message)
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search)
} else if (renderProps) {
res.status(200).send(renderToString(<RoutingContext {...renderProps} />))
} else {
res.status(404).send('Not found')
}
})
})
我一直在想,我对Client和Server之间的路由感到困惑。假设我在将请求发送回网络浏览器之前使用 ReactJS 进行服务器端渲染,并使用 react-router 作为客户端路由在页面之间切换而不刷新为 SPA。
想到的是:
- 如何解释路线?例如,从主页 (
/home
) 到帖子页面 (/posts
) 的请求
- 路由到哪里去,服务器端还是客户端?
- 它怎么知道它是如何处理的?
请注意,此答案涵盖 React Router 版本 0。13.x - upcoming version 1.0 看起来会有明显不同的实现细节
服务器
这是一个最小的server.js
react-router:
var express = require('express')
var React = require('react')
var Router = require('react-router')
var routes = require('./routes')
var app = express()
// ...express config...
app.use(function(req, res, next) {
var router = Router.create({location: req.url, routes: routes})
router.run(function(Handler, state) {
var html = React.renderToString(<Handler/>)
return res.render('react_page', {html: html})
})
})
其中 routes
模块导出路由列表:
var React = require('react')
var {DefaultRoute, NotFoundRoute, Route} = require('react-router')
module.exports = [
<Route path="/" handler={require('./components/App')}>
{/* ... */}
</Route>
]
每次向服务器发出请求时,您都会创建一个一次性 Router
实例,该实例配置了传入的 URL 作为其静态位置,该实例针对路由树进行解析设置适当的匹配路由,使用要呈现的顶级路由处理程序回调,并记录每个级别匹配的子路由。这是当您在路由处理组件中使用 <RouteHandler>
组件来呈现匹配的子路由时所参考的内容。
如果用户关闭了 JavaScript,或者加载速度很慢,他们点击的任何 link 都会再次访问服务器,如上再次解决。
客户
这是带有 react-router 的最小 client.js
(重新使用相同的路由模块):
var React = require('react')
var Router = require('react-router')
var routes = require('./routes')
Router.run(routes, Router.HistoryLocation, function(Handler, state) {
React.render(<Handler/>, document.body)
})
当您调用 Router.run()
时,它会在后台为您创建一个 Router 实例,每次您在应用程序中导航时都会重新使用该实例,因为 URL 可以是动态的客户端,而不是在单个请求具有固定 URL.
在本例中,我们使用 HistoryLocation
,它使用 History
API to make sure the right thing happens when you hit the back/forward button. There's also a HashLocation
which changes the URL hash
to make history entries and listens to the window.onhashchange
事件触发导航。
当你使用 react-router 的 <Link>
组件时,你给它一个 to
属性,它是路由的名称,加上任何 params
和 query
数据路线需要。此组件呈现的 <a>
有一个 onClick
处理程序,它最终使用您提供给 link 的道具在路由器实例上调用 router.transitionTo()
,看起来像这样:
/**
* Transitions to the URL specified in the arguments by pushing
* a new URL onto the history stack.
*/
transitionTo: function (to, params, query) {
var path = this.makePath(to, params, query);
if (pendingTransition) {
// Replace so pending location does not stay in history.
location.replace(path);
} else {
location.push(path);
}
},
对于常规 link 这最终会在您使用的任何位置类型上调用 location.push()
,它会处理设置历史记录的细节,以便使用后退和前进按钮进行导航,然后回调 router.handleLocationChange()
让路由器知道它可以继续转换到新的 URL 路径。
路由器然后使用新的 URL 调用自己的 router.dispatch()
方法,该方法处理确定哪些已配置路由与 URL 匹配的细节,然后调用任何 transition hooks 存在于匹配的路由中。您可以在任何路由处理程序上实现这些转换挂钩,以便在路由即将离开或导航到时采取一些行动,如果事情不符合您的喜好,则可以中止转换。
如果转换没有中止,最后一步是使用顶级处理程序组件和状态对象调用您提供给 Router.run()
的回调,其中包含 URL 和匹配的路线。顶级处理程序组件实际上是 Router
实例本身,它处理渲染匹配的最顶级路由处理程序。
每次您在客户端导航到新的 URL 时,上述过程都会重新 运行。
示例项目
在 1.0 中,React-Router 依赖 history 模块作为 peerDependency。该模块处理浏览器中的路由。默认情况下,React-Router 使用 HTML5 History API (pushState
, replaceState
),但您可以将其配置为使用基于哈希的路由(见下文)
路由处理现在在幕后完成,当路由发生变化时,ReactRouter 会向 Route 处理程序发送新的 props。每当路由发生变化时,路由器都会有一个新的 onUpdate
道具回调,用于页面浏览跟踪或更新 <title>
,例如。
客户端(HTML5路由)
import {Router} from 'react-router'
import routes from './routes'
var el = document.getElementById('root')
function track(){
// ...
}
// routes can be children
render(<Router onUpdate={track}>{routes}</Router>, el)
客户端(基于哈希的路由)
import {Router} from 'react-router'
import {createHashHistory} from 'history'
import routes from './routes'
var el = document.getElementById('root')
var history = createHashHistory()
// or routes can be a prop
render(<Router routes={routes} history={history}></Router>, el)
服务器
在服务器上,我们可以使用ReactRouter.match
,这个取自server rendering guide
import { renderToString } from 'react-dom/server'
import { match, RoutingContext } from 'react-router'
import routes from './routes'
app.get('*', function(req, res) {
// Note that req.url here should be the full URL path from
// the original request, including the query string.
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
if (error) {
res.status(500).send(error.message)
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search)
} else if (renderProps) {
res.status(200).send(renderToString(<RoutingContext {...renderProps} />))
} else {
res.status(404).send('Not found')
}
})
})