译者前言

毫无疑问,你肯定是在找一篇react的tutorial,一篇真正的tutorial. 和你一样,最近的半个月我一直茶饭不思,看了各种各样的react的入门教程, 甚至尝试了使用react的终极杀器: react-redux-starter-kit, 所谓的入门脚手架(绞手架?),里面需要这些知识储备:

  • React
  • Redux
  • react-router
  • webpack
  • Koa
  • Karma
  • Mocha
  • PhantomJS
  • ESLint
  • Babel

这场残暴的欢愉,终将以残暴结束

看完一遍我差点一口老血吐出,心情和"在2016年学习javascript是怎样一种体验"如出一辙。 当然,如果你有足够的自信,它还是一篇很不错的文章,针对github项目react-redux-starter-kit 的一个教程,看一看无妨,链接

废话少说,我之所以要翻译这系列react入门的三篇文章,是因为看了之后有种 茅塞顿开的感觉,起码可以使用react + redux搭建一个简单的单页面应用。 我个人认为,看这篇翻译之前,你最好先做到以下几点:

  • 知道react组件的用法, 基本了解,看过react官网的demo。
  • 知道redux的基本用法,我们为什么要使用redux?相比单纯使用react有什么优点?
  • 了解react-router的使用,github上的react router tutorial是一个非常好的demo.
  • 知道npm管理js的包,webpack的使用,以及它们的一些配置文件等。
  • 有一定的函数式编程基础,这会让你非常快的接受react的思想。当然即使你一点都不具备也没关系,这也许会为你打开一扇通往全新世界的窗口。

最后,在开始翻译之前,还是要感谢英文的原创作者Brad Westfall 为我们开启react的潘多拉魔盒

查看系列文章的代码

这里的三篇文章都有代码托管在github. 请随意clone上面的代码。 通过这一系列,我们将会构建一个简单的SPA,里面会涉及用户操作和小部件。

简而言之,我们现在假设这里的代码都可以从CDN中获取React, React Router,所以下面的例子中你 看不到有require()或者import出现。在本文最后,为了让你更容易理解github上的代码, 我们将会介绍Webpack和Babel。届时将会是ES6代码!

React-Router

React不是一个框架,只是一个库。因此,它没有给我们提供解决构建一个应用的所有解决方案。 React给我们一种非常棒的创建组件的方式以及提供了一个管理state的系统,但是真正构建一个 更加复杂的SPA需要一些别的扩展.我们首先来看React Router

如果你之前用过前端路由,这里很多概念都会很相似。与我之前用过的其他路由不同的是,React Router 使用JSX语法,一开始看起来或许有些许差异。

下面是一个入门级的组件:

// JSX
var Home = React.createClass({
  render: function() {
    return (<h1>Welcome to the Home Page</h1>);
  }
});

ReactDOM.render((
  <Home />
), document.getElementById('root'));

然后是Home组件在React Router中渲染的例子:

// JSX

...

ReactDOM.render((
  <Router>
    <Route path="/" component={Home} />
  </Router````````>
), document.getElementById('root'));

注意,<Router><Route>是不同的。它们都是react组件,但是它们并不会自己去真正创建DOM. 看起来感觉比较像是将<Router>直接渲染到挂载点root中,其实我们只是定义了应用运行的 一些路由规则。在后面,你将会接触到这样一些概念: 组件经常不是独自存在去创建DOM,而是和其他的 一些组件相互协作来完成这件事情。

在上面例子中,<Route>定义了一条路由规则,当访问路径/的时候,会将Home组件渲染到挂载点root中。

多条路由

在上一个例子中,只有一条简单的路由。它看起来并不是很有价值,因为我们即使不适用router也可以将组件Home 渲染到root中。

当我们要在不同的路由渲染不同的组件的时候,react router开始威力初现:

// JSX

ReactDOM.render((
  <Router>
    <Route path="/" component={Home} />
    <Route path="/users" component={Users} />
    <Route path="/widgets" component={Widgets} />
  </Router>
), document.getElementById('root'));

每一个<Route>组件,会在当前的路径与自己定义的URL匹配的时候,渲染相应的组件。 上面例子中三个组件每次只有有一个被渲染到root中。有了这种方法,我们可以只将router 挂载到root一次,在每次切换路径的时候回自动切换渲染的组件。

还有一个我们值得注意的地方,切换路径的时候我们并不需要真正去向服务器发请求,而每一个 组件都可以是一个新的页面。

复用Layout

我们现在简单的开始一个单页面应用(Single Page Application). 当然,我们可以 将三个组件写出全HTML的页面,但是这样有利于代码复用吗?可以改进的地方是三个组件共享了 header和sidebar,因此我们应该如何去避免在组件中写重复的代码?

想象一下我们的应用通过这样的方式组装:

image 1

当你开始思考怎样将这个页面拆成一些可复用的模块,你也许会想到这样的方式:

image 2

以嵌套的形式来构思组件和图层会让我们更加容易创建可复用的代码

也许忽然间,画图部门会告诉你,我们的应用需要一个搜索装置来提供搜索用户功能。因为User ListWidget List两个组件在搜索的时候需要类似的界面,因此将Search Layout设计为与这两个组件 分离开的一个组件会更好:

