前面實(shí)現(xiàn)了聯(lián)系人列表和詳情兩個(gè)頁(yè)面,并通過(guò)點(diǎn)擊事件和返回按鈕處理了兩個(gè)頁(yè)面之間有切換。但同時(shí)引起一個(gè)疑問(wèn):為什么不是單頁(yè)程序?
React 的出現(xiàn)不是為了單頁(yè)應(yīng)用,但在很多時(shí)候用于單頁(yè)應(yīng)用。由于其組件化的設(shè)計(jì),React 也的確很容易寫(xiě)單頁(yè)應(yīng)用。然而說(shuō)到單頁(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)一起被稱(chēng)為 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ì)增加訪問(wèn)歷史,也會(huì)觸發(fā)相應(yīng)的事件。但無(wú)論如何,hash 變化默認(rèn)情況都不會(huì)向服務(wù)器請(qǐng)求數(shù)據(jù)。因此路由的設(shè)計(jì)就利用 hash 的特點(diǎn),通過(guò) hash 的變化來(lái)改變當(dāng)前頁(yè)面的布局,再利用 AJAX 等技術(shù)獲取新頁(yè)面布局所需要的后端數(shù)據(jù),完成頁(yè)面的更新。
由此看來(lái),路由處理器的作用其實(shí)是在一定程度上代替了瀏覽器對(duì) URL 的處理,將由 URL 變化產(chǎn)生的整頁(yè)更新改變?yōu)橛?hash 改變而觸發(fā)局部更新。React 的設(shè)計(jì)在局部更新這個(gè)問(wèn)題進(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 的處理過(guò)程:
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é),不過(guò)直接看 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)不是通訊錄列表了,而是“查無(wú)此人”。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è)面用,并通過(guò)路由來(lái)控制顯示,那就很有必要把原來(lái)的頁(yè)面組件化——哦,原來(lái)的頁(yè)面本來(lái)就是以組件方式定義的,只不過(guò)是作為根組件渲染的。不過(guò)原來(lái)并沒(méi)有考慮到會(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ù)來(lái)引用,所以放在最前面。
在抄 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)介 兩篇文章之后,開(kāi)始著手修改。
在 React Router 的官網(wǎng)及各種文章中都看到這樣的示例
var Router = require("react-router");
這很明顯是 node.js 的語(yǔ)法。難道 React Router 不是用于前端的?似乎不太可能?。?/p>
終于在 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)該是可以在前端使用的,但是從源碼包沒(méi)有發(fā)現(xiàn)直接可用的 js 文件,只好按照 README.md 的步驟先 npm install react-router
從 NPM 下載一個(gè)下來(lái)。果然找到了 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 的辦法來(lái)處理 ReactRouter,把它簡(jiǎn)寫(xiě)成 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ù)。通過(guò) props.params 傳遞給 Main,再由 Main 通過(guò) props.params 傳遞給 RouteHandler……
至于 React Router 是怎么處理各個(gè)路由的,這里不深入研究。有興趣的同學(xué)可以去研究 React Router 的源碼。
經(jīng)過(guò)上面對(duì) app.jsx 的修改,跑起來(lái)已經(jīng)沒(méi)有問(wèn)題了。問(wèn)題在于詳情頁(yè)面顯示的總是“查無(wú)此人”。
之前的詳情頁(yè)面在加載數(shù)據(jù)的時(shí)候會(huì)根據(jù) hash 來(lái)篩選數(shù)據(jù),當(dāng)時(shí)的 hash 像這樣:#1001
。而現(xiàn)在 React Router 會(huì)將 hash 規(guī)范化處理成 #/1001
。因此只需要將原來(lái)的
"#" + p.id === window.location.hash;
改成
"#/" + p.id === window.location.hash;
就好。
之前自定義的 router 就定義了路由參數(shù),并且可以通過(guò)處理參數(shù)的形參獲取,再通過(guò) props 傳遞給組件。但是因?yàn)橥祽?,直接在組件內(nèi)部通過(guò)處理 hash 來(lái)獲取了。簡(jiǎn)單的路徑這么處理沒(méi)有問(wèn)題,但是復(fù)雜的路徑處理起來(lái)就比較復(fù)雜了,所以還是應(yīng)該用現(xiàn)成的。所以現(xiàn)在改用路由參數(shù)來(lái)篩選數(shù)據(jù)。
前面提到 React Router 一般是用 props.params 來(lái)傳遞參數(shù),所以在 DetailPage 中可以通過(guò) this.props.params.id
來(lái)獲取 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));
}
更多建議: