网站目录权限 user百度云搜索资源入口
flux react
在本系列的第一部分中 ,我们开始深入研究React的世界,以了解如何与Node.js一起使用它来构建React Universal Blog App。
在第二部分和最后一部分中,我们将通过学习如何添加和编辑内容将博客带入一个新的水平。 我们还将深入探讨如何使用React组织概念和Flux模式轻松扩展React Universal Blog App。
为我分解
当我们在博客中添加更多页面和内容时, routes.js
文件将Swift变得很大。 由于这是React的指导原则之一,就是将事情分解成较小的,可管理的部分,因此让我们将路由分成不同的文件。
打开您的routes.js
文件并对其进行编辑,使其具有以下代码:
// routes.js
import React from 'react'
import { Route, IndexRoute } from 'react-router'
// Store
import AppStore from './stores/AppStore'
// Main component
import App from './components/App'
// Pages
import Blog from './components/Pages/Blog'
import Default from './components/Pages/Default'
import Work from './components/Pages/Work'
import NoMatch from './components/Pages/NoMatch'
export default (
<Route path="/" data={AppStore.data} component={App}>
<IndexRoute component={Blog}/>
<Route path="about" component={Default}/>
<Route path="contact" component={Default}/>
<Route path="work" component={Work}/>
<Route path="/work/:slug" component={Work}/>
<Route path="/blog/:slug" component={Blog}/>
<Route path="*" component={NoMatch}/>
</Route>
)
我们已经在博客中添加了一些不同的页面,并且通过将页面分成单独的组件,大大减少了routes.js
文件的大小。 此外,请注意,我们通过包含AppStore
添加了Store,这对于扩展React应用程序的后续步骤非常重要。
商店:真理的唯一来源
在Flux模式中,Store是非常重要的部分,因为它充当了数据管理的唯一事实来源 。 这是理解React开发的工作原理的关键概念,也是React最受吹捧的好处之一。 这门学科的优点在于,在我们应用程序的任何给定状态下,我们都可以访问AppStore
的数据,并确切了解其中的运行情况。 如果要构建一个数据驱动的React应用程序,需要牢记一些关键事项:
- 我们永远不会直接操纵DOM。
- 我们的UI对数据和存储在商店中的数据的答案
- 如果需要更改UI,可以转到商店,商店将创建应用程序的新数据状态。
- 新数据被馈送到更高级别的组件,然后根据接收到的新数据,通过构成新UI的
props
向下传递到更低级别的组件。
有了这四点,我们基本上就为单向数据流应用程序奠定了基础。 这也意味着,在应用程序中的任何状态下,我们都可以console.log(AppStore.data)
,如果我们正确构建了应用程序,我们将确切知道可以看到的内容。 您还将体验到它对于调试的强大功能。
现在,我们创建一个名为stores
的store文件夹。 在其中创建一个名为AppStore.js
的文件,其内容如下:
// AppStore.js
import { EventEmitter } from 'events'
import _ from 'lodash'
export default _.extend({}, EventEmitter.prototype, {
// Initial data
data: {
ready: false,
globals: {},
pages: [],
item_num: 5
},
// Emit change event
emitChange: function(){
this.emit('change')
},
// Add change listener
addChangeListener: function(callback){
this.on('change', callback)
},
// Remove change listener
removeChangeListener: function(callback) {
this.removeListener('change', callback)
}
})
您可以看到我们已经附加了一个事件发射器。 这使我们可以编辑商店中的数据,然后使用AppStore.emitChange()
重新渲染应用程序。 这是一个功能强大的工具,仅应在我们应用程序中的某些地方使用。 否则,可能很难理解AppStore
数据的更改位置,这将使我们进入下一步。
React组件:较高和较低级别
丹·阿布拉莫夫(Dan Abramov)写了一篇关于智能和愚蠢组件概念的精彩文章 。 想法是将数据更改动作仅保留在较高级别(智能)组件中,而较低级别(哑)组件则通过props获取它们提供的数据,并基于该数据呈现UI。 每当在较低级别的组件上执行动作时,该事件就会通过prop传递到较高级别的组件,以便处理为一个动作。 然后,它通过应用程序将数据重新分配(单向数据流)。
话虽如此,让我们开始构建一些组件。 为此,请创建一个名为components
的文件夹。 在其中创建一个名为App.js
的文件,内容如下:
// App.js
import React, { Component } from 'react'
// Dispatcher
import AppDispatcher from '../dispatcher/AppDispatcher'
// Store
import AppStore from '../stores/AppStore'
// Components
import Nav from './Partials/Nav'
import Footer from './Partials/Footer'
import Loading from './Partials/Loading'
export default class App extends Component {
// Add change listeners to stores
componentDidMount(){
AppStore.addChangeListener(this._onChange.bind(this))
}
// Remove change listeners from stores
componentWillUnmount(){
AppStore.removeChangeListener(this._onChange.bind(this))
}
_onChange(){
this.setState(AppStore)
}
getStore(){
AppDispatcher.dispatch({
action: 'get-app-store'
})
}
render(){
const data = AppStore.data
// Show loading for browser
if(!data.ready){
document.body.className = ''
this.getStore()
let style = {
marginTop: 120
}
return (
<div className="container text-center" style={ style }>
<Loading />
</div>
)
}
// Server first
const Routes = React.cloneElement(this.props.children, { data: data })
return (
<div>
<Nav data={ data }/>
{ Routes }
<Footer data={ data }/>
</div>
)
}
}
在App.js
组件中,我们将事件侦听器附加到AppStore
,当AppStore
发出onChange
事件时,该事件侦听器将重新呈现状态。 然后,重新渲染的数据将作为道具传递给子组件。 还要注意,我们添加了一个getStore
方法,该方法将分派get-app-store
操作以在客户端呈现我们的数据。 从Cosmic JS API中获取数据后,它将触发AppStore
更改,其中包括将AppStore.data.ready
设置为true
,删除加载符号并呈现我们的内容。
页面组件
要构建博客的首页,请创建一个Pages
文件夹。 在其中,我们将使用以下代码创建一个名为Blog.js
的文件:
// Blog.js
import React, { Component } from 'react'
import _ from 'lodash'
import config from '../../config'
// Components
import Header from '../Partials/Header'
import BlogList from '../Partials/BlogList'
import BlogSingle from '../Partials/BlogSingle'
// Dispatcher
import AppDispatcher from '../../dispatcher/AppDispatcher'
export default class Blog extends Component {
componentWillMount(){
this.getPageData()
}
componentDidMount(){
const data = this.props.data
document.title = config.site.title + ' | ' + data.page.title
}
getPageData(){
AppDispatcher.dispatch({
action: 'get-page-data',
page_slug: 'blog',
post_slug: this.props.params.slug
})
}
getMoreArticles(){
AppDispatcher.dispatch({
action: 'get-more-items'
})
}
render(){
const data = this.props.data
const globals = data.globals
const pages = data.pages
let main_content
if(!this.props.params.slug){
main_content = <BlogList getMoreArticles={ this.getMoreArticles } data={ data }/>
} else {
const articles = data.articles
// Get current page slug
const slug = this.props.params.slug
const articles_object = _.keyBy(articles, 'slug')
const article = articles_object[slug]
main_content = <BlogSingle article={ article } />
}
return (
<div>
<Header data={ data }/>
<div id="main-content" className="container">
<div className="row">
<div className="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
{ main_content }
</div>
</div>
</div>
</div>
)
}
}
该页面将用作我们的博客列表页面(主页)和单个博客页面的模板。 在这里,我们为组件添加了一个方法,该方法将使用React生命周期componentWillMount
方法在组件安装之前获取页面数据。 然后,将组件安装在componentDidMount()
,我们会将页面标题添加到文档的<title>
标记中。
除了此更高级别组件中的某些渲染逻辑外,我们还包括了getMoreArticles
方法。 这是号召性用语的一个很好的例子,它存储在较高级别的组件中,并且可以通过props供较低级别的组件使用。
现在让我们进入BlogList
组件,看看它是如何工作的。
创建一个名为Partials
的新文件夹。 然后,在其中创建一个名为BlogList.js
的文件,其内容如下:
// BlogList.js
import React, { Component } from 'react'
import _ from 'lodash'
import { Link } from 'react-router'
export default class BlogList extends Component {
scrollTop(){
$('html, body').animate({
scrollTop: $("#main-content").offset().top
}, 500)
}
render(){
let data = this.props.data
let item_num = data.item_num
let articles = data.articles
let load_more
let show_more_text = 'Show More Articles'
if(data.loading){
show_more_text = 'Loading...'
}
if(articles && item_num <= articles.length){
load_more = (
<div>
<button className="btn btn-default center-block" onClick={ this.props.getMoreArticles.bind(this) }>
{ show_more_text }
</button>
</div>
)
}
articles = _.take(articles, item_num)
let articles_html = articles.map(( article ) => {
let date_obj = new Date(article.created)
let created = (date_obj.getMonth()+1) + '/' + date_obj.getDate() + '/' + date_obj.getFullYear()
return (
<div key={ 'key-' + article.slug }>
<div className="post-preview">
<h2 className="post-title pointer">
<Link to={ '/blog/' + article.slug } onClick={ this.scrollTop }>{ article.title }</Link>
</h2>
<p className="post-meta">Posted by <a href="https://cosmicjs.com" target="_blank">Cosmic JS</a> on { created }</p>
</div>
<hr/>
</div>
)
})
return (
<div>
<div>{ articles_html }</div>
{ load_more }
</div>
)
}
}
在BlogList
组件中,我们已向Show More Articles
按钮添加了onClick
事件。 后者执行getMoreArticles
方法,该方法作为道具从高层页面组件传递下来。 单击该按钮后,事件会冒泡到Blog
组件,然后触发对AppDispatcher
。 AppDispatcher
充当高级组件和AppStore
之间的中间人。

