
Давайте создадим приложение React с Firebase Auth, Express Backend и базой данных MongoDB.
Firebase - чрезвычайно хорошая платформа для разработки программного обеспечения, которая дает вам доступ как к аутентификации, ни к базам данных без SQL, ни к хранилищу, так и многому другому. Это настоящий источник энергии, и мне нравится простота использования Firebase и базы данных Firestore.
Все мы знаем, как важно использовать правильный инструмент для работы. Если вы разрабатываете приложение с помощью Firebase, вы можете столкнуться с ситуацией, когда облачных функций недостаточно, и вам понадобится отдельный внутренний сервер для выполнения некоторых задач. Вы даже можете использовать Firebase в качестве основного концентратора и подключать к нему другие микросервисы.
В любом случае, если вы покидаете Firebase, вам потребуется аутентификация. Было бы странно иметь разные системы аутентификации на Firebase и на вашем отдельном внутреннем сервере.
К счастью, Google позволяет нам использовать аутентификацию Firebase вне Firebase. Это работает путем отправки токена аутентификации Firebase с полезной нагрузкой на внутренний сервер и проверки его там. Давайте посмотрим, как мы можем с этим справиться.
Это будет пошаговое руководство, в котором мы создадим простой пример приложения. Мы тщательно пройдемся по каждому этапу, и вам не потребуется никакого опыта работы с Firebase, React или Express.
Оглавление
- Что мы строим
- Настройка Firebase
- Начало работы с React Frontend
- Начало работы с Express
- Общение с сервером
- От бэкэнда к фронту
- Отображение данных в веб-интерфейсе
- Заключительные мысли
Что мы строим
Мы создадим простую телефонную книгу. Пользователь сможет добавлять номера телефонов и просматривать список всех номеров телефонов. Мы не будем использовать стили CSS в этом проекте. Давайте посмотрим, какие технологии мы будем использовать.
Firebase
Мы будем использовать Firebase только в качестве платформы аутентификации.
https://firebase.google.com/
React
Мы будем использовать современные хуки и React на основе стрелок для нашего интерфейса.
https://reactjs.org/
Express
Мы напишем наш бэкэнд на NodeJS и будем использовать Express в качестве бэкэнд-фреймворка
https://expressjs.com/
MongoDB (MongoDB Atlas)
Мы будем использовать MongoDB Atlas в качестве нашей базы данных - облачную версию MongoDB, размещенную самой командой MongoDB. Обычная база данных SQL была бы наиболее естественным выбором с точки зрения функциональности этого приложения. Однако с помощью модуля npm Mongoose MongoDB будет действовать как реляционная база данных и предоставлять нам схемы для наших данных. Что хорошо в этом пути, так это то, что мы можем использовать Mongoose для структурированных данных и чистый MongoDB для неструктурированных данных. В этом приложении мы будем использовать MongoDB только через Mongoose.
https://www.mongodb.com/
Github с полным кодом
Полный исходный код этого проекта можно найти по адресу https://github.com/Devalo/Firebase-auth-react-express-mongodb
Это много интересного для такого простого приложения. Мы собираемся оторваться!
Настройка Firebase
Начнем с создания нового приложения Firebase. Перейдите на www.firebase.com, нажмите Начать и войдите в свою учетную запись Google.
Затем мы должны нажать на Добавить проект.

Мы дадим приложению имя

Для этого проекта нам не нужен Google Analytics.

И нажимаем «Создать проект». Через некоторое время наш проект будет готов. Когда все будет готово, нажмите «продолжить».
Если вы посмотрите на левое меню, вы найдете вкладку «Аутентификация». Мы выберем его и нажмем «настроить метод входа».

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

Теперь мы не будем создавать новых пользователей в этом проекте. Мы разрешим вход только пользователям. По этой причине мы добавим нашего нового пользователя прямо в Firebase Authentication. Нажмите на «пользователи».

и добавить нового пользователя.

Firebase автоматически правильно хеширует пароль. Вы не сможете больше вносить изменения для пользователя с этой панели. Все остальные изменения нужно делать программно. Однако создается наш первый пользователь:

Начало работы с React Frontend
Настройка React
Установив Firebase, мы приступим к настройке нашего интерфейса. Мы будем использовать приложение create-react-app для начальной загрузки нашего интерфейса.
$ npx create-react-app phone-frontend ... Happy hacking! $ cd phone-frontend /phone-frontend $ npm start
Если все пойдет по плану, мы должны увидеть это на http: // localhost: 3000.

Милая. Теперь большая часть нашей работы будет выполняться в папке src. наша структура папок в папке src будет следующей:
/components
- /sessions
Login.jsx
- /phonebook
AddNumber.jsx
ListAllNumbers.jsx
/services
- phonebookServices.js
App.js
index.js
Мы меняем наш index.js на:
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
и наш App.js для:
// App.js
function App() {
return (
<div className="App">
Hello World!
</div>
);
}
export default App;
И мы готовы начать работу над нашим интерфейсом React. Примечание. Возможно, вам придется перезапустить сервер реакции на этом этапе, чтобы изменения вступили в силу.
Теперь мы знаем, что у нас будет несколько представлений. Нам нужен способ переключения между страницей входа, перечислением всех телефонных номеров и добавлением номеров. Для этого мы воспользуемся модулем npm, который называется response-router-dom. Установим:
npm install react-router-dom
Нам также необходимо установить пакет firebase:
npm install firebase
Теперь давайте создадим наш маршрут входа и сделаем так, чтобы мы всегда попадали на нашу страницу входа, если мы не вошли в систему.
Добавьте firebase в наш интерфейс
На предыдущем шаге мы создали новое приложение Firebase. Есть некоторые конфигурации, которые нам нужно получить с веб-сайта firebase.
Вернувшись на веб-сайт firebase, щелкните самую верхнюю ссылку на левой панели навигации - Обзор проекта. Нам нужно добавить новое веб-приложение:

Даем ему имя:

И мы копируем объект firebaseConfig, и нажимаем «продолжить консоль».

В производственном приложении вы можете поместить эти конфигурации в переменные среды, но Google заявляет, что в этом нет строгой необходимости. Причина этого в том, что безопасность Firebase основана на нескольких правилах, которые ограничивают и открывают приложение. По моему скромному мнению, такие настройки всегда лучше скрыть. Хотя я могу ошибаться.
внутри нашей папки src мы создадим новый файл - fire.js и добавим в него наши конфигурации. Файл должен выглядеть так:
// fire.js
import firebase from 'firebase';
const firebaseConfig = {
apiKey: "AIzaSyAHDUwcM0zO4yMCoyazo4w-HOeKfGsQL2g",
authDomain: "phone-book-fe436.firebaseapp.com",
databaseURL: "https://phone-book-fe436.firebaseio.com",
projectId: "phone-book-fe436",
storageBucket: "phone-book-fe436.appspot.com",
messagingSenderId: "410083027161",
appId: "1:410083027161:web:e69ec743153a72d48df6c8"
};
try {
firebase.initializeApp(firebaseConfig);
} catch (err) {
if (!/already exists/.test(err.message)) {
console.error('Firebase initialization error', err.stack);
}
}
const fire = firebase;
export default fire;
Мы будем использовать firebase в этом файле. Теперь мы готовы приступить к аутентификации пользователей.
Настройка аутентификации
Firebase предоставляет нам очень простой способ проверить, прошли ли мы аутентификацию. Мы создадим новое состояние с помощью хука useState () и сохраним там результат:
// App.js
import React, { useState } from 'react';
import fire from './fire.js';
function App() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
fire.auth().onAuthStateChanged((user) => {
return user ? setIsLoggedIn(true) : setIsLoggedIn(false);
});
console.log('logged in?', isLoggedIn);
return (
<div className="App">
Hello World!
</div>
);
}
export default App;
Мы импортируем useState из библиотеки React и firebase из нашего файла конфигурации firebase. Мы используем функцию .onAuthStateChanged (), чтобы проверить, вошел ли пользователь в систему или нет. Если мы вошли в систему, мы установим для нашего состояния isLoggedIn значение true.
если мы проверим нашу консоль, она ясно покажет, что мы не вошли в систему.
Создадим наш первый маршрут. Теперь при каждом запросе мы хотим проверять, вошли ли мы в систему, прежде чем продолжить.
// App.js
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
...
<div className="App">
<Router>
{!isLoggedIn
? (
<>
<Switch>
<Route path="/">
<Login />
</Route>
</Switch>
</>
)
: (
<>
Hello World!
</>
)}
</Router>
</div>
...
Все маршруты являются дочерними по отношению к компоненту Switch, который является дочерним по отношению к компоненту Router. Если мы не вошли в систему, мы не увидим ничего, кроме компонента входа. Мы можем проверить это, посмотрев на нашу пустую страницу.
Давайте перейдем к компоненту входа в систему и посмотрим, сможем ли мы разместить что-нибудь на нашей странице.
Мы начнем с создания простой, работоспособной формы, в которой будет записываться как адрес электронной почты, так и пароль:
// components/session/Login.jsx
import React, { useState } from 'react';
import fire from '../../fire.js';
const Login = () => {
const [email, setEmail] = useState();
const [password, setPassword] = useState();
const handleSubmit = (e) => {
e.preventDefault();
console.log(`submitted email:
${email} password: ${password}`);
}
return (
<div>
<h2>Login</h2>
<form onSubmit={handleSubmit}>
<input
type="text"
onChange={({ target }) =>
setEmail(target.value)}
placeholder="Email"
/>
<br />
<input
type="password"
onChange={({ target}) =>
setPassword(target.value)}
placeholder="Password"
/>
<br />
<button type="submit">
Sign in
</button>
</form>
</div>
)
};
export default Login
Мы настроили два новых перехватчика состояния, которые будут хранить состояние нашего адреса электронной почты и пароля. Каждый из входов имеет атрибут onChange, который записывает входные данные непосредственно в каждый обработчик состояния. Если мы нажмем кнопку «Войти», введенные нами данные будут отображаться в нашей консоли. Довольно аккуратно. Войти будет очень просто. Нам просто нужно добавить одну строку кода в нашу функцию отправки:
// components/session/Login.jsx
...
const handleSubmit = (e) => {
e.preventDefault();
fire.auth().signInWithEmailAndPassword(email, password)
.catch((error) => {
console.error('Incorrect username or password');
});
}
...
Это простая линия. Если вы ввели неправильный адрес электронной почты и пароль:

Если мы войдем в систему с правильным именем пользователя и паролем, мы вернем наш Hello World! text, и теперь наша консоль выводит истину (которая исходит из обработчика состояния, удерживающего состояние нашего сеанса).
Найдите минутку и дайте ей понять. Это невероятно простая аутентификация. Я не думаю, что вам будет легче, чем это.
Вероятно, нам также следует найти способ выйти из системы. К счастью, Firebase также делает это невероятно простым. Замените текст «Hello World!» В App.js на:
// App.js
<span onClick={signOut}>
<a href="#">Sign out</a>
</span>
И добавьте новую функцию в компонент App:
const signOut = () => {
fire.auth().signOut()
};
Вот и все! Как только вы нажмете «Выйти», мы вернемся к форме входа. Хорошее начало!
Создание представления для наших телефонных номеров
Ранее мы создали файл компонента для наших телефонных номеров. Сейчас хорошее время, чтобы начать реализацию способа отображения наших данных. Начнем с создания нового маршрута. Мы разместим наш маршрут под нашей ссылкой "выйти":
// App.js
import ListAllNumbers from './components/phonebook/ListAllNumbers';
...
<span onClick={signOut}>
<a href="#">Sign out</a>
</span>
<Switch>
<Route path="/">
<ListAllNumbers />
</Route>
</Switch>
...
Приложение выйдет из строя, как только вы сохраните файл. Это нормально. Мы еще не создали компонент ListAllNumbers. Перейдем к файлу компонента.
А пока создадим очень простую таблицу, в которой будем отображать наши данные:
// components/phonebook/ListAllNumbers.jsx
import React from 'react';
import { Link } from 'react-router-dom';
const ListAllNumbers = () => {
return (
<div>
<Link to="/add-number">Add number</Link>
<h2>Phone numbers</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Number</th>
</tr>
</thead>
<tbody>
<tr>
<td>Foo Bar</td>
<td>999888777</td>
</tr>
</tbody>
</table>
</div>
)
};
export default ListAllNumbers;

