美文网首页reactWeb前端之路前端开发那些事
React+Redux打造“NEWS EARLY”单页应用 一

React+Redux打造“NEWS EARLY”单页应用 一

作者: LucasHC | 来源:发表于2017-03-27 18:31 被阅读2373次

    之前写过一篇文章,分享了我利用闲暇时间,使用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图加载)

    APP 使用截图
    • 页面顶部导航条
      包括:侧栏菜单开启按钮和右侧的刷新页面按钮。
    • 页面内容头部轮播图
      支持自动播放和手势滑动操控。
    • 页面主体部分
      主体部分是所对应的新闻频道的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!

    相关文章

      网友评论

      • bbb21986f76c:你好,我在你的github上面没找到这个项目的源码,你可以分享一下源码吗?
      • ccc7b37c9552:求源码学习
      • cef091f9ff1c:在你的GitHub上没找到这个项目的源码呢?
      • 6710561139ab:可以试一下redux-saga写起来更爽。处理action跟方便。
      • kaze才没有:想知道中间数据流动示意图是怎么实现的?
        有工具可以监听 store 变化并渲染出图表么?
        LucasHC:有的 react dev tool, redux dev tool
      • eb31cea41d1d:感谢分享,急需这种react的技术栈整合
        LucasHC:@mamba疯子 我发给你稍后
        031fb8c2b26d:有源码吗没找到? 在你的GitHub上没找到这个项目的源码:blush:
        LucasHC:@Lonly_Heart :heavy_check_mark:

      本文标题:React+Redux打造“NEWS EARLY”单页应用 一

      本文链接:https://www.haomeiwen.com/subject/pjtpnttx.html