前面實(shí)現(xiàn)了聯(lián)系人列表和詳情兩個(gè)頁(yè)面,并通過點(diǎn)擊事件和返回按鈕處理了兩個(gè)頁(yè)面之間有切換。但同時(shí)引起一個(gè)疑問:為什么不是單頁(yè)程序?
React 的出現(xiàn)不是為了單頁(yè)應(yīng)用,但在很多時(shí)候用于單頁(yè)應(yīng)用。由于其組件化的設(shè)計(jì),React 也的確很容易寫單頁(yè)應(yīng)用。然而說到單頁(yè)應(yīng)用,就不得不提到 router,這個(gè)曾經(jīng)只是在服務(wù)端使用的名詞被單頁(yè)應(yīng)用帶到了前端。
- router,路由器,路由處理器
- route,路由
大家都知道,URL 改變會(huì)觸發(fā)瀏覽器跳轉(zhuǎn)頁(yè)面——除了一種情況:只改變 #
后面的部分,因?yàn)?#
后面的部分是由瀏覽器為自己設(shè)計(jì)的跳轉(zhuǎn)標(biāo)記,連同 #
號(hào)一起被稱為 hash。它標(biāo)識(shí)了當(dāng)前頁(yè)面內(nèi)部的一個(gè)位置,這個(gè)位置可能是由 <a name="....">
標(biāo)記的,也有可能是標(biāo)簽中的 id
屬性標(biāo)記的。
關(guān)于 hash,可以參閱 阮一峰 URL的井號(hào)
現(xiàn)代瀏覽器中,hash 變化會(huì)增加訪問歷史,也會(huì)觸發(fā)相應(yīng)的事件。但無論如何,hash 變化默認(rèn)情況都不會(huì)向服務(wù)器請(qǐng)求數(shù)據(jù)。因此路由的設(shè)計(jì)就利用 hash 的特點(diǎn),通過 hash 的變化來改變當(dāng)前頁(yè)面的布局,再利用 AJAX 等技術(shù)獲取新頁(yè)面布局所需要的后端數(shù)據(jù),完成頁(yè)面的更新。
由此看來,路由處理器的作用其實(shí)是在一定程度上代替了瀏覽器對(duì) URL 的處理,將由 URL 變化產(chǎn)生的整頁(yè)更新改變?yōu)橛?hash 改變而觸發(fā)局部更新。React 的設(shè)計(jì)在局部更新這個(gè)問題進(jìn)行了非常優(yōu)秀的處理,尤其是大大增加了其處理效率。因此 React 非常適合用于單頁(yè) Web 應(yīng)用。
還記得早前提到的 Sample Mobile Application with React and Cordova 么,在它的 Iteration 5 就提到了 路由處理(Routing),而在其示例代碼中也出現(xiàn)了一個(gè)新的腳本:router.js。
router 處理的入口通常是 window.onhashchange
事件。在 router.js 中,return 之前就有一句
window.onhashchange = start;
所以主要的處理函數(shù)是 function start() {...}
。在 start 函數(shù)中,最外層循環(huán)是在 routes
中循環(huán),而 routes
數(shù)組中的內(nèi)容是由 addRoute()
添加的。所以基本上可以了解這個(gè)簡(jiǎn)易 router 的處理過程:
router.addRoute()
添加路由及其對(duì)應(yīng)的處理函數(shù)window.onhashchange
的時(shí)候從當(dāng)前 url 中取得 hash 并與配置好的路由進(jìn)行比較,找到合適的路由,執(zhí)行其處理函數(shù)
仔細(xì)分析 start()
中的循環(huán)可以發(fā)現(xiàn)路由處理的一些細(xì)節(jié),不過直接看 app.js 中配置 router 的部分可以更快明白這個(gè)簡(jiǎn)易 router 的用法。
通訊錄現(xiàn)在是由兩頁(yè)完成,index.html 和 detail.html,在使用路由就需要將這兩頁(yè)合并在一起。幸好這兩個(gè)頁(yè)面只有一句話不同,只需要將 detail.html 中的 <script type="text/jsx" src="js/detail.jsx"></script>
移到 index.html 中就可以完成合并。
<script type="text/jsx" src="js/index.jsx"></script>
<script type="text/jsx" src="js/detail.jsx"></script>
之后可以刪除 detail.html。但這樣的合并只是第一步。這個(gè)時(shí)候看到的效果已經(jīng)不是通訊錄列表了,而是“查無此人”。Why?因?yàn)?index.jsx 和 detai.jsx 都有 React.render()
語(yǔ)句對(duì) document.body
的內(nèi)容進(jìn)行重繪,最后執(zhí)行的一句覆蓋了之前的一句。這也是為什么 Sample Mobile Application with React and Cordova 的 app.js 中,路由處理函數(shù)可以起作用的原因。
要把兩個(gè)獨(dú)立頁(yè)面合并到一個(gè)頁(yè)面用,并通過路由來控制顯示,那就很有必要把原來的頁(yè)面組件化——哦,原來的頁(yè)面本來就是以組件方式定義的,只不過是作為根組件渲染的。不過原來并沒有考慮到會(huì)在同一個(gè)運(yùn)行上下文中使用兩個(gè)頁(yè)面,所以它們的名字都叫 Page。是時(shí)候改個(gè)名字:一個(gè)叫 IndexPage,一個(gè)叫 DetailPage 就挺好。
每個(gè)頁(yè)面組件都使用了一些其它的自定義組件,而這些組件不會(huì)被另一個(gè)頁(yè)面組件用到,所以可以對(duì)這些組件進(jìn)行一個(gè)私有化封裝。就像這樣
var IndexPage = (function(A) {
var Person = React.createClass({ ... });
return React.createClass({ ... });
})(AMUIReact);
var DetailPage = (function(A) {
var detailBase = { ... };
var DetailItem = React.createClass({ ... });
var DetailLinkItem = React.createClass({ ... });
var Detail = React.createClass({ ... });
return React.createClass({ ... });
})(AMUIReact);
組件化 IndexPage 和 DetailPage 的時(shí)候刪除了兩個(gè) jsx 中的 React.render(...)
,所以還需要一個(gè)渲染的入口,不妨加一個(gè) app.jsx:
router.addRoute("", function() {
React.render(<IndexPage />, document.body);
});
router.addRoute(":id", function() {
React.render(<DetailPage />, document.body);
});
router.start();
相應(yīng)的, index.jsx 中跳轉(zhuǎn)到詳情的鏈接也要從 "detail.html#" + this.props.id
改為 "#" + this.props.id
。
由于添加了 router.js
和 app.jsx
,index.html 中引用腳本的部分也需要做一些調(diào)整
<script src="js/router.js"></script>
<script type="text/jsx" src="js/index.jsx"></script>
<script type="text/jsx" src="js/detail.jsx"></script>
<script type="text/jsx" src="js/app.jsx"></script>
router.js 的位置只需要在 app.jsx 之前就行。這里把它當(dāng)作一個(gè)庫(kù)來引用,所以放在最前面。
在抄 router 的時(shí)候,我就猜想,如果 router 是一個(gè)常用的功能,那就一定已經(jīng)存在現(xiàn)成的庫(kù),即使不是 React 官方的,也會(huì)有第 3 方的出現(xiàn)。結(jié)果使用“react router”作為關(guān)鍵字一搜,就搜到了 React Router。然后參考了 再談 React Router 使用方法 和 React Router 簡(jiǎn)介 兩篇文章之后,開始著手修改。
在 React Router 的官網(wǎng)及各種文章中都看到這樣的示例
var Router = require("react-router");
這很明顯是 node.js 的語(yǔ)法。難道 React Router 不是用于前端的?似乎不太可能??!
終于在 React Router 的 README.md 中發(fā)現(xiàn)它提到了 CDN
If you just want to drop a <script& tag in your page and be done with it, you can use the UMD/global build hosted on cdnjs.
既然有 CDN,那應(yīng)該是可以在前端使用的,但是從源碼包沒有發(fā)現(xiàn)直接可用的 js 文件,只好按照 README.md 的步驟先 npm install react-router
從 NPM 下載一個(gè)下來。果然找到了 UMD build 文件:ReactRouter.js 和 ReactRouter.min.js,把這兩個(gè)文件和 CDN 上的一比較,一模一樣。這下放心了。
UMD(Universal Module Definition) 是 AMD 和 CommonJS 的糅合。UMD 先判斷是否支持 Node.js 模塊(即 exports 是否存在),存在則使用 Node.js 模式。再判斷是否支持 AMD(define 是否存在),存在則使用 AMD 方式加載。
如果不使用 CommonJS,也不使用 AMD,React Router 會(huì)掛在 global 對(duì)象上,即 window.ReactRouter。
因?yàn)椴幌攵嗉右粋€(gè)腳本文件,所以準(zhǔn)備把定義 Main 組件和處理路由配置都放在 app.jsx 中進(jìn)行。
首先是定義 Main。因?yàn)?IndexPage 和 DetailPage 都是直接在 body 上渲染的,所以這個(gè) Main 也不需要干多余的事情,直接渲染 RouteHandler 就好
var Main = (function(R) {
React.createClass({
render: function() {
return <R.RouteHandler params={this.props.params} />
}
});
})(ReactRouter);
還是按處理 AMUIReact 的辦法來處理 ReactRouter,把它簡(jiǎn)寫成 R
。
然后是配置路由
var routes = (
<R.Route path="/" handler={Main}>
<R.DefaultRoute handler={IndexPage} />
<R.Route path=":id" handler={DetailPage} />
</R.Route>
);
這里使用 Main 作為根路由處理器,默認(rèn)路由也就是 #/
的時(shí)候。渲染 IndexPage,所以把 IndexPage 作為默認(rèn)路由(DefaultRoute)處理器。下一層路由是詳情頁(yè)面,只需要給個(gè)路徑參數(shù) :id
,用 DetailPage 作處理器即可。
最后啟動(dòng)路由處理器
R.run(routes, function(Handler, state) {
React.render(<Handler params={state.params} />, document.body);
});
處理器的回調(diào)函數(shù)中,第 1 個(gè)參數(shù) Handler,就是在配置路由的時(shí)候給的根 handler
屬性,即對(duì) Main 封裝而成的處理函數(shù)。而 state 表示了當(dāng)前路由的狀態(tài),包括路徑,參數(shù)等。其中 state.params
就是路由參數(shù)。通過 props.params 傳遞給 Main,再由 Main 通過 props.params 傳遞給 RouteHandler……
至于 React Router 是怎么處理各個(gè)路由的,這里不深入研究。有興趣的同學(xué)可以去研究 React Router 的源碼。
經(jīng)過上面對(duì) app.jsx 的修改,跑起來已經(jīng)沒有問題了。問題在于詳情頁(yè)面顯示的總是“查無此人”。
之前的詳情頁(yè)面在加載數(shù)據(jù)的時(shí)候會(huì)根據(jù) hash 來篩選數(shù)據(jù),當(dāng)時(shí)的 hash 像這樣:#1001
。而現(xiàn)在 React Router 會(huì)將 hash 規(guī)范化處理成 #/1001
。因此只需要將原來的
"#" + p.id === window.location.hash;
改成
"#/" + p.id === window.location.hash;
就好。
之前自定義的 router 就定義了路由參數(shù),并且可以通過處理參數(shù)的形參獲取,再通過 props 傳遞給組件。但是因?yàn)橥祽?,直接在組件內(nèi)部通過處理 hash 來獲取了。簡(jiǎn)單的路徑這么處理沒有問題,但是復(fù)雜的路徑處理起來就比較復(fù)雜了,所以還是應(yīng)該用現(xiàn)成的。所以現(xiàn)在改用路由參數(shù)來篩選數(shù)據(jù)。
前面提到 React Router 一般是用 props.params 來傳遞參數(shù),所以在 DetailPage 中可以通過 this.props.params.id
來獲取 ID 參數(shù)。
componentDidMount: function() {
var id = this.props.params.id; // <--
$.getJSON("/js/data.json").then(function(data) {
if (this.isMounted()) {
this.setState({
person: data.filter(function(p) {
return p.id === id; // <--
})[0]
});
}
}.bind(this));
}
更多建議: