原文参见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
: 整个应用中能够重用的助手函数这里的文件夹结构有两个地方值得注意:
./
下.若是咱们有多个路由,我可能会使用react-router
包,同时建立pages
文件夹用于保存页面级别的组件.container
文件夹用于链接应用组件到Redux.我发现增长container
文件夹反而添加了没必要要的复杂性,让一些新手感到很困惑,由于开发者老是要从没有关联的位置中导入文件(container
想要链接组件,反之亦然). 在个人使用经验汇总,从当个来源导入文件工做的更好一点.由于咱们在使用styled-components
,因此能够删除掉index.css
和app.css
文件. 如今咱们要在src/styles
文件件中添加一些基础模板样式,建立文件global.js
和palette.js
文件
Palette包含了应用UI中使用的成组的颜色配置. 在src/styles/palette.js
中添加
global.js
用于生成应用中共享的基础样式. styled-components
的injectGlobal
方法应该要当心使用,可是用于应用级别的样式时时很是有用的.
注意: 在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
复制代码
在src/store
文件中,建立index.js
,reducer.js
和middleware.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.js
和action.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中能够看到下面的内容
如今 Redux 已经初始化完毕, 开始完成 UI 的工做. 首先声明一些会在应用中使用的样式常量. 在本应用中,咱们要建立mediaQueries(媒体查询)
文件包含构建响应式应用的常量. 建立src/styles/mediaQueries.js
文件,添加下面的代码
返回到src/components/App
文件夹, 在index.js
文件中,更新文件内容
其中使用了styled-components
的ThemeProvider
组件.这个组件尅让咱们把"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数据的单个条目
是时候在应用添加实际数据了.咱们经过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.js
和hackerNewsApi.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.js
和action.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-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.js
和URl
软件包帮助计算没有经过 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中,导入hasMoreStoriesSelector
在mapStateToProps
中添加 键hasMoreStories
.同时在mapDispatchProps
中添加fetchStories
action,便于滚动时 dispatch action.
咱们想在等待 API请求时使用动画显示. 建立src/components/Loader
文件夹,index.js
和styles.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.切换 light
和dark
的状态. 在ThemeProvider
组件中须要一个三元条件判断若是 state.app.theme==='dark'
就传递colorDark
,不然就传递colorsLight
.
若是你卡住了,能够看看源码的实现.加入Slack 寻求帮助. 试试咱们的办法.
对于应用的最后一步都是投入生产. 由于咱们的功能是在客户端,能够免费部署在 GitHub 主页的静态网站.
提交你的代码并推送到Github. 我命名仓库为hn-clone
.若是你在建立仓库和上传代码是遇到问题能够参照一下这个指导
如今使用以下的步骤发送过的 GitHub 主页:
"homepage":"http://<username>.github.io/<repo-name>"
使用你的实际值替换<username>
和<repo-name>
. 个人值是 treyhuffine
和hn-clone
.gh-pages
做为开发依赖项yarn add -D gh-pages
复制代码
package
.json文件中添加两个脚本"predeploy": "npm run build","deploy": "gh-pages -d build"
复制代码
yarn deploy
并访问在homepage中定义的URL.如今你的 Hacker News 投入生产了.
本文覆盖了构建 Hacker News clone所必须的全部的功能. 源码还有一些额外的特性,持续更新中, 查看一下是否有灵感出现能够继续添加功能,学习更多的 React 知识.
不要忘了下载Chrome 扩展, 并访问 gitconnectec.com网站,加入开发者社区.
原文发表在 gitconnected.com-开发者社区