原文參見css
本文是 gitconnected Hacktobrefest項目的逐步解決方法.react
在本教程中,我將會構建一個產品級別的的 Hacker News 克隆. 咱們會逐步實現應用的初始化,添加用於狀態管理的 Redux,用 React 構建 UI而且部署到 GitHub 主頁上.樣式將會採用styled-components](https://www.styled-components.com/),API方面使用[
axios](https://github.com/axios/axios)庫調用 [
Hacker News API`.ios
源代碼在這裏查看
.git
下載 Chrome應用
es6
若是你願意看視頻,能夠看看 youtube 上的教程. www.youtube.com/watch?v=oGB…github
使用create-react-app
來初始化項目.用這個包初始化項目,就不用擔憂配置問題了.首先要肯定已經安裝了create-react-app
.web
npm -i -g create-react-app
複製代碼
運行下面的命令來啓動項目. create-react-app
安裝了全部構建 React 應用的必備依賴包,還有默認的腳本用於管理開發和實際應用的打包.chrome
create-react-app hn-clone
# wait for everything to finish...
cd hn-clone
複製代碼
如今能夠安裝應用所需的核心軟件包了.目前我使用的是yarn
來管理依賴包,若是你使用的是npm
,只須要用npm install
替換掉yarn add
就能夠了.npm
yarn add redux styled-components react-redux redux-logger redux-thunk axios
複製代碼
create-react-app
使用NODE_PATH
環境變量(environment variable)來建立絕對路徑. 咱們能夠在.env
文件中聲明環境變量. create-react-app
會識別它,經過doten庫
來應用絕對路徑.json
#使用touch 命令建立.env文件
touch .env
# 在.env文件裏添加
#NODE_PATH=src
複製代碼
若是你對這個模式不太熟悉, 當咱們開始構建應用的時候,對你來講更爲有意義.設定環境變量可讓咱們直接導入文件而不用考慮文件的路徑. 相似這樣 ../../components/List
變爲components/List
- 使用上方便多了.
在src
文件夾裏面, 從應用要適應更爲大規模和重用性更強上考慮,作一些更新.
components
: 這個文件夾包含全部的 React 組件(container和 presentational 組件都包含).services
: Services能夠鏈接到API(例如,使用axios調用 HN API)或者爲應用提供擴展的功能(例如,添加markdown)支持.store
: store 包含了全部的Redux和state 管理的邏輯styles
: 在styles文件夾內,咱們聲明變量,模板和能夠在組件間共享的樣式模式utils
: 整個應用中能夠重用的助手函數這裏的文件夾結構有兩個地方值得注意:
./
下.若是咱們有多個路由,我可能會使用react-router
包,同時建立pages
文件夾用於保存頁面級別的組件.container
文件夾用於鏈接應用組件到Redux.我發現增長container
文件夾反而添加了沒必要要的複雜性,讓一些新手感到很困惑,由於開發者老是要從沒有關聯的位置中導入文件(container
想要鏈接組件,反之亦然). 在個人使用經驗彙總,從當個來源導入文件工做的更好一點.由於咱們在使用styled-components
,因此能夠刪除掉index.css
和app.css
文件. 如今咱們要在src/styles
文件件中添加一些基礎模板樣式,建立文件global.js
和palette.js
文件
Palette包含了應用UI中使用的成組的顏色配置. 在src/styles/palette.js
中添加
global.js
用於生成應用中共享的基礎樣式. styled-components
的injectGlobal
方法應該要當心使用,可是用於應用級別的樣式時時很是有用的.
注意: 在styled-components v4中injectGlobal
已經被createGlobalStyle
替代了.
在components
文件夾中建立App
文件夾,把全部的 CRA默認生成的文件都移動到這個文件中,把App.js
文件重命名爲index.js
文件. 這樣就能夠導入components/App
如今代開src/index.js
文件(項目的根文件),使用更新的文件結構更新文件.
注意,由於以前咱們定義了NODE_PATH
,如今使用components/App
導入App
文件,styles/globals
來導入setGlobalStyles
文件. 執行setGlobalStyles()
函數能夠在應用中導入全局的樣式.
如今咱們已經準備好了啓動應用開發環境的核心配置. 運行下面命令啓動應用,會在http://localhost:3000
看到應用. 如今看上去還不是太好,可是應用已經跑起來了 :)
yarn start
# npm 安裝用 npm start
複製代碼
在src/store
文件中,建立index.js
,reducer.js
和middleware.js
文件. 讓咱們來初始化一個app
專項(feature)來管理應用的state.
以個人經驗,在生產級別的應用中,若是按照特性而不是按照功能進行分組,Redux會更具備管理性,相似於鴨子方法
(Ducks approach). "按照功能分組(grouping by functionality)" 方法中全部的actions,reducers,等等都位於獨立的文件夾中, 當應用規模增長時,在不一樣文件中切換難度就增大了. 若是按照特性分組,你須要的文件老是在一個位置.
在index.js
文件中,建立configureStore
函數,用於初始化應有的 Redux.
使用createStore
構建初始化store
. 從根reducer文件導入reducer
,同時從middleware配置文件中導入 middleware(中間件)
. initialState
應該在程序運行時提供,並傳遞給咱們的函數. 在生產中,要可以管理複雜的功能例如 SSR(服務端渲染),或者在初始化時從服務器獲取傳遞的數據. 在這裏初始state,可讓咱們更優雅的和抽象出store的建立過程.
在reducer.js
文件中,使用combineReducers
函數建立根reducer.此函數把全部的reducer函數組合起來生成單個的state樹.
接下來在middleware.js
中建立中間件. 中間件是每一次dispatch action 時都必需要執行的函數. 中間在擴展Redux應用時很是有用. 在文件中添加以下代碼
也要構建第一個Reducer.在 src/store/app
文件加中建立 reducer.js
和action.js
文件. 須要添加日間/夜間的切換模式功能,因此讓咱們建立一個action來管理這個特性.在src/store/app/action.js
添加下面代碼
咱們建立了一個actionTypes
對象放置actio-type常量. 相似的常量在reducer中用於匹配改變state的類型. 也要建立actions
對象,包含了能夠從應用中dispatch
用於改變state的 action函數.每個action都包括了一個type
和一個payload
(譯註: type告訴store要幹什麼,payload 是執行action時攜帶的條件).
最後,建立咱們的reducer
當咱們dispatch
一個SET_THEME
action時, 將會使用payload
的內容更新 state中theme
的屬性值. payload
是一個對象,形式是{theme:'value'}
.使用es6的展開操做...
,state
中對應payload
鍵的值會被替換掉.
若是須要詳細理解 Redux的基礎 ,看看Dan Abramov的視頻
如今返回src/index.js
文件,作一些更新,須要把咱們的應用鏈接到 Redux. 爲Provider
添加一個導入,更新渲染方法
如今應該已經作完了 Redux的整個工做.返回到localhost:3000
,在Chrome的console中能夠看到下面的內容
如今 Redux 已經初始化完畢, 開始完成 UI 的工做. 首先聲明一些會在應用中使用的樣式常量. 在本應用中,咱們要建立mediaQueries(媒體查詢)
文件包含構建響應式應用的常量. 建立src/styles/mediaQueries.js
文件,添加下面的代碼
返回到src/components/App
文件夾, 在index.js
文件中,更新文件內容
其中使用了styled-components
的ThemeProvider
組件.這個組件尅讓咱們把"theme"做爲prop
傳遞給建立的styled components. 這裏初始化theme爲 colorDark
對象.
App
中包含的組件,如今尚未建立,因此如今來建立.首先構建styld-components 組件. 在App
文件夾裏建立styles.js
文件, 添加代碼
建立的用於頁面的div
稱爲Wrapper
. 用於頁面標題的h1
建立爲Title
組件. styled-components
語法使用styled
對象定義 HTML 元素. 能夠用字符串定義組件的 CSS 屬性.
注意代碼20行, 咱們使用了theme
prop. 包含props
參數的函數由styled-components
注入到樣式字符串中,這麼,咱們就可提起屬性或者添加用於動態構建樣式的邏輯,從組件中抽象出構建樣式的邏輯.
接下來, 建立包含 Hacker Nees故事的 List
組件. 建立src/components/List
文件夾並添加index.js
,styles.js
文件. 在index.js
文件中,添加代碼
在styles.js
文件中建立ListWrapper
.使用從ThemeProvider
組件獲得的theme
props 的background-color
屬性.
最後建立ListItem
組件用於顯示單個的故事. 建立src/components/ListItem
文件夾和index.js
,styles.js
文件.
咱們想讓 UI模仿 Hacker News. 目前會在ListItem
中使用fake 數據裏模擬. 在index.js
文件中添加代碼
每一個故事都有標題,做者,評分,發帖時間,URL地址,評論數. 初始化這幾個值,以便於查看 UI 的樣子. 基於安全緣由, 添加rel="nofollow noreferrer noopener"
.
在styles.js
文件中添加下面代碼
這些應該就是咱們須要的基礎 UI 組件了. 返回到瀏覽器,應該看到使用fake數據的單個條目
是時候在應用添加實際數據了.咱們經過axios
庫來調用 Hacker News的 API.調用 API 的過程會在應用中引入 "side effect(反作用)",意思是調用 API 會從外部資源影響本地環境的state.
API 調用之因此被稱爲 side effect,緣由是在應用的state中引入了外部的數據. 其餘的side effect的例子包括和瀏覽器的localStorage
的交互操做, 追蹤用戶分析,鏈接到web socket,等等. 在 Redux 應用中可使用不少庫來管理 side effect. 從簡單的redux-thunk
到更爲複雜的redux-saga
. 然而他們的目的是相同的,就是讓 Redux與外界交互. redux-thunk
是最簡單的庫, 能夠在action 對象
中再次 dispatch
JavaScript 函數
. 這個功能就是咱們在使用axios
時須要的功能,在 API調用管理返回的promise對象.
在src/services
文件夾中,建立Api.js
和hackerNewsApi.js
文件. axios
庫有着難以置信的強大功能和擴展性. Api.js
包含的配置使得執行axios
請求更容易. 這裏沒有拷貝完整代碼,你能夠在源代碼
中看到信息內容,其中包含了更爲精細的配置.
在src/services/hackerNewsApi.js
文件中, 咱們要定義請求 Hacker News API 的函數. 在Hacker New API 文檔
能夠找到,若是要獲取 IDs 的列表, 要使用/v0/topstories
入口. 獲取每一個 id的獨立故事要使用/v0/items/<id>
入口.
v0/topstories
入口返回列表中 IDs的 400-500條故事. 由於咱們要獲取單個故事的數據,若是馬上獲取500個故事的數據會嚴重影響性能. 爲了解決這個問題,咱們一次只獲取20個故事的數據. 使用.slice()
函數基於頁面的故事 ID進行分割. 由於咱們使用/v0/item/<id>
調用每一個故事的數據, 所以使用Promise.all
把全部的請求返回的promise對象壓縮的一個數組中,而後用一個then()
,resolve返回獲取數據,而且保存 IDs 的順序標記.
爲了在應用管理咱們的故事state,咱們來建立一個story
reducer. 建立src/store/story
文件夾, 添加reducer.js
和action.js
文件. 在action.js
文件中添加代碼
爲 IDs請求和stor用的API 調用都建立了 request,success,failure的 actionTypes
.
咱們的actions
對象中包含了 用於請求管理的thunk
函數. 經過dispatch 函數而不是dispatch action 對象. 咱們就能夠在請求週期的不一樣點 dispatch 不一樣的acitons了.
函數getTopStoryIds
會執行 API 調用,獲取整個故事的列表. 在getTopStoryIds
函數中success(成功)的回調函數執行時,咱們會dispatch fetchStories
action,用於獲取第一頁故事的結果.
當 API 調用成功返回時,就能夠dispatch success Action,這樣就可使用新獲取的數據來更新 Redux的 store了.
thunk軟件包的基礎實現只是用了幾行代碼. 要充分理解它,須要對 Redux的中間件有了解,可是從代碼中,咱們能夠看到,若是咱們使用一個函數
來代替一個對象
,就能夠執行一個函數,而且把dispatch
做爲函數的參數傳遞.
如今咱們須要建立reducer用於 Redux store中的數據存儲. 在src/store/story/reudcer.js
中添加代碼
對於 FETCH_STORY_IDS_SUCCESS
action type,咱們展開當前 state和 payload. 在 payload 中惟一的鍵/值是storyIds
,展開操做將會用新的值來更新 state.
對於FETCH_STORIES_SUCCESS
action type. 在以前的故事列表中按順序添加故事,以便於獲取更多的頁面. 此外,增長page 數, 設置isFetching
state 爲false.
如今,State已經由 Redux管理了, 咱們就能夠在組件中顯示數據了.
經過使用react-redux
綁定,咱們能夠把組件鏈接到 Redux的store, 以props的形式接收Redux的 State.以後,只要 store 有更新,props就會引發組件的從新渲染,由此就更新了 UI.
在須要dispatch
action 的組件中,以 props 的形式傳遞函數. 以後在組件內部調用這些函數,就能夠觸發 Redux store 中的state變化.
來看看如何在應用中管理這個變化. 返回到src/components/App
文件夾,建立一個 App.js
文件, 從src/components/App/index.js
拷貝內容進來. 在index.js
文件裏面,咱們將會把App
組件鏈接到 Redux. 在index.js
文件中添加代碼
mapStateToProps
函數接受 Redux store
做爲參數,返回一些屬性到鏈接的組件中.對於App
,咱們須要 stories
數組, 當前頁 page
,storyIds
數組還有isFetching
指示器.
mapDispatchToProps
函數接受dispatch
函數做爲參數,把返回的函數對象做爲props傳遞給咱們的組件. 建立的函數fetchStoriesFirstPage
,執行時會dispatch
action 來獲取story IDs(而後獲取第一頁故事的內容).
咱們在App.js
中使用這兩個props,首先添加componentDidMount
,當組件在 DOM 中渲染完就能夠馬上獲取數據. 爲List
組件傳遞stories
props.
在src/components/List/index.js
中,遍歷stories 數組, 建立 ListItem
組件的數組. 設置列表的key爲story ID,而且展開story對象: ...story
.展開操做會把對象的屬性值做爲單個的props傳遞給組件. key
prop 是 React中組件做爲數組加載時的一個策略,可讓列表形式的渲染更新速度更快.
若是如今觀察屏幕,應該看到到的是硬編碼的20行列表數據
咱們須要使用從stories 獲取的數據對ListItem
進行更新.同時在 Hacker News中, 也會顯示上次故事更新的時間和來源的地址. 須要安裝 timeago.js
和URl
軟件包幫助計算沒有經過 API 直接獲取的數據, 使用下面命令執行安裝
yarn add timeago.js url
複製代碼
須要編寫助手函數來構建這些值. 從源碼的src/utils
文件夾中拷貝文件
如今更新 src/components/ListItem/index.js
文件
經過這一步, 如今就能夠在應用顯示前20個故事了- cool!
如今,咱們想實現的是當用於頁面滾動到底部, 獲取新的一頁.回憶一下,每次成功獲取故事以後,咱們都增長了store中page的數字. 因此在第一頁到達以後,Redux store 如今應該是page:1
.咱們須要在滾動到底部時dispatch
fetchStories
action.
爲了實現無限滾動,咱們會使用react-infinite-scroll-component
組件. 咱們也想實現一個方法來決定管是否要加載更多的頁面,這一點咱們使用reselect
在selector中實現.
yarn add react-infinite-scroll-component reselect
複製代碼
首先構建selector來計算是否有更多的故事存在. 建立 src/store/story/selecor.js
文件. 爲了判斷是否有更多故事存在, 咱們 Redux store中的storyIds
數組的長度是否和stories
的長度相同, 若是stories
的長度短一點,意思就是有更多的頁面存在
在src/components/App/index.js
container中,導入hasMoreStoriesSelector
在mapStateToProps
中添加 鍵hasMoreStories
.同時在mapDispatchProps
中添加fetchStories
action,便於滾動時 dispatch action.
咱們想在等待 API請求時使用動畫顯示. 建立src/components/Loader
文件夾,index.js
和styles.js
文件. 須要的動畫是閃動的三個圓點.
在styles.js
文件中添加下面代碼
@keyframe 是定義動畫的 CSS 技術. 上面代碼顯示了在 Styled Components中的代碼抽象. 有三個圓點,透明度從0.2開始增長到1, 而後返回到0.2, 給第二個和第三個點添加延遲,表現出彈跳式的偏移.
咱們的Loader
組件就是有三個獨立span元素的動畫styled components動畫組件.
如今,準備爲列表添加功能,在App
組件中導入無限加載模塊和Loader
組件.也要建立fetchStories
回調函數,將會調用fetchStories
prop dispatch 下一頁的action. 只有在isFetching
爲 false 時dispatch fetchStories
action. 若是爲 true.咱們就屢次獲取統一頁面. 你的src/components/App/App.js
文件應該以下
當咱們滾動到頁面底部, 只要hasMoreStories
爲真,InfiniteScroll
組件將會調用this.fetchStroies
. 當fetchStories API 請求返回時,新的故事會添加到stories
數組的尾部,渲染到頁面中.
在教程剛開始, 咱們初始化了一個theme
property.如今,留給你實現一個toggle功能. 在一些組件中添加點擊事件,dispatch setTheme
action.切換 light
和dark
的狀態. 在ThemeProvider
組件中須要一個三元條件判斷若是 state.app.theme==='dark'
就傳遞colorDark
,不然就傳遞colorsLight
.
若是你卡住了,能夠看看源碼的實現.加入Slack 尋求幫助. 試試咱們的辦法.
對於應用的最後一步都是投入生產. 由於咱們的功能是在客戶端,能夠免費部署在 GitHub 主頁的靜態網站.
提交你的代碼並推送到Github. 我命名倉庫爲hn-clone
.若是你在建立倉庫和上傳代碼是遇到問題能夠參照一下這個指導
如今使用以下的步驟發送過的 GitHub 主頁:
"homepage":"http://<username>.github.io/<repo-name>"
使用你的實際值替換<username>
和<repo-name>
. 個人值是 treyhuffine
和hn-clone
.gh-pages
做爲開發依賴項yarn add -D gh-pages
複製代碼
package
.json文件中添加兩個腳本"predeploy": "npm run build","deploy": "gh-pages -d build"
複製代碼
yarn deploy
並訪問在homepage中定義的URL.如今你的 Hacker News 投入生產了.
本文覆蓋了構建 Hacker News clone所必須的全部的功能. 源碼還有一些額外的特性,持續更新中, 查看一下是否有靈感出現能夠繼續添加功能,學習更多的 React 知識.
不要忘了下載Chrome 擴展, 並訪問 gitconnectec.com網站,加入開發者社區.
原文發表在 gitconnected.com-開發者社區