React+Redux打造“NEWS EARLY”單頁應(yīng)用 一個(gè)項(xiàng)目理解最前沿技術(shù)棧真諦

之前寫過一篇文章,分享了我利用閑暇時(shí)間,使用React+Redux技術(shù)棧重構(gòu)的百度某產(chǎn)品個(gè)人中心頁面。您可以參考這里,或者參考Github代碼倉庫地址。
這個(gè)工程實(shí)例中,我采用了廠內(nèi)的工程構(gòu)建工具-FIS3,并貫穿了react+redux基本思想。

今天這篇文章給大家分享一個(gè)更加復(fù)雜,但是非常有趣的一個(gè)項(xiàng)目-
News Early單頁應(yīng)用

最近我發(fā)現(xiàn),React Redux生態(tài)圈項(xiàng)目活躍。但是作品質(zhì)量“良莠不齊”,很多非常熱門的項(xiàng)目不僅沒有起到“布道”作用,而且在一定程度上“誤導(dǎo)”了讀者。在這篇文章里面我會(huì)有詳細(xì)說明。當(dāng)然,我自己也是資歷淺顯,水平有限。希望大神能夠給與斧正。

我把這個(gè)項(xiàng)目所有代碼托管在了我個(gè)人Github之中,感興趣的讀者可以跟我探討。

同時(shí)通過這個(gè)項(xiàng)目實(shí)例和這篇文章,一步一步說明了這個(gè)項(xiàng)目開發(fā)細(xì)節(jié),并且包括了優(yōu)化手段等內(nèi)容。希望使大家對于React技術(shù)棧,包括:React UI框架 + Redux數(shù)據(jù)流框架+React Router路由管理+Webpack構(gòu)建工具等,有一個(gè)更加清晰深刻的理解。

項(xiàng)目背景

在國外上學(xué)和工作期間,能暢通無阻的訪問諸如:BBC,CNN,ESPN,Le Figaro等新聞媒體是一大便利,也是我個(gè)人閑暇時(shí)期一個(gè)喜好之一。
甚至外出旅游時(shí),在酒店收看這些媒體衛(wèi)視(尤其CNN)竟然也是放松休閑的一大方式。。。

當(dāng)然,國內(nèi)環(huán)境對于這些境外媒體顯然不是太友好。
基于此,我設(shè)計(jì)開發(fā)了News Early項(xiàng)目。

這個(gè)項(xiàng)目是一個(gè)包括:BBC,CNN,The NewYork Times等70多個(gè)國際知名媒體的即時(shí)頭條新聞聚合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).

整個(gè)項(xiàng)目我使用了包括但不限于以下技術(shù)棧和構(gòu)建工具:
1)React UI框架from Facebook;
2)JSX模版;
3)Redux數(shù)據(jù)流設(shè)計(jì);
4)Webpack構(gòu)建工具;
5)Less預(yù)處理器;
......

項(xiàng)目設(shè)計(jì)

整個(gè)Web APP的部分使用體驗(yàn),我用以下GIF圖示來呈現(xiàn):
(請耐心等待GIF圖加載)

APP 使用截圖
  • 頁面頂部導(dǎo)航條
    包括:側(cè)欄菜單開啟按鈕和右側(cè)的刷新頁面按鈕。
  • 頁面內(nèi)容頭部輪播圖
    支持自動(dòng)播放和手勢滑動(dòng)操控。
  • 頁面主體部分
    主體部分是所對應(yīng)的新聞?lì)l道的headlines頭條新聞,一般有10-20個(gè)items左右。每一個(gè)item包含一張新聞圖片,新聞導(dǎo)讀(Abstract)以及新聞發(fā)布時(shí)間(publish time)。
  • 左側(cè)折疊菜單欄
    功能用于新聞?lì)l道的篩選。
    以Gif圖截取為止,一共接入了:BBC News,BBC Sport,CNN,ESPN,F(xiàn)inancial Times,USA Today,MTV News7家國際媒體。

因?yàn)槲也皇歉阋曈X設(shè)計(jì)的,也不是做頁面交互設(shè)計(jì)的(好吧,我只是一枚碼農(nóng))。所以為了節(jié)省時(shí)間,整體APP的樣式上,包括界面顏色等,我參考了賣座網(wǎng)的實(shí)現(xiàn)。

項(xiàng)目架構(gòu)和落地

下面,我為大家介紹一下整個(gè)項(xiàng)目的設(shè)計(jì)構(gòu)成和開發(fā)細(xì)節(jié)。

數(shù)據(jù)流狀態(tài)演示

熟悉Redux數(shù)據(jù)流框架的同學(xué),應(yīng)該對于store,dispatch,action,reducer,以及中間件等概念比較熟悉。這里不再進(jìn)行講解。
這套架構(gòu)中,最重要的就是數(shù)據(jù)流的設(shè)計(jì)。

首先,我們先整體看一下在“切換頻道”這個(gè)交互發(fā)生時(shí),整個(gè)項(xiàng)目的數(shù)據(jù)流向和數(shù)據(jù)結(jié)構(gòu)的演示:

數(shù)據(jù)流動(dòng)示意圖

目錄結(jié)構(gòu)

如圖所示:

目錄結(jié)構(gòu)

整個(gè)項(xiàng)目業(yè)務(wù)代碼部分,我拆分成9個(gè)UI組件,1個(gè)全局Store,一個(gè)actions定義文件。

  • app是開發(fā)目錄
    • actions目錄集中了全局所有的actions
    • components目錄集中了全局用到的所有UI組件
    • reducers目錄集中了Redux架構(gòu)中的所有reducers
    • store目錄定義全局唯一的store
    • style目錄集中了全局所有組件的樣式文件
    • main.js為全局的入口函數(shù)
  • build是打包后結(jié)果目錄
    • index.html是輸出頁面文件
    • bundle.js開發(fā)目錄下腳本文件打包后的產(chǎn)出
    • img文件定義了APP開啟時(shí)的loading圖片
  • node_modules相信大家不會(huì)陌生,這是依賴文件
    ...
    其他配置文件不再一一介紹。

10個(gè)組件包括:

  • appIndex: 組件容器
  • billboardCarousel: 頁面輪播圖組件
  • currentChanel: 頁面headlines新聞?lì)^條組件
  • homeView: 主體頁面
  • imagePlaceholder: 占位圖組件
  • loading: 加載提示組件
  • navBar :頂部導(dǎo)航組件
  • sideBar: 側(cè)邊欄組件
  • routerWrap: 路由相關(guān)組件

骨架構(gòu)建

我認(rèn)為,redux之所以學(xué)習(xí)曲線陡,很大程度上就在于數(shù)據(jù)流的貫通上。

“組件觸發(fā)(dispatch)各種action,單向數(shù)據(jù)流流向reducer,reducer是一個(gè)純函數(shù)(函數(shù)式編程思想),接收處理action,返回新的數(shù)據(jù),組件進(jìn)而更新”

這一套理論并不難理解。

但是落實(shí)在工程上,尤其要結(jié)合react,那就不好做了。即使有人做出來,業(yè)務(wù)就算可以跑得通,但是相比核心思想,卻是背道而馳。社區(qū)上我看過很多項(xiàng)目,在寫法上不分青紅皂白,只要能運(yùn)行,胡亂設(shè)計(jì)一通,誤導(dǎo)初學(xué)者。

比如在整個(gè)項(xiàng)目中,存在多個(gè)stores這種常見的問題。

那么,為什么不建議存在多個(gè)store呢?
答案可以在官方FAQ中找到。內(nèi)容較多,如果英文閱讀吃力,我大體翻譯一下:
熟悉Flux原始模型的讀者可能了解,F(xiàn)lux存在多個(gè)stores,每個(gè)store都維護(hù)了不同層次的數(shù)據(jù)。這樣設(shè)計(jì)的問題在于,一個(gè)store需要等待另外一個(gè)store的操作處理。我們Redux實(shí)現(xiàn)了切分?jǐn)?shù)據(jù)層次,避免了這種情況的發(fā)生。
僅維持單個(gè)store不僅可以使用Redux DevTools,還能簡化數(shù)據(jù)的持久化及深加工、精簡訂閱的邏輯處理。
單一store這種方式,我們不用考慮store模塊的導(dǎo)入、 Redux應(yīng)用的封裝,后期支持服務(wù)器渲染也將變得更為簡便。

如果上邊這段話過于抽象,難以理解的話,那就直接看我的代碼實(shí)現(xiàn)吧。

定義全局唯一的store:

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

其中,我使用了redux-thunk作為中間件,用于處理異步action。這樣,把異步過程放在action級別解決,對component沒有影響。
另外composeEnhancers是用于使用redux devtool的設(shè)置。

容器組件構(gòu)建:

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進(jìn)行連接。AppIndex是整個(gè)項(xiàng)目唯一的容器組件。進(jìn)行action的dispatch,以及向下傳遞props給UI組件(木偶組件)。

如果你還不理解容器組件UI組件的區(qū)別,可以去官方文檔學(xué)習(xí)。這兩個(gè)概念極其重要,它直接決定你是否能設(shè)計(jì)出有效且合理的組件架構(gòu)。

另外,你會(huì)發(fā)現(xiàn)我使用了react-router進(jìn)行路由管理。其實(shí)整個(gè)項(xiàng)目沒有必要使用單頁路由。這個(gè)路由管理的引入,說實(shí)話,比較雞肋。但并不會(huì)對項(xiàng)目產(chǎn)生任何影響。我引入他的原因主要有兩點(diǎn)。

  • 第一是,后續(xù)進(jìn)行二次開發(fā),考慮到更多的產(chǎn)品迭代的話,使用路由管理是必須的,我們要為長遠(yuǎn)準(zhǔn)備。
  • 另一個(gè)原因就是,我從來沒用用過,好吧,想嘗鮮下。

actions設(shè)計(jì)

