React是一个很棒的JavaScript库,用于构建用户界面。 自创建React应用程序发布以来, 搭建准系统React应用程序变得非常容易。
在本文中,我们将结合使用Firebase和Create React App来构建功能类似于Reddit的应用程序。 它将允许用户提交新的链接,然后可以对其进行投票。
这是我们将要构建的实时演示 。
为什么选择Firebase?
使用Firebase将使我们很容易向用户显示实时数据。 用户对链接进行投票后,反馈将立即生效。 Firebase的实时数据库将帮助我们开发此功能。 另外,它将帮助我们了解如何使用Firebase引导React应用程序。
为什么要反应?
React因使用组件架构创建用户界面而闻名。 每个组件都可以包含内部状态,也可以作为prop传递数据。 状态和道具是React中两个最重要的概念。 这两件事有助于我们在任何时间点确定应用程序的状态。 如果您不熟悉这些术语,请先访问React文档 。
注意:您也可以使用Redux或MobX之类的状态容器,但是为了简单起见,本教程中不会使用一个状态容器。
整个项目可在GitHub上找到 。
设置项目
让我们逐步完成设置项目结构和所有必要依赖项的步骤。
安装create-react-app
如果尚未安装,则需要安装create-react-app 。 为此,您可以在终端中键入以下内容:
npm install -g create-react-app
全局安装后,您可以使用它在任何文件夹中搭建一个React项目。
现在,让我们创建一个新应用,并将其称为reddit-clone 。
create-react-app reddit-clone
这将在reddit-clone文件夹中搭建一个新的create-react-app项目。 一旦完成引导,我们可以进入reddit-clone目录并启动开发服务器:
npm start
此时,您可以转到http:// localhost:3000 /并查看您的应用程序框架并开始运行。
构建应用程序
为了维护,我总是喜欢将容器和组件分开。 容器是包含我们应用程序的业务逻辑并管理Ajax请求的智能组件。 组件只是愚蠢的呈现组件。 它们可以具有自己的内部状态,可以用来控制该组件的逻辑(例如,显示受控输入组件的当前状态)。
删除不必要的徽标和CSS文件后,这就是您的应用现在的外观。 我们创建了一个components
文件夹和一个containers
文件夹。 让我们将App.js
移到containers/App
文件夹内,并在utils
文件夹内创建registerServiceWorker.js
。
您的src/containers/App/index.js
文件应如下所示:
// src/containers/App/index.js
import React, { Component } from 'react';
class App extends Component {
render() {
return (
<div className="App">
Hello World
</div>
);
}
}
export default App;
您的src/index.js
文件应如下所示:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './containers/App';
import registerServiceWorker from './utils/registerServiceWorker';
ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();
转到浏览器,如果一切正常,您将在屏幕上看到Hello World 。
您可以在GitHub上检查我的提交 。
添加反应路由器
React-router将帮助我们定义应用程序的路由。 它是非常可定制的,并且在React生态系统中非常流行。
我们将使用3.0.0版本的react-router 。
npm install --save react-router@3.0.0
现在,使用以下代码在src
文件夹中添加一个新文件routes.js
:
// routes.js
import React from 'react';
import { Router, Route } from 'react-router';
import App from './containers/App';
const Routes = (props) => (
<Router {...props}>
<Route path="/" component={ App }>
</Route>
</Router>
);
export default Routes;
Router
组件包装所有Route
组件。 基于Route
组件的path
属性,传递给该component
属性的component
将呈现在页面上。 在这里,我们正在设置根URL( /
)以使用Router
组件加载我们的App
组件。
<Router {...props}>
<Route path="/" component={ <div>Hello World!</div> }>
</Route>
</Router>
上面的代码也是有效的。 对于路径/
,将安装<div>Hello World!</div>
。
现在,我们需要从src/index.js
文件中调用routes.js
文件。 该文件应具有以下内容:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { browserHistory } from 'react-router';
import App from './containers/App';
import Routes from './routes';
import registerServiceWorker from './utils/registerServiceWorker';
ReactDOM.render(
<Routes history={browserHistory} />,
document.getElementById('root')
);
registerServiceWorker();
基本上,我们是从routes.js
文件中挂载Router
组件的。 我们将history
道具传递给它,以便路线知道如何处理历史跟踪 。
您可以在GitHub上检查我的提交 。
添加Firebase
如果您没有Firebase帐户,请立即访问他们的网站来创建一个帐户(免费!)。 创建新帐户后,登录到您的帐户并进入控制台页面,然后点击添加项目 。
输入您的项目名称(我将其称为mine reddit-clone ),选择您所在的国家/地区,然后单击“ 创建项目”按钮。
现在,在继续之前,我们需要更改数据库规则 ,因为默认情况下,Firebase希望对用户进行身份验证才能读取和写入数据。 如果选择项目,然后单击左侧的“ 数据库”选项卡,则可以查看数据库。 您需要单击顶部的“规则”选项卡,这会将我们重定向到一个包含以下数据的屏幕:
{
"rules": {
".read": "auth != null",
".write": "auth != null"
}
}
我们需要将其更改为以下内容:
{
"rules": {
".read": "auth === null",
".write": "auth === null"
}
}
这将使用户无需登录即可更新数据库。如果我们实现了在对数据库进行更新之前进行身份验证的流程,则需要Firebase提供的默认规则。 为了简化此应用程序,我们将不进行身份验证。
重要提示:如果您不进行此修改,Firebase将不允许您从应用程序更新数据库。
现在,通过运行以下代码,将firebase
npm模块添加到我们的应用中:
npm install --save firebase
接下来,将该模块导入您的App/index.js
文件中,如下所示:
// App/index.js
import * as firebase from "firebase";
登录Firebase后选择项目时,将获得一个选项Add Firebase to your web app 。
如果单击该选项,将出现一个模态,该模态将向我们显示将在componentWillMount
方法中使用的config
变量。
让我们创建Firebase配置文件。 我们将这个文件称为firebase-config.js
,它将包含将我们的应用程序与Firebase连接所需的所有配置:
// App/firebase-config.js
export default {
apiKey: "AIzaSyBRExKF0cHylh_wFLcd8Vxugj0UQRpq8oc",
authDomain: "reddit-clone-53da5.firebaseapp.com",
databaseURL: "https://reddit-clone-53da5.firebaseio.com",
projectId: "reddit-clone-53da5",
storageBucket: "reddit-clone-53da5.appspot.com",
messagingSenderId: "490290211297"
};
我们将Firebase配置导入App/index.js
:
// App/index.js
import config from './firebase-config';
我们将在constructor
初始化Firebase数据库连接。
// App/index.js
constructor() {
super();
// Initialize Firebase
firebase.initializeApp(config);
}
在componentWillMount()
生命周期挂钩中,我们使用刚安装的软件包firebase
并调用其initializeApp
方法并将config
变量传递给它。 该对象包含有关我们应用程序的所有数据。 initializeApp
方法会将我们的应用程序连接到Firebase数据库,以便我们可以读取和写入数据。
让我们向Firebase添加一些数据以检查我们的配置是否正确。 转到数据库选项卡,然后将以下结构添加到数据库:
单击添加会将数据保存到我们的数据库。
现在,让我们向componentWillMount
方法添加一些代码,以使数据出现在屏幕上:
// App/index.js
componentWillMount() {
…
let postsRef = firebase.database().ref('posts');
let _this = this;
postsRef.on('value', function(snapshot) {
console.log(snapshot.val());
_this.setState({
posts: snapshot.val(),
loading: false
});
});
}
firebase.database()
为我们提供了对数据库服务的引用。 使用ref()
,我们可以从数据库中获取特定的引用。 例如,如果调用ref('posts')
,我们将从数据库中获取posts
引用,并将该引用存储在postsRef
。
每当数据库中有任何更改时postsRef.on('value', …)
都会为我们提供更新后的值。 当我们需要基于任何数据库事件实时更新用户界面时,这非常有用。
使用postsRef.once('value', …)
只会给我们一次数据。 这对于只需要加载一次且不会经常更改或需要主动监听的数据很有用。
在on()
回调中获取更新后的值之后,我们将这些值存储在posts
状态中。
现在,我们将在控制台上看到数据。
另外,我们会将这些数据传递给我们的孩子。 因此,我们需要修改App/index.js
文件的render
函数:
// App/index.js
render() {
return (
<div className="App">
{this.props.children && React.cloneElement(this.props.children, {
firebaseRef: firebase.database().ref('posts'),
posts: this.state.posts,
loading: this.state.loading
})}
</div>
);
}
这里的主要目的是使posts数据在我们所有的子组件中都可用,这将通过react-router
传递。
我们正在检查this.props.children
存在,如果存在,我们将克隆该元素并将所有props传递给所有孩子。 这是将道具传递给有活力的孩子的一种非常有效的方法。
调用cloneElement将浅合并在现有的道具this.props.children
,我们在这里通过的道具( firebaseRef
, posts
和loading
)。
使用此技术, firebaseRef
, posts
和loading
道具将可用于所有路线。
您可以在GitHub上检查我的提交 。
将应用程序与Firebase连接
Firebase只能将数据存储为对象。 它没有对数组的任何本机支持 。 我们将以以下格式存储数据:
手动在上面的屏幕截图中添加数据,以便您可以测试视图。
添加所有帖子的视图
现在,我们将添加视图以显示所有帖子。 创建具有以下内容的文件src/containers/Posts/index.js
:
// src/containers/Posts/index.js
import React, { Component } from 'react';
class Posts extends Component {
render() {
if (this.props.loading) {
return (
<div>
Loading…
</div>
);
}
return (
<div className="Posts">
{ this.props.posts.map((post) => {
return (
<div>
{ post.title }
</div>
);
})}
</div>
);
}
}
export default Posts;
在这里,我们只是映射数据并将其呈现到用户界面。
接下来,我们需要将此添加到我们的routes.js
文件中:
// routes.js
…
<Router {...props}>
<Route path="/" component={ App }>
<Route path="/posts" component={ Posts } />
</Route>
</Router>
…
这是因为我们希望帖子仅显示在/posts
路线上。 因此,我们仅将Posts
组件传递给component
prop,将/posts
传递给react-router的Route
组件的path
prop。
如果转到URL localhost:3000 / posts ,将看到Firebase数据库中的帖子。
您可以在GitHub上检查我的提交 。
添加视图以撰写新帖子
现在,让我们从可以添加新帖子的位置创建一个视图。 创建具有以下内容的文件src/containers/AddPost/index.js
:
// src/containers/AddPost/index.js
import React, { Component } from 'react';
class AddPost extends Component {
constructor() {
super();
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
state = {
title: ''
};
handleChange = (e) => {
this.setState({
title: e.target.value
});
}
handleSubmit = (e) => {
e.preventDefault();
this.props.firebaseRef.push({
title: this.state.title
});
this.setState({
title: ''
});
}
render() {
return (
<div className="AddPost">
<input
type="text"
placeholder="Write the title of your post"
onChange={ this.handleChange }
value={ this.state.title }
/>
<button
type="submit"
onClick={ this.handleSubmit }
>
Submit
</button>
</div>
);
}
}
export default AddPost;
在这里, handleChange
方法使用输入框中存在的值更新状态。 现在,当我们单击按钮时,将触发handleSubmit
方法。 handleSubmit
方法负责使API请求写入我们的数据库。 我们使用传递给所有孩子的firebaseRef
道具来做到这一点。
this.props.firebaseRef.push({
title: this.state.title
});
上面的代码块将标题的当前值设置到我们的数据库中。
新帖子存储在数据库中之后,我们再次将输入框设为空,准备添加新帖子。
现在我们需要将此页面添加到我们的路线中:
// routes.js
import React from 'react';
import { Router, Route } from 'react-router';
import App from './containers/App';
import Posts from './containers/Posts';
import AddPost from './containers/AddPost';
const Routes = (props) => (
<Router {...props}>
<Route path="/" component={ App }>
<Route path="/posts" component={ Posts } />
<Route path="/add-post" component={ AddPost } />
</Route>
</Router>
);
export default Routes;
在这里,我们只是添加了/add-post
路由,以便我们可以从该路由添加新的帖子。 因此,我们将AddPost
组件传递给了它的组件prop。
另外,让我们修改src/containers/Posts/index.js
文件的render
方法,以便它可以遍历对象而不是数组(因为Firebase不存储数组)。
// src/containers/Posts/index.js
render() {
let posts = this.props.posts;
if (this.props.loading) {
return (
<div>
Loading...
</div>
);
}
return (
<div className="Posts">
{ Object.keys(posts).map(function(key) {
return (
<div key={key}>
{ posts[key].title }
</div>
);
})}
</div>
);
}
现在,如果我们转到localhost:3000 / add-post ,我们可以添加一个新帖子。 单击提交按钮后,新帖子将立即显示在帖子页面上 。
您可以在GitHub上检查我的提交 。
实施投票
现在,我们需要允许用户对帖子进行投票。 为此,让我们修改src/containers/App/index.js
的render
方法:
// src/containers/App/index.js
render() {
return (
<div className="App">
{this.props.children && React.cloneElement(this.props.children, {
// https://github.com/ReactTraining/react-router/blob/v3/examples/passing-props-to-children/app.js#L56-L58
firebase: firebase.database(),
posts: this.state.posts,
loading: this.state.loading
})}
</div>
);
}
我们将firebase
道具从firebaseRef: firebase.database().ref('posts')
更改为firebase: firebase.database()
因为我们将使用Firebase的set
方法更新投票计数。 这样一来,如果我们有更多的火力地堡裁判,这将是很容易让我们仅使用处理它们firebase
道具。
在进行投票之前,让我们handleSubmit
修改一下src/containers/AddPost/index.js
文件中的handleSubmit
方法:
// src/containers/AddPost/index.js
handleSubmit = (e) => {
…
this.props.firebase.ref('posts').push({
title: this.state.title,
upvote: 0,
downvote: 0
});
…
}
我们将firebaseRef
道具重命名为firebase
道具。 因此,我们将this.props.firebaseRef.push
更改为this.props.firebase.ref('posts').push
。
现在,我们需要修改src/containers/Posts/index.js
文件以适应投票。
render
方法应该修改为:
// src/containers/Posts/index.js
render() {
let posts = this.props.posts;
let _this = this;
if (!posts) {
return false;
}
if (this.props.loading) {
return (
<div>
Loading...
</div>
);
}
return (
<div className="Posts">
{ Object.keys(posts).map(function(key) {
return (
<div key={key}>
<div>Title: { posts[key].title }</div>
<div>Upvotes: { posts[key].upvote }</div>
<div>Downvotes: { posts[key].downvote }</div>
<div>
<button
onClick={ _this.handleUpvote.bind(this, posts[key], key) }
type="button"
>
Upvote
</button>
<button
onClick={ _this.handleDownvote.bind(this, posts[key], key) }
type="button"
>
Downvote
</button>
</div>
</div>
);
})}
</div>
);
}
单击按钮后,Firebase数据库中的upvote或downvote计数将增加。 为了处理该逻辑,我们创建了另外两个方法: handleUpvote()
和handleDownvote()
:
// src/containers/Posts/index.js
handleUpvote = (post, key) => {
this.props.firebase.ref('posts/' + key).set({
title: post.title,
upvote: post.upvote + 1,
downvote: post.downvote
});
}
handleDownvote = (post, key) => {
this.props.firebase.ref('posts/' + key).set({
title: post.title,
upvote: post.upvote,
downvote: post.downvote + 1
});
}
在这两种方法中,每当用户单击任一按钮时,数据库中的相应计数就会增加,并在浏览器中立即更新。
如果我们使用localhost:3000 / posts打开两个选项卡,然后单击帖子的投票按钮,我们将看到每个选项卡几乎立即得到更新。 这是使用像Firebase这样的实时数据库的魔力。
您可以在GitHub上检查我的提交 。
在存储库中 ,我已将/posts
路由添加到应用程序的IndexRoute
,以默认显示在localhost:3000上的帖子。 您可以在GitHub上检查该提交 。
结论
公认的最终结果是有些准系统,因为我们没有尝试实现任何设计(尽管演示中添加了一些基本样式)。 我们也没有添加任何身份验证,以减少教程的复杂性和篇幅,但是显然任何实际应用程序都需要它。
Firebase在您不想创建和维护单独的后端应用程序的地方,或者您想要实时数据而又不花费太多时间开发API的地方非常有用。 正如您希望从本文中看到的那样,它在React中非常有效。
希望本教程对您将来的项目有所帮助。 请随时在下面的评论部分中分享您的反馈。
进一步阅读
本文由Michael Wanyoike进行同行评审。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!
From: https://www.sitepoint.com/reddit-clone-react-firebase/