React+Redux打造「NEWS EARLY」單頁應用 一步步讓你理解最前沿技術棧的真諦

之前寫過一篇文章,分享了我利用閑暇時間,使用React+Redux技術棧重構的百度某產品個人中心頁面。您可以參考這裡,或者參考Github代碼倉庫地址。

這個工程實例中,我採用了廠內的工程構建工具-FIS,並貫穿了react+redux基本思想。

今天這篇文章給大家分享一個更加複雜,但是非常有趣的一個項目-

News Early單頁應用。

我把這個項目所有代碼託管在了我個人Github之中,感興趣的讀者可以跟我探討。

最近我發現,React Redux生態圈項目活躍。但是作品質量「良莠不齊」,很多非常熱門的項目不僅沒有起到「佈道」作用,而且在一定程度上「誤導」了讀者。在這篇文章裡面我會有詳細說明。當然,我自己也是資歷淺顯,水平有限。希望大神能夠給與斧正。

同時通過這個項目實例和這篇文章,一步一步說明了這個項目開發細節,並且包括了優化手段等內容。希望使大家對於React技術棧,包括:Redux數據流框架+React Router路由管理+Webpack構建工具等,有一個更加清晰深刻的理解。

項目背景

在國外上學和工作期間,能暢通無阻的訪問諸如:BBC,CNN,ESPN,Le Figaro等新聞媒體是一大便利,也是我個人閑暇時期一個喜好之一。

甚至外出旅遊時,在酒店收看這些媒體衛視(尤其CNN)竟然也是放鬆休閑的一大方式。。。

當然,國內環境對於這些境外媒體顯然不是太友好。

基於此,我設計開發了News Early項目。

這個項目是一個包括:BBC,CNN,The NewYork Times等70多個國際知名媒體的即時頭條新聞聚合APP。

News Early is a simple and easy-to-use Web APP that gathers the headlines currently published on a range of news sources and blogs (70 and counting so far).

整個項目我使用了包括但不限於以下技術棧和構建工具:

1)React UI框架from Facebook;

2)JSX模版;

3)Redux數據流設計;

4)Webpack構建工具;

5)Less預處理器;

……

項目設計

整個Web APP的部分使用體驗,我用以下GIF圖示來呈現:

(請耐心等待GIF圖載入)

APP 使用截圖

  • 1)頁面頂部導航條

    包括:側欄菜單開啟按鈕和右側的刷新頁面按鈕。
  • 2)頁面內容頭部輪播圖

    支持自動播放和手勢滑動操控。

  • 3)頁面主體部分

    主體部分是所對應的新聞頻道的headlines頭條新聞,一般有10-20個items左右。每一個item包含一張新聞圖片,新聞導讀(Abstract)以及新聞發布時間(publish time)。
  • 4)左側摺疊菜單欄

    功能用於新聞頻道的篩選。

    以Gif圖截取為止,一共接入了:BBC News,BBC Sport,CNN,ESPN,Financial Times,USA Today,MTV News7家國際媒體。

因為我不是搞視覺設計的,也不是做頁面交互設計的。我只是一枚碼農。所以為了節省時間,整體APP的樣式上,包括界面顏色等,我參考了賣座網的實現。

項目架構和落地

下面,我為大家介紹一下整個項目的設計構成和開發細節。

數據流狀態演示

熟悉Redux數據流框架的同學,應該對於store,dispatch,action,reducer,以及中間件等概念比較熟悉。這裡不再進行講解。

這套架構中,最重要的就是數據流的設計。

首先,我們先整體看一下在「切換頻道」這個交互發生時,整個項目的數據流向和數據結構的演示:

數據流動示意圖

目錄結構

如圖所示:

目錄結構

整個項目業務代碼部分,我拆分成9個UI組件,1個全局Store,一個actions定義文件。

  • app是開發目錄
    • actions目錄集中了全局所有的actions
    • components目錄集中了全局用到的所有UI組件
    • reducers目錄集中了Redux架構中的所有reducers
    • store目錄定義全局唯一的store
    • style目錄集中了全局所有組件的樣式文件
    • main.js為全局的入口函數
  • build是打包後結果目錄
    • index.html是輸出頁面文件
    • bundle.js開發目錄下腳本文件打包後的產出
    • img文件定義了APP開啟時的loading圖片
  • node_modules相信大家不會陌生,這是依賴文件

    其他配置文件不再一一介紹。

10個組件包括:

  • appIndex: 組件容器
  • billboardCarousel: 頁面輪播圖組件
  • currentChanel: 頁面headlines新聞頭條組件
  • homeView: 主體頁面
  • imagePlaceholder: 占點陣圖組件
  • loading: 載入提示組件
  • navBar :頂部導航組件
  • sideBar: 側邊欄組件
  • routerWrap: 路由相關組件

骨架構建

我認為,redux之所以學習曲線陡,很大程度上就在於數據流的貫通上。

「組件觸發(dispatch)各種action,單向數據流流向reducer,reducer是一個純函數(函數式編程思想),接收處理action,返回新的數據,組件進而更新」

