我的專案react-bookstore總結與反思
react-bookstore
學習了react相關技術,需要貫通一下。所以有了這個。會持續更新。專案不復雜,但我本來就是來練手的。我覺得達到了練手的效果。包括redux/react-redux的使用,以及使用redux-thunk處理非同步請求,並且非同步中還嘗試使用了async/await。當然最重要的是實踐了react-router。由於起步較晚,之前並沒有使用了2.x,3.x的版本,所以看了v4的文件和官方demo後直接上手的。在瀏覽器中主要使用的是react-router-dom,很好的實踐了其中的元件。包括其中註冊登入的彈出框(而不是在新的頁面)就是參照官方文件的。我看的是中文翻譯後的文件
- 使用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
部署
目前已完成的功能
1.路由和搜尋
2.分頁
5月9日更新(接下來這個專案不知道往哪寫了,有沒有老鐵們star一下)
3.註冊和登入功能
註冊:在reduces中使用state.user.users儲存使用者的使用者名稱。註冊時做了驗證。
登入:檢視state.user.users檢視是否匹配。
5月14日更新
4.商品詳情頁
商品詳情:這些資料不是從路由傳過來的(當然從路由傳過來也可以,通過history.push(URL[,state])的第二個引數),而是請求豆瓣介面的。所以再次開啟頁面還是可以看到資料的。
5.新增到購物車
給新增到購物車動作(action)的反饋
6.檢視購物車
感覺達到了練手的效果,剩下的就沒寫了。