actions當(dāng)然是必不可少的,我這里選取最重要的“fetchContents”這個(gè)action creator來討論一下。

初次進(jìn)入頁面時(shí),以及左側(cè)邊欄點(diǎn)擊選擇新聞?lì)l道時(shí),都要去拉取數(shù)據(jù)。比如,APP第一次渲染,默認(rèn)加載“BBC News”新聞?lì)l道,頁面主體組件在掛載完成后:

componentDidMount() {
    //獲取內(nèi)容
    this.props.fetchContents('bbc-news');
}

向上調(diào)用fetchContents方法,并逐級上傳到容器組件。由容器組件進(jìn)行dispatch:

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

source表示拉取的新聞?lì)l道。此處當(dāng)然是'bbc-news'。

在actions.js文件中,進(jìn)行異步action的處理并拉取數(shù)據(jù)。這里,我使用了最新的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));
        })
}

對應(yīng)的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)
            })
        }
    }
}

請求優(yōu)化

我們知道,這些異步請求的訪問速度是很慢的。因此,我采用了幾種方法來進(jìn)行優(yōu)化。

  • 第一個(gè)方法就是加載時(shí)的loading美化。
    我使用了來自網(wǎng)絡(luò)的圖片占位。
    當(dāng)我把控制臺(tái)中網(wǎng)絡(luò)環(huán)境人為的模擬為3G時(shí),頁面效果如下:
    (請耐心等待GIF圖加載)
加載占為圖

原諒我使用了這么粉嫩少女的加載圖。。。

  • 第二個(gè)方法其實(shí)是一個(gè)trick,我的全局圖片在初始狀態(tài)時(shí)opacity設(shè)置為0,在onload事件觸發(fā)時(shí)設(shè)置一個(gè)fadeIn的效果:

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

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

  • Web Storage來進(jìn)行優(yōu)化
    因?yàn)楦鞔笮侣劽襟w的headlines發(fā)布更新是不定時(shí)的,這個(gè)時(shí)間間隔可能較長。而我考慮到用戶使用這個(gè)Web APP一般都是在碎片時(shí)間中。因此我采用了sessionStorage進(jìn)行緩存內(nèi)容。不要問我為什么不使用localStorage...,如果你存在疑問,建議對于Web Storage的特性再去回爐重修一下。

具體實(shí)現(xiàn)方式就是在發(fā)送請求時(shí)判斷sessionStorage是否已經(jīng)存在此新聞媒體(比如bbc)的數(shù)據(jù)。如果存在就使用緩存。否則就去進(jìn)行AJAX請求,請求成功的回調(diào)函數(shù)里進(jìn)行緩存的種植。
代碼部分如下:

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)
    })
}

當(dāng)然,有種植緩存,就要有清除緩存。這個(gè)按鈕我設(shè)置在里navBar組件的最右側(cè):

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

其他細(xì)節(jié)

為了使用先進(jìn)的構(gòu)建工具的需求,我使用了node最新版本。但是因?yàn)楣ぷ鳂I(yè)務(wù)的需要,又要同時(shí)保留低版本node環(huán)境。為此,我使用了:n這個(gè)利器進(jìn)行node版本管理。

同時(shí),我使用了webPack一系列強(qiáng)大開發(fā)功能和構(gòu)建功能。包括但不限于:

  • 熱更新
  • Less編譯插件
  • 服務(wù)器構(gòu)建,使用了8088端口
  • jsx,es6編譯
  • 打包發(fā)布
  • 彩色日志

...等等,但是我可不是webpack專家。在狼廠,當(dāng)然使用更多的是FIS構(gòu)建工具。關(guān)于FIS和webpack的比較,我的網(wǎng)紅同事@顏大神有過探索。

總結(jié)

這篇文章涉及到了較為前沿的前端開發(fā)技術(shù)棧。包括了React框架,Redux數(shù)據(jù)流框架以及函數(shù)式編程、異步action中間件,fetch異步請求,webpack配置等等。也無形中涉及到了一些成熟產(chǎn)品的設(shè)計(jì)理念思路。當(dāng)然這個(gè)項(xiàng)目還遠(yuǎn)沒有成熟。在代碼倉庫中,我會(huì)不間斷進(jìn)行更新。
希望本文對大家在各個(gè)維度都有所啟發(fā)。也懇請業(yè)界大牛不吝賜教,進(jìn)行斧正。

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

現(xiàn)在想想當(dāng)時(shí)那么菜的原因還是在于自己的JS基礎(chǔ)不夠牢固。當(dāng)你面對迅速更新?lián)Q代的前端技術(shù)踟躕茫然時(shí),唯一的捷徑就是從基礎(chǔ)抓起,從JS原型原型鏈,this,執(zhí)行環(huán)境上下文等等看起。

覺得前端知識有欠缺的讀者們,歡迎follow我。最近我會(huì)帶大家“重讀”JS經(jīng)典書籍,以code demo的形式提煉知識點(diǎn),并會(huì)同步到博客和個(gè)人Github上。

Happying code!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容