React入門與進階之路由

zjdy4615 8年前發布 | 22K 次閱讀 React 前端技術

React入門與進階之路由

在傳統的網頁應用中,一般是根據用戶的操作指向不同的url,然后服務器渲染出不同的html代碼,后來有了ajax,在同一頁面里,可以為不同操作,指定處理器函數,在不刷新頁面的情況下更新局部視圖,但是局限依然較大,一旦跳轉了URL,依然需要服務器渲染模板返回;而在Backbone,Angular,React出現以后,在單頁面應用中,我們可以給不同URL指定處理器函數,保持URL與視圖的同步,渲染模板的功能已經轉移到客戶端進行,與服務器的交互只涉及到數據,這就是路由的功能。

React中的路由

React是一個用戶界面類庫,僅相當于MVC模式中的V-view視圖,其本身并不包含路由功能,但是它以模塊的方式提供了路由功能,可以很好的與React進行協作開發,當然這也并不是必須使用的。現在很多單頁應用框架都實現了各自的路由模塊,如Backbone,Angular等。而這其中很多路由模塊也能與React搭配使用。本文將介紹本人使用過的兩種:Backbone.Router和react-router。

不使用路由模塊,我們需要處理一個展示筆記列表和展示特定筆記對應不同視圖時,React處理視圖更新的方式大致如此:

    // 狀態變量
    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> 

如上,沒有路由模塊管理,只是通過返回的組件狀態變更,更新對應視圖,隨著應用越來越大處理會越發繁瑣,接下來我們通過使用路由管理URL和視圖的同步的方式來達到相同目的。

Backbone.Router

Backbone是一種MVC(Model-View-Controller)模式的框架,其路由模塊屬于Controller層,獨立于其他模塊,可以很好的與React搭配使用,之前的例子使用Backbone.Router實現后如下:

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

如上,如果訪問一個如/notings的URL地址,將會展示所以noting,而訪問諸如/notings/123這種的URL則會展示特定id為123的noting。

react-router

注:react-router 1.0.x和0.13.x兩個大版本之前API語法有差別,本文使用最新的語法介紹react-router。

我們在前文了解了Backbone.Router如何作為路由模塊與React搭配使用,現在繼續學習react-router。

不同于Backbone的路由模塊,react-router完全由React組件(component)構成:路由本身被定義成組件使用,而其內部路由分發處理器也定義成組件的形式使用。

現在讓給我們繼續使用react-router來實現之前展示noting的例子的路由管理:

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

如上為React Component主要代碼,路由模塊代碼如下:

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

