先貼上地址,喜歡能夠先 star 一波 😳css
在線預覽地址: http://118.24.21.99:5000/ (加載時間略長)react
GitHub倉庫地址: douban-movie-reactnginx
(在線預覽在電影頁有些靜態資源加載不到應該是 Nginx 配置的問題,想獲取最佳體驗能夠 clone 到本地,運行方法見 GitHub 文檔)git
nginx 開啓 gzip 後加載速度已明顯提高。。。github
基於 React 的超高仿豆瓣電影 PC 版,實現了 主頁,電影頁,人物頁,排行榜,短評頁,長評頁,影訊&購票頁,分類頁,排行榜頁,搜索頁,404 頁。redux
本項目有一個很大的(特)缺點就是全部向 API 請求的數據都存在了 redux store 中,而且是每一個組件發出一個對應內容的請求(寫到一半發現這樣寫很差,可是後面懶得改了,逃),直接致使了要寫的樣本代碼量增長。實際上這些數據應該放在各個組件的 react state 或者由一個高階組件完成多個請求的發起,再將數據傳遞給各個木偶組件,redux 的文檔中討論了究竟什麼樣的數據適合放在 store 而不是放在 state 中,有如下幾點原則:後端
Do other parts of the application care about this data?數組
不會,都只是一些固定的數據展現,數據之間不會有交互。緩存
Do you need to be able to create further derived data based on this original data?antd
不會,只是一些固定的數據展現。
Is the same data being used to drive multiple components?
不會,豆瓣提供的各個 API,不少數據都是重疊的,好比獲取某電影的的評論的數據,返回值中不光會有評論的一個 array,還會包含這個電影的一個 subject 的數據,不須要經過 redux 來存儲評論所在的電影的數據來展現到評論頁上。
Is there value to you in being able to restore this state to a given point in time (ie, time travel debugging)?
不會,沒有什麼 undo, redo 一類的的操做。
Do you want to cache the data (ie, use what's in state if it's already there instead of re-requesting it)?
若是說有必要將數據放在 redux store 中的話,勉強符合條件的就是這點了,咱們能夠將瀏覽過的電影頁面的數據給緩存起來(畢竟一個電影的信息在瀏覽期間幾乎不可能變更)
頁面上每一個組件的內容都是經過請求後端得到的,若是每一個組件都 將發請求、建立各類action,reducer什麼的寫一遍,樣本代碼量就有些太大了,再加上基本每一個組件都是單純的展現組件,邏輯都是類似的,因此項目裏將重複的代碼抽象出來。
這是store的結構(我本身也很想吐槽):
每一個展現組件不相同的部分是 pageName
, moduleName
(用於組織 redux store), API
(不一樣的展現組件請求不一樣的 URI), view
(每一個組件用來展現的木偶組件),param
(好多 URI 是須要經過路由裏的參數來拼接的,好比請求 /subject/26430636
對應的 API 是 /subject/:id
)。因此,每次在建立一個新的組件時只須要這樣:
export default viewGenerator(
{
pageName,
moduleName,
API: API_CELEBRITY,
view: Celebrity
}
)
// 再傳入路由中的query對和對應的值便可
<Celebrity id={id}
params={{
id: this.props.match.params.id,
}}
/>
複製代碼
原理也簡單,利用 pageName
, moduleName
經過 actionCreator
生成對應的發起請求的函數,和 redux store 中對應的的 state 做爲 mapStateToProps
和 mapDispatchToProps
去 connect 出 HOC,這個 HOC 只起一個Decorator 的做用,完成這些展現組件相同的數據請求邏輯 —— 根據傳入的 props 來 fetchByParam(params)
(API 被柯里化掉了,由於也不會變,因此只須要提供 params
便可),這個 HOC 會在 constructor
中會調用傳進來的請求函數,再 render 它要包裹的木偶組件,將數據邏輯與界面分離,具體的代碼能夠查看utils/fetchGenerator
。
至此,每次生成一個新組建只須要寫這個組件對應的木偶組件便可,生成組件的流程是 viewGenerator -> viewDecorator -> (木偶)view
,viewGenerator
負責生成對應的請求函數和 state, viewDecorator
負責套用執行數據邏輯,完成後將數據傳遞給 view
並渲染 view
。
在 reducer 中也須要相似操做:
let contentReducer = reducerGenerator(
{
pageName,
moduleName
}
)
複製代碼
便可生成對應的 reducer,也容許傳入自定義的 reducer,會覆蓋掉相同 action.type
的 reducer,好比在 tag 頁加載更多數據的時候,新的 reducer 就是要 concat 新請求到的數據而不是替換,傳入的自定義的 reducer:
[SUCCESS_ACTION]: (state, action) => {
let payload = action.doesPushBack ?
pushPayload(state.payload, action.payload) :
action.payload
return {
...state,
isLoading: false,
payload
}
}
複製代碼
有些頁其實加載過一次以後其實不必再從新請求一次了(好比電影頁),在這裏原本能夠用 redux-presist
可是試着本身寫了個更簡單粗暴(簡陋)的 middleware,就是直接根據 URI 請求到的數據來緩存,若是想緩存某個URI 的返回值就直接在請求成功的那個 action 那裏肯定 cacheKey
和 cacheValue
,再在發送請求前加上isCached
的判斷便可,若是被緩存了就無需再次發送請求,直接去 caches
裏去拿到緩存,這裏全部緩存都是存儲在內存裏而不是 localStorage 中,整個 middleware 代碼以下:
const caches = {}
const hasCachedKey = (cacheKey) => {
return Object.keys(caches).indexOf(cacheKey) >= 0
}
const cacheMiddleware = store => next => action => {
if (typeof action.cacheKey === 'undefined' ||
typeof action.cacheValue === 'undefined') {
next(action)
return
}
if (!hasCachedKey(action.cacheKey)) {
caches[action.cacheKey] = action.cacheValue
}
next(action)
}
const getCache = (cacheKey) => {
if (hasCachedKey) {
return caches[cacheKey]
}
}
const isCached = (cacheKey, cacheDetector = hasCachedKey) => {
return hasCachedKey(cacheKey)
}
export { cacheMiddleware, isCached, getCache }
複製代碼
TODO:
兩個輪播圖 + 一個自定義的 List,輪播圖用的是 react-slick
,那個頁數指示器 react-slick
沒有提供,在父組件的 state 中定義頁數,將 arrow-prev
和 arrow-next
的 onClick 用來 setState 頁數便可。鼠標懸浮的預覽相似於 modal,位置可能超過父組件的範圍,是經過 createProtal
來實現的,加在了 document.body
上,經過getBoundingBox
讓父組件給他傳遞要渲染的位置。
短評頁的內容是經過解析路由 query 中的 start
和 count
去獲取數據的,封裝了一個 pagination 組件來改變路由的 query 便可作到切換上下頁
同短評頁,多了一點,爲了實現和豆瓣同樣的展開長內容後,收起欄 fix 在底部的效果,用 getBoundingBox
來判斷當前展開的長評是否應該 fix 收起欄 —— !this.props.isFold && contentRect.top <= innerHeight && contentRect.bottom >= innerHeight - barRectHeight
,這樣來判斷便可,須要注意的是直接點擊展開時也要進行一次檢測,由於不僅是須要在滾動時判斷。
影視的分類是寫死的,點擊引發路由改變再引發 param 的改變就會從新發起。加載更多這個按鈕發出的請求和初始化的請求的區別就是 URI 中的 start 不一樣,好比初始化時的請求是 /movie/search?tag=電影&count=20
,第一次點擊加載更多就是 /movie/search?tag=電影&start=20
。既然如此就只須要再發一次請求而後把第二次請求到的數據加上去,和第一次的不一樣的是須要在點擊的時候,即 ACTION.START
時就要在對應的 state 裏完成 count += 20
的操做(不然若是連續點擊兩次,會發出兩個相同的請求),而後自定義一個reducer, 將請求到的含有電影數組的數據 concat 到原來的數據後面。
偷懶直接用了ant的輸入框,可是要記得給輸入框的 onChange
函數加個 debounce。
3,2,1,回首頁
項目中的 API 來源
再一次貼上地址,喜歡能夠star 😳
在線預覽地址: http://118.24.21.99:5000/
GitHub倉庫地址: douban-movie-react
斷斷續續寫了幾個月,如今看來依舊寫的很渣,但願各位大佬多多提出寶貴意見,歡迎留言討論。