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/