Это шедевр ..
Создание представления для добавления телефонных номеров
Прежде чем приступить к рассмотрению какой-либо логики, мы создадим последнее представление.
Мы создадим новый маршрут поверх предыдущего.
Весь App.js будет выглядеть так:
// App.js
import React, { useState } from 'react';
import { BrowserRouter as Router, Switch, Route} from 'react-router-dom';
import fire from './fire.js';
import Login from './components/session/Login';
import ListAllNumbers from './components/phonebook/ListAllNumbers';
import AddNumber from './components/phonebook/AddNumber';
function App() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
fire.auth().onAuthStateChanged((user) => {
return user ? setIsLoggedIn(true) : setIsLoggedIn(false);
});
const signOut = () => {
fire.auth().signOut()
};
console.log(isLoggedIn);
return (
<div className="App">
<Router>
{!isLoggedIn
? (
<>
<Switch>
<Route path="/">
<Login />
</Route>
</Switch>
</>
)
: (
<>
<span onClick={signOut}>
<a href="#">Sign out</a>
</span>
<Switch>
<Route path="/add-number">
<AddNumber />
</Route>
<Route path="/">
<ListAllNumbers />
</Route>
</Switch>
</>
)}
</Router>
</div>
);
}
export default App;
Идеально. Наши маршруты настроены. Перейдем к компоненту AddNumber и добавим необходимый код:
// components/phonebook/AddNumber.js
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
const AddNumber = () => {
const [name, setName] = useState();
const [phone, setPhone] = useState();
const handleSubmit = (e) => {
e.preventDefault();
console.log(`submitted: ${name} - ${phone}`);
};
return (
<div>
<Link to="/">View phonebook</Link>
<h2>Add Number</h2>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Name"
onChange={({ target }) => setName(target.value)}
/><br />
<input
type="text"
placeholder="Number"
onChange={({ target }) => setPhone(target.value)}
/><br />
<button type="submit">
Add number
</button>
</form>
</div>
)
};
export default AddNumber;
Как и в случае с полем входа в систему, мы сохраняем оба поля ввода в ловушке состояния. При отправке мы выводим входные данные на нашу консоль. Идеально. Это так же красиво, как и наш интерфейс. На данный момент мы закончили с интерфейсом. Мы должны создать бэкэнд, а затем подключить базу данных. Мы скоро вернемся к интерфейсу.
Начало работы с серверной частью Express
Мы будем строить наш бэкэнд с помощью фреймворка Express. Наш бэкэнд будет независимым приложением. Давайте выйдем из внешнего интерфейса и создадим новую папку для нашего внутреннего интерфейса:
/phone-frontend $ cd .. $ mkdir phone-backend $ cd phone-backend /phone-backend $ npm init
Мы начинаем новый проект JS с «npm init». Ни один из приведенных здесь вопросов сейчас не имеет для нас никакого значения, поэтому просто нажимайте Enter до конца. После этого для нас будет создан файл package.json в нашей внутренней папке телефона.
Поскольку наш бэкэнд будет JSON-API, мы не будем использовать генераторы Express для начальной загрузки нашего приложения. Начнем с установки Express:
/phone-backend $ npm install express
И создадим наш файл endpoint index.js:
/phone-backend $ touch index.js
В нашем индексном файле мы напишем быстрый сервер:
// index.js
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello World');
});
const PORT = 3001;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
Это все, что нужно для создания конечной точки сервера. Мы можем определить маршрут с помощью app.get (). Если вы перейдете на http: // localhost: 3001, вы получите прекрасное сообщение:

Давайте подумаем, каким будет поток приложения.
- Frontend отправит запрос GET на наш корневой маршрут, запрашивая все записи
- Бэкэнд получит запрос и загрузит его из БД
- Frontend отправит запрос POST на наш корневой маршрут, добавив запись
- Бэкэнд получит запрос и добавит в БД
Лучше всего начать с каркаса маршрутов. Мы будем структурировать наше приложение следующим образом:
- controllers phones.js - models phone.js index.js
Записываем наш контроллер, устанавливаем nodemon
Мы начнем с нашего контроллера, который содержит маршруты. Их всего два, так что кодировать будет довольно просто. Мы будем использовать Express Router, мощный встроенный маршрутизатор.
Наш файл контроллера будет выглядеть так:
// controllers/phones.js
const phonesRouter = require('express').Router();
phonesRouter.get('/', (req, res) => {
return res.send('Hi, from within the phones router GET');
});
phonesRouter.post('/', (req, res) => {
return res.send('Hi, from within the phones router POST');
});
module.exports = phonesRouter;
Два маршрута. Один для запросов GET и один для запросов POST. При вызове они вернут сообщение. Теперь мы импортируем его в наш index.js и заменим предыдущий маршрут на phoneRouter. Обратите внимание, что мы добавили "/ api" к нашему URL-адресу. URL-адрес, по которому мы запрашиваем наш API, будет http: // localhost: 3001 / api.
const express = require('express');
const phonesRouter = require('./controllers/phones');
const app = express();
app.use('/api', phonesRouter);
const PORT = 8080;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
Если вы попытаетесь перейти на наш новый URL, ничего не произойдет. Оказывается, наше приложение не перезагружается автоматически, когда мы меняем код. Чтобы бороться с этим, мы установим пакет npm под названием nodemon, который будет делать именно это - перезагружать при изменении кода:
/phone-backend $ npm install nodemon
Чтобы использовать nodemon, мы создадим небольшой скрипт внутри нашего файла package.json:
// package.json
...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "nodemon index.js"
},
...
сценарий «dev» запустит наше приложение с помощью nodemon. Запускаем наше приложение командой npm run dev:
/phone-backend $ npm run dev > nodemon index.js [nodemon] 2.0.6 [nodemon] to restart at any time, enter `rs` [nodemon] watching path(s): *.* [nodemon] watching extensions: js,mjs,json [nodemon] starting `node index.js` Server is running on port 3001
Замечательно, если вы вернетесь на http: // localhost: 3001 / api, вы получите сообщение прямо с маршрута. Мы приближаемся к извлечению данных из базы данных. В связи с этим - давайте настроим новую базу данных MongoDB Atlas.
Настройка Атласа MongoDB
Первое, что нам нужно сделать, это зайти в MongoDB Atlas и создать нашу учетную запись: перейдите на https://www.mongodb.com/cloud/atlas/signup и создайте нового пользователя.
Выберите бесплатный кластер:

