Как создать TodoApp с помощью ReactJS и Firebase

Привет, ребята, добро пожаловать в этот урок. Прежде чем мы начнем, вы должны быть знакомы с основными концепциями ReactJS. Если нет, я бы порекомендовал вам ознакомиться с документацией ReactJS.

В этом приложении мы будем использовать следующие компоненты:

  1. ReactJS
  2. Материал UI
  3. Firebase
  4. ExpressJS
  5. Почтальон

Как будет выглядеть наше приложение:

Архитектура приложения:

Понимание наших компонентов:

Вам может быть интересно, почему мы используем firebase в этом приложении. Что ж, он обеспечивает безопасную аутентификацию , базу данных в реальном времени , бессерверный компонент и хранилище .

Мы используем здесь Express, поэтому нам не нужно обрабатывать исключения HTTP. Мы собираемся использовать все пакеты firebase в нашем компоненте функций. Это потому, что мы не хотим делать наше клиентское приложение слишком большим, что может замедлить процесс загрузки пользовательского интерфейса.

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

Раздел 1. Разработка API-интерфейсов Todo

В этомраздел , мы собираемся разработать эти элементы:

  1. Настройте функции firebase.
  2. Установите платформу Express и создайте API Todo.
  3. Настройка firestore как базы данных.

Код API Todo, реализованный в этом разделе, можно найти в этом коммите.

Настройте функции Firebase:

Зайдите в консоль Firebase.

Выберите опцию Добавить проект . После этого следуйте приведенному ниже гифке, шаг за шагом, чтобы настроить проект firebase.

Перейдите на вкладку функций и нажмите кнопку Приступить к работе :

Вы увидите диалоговое окно с инструкциями по настройке функций Firebase . Пойдите в вашу местную среду. Откройте инструмент командной строки. Чтобы установить инструменты firebase на свой компьютер, используйте команду ниже:

 npm install -g firebase-tools

Как только это будет сделано, используйте команду firebase initдля настройки функций firebase в вашей локальной среде. Выберите следующие параметры при инициализации функции firebase в локальной среде:

  1. Какие функции Firebase CLI вы хотите настроить для этой папки? Нажмите пробел, чтобы выбрать функции, затем нажмите Enter, чтобы подтвердить свой выбор => Функции: настройка и развертывание облачных функций
  2. Во-первых, давайте свяжем этот каталог проекта с проектом Firebase…. => Использовать существующий проект
  3. Выберите проект Firebase по умолчанию для этого каталога => имя_приложения
  4. На каком языке вы хотите писать облачные функции? => JavaScript
  5. Вы хотите использовать ESLint для обнаружения возможных ошибок и обеспечения соблюдения стиля? => N
  6. Вы хотите установить зависимости с npm сейчас? (Д / п) => Д

После завершения настройки вы получите следующее сообщение:

✔ Firebase initialization complete!

Это будет наша структура каталогов после завершения инициализации:

+-- firebase.json +-- functions | +-- index.js | +-- node_modules | +-- package-lock.json | +-- package.json

Теперь откройте index.jsкаталог с функциями и скопируйте и вставьте следующий код:

const functions = require('firebase-functions'); exports.helloWorld = functions.https.onRequest((request, response) => { response.send("Hello from Firebase!"); });

Разверните код для функций firebase с помощью следующей команды:

firebase deploy

После завершения развертывания вы получите следующую строку журнала в конце командной строки:

> ✔ Deploy complete! > Project Console: //console.firebase.google.com/project/todoapp-/overview

Перейдите в Консоль проекта> Функции и там вы найдете URL-адрес API. URL-адрес будет выглядеть так:

//-todoapp-.cloudfunctions.net/helloWorld

Скопируйте этот URL-адрес и вставьте его в браузер. Вы получите следующий ответ:

Hello from Firebase!

Это подтверждает, что наша функция Firebase настроена правильно.

Установите Express Framework:

Теперь давайте установим Expressфреймворк в наш проект с помощью следующей команды:

npm i express

Теперь давайте создадим каталог API внутри каталога функций . Внутри этого каталога мы создадим файл с именем todos.js. Удалите все из файла, index.jsа затем скопируйте и вставьте следующий код:

//index.js const functions = require('firebase-functions'); const app = require('express')(); const { getAllTodos } = require('./APIs/todos') app.get('/todos', getAllTodos); exports.api = functions.https.onRequest(app);

Мы назначили функцию getAllTodos маршруту / todos . Таким образом, все вызовы API на этом маршруте будут выполняться через функцию getAllTodos. Теперь перейдите к todos.jsфайлу в каталоге API, и здесь мы напишем функцию getAllTodos.

//todos.js exports.getAllTodos = (request, response) => { todos = [ { 'id': '1', 'title': 'greeting', 'body': 'Hello world from sharvin shah' }, { 'id': '2', 'title': 'greeting2', 'body': 'Hello2 world2 from sharvin shah' } ] return response.json(todos); }

Здесь мы объявили образец объекта JSON. Позже мы извлечем это из Firestore. Но пока мы вернем это. Теперь разверните это в своей функции firebase с помощью команды firebase deploy. Он спроситдля разрешения на удаление модуля helloworld - просто введите y .

The following functions are found in your project but do not exist in your local source code: helloWorld Would you like to proceed with deletion? Selecting no will continue the rest of the deployments. (y/N) y

Как только это будет сделано, перейдите в Консоль проекта> Функции и там вы найдете URL-адрес API. API будет выглядеть так:

//-todoapp-.cloudfunctions.net/api

Теперь перейдите в браузер, скопируйте и вставьте URL-адрес и добавьте / todos в конце этого URL-адреса. Вы получите следующий результат:

[ { 'id': '1', 'title': 'greeting', 'body': 'Hello world from sharvin shah' }, { 'id': '2', 'title': 'greeting2', 'body': 'Hello2 world2 from sharvin shah' } ]

Firebase Firestore:

Мы будем использовать firebase firestore в качестве базы данных в реальном времени для нашего приложения. Теперь перейдите в Консоль> База данных в консоли Firebase. Чтобы настроить firestore, следуйте гифке ниже:

После завершения настройки нажмите кнопку « Начать сбор» и установите идентификатор коллекции как задачи . Нажмите Далее, и вы получите следующее всплывающее окно:

Игнорируйте ключ DocumentID. Информацию о поле, типе и значении см. В JSON ниже. Соответственно обновите значение:

{ Field: title, Type: String, Value: Hello World }, { Field: body, Type: String, Value: Hello folks I hope you are staying home... }, { Field: createtAt, type: timestamp, value: Add the current date and time here }

Нажмите кнопку сохранения. Вы увидите, что коллекция и документ созданы. Вернитесь в местную среду. Нам нужно установить, в firebase-adminкотором есть нужный нам пакет firestore. Используйте эту команду для его установки:

npm i firebase-admin

Создайте каталог с именем util в каталоге функций .Перейдите в этот каталог и создайте имя файла admin.js. В этом файле мы импортируем административный пакет firebase и инициализируем объект базы данных firestore. Мы экспортируем это, чтобы другие модули могли его использовать.

//admin.js const admin = require('firebase-admin'); admin.initializeApp(); const db = admin.firestore(); module.exports = { admin, db };

Теперь давайте напишем API для получения этих данных. Перейти к todos.jsпод функции> API , каталог. Удалите старый код и скопируйте и вставьте код ниже:

//todos.js const { db } = require('../util/admin'); exports.getAllTodos = (request, response) => { db .collection('todos') .orderBy('createdAt', 'desc') .get() .then((data) => { let todos = []; data.forEach((doc) => { todos.push({ todoId: doc.id, title: doc.data().title, body: doc.data().body, createdAt: doc.data().createdAt, }); }); return response.json(todos); }) .catch((err) => { console.error(err); return response.status(500).json({ error: err.code}); }); };

