之前寫過一篇文章,分享了我利用閒暇時間,使用React+Redux技術棧重構的百度某產品個人中心頁面。您可以參考這裡,或者參考Github程式碼倉庫地址。
這個工程例項中,我採用了廠內的工程構建工具-FIS3,並貫穿了react+redux基本思想。
今天這篇文章給大家分享一個更加複雜,但是非常有趣的一個專案-
News Early單頁應用。
最近我發現,React Redux生態圈專案活躍。但是作品質量“良莠不齊”,很多非常熱門的專案不僅沒有起到“佈道”作用,而且在一定程度上“誤導”了讀者。在這篇文章裡面我會有詳細說明。當然,我自己也是資歷淺顯,水平有限。希望大神能夠給與斧正。
我把這個專案所有程式碼託管在了我個人Github之中,感興趣的讀者可以跟我探討。
同時通過這個專案例項和這篇文章,一步一步說明了這個專案開發細節,並且包括了優化手段等內容。希望使大家對於React技術棧,包括:React UI框架 + 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圖載入)
- 頁面頂部導航條
包括:側欄選單開啟按鈕和右側的重新整理頁面按鈕。 - 頁面內容頭部輪播圖
支援自動播放和手勢滑動操控。 - 頁面主體部分
主體部分是所對應的新聞頻道的headlines頭條新聞,一般有10-20個items左右。每一個item包含一張新聞圖片,新聞導讀(Abstract)以及新聞釋出時間(publish time)。 - 左側摺疊選單欄
功能用於新聞頻道的篩選。
以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!