不知道你們還記不記得這個綜藝節目?「你要去哪裡?」,雖然不知道是不是事先喬好的,但我記得我那個時候很愛看,就看著他們每次都出乎意料地到各個不同的地方冒險。如果你記得這節目,那應該有勾起你的一些回憶。如果你不記得,那也就算了,反正這也不是這篇文章的重點。
之前我們有講到說假如你要寫 SPA 的話,一堆麻煩的事情都要自己做,而這些麻煩的事情就包含路由,Router。路由就是決定什麼路徑要做什麼事情。
為了怕你忘記,我們回顧一下我們之前在 Express 是怎麼寫的:
app.get('/', function (req, res) {
res.send('hello world');
})
app.get('/hello', function (req, res) {
res.send('你好');
})
app.get('/hello/:name', function (req, res) {
res.send('你好, ' + req.params.name);
})
上一章的範例裡面,我們用 React 做出了一個非常簡單的小程式,但接下來我們想要挑戰更複雜的,例如說可以在多個頁面之間切換。假設我們像以往一樣用 server side 去做的話,就是像上面那樣,用 Express 寫路由然後 render 出相對應的 view。但是我們現在的目標是前後端完全分離,所以前端要寫一個 SPA,讓前端變成一個完整的 App。
因此,我們就要在原有的基礎上再加更多功能,才能讓 React 小程式變成一個完整的 App。想加上路由的功能,我們需要 react-router,它的用法也十分簡單,就跟我們之前用 component 差不多:
ReactDOM.render(
(
<Router history={hashHistory}>
<Route path="/" component={App}>
<Route path="about" component={About}/>
<Route path="users/:userId" component={Users} />
</Route>
</Router>
),
document.getElementById('root')
);
history
有分成兩種:browserHistory
跟hashHistory
,這兩種有什麼差別呢?前者的網址長得會像 http://localhost:3000/users/2 ,後者的會是:http://localhost:3000/#/users/2 ,其實就是hashHistory
多了一個 hash,#
的符號。
為什麼要分這兩種呢?首先,一定是第一種的可讀性比較好嘛,因為看起來沒有任何異狀,就跟你現在瀏覽任何網頁一樣。只是第一種如果必須搭配 nginx 或是其他程式才能夠順利運行。
例如說你現在放你的 index.html 到 http://example.com ,你在網址列上面打:http://example.com ,瀏覽器嘗試開啟預設的 index.html,找到之後就可以連上了。但如我你在網址列上面打:http://example.com/users ,對「伺服器」來說,這個網址並沒有對應到任何檔案,所以會返回 404 not found。也就是說,你不能直接打這個網址連進去那個頁面。
那你要怎麼成功看到 http://example.com/users ?你要先到 http://example.com ,再從頁面上面「點擊前往 users 的按鈕」才行。因為你在這個頁面上點擊,這個事件會先被 react router 攔截著,知道你要切換頁面,於是就把後面的控制權全部都搶去,自己處理之後的事情。所以雖然你網址上看到的是 http://example.com/users ,但這完全是前端 JavaScript 自己處理的結果,你實際上發這個 request 給 server 的話,是找不到任何東西的。
有 hash 的話,情況就不一樣了。因為 http://localhost:3000/#/users/2 這個網址,其實會跟 server 拿到的檔案還是 http://localhost:3000/index.html ,#
之後的那些參數都是給瀏覽器這邊看的而已。
所以我們在這邊先選擇 hashHistory
。
(不過如果你實際嘗試上面那一段,會發現無論用哪一種,都可以順利連到,那是因為我們使用的 create-react-app 這套 library 在 run 的時候有自動內建了一個 Express server。)
好,讓我們再把重點切換回最重要的 Router 的部分,再看一次程式碼:
ReactDOM.render(
(
<Router history={hashHistory}>
<Route path="/" component={App}>
<Route path="about" component={About}/>
<Route path="users/:userId" component={Users} />
</Route>
</Router>
),
document.getElementById('root')
);
你比對一下大概就知道是什麼意思了,就只是在宣告說哪個路徑要渲染哪一個 Component 而已。這邊比較需要注意的是巢狀關係,about 跟 users 是被包在 App 裡面的。讓我們來看一下 App 這個 component 會長怎樣:
class App extends React.Component {
render () {
return (
<div>
<h1>App</h1>
<ul>
<li><Link to="/about">About</Link></li>
<li><Link to="/users/1">Users 1</Link></li>
<li><Link to="/users/2">Users 2</Link></li>
</ul>
{this.props.children}
</div>
);
}
}
重點在{this.props.children}
,這個是什麼呢?這個就是 react-router 會傳給你的 props,例如說你現在的網址是/
,這個東西就會是空的。如果你現在的網址是/about
,this.props.children
就會是你在上面指定好的About
這個 component。總之就是匹配到哪一個,就會傳入那一個相對應的組件。
為了方便起見,在這邊我們把所有組件的程式碼都寫在一起:
import React from 'react';
import ReactDOM from 'react-dom';
import { Router, Route, Link, hashHistory } from 'react-router'
import './index.css';
class App extends React.Component {
render () {
return (
<div>
<h1>App</h1>
<ul>
<li><Link to="/about">About</Link></li>
<li><Link to="/users/1">Users 1</Link></li>
<li><Link to="/users/2">Users 2</Link></li>
</ul>
{this.props.children}
</div>
);
}
}
class About extends React.Component {
render () {
return (<h1>About</h1>);
}
}
class Users extends React.Component {
render () {
const id = this.props.params.userId;
return (
<div>
user id: {id}
</div>
)
}
}
ReactDOM.render(
(
<Router history={hashHistory}>
<Route path="/" component={App}>
<Route path="about" component={About}/>
<Route path="users/:userId" component={Users} />
</Route>
</Router>
),
document.getElementById('root')
);
在Users
裡面,this.props.params.userId
對應到的就是你在 Route 裡面宣告的 <Route path="users/:userId" component={Users} />
。於是就可以成功的取到網址上面的參數。
上面是一個非常簡單的範例,但我認為已經能夠大致上說明 react router 的用法,你仔細研究一下之後應該就能搞懂這個到底在幹嘛。如果你想知道更多,可以去看官方說明文件或是參考其他教學,最後附上上面那段程式碼跑出來的結果: