接著上回新聞搜索的例子。現在我們要通過路由進入一個新的頁面來查看新聞詳細內容。 react和路由並沒有什麼直接關係,用什麼路由都可以。不過使用react-router可以讓我們的代碼風格統一, 並且有些工具使用起來很方便。 先來安裝react-router庫(我目前安裝的版本是2.0.1,跟1.x版 ...
接著上回新聞搜索的例子。現在我們要通過路由進入一個新的頁面來查看新聞詳細內容。
react和路由並沒有什麼直接關係,用什麼路由都可以。不過使用react-router可以讓我們的代碼風格統一, 並且有些工具使用起來很方便。
先來安裝react-router庫(我目前安裝的版本是2.0.1,跟1.x版本區別比較大):
npm install react-router --save
從使用上來說,react-router不過是一些react組件,所以用起來特別方便。不用多說,看個例子就知道怎麼用了。 先把咱們已經做好的Login和NewsList兩個頁面放到路由里。只需修改src/index.js文件:
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { Router, Route, browserHistory } from 'react-router'
import configureStore from './stores';
import Login from './containers/Login'
import NewsList from './containers/NewsList';
const store = configureStore();
render(
<Provider store={store}>
<Router history={browserHistory}>
<Route path="newslist" component={NewsList} />
<Route path="login" component={Login} />
</Router>
</Provider>,
document.getElementById('app')
);
這個文件相比以前只是把Provider標簽裡面的內容換了。以前咱們只放一個Login或者NewsList組件, 現在是放一個Router組件。Router組件只需要一個history屬性,讓我們可以選擇使用哪種歷史管理方式。 我們常用的就是browserHistory和hashHistory。browserHistory就是我們最熟悉的瀏覽器管理歷史, 使用這種歷史管理方式感覺上跟普通瀏覽網頁的方式一樣:url路徑會隨著跳轉及前進、後退按鈕而變化, 但是在react-router的browserHistory管理下,url的變化不會導致頁面刷新。 hashHsitory只控制url中#號後面的部分,這是前一段時間單頁應用比較通用的方式,但是隨著HTML5的普及, 這個方式有逐漸被淘汰的趨勢。這裡我們使用browserHistory。
現在我們已經可以通過http://localhost:8000/newslist訪問上一節做的新聞列表頁面了。
接著把新聞詳情頁做出來吧。由於我們在新聞列表介面已經取到了全部的新聞內容,也為了簡單,也為了反應快, 我們就直接用新聞列表介面提供的數據,而不再訪問伺服器了。
數據都在store里,任我們怎麼玩。新聞詳情頁訪問數據有兩種方案:一種是記錄新聞列表的index,然後直接根據index訪問列表裡相應的內容; 另一種是把要打開的新聞內容單拿出來一份另放到一個state里。我們用第二種方案。 還是先寫action,直接在src/actions/news.js裡面添加內容:
export const SET_CURRENT_NEWS = 'SET_CURRENT_NEWS'
const setCurrent = cac(SET_CURRENT_NEWS, 'news')
export const chooseNews = index => (dispatch, getState) => {
let current = getState().news.list[index]
dispatch(setCurrent(current))
}
setCurrentNews就是要把一個新聞對象放到相應的state中。chooseNews則是在組件里要調用的, 它根據一個index找出相應的新聞對象並放到當前新聞的state里。
然後往src/reducers/news.js添加新的reducer:
current: cr({}, {
[SET_CURRENT_NEWS](state, {news}){return news}
})
別忘了引入新定義的的action常量。
NewsList組件得派發設置當前新聞的動作,並跳轉到新聞詳情頁面,只需要改renderList方法就行:
renderList(){
return this.props.list.map((item, i) =>{
item.key = item.title
item.onGotoDetail = () => {
this.props.dispatch(chooseNews(i))
this.props.history.push('/newsviewer')
}
return React.createElement(NewsOverview, item)
})
}
這裡給每個NewsOverview組件都傳了個onGotoDetail屬性,NewsOverview在被點擊時要調用這個屬性的函數,只需要在最外層div加個click事件處理,像這樣:
<div onClick={this.props.onGotoDetail}>
在item.onGotoDetail函數中有個this.props.history,它就是我們前面在構建路由時選擇的那個browserHistory,當我們的組件作為Route組件的屬性使用時,Route會給我們的組件註入這個history屬性,這樣用起來就比較方便了。這個history的方法和瀏覽器里的history所擁有的那幾個方法功能差不多,常用的就是go(跳轉)、goBack(回退一個歷史)、goForword(前進一個歷史)、push(跳轉到一個url並添加一個歷史狀態)、replace(跳轉到一個url並替換當前歷史狀態)。具體的可以參考專門對瀏覽器history論述的文章。如果我們想在組件之外控制歷史狀態(比如action里),從react-router里引入browserHistory或hashHsitory直接用就可以。
最後添加新聞詳情頁面的組件,這就很簡單了吧。不過這個組件跟NewsOverview比較起來實在太像,就是新聞概述和詳細內容的區別。 所以這裡我偷個懶,讓NewsOverview通過一個屬性變身為可配置成新聞詳情的組件。把NewsOverview裡面最後一個P標簽改成這樣就行:
{this.props.showDetail ?
<p dangerouslySetInnerHTML={{__html:this.props.message}}/> :
<p>{this.props.description}</p>
}
要在react的jsx裡面直接放數據里的html文本,只能用dangerouslySetInnerHTML屬性, 看這屬性意思就知道react是多麼不希望我們用這個屬性。所以不到萬不得已還是不用為好。誰讓現在咱們是依賴別人現成的介面呢。
然後新建個src/containers/NewsViewer.js,它就很簡單了:
import React from 'react'
import {connect} from 'react-redux'
import NewsOverview from 'components/NewsOverview'
class NewsViewer extends React.Component{
render(){
return (
<div>
{React.createElement(NewsOverview, Object.assign({
showDetail: true
}))}
</div>
)
}
}
export default connect(state => {return {news: state.news.current}})(NewsViewer)
最後在index.js裡面再添加一個路由:
<Route path="newsviewer" component={NewsViewer} />
功能是完美地實現了,但是想一下我們為什麼要用路由?而且還要用瀏覽器管理歷史的路由? 一個很重要的原因就是網站不同於app,它要保證輸入任何一個有效的url後都要給用戶呈現出一個可用的頁面。 一個非常實用的場景就是剛纔我在新聞詳情頁里閱讀到一則很好的新聞,想給分享出去,那別人要通過這個url還能查看到這個新聞。 我們目前沒做到這個。現在我們要實現依靠id訪問到新聞。
id一定是通過url傳來的,可以用query參數,但我們用一個更簡潔的形式:“/newsviewer/30998729”,後面那串數字是新聞的id。 配置很簡單,把新聞詳情頁的路由改成這樣就行了:
<Route path="newsviewer/:id" component={NewsViewer} />
然後要修改src/containers/NewsList.js裡面路由跳轉的那句:
this.props.history.push('/newsviewer/' + item.id)
NewsViewer組件將要載入時讓它去獲取一下新聞詳細內容。還記得目前數據來源是直接從新聞列表裡拽過來的是吧, 沒關係,還讓它拽吧,這樣既能有一般情況下訪問的“唰”一下的用戶體驗,又能保證直接訪問url能獲取到內容。
給src/actions/news.js再加一個獲取數據的action:
export const fetchNewsDetail = id => dispatch => window.$.ajax({
url: 'http://www.tngou.net/api/top/show',
data: {id},
dataType: 'jsonp',
success: data => data.status && dispatch(setCurrentNews(data))
})
給src/containers/NewsViewer.js加一個componentWillMount方法,讓組件將要載入時就去獲取數據:
componentWillMount(){
// 在react-router的幫助下,我們可以很輕鬆地拿到url路徑上的參數id
this.props.dispatch(fetchNewsDetail(this.props.params.id))
}
現在就可以直接通過http://localhost:8000/newsviewer/3864來訪問新聞詳情頁面了。哦,可能會有找不到assets/app.js的報錯, 在index.html裡面把引用他的路徑改成絕對路徑“/assets/app.js”就行了。
我們在開發環境中直接訪問http://localhost:8000/newslist或者http://localhost:8000/newsviewer/3864 這樣的路徑都沒啥問題,但是你要嘗試一下把項目導出部署到生產環境的靜態的伺服器上,再訪問http://xxx.xxx/newslist就悲劇的404了。 因為那個伺服器真去找newslist這個文件了,哪有這個文件呀,咱只有index.html。 要想使用browserHistory只好去配置生產環境的伺服器。具體配置等到後面生產環境配置一節再說吧。react-router的路由並不是扁平的,而是樹狀結構的,不僅路徑可以組織成樹狀結構,組件也可以組織成相應的樹狀結構。
比如我們想要個通用的header,裡面還有返回和登錄按鈕。先把header作為一個組件寫出來再說。
src/components/Header.js:
import React from 'react';
import {Link} from 'react-router'
export default class Header extends React.Component {
render(){
let styl = {
textAlign:'center',
lineHeight:'32px',
width:'15%',
float:'left'
}
return (
<div style={{background: '#ddd', height:'32px'}}>
<div style={styl} onClick={this.props.onGoBack}>{'<'}</div>
<div style={Object.assign({},styl,{width:'70%'})}>{this.props.text}</div>
<Link style={Object.assign({},styl,{float: 'right'})} to="/login">登錄</Link>
</div>
)
}
}
然後再把原來那個App.js找回來吧,它作為路由中的頂層組件,對應根路徑“/”。把前面做的Header放進去:
src/containers/App.js:
import React from 'react';
import Header from 'components/Header'
class App extends React.Component {
render() {
return (
<div>
<Header onGoBack={this.goBack.bind(this)} text="歡迎訪問"/>
<div style={{paddingTop:'10px'}}>
{this.props.children}
</div>
</div>
)
}
goBack(){
this.props.history.goBack()
}
}
export default connect()(App);
上面代碼的render方法里,除了放進去了Header,還要註意那個this.props.children,react-router就是把這個屬性所對應的組件作為App所對應路徑的下一級路由的。
再來改一下src/index.js裡面的路由。由於以後路由會越來越多,所以我打算把所有的route標簽拿出去,放到一個單獨的src/routes.js文件里,index.js里只要引入這個文件並放到原來route們的位置上就行了。
src/routes.js
import React from 'react'
import { Route } from 'react-router'
import App from './containers/App';
import Login from './containers/Login'
import NewsList from './containers/NewsList';
import NewsViewer from './containers/NewsViewer'
export default (
<Route path="/" component={App}>
<Route path="news" component={NewsList} />
<Route path="news/:id" component={NewsViewer} />
<Route path="login" component={Login} />
</Route>
)
做一個小小的手腳,為了url簡潔,我把原來的newslist改成了news,而news後面加斜杠id的形式作為新聞詳情。這兩個url是平級的,看上去像是父子關係,其實結構上是完全平等的。別忘了NewsOverview.js里的連接也要改。
現在訪問/news可以搜索新聞,點擊新聞標題可以跳轉到/news/xxx查看詳細內容,點擊登錄可以跳轉登陸頁,可是,訪問根路徑卻只有一個帶標題的空白頁。我們可以加一個預設頁面,就是在訪問某一級帶有子路徑路由時,可以給它一個對應到這個路徑的頁面,不一定是跟路徑哦。做個索引作為預設頁面吧,src/containers/Index.js:
import React from 'react';
import {Link} from 'react-router'
class Index extends React.Component {
render(){
return (
<ul>
<li><Link to="/news">新聞</Link></li>
</ul>
)
}
}
export default Index
雖然這個組件目前沒有連接到redux,我還是忍不住把它放到了containers目錄下麵,畢竟它是一個頁面級別的組件,沒準哪天產品經理有個啥想法它就要和外界打交道了。
然後添加路由,這個路由比較特殊,不是用Route,而要用個專門的組件IndexRoute,整個src/routes.js代碼如下:
import React from 'react'
import { Route, IndexRoute } from 'react-router'
import Index from './containers/Index';
import App from './containers/App';
import Login from './containers/Login'
import NewsList from './containers/NewsList';
import NewsViewer from './containers/NewsViewer'
export default (
<Route path="/" component={App}>
<IndexRoute component={Index} />
<Route path="news" component={NewsList} />
<Route path="news/:id" component={NewsViewer} />
<Route path="login" component={Login} />
</Route>
)
至此,我們可以用react和相關技術打造完整的單頁web應用了。
上一節 【2】非同步action和redux中間件下一節 【4】生產部署和優化