image 3

现在,Search Layout可以是任意一个需要搜索页面的一个父模板。需要搜索功能的页面可以使用 Search Layout,其他一些不用的可以用Main Layout:

image 4

如果你使用过任意的模板系统,这会是一种非常常见的策略。现在让我们来处理HTML. 我们先暂时使用静态 HTML而不包括Javascript:

<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>

记住,root结点总是会被展示出来,因为这个是HTML的body中仅有的结点。称之为root绝非 浪得虚名,因为这是整个react应用挂载到HTML中的点,但这个叫法没有一个约定俗成的名字。 我在接下来的部分都会称之为root。要知道直接挂载到<body>结点是不好的做法, 参考

在创建静态HTML之后,将它转换为React组件:

// JSX

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>
        <aside className="primary-aside"></aside>
        <main>
          {this.props.children}
        </main>
      </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>
    );
  }
});

不要太纠结LayoutComponent的区别。例子中的三个都是React的组件。 我将其中的两个命名为Layout是因为它们确实扮演了Layout的角色。

我们将使用"嵌套路由"的技术将UserList组件放到SearchLayout中,然后再 放到MainLayout中。首先要明白的一点是我们将UserList放在SearchLayout中 的时候,将会在父亲SearchLayout中的this.props.children位置展示。 组件如果嵌套到父组件中,父组件将自动拥有this.props.children这个属性,一个没有子组件的组件, 它的this.props.children属性将会是null我们将使用"嵌套路由"的技术将UserList组件放到SearchLayout中,然后再 放到MainLayout中。首先要明白的一点是我们将UserList放在SearchLayout中 的时候,将会在父亲SearchLayout中的this.props.children位置展示。 组件如果嵌套到父组件中,父组件将自动拥有this.props.children这个属性,一个没有子组件的组件, 它的this.props.children属性将会是null.

嵌套路由

所以我们怎样将组件嵌套起来?答案当然是嵌套路由:

// JSX

ReactDOM.render((
  <Router>
    <Route component={MainLayout}>
      <Route component={SearchLayout}>
        <Route path="users" component={UserList} />
      </Route>
    </Route>
  </Router>
), document.getElementById('root'));

组件嵌套的形式就如<Route>标签里面一样。当用户访问/users路径, React Router将会由外到内展示MainLayout -> SearchLayout -> UserList. 嵌套起来的组件置于root结点中展示.

上面例子中我们为了简单说明,没有指定用户访问主页路径(/)时的规则, 现在把它们加进来:

// JSX

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'));

你也许已经注意到,Route组件可以有两种写法:<Route />或者<Route>...</Route>. 这在JSX语法中对于所有组件都是可行的,如<div />在渲染时会转化为<div></div> 上面中的WidgetList可以认为类似UserList

正如<Route component={SearchLayout}>组件有两个子路由,用户在访问 /users或者/widgets的时候,React Router会自动选择对应的组件来渲染到 SearchLayout中.

还有一点,因为在路由嵌套中HomeSearchLayout所在的<Route>是平行的关系, 所以Home将直接放到MainLayout中而不会引入SearchLayout.由此可以看出你可以通过 控制Route嵌套的方式随心所欲的嵌套组件。

IndexRoutes

你肯定会发现,实现刚才的路由层级其实还有另外一种方法来做同样的事情,所以不用担心你的写法, 路由展示的正如你所想的,上面例子中的另一种写法如下:

// JSX

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'));

注: IndexRoute表示"/"路由下默认的渲染。

可选属性

有时候一个<Route>可能拥有component属性而path为空,正如上面的SearchLayout. <Route>还可以拥有path属性而component为空,如下面的例子:

// JSX

<Route path="product/settings" component={ProductSettings} />
<Route path="product/inventory" component={ProductInventory} />
<Route path="product/orders" component={ProductOrders} />

很明显三个Route的path中都有同样的"product"前缀,我们可以将它抽离出来,而使用一个 新的<Route>将它们包装起来:

// JSX

<Route path="product">
  <Route path="settings" component={ProductSettings} />
  <Route path="inventory" component={ProductInventory} />
  <Route path="orders" component={ProductOrders} />
</Route>

这个例子中的React Router再次表现了它灵活的表现力。 考验你一下: 你是否注意到这个例子 中存在一个小问题? 就是当我们访问/product路径的时候没有对应的规则。 为了修复这个问题,我们增加一条IndexRoute:

// JSX

<Route path="product">
  <IndexRoute component={ProductProfile} />
  <Route path="settings" component={ProductSettings} />
  <Route path="inventory" component={ProductInventory} />
  <Route path="orders" component={ProductOrders} />
</Route>

在React Router中使用而不是

在给路由创建链接的时候,你需要使用<Link to="">而不是<a href="">. 不用担心,它们只是类似的用法,React Router会替你处理剩下的事情。在我们的MainLayout 中加入一些链接:

// JSX

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>
    );
  }
});

JSX中的<Link to="/users" className="users"> 将会渲染成<a href="/users" class="users"> 如果你需要创建站外的链接,使用普通的链接标签即可。参考documentation for IndexRoute and Link

