Давайте создадим приложение 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
До следующего раза
Стефан Баккелунд Валуа