Вы сможете выбрать провайдера, на котором будет размещена ваша база данных, и регион. Я собираюсь разместить базу данных в Google. Нажмите «Создать кластер»:

Появится мастер. Давайте выполним его шаги, когда кластер будет запущен и заработает. Это может занять пару минут:

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

Создайте нового пользователя:

И добавляем пользователя. После этого нам нужно внести наш сервер в белый список. Здесь мы будем использовать стандартный IP 0.0.0.0:

Внесение IP-адреса в белый список обеспечивает дополнительный уровень безопасности. Однако наш бэкэнд будет входить в базу данных с именем пользователя и паролем, так что в любом случае он не будет полностью открыт.
После этого следующим шагом будет подключение нашей базы данных к нашему бэкэнду.
Мы выберем для подключения наше приложение:

скопируйте URL. Это URL-адрес, который мы будем использовать для подключения к базе данных. Сохраните URL-адрес на будущее:

MongoDB Atlas настроен. Давайте вернемся к нашему бэкэнду и подключим его.
Подключение бэкэнда к MongoDB Atlas
Как было сказано ранее, мы будем использовать Mongoose вместо чистого MongoDB. Это даст нам некоторую структуру. (Это как бы противоречит цели использования базы данных noSQL, но с Mongoose вы можете сделать и то, и другое). Установим:
/phones-backend $ npm install mongoose
Чтобы упростить задачу, мы добавим наш код для подключения к MongoDB в наш index.js. Конечно, вы можете сохранить его в другом месте и импортировать в index.js:
// index.js
const mongoose = require('mongoose');
...
mongoose.connect(
'mongodb+srv://foobar:[email protected]/phonebook?retryWrites=true&w=majority',
{ useNewUrlParser: true, useUnifiedTopology: true })
.then(() => {
console.log('Connected to database');
})
.catch((err) => {
console.log('Error connecting to DB', err.message);
});
...
Вставьте URL-адрес MongoDB, который вы скопировали ранее, в функцию подключения мангуста. Замените ‹password› и ‹dbname› своими учетными данными. Когда вы сохраните, сервер перезагрузится, и вы получите прекрасное сообщение на консоли о том, что вы подключены к базе данных. Если по какой-то причине вы получите сообщение об ошибке, убедитесь, что вы ввели правильные учетные данные.
Теперь соединение между нашим внутренним сервером и базой данных установлено. Это означает, что мы можем начать извлекать данные из базы данных и помещать их в нее. Прежде чем мы это сделаем, вернемся к интерфейсу.
Общение с серверной частью
Если мы вернемся к потоку нашего приложения, которое мы написали немного ранее
- Frontend отправит запрос GET на наш корневой маршрут, запрашивая все записи
- Бэкэнд получит запрос и загрузит его из БД
- Frontend отправит запрос POST на наш корневой маршрут, добавив запись
- Бэкэнд получит запрос и добавит в БД
Мы видим, что веб-интерфейс должен отправить запрос на получение серверной части. Мы хотим включить в запрос токен аутентификации firebase. Таким образом, серверная часть может проверить пользователя перед тем, как разрешить доступ к базе данных.
Мы хотим отделить код, который взаимодействует с серверной частью, от самого компонента представления. В нашем файле AddNumber.jsx мы отправим данные формы в службу, которая будет заниматься внутренней связью. Если вы не создали файл phonebookServices.js в служебной папке, сейчас было бы отличное время.
Передать данные в сервис
Прежде всего, давайте заставим интерфейс и серверную часть взаимодействовать друг с другом. Мы сосредоточимся на этом, прежде чем приступить к отправке токена firebase. Внутри AddNumber.jsx мы добавим немного кода в нашу функцию handleSubmit ():
// components/phonebook/AddNumber.jsx
import { addToPhonebook } from '../../services/phonebookServices';
...
const handleSubmit = (e) => {
e.preventDefault();
if (name && phone) {
phonebookServices.addToPhonebook(name, phone);
}
console.log('You must enter a name and a number');
};
...
Если оба поля заполнены, мы отправим данные в нашу службу. Если нет, печатаем в консоль. Если вы на самом деле что-то заполните и отправите, приложение взорвется.
А теперь давайте исправим сервис!
Передавать данные из сервиса в базу данных… И обратно.
Служба будет отвечать за обмен данными с серверной службой. Для этого мы могли бы использовать встроенную библиотеку выборки. Вместо этого мы воспользуемся библиотекой axios, так как с ней мне немного легче работать. Установим:
/phone-frontend $ npm install axios
Мы знаем, что наш внутренний сервер работает по адресу http: // localhost: 3001 / api, поэтому пришло время отправить данные формы на этот URL. В нашем служебном файле:
// services/phonebookServices.js import axios from 'axios'; const url = 'http://localhost:3001/api'; export const addToPhonebook = (name, number) => { const payload = { name, number, } axios.post(url, payload); };
Мы создаем новый объект JS и передаем его в axios с URL-адресом серверной части. Взволнованные как никогда, мы заполняем нашу форму и отправляем ее:

Вот это да. Это не сработало. Ну, это из-за совместного использования ресурсов между разными источниками, или для краткости CORS.
от разработчика Mozilla
Совместное использование ресурсов между источниками (CORS) - это механизм, который использует дополнительные заголовки HTTP, чтобы указать браузерам предоставить веб-приложению, работающему в одном источнике, доступ к выбранным ресурсам из другого источника. . Веб-приложение выполняет HTTP-запрос с несколькими источниками, когда оно запрашивает ресурс, который имеет источник, отличный от его собственного (домен, протокол или порт).
Мы разберемся с этим, установив модуль под названием cors. Перейдите на бэкэнд и установите его:
/phone-backend $ npm install cors
И примените его к нашему бэкэнду:
// index.js
const cors = require('cors');
...
const app = express();
app.use(cors());
...
Установив cors, мы снова пробуем наш интерфейс, и все работает как положено:

Это потрясающе. Теперь интерфейс и серверная часть обмениваются данными. Следующее, что нам нужно сделать, это выяснить, как отправить токен Firebase с полезной нагрузкой из внешнего интерфейса в серверный.
Токен Frontend Firebase
Самое замечательное в Firebase - это простота использования. То же самое и с созданием токенов. Мы создадим новую функцию в нашем служебном файле, которая создаст для нас этот токен.
// services/phonebookService.js
import fire from '../fire';
...
const createToken = async () => {
const user = fire.auth().currentUser;
const token = user && (await user.getIdToken());
const payloadHeader = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
};
return payloadHeader;
}
...
Эта функция выбирает текущего пользователя из библиотеки Firebase Auth. Мы используем текущего пользователя для получения токена id. Нам нужно поместить этот токен в заголовок нашего запроса. Самый простой способ сделать это - создать и вернуть заголовок с включенным токеном.
Внутри функции addToPhonebook мы добавим в код асинхронный блок try… catch и включим наш недавно сгенерированный заголовок auth с полезной нагрузкой:
// services/phonebookServices.js
...
export const addToPhonebook = async (name, number) => {
const header = await createToken();
const payload = {
name,
number,
}
try {
const res = await axios.post(url, payload, header);
return res.data;
} catch (e) {
console.error(e);
}
};
Пока мы занимаемся этим, давайте создадим функцию, которая будет извлекать все номера телефонов из базы данных. Создаем его в том же файле:
// services/phonebookServices.js
...
export const getPhonebookEntries = async () => {
const header = await createToken();
try {
const res = await axios.get(url, header);
return res.data;
} catch (e) {
console.error(e);
}
}
Мы снова используем аксиомы. На этот раз мы отправим запрос GET на серверную часть. Это запросит у серверной части все записи телефонной книги. Вернемся к серверной части.
От бэкэнда к интерфейсу
Оглядываясь назад на поток наших приложений
- Frontend отправит запрос GET на наш корневой маршрут, запрашивая все записи
- Бэкэнд получит запрос и загрузит его из БД
- Frontend отправит запрос POST на наш корневой маршрут, добавив запись
- Бэкэнд получит запрос и добавит в БД
Мы можем пересечь два шага, что сужает их до:
- Бэкэнд получит запрос и загрузит его из БД
- Бэкэнд получит запрос и добавит в БД
Прежде чем мы действительно получим и отправим в базу данных, мы должны проверить пользователя. Токен аутентификации включен в запрос. Это означает, что мы можем создать промежуточное ПО, которое будет передавать запрос и немного его изменять. Мы изменим его так, чтобы мы могли найти проверенного пользователя в запросе, как только он достигнет нашего контроллера и выполнит маршрутизацию.
Создание промежуточного программного обеспечения firebase
Поскольку мы используем Firebase в качестве поставщика аутентификации, нам необходимо установить серверный модуль Firebase. Это позволит нам проверить токен аутентификации с помощью Firebase. Давайте установим его:
/phonebook-backend $ npm install firebase-admin
Нам нужно сгенерировать файл закрытого ключа учетной записи службы Google. Следуя инструкциям на странице https://firebase.google.com/docs/admin/setup

