Статьи

Как создать клон Reddit, используя React и Firebase

React — это потрясающая библиотека JavaScript для создания пользовательских интерфейсов. Со времени публикации приложения Create React стало очень легко создавать базовые приложения React.

В этой статье мы будем использовать Firebase вместе с Create React App для создания приложения, которое будет работать аналогично Reddit . Это позволит пользователю отправить новую ссылку, по которой затем можно будет проголосовать.

Вот живая демонстрация того, что мы будем строить.

Изобретатель, вкладывающий сердце FireBase в создание робота

Почему Firebase?

Использование Firebase позволит нам очень легко показывать данные в реальном времени пользователю. Когда пользователь проголосует за ссылку, обратная связь будет мгновенной. База данных Firebase в реальном времени поможет нам в разработке этой функции. Кроме того, это поможет нам понять, как загрузить приложение React с помощью Firebase.

Зачем реагировать?

React особенно известен созданием пользовательских интерфейсов с использованием компонентной архитектуры. Каждый компонент может содержать внутреннее состояние или передаваться в качестве реквизита . Состояние и реквизит являются двумя наиболее важными понятиями в React. Эти две вещи помогают нам определить состояние нашего приложения в любой момент времени. Если вы не знакомы с этими условиями, пожалуйста, сначала ознакомьтесь с документами React .

Примечание: вы также можете использовать контейнер состояний, такой как Redux или MobX , но для простоты мы не будем использовать его для этого урока.

Весь проект доступен на GitHub .

Настройка проекта

Давайте пройдемся по шагам, чтобы настроить структуру нашего проекта и любые необходимые зависимости.

Установка приложения create-реагировать

Если вы этого еще не сделали, вам нужно установить приложение create-реагировать на это . Для этого вы можете ввести в своем терминале следующее:

npm install -g create-react-app 

После того, как вы установили его глобально, вы можете использовать его для создания проекта React в любой папке.

Теперь давайте создадим новое приложение и назовем его reddit-clone .

 create-react-app reddit-clone 

Это создаст новый проект create-Reaction-app в папке reddit-clone . После завершения начальной загрузки мы можем зайти в каталог reddit-clone и запустить сервер разработки:

 npm start 

На этом этапе вы можете перейти по адресу http: // localhost: 3000 / и увидеть, как работает ваш скелет.

Структурирование приложения

Для обслуживания я всегда люблю отделять свои контейнеры и компоненты . Контейнеры — это интеллектуальные компоненты, которые содержат бизнес-логику нашего приложения и управляют Ajax-запросами. Компоненты просто тупые презентационные компоненты. Они могут иметь свое собственное внутреннее состояние, которое может использоваться для управления логикой этого компонента (например, отображение текущего состояния управляемого компонента ввода).

После удаления ненужного логотипа и CSS-файлов, вот так должно выглядеть ваше приложение. Мы создали папку components папку containers . Давайте переместим App.js в папку App.js containers/App и создадим registerServiceWorker.js внутри папки utils .

Структурирование приложения

Ваш файл 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 реакции-маршрутизатора .

 npm install --save [email protected] 

Теперь добавьте новый файл routes.js в папку src со следующим кодом:

 // 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 компонента Route компонент, переданный component prop, будет отображаться на странице. Здесь мы настраиваем корневой URL ( / ) для загрузки нашего компонента App с помощью компонента Router .

 <Router {...props}> <Route path="/" component={ <div>Hello World!</div> }> </Route> </Router> 

Приведенный выше код также действителен. Для пути / будет смонтирован <div>Hello World!</div> .

Теперь нам нужно вызвать наш файл routes.js из нашего файла src/index.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(); 

По сути, мы монтируем наш компонент Router из нашего файла routes.js Мы передаем ему history чтобы маршруты знали, как обрабатывать историю .

Вы можете проверить мой коммит на GitHub.

Добавление Firebase

Если у вас нет учетной записи Firebase , создайте ее сейчас (это бесплатно!), Зайдя на их сайт. После того, как вы закончите создание новой учетной записи, войдите в свою учетную запись, перейдите на страницу консоли и нажмите « Добавить проект» .

Введите название вашего проекта (я назову мой 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, мы получим опцию Добавить Firebase в ваше веб-приложение .

Добавьте Firebase в свое веб-приложение

Если мы нажмем на эту опцию, появится модальное окно, которое покажет нам переменную config которую мы будем использовать в нашем методе componentWillMount .

Configs

Давайте создадим файл конфигурации Firebase. Мы назовем этот файл firebase-config.js , и он будет содержать все 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'; 

Мы инициализируем наше соединение с базой данных Firebase в constructor .

 // App/index.js constructor() { super(); // Initialize Firebase firebase.initializeApp(config); } 

В хуке жизненного цикла componentWillMount() мы используем только что установленный пакет firebase вызываем его метод initializeApp и config переменную 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 .

postsRef.on('value', …) дает нам обновленное значение всякий раз, когда есть какие-либо изменения в базе данных. Это очень полезно, когда нам требуется обновление нашего пользовательского интерфейса в режиме реального времени на основе любых событий в базе данных.

Использование postsRef.once('value', …) даст нам данные только один раз. Это полезно для данных, которые нужно загружать только один раз и которые не должны часто меняться или требовать активного прослушивания.

После того как мы получим обновленное значение в нашем обратном вызове on() , мы сохраняем значения в нашем состоянии posts .

Теперь мы увидим данные, появляющиеся на нашей консоли.

Образец данных

Также мы передадим эти данные нашим детям. Итак, нам нужно изменить функцию render нашего файла App/index.js :

 // 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> ); } 

Основная цель здесь — сделать данные постов доступными во всех наших дочерних компонентах, которые будут передаваться через react-router .

Мы проверяем, существует ли this.props.children или нет, и если он существует, мы клонируем этот элемент и передаем все наши реквизиты всем нашим детям. Это очень эффективный способ передачи реквизита динамичным детям.

Вызов 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 компоненту prop, а /posts — методу Route компонента Route реагирующего маршрутизатора.

Если мы перейдем по 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 . Метод 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 нашего файла src/containers/Posts/index.js чтобы он мог перебирать объекты вместо массивов (поскольку 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 :

 // 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() потому что мы будем использовать метод set Firebase для обновления нашего подсчета голосов. Таким образом, если бы у нас было больше ссылок на Firebase, нам было бы очень легко справиться с ними, используя только опору firebase .

Прежде чем приступить к голосованию, давайте handleSubmit метод handleSubmit в нашем файле src/containers/AddPost/index.js :

 // src/containers/AddPost/index.js handleSubmit = (e) => { … this.props.firebase.ref('posts').push({ title: this.state.title, upvote: 0, downvote: 0 }); … } 

Мы переименовали нашу опору firebase опору 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 будет увеличиваться количество повышенных или пониженных голосов. Чтобы обработать эту логику, мы создадим еще два метода: 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, как вы можете надеяться, из статьи.

Я надеюсь, что этот урок поможет вам в ваших будущих проектах. Пожалуйста, не стесняйтесь поделиться своим мнением в разделе комментариев ниже.

дальнейшее чтение

Эта статья была рецензирована Майклом Ванёйке . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!