這一套理論並不難理解。

但是落實在工程上,尤其要結合react,那就不好做了。即使有人做出來,業務就算可以跑得通,但是相比核心思想,卻是背道而馳。社區上我看過很多項目,在寫法上不分青紅皂白,只要能運行,胡亂設計一通,誤導初學者。

比如在整個項目中,存在多個stores這種常見的問題。

那麼,為什麼不建議存在多個store呢?

答案可以在官方FAQ中找到。內容較多,如果英文閱讀吃力,我大體翻譯一下:

熟悉Flux原始模型的讀者可能了解,Flux存在多個stores,每個store都維護了不同層次的數據。這樣設計的問題在於,一個store需要等待另外一個store的操作處理。我們Redux實現了切分數據層次,避免了這種情況的發生。

僅維持單個store不僅可以使用Redux DevTools,還能簡化數據的持久化及深加工、精簡訂閱的邏輯處理。

單一store這種方式,我們不用考慮store模塊的導入、 Redux應用的封裝,後期支持伺服器渲染也將變得更為簡便。

如果上邊這段話過於抽象,難以理解的話,那就直接看我的代碼實現吧。

定義全局唯一的store:

const store = createStore( combineReducers({ sideBarChange, contents, routing: routerReducer }), composeEnhancers(applyMiddleware(thunkMiddleware)),);

其中,我使用了redux-thunk作為中間件,用於處理非同步action。這樣,把非同步過程放在action級別解決,對component沒有影響。

另外composeEnhancers是用於使用redux devtool的設置。

容器組件構建:

const mapStateToProps = (state) => { return { showLeftNav: state.sideBarChange.showLeftNav, loading: state.contents.loading, contents: state.contents.contents, currentChanel: state.contents.currentChanel }}var App = connect(mapStateToProps)(AppIndex);render( <Provider store={store}> <Router history={history}> <Route path="/" component={App}> <Route path="home" component={HomeView}/> </Route> </Router> </Provider>, document.getElementById("app"));

其中,我使用了react-redux進行連接。AppIndex是整個項目唯一的容器組件。進行action的dispatch,以及向下傳遞props給UI組件(木偶組件)。

如果你還不理解容器組件UI組件的區別,可以去官方文檔學習。這兩個概念極其重要,它直接決定你是否能設計出有效且合理的組件架構。

另外,你會發現我使用了react-router進行路由管理。其實整個項目沒有必要使用單頁路由。這個路由管理的引入,說實話,比較雞肋。但並不會對項目產生任何影響。我引入他的原因主要有兩點。

  • 第一是,後續進行二次開發,考慮到更多的產品迭代的話,使用路由管理是必須的,我們要為長遠準備。
  • 另一個原因就是,我從來沒用用過,好吧,想嘗鮮下。

actions設計

actions當然是必不可少的,我這裡選取最重要的「fetchContents」這個action creator來討論一下。

初次進入頁面時,以及左側邊欄點擊選擇新聞頻道時,都要去拉取數據。比如,APP第一次渲染,默認載入「BBC News」新聞頻道,頁面主體組件在掛載完成後:

