單頁(yè)應(yīng)用(Width Router)

2018-05-25 23:27 更新

前面實(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,路由

單頁(yè)應(yīng)用路由的原理和作用

大家都知道,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)用。

抄一個(gè) router

還記得早前提到的 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ò)程:

  1. 配置階段使用 router.addRoute() 添加路由及其對(duì)應(yīng)的處理函數(shù)
  2. 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 Cordovaapp.js 中,路由處理函數(shù)可以起作用的原因。

把頁(yè)面組件化

要把兩個(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.jsapp.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)引用,所以放在最前面。

用別人的輪子:React Router

在抄 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)始著手修改。

ReactRouter.js

在 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。

React Router 要點(diǎn)

  1. React Router 是作為 React 組件設(shè)計(jì)的,所以它的路由配置是以組件的方式進(jìn)行的。主要使用 ReactRouter.Route 組件進(jìn)行配置。

  1. React Router 同樣也是以組件的方式進(jìn)行渲染的,配置用于顯示的組件,最終會(huì)顯示在 ReactRouter.RouteHandler 組件中。

  1. 需要定義一個(gè) Main 組件來(lái)渲染 RouterHandler。而這個(gè) Main 組件將作為 router 配置的根路由處理器(Handler)。

  1. ReactRouter.run 用于啟動(dòng) router 處理。路由的參數(shù)(:id 這種)一般通過(guò) props.params 對(duì)象傳遞。

修改 app.jsx

因?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 的源碼。

修改 detail.jsx

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

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)