react、redux什麼的都用起來 【3】穿越spa的路由

接着上回新聞搜索的例子。如今咱們要經過路由進入一個新的頁面來查看新聞詳細內容。javascript

react和路由並無什麼直接關係,用什麼路由均可以。不過使用react-router可讓咱們的代碼風格統一, 而且有些工具使用起來很方便。html

先來安裝react-router庫(我目前安裝的版本是2.0.1,跟1.x版本區別比較大):java

npm install react-router --save

從使用上來講,react-router不過是一些react組件,因此用起來特別方便。不用多說,看個例子就知道怎麼用了。 先把我們已經作好的Login和NewsList兩個頁面放到路由裏。只需修改src/index.js文件:react

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

如今咱們已經能夠經過http://localhost:8000/newslist訪問上一節作的新聞列表頁面了。ajax

接着把新聞詳情頁作出來吧。因爲咱們在新聞列表接口已經取到了所有的新聞內容,也爲了簡單,也爲了反應快, 咱們就直接用新聞列表接口提供的數據,而再也不訪問服務器了。npm

數據都在store裏,任咱們怎麼玩。新聞詳情頁訪問數據有兩種方案:一種是記錄新聞列表的index,而後直接根據index訪問列表裏相應的內容; 另外一種是把要打開的新聞內容單拿出來一份另放到一個state裏。咱們用第二種方案。 仍是先寫action,直接在src/actions/news.js裏面添加內容:json

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裏。redux

而後往src/reducers/news.js添加新的reducer:api

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應用了。

相關文章
相關標籤/搜索