關於
先貼上地址,喜歡可以先 star 一波 ?
線上預覽地址: http://118.24.21.99:5000/ (載入時間略長)
GitHub倉庫地址: douban-movie-react
(線上預覽在電影頁有些靜態資源載入不到應該是 Nginx 配置的問題,想獲取最佳體驗可以 clone 到本地,執行方法見 GitHub 文件)
nginx 開啟 gzip 後載入速度已明顯提升。。。
基於 React 的超高仿豆瓣電影 PC 版,實現了 主頁,電影頁,人物頁,排行榜,短評頁,長評頁,影訊&購票頁,分類頁,排行榜頁,搜尋頁,404 頁。
技術棧
- react
- redux
- redux-thunk
- react-router
- react-slick
- antd
- scss
- css-modules
store vs. state
本專案有一個很大的(特)缺點就是所有向 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?
不會,只是一些固定的資料展示。
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:
- 首屏載入過慢(臥槽足足500k。。。),對根據路由的 code splitting 意義不大,主要是各種第三方庫太TM大了,雖然已經按需載入了,還是需要優化。
- 服務端渲染
- 適配移動端
預覽
主頁
兩個輪播圖 + 一個自定義的 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。
404頁
3,2,1,回首頁
API
專案中的 API 來源
總結
再一次貼上地址,喜歡可以star ?
線上預覽地址: http://118.24.21.99:5000/
GitHub倉庫地址: douban-movie-react
斷斷續續寫了幾個月,現在看來依舊寫的很渣,希望各位大佬多多提出寶貴意見,歡迎留言討論。