如上路由,用戶訪問跟路由/或/index時(比如http://localhost:3000/或http://localhost:3000/index),頁面加載NotingApp組件,訪問諸如/notings/all時將在NotingApp下加載NotingList組件,訪問/noting/123路徑時,則將在NotingApp下加載NotingItem組件。

獲取URL參數

React路由支持我們通過 query 字符串來訪問URL參數。比如訪問 noting/1234?name=jh,你可以通過訪問 this.props.location.query.來訪問URL參數對象,通過this.props.location.query.name從Route組件中獲得”jh”值。

路由配置

路由配置是一系列嵌套指令,它定義路由如何匹配URL和匹配URL后的操作(具體而言,即組件切換或更新)。

Router

React所有路由實例必須包含在< Router>標簽內,以組件形式定義,該組件有history屬性,其值有三種,將在后文介紹。

Route

定義具體路由節點使用Route指令,該指令有許多屬性:

  • path

    聲明該路由節點所匹配URL(絕對路徑)或URL片段(相對路徑)字符串值,若該屬性未賦值,則表示此路由節點不需匹配URL,直接嵌套渲染組件。

  • component

    特定URL匹配到該節點時,渲染此路由節點定義的組件,如此,層層嵌套下去。

**this.props.children–Render,對應當前路由的默認子路由,將渲染返回默認子路由的組件。 **

IndexRoute

默認情況下,this.props.children值是undefined,即默認子路由不存在,我們可以通過IndexRoute指令指定路由的默認子路由,匹配且僅匹配到當前路由的URL將層層渲染路由組件到此默認子路由組件。

Redirect

顧名思義,這是一個重定向指令,很多時候我們可能改變了某些URL,之前的和改變后的URL都需要匹配該路由,這時,就需要使用Redirect指令:

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

onEnter 和onLeave鉤子

React路由也可以定義onEnter和onLeave鉤子函數(Hooks),在路由變更發生時觸發,分別表示在離開某路由,進入某路由觸發定義的鉤子回調函數。

  • onLeave

    路由變更時,首先觸發onLeave鉤子回調函數,且在離開的所有定義過的路由都會由外到內觸發。

  • onEnter

    路由變更時,接著onLeave鉤子回調函數執行后,會從外到內層層觸發onEnter回調函數。

JSX和Plain Object

React提供JSX語法開發React應用,也支持原生JavaScript語法,路由模塊也是,對于路由,可以使用Object對象的形式定義:

var routes = {

};

render( , document.body);

原生語法不支持Redirect指令,我們只能使用onEnter鉤子,在回調函數里面處理重定向。

路由匹配(Route Matching)

如果想要明白路由如何匹配某特定URL,就必須學習路由的三個相關概念和屬性:

  • 嵌套關系(nesting)

  • 路徑語法(path)

  • 優先級(precedence)

嵌套關系(nesting)

React路由嵌套路由的概念,就像DOM樹一樣;應用路由以嵌套的形式定義,路由對應的視圖組件也形成嵌套,

當訪問的特定URL層層向下匹配成功時,最后的匹配路由節點及其所有嵌套父級路由對應的組件都將被渲染。

React Router traverses the route config depth-first searching for a route that matches the URL.

React路由會對路由配置進行深度優先搜索,以找到匹配特定URL的路由。

路徑語法(path syntax)

路由的path屬性值是字符串類型,能匹配特定URL或該URL的部分,除了可能包含的以下特殊符號,該值都是按照字面量進行解釋的:

  • :paramName 路由參數,匹配特定URL中/,?或#字符后面的部分

  • () 表示URL中該部分是可選的

  • 匹配任意數量任意字符,直到下一個匹配點

  • ** 匹配任意字符,直到遇見/,?或#字符,并且會產生一個splat參數

    <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

路由的相對路徑與絕對路徑

  • 絕對路徑,即以/開頭的路徑字符串,是一個獨立的路徑

  • 相對路徑,不以/開頭,相對于其父級路由路徑值

If a route uses a relative path, it builds upon the accumulated path of its ancestors.

Nested routes may opt-out of this behavior by using an absolute path.

如果一個路由使用的是相對路徑,則該路由匹配的URL是由該路由路勁及其所有祖先路由節點路徑值從外到內拼接組成;

若使用的是絕對路徑,則該路由就忽略嵌套關系直接匹配URL。

優先級(precedence)

路由算法按照路由定義的順序,從上到下匹配URL,所以,在有兩個或多個同級路由節點時,必須保證前面的路由不能與后面的路由匹配同一個URL。

History

History可以監聽瀏覽器地址欄的變化并且能把URL解析成一個location對象,React路由基于History,

將URL解析成location對象,然后使用該對象來匹配路由并且正確的嵌套渲染組件。

React提供了三種最常用的history,當然react router也支持我們實現自定義history:

  • browserHistory

  • hashHistory

  • createMemoryHistory

我們可以直接從react router包中直接導入這些history:

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

他們都是有對應的create方法產生創建的,更多查看https://github.com/mjackson/history/:

  • createBrowserHistory is for use in modern web browsers that support the HTML5 history API (see cross-browser compatibility)
  • createMemoryHistory is used as a reference implementation and may also be used in non-DOM environments, like React Native
  • createHashHistory is for use in legacy web browsers
    // JavaScript 模塊導入(譯者注:ES6 形式)
    import createBrowserHistory from 'history/lib/createBrowserHistory';
    // 或者以 commonjs 的形式導入
    const createBrowserHistory = require('history/lib/createBrowserHistory';

browserHistory

browserHistory適合利用React路由開發的瀏覽器應用使用。在瀏覽器中,通過History API創建以管理真實URL,比如創建一個新的URL:example.com/some/path。

使用該類型history前,我們必須對服務器進行配置,對所有URL,我們都返回同一html文件,如:index.html。

  • 若使用node服務,則需要添加以下設置:

        // 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'))
        });
  • 對于nginx服務器,需添加如下配置:

        server {
              //...
              location / {
                    try_files $uri /index.html;
              }
        }
  • 對于Apache服務器,首先在項目根目錄下創建一個.htaccess文件,添加如下代碼:

        RewriteBase /
        RewriteRule ^index\.html$ - [L]
        RewriteCond %{REQUEST_FILENAME} !-f
        RewriteCond %{REQUEST_FILENAME} !-d
        RewriteRule . /index.html [L]