нам нужно перейти на https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk, чтобы создать его. также скопируйте URL из примера конфигурации Google
После того, как вы скачали файл JSON, добавьте его в корневой каталог нашей серверной части. Вам также следует изменить его название на более простое. Я позвонил в свой serviceAccount.json. Сейчас самое время поместить конфигурацию в отдельные переменные среды. Не стесняйтесь делать это, если хотите.
Давайте создадим новый файл - authenticateToken.js и поместим его в корневой каталог. Мы напишем наше промежуточное ПО здесь:
// authenticateToken.js
const admin = require('firebase-admin');
const serviceAccount require('./serviceAccount.json');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: 'https://phone-book-fe436.firebaseio.com',
});
async function decodeIDToken(req, res, next) {
const header = req.headers?.authorization;
if (header !== 'Bearer null' && req.headers?.authorization?.startsWith('Bearer ')) {
const idToken = req.headers.authorization.split('Bearer ')[1];
try {
const decodedToken = await admin.auth().verifyIdToken(idToken);
req['currentUser'] = decodedToken;
} catch (err) {
console.log(err);
}
}
next();
}
module.exports = decodeIDToken;
Мы импортируем наш файл JSON с конфигурациями Firebase и используем его для инициализации Firebase на нашем сервере. мы создаем функцию - decodeIDToken, которая проверяет входящий запрос на токен Bearer. если токен существует, мы отправляем его в Firebase для проверки. Если проверено, мы помещаем его в запрос. Таким образом, наши маршруты смогут его использовать.
Нам также нужно добавить его в наш index.js:
// index.js
...
const decodeIDToken = require('./authenticateToken');
...
app.use(decodeIDToken);
...
Теперь мы можем аутентифицировать каждый входящий запрос:
// controllers/phones.js
...
phonesRouter.post('/', (req, res) => {
const auth = req.currentUser;
if (auth) {
console.log('authenticated!', auth);
return res.send('Hi, from within the phones router POST');
}
return res.status(403).send('Not authorized')
});
Мы можем получить аутентифицированного пользователя прямо из запроса. Если он существует, мы выводим пользователя на консоль:

Создание телефонной книги
Наконец-то мы можем начать использовать мангуста по-настоящему. Мы создадим новую модель телефонной книги с помощью мангуста. Внутри нашего пустого phone.js:
// models/phone.js
const mongoose = require('mongoose');
const phoneSchema = new mongoose.Schema({
name: String,
number: String,
});
phoneSchema.set('toJSON', {
transform: (doc, returnedObject) => {
returnedObject.id = returnedObject._id.toString();
delete returnedObject._id;
delete returnedObject.__v;
},
});
module.exports = mongoose.model('Phone', phoneSchema);
Сначала мы создаем схему базы данных. Здесь оба наших поля будут строками. Как поля в нашем интерфейсе. Затем мы убеждаемся, что объект, возвращаемый из базы данных, содержит идентификатор в виде строки. Первоначально это был объект.
Это все для нашей модели. Теперь мы можем отправлять сообщения в базу данных.
Размещение записей в базе данных
Чтобы иметь возможность правильно управлять полезной нагрузкой из внешнего интерфейса, нам нужно добавить еще одно встроенное промежуточное ПО в наше приложение. добавить в index.js:
app.use(express.json());
Мы внесем некоторые изменения в наш маршрут POST:
// controllers/phones.js
...
const Phone = require('../models/phone');
...
phonesRouter.post('/', (req, res) => {
const auth = req.currentUser;
if (auth) {
const phone = new Phone(req.body);
const savedPhone = phone.save();
return res.status(201).json(savedPhone);
}
return res.status(403).send('Not authorized');
});
Проверяем запрос, создаем новый объект Phone из нашей модели и сохраняем его.
Если мы заглянем внутрь нашей коллекции MongoDB:

Спасено!
Пока мы в ударе, давайте запрограммируем маршрут GET.
Получение записей из базы данных
Мы кодируем наш маршрут GET:
// controllers/phones.js
...
phonesRouter.get('/', async (req, res) => {
const auth = req.currentUser;
if (auth) {
const phones = await Phone.find({});
return res.json(phones.map((phone) => phone.toJSON()));
}
return res.status(403).send('Not authorized');
});
...
мангуст, конечно, облегчает нам задачу. Передав пустой объект, мы запрашиваем все документы в коллекции. Затем мы переберем каждый объект, применяя изменения, внесенные в файл модели. Это приведет к удалению поля __v и исправлению идентификатора. После этого мы возвращаем ему фронтенд.
Отображение данных в веб-интерфейсе
Если мы еще раз посмотрим на наши шаги:
- Бэкэнд получит запрос и загрузит его из БД
- Бэкэнд получит запрос и добавит в БД
Мы действительно получаем данные из базы данных. Мы также пополняем базу. Это означает, что мы приближаемся к нашей конечной цели. Теперь давайте выясним, как мы можем перечислить все записи в нашем интерфейсе
наш служебный файл возвращает полученные данные. Давайте вызовем getPhonebookEntries () из нашего компонента ListAllNumbers. Полный компонент ListAllNumbers будет выглядеть так:
// components/phonebook/ListAllNumbers.jsx
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { getPhonebookEntries } from '../../services/phonebookServices';
const ListAllNumbers = () => {
const [entries, setEntries] = useState();
useEffect(() => {
const fetchEntries = async () => {
const fetchedEntries = await getPhonebookEntries();
setEntries(fetchedEntries);
}
fetchEntries();
}, [])
if (entries === undefined) {
return null;
}
return (
<div>
<Link to="/add-number">Add number</Link>
<h2>Phone numbers</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Number</th>
</tr>
</thead>
<tbody>
{entries.map((entry) => (
<tr>
<td>{entry.name}</td>
<td>{entry.number}</td>
</tr>
))}
</tbody>
</table>
</div>
)
};
export default ListAllNumbers;
мы настраиваем состояние, в котором будут храниться наши записи. Мы запрашиваем базу данных, используя наш сервис в хуке useEffect. Внутри шаблона мы перебираем записи с помощью функции карты и отображаем каждую запись.
Из документа React:
Что делает
useEffect? С помощью этого хука вы сообщаете React, что ваш компонент должен что-то сделать после рендеринга. React запомнит переданную вами функцию (мы будем называть ее нашим «эффектом») и вызовет ее позже, после выполнения обновлений DOM. В этом случае мы устанавливаем заголовок документа, но мы также можем выполнять выборку данных или вызывать какой-либо другой императивный API.
Мы получаем данные от бэкэнда, и устанавливаем их в состояние. Поскольку запрос к базе данных является асинхронной операцией, мы проверяем, существуют ли записи. Если нет, мы возвращаем null. Как только данные вернутся из бэкэнда, записи будут заполнены автоматически, и наши данные будут присутствовать.
Если мы попытаемся добавить еще одну запись и вернемся в нашу телефонную книгу:

Все сохраняется и извлекается, как ожидалось.
Заключительные мысли
Как я уже говорил ранее, Firebase не всегда подходит для работы. Однако нам всегда нужна какая-то аутентификация. Firebase представляет нам это в действительно отличном пакете вкусностей.
Надеюсь, вы кое-что узнали из этого огромного поста. Вы можете найти полный исходный код по адресу https://github.com/Devalo/Firebase-auth-react-express-mongodb
До следующего раза
Стефан Баккелунд Валуа