React入门与进阶之路由

zjdy4615 7年前
   <h2><strong>React入门与进阶之路由</strong></h2>    <p>在传统的网页应用中,一般是根据用户的操作指向不同的url,然后服务器渲染出不同的html代码,后来有了ajax,在同一页面里,可以为不同操作,指定处理器函数,在不刷新页面的情况下更新局部视图,但是局限依然较大,一旦跳转了URL,依然需要服务器渲染模板返回;而在Backbone,Angular,React出现以后,在单页面应用中,我们可以给不同URL指定处理器函数,保持URL与视图的同步,渲染模板的功能已经转移到客户端进行,与服务器的交互只涉及到数据,这就是路由的功能。</p>    <h2><strong>React中的路由</strong></h2>    <p>React是一个用户界面类库,仅相当于MVC模式中的V-view视图,其本身并不包含路由功能,但是它以模块的方式提供了路由功能,可以很好的与React进行协作开发,当然这也并不是必须使用的。现在很多单页应用框架都实现了各自的路由模块,如Backbone,Angular等。而这其中很多路由模块也能与React搭配使用。本文将介绍本人使用过的两种:Backbone.Router和react-router。</p>    <p>不使用路由模块,我们需要处理一个展示笔记列表和展示特定笔记对应不同视图时,React处理视图更新的方式大致如此:</p>    <pre>  <code class="language-javascript">    // 状态变量      var NOTINGSTATUS = {          SHOW: 0,          EDIT: 1      };        // 模拟数据      var notings ={          10001: {              id: 10001,              title: 'React',              content: 'React Router',              status: NOTINGSTATUS.SHOW          },          10002: {              id: 10002,              title: 'Backbone',              content: 'Backbone Router',              status: NOTINGSTATUS.EDIT          }      };        // React-Noting应用入口组件      var NotingApp = React.createClass({          getInitialState: function() {              return {                };          },          render: function() {              return (                  <div>                      <h1>React Noting</h1>                      <NotingList notings={this.props.notings.slice(0, this.props.indexNotingListLength)}/>                  </div>              );          }      });        // 展示列表组件      var NotingList = React.createClass({          getNotings: function() {              return notings;          },          getInitialState: function() {              return {                  notings: this.props.notings || this.getNotings()              };          },          render: function() {              var notings = this.state.notings;              var _notingList = [];                for (var key in notings) {                  _notingList.push(<NotingItem key={key} noting={notings[key]} />);              }              return (                  <div>                      <ul>{_notingList}</ul>                  </div>              );          }      });      // 展示特定项组件      var NotingItem = React.createClass({          getInitialState: function() {              return {                  noting: this.props.noting,                  status: this.props.noting.status              };          },          editNote: function() {              this.setState({                  status: NOTINGSTATUS.EDIT;              });          },          render: function() {              var status = this.state.status;              var noting = this.state.noting;                if (status === NOTINGSTATUS.EDIT) {                  return (                      <li>                          <input type="text" value={noting.title} autoFocus={true} />                      </li>                  );              }else if (status === NOTINGSTATUS.SHOW) {                  return (                      <li>                          <h3 onClick={this.editNote}>{noting.title}</h3>                      </li>                  );              }          }      });        var indexNotingListLength = 3;      React.render(          <NotingApp notings={notings} length={indexNotingListLength} />,          document.querySelector('.noting-wrap')      );</code></pre>    <p>如上,没有路由模块管理,只是通过返回的组件状态变更,更新对应视图,随着应用越来越大处理会越发繁琐,接下来我们通过使用路由管理URL和视图的同步的方式来达到相同目的。</p>    <h2><strong>Backbone.Router</strong></h2>    <p>Backbone是一种MVC(Model-View-Controller)模式的框架,其路由模块属于Controller层,独立于其他模块,可以很好的与React搭配使用,之前的例子使用Backbone.Router实现后如下:</p>    <pre>  <code class="language-javascript">    var NOTINGSTATUS = {          SHOW: 0,          EDIT: 1      };      var NOTINGROUTERTYPE = {          LIST: 0,          ITEM: 1      };        var NotingRouter = Backbone.Router.extend({          routes; {              '/': 'index',              'notings': 'showNotings',              'notings/:id': 'showNoting'          },          render: function(type, extraData) {              if (type === NOTINGROUTERTYPE.LIST) {                  React.render(                      <NotingList notings={notings} />,                      document.querySelector('.noting-wrap')                  );              }else if (type === NOTINGROUTERTYPE.ITEM) {                  React.render(                      <NotingItem noting={notings[extraData.id]} />,                      document.querySelector('.noting-wrap');                  );              }          },          index: function() {            },          showNotings: function() {              this.render(NOTINGROUTERTYE.LIST);          },          showNoting: function(id) {              this.render(NOTINGROUTERTYPE.ITEM, {id: id});          }      });        var notingRouter = new NotingRouter();      Backbone.history.start();</code></pre>    <p>如上,如果访问一个如/notings的URL地址,将会展示所以noting,而访问诸如/notings/123这种的URL则会展示特定id为123的noting。</p>    <h2><strong>react-router</strong></h2>    <p>注:react-router 1.0.x和0.13.x两个大版本之前API语法有差别,本文使用最新的语法介绍react-router。</p>    <p>我们在前文了解了Backbone.Router如何作为路由模块与React搭配使用,现在继续学习react-router。</p>    <p>不同于Backbone的路由模块,react-router完全由React组件(component)构成:路由本身被定义成组件使用,而其内部路由分发处理器也定义成组件的形式使用。</p>    <p>现在让给我们继续使用react-router来实现之前展示noting的例子的路由管理:</p>    <pre>  <code class="language-javascript">    var Router = require('react-router').Router;      var Route = require('react-router').Route;      var Link = require('react-router').Link;          /**       * NotingApp.js       * @author [惊鸿]       * @description [应用入口js]       * @Date 2016/09/23       */        var React = require('react');      var Link = require('react-router').Link;      var NotingDate = require('./NotingDate');        var NotingApp = React.createClass({            getInitialState: function() {              return {}          },          render: function() {              var date = new Date();                return (                  <div>                      <Link to="/index"><h2>React Noting</h2></Link>                      <NotingDate date={date} />                      {this.props.children}                  </div>              );          }      });        /**       * NotingList.js       */      var React = require('react');      var NotingAction = require('../actions/NotingActions');      var NotingStore = require('../stores/NotingStore');      var NotingInput = require('./NotingInput');      var NotingItem = require('./NotingItem');      var Utils = require('../commons/utils.js');      var ReactPropTypes = React.PropTypes;        var indexNotingLength = 3;      function getNotingState(length) {          var notings = NotingStore.getAllNotings();          var _notings;            _notings = Utils.sliceObj(notings, length);            return {              notings: _notings          };      }        var NotingList = React.createClass({          indexNotingLength: indexNotingLength,          isAutoFocus: true,          placeholderTxt: '添加一条新笔记',          propTypes: {              notings: ReactPropTypes.object          },          type: null,          getInitialState: function() {              var notings = getNotingState().notings;              var _notings = this.props.notings || notings;                this.type = this.props.params.type;              if (this.type === 'all') {                  _notings = notings;              }else {                  _notings = getNotingState(this.indexNotingLength).notings;              }                return {                  notings: _notings              };          },          render: function() {              var notings = this.state.notings;              var _notingList = [];                if (Object.keys(notings).length < 1) {                  return null;              }              for (var key in notings) {                  _notingList.push(<NotingItem key={key} noting={notings[key]} />);              }              return (                  <div className="notings-wrap">                      <NotingInput autoFocus={this.isAutoFocus} placeholder={this.placeholderTxt} onSave={this._onSave} />                      <ul className="noting-list">{_notingList}</ul>                  </div>              );          }      });        /**       * NotingItem.js       */      var React = require('react');      var Link = require('react-router').Link;      var ReactPropTypes = React.PropTypes;      var NotingStore = require('../stores/NotingStore');      var Utils = require('../commons/utils');        function getNotingState(id) {          var notings = NotingStore.getAllNotings();          return notings[id];      }        var NotingItem = React.createClass({          propTypes: {              noting: ReactPropTypes.object          },          getInitialState: function() {              return {                  noting: this.props.noting || getNotingState(this.props.params.id)              };          },          render: function() {              var _noting = this.state.noting;              var _btn;                if (!Utils.isPlainObject(_noting)) {                  return null;              }                if (this.props.params && this.props.params.id) {                  _btn = <Link to="/notings/all">Show More</Link>;              }else {                  _btn = <Link to={{pathname:'/noting/' + _noting.id}}>Read More</Link>;              }                return (                  <li className="notings-item" key={_noting.id}>                      <h3 className="noting-title">{_noting.title}</h3>                      <div className="noting-content">{_noting.content}</div>                      {_btn}                  </li>              );          }      });</code></pre>    <p>如上为React Component主要代码,路由模块代码如下:</p>    <pre>  <code class="language-javascript">    /**       * NotingRouter.js       * @description [React-Noting应用路由模块]       * @author [惊鸿]       * @Date 2016/10/07       */        var React = require('react');      var ReactRouter = require('react-router');      var Router = ReactRouter.Router;      var Route = ReactRouter.Route;      var IndexRoute = ReactRouter.IndexRoute;      var Link = ReactRouter.Link;      var browserHistory = ReactRouter.browserHistory;      var NotingApp = require('../components/NotingApp.js');      var NotingList = require('../components/NotingList.js');      var NotingItem = require('../components/NotingItem.js');        var NotingRouter = (          <Router history={browserHistory}>              <Route path="/" component={NotingApp}>                  <IndexRoute component={NotingList} />                  <Route path="notings/:type" component={NotingList} />                  <Route path="noting/:id" component={NotingItem} />              </Route>              <Route path="/index" component={NotingApp}>                  <IndexRoute component={NotingList} />              </Route>          </Router>      );        React.render(          NotingRouter,          document.querySelector('.noting-wrap')      );</code></pre>    <p>如上路由,用户访问跟路由/或/index时(比如http://localhost:3000/或http://localhost:3000/index),页面加载NotingApp组件,访问诸如/notings/all时将在NotingApp下加载NotingList组件,访问/noting/123路径时,则将在NotingApp下加载NotingItem组件。</p>    <h3><strong>获取URL参数</strong></h3>    <p>React路由支持我们通过 query 字符串来访问URL参数。比如访问 noting/1234?name=jh,你可以通过访问 this.props.location.query.来访问URL参数对象,通过this.props.location.query.name从Route组件中获得”jh”值。</p>    <h3><strong>路由配置</strong></h3>    <p>路由配置是一系列嵌套指令,它定义路由如何匹配URL和匹配URL后的操作(具体而言,即组件切换或更新)。</p>    <p><strong>Router</strong></p>    <p>React所有路由实例必须包含在< Router>标签内,以组件形式定义,该组件有history属性,其值有三种,将在后文介绍。</p>    <p><strong>Route</strong></p>    <p>定义具体路由节点使用Route指令,该指令有许多属性:</p>    <ul>     <li> <p>path</p> <p>声明该路由节点所匹配URL(绝对路径)或URL片段(相对路径)字符串值,若该属性未赋值,则表示此路由节点不需匹配URL,直接嵌套渲染组件。</p> </li>     <li> <p>component</p> <p>特定URL匹配到该节点时,渲染此路由节点定义的组件,如此,层层嵌套下去。</p> </li>    </ul>    <p>**this.props.children–Render,对应当前路由的默认子路由,将渲染返回默认子路由的组件。 **</p>    <p><strong>IndexRoute</strong></p>    <p>默认情况下,this.props.children值是undefined,即默认子路由不存在,我们可以通过IndexRoute指令指定路由的默认子路由,匹配且仅匹配到当前路由的URL将层层渲染路由组件到此默认子路由组件。</p>    <p><strong>Redirect</strong></p>    <p>顾名思义,这是一个重定向指令,很多时候我们可能改变了某些URL,之前的和改变后的URL都需要匹配该路由,这时,就需要使用Redirect指令:</p>    <pre>  <code class="language-javascript">    React.render((        <Router>          <Route path="/" component={App}>            <IndexRoute component={Dashboard} />            <Route path="about" component={About} />            <Route path="inbox" component={Inbox}>              {/* Redirect /inbox/messages/:id to /messages/:id */}              <Redirect from="messages/:id" to="/messages/:id" />            </Route>              <Route component={Inbox}>              <Route path="messages/:id" component={Message} />            </Route>          </Route>        </Router>      ), document.body);</code></pre>    <p><strong>onEnter 和onLeave钩子</strong></p>    <p>React路由也可以定义onEnter和onLeave钩子函数(Hooks),在路由变更发生时触发,分别表示在离开某路由,进入某路由触发定义的钩子回调函数。</p>    <ul>     <li> <p>onLeave</p> <p>路由变更时,首先触发onLeave钩子回调函数,且在离开的所有定义过的路由都会由外到内触发。</p> </li>     <li> <p>onEnter</p> <p>路由变更时,接着onLeave钩子回调函数执行后,会从外到内层层触发onEnter回调函数。</p> </li>    </ul>    <p><strong>JSX和Plain Object</strong></p>    <p>React提供JSX语法开发React应用,也支持原生JavaScript语法,路由模块也是,对于路由,可以使用Object对象的形式定义:</p>    <p>var routes = {</p>    <p>};</p>    <p>render( , document.body);</p>    <p>原生语法不支持Redirect指令,我们只能使用onEnter钩子,在回调函数里面处理重定向。</p>    <h3><strong>路由匹配(Route Matching)</strong></h3>    <p>如果想要明白路由如何匹配某特定URL,就必须学习路由的三个相关概念和属性:</p>    <ul>     <li> <p>嵌套关系(nesting)</p> </li>     <li> <p>路径语法(path)</p> </li>     <li> <p>优先级(precedence)</p> </li>    </ul>    <p><strong>嵌套关系(nesting)</strong></p>    <p>React路由嵌套路由的概念,就像DOM树一样;应用路由以嵌套的形式定义,路由对应的视图组件也形成嵌套,</p>    <p>当访问的特定URL层层向下匹配成功时,最后的匹配路由节点及其所有嵌套父级路由对应的组件都将被渲染。</p>    <p>React Router traverses the route config depth-first searching for a route that matches the URL.</p>    <p>React路由会对路由配置进行深度优先搜索,以找到匹配特定URL的路由。</p>    <p><strong>路径语法(path syntax)</strong></p>    <p>路由的path属性值是字符串类型,能匹配特定URL或该URL的部分,除了可能包含的以下特殊符号,该值都是按照字面量进行解释的:</p>    <ul>     <li> <p>:paramName 路由参数,匹配特定URL中/,?或#字符后面的部分</p> </li>     <li> <p>() 表示URL中该部分是可选的</p> </li>     <li> <p>匹配任意数量任意字符,直到下一个匹配点</p> </li>     <li> <p>** 匹配任意字符,直到遇见/,?或#字符,并且会产生一个splat参数</p> </li>    </ul>    <pre>  <code class="language-javascript">    <Route path="/notings/:id">        // matches /notings/23 and /notings/12345      <Route path="/notings(/:id)">      // matches /notings, /notings/23, and /notings/12345      <Route path="/blog/*.*">           // matches /blog/a.jpg and /blog/a.html      <Route path="/**/*.html">          // matches /blog/a.html and /blogs/demo/b.html</code></pre>    <p><strong>路由的相对路径与绝对路径</strong></p>    <ul>     <li> <p>绝对路径,即以/开头的路径字符串,是一个独立的路径</p> </li>     <li> <p>相对路径,不以/开头,相对于其父级路由路径值</p> </li>    </ul>    <p>If a route uses a relative path, it builds upon the accumulated path of its ancestors.</p>    <p>Nested routes may opt-out of this behavior by using an absolute path.</p>    <p>如果一个路由使用的是相对路径,则该路由匹配的URL是由该路由路劲及其所有祖先路由节点路径值从外到内拼接组成;</p>    <p>若使用的是绝对路径,则该路由就忽略嵌套关系直接匹配URL。</p>    <p><strong>优先级(precedence)</strong></p>    <p>路由算法按照路由定义的顺序,从上到下匹配URL,所以,在有两个或多个同级路由节点时,必须保证前面的路由不能与后面的路由匹配同一个URL。</p>    <h3><strong>History</strong></h3>    <p>History可以监听浏览器地址栏的变化并且能把URL解析成一个location对象,React路由基于History,</p>    <p>将URL解析成location对象,然后使用该对象来匹配路由并且正确的嵌套渲染组件。</p>    <p>React提供了三种最常用的history,当然react router也支持我们实现自定义history:</p>    <ul>     <li> <p>browserHistory</p> </li>     <li> <p>hashHistory</p> </li>     <li> <p>createMemoryHistory</p> </li>    </ul>    <p>我们可以直接从react router包中直接导入这些history:</p>    <pre>  <code class="language-javascript">    // CommonJs require方式      var browserHistory = require('react-router').browserHistory;      var hashHistory = require('react-router').hashHistory;        // ES6 module import      import { browserHistory } from 'react-router';      import { hashHistory } from 'react-router';        React.render(        <Router history={browserHistory} routes={routes} />,        document.getElementById('app')      );</code></pre>    <p>他们都是有对应的create方法产生创建的,更多查看https://github.com/mjackson/history/:</p>    <ul>     <li>createBrowserHistory is for use in modern web browsers that support the HTML5 history API (see cross-browser compatibility)</li>     <li>createMemoryHistory is used as a reference implementation and may also be used in non-DOM environments, like React Native</li>     <li>createHashHistory is for use in legacy web browsers</li>    </ul>    <pre>  <code class="language-javascript">    // JavaScript 模块导入(译者注:ES6 形式)      import createBrowserHistory from 'history/lib/createBrowserHistory';      // 或者以 commonjs 的形式导入      const createBrowserHistory = require('history/lib/createBrowserHistory';</code></pre>    <p><strong>browserHistory</strong></p>    <p>browserHistory适合利用React路由开发的浏览器应用使用。在浏览器中,通过History API创建以管理真实URL,比如创建一个新的URL:example.com/some/path。</p>    <p>使用该类型history前,我们必须对服务器进行配置,对所有URL,我们都返回同一html文件,如:index.html。</p>    <ul>     <li> <p>若使用node服务,则需要添加以下设置:</p> <pre>  <code class="language-javascript">    // handle every other route with index.html, which will contain      // a script tag to your application's JavaScript file(s).      app.get('*', function (request, response){        response.sendFile(path.resolve(__dirname, 'public', 'index.html'))      });</code></pre> </li>     <li> <p>对于nginx服务器,需添加如下配置:</p> <pre>  <code class="language-javascript">    server {            //...            location / {                  try_files $uri /index.html;            }      }</code></pre> </li>     <li> <p>对于Apache服务器,首先在项目根目录下创建一个.htaccess文件,添加如下代码:</p> <pre>  <code class="language-javascript">    RewriteBase /      RewriteRule ^index\.html$ - [L]      RewriteCond %{REQUEST_FILENAME} !-f      RewriteCond %{REQUEST_FILENAME} !-d      RewriteRule . /index.html [L]</code></pre> </li>    </ul>    <p><strong>兼容性与IE8+</strong></p>    <p>通过使用特性检测检测是否支持浏览器本地window.history相关API,</p>    <p>如果不支持,如IE8/9,为了更方便的开发,及更好的用户体验,在每次URL变更通过刷新页面来兼容旧浏览器。</p>    <p><strong>注:index.html引入js文件必须是绝对路径,否则找不到该文件</strong></p>    <p><strong>hashHistory</strong></p>    <p>hashHistory使用URL的hash(#)片段,创建诸如example.com/#/some/path的路径。</p>    <p><strong>hashHistory与browserHistory</strong></p>    <p>使用hashHistory不需要配置服务器,并且兼容IE8+,但是依然不推荐使用,</p>    <p>因为每一个web应用,都应该有清晰的URL地址变化,并且需要支持服务器端渲染,这些对于hashHistory来说是不可能实现的。</p>    <p>但是,如果在不支持window.historyAPI的老旧浏览器中,我们也不希望每次操作变更都刷新页面,这时候是需要hashHistory的。</p>    <p><strong>createMemoryHistory</strong></p>    <p>memoryHistory不操作或读取浏览器地址栏URL,它存在于内存中,我们使用它实现服务器渲染,适用于测试或渲染环境,如React Native。</p>    <p>不同于前两种history,React已经为我们创建好了,memoryHistory,必须在应用中主动创建:</p>    <pre>  <code class="language-javascript">    var createMemoryHistory = require('react-router').createMemoryHistory;      var history = createMemoryHistory(location);</code></pre>    <p><strong>实例</strong></p>    <pre>  <code class="language-javascript">    var React = require('react');      var render = require('react-dom');      var browserHistory = require('react-router').browserHistory;      var Router = require('react-router').Router;      var Route = require('react-router').Route;      var IndexRoute = require('react-router').IndexRoute;      var App = require( '../components/App');      var Home = require('../components/Home');      var About = require('../components/About');      var Features = require('../components/Features');        React.render(        <Router history={browserHistory}>          <Route path='/' component={App}>            <IndexRoute component={Home} />            <Route path='about' component={About} />            <Route path='features' component={Features} />          </Route>        </Router>,        document.getElementById('app')      );</code></pre>    <h3><strong>默认路由及相关默认指令(IndexRoute,IndexRedirect和IndexLink)</strong></h3>    <p><strong>Index Route</strong></p>    <p>前文已经介绍< IndexRoute>指令,该指令声明当前路由的默认子路由及其对应需默认渲染的组件,更多请查看上文。</p>    <p><strong>Index Redirects</strong></p>    <p>有时候,我们在访问URL的时候,希望将当前路由默认重定向到另一路由,与前文的Redirect指令不同,Redirect指令是将匹配from属性值对应的路由的URL重定向到匹配to属性值对应的路由的URL;而IndexRedirect,是将当前路由的默认子路由重定向为其他子路由,重定向对象是当前路由的子路由。</p>    <p><strong>Index Links</strong></p>    <ul>     <li> <p>Link</p> <p>React直接提供Link链接组件,to属性声明链接到的地址:</p> <pre>  <code class="language-javascript">    var Link = require('react-router').Link;        var app = React.createClass({          render: function() {              return(                  <Link to="/notings">noting列表</Link>              );          }      });        React.render(<app />, document.body);</code></pre> </li>    </ul>    <p>如上,点击noting列表将导航到项目noting列表展示页,即/notings路由下。</p>    <ul>     <li> <p>IndexLink</p> <p>不同于Link指令,Link指令是提供一个链接,而React路由的Link是有激活状态的,如它的activeStyle属性,可以声明当前页面链接激活时的样式,假如有一个Link链接</p> <p>noting列表,当/notings路由或其子路由(如/notings/123)被渲染时,都会使该链接处于激活状态;而如果使用 noting列表,则需要/notings路由被渲染后才激活该链接。</p> </li>    </ul>    <p> </p>    <p> </p>    <p>来自:http://blog.codingplayboy.com/2016/10/24/react_router/</p>    <p> </p>