Здесь мы получаем все задачи из базы данных и пересылаем их клиенту в виде списка.

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

  1. Перейдите в настройки проекта (значок настроек вверху слева).
  2. Перейдите на вкладку сервисных аккаунтов  
  3. Внизу будет возможность сгенерировать новый ключ . Нажмите на эту опцию, и он загрузит файл с расширением JSON.
  4. Нам нужно экспортировать эти учетные данные в сеанс командной строки. Для этого используйте команду ниже:
export GOOGLE_APPLICATION_CREDENTIALS="/home/user/Downloads/[FILE_NAME].json"

После этого запустите команду firebase serve. Если вы все еще получаете сообщение об ошибке , то используйте следующую команду: firebase login --reauth. В браузере откроется страница входа в Google. После входа в систему он будет работать без ошибок.

Вы найдете URL-адрес в журналах вашего инструмента командной строки, когда вы запустите команду firebase serve. Откройте этот URL-адрес в браузере и добавьте /todosпосле него.

✔ functions[api]: http function initialized (//localhost:5000/todoapp-//api).

В браузере вы получите следующий вывод JSON:

[ { "todoId":"W67t1kSMO0lqvjCIGiuI", "title":"Hello World", "body":"Hello folks I hope you are staying home...", "createdAt":{"_seconds":1585420200,"_nanoseconds":0 } } ]

Написание других API:

Пришло время написать все остальные API-интерфейсы задач, которые нам понадобятся для нашего приложения.

  1. Создать элемент Todo: перейдите в index.jsкаталог функций. Импортируйте метод postOneTodo в существующий getAllTodos. Кроме того, назначьте этому методу маршрут POST.
//index.js const { .., postOneTodo } = require('./APIs/todos') app.post('/todo', postOneTodo);

Перейдите в todos.jsкаталог функций и добавьте новый метод postOneTodoпод существующий getAllTodos.

//todos.js exports.postOneTodo = (request, response) => { if (request.body.body.trim() === '') { return response.status(400).json({ body: 'Must not be empty' }); } if(request.body.title.trim() === '') { return response.status(400).json({ title: 'Must not be empty' }); } const newTodoItem = { title: request.body.title, body: request.body.body, createdAt: new Date().toISOString() } db .collection('todos') .add(newTodoItem) .then((doc)=>{ const responseTodoItem = newTodoItem; responseTodoItem.id = doc.id; return response.json(responseTodoItem); }) .catch((err) => { response.status(500).json({ error: 'Something went wrong' }); console.error(err); }); };

В этом методе мы добавляем новый Todo в нашу базу данных. Если элементы нашего тела пусты, мы вернем ответ 400, иначе мы добавим данные.

Запустите команду firebase serve и откройте приложение почтальон. Создайте новый запрос и выберите тип метода POST . Добавьте URL-адрес и текст JSON.

URL: //localhost:5000/todoapp-//api/todo METHOD: POST Body: { "title":"Hello World", "body": "We are writing this awesome API" }

Нажмите кнопку отправки, и вы получите следующий ответ:

{ "title": "Hello World", "body": "We are writing this awesome API", "createdAt": "2020-03-29T12:30:48.809Z", "id": "nh41IgARCj8LPWBYzjU0" }

2. Удалить элемент Todo: перейдите в index.jsкаталог функций. Импортируйте метод deleteTodo в существующий файл postOneTodo. Кроме того, назначьте этому методу маршрут DELETE.

//index.js const { .., deleteTodo } = require('./APIs/todos') app.delete('/todo/:todoId', deleteTodo);

Перейдите к todos.jsи добавьте новый метод deleteTodoпод существующий postOneTodoметод.

//todos.js exports.deleteTodo = (request, response) => { const document = db.doc(`/todos/${request.params.todoId}`); document .get() .then((doc) => { if (!doc.exists) { return response.status(404).json({ error: 'Todo not found' }) } return document.delete(); }) .then(() => { response.json({ message: 'Delete successfull' }); }) .catch((err) => { console.error(err); return response.status(500).json({ error: err.code }); }); };

В этом методе мы удаляем Todo из нашей базы данных. Запустите команду firebase serve и перейдите к почтальону. Создайте новый запрос, выберите тип метода как УДАЛИТЬ и добавьте URL-адрес.

URL: //localhost:5000/todoapp-//api/todo/ METHOD: DELETE

Нажмите кнопку отправки, и вы получите следующий ответ:

{ "message": "Delete successfull" }

3. Отредактируйте элемент Todo: перейдите в index.jsкаталог функций. Импортируйте метод editTodo в существующий deleteTodo. Также назначьте этому методу маршрут PUT.

//index.js const { .., editTodo } = require('./APIs/todos') app.put('/todo/:todoId', editTodo);

Перейдите к todos.jsи добавьте новый метод editTodoпод существующий deleteTodoметод.

//todos.js exports.editTodo = ( request, response ) => { if(request.body.todoId || request.body.createdAt){ response.status(403).json({message: 'Not allowed to edit'}); } let document = db.collection('todos').doc(`${request.params.todoId}`); document.update(request.body) .then(()=> { response.json({message: 'Updated successfully'}); }) .catch((err) => { console.error(err); return response.status(500).json({ error: err.code }); }); };

В этом методе мы редактируем Todo из нашей базы данных. Помните, что здесь мы не позволяем пользователю редактировать поля todoId или createdAt. Запустите команду firebase serve и перейдите к почтальону. Создайте новый запрос, выберите тип метода как PUT и добавьте URL-адрес.

URL: //localhost:5000/todoapp-//api/todo/ METHOD: PUT

Нажмите кнопку отправки, и вы получите следующий ответ:

{ "message": "Updated successfully" }

Структура каталогов до сих пор:

+-- firebase.json +-- functions | +-- API | +-- +-- todos.js | +-- util | +-- +-- admin.js | +-- index.js | +-- node_modules | +-- package-lock.json | +-- package.json | +-- .gitignore

На этом мы завершили первый раздел заявки. Вы можете пойти выпить кофе, сделать перерыв, и после этого мы займемся разработкой пользовательских API.

Раздел 2: Разработка пользовательских API

В этомраздел , мы собираемся разработать следующие компоненты:

  1. API аутентификации пользователя (вход и регистрация).
  2. Получить и обновить API сведений о пользователе.
  3. Обновите API изображения профиля пользователя.
  4. Обеспечение безопасности существующего API Todo.

Код пользовательского API, реализованный в этом разделе, можно найти в этом коммите.

Итак, приступим к созданию API аутентификации пользователей. Перейдите в консоль Firebase> Аутентификация.

Нажмите кнопку « Настроить способ входа» . Мы будем использовать адрес электронной почты и пароль для проверки пользователя. Включите опцию Электронная почта / Пароль .

Прямо сейчас мы вручную создадим нашего пользователя. Сначала мы создадим API входа в систему. После этого мы создадим Sign-Up API.

Перейдите на вкладку «Пользователи» в разделе «Аутентификация», введите сведения о пользователе и нажмите кнопку « Добавить пользователя» .

1. API входа пользователя:

Во-первых, нам нужно установить firebaseпакет, который состоит из библиотеки Firebase Authentication, используя следующую команду:

npm i firebase

После завершения установки перейдите в каталог functions> APIs . Здесь мы создадим users.jsфайл. Теперь внутри index.jsмы импортируем метод loginUser и назначаем ему маршрут POST.

//index.js const { loginUser } = require('./APIs/users') // Users app.post('/login', loginUser);

Перейдите в Настройки проекта> Общие и там вы найдете следующую карточку:

Выберите значок Интернета, а затем следуйте рисунку ниже:

Выберите вариант « продолжить в консоль» . Как только это будет сделано, вы увидите JSON с конфигурацией firebase. Перейдите в каталог functions> util и создайте   config.jsфайл. Скопируйте и вставьте в этот файл следующий код:

// config.js module.exports = { apiKey: "............", authDomain: "........", databaseURL: "........", projectId: ".......", storageBucket: ".......", messagingSenderId: "........", appId: "..........", measurementId: "......." };

Замените ............значениями, которые вы получаете в консоли Firebase> Настройки проекта> Общие> ваши приложения> Фрагмент Firebase SD> config .

Скопируйте и вставьте в users.jsфайл следующий код :

// users.js const { admin, db } = require('../util/admin'); const config = require('../util/config'); const firebase = require('firebase'); firebase.initializeApp(config); const { validateLoginData, validateSignUpData } = require('../util/validators'); // Login exports.loginUser = (request, response) => { const user = { email: request.body.email, password: request.body.password } const { valid, errors } = validateLoginData(user); if (!valid) return response.status(400).json(errors); firebase .auth() .signInWithEmailAndPassword(user.email, user.password) .then((data) => { return data.user.getIdToken(); }) .then((token) => { return response.json({ token }); }) .catch((error) => { console.error(error); return response.status(403).json({ general: 'wrong credentials, please try again'}); }) };

Здесь мы используем модуль firebase signInWithEmailAndPassword, чтобы проверить правильность отправленных пользователем учетных данных. Если они правы, мы отправляем токен этого пользователя или статус 403 с сообщением «неправильные учетные данные».

Теперь давайте создадим validators.jsв каталоге functions> util . Скопируйте и вставьте в этот файл следующий код:

// validators.js const isEmpty = (string) => { if (string.trim() === '') return true; else return false; }; exports.validateLoginData = (data) => { let errors = {}; if (isEmpty(data.email)) errors.email = 'Must not be empty'; if (isEmpty(data.password)) errors.password = 'Must not be empty'; return { errors, valid: Object.keys(errors).length === 0 ? true : false }; };

На этом наш LoginAPI завершен. Запускаем firebase serveкоманду и идем к почтальону. Создайте новый запрос, выберите тип метода POST и добавьте URL-адрес и тело.

URL: //localhost:5000/todoapp-//api/login METHOD: POST Body: { "email":"Add email that is assigned for user in console", "password": "Add password that is assigned for user in console" }

Нажмите кнопку отправки запроса в почтальоне, и вы получите следующий результат:

{ "token": ".........." }

Мы будем использовать этот токен в следующей части, чтобы получить сведения о пользователе . Помните, что этот токен истекает через 60 минут . Чтобы сгенерировать новый токен, снова используйте этот API.

2. API регистрации пользователей:

Механизм аутентификации по умолчанию firebase позволяет хранить только такую ​​информацию, как электронная почта, пароль и т. Д. Но нам нужна дополнительная информация, чтобы определить, владеет ли этот пользователь этим делом, чтобы он мог выполнять операции чтения, обновления и удаления с ним.

Для достижения этой цели мы собираемся создать новую коллекцию под названием users . В этой коллекции мы будем хранить данные пользователя, которые будут сопоставлены с задачей на основе имени пользователя. Каждое имя пользователя будет уникальным для всех пользователей платформы.

Перейти к index.js. Мы импортируем метод signUpUser и назначаем ему маршрут POST.

//index.js const { .., signUpUser } = require('./APIs/users') app.post('/signup', signUpUser);

Теперь перейдите к методу validators.jsи добавьте следующий код под validateLoginDataметодом.

// validators.js const isEmail = (email) => { const emailRegEx = /^(([^()\[\]\\.,;:\[email protected]"]+(\.[^()\[\]\\.,;:\[email protected]"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; if (email.match(emailRegEx)) return true; else return false; }; exports.validateSignUpData = (data) => { let errors = {}; if (isEmpty(data.email)) { errors.email = 'Must not be empty'; } else if (!isEmail(data.email)) { errors.email = 'Must be valid email address'; } if (isEmpty(data.firstName)) errors.firstName = 'Must not be empty'; if (isEmpty(data.lastName)) errors.lastName = 'Must not be empty'; if (isEmpty(data.phoneNumber)) errors.phoneNumber = 'Must not be empty'; if (isEmpty(data.country)) errors.country = 'Must not be empty'; if (isEmpty(data.password)) errors.password = 'Must not be empty'; if (data.password !== data.confirmPassword) errors.confirmPassword = 'Passowrds must be the same'; if (isEmpty(data.username)) errors.username = 'Must not be empty'; return { errors, valid: Object.keys(errors).length === 0 ? true : false }; };

Теперь перейдите к users.jsи добавьте следующий код под loginUserмодулем.

// users.js exports.signUpUser = (request, response) => { const newUser = { firstName: request.body.firstName, lastName: request.body.lastName, email: request.body.email, phoneNumber: request.body.phoneNumber, country: request.body.country, password: request.body.password, confirmPassword: request.body.confirmPassword, username: request.body.username }; const { valid, errors } = validateSignUpData(newUser); if (!valid) return response.status(400).json(errors); let token, userId; db .doc(`/users/${newUser.username}`) .get() .then((doc) => { if (doc.exists) { return response.status(400).json({ username: 'this username is already taken' }); } else { return firebase .auth() .createUserWithEmailAndPassword( newUser.email, newUser.password ); } }) .then((data) => { userId = data.user.uid; return data.user.getIdToken(); }) .then((idtoken) => { token = idtoken; const userCredentials = { firstName: newUser.firstName, lastName: newUser.lastName, username: newUser.username, phoneNumber: newUser.phoneNumber, country: newUser.country, email: newUser.email, createdAt: new Date().toISOString(), userId }; return db .doc(`/users/${newUser.username}`) .set(userCredentials); }) .then(()=>{ return response.status(201).json({ token }); }) .catch((err) => { console.error(err); if (err.code === 'auth/email-already-in-use') { return response.status(400).json({ email: 'Email already in use' }); } else { return response.status(500).json({ general: 'Something went wrong, please try again' }); } }); }

Мы проверяем наши пользовательские данные, после чего отправляем электронное письмо и пароль в модуль firebase createUserWithEmailAndPassword для создания пользователя. После успешного создания пользователя мы сохраняем учетные данные пользователя в базе данных.

На этом наш SignUp API завершен. Запускаем firebase serveкоманду и идем к почтальону. Создайте новый запрос, выберите тип метода POST . Добавьте URL и тело.

URL: //localhost:5000/todoapp-//api/signup METHOD: POST Body: { "firstName": "Add a firstName here", "lastName": "Add a lastName here", "email":"Add a email here", "phoneNumber": "Add a phone number here", "country": "Add a country here", "password": "Add a password here", "confirmPassword": "Add same password here", "username": "Add unique username here" }

Нажмите кнопку отправки запроса в почтальоне, и вы получите следующий результат:

{ "token": ".........." }

Теперь перейдите в консоль Firebase> База данных, и вы увидите следующий результат:

Как видите, коллекция нашего пользователя успешно создана с одним документом в ней.

3. Загрузите изображение профиля пользователя:

Наши пользователи смогут загрузить свою аватарку. Для этого мы будем использовать Storage bucket. Перейти к Firebase консоли> Хранение и нажмите на НАЧАТЬ РАБОТУ кнопку. Следуйте приведенному ниже GIF-изображению для настройки:

Теперь перейдите на вкладку « Правила » в разделе «Хранилище» и обновите разрешение для доступа к корзине, как показано на изображении ниже:

Для загрузки изображения профиля мы будем использовать пакет с именем busboy. Чтобы установить этот пакет, используйте следующую команду:

npm i busboy

Перейти к index.js. Импортируйте метод uploadProfilePhoto под существующим методом signUpUser. Также назначьте этому методу маршрут POST.

//index.js const auth = require('./util/auth'); const { .., uploadProfilePhoto } = require('./APIs/users') app.post('/user/image', auth, uploadProfilePhoto);

Здесь мы добавили уровень аутентификации, чтобы только пользователь, связанный с этой учетной записью, мог загрузить изображение. Теперь создайте файл с именем auth.jsв каталоге functions> utils . Скопируйте и вставьте в этот файл следующий код:

// auth.js const { admin, db } = require('./admin'); module.exports = (request, response, next) => { let idToken; if (request.headers.authorization && request.headers.authorization.startsWith('Bearer ')) { idToken = request.headers.authorization.split('Bearer ')[1]; } else { console.error('No token found'); return response.status(403).json({ error: 'Unauthorized' }); } admin .auth() .verifyIdToken(idToken) .then((decodedToken) => { request.user = decodedToken; return db.collection('users').where('userId', '==', request.user.uid).limit(1).get(); }) .then((data) => { request.user.username = data.docs[0].data().username; request.user.imageUrl = data.docs[0].data().imageUrl; return next(); }) .catch((err) => { console.error('Error while verifying token', err); return response.status(403).json(err); }); };

Здесь мы используем модуль firebase verifyIdToken для проверки токена. После этого мы расшифровываем данные пользователя и передаем их в существующий запрос.

Перейдите к методу users.jsи добавьте следующий код под signupметодом:

// users.js deleteImage = (imageName) => { const bucket = admin.storage().bucket(); const path = `${imageName}` return bucket.file(path).delete() .then(() => { return }) .catch((error) => { return }) } // Upload profile picture exports.uploadProfilePhoto = (request, response) => { const BusBoy = require('busboy'); const path = require('path'); const os = require('os'); const fs = require('fs'); const busboy = new BusBoy({ headers: request.headers }); let imageFileName; let imageToBeUploaded = {}; busboy.on('file', (fieldname, file, filename, encoding, mimetype) => { if (mimetype !== 'image/png' && mimetype !== 'image/jpeg') { return response.status(400).json({ error: 'Wrong file type submited' }); } const imageExtension = filename.split('.')[filename.split('.').length - 1]; imageFileName = `${request.user.username}.${imageExtension}`; const filePath = path.join(os.tmpdir(), imageFileName); imageToBeUploaded = { filePath, mimetype }; file.pipe(fs.createWriteStream(filePath)); }); deleteImage(imageFileName); busboy.on('finish', () => { admin .storage() .bucket() .upload(imageToBeUploaded.filePath, { resumable: false, metadata: { metadata: { contentType: imageToBeUploaded.mimetype } } }) .then(() => { const imageUrl = `//firebasestorage.googleapis.com/v0/b/${config.storageBucket}/o/${imageFileName}?alt=media`; return db.doc(`/users/${request.user.username}`).update({ imageUrl }); }) .then(() => { return response.json({ message: 'Image uploaded successfully' }); }) .catch((error) => { console.error(error); return response.status(500).json({ error: error.code }); }); }); busboy.end(request.rawBody); };

На этом наш API для загрузки изображений профиля завершен. Запускаем firebase serveкоманду и идем к почтальону. Создайте новый запрос, выберите тип метода как POST , добавьте URL-адрес и в разделе body выберите тип как form-data.

Запрос защищен, поэтому вам также необходимо отправить токен на предъявителя . Чтобы отправить токен на предъявителя, войдите в систему еще раз, если срок действия токена истек. После этого в приложении «Почтальон»> вкладка «Авторизация»> «Тип»> «Токен на предъявителя» и в разделе токенов вставьте токен.

URL: //localhost:5000/todoapp-//api/user/image METHOD: GET Body: { REFER THE IMAGE down below }

Нажмите кнопку отправки запроса в почтальоне, и вы получите следующий результат:

{ "message": "Image uploaded successfully" }

4. Получите сведения о пользователе:

Здесь мы получаем данные нашего пользователя из базы данных. Перейдите в index.jsи импортируйте метод getUserDetail и назначьте ему маршрут GET.

// index.js const { .., getUserDetail } = require('./APIs/users') app.get('/user', auth, getUserDetail);

Теперь перейдите к users.jsи добавьте следующий код после uploadProfilePhotoмодуля:

// users.js exports.getUserDetail = (request, response) => { let userData = {}; db .doc(`/users/${request.user.username}`) .get() .then((doc) => { if (doc.exists) { userData.userCredentials = doc.data(); return response.json(userData); } }) .catch((error) => { console.error(error); return response.status(500).json({ error: error.code }); }); }

Мы используем модуль firebase doc (). Get () для получения сведений о пользователе. На этом наш GET User Details API завершен. Запускаем firebase serveкоманду и идем к почтальону. Создайте новый запрос, выберите тип метода: GET и добавьте URL-адрес и тело.

Запрос защищен, поэтому вам также необходимо отправить токен на предъявителя . Чтобы отправить токен на предъявителя, войдите в систему еще раз, если срок действия токена истек.

URL: //localhost:5000/todoapp-//api/user METHOD: GET

Нажмите кнопку отправки запроса в почтальоне, и вы получите следующий результат:

{ "userCredentials": { "phoneNumber": "........", "email": "........", "country": "........", "userId": "........", "username": "........", "createdAt": "........", "lastName": "........", "firstName": "........" } }

5. Обновите данные пользователя:

Теперь добавим функциональность для обновления сведений о пользователе. Перейдите к index.jsи скопируйте и вставьте следующий код:

// index.js const { .., updateUserDetails } = require('./APIs/users') app.post('/user', auth, updateUserDetails);

Теперь перейдите в users.jsи добавьте updateUserDetailsмодуль под существующим getUserDetails:

// users.js exports.updateUserDetails = (request, response) => { let document = db.collection('users').doc(`${request.user.username}`); document.update(request.body) .then(()=> { response.json({message: 'Updated successfully'}); }) .catch((error) => { console.error(error); return response.status(500).json({ message: "Cannot Update the value" }); }); }

Здесь мы используем метод обновления firebase . На этом наш API обновлений сведений о пользователе завершен. Выполните ту же процедуру для запроса, что и для API получения сведений о пользователе выше, с одним изменением. Добавьте сюда тело запроса и укажите метод POST.

URL: //localhost:5000/todoapp-//api/user METHOD: POST Body : { // You can edit First Name, last Name and country // We will disable other Form Tags from our UI }

Нажмите кнопку отправки запроса в почтальоне, и вы получите следующий результат:

{ "message": "Updated successfully" }

6. Защита API Todo:

Чтобы защитить API Todo, чтобы только выбранный пользователь мог получить к нему доступ, мы внесем несколько изменений в наш существующий код. Во-первых, мы обновим наш index.jsследующим образом:

// index.js // Todos app.get('/todos', auth, getAllTodos); app.get('/todo/:todoId', auth, getOneTodo); app.post('/todo',auth, postOneTodo); app.delete('/todo/:todoId',auth, deleteTodo); app.put('/todo/:todoId',auth, editTodo);

Мы обновили все маршруты Todo , добавив authтак, чтобы все вызовы API требовали токена и могли быть доступны только конкретному пользователю.

После этого перейдите в todos.jsрамках функции> API , каталог.

  1. Создайте API Todo: откройте todos.jsи под методом postOneTodo добавьте ключ имени пользователя следующим образом:
const newTodoItem = { .., username: request.user.username, .. }

2. ПОЛУЧИТЬ All Todos API: откройте todos.jsи под методом getAllTodos добавьте предложение where следующим образом:

db .collection('todos') .where('username', '==', request.user.username) .orderBy('createdAt', 'desc')

Запустите службу firebase и протестируйте наш GET API. Не забудьте отправить жетон на предъявителя. Здесь вы получите следующую ошибку ответа:

{ "error": 9 }

Перейдите в командную строку, и вы увидите следующие записанные строки:

i functions: Beginning execution of "api"> Error: 9 FAILED_PRECONDITION: The query requires an index. You can create it here: > at callErrorFromStatus

Откройте это в браузере и нажмите «Создать индекс».

Как только индекс будет построен, отправьте запрос еще раз, и вы получите следующий результат:

[ { "todoId": "......", "title": "......", "username": "......", "body": "......", "createdAt": "2020-03-30T13:01:58.478Z" } ]

3.   Удалите Todo API: откройте todos.jsи под методом deleteTodo добавьте следующее условие. Добавьте это условие в запрос document.get (). Then () под условием ! Doc.exists .

.. if(doc.data().username !== request.user.username){ return response.status(403).json({error:"UnAuthorized"}) }

Структура каталогов до настоящего момента:

+-- firebase.json +-- functions | +-- API | +-- +-- todos.js | +-- +-- users.js | +-- util | +-- +-- admin.js | +-- +-- auth.js | +-- +-- validators.js | +-- index.js | +-- node_modules | +-- package-lock.json | +-- package.json | +-- .gitignore

На этом мы завершили наш бэкэнд API. Сделаем перерыв, выпьем кофе, и после этого мы приступим к созданию внешнего интерфейса нашего приложения.

Раздел 3: Панель управления пользователя

В этомраздел , мы собираемся разработать следующие компоненты:

  1. Настройте ReactJS и Material UI.
  2. Создание формы входа и регистрации.
  3. Секция создания счетов.

Код пользовательской панели инструментов, реализованный в этом разделе, можно найти в этом коммите.

1. Настройте ReactJS и Material UI:

Мы будем использовать шаблон create-react-app. Это дает нам фундаментальную структуру для разработки приложения. Чтобы установить его, используйте следующую команду:

npm install -g create-react-app

Перейдите в корневую папку проекта, где находится каталог функций. Инициализируем наше внешнее приложение с помощью следующей команды:

create-react-app view

Не забудьте использовать версию v16.13.1 избиблиотека ReactJS .

После завершения установки в журналах командной строки вы увидите следующее:

cd view npm start Happy hacking!

На этом мы настроили наше приложение React. Вы получите следующую структуру каталогов:

+-- firebase.json +-- functions { This Directory consists our API logic } +-- view { This Directory consists our FrontEnd Compoenents } +-- .firebaserc +-- .gitignore

Теперь запустите приложение с помощью команды npm start. Зайдите в браузер, //localhost:3000/и вы увидите следующий результат:

Теперь удалим все ненужные компоненты. Перейдите в каталог просмотра и затем удалите все файлыперед которыми стоит [Remove] . Для этого обратитесь к древовидной структуре каталогов ниже.

+-- README.md [ Remove ] +-- package-lock.json +-- package.json +-- node_modules +-- .gitignore +-- public | +-- favicon.ico [ Remove ] | +-- index.html | +-- logo192.png [ Remove ] | +-- logo512.png [ Remove ] | +-- manifest.json | +-- robots.txt +-- src | +-- App.css | +-- App.test.js | +-- index.js | +-- serviceWorker.js | +-- App.js | +-- index.css [ Remove ] | +-- logo.svg [ Remove ] | +-- setupTests.js

Перейдите index.htmlв общий каталог и удалите следующие строки:

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

import React from 'react'; function App() { return ( ); } export default App;

Перейдите в index.jsи удалите следующий импорт:

import './index.css'

Я не удалял и App.cssне использую его в этом приложении. Но если вы хотите удалить или использовать его, вы можете это сделать.

Зайдите в браузер, //localhost:3000/и вы получите пустой экран.

Чтобы установить Material UI, перейдите в каталог просмотра и скопируйте и вставьте эту команду в терминал:

npm install @material-ui/core

Не забудьте использовать версию v4.9.8 библиотеки Material UI.

2. Форма входа:

Чтобы разработать форму входа, перейдите по ссылке App.js. Вверху App.jsдобавьте следующий импорт:

import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; import login from './pages/login';

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

// App.js 

Создайте каталог страниц в существующем каталоге просмотра и файл с именем login.jsв каталоге страниц .

Мы импортируем компоненты пользовательского интерфейса материала и пакет Axios в login.js:

// login.js // Material UI components import React, { Component } from 'react'; import Avatar from '@material-ui/core/Avatar'; import Button from '@material-ui/core/Button'; import CssBaseline from '@material-ui/core/CssBaseline'; import TextField from '@material-ui/core/TextField'; import Link from '@material-ui/core/Link'; import Grid from '@material-ui/core/Grid'; import LockOutlinedIcon from '@material-ui/icons/LockOutlined'; import Typography from '@material-ui/core/Typography'; import withStyles from '@material-ui/core/styles/withStyles'; import Container from '@material-ui/core/Container'; import CircularProgress from '@material-ui/core/CircularProgress'; import axios from 'axios';

Мы добавим следующие стили на нашу страницу входа:

// login.js const styles = (theme) => ({ paper: { marginTop: theme.spacing(8), display: 'flex', flexDirection: 'column', alignItems: 'center' }, avatar: { margin: theme.spacing(1), backgroundColor: theme.palette.secondary.main }, form: { width: '100%', marginTop: theme.spacing(1) }, submit: { margin: theme.spacing(3, 0, 2) }, customError: { color: 'red', fontSize: '0.8rem', marginTop: 10 }, progess: { position: 'absolute' } });

Мы создадим класс с именем login, который имеет форму и обработчик отправки внутри него.

// login.js class login extends Component { constructor(props) { super(props); this.state = { email: '', password: '', errors: [], loading: false }; } componentWillReceiveProps(nextProps) { if (nextProps.UI.errors) { this.setState({ errors: nextProps.UI.errors }); } } handleChange = (event) => { this.setState({ [event.target.name]: event.target.value }); }; handleSubmit = (event) => { event.preventDefault(); this.setState({ loading: true }); const userData = { email: this.state.email, password: this.state.password }; axios .post('/login', userData) .then((response) => { localStorage.setItem('AuthToken', `Bearer ${response.data.token}`); this.setState({ loading: false, }); this.props.history.push('/'); }) .catch((error) => { this.setState({ errors: error.response.data, loading: false }); }); }; render() { const { classes } = this.props; const { errors, loading } = this.state; return ( Login      Sign In {loading && }     {"Don't have an account? Sign Up"}    {errors.general && (  {errors.general}  )} ); } }

В конце этого файла добавьте следующий экспорт:

export default withStyles(styles)(login); 

Добавьте URL-адрес наших функций firebase для просмотра> package.json следующим образом:

Помните: добавьте ключ с именем прокси под существующим объектом JSON списка браузеров.
"proxy": "//-todoapp-.cloudfunctions.net/api"

Установите пакет значков Axios и материалов , используя следующие команды:

// Axios command: npm i axios // Material Icons: npm install @material-ui/icons

Мы добавили маршрут входа в систему App.js. В нем login.jsмы создали компонент класса, который обрабатывает состояние, отправляет почтовый запрос в API входа в систему с помощью пакета Axios. Если запрос выполнен успешно, мы сохраняем токен. Если мы получаем ошибки в ответе, мы просто отображаем их в пользовательском интерфейсе.

Перейдите в браузер по адресу, //localhost:3000/loginи вы увидите следующий пользовательский интерфейс входа.

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

3. Форма регистрации:

Чтобы разработать форму регистрации, перейдите к App.jsсуществующему Routeкомпоненту и обновите его, используя следующую строку:

// App.js 

Не забудьте импортировать:

// App.js import signup from './pages/signup';

Создайте файл с именем signup.jsв каталоге страниц .

Внутри signup.js мы импортируем пакет Material UI и Axios:

// signup.js import React, { Component } from 'react'; import Avatar from '@material-ui/core/Avatar'; import Button from '@material-ui/core/Button'; import CssBaseline from '@material-ui/core/CssBaseline'; import TextField from '@material-ui/core/TextField'; import Link from '@material-ui/core/Link'; import Grid from '@material-ui/core/Grid'; import LockOutlinedIcon from '@material-ui/icons/LockOutlined'; import Typography from '@material-ui/core/Typography'; import Container from '@material-ui/core/Container'; import withStyles from '@material-ui/core/styles/withStyles'; import CircularProgress from '@material-ui/core/CircularProgress'; import axios from 'axios';

Мы добавим на нашу страницу регистрации следующие стили:

// signup.js const styles = (theme) => ({ paper: { marginTop: theme.spacing(8), display: 'flex', flexDirection: 'column', alignItems: 'center' }, avatar: { margin: theme.spacing(1), backgroundColor: theme.palette.secondary.main }, form: { width: '100%', // Fix IE 11 issue. marginTop: theme.spacing(3) }, submit: { margin: theme.spacing(3, 0, 2) }, progess: { position: 'absolute' } }); 

Мы создадим класс с именем signup, в котором будет форма и обработчик отправки.

// signup.js class signup extends Component { constructor(props) { super(props); this.state = { firstName: '', lastName: '', phoneNumber: '', country: '', username: '', email: '', password: '', confirmPassword: '', errors: [], loading: false }; } componentWillReceiveProps(nextProps) { if (nextProps.UI.errors) { this.setState({ errors: nextProps.UI.errors }); } } handleChange = (event) => { this.setState({ [event.target.name]: event.target.value }); }; handleSubmit = (event) => { event.preventDefault(); this.setState({ loading: true }); const newUserData = { firstName: this.state.firstName, lastName: this.state.lastName, phoneNumber: this.state.phoneNumber, country: this.state.country, username: this.state.username, email: this.state.email, password: this.state.password, confirmPassword: this.state.confirmPassword }; axios .post('/signup', newUserData) .then((response) => { localStorage.setItem('AuthToken', `${response.data.token}`); this.setState({ loading: false, }); this.props.history.push('/'); }) .catch((error) => { this.setState({ errors: error.response.data, loading: false }); }); }; render() { const { classes } = this.props; const { errors, loading } = this.state; return ( Sign up                              Sign Up {loading && }     Already have an account? Sign in ); } }

В конце этого файла добавьте следующий экспорт:

export default withStyles(styles)(signup); 

Логика для компонента регистрации такая же, как и для компонента входа в систему. Зайдите в браузер по адресу, //localhost:3000/signupи вы увидите следующий интерфейс регистрации. После успешной регистрации мы вернемся на главную страницу.

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

4. Раздел учетной записи:

Чтобы создать страницу учетной записи, нам нужно сначала создать нашу домашнюю страницу, с которой мы будем загружать раздел учетной записи . Перейдите App.jsи обновите следующий маршрут:

// App.js 

Не забывайте импорт:

// App.js import home from './pages/home';

Создайте новый файл с именем home.js. Этот файл будет индексом нашего приложения. Разделы Account и Todo загружаются на эту страницу при нажатии кнопки.

Импортируйте пакеты пользовательского интерфейса материала, пакет Axios, нашу настраиваемую учетную запись, компоненты задач и промежуточное ПО для проверки подлинности.

// home.js import React, { Component } from 'react'; import axios from 'axios'; import Account from '../components/account'; import Todo from '../components/todo'; import Drawer from '@material-ui/core/Drawer'; import AppBar from '@material-ui/core/AppBar'; import CssBaseline from '@material-ui/core/CssBaseline'; import Toolbar from '@material-ui/core/Toolbar'; import List from '@material-ui/core/List'; import Typography from '@material-ui/core/Typography'; import Divider from '@material-ui/core/Divider'; import ListItem from '@material-ui/core/ListItem'; import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemText from '@material-ui/core/ListItemText'; import withStyles from '@material-ui/core/styles/withStyles'; import AccountBoxIcon from '@material-ui/icons/AccountBox'; import NotesIcon from '@material-ui/icons/Notes'; import Avatar from '@material-ui/core/avatar'; import ExitToAppIcon from '@material-ui/icons/ExitToApp'; import CircularProgress from '@material-ui/core/CircularProgress'; import { authMiddleWare } from '../util/auth'

Мы установим нашу drawerWidth следующим образом:

const drawerWidth = 240;

Мы добавим на нашу домашнюю страницу следующий стиль:

const styles = (theme) => ({ root: { display: 'flex' }, appBar: { zIndex: theme.zIndex.drawer + 1 }, drawer: { width: drawerWidth, flexShrink: 0 }, drawerPaper: { width: drawerWidth }, content: { flexGrow: 1, padding: theme.spacing(3) }, avatar: { height: 110, width: 100, flexShrink: 0, flexGrow: 0, marginTop: 20 }, uiProgess: { position: 'fixed', zIndex: '1000', height: '31px', width: '31px', left: '50%', top: '35%' }, toolbar: theme.mixins.toolbar });

Мы создадим класс с именем home. Этот класс будет иметь вызов API для получения изображения профиля пользователя, имени и фамилии. Также у него будет логика выбора, какой компонент отображать, Todo или Account:

class home extends Component { state = { render: false }; loadAccountPage = (event) => { this.setState({ render: true }); }; loadTodoPage = (event) => { this.setState({ render: false }); }; logoutHandler = (event) => { localStorage.removeItem('AuthToken'); this.props.history.push('/login'); }; constructor(props) { super(props); this.state = { firstName: '', lastName: '', profilePicture: '', uiLoading: true, imageLoading: false }; } componentWillMount = () => { authMiddleWare(this.props.history); const authToken = localStorage.getItem('AuthToken'); axios.defaults.headers.common = { Authorization: `${authToken}` }; axios .get('/user') .then((response) => { console.log(response.data); this.setState({ firstName: response.data.userCredentials.firstName, lastName: response.data.userCredentials.lastName, email: response.data.userCredentials.email, phoneNumber: response.data.userCredentials.phoneNumber, country: response.data.userCredentials.country, username: response.data.userCredentials.username, uiLoading: false, profilePicture: response.data.userCredentials.imageUrl }); }) .catch((error) => { if(error.response.status === 403) { this.props.history.push('/login') } console.log(error); this.setState({ errorMsg: 'Error in retrieving the data' }); }); }; render() { const { classes } = this.props; if (this.state.uiLoading === true) { return ( {this.state.uiLoading && } ); } else { return ( TodoApp 

{' '} {this.state.firstName} {this.state.lastName}

{' '} {' '} {' '} {' '} {' '} {' '} {this.state.render ? : } ); } } }

Здесь в коде вы увидите, что authMiddleWare(this.props.history);используется. Это промежуточное ПО проверяет, является ли authToken нулевым. Если да, то он вернет пользователя к файлу login.js. Это добавлено, чтобы наш пользователь не мог получить доступ к /маршруту без регистрации или входа в систему. В конце этого файла добавьте следующий экспорт:

export default withStyles(styles)(home); 

Теперь вам интересно, что делает этот код home.js?

 {this.state.render ?  : } 

Он проверяет состояние рендеринга, которое мы устанавливаем при нажатии кнопки. Давайте создадим каталог компонентов и в этом каталоге два файла: account.jsи todo.js.

Давайте создадим каталог с именем util и файл с именем auth.jsв этом каталоге. Скопируйте и вставьте следующий код в auth.js:

export const authMiddleWare = (history) => { const authToken = localStorage.getItem('AuthToken'); if(authToken === null){ history.push('/login') } }

На время нахождения внутри todo.jsфайла, мы просто напишем класс, который отображает текст Hello I am todo . Мы будем работать над нашими задачами в следующем разделе:

import React, { Component } from 'react' import withStyles from '@material-ui/core/styles/withStyles'; import Typography from '@material-ui/core/Typography'; const styles = ((theme) => ({ content: { flexGrow: 1, padding: theme.spacing(3), }, toolbar: theme.mixins.toolbar, }) ); class todo extends Component { render() { const { classes } = this.props; return ( Hello I am todo   ) } } export default (withStyles(styles)(todo));

Пришло время для раздела учетной записи. Импортируйте материальный интерфейс, clsx, axios и authmiddleWare в наш account.js.

// account.js import React, { Component } from 'react'; import withStyles from '@material-ui/core/styles/withStyles'; import Typography from '@material-ui/core/Typography'; import CircularProgress from '@material-ui/core/CircularProgress'; import CloudUploadIcon from '@material-ui/icons/CloudUpload'; import { Card, CardActions, CardContent, Divider, Button, Grid, TextField } from '@material-ui/core'; import clsx from 'clsx'; import axios from 'axios'; import { authMiddleWare } from '../util/auth';

Мы добавим на нашу страницу учетной записи следующий стиль:

// account.js const styles = (theme) => ({ content: { flexGrow: 1, padding: theme.spacing(3) }, toolbar: theme.mixins.toolbar, root: {}, details: { display: 'flex' }, avatar: { height: 110, width: 100, flexShrink: 0, flexGrow: 0 }, locationText: { paddingLeft: '15px' }, buttonProperty: { position: 'absolute', top: '50%' }, uiProgess: { position: 'fixed', zIndex: '1000', height: '31px', width: '31px', left: '50%', top: '35%' }, progess: { position: 'absolute' }, uploadButton: { marginLeft: '8px', margin: theme.spacing(1) }, customError: { color: 'red', fontSize: '0.8rem', marginTop: 10 }, submitButton: { marginTop: '10px' } });

Мы создадим компонент класса с именем account. Пока просто скопируйте и вставьте следующий код:

// account.js class account extends Component { constructor(props) { super(props); this.state = { firstName: '', lastName: '', email: '', phoneNumber: '', username: '', country: '', profilePicture: '', uiLoading: true, buttonLoading: false, imageError: '' }; } componentWillMount = () => { authMiddleWare(this.props.history); const authToken = localStorage.getItem('AuthToken'); axios.defaults.headers.common = { Authorization: `${authToken}` }; axios .get('/user') .then((response) => { console.log(response.data); this.setState({ firstName: response.data.userCredentials.firstName, lastName: response.data.userCredentials.lastName, email: response.data.userCredentials.email, phoneNumber: response.data.userCredentials.phoneNumber, country: response.data.userCredentials.country, username: response.data.userCredentials.username, uiLoading: false }); }) .catch((error) => { if (error.response.status === 403) { this.props.history.push('/login'); } console.log(error); this.setState({ errorMsg: 'Error in retrieving the data' }); }); }; handleChange = (event) => { this.setState({ [event.target.name]: event.target.value }); }; handleImageChange = (event) => { this.setState({ image: event.target.files[0] }); }; profilePictureHandler = (event) => { event.preventDefault(); this.setState({ uiLoading: true }); authMiddleWare(this.props.history); const authToken = localStorage.getItem('AuthToken'); let form_data = new FormData(); form_data.append('image', this.state.image); form_data.append('content', this.state.content); axios.defaults.headers.common = { Authorization: `${authToken}` }; axios .post('/user/image', form_data, { headers: { 'content-type': 'multipart/form-data' } }) .then(() => { window.location.reload(); }) .catch((error) => { if (error.response.status === 403) { this.props.history.push('/login'); } console.log(error); this.setState({ uiLoading: false, imageError: 'Error in posting the data' }); }); }; updateFormValues = (event) => { event.preventDefault(); this.setState({ buttonLoading: true }); authMiddleWare(this.props.history); const authToken = localStorage.getItem('AuthToken'); axios.defaults.headers.common = { Authorization: `${authToken}` }; const formRequest = { firstName: this.state.firstName, lastName: this.state.lastName, country: this.state.country }; axios .post('/user', formRequest) .then(() => { this.setState({ buttonLoading: false }); }) .catch((error) => { if (error.response.status === 403) { this.props.history.push('/login'); } console.log(error); this.setState({ buttonLoading: false }); }); }; render() { const { classes, ...rest } = this.props; if (this.state.uiLoading === true) { return ( {this.state.uiLoading && }  ); } else { return ( {this.state.firstName} {this.state.lastName}  

В конце этого файла добавьте следующий экспорт:

export default withStyles(styles)(account); 

В account.jsнем используется много компонентов. Сначала посмотрим, как выглядит наше приложение. После этого я объясню все компоненты, которые используются, и почему они используются.

Зайдите в браузер, и если срок действия вашего токена истек, он перенаправит вас на   loginстраницу. Добавьте свои данные и войдите снова. Как только вы это сделаете, перейдите на вкладку Учетная запись, и вы найдете следующий интерфейс:

В разделе учетной записи есть 3 обработчика:

  1. componentWillMount : это встроенный метод жизненного цикла React. Мы используем его для загрузки данных перед жизненным циклом рендеринга и обновления наших значений состояния.
  2. ProfilePictureUpdate: это наш настраиваемый обработчик, который мы используем, чтобы, когда наш пользователь нажимает кнопку «Загрузить фото», он отправляет данные на сервер и перезагружает страницу, чтобы показать новое изображение профиля пользователя.
  3. updateFormValues: это также наш собственный обработчик для обновления сведений о пользователе. Здесь пользователь может обновить свое имя, фамилию и страну. Мы не разрешаем обновлять электронную почту и имя пользователя, потому что наша внутренняя логика зависит от этих ключей.

Помимо этих трех обработчиков, это страница формы со стилем поверх нее. Вот структура каталогов до этого момента внутри папки просмотра:

+-- public +-- src | +-- components | +-- +-- todo.js | +-- +-- account.js | +-- pages | +-- +-- home.js | +-- +-- login.js | +-- +-- signup.js | +-- util | +-- +-- auth.js | +-- README.md | +-- package-lock.json | +-- package.json | +-- .gitignore

На этом мы завершили нашу панель управления учетной записью. Теперь пойдем выпьем кофе, сделаем перерыв, и в следующем разделе мы создадим панель инструментов Todo.

Раздел 4: Панель инструментов Todo

В этомВ разделе , мы собираемся разработать пользовательский интерфейс для следующих функций панели инструментов Todos:

  1. Добавить Todo:
  2. Получить все задачи:
  3. Удалить задачу
  4. Редактировать задачу
  5. Получите задачу
  6. Применение темы

Код панели инструментов Todo, реализованный в этом разделе, можно найти в этом коммите.

Перейдите todos.jsв каталог компонентов . Добавьте следующий импорт к существующему импорту:

import Button from '@material-ui/core/Button'; import Dialog from '@material-ui/core/Dialog'; import AddCircleIcon from '@material-ui/icons/AddCircle'; import AppBar from '@material-ui/core/AppBar'; import Toolbar from '@material-ui/core/Toolbar'; import IconButton from '@material-ui/core/IconButton'; import CloseIcon from '@material-ui/icons/Close'; import Slide from '@material-ui/core/Slide'; import TextField from '@material-ui/core/TextField'; import Grid from '@material-ui/core/Grid'; import Card from '@material-ui/core/Card'; import CardActions from '@material-ui/core/CardActions'; import CircularProgress from '@material-ui/core/CircularProgress'; import CardContent from '@material-ui/core/CardContent'; import MuiDialogTitle from '@material-ui/core/DialogTitle'; import MuiDialogContent from '@material-ui/core/DialogContent'; import axios from 'axios'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; import { authMiddleWare } from '../util/auth';

Нам также необходимо добавить следующие элементы CSS в существующие компоненты стиля:

const styles = (theme) => ({ .., // Existing CSS elements title: { marginLeft: theme.spacing(2), flex: 1 }, submitButton: { display: 'block', color: 'white', textAlign: 'center', position: 'absolute', top: 14, right: 10 }, floatingButton: { position: 'fixed', bottom: 0, right: 0 }, form: { width: '98%', marginLeft: 13, marginTop: theme.spacing(3) }, toolbar: theme.mixins.toolbar, root: { minWidth: 470 }, bullet: { display: 'inline-block', margin: '0 2px', transform: 'scale(0.8)' }, pos: { marginBottom: 12 }, uiProgess: { position: 'fixed', zIndex: '1000', height: '31px', width: '31px', left: '50%', top: '35%' }, dialogeStyle: { maxWidth: '50%' }, viewRoot: { margin: 0, padding: theme.spacing(2) }, closeButton: { position: 'absolute', right: theme.spacing(1), top: theme.spacing(1), color: theme.palette.grey[500] } });

Мы добавим переход для всплывающего диалогового окна:

const Transition = React.forwardRef(function Transition(props, ref) { return ; });

Удалите существующий класс задачи и скопируйте и вставьте следующий класс:

class todo extends Component { constructor(props) { super(props); this.state = { todos: '', title: '', body: '', todoId: '', errors: [], open: false, uiLoading: true, buttonType: '', viewOpen: false }; this.deleteTodoHandler = this.deleteTodoHandler.bind(this); this.handleEditClickOpen = this.handleEditClickOpen.bind(this); this.handleViewOpen = this.handleViewOpen.bind(this); } handleChange = (event) => { this.setState({ [event.target.name]: event.target.value }); }; componentWillMount = () => { authMiddleWare(this.props.history); const authToken = localStorage.getItem('AuthToken'); axios.defaults.headers.common = { Authorization: `${authToken}` }; axios .get('/todos') .then((response) => { this.setState({ todos: response.data, uiLoading: false }); }) .catch((err) => { console.log(err); }); }; deleteTodoHandler(data) { authMiddleWare(this.props.history); const authToken = localStorage.getItem('AuthToken'); axios.defaults.headers.common = { Authorization: `${authToken}` }; let todoId = data.todo.todoId; axios .delete(`todo/${todoId}`) .then(() => { window.location.reload(); }) .catch((err) => { console.log(err); }); } handleEditClickOpen(data) { this.setState({ title: data.todo.title, body: data.todo.body, todoId: data.todo.todoId, buttonType: 'Edit', open: true }); } handleViewOpen(data) { this.setState({ title: data.todo.title, body: data.todo.body, viewOpen: true }); } render() { const DialogTitle = withStyles(styles)((props) => { const { children, classes, onClose, ...other } = props; return (  {children} {onClose ? (    ) : null}  ); }); const DialogContent = withStyles((theme) => ({ viewRoot: { padding: theme.spacing(2) } }))(MuiDialogContent); dayjs.extend(relativeTime); const { classes } = this.props; const { open, errors, viewOpen } = this.state; const handleClickOpen = () => { this.setState({ todoId: '', title: '', body: '', buttonType: '', open: true }); }; const handleSubmit = (event) => { authMiddleWare(this.props.history); event.preventDefault(); const userTodo = { title: this.state.title, body: this.state.body }; let options = {}; if (this.state.buttonType === 'Edit') { options = { url: `/todo/${this.state.todoId}`, method: 'put', data: userTodo }; } else { options = { url: '/todo', method: 'post', data: userTodo }; } const authToken = localStorage.getItem('AuthToken'); axios.defaults.headers.common = { Authorization: `${authToken}` }; axios(options) .then(() => { this.setState({ open: false }); window.location.reload(); }) .catch((error) => { this.setState({ open: true, errors: error.response.data }); console.log(error); }); }; const handleViewClose = () => { this.setState({ viewOpen: false }); }; const handleClose = (event) => { this.setState({ open: false }); }; if (this.state.uiLoading === true) { return ( {this.state.uiLoading && }  ); } else { return ( {this.state.buttonType === 'Edit' ? 'Edit Todo' : 'Create a new Todo'}   {this.state.buttonType === 'Edit' ? 'Save' : 'Submit'}                {this.state.todos.map((todo) => (     {todo.title}   {dayjs(todo.createdAt).fromNow()}   {`${todo.body.substring(0, 65)}`}     this.handleViewOpen({ todo })}> {' '} View{' '}   this.handleEditClickOpen({ todo })}> Edit   this.deleteTodoHandler({ todo })}> Delete     ))}    {this.state.title}       ); } } }

