以前写过一篇文章,分享了我利用闲暇时间,使用React+Redux技术栈重构的百度某产品我的中心页面。您能够参考这里,或者参考Github代码仓库地址。
这个工程实例中,我采用了厂内的工程构建工具-FIS3,并贯穿了react+redux基本思想。javascript
今天这篇文章给你们分享一个更加复杂,可是很是有趣的一个项目-
News Early单页应用。html
最近我发现,React Redux生态圈项目活跃。可是做品质量“参差不齐”,不少很是热门的项目不只没有起到“布道”做用,并且在必定程度上“误导”了读者。在这篇文章里面我会有详细说明。固然,我本身也是资历浅显,水平有限。但愿大神可以给与斧正。前端
我把这个项目全部代码托管在了我我的Github之中,感兴趣的读者能够跟我探讨。java
同时经过这个项目实例和这篇文章,一步一步说明了这个项目开发细节,而且包括了优化手段等内容。但愿使你们对于React技术栈,包括:React UI框架 + Redux数据流框架+React Router路由管理+Webpack构建工具等,有一个更加清晰深入的理解。node
在国外上学和工做期间,能畅通无阻的访问诸如:BBC,CNN,ESPN,Le Figaro等新闻媒体是一大便利,也是我我的闲暇时期一个喜爱之一。
甚至外出旅游时,在酒店收看这些媒体卫视(尤为CNN)居然也是放松休闲的一大方式。。。react
固然,国内环境对于这些境外媒体显然不是太友好。
基于此,我设计开发了News Early项目。webpack
这个项目是一个包括:BBC,CNN,The NewYork Times等70多个国际知名媒体的即时头条新闻聚合APP。git
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).es6
整个项目我使用了包括但不限于如下技术栈和构建工具:
1)React UI框架from Facebook;
2)JSX模版;
3)Redux数据流设计;
4)Webpack构建工具;
5)Less预处理器;
......github
整个Web APP的部分使用体验,我用如下GIF图示来呈现:
(请耐心等待GIF图加载)
由于我不是搞视觉设计的,也不是作页面交互设计的(好吧,我只是一枚码农)。因此为了节省时间,总体APP的样式上,包括界面颜色等,我参考了卖座网的实现。
下面,我为你们介绍一下整个项目的设计构成和开发细节。
熟悉Redux数据流框架的同窗,应该对于store,dispatch,action,reducer,以及中间件等概念比较熟悉。这里再也不进行讲解。
这套架构中,最重要的就是数据流的设计。
首先,咱们先总体看一下在“切换频道”这个交互发生时,整个项目的数据流向和数据结构的演示:
如图所示:
整个项目业务代码部分,我拆分红9个UI组件,1个全局Store,一个actions定义文件。
10个组件包括:
我认为,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固然是必不可少的,我这里选取最重要的“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)
})
}
}
}复制代码
咱们知道,这些异步请求的访问速度是很慢的。所以,我采用了几种方法来进行优化。
原谅我使用了这么粉嫩少女的加载图。。。
第二个方法实际上是一个trick,个人全局图片在初始状态时opacity设置为0,在onload事件触发时设置一个fadeIn的效果:
<img ref="image" src={imgSrc} onLoad=
{this.handleImageLoaded.bind(this)}/>
handleImageLoaded() {
this.refs['image'].style.opacity = 1;
}复制代码
这样的一个小技巧最初来自Facebook对用户体验的研究。若是您对此有兴趣,能够在个人另一篇文章中找到相关内容。
具体实现方式就是在发送请求时判断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一系列强大开发功能和构建功能。包括但不限于:
...等等,可是我可不是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!