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