Active Links

使用<Link>一个非常酷的地方是它知道自己当前是否是激活状态:

// JSX

<Link to="/users" activeClassName="active">Users</Link>

如果用户当前就在/users路径下,路由会自动找到这个<Link>并给予它active的属性

浏览历史

为了不让你难过,我留着一个很重要的细节现在才说。<Router>需要知道浏览历史的追踪策略, 见:链接

React Router文档中建议使用browserHistory 如下:

// JSX

var browserHistory = ReactRouter.browserHistory;

ReactDOM.render((
  <Router history={browserHistory}>
    ...
  </Router>
), document.getElementById('root'));

之前的例子中我们没有使用history属性,因此默认会使用hashHistory 顾名思义,它会使用一个#哈希值加到URL中,来管理前端单页面的路由,也许你曾在Backbone.js中 见过类似用法。

使用hashHistory,URLs会长这个样子:

你也许无法忍受URL如此"怪异"

使用了browserHistory之后,一切都美好起来:

在前端使用browserHistory的时候在服务器端有一点需要注意。如果用户开始访问example.com, 然后点击跳转到/users/widgets,React Router可以正常的提供路由。然而, 如果用户直接在网址栏输入example.com/widgets,或者用户在example.com/widgets页面 点击刷新,浏览器会向服务器请求/widgets.因为我们没有在服务器端设置这个路由, 因此会出现404:

image 5

为了在服务器解决这个404问题,React Router 建议在服务器使用wildcart router 有了这种方法,无论向服务器请求哪个路径,服务器都会返回同样的HTML文件,而具体的路由React Router 在前端会为我们搞定。

用户也许没注意到什么奇怪的地方,但你可能担心永远使用同一个HTML页面。在我们后面的例子中, 将会一直使用wildcard router的方法,但你可以自己选择怎样在服务器端解决请求不同路由的问题。

使用React Router能否做到前端展示的路由和服务器端的同构(isomorphic way)? 当然可以,但这已经不是我们说好的"入门"范畴。

使用browserHistory的重定向

你可以在任意文件中使用browserHistory对象,如果你要实现重定向,使用push方法:

// JSX

browserHistory.push('/some/path');

路由参数捕获

React Router使用和其他路由类似的方法来处理route matching

// JSX

<Route path="users/:userId" component={UserProfile} />

这个路由可以匹配users/前缀的任何路径,例如/users/1, /users/143, 甚至/users/abc (你需要自己处理这种id= =)

:userId可以捕获参数作为prop传到UserProfile,在UserProfile中可以通过访问 this.props.params.userId来访问。

Demo

我们现在已经完成了一个简单demo的所有代码。 点击查看

如果你点击了几个页面,你会发现浏览器的前进回退功能同样可以工作。这是使用history策略的一个 很重要的原因。你还会发现,你点击每个路由的时候,浏览器不会向服务器发出请求,除了第一次加载 页面的时候,酷吧?

ES6

在我们的例子中,React, ReactDOMReactRouter都是来自于CDN的全局变量。 而Router, Route都是从ReactRouter中引入,一种用法如下:

// JSX

ReactDOM.render((
  <ReactRouter.Router>
    <ReactRouter.Route ... />
  </ReactRouter.Router>
), document.getElementById('root'));

如果你觉得每次加个ReactRouter麻烦,可以使用ES6新的 解构语法 如下:

// JSX

var { Router, Route, IndexRoute, Link } = ReactRouter

然后你就可以直接使用Router, Route等.

从现在开始,我们例子中将会使用很多的ES6语法,包括destructuring, spread operator, imports/exports,或许还有其他。使用它们将会使我们的React代码更加优雅。

使用webpack和Babel打包

前面提到我们的代码托管在github 中,请clone下来do it by yourself. 我们将使用 webpackBabel 来构建真正的SPA.

  • webpack将多个js文件打包成浏览器可以加载的单个文件
  • Babel可以将ES6语法的js代码转译为ES5,因为目前很多浏览器还不能解析ES6.相信以后浏览器将会支持ES6语法也不再需要Babel

如果使用的这些东西你感到不熟悉,不用担心,我们github项目中的代码已经为你做好了所有事情, 让你可以专注于编写React代码。请阅读项目中的README.md

注意过期的语法

使用谷歌搜索的时候,你会发现很多StackOverflow中的问题是在React Router 1.0版本的时候 的文章。很多pre-1.0里面的特性现在已经过期(请使用新的用法代替):

  • <Route name="" />已过期,使用Route path="" />代替
  • <Route handler="" /> 已过期,使用<Route component="" />代替
  • <NotFoundRoute /> 已过期,查看代替
  • <RouteHandler /> 已过期
  • willTransitionTo 已过期,使用onEnter
  • willTransitionFrom 已过期,使用onLeave
  • Locations现在叫做histories

完整的列表参考 1.0.02.0.0

总结

我们还有很多React Router的特性没有介绍,请查看API文档 React Router的作者还有一个非常好的step-by-step tutorial, 还有视频关于React Router是如何诞生的

特别感谢Lynn Fisher的美术作品@lynnandtonic