react-bookstore
線上地址:https://react-bookstore.herokuapp.com
github地址:https://github.com/yuwanlin/react-bookstore
學習了react相關技術,需要貫通一下。所以有了這個。會持續更新。項目不復雜,但我本來就是來練手的。我覺得達到了練手的效果。包括redux/react-redux的使用,以及使用redux-thunk處理異步請求,並且異步中還嘗試使用了async/await。當然最重要的是實踐了react-router。由於起步較晚,之前並沒有使用了2.x,3.x的版本,所以看了v4的文檔和官方demo後直接上手的。在瀏覽器中主要使用的是react-router-dom,很好的實踐了其中的組件。包括其中註冊登錄的彈出框(而不是在新的頁面)就是參照官方文檔的。我看的是中文翻譯後的文檔,再次感謝翻譯人員。除此之外,我還結合了github上react-router項目的文檔這裏關於某些部分的解釋更爲詳細。
- 使用create-react-app創建的react程序。省去了相對繁瑣的webpack配置。
- 技術棧包括react + redux + react-redux + redux-thunk + react-router(v4,在瀏覽器端主要使用的是react-router-dom)
React.PropTypes
已經棄用了,現在使用的庫是prop-types
(import PropTypes from 'prop-types';
),當然這個小項目中沒有用到屬性驗證(懶)。
使用方法
dev爲開發分支,master爲主分支(用於功能展示)。
git clone https://github.com/yuwanlin/react-bookstore.git
cd react-bookstore
yarn # or npm install
瀏覽器地址欄:
localhost:3000
react-router-dom
中文文檔
在這個項目中使用了:
組件:BrowserRouter
、Link
、Redirect
、Route
、Switch
、 withRouter
API:history
、match
、location
。
在瀏覽器環境下,這些知識足夠了。
BrowserRouter
<Router>
使用 HTML5 提供的 history API (pushState, replaceState 和 popstate 事件) 來保持 UI 和 URL 的同步。現代瀏覽器都支持H5的history API。在傳統的瀏覽器中才是用HashRouter。
import { BrowserRouter } from 'react-router-dom'
<BrowserRouter
basename={optionalString}
forceRefresh={optionalBool}
getUserConfirmation={optionalFunc}
keyLength={optionalNumber}
>
<App/>
</BrowserRouter>
在我的項目中src/index.js
,我使用的是
<Route component={App}/>
而不是直接的App組件。相同點是因爲無論什麼location,都會匹配到App組件,因爲這裏的Route沒有明確的path。不同點是因爲Route導航的組件的props中會默認有match、location、history參數。而我在App組件中就使用了location來判斷模態框。
Link
Link是一個鏈接(實際上一個a標籤),用來跳轉到相應的路由。其中的to
屬性可以是string或者object。
<Link to="/courses"/>
<Link to={{
pathname: '/courses',
search: '?sort=name',
hash: '#the-hash',
state: { fromDashboard: true }
}}/>
其中pathname可以通過location.pathname或者match.URL得到。search可以通過location.search獲取到。hash通過location.hash獲取到。
Link還有個replace
屬性,表示使用Link的頁面代替現有頁面而不是將新頁面添加到history棧(即history.replace而不是history.push)。
Redirect
表示重定向到一個新的頁面。默認是使用新頁面代替舊頁面。如果加上push
屬性表示將新頁面添加到history棧。
<Redirect push to='/courses'/>
其中to
屬性和Link的to屬性是一樣的。Redirect還有一個from
屬性,用來Switch組件中重定向。
Route
Route或許是最重要的組件了。它定義了路由對應的組件。它的path
屬性和Link組件的to屬性是相對應的。
import { BrowserRouter as Router, Route } from 'react-router-dom'
<Router>
<div>
<Route exact path="/" component={Home}/>
<Route path="/news" component={NewsFeed}/>
</div>
</Router>
exact
屬性表示Route值匹配和path一樣的URL。而不包括其二級目錄。比如/book
可以匹配到/book/abc
。
strict
屬性用來設置結尾斜線(/)相關的內容。
path | location.pathname | matches? |
---|---|---|
/one/ | /one | no |
/one/ | /one/ | yes |
/one/ | /one/two | yes |
path | location.pathname | matches? |
---|---|---|
/one | /one | yes |
/one | /one/ | no |
/one | /one/two | no |
然後就是渲染對應組件的三種方式:component
,render
,children
。這三種渲染方法都會獲得相同的三個屬性。分別是match、location、history。
component
: 如果你使用component(而不是像下面這樣使用render),路由會根據指定的組件使用React.createElement來創建一個新的React element。這就意味着如果你提供的是一個內聯的函數的話會帶來很多意料之外的重新掛載。所以,對於內聯渲染,要使用render屬性(如下所示)。
<Route path="/user/:username" component={User}/>
const User = ({ match }) => {
return <h1>Hello {match.params.username}!</h1>
}
render
: 使用render屬性,你可以選擇傳一個在地址匹配時被調用的函數,而不是像使用component屬性那樣得到一個新創建的React element。使用render屬性會獲得跟使用component屬性一樣的route props。
// 便捷的行內渲染
<Route path="/home" render={() => <div>Home</div>}/>
// 包裝/合成
const FadingRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
<FadeIn>
<Component {...props}/>
</FadeIn>
)}/>
)
<FadingRoute path="/cool" component={Something}/>
警告: <Route component>
的優先級要比<Route render>
高,所以不要在同一個 <Route>
中同時使用這兩個屬性。
children
: 有時候你可能想不管地址是否匹配都渲染一些內容,這種情況你可以使用children屬性。它與render屬性的工作方式基本一樣,除了它是不管地址匹配與否都會被調用。
除了在路徑不匹配URL時match的值爲null之外,children渲染屬性會獲得與component和render一樣的route props。這就允許你根據是否匹配路由來動態地調整UI了,來看這個例子,如果理由匹配的話就添加一個active類:
<Route children={({ match, ...rest }) => (
{/* Animate總會被渲染, 所以你可以使用生命週期來使它的子組件出現
或者隱藏
*/}
<Animate>
{match && <Something {...rest}/>}
</Animate>
)}/>
警告: <Route component>
和<Route render>
的優先級都比<Route children>
高,所以在同一個<Route>
中不要同時使用一個以上的屬性.
Switch
無論如何,它最多隻會渲染一個路由。或者是Route的,或者是Redirect的。考慮以下代碼:
<Route path="/about" component={About}/>
<Route path="/:user" component={User}/>
<Route component={NoMatch}/>
如果Route不在Switch組件中,那麼當URL是/about時,這三個路由組件都會渲染。其中第二個Route中,通過match.params.user可以獲取URL–about。這種設計,允許我們以多種方式將多個 組合到我們的應用程序中,例如側欄(sidebars),麪包屑(breadcrumbs),bootstrap tabs等等。 然而,偶爾我們只想選擇一個<Route>
來渲染。如果我們現在處於 /about ,我們也不希望匹配 /:user (或者顯示我們的 “404” 頁面 )。以下是使用 Switch 的方法來實現:
<Switch>
<Route path="/about" component={About}/>
<Route path="/:user" component={User}/>
<Route component={NoMatch}/>
</Switch>
現在,只有第一個路由組件會渲染了。其緣由是Switch僅僅渲染一個路由。
對於當前地址(location),Route組件使用path匹配,Redirect組件使用from匹配。所以對於沒有path的Route組件或者沒有from的Redirect組件,總是可以匹配所有的地址。這一重大的應用當然就是404了。。
withRouter
前面提到,使用Route組件匹配到的組件總是可以獲得match、location、history屬性。然後,對於一個普通的組件,有時也需要相關數據。這時,就可以使用到withRouter了。
const CommonComponent = ({match, location, history}) => null
const CommonComponent2 = withRouter(CommonComponent);
另外,有時候URL改變可能組件沒有重載,這時因爲組件可能檢查到它的屬性沒有變。同理,使用withRoute,當URL改變,組件的this.props.location一定會改變,這樣就可以使組件重載了。
match
- params -( object 類型)即路徑參數,通過解析URL中動態的部分獲得的鍵值對。
- isExact - 當爲 true 時,整個URL都需要匹配。
- path -( string 類型)用來做匹配的路徑格式。在需要嵌套 的時候用到。
- url -( string 類型)URL匹配的部分,在需要嵌套 的時候會用到。
地址欄: /user/real
<Route path="/user/:user">
此時:match如下:
{
params: { user: "real"}
isExact: true,
path: "/user/:user",
url: "user/real"
}
location
location 是指你當前的位置,下一步打算去的位置,或是你之前所在的位置,形式大概就像這樣:
地址欄:/user/real?q=abc#sunny
{
key: 'ac3df4', // 在使用 hashHistory 時,沒有key。值不一定
pathname: '/user/real'
search: '?q=abc',
hash: '#sunny',
state: undefined
}
在react-router中,可以在下列環境使用location。
- Web Link to
- Native Link to
- Redirect to
- history.push
- history.replace
通常,我們只需要使用字符串表示location,如下:
<Link to="/user/:user">
使用對象形式可以表達更多的信息。如果需要從一個頁面傳遞數據到另一個頁面(除了URL相關數據),使用state是一個好方法。
<Link to={{
pathname: '/user/real',
state: { data: 'your data'}
}}>
我們也可以通過history.location獲取location對象,但是不要這樣做,因爲history是可變的。而location是不可變的(URL發生變化location一定變化)。
class Comp extends React.Component {
componentWillReceiveProps(nextProps) {
// locationChanged 變量爲 true
const locationChanged = nextProps.location !== this.props.location
// 不正確,locationChanged 變量會 *永遠* 爲 false ,因爲 history 是可變的(mutable)。
const locationChanged = nextProps.history.location !== this.props.history.location
}
}
history
- 「browser history」 - history 在 DOM 上的實現,經常使用於支持 HTML5 history API 的瀏覽器端。
- 「hash history」 - history 在 DOM 上的實現,經常使用於舊版本瀏覽器端。
- 「memory history」 - 一種存儲於內存的 history 實現,經常用於測試或是非 DOM 環境(例如 React Native)。
history 對象通常會具有以下屬性和方法:
length -( number 類型)指的是 history 堆棧的數量。
action -( string 類型)指的是當前的動作(action),例如 PUSH,REPLACE 以及 POP 。
location -( object類型)是指當前的位置(location),location 會具有如下屬性:
- pathname -( string 類型)URL路徑。
- search -( string 類型)URL中的查詢字符串(query string)。
- hash -( string 類型)URL的 hash 分段。
- state -( string 類型)是指 location 中的狀態,例如在 push(path, state) 時,state會描述什麼時候 location 被放置到堆棧中等信息。這個 state 只會出現在 browser history 和 memory history 的環境裏。push(path, [state]) -( function 類型)在 hisotry 堆棧頂加入一個新的條目。
replace(path, [state]) -( function 類型)替換在 history 堆棧中的當前條目。
go(n) -( function 類型)將 history 對戰中的指針向前移動 n 。
goBack() -( function 類型)等同於 go(-1) 。
goForward() -( function 類型)等同於 go(1) 。
block(prompt) -( function 類型)阻止跳轉,(請參照 history 文檔)。
react
react組件的生命週期中主要用到的有componentDidMount
,componentWillReceiveProps
、componentWillUpdate
、render
。
render
render方法自然不必多說,當組件的state或者props改變的時候組件會重新渲染。
componentDidMount
這個方法在組件的聲明週期中只會執行一次,這代表組件已經掛載了。所以在此方法中可以進行dom操作,異步請求(比如我用的dispatch,action中使用redux-thunk處理的異步請求)等。
componentWillReceiveProps
這個方法也是常用的,它接受一個參數nextProps
。在一些組件中我使用了react-redux的connect方法,並獲取state中的某些數據映射到組件的屬性。對於一些數據,比如在BookDetail組件中,const { bookDetail, history } = nextProps;
,bookDetail一開始是沒有的,我在componentDidMount
方法中dispatch(getSomeBook(bookId));
,這是一個異步請求,然後請求豆瓣數據,改變state,再映射到組件的屬性(mapStateToProps
),這樣組件會再次渲染,並且bookDetail屬性也有了實際的內容。通常使用nextProps和當前的this.props某些屬性做對比,然後決定下一步該怎麼做。
componentWillUpdate
這個方法接受兩個參數,分別是nextProps
,nextState
。這是在組件確認需要更新之後執行的方法。在App組件中,爲了記住組件的上一個location,就是在這個聲明週期這種進行的。這個函數中不可以更新props或者state。如果需要更新,應該在componentWillReceiveProps
方法中進行更新。
redux
redux提供的api主要有applyMiddleware
,bindActionCreators
,compose
,combineReducers
,createStore
。
applyMiddleware
接受中間件。對於多箇中間價,可以使用...
擴展運算符。
const middlewares = [];
applyMiddleware(...middlewares)
bindActionCreators
這個函數自帶dispatch。可能在mapDispatchToProps總會用到。
import actions as * from '../actions/index.js';
const mapDispatchToProps = (dispatch) => bindActionCreators(actions, dispatch);
compose
有時候,項目中已經引入了一些middleware或別的store enhancer(applyMiddleware的結果本身就是store enhancer),如下:
const store = createStore(
reducer,
preloadState,
applyMiddleware(...middleware)
)
這時候,需要將現有的enhancer與window.devToolsExtension()組合後傳入,組合可以使用redux提供的輔助方法compose。
import { createStore, compose, applyMiddleware } from 'redux';
const store = createStore(
reducer,
preloadState,
compose(
applyMiddleware(...middleware),
window.devToolsExtension ? window.devToolsExtension() : f => f
)
)
compose的效果很簡單:compose(a,b)的行爲等價於(…args) => a(b(…args))。即從右向左執行,並將右邊函數的返回值作爲它左邊函數的參數。如果window.devToolsExtension
不存在,其行爲等價於compose(a, f=>f),等價於a(f=>f輸入什麼參數就返回什麼參數)。
combineReducers
將多個小的reducer合併成一個。由於redux中state只有一個,所以每個小的reducer都是state的一個屬性。
createStore
上面已經介紹過。
react-redux
react-redux主要提供兩個接口。Provider
和connect
。
Provider
顧名思義,Provider的主要作用是“provide”。Provider的角色是store的提供者。一般情況下,把原有組件樹根節點包裹在Provider中,這樣整個組件樹上的節點都可以通過connect獲取store。
<Provider store={store}>
<App />
</Provider>
connect
connect用來“連接”組件與store。它的形式如下:
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
[mapStateToProps(state, [ownProps]): stateProps] (Function): 如果定義該參數,組件將會監聽 Redux store 的變化。任何時候,只要 Redux store 發生改變,mapStateToProps 函數就會被調用。該回調函數必須返回一個純對象,這個對象會與組件的 props 合併。如果你省略了這個參數,你的組件將不會監聽 Redux store。如果指定了該回調函數中的第二個參數 ownProps,則該參數的值爲傳遞到組件的 props,而且只要組件接收到新的 props,mapStateToProps 也會被調用。
[mapDispatchToProps(dispatch, [ownProps]): dispatchProps] (Object or Function): 如果傳遞的是一個對象,那麼每個定義在該對象的函數都將被當作 Redux action creator,而且這個對象會與 Redux store 綁定在一起,其中所定義的方法名將作爲屬性名,合併到組件的 props 中。如果傳遞的是一個函數,該函數將接收一個 dispatch 函數,然後由你來決定如何返回一個對象,這個對象通過 dispatch 函數與 action creator 以某種方式綁定在一起(提示:你也許會用到 Redux 的輔助函數 bindActionCreators())。如果你省略這個 mapDispatchToProps 參數,默認情況下,dispatch 會注入到你的組件 props 中。如果指定了該回調函數中第二個參數 ownProps,該參數的值爲傳遞到組件的 props,而且只要組件接收到新 props,mapDispatchToProps 也會被調用。
[mergeProps(stateProps, dispatchProps, ownProps): props] (Function): 如果指定了這個參數,mapStateToProps() 與 mapDispatchToProps() 的執行結果和組件自身的 props 將傳入到這個回調函數中。該回調函數返回的對象將作爲 props 傳遞到被包裝的組件中。你也許可以用這個回調函數,根據組件的 props 來篩選部分的 state 數據,或者把 props 中的某個特定變量與 action creator 綁定在一起。如果你省略這個參數,默認情況下返回 Object.assign({}, ownProps, stateProps, dispatchProps) 的結果。
[options] (Object) 如果指定這個參數,可以定製 connector 的行爲。
- [pure = true] (Boolean): 如果爲 true,connector 將執行 shouldComponentUpdate 並且淺對比 mergeProps 的結果,避免不必要的更新,前提是當前組件是一個“純”組件,它不依賴於任何的輸入或 state 而只依賴於 props 和 Redux store 的 state。默認值爲 true。
- [withRef = false] (Boolean): 如果爲 true,connector 會保存一個對被包裝組件實例的引用,該引用通過 getWrappedInstance() 方法獲得。默認值爲 false
部署
項目地址:https://react-bookstore.herokuapp.com
- 首先下載heroku cli
https://devcenter.heroku.com/articles/getting-started-with-nodejs#set-up - heroku login
- heroku config:set NPM_CONFIG_PRODUCTION=false
- git push heroku master
- heroku open
目前已完成的功能
1.路由和搜索
2.分頁
5月9日更新(接下來這個項目不知道往哪寫了,有沒有老鐵們star一下)
3.註冊和登錄功能
註冊:在reduces中使用state.user.users保存用戶的用戶名。註冊時做了驗證。
登錄:查看state.user.users查看是否匹配。
5月14日更新
4.商品詳情頁
商品詳情:這些數據不是從路由傳過來的(當然從路由傳過來也可以,通過history.push(URL[,state])的第二個參數),而是請求豆瓣接口的。所以再次打開頁面還是可以看到數據的。
5.添加到購物車
給添加到購物車動作(action)的反饋
6.查看購物車
感覺達到了練手的效果,剩下的就沒寫了。