翻譯|React & Redux Tutorial — Build a Hacker News Clone

原文參見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: 整個應用中能夠重用的助手函數

這裏的文件夾結構有兩個地方值得注意:

  1. 應用中只有一個路由,位於根./下.若是咱們有多個路由,我可能會使用react-router包,同時建立pages文件夾用於保存頁面級別的組件.
  2. 我沒有使用單獨的container文件夾用於鏈接應用組件到Redux.我發現增長container文件夾反而添加了沒必要要的複雜性,讓一些新手感到很困惑,由於開發者老是要從沒有關聯的位置中導入文件(container想要鏈接組件,反之亦然). 在個人使用經驗彙總,從當個來源導入文件工做的更好一點.

由於咱們在使用styled-components,因此能夠刪除掉index.cssapp.css文件. 如今咱們要在src/styles文件件中添加一些基礎模板樣式,建立文件global.jspalette.js文件

Palette包含了應用UI中使用的成組的顏色配置. 在src/styles/palette.js中添加

global.js用於生成應用中共享的基礎樣式. styled-componentsinjectGlobal方法應該要當心使用,可是用於應用級別的樣式時時很是有用的.

注意: 在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

複製代碼

在 React 應用添加Redux

src/store文件中,建立index.js,reducer.jsmiddleware.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.jsaction.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中能夠看到下面的內容

使用 React和Styld Components 構建 UI

如今 Redux 已經初始化完畢, 開始完成 UI 的工做. 首先聲明一些會在應用中使用的樣式常量. 在本應用中,咱們要建立mediaQueries(媒體查詢) 文件包含構建響應式應用的常量. 建立src/styles/mediaQueries.js文件,添加下面的代碼

返回到src/components/App文件夾, 在index.js文件中,更新文件內容

其中使用了styled-componentsThemeProvider組件.這個組件尅讓咱們把"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數據的單個條目

使用 Redux 和 Axios 構建 API 調用

是時候在應用添加實際數據了.咱們經過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.jshackerNewsApi.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.jsaction.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 APP 鏈接到 Redux Store

經過使用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.jsURl 軟件包幫助計算沒有經過 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中,導入hasMoreStoriesSelectormapStateToProps中添加 鍵hasMoreStories.同時在mapDispatchProps中添加fetchStories action,便於滾動時 dispatch action.

咱們想在等待 API請求時使用動畫顯示. 建立src/components/Loader文件夾,index.jsstyles.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.切換 lightdark的狀態. 在ThemeProvider組件中須要一個三元條件判斷若是 state.app.theme==='dark'就傳遞colorDark,不然就傳遞colorsLight.

若是你卡住了,能夠看看源碼的實現.加入Slack 尋求幫助. 試試咱們的辦法.

部署到GitHub 主頁

對於應用的最後一步都是投入生產. 由於咱們的功能是在客戶端,能夠免費部署在 GitHub 主頁的靜態網站.

提交你的代碼並推送到Github. 我命名倉庫爲hn-clone.若是你在建立倉庫和上傳代碼是遇到問題能夠參照一下這個指導

如今使用以下的步驟發送過的 GitHub 主頁:

  1. 在package.json文件中添加 "homepage":"http://<username>.github.io/<repo-name>" 使用你的實際值替換<username><repo-name>. 個人值是 treyhuffinehn-clone.

  1. 安裝gh-pages做爲開發依賴項
yarn add -D gh-pages
複製代碼
  1. package.json文件中添加兩個腳本
"predeploy": "npm run build","deploy": "gh-pages -d build"
複製代碼

  1. 最後運行yarn deploy 並訪問在homepage中定義的URL.

如今你的 Hacker News 投入生產了.

結論

本文覆蓋了構建 Hacker News clone所必須的全部的功能. 源碼還有一些額外的特性,持續更新中, 查看一下是否有靈感出現能夠繼續添加功能,學習更多的 React 知識.

不要忘了下載Chrome 擴展, 並訪問 gitconnectec.com網站,加入開發者社區.

原文發表在 gitconnected.com-開發者社區

相關文章
相關標籤/搜索