В конце этого файла добавьте следующий экспорт:

export default withStyles(styles)(todo); 

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

Нажмите кнопку Добавить в правом нижнем углу, и вы увидите следующий экран:

Добавьте заголовок и детали Todo и нажмите кнопку отправки. Вы получите следующий экран:

После этого нажмите кнопку просмотра, и вы сможете увидеть полную информацию о Todo:

Нажмите кнопку «Изменить», и вы сможете отредактировать задачу:

Нажмите кнопку удаления, и вы сможете удалить Todo. Теперь, когда мы знаем, как работает Dashboard, мы разберемся с используемыми в нем компонентами.

1. Добавить задачу: для реализации задачи добавления мы будем использовать компонент Dialogue в UI материала. Этот компонент реализует функцию перехвата. Мы используем классы, поэтому удалим эту функциональность.

// This sets the state to open and buttonType flag to add: const handleClickOpen = () => { this.setState({ todoId: '', title: '', body: '', buttonType: '', open: true }); }; // This sets the state to close: const handleClose = (event) => { this.setState({ open: false }); };

Помимо этого, мы также изменим размещение кнопки Добавить Todo.

// Position our button floatingButton: { position: 'fixed', bottom: 0, right: 0 }, 

Теперь мы заменим тег списка формой внутри этого диалога. Это поможет нам добавить новое задание.