componentDidMount() { //獲取內容 this.props.fetchContents("bbc-news");}

向上調用fetchContents方法,並逐級上傳到容器組件。由容器組件進行dispatch:

fetchContents={(source)=>{this.props.dispatch(action.fetchContents(source))}}

source表示拉取的新聞頻道。此處當然是』bbc-news』。

在actions.js文件中,進行非同步action的處理並拉取數據。這裡,我使用了最新的fetch API來代替古老的XHR,並利用fetch的promise的理念,封裝了一層_get方法,用於AJAX非同步請求:

const sendByGet = ({url}, dispatch) => {let finalUrl = url + "&apiKey=1a445a0861e"return fetch(finalUrl) .then(res => { if (res.status >= 200 && res.status < 300) { return res.json(); } return Promise.reject(new Error(res.status)); })}

對應的action操作:

export const fetchContents = (source) => { const url = "..."; return (dispatch) => { dispatch({type: FETCH_CONTENTS_START}); if (sessionStorage.getItem(source)) { console.log("get from sessionStorage"); let articles = JSON.parse(sessionStorage.getItem(source)); dispatch({type: FETCH_CONTENTS_SUCCESS, contents: Object.assign(articles, {currentChanel: source.toUpperCase()})}) } else { sendByGet({url}, dispatch) .then((json) => { if (json.status === "ok") { sessionStorage.setItem(source, JSON.stringify(json.articles)); return dispatch({type: FETCH_CONTENTS_SUCCESS, contents: Object.assign(json.articles, {currentChanel: source.toUpperCase()})}) } return Promise.reject(new Error("FETCH_CONTENTS_SUCCESS failure")); }) .catch((error) => { return Promise.reject(error) }) } }}

請求優化

我們知道,這些非同步請求的訪問速度是很慢的。因此,我採用了幾種方法來進行優化。

  • 第一個方法就是載入時的loading美化。

    我使用了來自網路的圖片佔位。

    當我把控制台中網路環境人為的模擬為3G時,頁面效果如下:

    (請耐心等待GIF圖載入)

載入佔為圖

原諒我使用了這麼粉嫩少女的載入圖。。。

  • 第二個方法其實是一個trick,我的全局圖片在初始狀態時opacity設置為0,在onload事件觸發時設置一個fadeIn的效果:

    <img ref="image" src={imgSrc} onLoad={this.handleImageLoaded.bind(this)}/>handleImageLoaded() { this.refs["image"].style.opacity = 1;}

這樣的一個小技巧最初來自Facebook對用戶體驗的研究。如果您對此有興趣,可以在我的另外一篇文章中找到相關內容。

  • Web Storage來進行優化

    因為各大新聞媒體的headlines發布更新是不定時的,這個時間間隔可能較長。而我考慮到用戶使用這個Web APP一般都是在碎片時間中。因此我採用了sessionStorage進行緩存內容。不要問我為什麼不使用localStorage…,如果你存在疑問,建議對於Web Storage的特性再去回爐重修一下。

具體實現方式就是在發送請求時判斷sessionStorage是否已經存在此新聞媒體(比如bbc)的數據。如果存在就使用緩存。否則就去進行AJAX請求,請求成功的回調函數里進行緩存的種植。

代碼部分如下:

if (sessionStorage.getItem(source)) { console.log("get from sessionStorage"); let articles = JSON.parse(sessionStorage.getItem(source)); dispatch({type: FETCH_CONTENTS_SUCCESS, contents: Object.assign(articles, {currentChanel: source.toUpperCase()})})}else { sendByGet({url}, dispatch) .then((json) => { if (json.status === "ok") { sessionStorage.setItem(source, JSON.stringify(json.articles)); return dispatch({type: FETCH_CONTENTS_SUCCESS, contents: Object.assign(json.articles, {currentChanel: source.toUpperCase()})}) } return Promise.reject(new Error("FETCH_CONTENTS_SUCCESS failure")); }) .catch((error) => { return Promise.reject(error) })}

當然,有種植緩存,就要有清除緩存。這個按鈕我設置在里navBar組件的最右側:

const CLEAR_SESSIONSTORAGE = "CLEAR_SESSIONSTORAGE";export const refresh = () => { sessionStorage.clear(); return dispatch => dispatch({type: CLEAR_SESSIONSTORAGE});}

其他細節

為了使用先進的構建工具的需求,我使用了node最新版本。但是因為工作業務的需要,又要同時保留低版本node環境。為此,我使用了:n這個利器進行node版本管理。

同時,我使用了webPack一系列強大開發功能和構建功能。包括但不限於:

  • 熱更新
  • Less編譯插件
  • 伺服器構建,使用了8088埠
  • jsx,es6編譯
  • 打包發布
  • 彩色日誌

…等等,但是我可不是webpack專家。在狼廠,當然使用更多的是FIS構建工具。關於FIS和webpack的比較,我的網紅同事@顏大神有過探索。

總結

這篇文章涉及到了較為前沿的前端開發技術棧。包括了React框架,Redux數據流框架以及函數式編程、非同步action中間件,fetch非同步請求,webpack配置等等。也無形中涉及到了一些成熟產品的設計理念思路。當然這個項目還遠沒有成熟。在代碼倉庫中,我會不間斷進行更新。

希望本文對大家在各個維度都有所啟發。也懇請業界大牛不吝賜教,進行斧正。

最後想跟大家談一下對於框架和前端學習的一些感受。我記得我剛開始工作,在初次接觸前端時,是使用ionic,即Angular框架和phoneGap開發hybrid移動APP。當時我是完全懵b的,只是感覺比利時同事用的超high,6到飛起。每次他用濃重的比利時口音法語給我講解時,我聽的雲里霧裡,不知所以。

現在想想當時那麼菜的原因還是在於自己的JS基礎不夠牢固。當你面對迅速更新換代的前端技術踟躕茫然時,唯一的捷徑就是從基礎抓起,從JS原型原型鏈,this,執行環境上下文等等看起。

覺得前端知識有欠缺的讀者們,歡迎follow我。最近我會帶大家「重讀」JS經典書籍,以code demo的形式提煉知識點,並會同步到博客和個人Github上。

Happying code!

PS:百度知識搜索部大前端繼續招兵買馬,有意向者火速聯繫。。。

本文作者:留學法國的帥哥,國家二級運動員,百度高級FE,快來關注 Lucas HC - 知乎

原文鏈接:React+Redux打造「NEWS EARLY」單頁應用 一步步讓你理解最前沿技術棧的真諦

微信公眾號

關注微信公眾號:顏海鏡,最新博文優先推送,不再錯過精彩內容。


推薦閱讀:

setState何時同步更新狀態
CSS Modules入門Ⅲ:與React協同
基於 JSX 的動態數據綁定
React 從青銅到王者系列教程之倔強青銅篇

TAG:React | Redux | JavaScript |