翻译|React & Redux Tutorial — Build a Hacker News Clone

原文参见css

本文是 gitconnected Hacktobrefest项目的逐步解决方法.react

在本教程中,我将会构建一个产品级别的的 Hacker News 克隆. 咱们会逐步实现应用的初始化,添加用于状态管理的 Redux,用 React 构建 UI而且部署到 GitHub 主页上.样式将会采用styled-components](https://www.styled-components.com/),API方面使用[axios](https://github.com/axios/axios)库调用 [Hacker News API`.ios

源代码在这里查看.git

下载 Chrome应用es6

若是你愿意看视频,能够看看 youtube 上的教程. www.youtube.com/watch?v=oGB…github

初始化项目

使用create-react-app来初始化项目.用这个包初始化项目,就不用担忧配置问题了.首先要肯定已经安装了create-react-app.web

npm -i -g create-react-app
复制代码

运行下面的命令来启动项目. create-react-app 安装了全部构建 React 应用的必备依赖包,还有默认的脚本用于管理开发和实际应用的打包.chrome

create-react-app hn-clone

# wait for everything to finish...

cd hn-clone
复制代码

如今能够安装应用所需的核心软件包了.目前我使用的是yarn来管理依赖包,若是你使用的是npm,只须要用npm install替换掉yarn add就能够了.npm

yarn add redux styled-components react-redux redux-logger redux-thunk axios
复制代码

create-react-app使用NODE_PATH 环境变量(environment variable)来建立绝对路径. 咱们能够在.env文件中声明环境变量. create-react-app会识别它,经过doten库 来应用绝对路径.json

#使用touch 命令建立.env文件

touch .env
# 在.env文件里添加
#NODE_PATH=src
复制代码

若是你对这个模式不太熟悉, 当咱们开始构建应用的时候,对你来讲更为有意义.设定环境变量可让咱们直接导入文件而不用考虑文件的路径. 相似这样 ../../components/List 变为components/List- 使用上方便多了.

文件组织结构

src文件夹里面, 从应用要适应更为大规模和重用性更强上考虑,作一些更新.

  • components: 这个文件夹包含全部的 React 组件(container和 presentational 组件都包含).
  • services: Services能够链接到API(例如,使用axios调用 HN API)或者为应用提供扩展的功能(例如,添加markdown)支持.
  • store: store 包含了全部的Redux和state 管理的逻辑
  • styles: 在styles文件夹内,咱们声明变量,模板和能够在组件间共享的样式模式
  • utils: 整个应用中能够重用的助手函数

这里的文件夹结构有两个地方值得注意:

  1. 应用中只有一个路由,位于根./下.若是咱们有多个路由,我可能会使用react-router包,同时建立pages文件夹用于保存页面级别的组件.
  2. 我没有使用单独的container文件夹用于链接应用组件到Redux.我发现增长container文件夹反而添加了没必要要的复杂性,让一些新手感到很困惑,由于开发者老是要从没有关联的位置中导入文件(container想要链接组件,反之亦然). 在个人使用经验汇总,从当个来源导入文件工做的更好一点.

由于咱们在使用styled-components,因此能够删除掉index.cssapp.css文件. 如今咱们要在src/styles文件件中添加一些基础模板样式,建立文件global.jspalette.js文件

Palette包含了应用UI中使用的成组的颜色配置. 在src/styles/palette.js中添加

global.js用于生成应用中共享的基础样式. styled-componentsinjectGlobal方法应该要当心使用,可是用于应用级别的样式时时很是有用的.

注意: 在styled-components v4中injectGlobal已经被createGlobalStyle替代了.

components文件夹中建立App文件夹,把全部的 CRA默认生成的文件都移动到这个文件中,把App.js文件重命名为index.js文件. 这样就能够导入components/App

如今代开src/index.js文件(项目的根文件),使用更新的文件结构更新文件.

注意,由于以前咱们定义了NODE_PATH,如今使用components/App导入App文件,styles/globals来导入setGlobalStyles文件. 执行setGlobalStyles()函数能够在应用中导入全局的样式.

如今咱们已经准备好了启动应用开发环境的核心配置. 运行下面命令启动应用,会在http://localhost:3000看到应用. 如今看上去还不是太好,可是应用已经跑起来了 :)

yarn start
# npm 安装用 npm start

复制代码

在 React 应用添加Redux

src/store文件中,建立index.js,reducer.jsmiddleware.js文件. 让咱们来初始化一个app专项(feature)来管理应用的state.

以个人经验,在生产级别的应用中,若是按照特性而不是按照功能进行分组,Redux会更具备管理性,相似于鸭子方法(Ducks approach). "按照功能分组(grouping by functionality)" 方法中全部的actions,reducers,等等都位于独立的文件夹中, 当应用规模增长时,在不一样文件中切换难度就增大了. 若是按照特性分组,你须要的文件老是在一个位置.

index.js文件中,建立configureStore函数,用于初始化应有的 Redux.

使用createStore构建初始化store. 从根reducer文件导入reducer,同时从middleware配置文件中导入 middleware(中间件). initialState 应该在程序运行时提供,并传递给咱们的函数. 在生产中,要可以管理复杂的功能例如 SSR(服务端渲染),或者在初始化时从服务器获取传递的数据. 在这里初始state,可让咱们更优雅的和抽象出store的建立过程.

reducer.js文件中,使用combineReducers函数建立根reducer.此函数把全部的reducer函数组合起来生成单个的state树.

接下来在middleware.js中建立中间件. 中间件是每一次dispatch action 时都必需要执行的函数. 中间在扩展Redux应用时很是有用. 在文件中添加以下代码

也要构建第一个Reducer.在 src/store/app文件加中建立 reducer.jsaction.js文件. 须要添加日间/夜间的切换模式功能,因此让咱们建立一个action来管理这个特性.在src/store/app/action.js 添加下面代码

咱们建立了一个actionTypes对象放置actio-type常量. 相似的常量在reducer中用于匹配改变state的类型. 也要建立actions对象,包含了能够从应用中dispatch 用于改变state的 action函数.每个action都包括了一个type和一个payload(译注: type告诉store要干什么,payload 是执行action时携带的条件).

最后,建立咱们的reducer

当咱们dispatch一个SET_THEME action时, 将会使用payload的内容更新 state中theme的属性值. payload是一个对象,形式是{theme:'value'}.使用es6的展开操做...,state中对应payload键的值会被替换掉.

若是须要详细理解 Redux的基础 ,看看Dan Abramov的视频

如今返回src/index.js文件,作一些更新,须要把咱们的应用链接到 Redux. 为Provider添加一个导入,更新渲染方法

如今应该已经作完了 Redux的整个工做.返回到localhost:3000,在Chrome的console中能够看到下面的内容

使用 React和Styld Components 构建 UI

如今 Redux 已经初始化完毕, 开始完成 UI 的工做. 首先声明一些会在应用中使用的样式常量. 在本应用中,咱们要建立mediaQueries(媒体查询) 文件包含构建响应式应用的常量. 建立src/styles/mediaQueries.js文件,添加下面的代码

返回到src/components/App文件夹, 在index.js文件中,更新文件内容

其中使用了styled-componentsThemeProvider组件.这个组件尅让咱们把"theme"做为prop传递给建立的styled components. 这里初始化theme为 colorDark对象.

App中包含的组件,如今尚未建立,因此如今来建立.首先构建styld-components 组件. 在App文件夹里建立styles.js文件, 添加代码

建立的用于页面的div称为Wrapper. 用于页面标题的h1建立为Title组件. styled-components语法使用styled对象定义 HTML 元素. 能够用字符串定义组件的 CSS 属性.

注意代码20行, 咱们使用了theme prop. 包含props参数的函数由styled-components 注入到样式字符串中,这么,咱们就可提起属性或者添加用于动态构建样式的逻辑,从组件中抽象出构建样式的逻辑.

接下来, 建立包含 Hacker Nees故事的 List 组件. 建立src/components/List文件夹并添加index.js,styles.js文件. 在index.js文件中,添加代码

styles.js文件中建立ListWrapper.使用从ThemeProvider组件获得的theme props 的background-color属性.

最后建立ListItem组件用于显示单个的故事. 建立src/components/ListItem文件夹和index.js,styles.js文件.

咱们想让 UI模仿 Hacker News. 目前会在ListItem中使用fake 数据里模拟. 在index.js文件中添加代码

每一个故事都有标题,做者,评分,发帖时间,URL地址,评论数. 初始化这几个值,以便于查看 UI 的样子. 基于安全缘由, 添加rel="nofollow noreferrer noopener".

styles.js文件中添加下面代码

这些应该就是咱们须要的基础 UI 组件了. 返回到浏览器,应该看到使用fake数据的单个条目

使用 Redux 和 Axios 构建 API 调用

是时候在应用添加实际数据了.咱们经过axios库来调用 Hacker News的 API.调用 API 的过程会在应用中引入 "side effect(反作用)",意思是调用 API 会从外部资源影响本地环境的state.

API 调用之因此被称为 side effect,缘由是在应用的state中引入了外部的数据. 其余的side effect的例子包括和浏览器的localStorage的交互操做, 追踪用户分析,链接到web socket,等等. 在 Redux 应用中可使用不少库来管理 side effect. 从简单的redux-thunk 到更为复杂的redux-saga. 然而他们的目的是相同的,就是让 Redux与外界交互. redux-thunk是最简单的库, 能够在action 对象中再次 dispatch JavaScript 函数. 这个功能就是咱们在使用axios时须要的功能,在 API调用管理返回的promise对象.

src/services文件夹中,建立Api.jshackerNewsApi.js文件. axios库有着难以置信的强大功能和扩展性. Api.js包含的配置使得执行axios请求更容易. 这里没有拷贝完整代码,你能够在源代码中看到信息内容,其中包含了更为精细的配置.

src/services/hackerNewsApi.js文件中, 咱们要定义请求 Hacker News API 的函数. 在Hacker New API 文档 能够找到,若是要获取 IDs 的列表, 要使用/v0/topstories 入口. 获取每一个 id的独立故事要使用/v0/items/<id> 入口.

v0/topstories 入口返回列表中 IDs的 400-500条故事. 由于咱们要获取单个故事的数据,若是马上获取500个故事的数据会严重影响性能. 为了解决这个问题,咱们一次只获取20个故事的数据. 使用.slice()函数基于页面的故事 ID进行分割. 由于咱们使用/v0/item/<id> 调用每一个故事的数据, 所以使用Promise.all把全部的请求返回的promise对象压缩的一个数组中,而后用一个then(),resolve返回获取数据,而且保存 IDs 的顺序标记.

为了在应用管理咱们的故事state,咱们来建立一个story reducer. 建立src/store/story文件夹, 添加reducer.jsaction.js文件. 在action.js文件中添加代码

为 IDs请求和stor用的API 调用都建立了 request,success,failure的 actionTypes.

咱们的actions 对象中包含了 用于请求管理的thunk 函数. 经过dispatch 函数而不是dispatch action 对象. 咱们就能够在请求周期的不一样点 dispatch 不一样的acitons了.

函数getTopStoryIds会执行 API 调用,获取整个故事的列表. 在getTopStoryIds函数中success(成功)的回调函数执行时,咱们会dispatch fetchStories action,用于获取第一页故事的结果.

当 API 调用成功返回时,就能够dispatch success Action,这样就可使用新获取的数据来更新 Redux的 store了.

thunk软件包的基础实现只是用了几行代码. 要充分理解它,须要对 Redux的中间件有了解,可是从代码中,咱们能够看到,若是咱们使用一个函数来代替一个对象,就能够执行一个函数,而且把dispatch做为函数的参数传递.

如今咱们须要建立reducer用于 Redux store中的数据存储. 在src/store/story/reudcer.js中添加代码

对于 FETCH_STORY_IDS_SUCCESS action type,咱们展开当前 state和 payload. 在 payload 中惟一的键/值是storyIds,展开操做将会用新的值来更新 state.

对于FETCH_STORIES_SUCCESS action type. 在以前的故事列表中按顺序添加故事,以便于获取更多的页面. 此外,增长page 数, 设置isFetching state 为false.

如今,State已经由 Redux管理了, 咱们就能够在组件中显示数据了.

把React APP 链接到 Redux Store

经过使用react-redux绑定,咱们能够把组件链接到 Redux的store, 以props的形式接收Redux的 State.以后,只要 store 有更新,props就会引发组件的从新渲染,由此就更新了 UI.

在须要dispatch action 的组件中,以 props 的形式传递函数. 以后在组件内部调用这些函数,就能够触发 Redux store 中的state变化.

来看看如何在应用中管理这个变化. 返回到src/components/App文件夹,建立一个 App.js文件, 从src/components/App/index.js拷贝内容进来. 在index.js文件里面,咱们将会把App组件链接到 Redux. 在index.js文件中添加代码

mapStateToProps函数接受 Redux store做为参数,返回一些属性到链接的组件中.对于App,咱们须要 stories 数组, 当前页 page,storyIds数组还有isFetching指示器.

mapDispatchToProps函数接受dispatch函数做为参数,把返回的函数对象做为props传递给咱们的组件. 建立的函数fetchStoriesFirstPage,执行时会dispatch action 来获取story IDs(而后获取第一页故事的内容).

咱们在App.js中使用这两个props,首先添加componentDidMount,当组件在 DOM 中渲染完就能够马上获取数据. 为List组件传递stories props.

src/components/List/index.js中,遍历stories 数组, 建立 ListItem组件的数组. 设置列表的key为story ID,而且展开story对象: ...story.展开操做会把对象的属性值做为单个的props传递给组件. key prop 是 React中组件做为数组加载时的一个策略,可让列表形式的渲染更新速度更快.

若是如今观察屏幕,应该看到到的是硬编码的20行列表数据

咱们须要使用从stories 获取的数据对ListItem进行更新.同时在 Hacker News中, 也会显示上次故事更新的时间和来源的地址. 须要安装 timeago.jsURl 软件包帮助计算没有经过 API 直接获取的数据, 使用下面命令执行安装

yarn add timeago.js url
复制代码

须要编写助手函数来构建这些值. 从源码的src/utils文件夹中拷贝文件

如今更新 src/components/ListItem/index.js文件

经过这一步, 如今就能够在应用显示前20个故事了- cool!

使用无限滚动来对请求分页

如今,咱们想实现的是当用于页面滚动到底部, 获取新的一页.回忆一下,每次成功获取故事以后,咱们都增长了store中page的数字. 因此在第一页到达以后,Redux store 如今应该是page:1.咱们须要在滚动到底部时dispatch fetchStories action.

为了实现无限滚动,咱们会使用react-infinite-scroll-component组件. 咱们也想实现一个方法来决定管是否要加载更多的页面,这一点咱们使用reselect 在selector中实现.

yarn add react-infinite-scroll-component reselect
复制代码

首先构建selector来计算是否有更多的故事存在. 建立 src/store/story/selecor.js文件. 为了判断是否有更多故事存在, 咱们 Redux store中的storyIds 数组的长度是否和stories的长度相同, 若是stories的长度短一点,意思就是有更多的页面存在

src/components/App/index.js container中,导入hasMoreStoriesSelectormapStateToProps中添加 键hasMoreStories.同时在mapDispatchProps中添加fetchStories action,便于滚动时 dispatch action.

咱们想在等待 API请求时使用动画显示. 建立src/components/Loader文件夹,index.jsstyles.js文件. 须要的动画是闪动的三个圆点.

styles.js文件中添加下面代码

@keyframe 是定义动画的 CSS 技术. 上面代码显示了在 Styled Components中的代码抽象. 有三个圆点,透明度从0.2开始增长到1, 而后返回到0.2, 给第二个和第三个点添加延迟,表现出弹跳式的偏移.

咱们的Loader组件就是有三个独立span元素的动画styled components动画组件.

如今,准备为列表添加功能,在App组件中导入无限加载模块和Loader组件.也要建立fetchStories回调函数,将会调用fetchStories prop dispatch 下一页的action. 只有在isFetching为 false 时dispatch fetchStories action. 若是为 true.咱们就屡次获取统一页面. 你的src/components/App/App.js文件应该以下

当咱们滚动到页面底部, 只要hasMoreStories为真,InfiniteScroll组件将会调用this.fetchStroies. 当fetchStories API 请求返回时,新的故事会添加到stories数组的尾部,渲染到页面中.

最后的挑战

在教程刚开始, 咱们初始化了一个theme property.如今,留给你实现一个toggle功能. 在一些组件中添加点击事件,dispatch setTheme action.切换 lightdark的状态. 在ThemeProvider组件中须要一个三元条件判断若是 state.app.theme==='dark'就传递colorDark,不然就传递colorsLight.

若是你卡住了,能够看看源码的实现.加入Slack 寻求帮助. 试试咱们的办法.

部署到GitHub 主页

对于应用的最后一步都是投入生产. 由于咱们的功能是在客户端,能够免费部署在 GitHub 主页的静态网站.

提交你的代码并推送到Github. 我命名仓库为hn-clone.若是你在建立仓库和上传代码是遇到问题能够参照一下这个指导

如今使用以下的步骤发送过的 GitHub 主页:

  1. 在package.json文件中添加 "homepage":"http://<username>.github.io/<repo-name>" 使用你的实际值替换<username><repo-name>. 个人值是 treyhuffinehn-clone.

  1. 安装gh-pages做为开发依赖项
yarn add -D gh-pages
复制代码
  1. package.json文件中添加两个脚本
"predeploy": "npm run build","deploy": "gh-pages -d build"
复制代码

  1. 最后运行yarn deploy 并访问在homepage中定义的URL.

如今你的 Hacker News 投入生产了.

结论

本文覆盖了构建 Hacker News clone所必须的全部的功能. 源码还有一些额外的特性,持续更新中, 查看一下是否有灵感出现能够继续添加功能,学习更多的 React 知识.

不要忘了下载Chrome 扩展, 并访问 gitconnectec.com网站,加入开发者社区.

原文发表在 gitconnected.com-开发者社区

相关文章
相关标签/搜索