该篇为翻译文章:原文为:Leveling Up With React: React Router
该系列文章目录:
第一部分:学习React Router(当前)
第二部分:React的“容器组件”和“可视化组件”-译文(原文)
第三部分:React 之 Redux-译文(原文)
当我刚开始学习React
的时候,我看了很多入门教程(比如: 1, 2, 3, 4)。很多教程都只是教我们如何做一个组件放到页面当中,这些教程其实都只是讲了一些JSX
的基础,但我想给大家介绍的远远不只这些,而是更广泛的一些东西,比如一个真正的单页面应用(SPA)。所以,看这篇文章之前,你要有一些React
的基础。
源码
文章所写Demo的源码可以到GitHub进行下载。
为了使例子看起来一目了然,React
和React Router
都是用的CDN版本。所以在例子当中你不会看到require()
或import
这些。但在这篇文章结束的时候,我们将介绍使用Webpack和Babel,这将全部使用ES6!
React-Router
首先,React
并不是一个框架,它只是一个UI库。因此,它不能解决所有问题。我们可以用它来创建一些组件和管理组件状态,但如果单单靠它来做一个复杂的SPA应用,是远远不够的。如果用React来做应用,我们还需要其它的东西来支持。React Router
是最我们第一个要用到的。
如果你之前用过其它的前端路由管理的库,会发现它们都是差不多的。但是React Router
是不同的,它使用的是JSX语法,可能刚用的时候会有点奇怪。
它用起来就像一般的组件渲染一样:
var Home = React.createClass({
render: function() {
return (<h1>Welcome to the Home Page</h1>);
}
});
ReactDOM.render((
<Home />
), document.getElementById('root'));
上面的Home
组件如果用React Router
渲染的话是这样的:
ReactDOM.render((
<Router>
<Route path="/" component={Home} />
</Router>
), document.getElementById('root'));
注意,上面的<router>
和<route>
是两个不同的东西。可以看出,它们都是React
组件,但是它们不会创建出DOM元素。这些组件只是定义了一些让整个应用跑起来的规则。通过这些你会发现,有时候组件不是直接生成DOM元素的,而是为了配合其它组件的生成而存在的。
</route><route>
定义了一个规则,当我访问首页/
的时候,那么此时,Home
组件将会被渲染到ID为root
元素当中去。
配制多个路由规则
前面我们配制了一个首页,但实际项目当中不可能只有一个页面的。使用React Router
来配制多个路由也非常的简单:
ReactDOM.render((
<Router>
<Route path="/" component={Home} />
<Route path="/users" component={Users} />
<Route path="/widgets" component={Widgets} />
</Router>
), document.getElementById('root'));
每一个</route><route>
都指定了一个组件和路径。当我们访问相应路径的时候,那么它就会渲染不同的组件到root
当中去。
可重用的布局
我们已经看到了一个单页面应用的雏形,真正的SPA应用不只是这样就OK了,通过知道了上面的路由配制,我们显然知道了每一个组件就是一个完整的HTML页面,但是如何做到代码的重用呢?比如,一个网站有相同的头部、侧边栏、底部等,我们如何把它们做成可以重要使用的组件呢?
比如我们现在要做这样一个东西:
把这个模型拆开,做到重用度最高,你可能会这样做:
突然,你接到一个要求,就是不但要搜索User,还要搜索Widget,但这个功能的界面和User的搜索列表是一样的。那么现在Widget搜索页的组件的划分是这样的:
“搜索”的布局就可以这样进行划分,即有一个组件作为父层,然后把不同的搜索结果放到这个父层组件当中:
其实这是一种非常常规的策略,你以前可能用其它的模板引擎做过相同的划分。现在,我们通过HTML来看一下:
<div id="root">
<!-- Main Layout -->
<div class="app">
<header class="primary-header"><header>
<aside class="primary-aside"></aside>
<main>
<!-- Search Layout -->
<div class="search">
<header class="search-header"></header>
<div class="results">
<!-- User List -->
<ul class="user-list">
<li>Dan</li>
<li>Ryan</li>
<li>Michael</li>
</ul>
</div>
<div class="search-footer pagination"></div>
</div>
</main>
</div>
</div>
HTML写好后,我们来把它改成React组件:
var MainLayout = React.createClass({
render: function() {
// Note the `className` rather than `class`
// `class` is a reserved word in JavaScript, so JSX uses `className`
// Ultimately, it will render with a `class` in the DOM
return (
<div className="app">
<header className="primary-header"></header><header>
<aside className="primary-aside"></aside>
<main>
{this.props.children}
</main>
</header></div>
);
}
});
var SearchLayout = React.createClass({
render: function() {
return (
<div className="search">
<header className="search-header"></header>
<div className="results">
{this.props.children}
</div>
<div className="search-footer pagination"></div>
</div>
);
}
});
var UserList = React.createClass({
render: function() {
return (
<ul className="user-list">
<li>Dan</li>
<li>Ryan</li>
<li>Michael</li>
</ul>
);
}
});
我们利用路由的嵌套将UserList
放到了SearchLayout
里,然后再放到MainLayout
里。注意,这时React
是在父层当中通过this.props.children
来找到子级并放进来的。所有的组件里面都会有一个this.props.children
,但只有当这个组件有嵌套组件时,这个属性才会起作用,否则它将是null
。
嵌套路由
上面的组件写完之后,那么我们的路由就可以这样设置:
ReactDOM.render((
<Router>
<Route component={MainLayout}>
<Route component={SearchLayout}>
<Route path="users" component={UserList} />
</Route>
</Route>
</Router>
), document.getElementById('root'));
组件将会按照这个路由的嵌套来进行嵌套。当我们访问/users
时,React Router
将会把UserList
放到SearchLayout
里,然后SearchLayout
放到MainLayout
里,最后整个会放到root
当中。
注意,我们并没有设置访问/
时的首页路由,也没有设置搜索widgets的路由,现在我们把它们加上:
ReactDOM.render((
<Router>
<Route component={MainLayout}>
<Route path="/" component={Home} />
<Route component={SearchLayout}>
<Route path="users" component={UserList} />
<Route path="widgets" component={WidgetList} />
</Route>
</Route>
</Router>
), document.getElementById('root'));
你会发现,JSX是遵循XML的语法的,比如一个组件可以写成一个单标签:</route><route></route>
或者分开写:<route>...</route>
。实际上,JSX里面不只有自定义的组件 ,还有真正的DOM元素,比如<div></div>
在JSX会被解析渲染成DOM元素<div></div>
。
<route component={SearchLayout}>
现在有两个子路由,如果访问/users
或者/widgets
,那么就会加载相应的组件。
IndexRoutes
React Router还有许多其它的用法,比如上面例子的写法还可以写成这样:
ReactDOM.render((
<Router>
<Route path="/" component={MainLayout}>
<IndexRoute component={Home} />
<Route component={SearchLayout}>
<Route path="users" component={UserList} />
<Route path="widgets" component={WidgetList} />
</Route>
</Route>
</Router>
), document.getElementById('root'));
Route 的属性
有时,</route><route>
指定了组件但是没有指定路径(path
),比如上面有一条设置了SearchLayout
组件,但没有path
。还有时候 ,可能只需要设置path
,而不需要指定组件,从这个例子开始:
<Route path="product/settings" component={ProductSettings} />
<Route path="product/inventory" component={ProductInventory} />
<Route path="product/orders" component={ProductOrders} />
/product
重复了好多次,我们可以把重复的部分换成一个新的</route><route>
:
<Route path="product">
<Route path="settings" component={ProductSettings} />
<Route path="inventory" component={ProductInventory} />
<Route path="orders" component={ProductOrders} />
</Route>
你会发现,当我们访问/product
的时候,此时它会不知道如何去渲染,这时候可以添加之前说到过的IndexRoute
:
<Route path="product">
<IndexRoute component={ProductProfile} />
<Route path="settings" component={ProductSettings} />
<Route path="inventory" component={ProductInventory} />
<Route path="orders" component={ProductOrders} />
</Route>
使用 <link />
代替 <a>
当我们在路由当中添加链接时,你需要使用<link to=""/>
来代替<a href="">
。当我们使用了<link />
组件时,React Router
会帮你在DOM当中渲染出a
标签。
让我们来添加一些链接到MainLayout
:
var MainLayout = React.createClass({
render: function() {
return (
<div className="app">
<header className="primary-header"></header>
<aside className="primary-aside">
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/users">Users</Link></li>
<li><Link to="/widgets">Widgets</Link></li>
</ul>
</aside>
<main>
{this.props.children}
</main>
</div>
);
}
});
<link />
的to
属性将最终被解析成a
标签的href
,比如:
<Link to="/users" className="users">
当渲染到DOM当中会变成:
<a href="/users" class="users">
如果你想写一个链到外面的链接 ,那么直接使用a
标签就可以了。更多资料请查看 IndexRoute和Link的官方文档。
当前Link
<link />
有一个很方便的属性来添加激活状态:
<Link to="/users" activeClassName="active">Users</Link>
如果现在访问/user
路径 ,那么React Router
会给<link />
生成的a
标签添加一个active
的class。更多请访问这里。
浏览器的历史记录
为了防止大家感到困惑,我到现在才说这个问题。<router>
需要一个历史管理工具 browserHistory:
var browserHistory = ReactRouter.browserHistory;
ReactDOM.render((
<Router history={browserHistory}>
...
</Router>
), document.getElementById('root'));
在React Router
之前的一些版本当中是没有history
属性不是必须的。默认是用hashHistory
,顾名思义,就是使用#
哈希符号来管理历史,比如这样:
example.com
example.com/#/users?_k=ckuvup
example.com/#/widgets?_k=ckuvup
当用了browserHistory
后,路径则会变成这样:
example.com
example.com/users
example.com/widgets
当你在前端使用了browserHistory
后,服务端有一点是有一点要非常注意的。比如你现在访问example.com
,然后通过里面的链接点到了/users
或者/widgets
,React Router
操作管理着这一切。但是,如果你直接在浏览器的地址栏里输入example.com/widgets
或者之前点击链接打开了example.com/widgets
,现在重新刷新了,那么浏览器就会向服务器进行发送请求了,请求什么呢?就是/widgets
。如果服务器端没有相应的反馈,那么就会返回404
了:
React Router
推荐在服务端使用通配符来防止404错误的发生。但是使用这种方法后,不管向服务器发送什么样的路由请求,服务器都会返回相同的HTML内容。如果现在直接访问example.com/widgets
,即使服务器返回的HTML内容和之前一样,React Router
也能够加载对的组件。
对于用户来说,只要前端的页面加载内容是正确的,他不会知道服务端返回了什么。但是对于我们开发者来说可能比较担心服务器老是返回相同的HTML会不会有什么问题。在我们的示例代码当中,将一直使用“通配符”这种方法,但是当我们在实际项目当的时候,你就需要根据你的需要来配制服务端的路由了。
使用browserHistory进行重定向
browserHistory
是一个单例对象,所以你可以在你的任何文章当中调用它。如果你需要在代码当中来进行手动的进行重定向,你可以使用这个方法:
browserHistory.push('/some/path');
路由的匹配
React router
当中路由的匹配和其它的方式是差不多的:
<Route path="users/:userId" component={UserProfile} />
当我们访问到以user/
开头,并且后面跟一个值的时候,比如/users/1
,/users/143
,都会进行匹配到。
React Router
将会把:userId
的值作为UserProfile
的属性传送过去。这个属性在UserProfile
当中可以用this.props.params.userId
进行获取到。
一个Demo
写到这里,我们完全可以做出一个简单的Demo出来了。
See the Pen React-Router Demo by Brad Westfall (@bradwestfall) on CodePen.
上面的例子当中,当你点了链接之后,你会发现浏览器的后退和前进按钮是有用的,这就使用了历史管理。
ES6
上面的例子当中,React
,ReactDOM
和ReactRouter
都是用的CDN版本。在ReactRouter
当中有我们需要的Router
和Route
组件。所以我们也可以这样用:
ReactDOM.render((
<ReactRouter.Router>
<ReactRouter.Route ... />
</ReactRouter.Router>
), document.getElementById('root'));
但是,如果使用ES6的解构赋值语法,我们可以这样写:
var { Router, Route, IndexRoute, Link } = ReactRouter
译者:
如果不清楚ES6的解构赋值,可以看《ES6中的解构赋值》一文。
使用webpack 和 Babel进行打包
- Webpack主要是用来进行JS的打包。(当然它还有许多其它的用途)
- Babel主要是将ES6的语法转成ES5的讲法,因为现在不是所有浏览器都支持ES6的讲法。
如果你还不太习惯用这些工具,不用担心,在例子当中已经将所有的东西都设置好了,你只需要关注React
这一块就行。但是你仍然需要看一下文档(GitHub上面的README.md
)。
一些废弃的语法
当你在网上搜索关于React Router
的资料时,可能会搜索到很多,但是它们用的版本可能大部分都是pre-1.0
,实际上pre-1.0
版本当中有很多的语法已经被废弃了,这里列举一些:
<route name=""></route>
由<route path=""></route>
代替<route handler=""></route>
由<route component=""></route>
代替<notfoundroute></notfoundroute>
废弃,替代品点这里<routehandler></routehandler>
废弃willTransitionTo
废弃,使用onEnter属性替代willTransitionFrom
废弃,使用onLeave属性替代- “Locations” 现在叫做 “histories”.