兼容性與IE8+

通過使用特性檢測檢測是否支持瀏覽器本地window.history相關API,

如果不支持,如IE8/9,為了更方便的開發,及更好的用戶體驗,在每次URL變更通過刷新頁面來兼容舊瀏覽器。

注:index.html引入js文件必須是絕對路徑,否則找不到該文件

hashHistory

hashHistory使用URL的hash(#)片段,創建諸如example.com/#/some/path的路徑。

hashHistory與browserHistory

使用hashHistory不需要配置服務器,并且兼容IE8+,但是依然不推薦使用,

因為每一個web應用,都應該有清晰的URL地址變化,并且需要支持服務器端渲染,這些對于hashHistory來說是不可能實現的。

但是,如果在不支持window.historyAPI的老舊瀏覽器中,我們也不希望每次操作變更都刷新頁面,這時候是需要hashHistory的。

createMemoryHistory

memoryHistory不操作或讀取瀏覽器地址欄URL,它存在于內存中,我們使用它實現服務器渲染,適用于測試或渲染環境,如React Native。

不同于前兩種history,React已經為我們創建好了,memoryHistory,必須在應用中主動創建:

    var createMemoryHistory = require('react-router').createMemoryHistory;
    var history = createMemoryHistory(location);

實例

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

默認路由及相關默認指令(IndexRoute,IndexRedirect和IndexLink)

Index Route

前文已經介紹< IndexRoute>指令,該指令聲明當前路由的默認子路由及其對應需默認渲染的組件,更多請查看上文。

Index Redirects

有時候,我們在訪問URL的時候,希望將當前路由默認重定向到另一路由,與前文的Redirect指令不同,Redirect指令是將匹配from屬性值對應的路由的URL重定向到匹配to屬性值對應的路由的URL;而IndexRedirect,是將當前路由的默認子路由重定向為其他子路由,重定向對象是當前路由的子路由。

Index Links

  • Link

    React直接提供Link鏈接組件,to屬性聲明鏈接到的地址:

        var Link = require('react-router').Link;
    
        var app = React.createClass({
            render: function() {
                return(
                    <Link to="/notings">noting列表</Link>
                );
            }
        });
    
        React.render(<app />, document.body);

如上,點擊noting列表將導航到項目noting列表展示頁,即/notings路由下。

  • IndexLink

    不同于Link指令,Link指令是提供一個鏈接,而React路由的Link是有激活狀態的,如它的activeStyle屬性,可以聲明當前頁面鏈接激活時的樣式,假如有一個Link鏈接

    noting列表,當/notings路由或其子路由(如/notings/123)被渲染時,都會使該鏈接處于激活狀態;而如果使用 noting列表,則需要/notings路由被渲染后才激活該鏈接。

 

 

來自:http://blog.codingplayboy.com/2016/10/24/react_router/

 

 本文由用戶 zjdy4615 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!