免费学习PHP!
全面介绍PHP和MySQL,从而实现服务器端编程的飞跃。
原价$ 11.95 您的完全免费
为了简洁起见,我们不会在本教程中构建所有的Page
和Partial
组件,因此请下载GitHub存储库并从components
文件夹中添加它们。
AppDispatcher
AppDispatcher
是我们应用程序中的操作员,该操作员从更高级别的组件中接收信息并将操作分发到商店,然后商店重新提供我们的应用程序数据。
要继续本教程,请创建一个名为dispatcher
的文件夹。 在其中创建一个名为AppDispatcher.js
的文件,其中包含以下代码:
// AppDispatcher.js
import { Dispatcher } from 'flux'
import { getStore, getPageData, getMoreItems } from '../actions/actions'
const AppDispatcher = new Dispatcher()
// Register callback with AppDispatcher
AppDispatcher.register((payload) => {
let action = payload.action
switch(action) {
case 'get-app-store':
getStore()
break
case 'get-page-data':
getPageData(payload.page_slug, payload.post_slug)
break
case 'get-more-items':
getMoreItems()
break
default:
return true
}
return true
})
export default AppDispatcher
我们已将Flux
模块引入此文件中以构建调度程序。 现在让我们添加操作。
行动:商店前的最后一站
首先,让我们在新创建的名为actions
文件夹中创建一个actions.js
文件。 该文件将具有以下内容:
// actions.js
import config from '../config'
import Cosmic from 'cosmicjs'
import _ from 'lodash'
// AppStore
import AppStore from '../stores/AppStore'
export function getStore(callback){
let pages = {}
Cosmic.getObjects(config, function(err, response){
let objects = response.objects
/* Globals
======================== */
let globals = AppStore.data.globals
globals.text = response.object['text']
let metafields = globals.text.metafields
let menu_title = _.find(metafields, { key: 'menu-title' })
globals.text.menu_title = menu_title.value
let footer_text = _.find(metafields, { key: 'footer-text' })
globals.text.footer_text = footer_text.value
let site_title = _.find(metafields, { key: 'site-title' })
globals.text.site_title = site_title.value
// Social
globals.social = response.object['social']
metafields = globals.social.metafields
let twitter = _.find(metafields, { key: 'twitter' })
globals.social.twitter = twitter.value
let facebook = _.find(metafields, { key: 'facebook' })
globals.social.facebook = facebook.value
let github = _.find(metafields, { key: 'github' })
globals.social.github = github.value
// Nav
const nav_items = response.object['nav'].metafields
globals.nav_items = nav_items
AppStore.data.globals = globals
/* Pages
======================== */
let pages = objects.type.page
AppStore.data.pages = pages
/* Articles
======================== */
let articles = objects.type['post']
articles = _.sortBy(articles, 'order')
AppStore.data.articles = articles
/* Work Items
======================== */
let work_items = objects.type['work']
work_items = _.sortBy(work_items, 'order')
AppStore.data.work_items = work_items
// Emit change
AppStore.data.ready = true
AppStore.emitChange()
// Trigger callback (from server)
if(callback){
callback(false, AppStore)
}
})
}
export function getPageData(page_slug, post_slug){
if(!page_slug || page_slug === 'blog')
page_slug = 'home'
// Get page info
const data = AppStore.data
const pages = data.pages
const page = _.find(pages, { slug: page_slug })
const metafields = page.metafields
if(metafields){
const hero = _.find(metafields, { key: 'hero' })
page.hero = config.bucket.media_url + '/' + hero.value
const headline = _.find(metafields, { key: 'headline' })
page.headline = headline.value
const subheadline = _.find(metafields, { key: 'subheadline' })
page.subheadline = subheadline.value
}
if(post_slug){
if(page_slug === 'home'){
const articles = data.articles
const article = _.find(articles, { slug: post_slug })
page.title = article.title
}
if(page_slug === 'work'){
const work_items = data.work_items
const work_item = _.find(work_items, { slug: post_slug })
page.title = work_item.title
}
}
AppStore.data.page = page
AppStore.emitChange()
}
export function getMoreItems(){
AppStore.data.loading = true
AppStore.emitChange()
setTimeout(function(){
let item_num = AppStore.data.item_num
let more_item_num = item_num + 5
AppStore.data.item_num = more_item_num
AppStore.data.loading = false
AppStore.emitChange()
}, 300)
}
这里有一些此actions.js
文件公开的方法。 getStore()
连接到Cosmic JS API以提供我们博客的内容。 getPageData()
从所提供的获取页面数据slug
(或键)。 getMoreItems()
控制在BlogList
和WorkList
组件中可以看到多少个项目。
触发getMoreItems()
,首先将AppStore.data.loading
设置为true
。 然后,在300毫秒后(生效),它允许将五个以上的项目添加到我们的博客文章或工作项目列表中。 最后,它将AppStore.data.loading
设置为false
。
配置您的Cosmic JS CMS
要开始从Cosmic JS上的云托管内容API接收数据,让我们创建一个config.js
文件。 打开此文件并粘贴以下内容:
// config.js
export default {
site: {
title: 'React Universal Blog'
},
bucket: {
slug: process.env.COSMIC_BUCKET || 'react-universal-blog',
media_url: 'https://cosmicjs.com/uploads',
read_key: process.env.COSMIC_READ_KEY || '',
write_key: process.env.COSMIC_WRITE_KEY || ''
},
}
这意味着内容将来自Cosmic JS存储桶react-universal-blog
。 要为自己的博客或应用创建内容,请使用Cosmic JS注册免费帐户 。 当被要求“添加新存储桶”时,单击“安装入门存储桶”,您将可以按照以下步骤安装“ React Universal Blog”。 完成此操作后,您可以将唯一的存储段的子段添加到此配置文件中。
服务器端渲染
现在我们已经完成了大多数React组件和Flux架构的设置,接下来,我们通过编辑app-server.js
文件以呈现服务器端生产中的所有内容来结束app-server.js
。 该文件将具有以下代码:
// app-server.js
import React from 'react'
import { match, RoutingContext, Route, IndexRoute } from 'react-router'
import ReactDOMServer from 'react-dom/server'
import express from 'express'
import hogan from 'hogan-express'
import config from './config'
// Actions
import { getStore, getPageData } from './actions/actions'
// Routes
import routes from './routes'
// Express
const app = express()
app.engine('html', hogan)
app.set('views', __dirname + '/views')
app.use('/', express.static(__dirname + '/public/'))
app.set('port', (process.env.PORT || 3000))
app.get('*',(req, res) => {
getStore(function(err, AppStore){
if(err){
return res.status(500).end('error')
}
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
// Get page data for template
const slug_arr = req.url.split('/')
let page_slug = slug_arr[1]
let post_slug
if(page_slug === 'blog' || page_slug === 'work')
post_slug = slug_arr[2]
getPageData(page_slug, post_slug)
const page = AppStore.data.page
res.locals.page = page
res.locals.site = config.site
// Get React markup
const reactMarkup = ReactDOMServer.renderToStaticMarkup(<RoutingContext {...renderProps} />)
res.locals.reactMarkup = reactMarkup
if (error) {
res.status(500).send(error.message)
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search)
} else if (renderProps) {
// Success!
res.status(200).render('index.html')
} else {
res.status(404).render('index.html')
}
})
})
})
app.listen(app.get('port'))
console.info('==> Server is listening in ' + process.env.NODE_ENV + ' mode')
console.info('==> Go to http://localhost:%s', app.get('port'))
该文件使用getStore
操作方法从Cosmic JS API服务器端获取我们的内容,然后通过React Router确定将要安装的组件。 然后,将使用renderToStaticMarkup
将所有内容呈现为静态标记。 然后,此输出存储在模板变量中,供我们的views/index.html
文件使用。
再次,让我们更新package.json
文件的scripts
部分,使其看起来如下图所示:
"scripts": {
"start": "npm run production",
"production": "rm -rf public/index.html && NODE_ENV=production webpack -p && NODE_ENV=production babel-node app-server.js --presets es2015",
"webpack-dev-server": "NODE_ENV=development PORT=8080 webpack-dev-server --content-base public/ --hot --inline --devtool inline-source-map --history-api-fallback",
"development": "cp views/index.html public/index.html && NODE_ENV=development webpack && npm run webpack-dev-server"
},
现在,我们可以通过热重载在开发模式下运行,并且可以通过服务器呈现的标记在生产模式下运行。 运行以下命令以在生产模式下运行完整的React Universal Blog Application:
npm start
现在可以在http:// localhost:3000上查看我们的博客。 可以在服务器端,浏览器端进行查看,而我们的内容可以通过我们的云托管内容平台Cosmic JS进行管理。
结论
React是一种管理应用程序中的UI和数据的非常复杂的方法。 这也是呈现服务器端内容,安抚JavaScript破坏的Web爬网程序以及呈现UI浏览器端以使我们快速浏览的绝佳选择。 通过使我们的应用程序通用,我们可以获得两个世界的最佳结果。
我真的希望您喜欢这篇文章。 再次, 完整的代码可以从GitHub下载 。
翻译自: https://www.sitepoint.com/building-a-react-universal-blog-app-implementing-flux/
flux react