// Show Edit or Save depending on buttonType state {this.state.buttonType === 'Edit' ? 'Save' : 'Submit'} // Our Form to add a todo    // TextField here   // TextField here   

ВhandleSubmitсостоит из логики чтения buttonTypeсостояния. Если состояние является пустой строкой, (“”)оно будет опубликовано в Add Todo API. Если состояние является, Editто в этом сценарии он обновит файл Edit Todo.

2. Получите задачи: чтобы отобразить задачи, мы будем использовать значок, Grid containerа внутри него мы помещаем Grid item. Внутри этого мы будем использовать Cardкомпонент для отображения данных.

 {this.state.todos.map((todo) => (    // Here will show Todo with view, edit and delete button   ))} 

Мы используем карту для отображения элемента задачи, когда API отправляет их в виде списка. Мы будем использовать жизненный цикл componentWillMount, чтобы получить и установить состояние перед выполнением рендеринга. Есть 3 кнопки ( просмотр, редактирование и удаление ), поэтому нам понадобятся 3 обработчика для обработки операции при нажатии кнопки. Мы узнаем об этих кнопках в соответствующих подразделах.

3. Редактировать задачу: для задачи редактирования мы повторно используем код всплывающего диалогового окна, который используется при добавлении задачи. Чтобы различать нажатия кнопок, мы используем buttonTypeсостояние. Для «Добавить задачу» это   buttonTypeсостояние, (“”)а для задачи редактирования - состояние Edit.

handleEditClickOpen(data) { this.setState({ .., buttonType: 'Edit', .. }); }

В handleSubmitметоде мы читаем buttonTypeсостояние, а затем отправляем соответствующий запрос.

4. Удалить задачу: при нажатии этой кнопки мы отправляем объект задачи нашему deleteTodoHandler, а затем он отправляет запрос в бэкэнд.

 this.deleteTodoHandler({ todo })}>Delete

5. Просмотр задачи: при отображении данных мы усекли их, чтобы пользователь мог получить представление о том, о чем задача. Но если пользователь хочет узнать об этом больше, ему нужно нажать кнопку просмотра.

Для этого воспользуемся настраиваемым диалогом. Внутри мы используем DialogTitle и DialogContent. Он отображает наш заголовок и контент. В DialougeContent мы будем использовать форму для отображения содержимого, опубликованного пользователем. (Это одно из решений, которых, как я обнаружил, много, и вы можете попробовать другое.)

// This is used to remove the underline of the Form InputProps={{ disableUnderline: true }} // This is used so that user cannot edit the data readonly

6. Применение темы: это последний шаг нашего приложения. Мы применим тему к нашему приложению. Для этого мы используем createMuiThemeи ThemeProviderиз материала UI. Скопируйте и вставьте следующий код в App.js:

import { ThemeProvider as MuiThemeProvider } from '@material-ui/core/styles'; import createMuiTheme from '@material-ui/core/styles/createMuiTheme'; const theme = createMuiTheme({ palette: { primary: { light: '#33c9dc', main: '#FF5722', dark: '#d50000', contrastText: '#fff' } } }); function App() { return (  // Router and switch will be here.  ); }

Мы пропустили применив тему нашей кнопки в todo.jsв CardActions. Добавьте тег цвета для кнопки просмотра, редактирования и удаления.

Зайдите в браузер, и вы обнаружите, что все то же самое, за исключением того, что приложение другого цвета.

И готово! Мы создали TodoApp с использованием ReactJS и Firebase. Если вы построили его до этого момента, то очень поздравляем вас с этим достижением.

Не стесняйтесь связываться со мной в Twitter и Github.