Как создать и развернуть Full-Stack React-приложение

от автора

Привет, Хабр! Представляю вашем вниманию перевод статьи «How to Build and Deploy a Full-Stack React-App» автора Frank Zickert.

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

Хотите стать full-stack разработчиком? Full-stack приложение дополняет интерактивный веб-интерфейс React сервером и базой данных. Но такое приложение требует гораздо больше настроек, чем простое одностраничное приложение.

Мы используем компоненты инфраструктуры. Эти React-Компоненты позволяют нам определять нашу инфраструктурную архитектуру как часть нашего React-приложения. Нам больше не нужны никакие другие настройки, такие как Webpack, Babel или Serverless.

Старт

Вы можете настроить свой проект тремя способами:

  • загрузите ваш индивидуальный шаблонный код с www.infrastructure-components.com,
  • или склонируйте этот GitHub-repository,
  • или установите библиотеки вручную, см. этот пост для более подробной информации.

После того, как вы установили зависимости (запустите npm install), вы можете собрать проект с помощью одной команды: npm run build.

Скрипт сборки добавляет еще три скрипта в package.json:

  • npm run{your-project-name} запускает ваше React-приложение локально (режим hot-dev, без серверной части и базы данных)
  • npm run start-{your-env-name} запускает весь программный стек локально. Примечание: Для запуска базы данных в offline режиме требуется Java 8 JDK. Вот как вы можете установить JDK.
  • npm run deploy-{your-env-name} разворачивает ваше приложение на AWS.

Примечание. Для развертывания приложения в AWS вам необходим технический пользователь IAM с этими правами. Поместите учетные данные пользователя в ваш .env-файл следующим образом:

AWS_ACCESS_KEY_ID = *** AWS_SECRET_ACCESS_KEY = ***

Определите архитектуру вашего приложения

Проекты, основанные на компонентах инфраструктуры, имеют четкую структуру. У вас есть один компонент верхнего уровня. Это определяет общую архитектуру вашего приложения.

Подкомпоненты (дочерние компоненты) уточняют(расширяют) поведение приложения и добавляют функции.

В следующем примере, компонент <ServiceOrientedApp /> — это наш компонент верхнего уровня. Мы экспортируем его как файл по умолчанию в наш файл точку входа (src / index.tsx).

export default (    <ServiceOrientedApp        stackName = "soa-dl"        buildPath = 'build'        region='eu-west-1'>        <Environment name="dev" />        <Route            path='/'            name='My Service-Oriented React App'            render={()=><DataForm />}        />        <DataLayer id="datalayer">            <UserEntry />            <GetUserService />            <AddUserService />        </DataLayer>    </ServiceOrientedApp> );

<ServiceOrientedApp /> — это интерактивное веб-приложение. Вы можете уточнить(расширить) функционал этого приложения с помощью предоставленных вами дочерних компонентов. Оно поддерживает компоненты <Environment />, <Route />, <Service /> и <DataLayer />.

<Envrionment /> определяет время выполнения вашего приложения. Например, вы можете иметь dev и prod версию. Вы можете запускать и развертывать каждую отдельно.

<Route /> — это страница вашего приложения. он работает как <Route /> в react-router. Вот туториал о том, как работать с маршрутами.

<Service /> определяет функцию, которая выполняется на стороне сервера. Он может иметь один или несколько <Middleware /> — компонентов в качестве дочерних.

<Middleware /> работает как Express.js-middleware.
<DataLayer /> добавляет базу данных NoSQL в ваше приложение. Принимает <Entry /> — компоненты как дочерние. <Entry /> описывает тип элементов в вашей базе данных.

Эти компоненты — все, что нам нужно для создания нашего full-stack приложения. Как видите, наше приложение имеет: одну среду выполнения, одна страница, два сервиса и базу данных с одной записью.

Структура компонентов обеспечивает четкое представление о вашем приложении. Чем больше становится ваше приложение, тем важнее это.

Возможно, вы заметили, что <Service /> являются дочерними для <DataLayer />. Это имеет простое объяснение. Мы хотим, чтобы наши сервисы имели доступ к базе данных. Это действительно так просто!

Проектирование Базы данных

<DataLayer /> создает Amazon DynamoDB. Это база данных ключ-значение (NoSQL). Она обеспечивает высокую производительность в любом масштабе. Но в отличие от реляционных баз данных, она не поддерживает сложные запросы.

Схема базы данных имеет три поля: primaryKey, rangeKey и data. Это важно, потому что вам нужно знать, что вы можете найти записи только по его ключам. Либо по primaryKey, либо по rangeKey, либо по обоим.

Обладая этими знаниями, давайте взглянем на наш <Entry />:

export const USER_ENTRY_ID = "user_entry"; export default function UserEntry (props)  { 	return <Entry     	id={ USER_ENTRY_ID }         primaryKey="username"         rangeKey="userid"     	data={{         	age: GraphQLString,         	address: GraphQLString     	}} 	/> };

<Entry /> описывает структуру наших данных. Мы определяем имена для наших primaryKey и rangeKey. Вы можете использовать любое имя, кроме некоторых ключевых слов DynamoDB, которые вы можете найти здесь. Но имена, которые мы используем, имеют функциональные последствия:

  • Когда мы добавляем элементы в нашу базу данных, мы должны предоставить значения для этих ключевых имен.
  • Комбинация обоих ключей описывает уникальный элемент в базе данных.
  • Не должно быть другого <Entry /> с такими же именами ключей (одно имя может совпадать, но не оба).
  • Мы можем найти элементы в базе данных только тогда, когда у нас есть значение хотя бы одного ключевого имени.

В нашем примере это означает, что:

  • каждый User должен иметь username и userid.
  • не может быть второго User с тем же username и тем же userid. С точки зрения базы данных было бы хорошо, если бы у двух User было одно и то же username, когда у них разные userid (или наоборот).
  • У нас не может быть другого <Entry /> в нашей базе данных с primaryKey = «username» и rangeKey = «userid».
  • Мы можем запросить базу данных для пользователей, когда у нас есть имя пользователя или id пользователя. Но мы не можем запросить по возрасту или адресу.

Добавить элементы в базу данных

Мы определили два <Service /> — компонента в нашем <ServiceOrientedApp />. POST-сервис, который добавляет пользователя в базу данных и GET-сервис, который извлекает пользователя из нее.

Давайте начнем с <AddUserService />. Вот код этого сервиса:

import * as React from 'react'; import { 	callService, 	Middleware, 	mutate, 	Service,     serviceWithDataLayer } from "infrastructure-components";  import { USER_ENTRY_ID, IUserEntry } from './user-entry';  const ADDUSER_SERVICE_ID = "adduser";  export default function AddUserService () { 	return <Service     	id={ ADDUSER_SERVICE_ID }         path="/adduser"         method="POST">      	<Middleware             callback={serviceWithDataLayer(async function (dataLayer, req, res, next) {                 const parsedBody: IUserEntry = JSON.parse(req.body);                   await mutate(                     dataLayer.client,                     dataLayer.setEntryMutation(USER_ENTRY_ID, parsedBody)                 );                   res.status(200).set({                     "Access-Control-Allow-Origin" : "*", // Required for CORS support to work             	}).send("ok");           	})}/>   	</Service> };

Компонент <Service /> — принимает три параметра:

  • Идентификатор(id) должен быть уникальной строкой. Мы используем идентификатор(id), когда нам нужно обратиться к service в других компонентах.
  • Путь(path) (с начальным /) указывает относительный путь URL вашей службы
  • Метод(method) должен быть одним из GET, POST, UPDATE, DELETE. Он указывает HTTP-запрос, который мы используем при вызове сервиса.

Мы добавляем <Middleware /> в качестве дочернего элемента. Этот <Middleware /> принимает функцию обратного вызова в качестве параметра. Мы могли бы напрямую предоставить промежуточное программное обеспечение Express.js. Поскольку мы хотим получить доступ к базе данных, мы заключаем функцию в serviceWithDataLayer. Это добавляет dataLayer в качестве первого параметра к нашему обратному вызову.

DataLayer обеспечивает доступ к базе данных. Посмотрим как!

Асинхронная функция mutate применяет изменения к данным в нашей базе данных. Это требует клиента и команды мутации в качестве параметров.

Данные элемента — это объект Javascript, который имеет все необходимые пары ключ-значение. В нашем сервисе мы получаем этот объект из тела запроса. Для User объект имеет следующую структуру:

export interface IUserEntry { 	username: string, 	userid: string, 	age: string, 	address: string }

Этот объект принимает имена primaryKey и rangeKey и все ключи данных, которые мы определили в <Entry />.

Примечание: на данный момент единственным поддерживаемым типом является строка, которая соответствует GraphQLString в определении <Entry />.
Мы упоминали выше, что мы берем данные IUserEntry из тела. Как это происходит?

Компоненты инфраструктуры предоставляют асинхронную функцию callService (serviceId, dataObject). Эта функция принимает идентификатор службы, Javascript-объект (для отправки в качестве тела запроса при использовании POST), функцию success — и функцию обратного вызова с ошибкой.

В следующем фрагменте показано, как мы используем эту функцию для вызова нашего <AddUserService />. Мы указываем serviceId. И мы передаем userData, которые мы берем в качестве параметра для нашей функции.

export async function callAddUserService (userData: IUserEntry) { 	await callService(     	ADDUSER_SERVICE_ID,     	userData,     	(data: any) => {         	console.log("received data: ", data);     	},     	(error) => {         	console.log("error: " , error)     	} 	); };

Теперь callAddUserService-функция — это все, что нам нужно, когда мы хотим добавить нового пользователя. Например, вызывайте ее, когда пользователь нажимает кнопку:

<button onClick={() => callAddUserService({ 	username: username, 	userid: userid, 	age: age, 	address: address })}>Save</button>

Мы просто вызываем его с помощью объекта IUserEntry. Он вызывает правильный сервис (как указано его идентификатором(id)). Он помещает userData в тело запроса. <AddUserService /> берет данные из тела и помещает их в базу данных.

Получить элементы из базы данных

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

export default function GetUserService () { 	return <Service     	id={ GETUSER_SERVICE_ID }     	path="/getuser"     	method="GET">       	<Middleware             callback={serviceWithDataLayer(async function (dataLayer, req, res, next) {         	             	const data = await select(                 	dataLayer.client,                     dataLayer.getEntryQuery(USER_ENTRY_ID, {                     	username: req.query.username,                     	userid: req.query.userid        	         })             	);               	res.status(200).set({                     "Access-Control-Allow-Origin" : "*", // Required for CORS support to work             	}).send(JSON.stringify(data));           	})}/>   	</Service> }

Опять же, мы используем <Service />, <Middleware /> и функцию обратного вызова с доступом к базе данных.

Вместо функции mutate, которая добавляет элемент в базу данных, мы используем функцию select. Эта функция запрашивает клиента, который мы берем из dataLayer. Второй параметр — команда select. Как и команда mutation, мы можем создать команду select с помощью dataLayer.

На этот раз мы используем функцию getEntryQuery. Мы предоставляем идентификатор(id) <Entry />, чей элемент мы хотим получить. И мы предоставляем ключи (primaryKey и rangeKey) конкретного элемента в объекте Javascript. Поскольку мы предоставляем оба ключа, мы получаем один элемент обратно. Если он существует.

Как вы могли видеть, мы берем значения ключа из запроса. Но на этот раз мы берем их из request.query, а не из request.body. Причина в том, что этот сервис использует GET-метод. Этот метод не поддерживает тело в запросе. Но он предоставляет все данные в качестве параметров запроса.

Функция callService обрабатывает это для нас. Как и в callAddUserService-function, мы предоставляем идентификатор(id) <Service />, который мы хотим вызвать. Предоставляем необходимые данные. Здесь это только ключи. И мы предоставляем функции обратного вызова.

Успешный обратный вызов обеспечивает ответ. Тело ответа в формате json содержит наш найденный элемент. Мы можем получить доступ к этому элементу через ключ get_user_entry. «Get_» определяет запрос, который мы поместили в нашу функцию выбора. «User_entry» — это ключ нашего <Entry />.

export async function callGetUserService (username: string, userid: string, onData: (userData: IUserEntry) => void) {   	await callService(         GETUSER_SERVICE_ID,     	{         	username: username,         	userid: userid     	},     	async function (response: any) {             await response.json().then(function(data) {                 console.log(data[`get_${USER_ENTRY_ID}`]);                 onData(data[`get_${USER_ENTRY_ID}`]);         	});     	},     	(error) => {             console.log("error: " , error)     	} 	);   }

Посмотрите на свое Full-Stack приложение в действии

Если вы еще не запустили свое приложение, сейчас самое время сделать это: npm run start-{your-env-name}.

Вы даже можете развернуть свое приложение в AWS с помощью одной команды: npm run deploy-{your-env-name}. (Не забудьте поместить учетные данные AWS в .env-файл).

Этот пост не описывает, как вы вводите данные, которые вы помещаете в базу данных, и как вы отображаете результаты. callAddUserService и callGetUserService инкапсулируют все, что является специфическим для сервисов и базы данных. Вы просто помещаете туда объект Javascript и получаете его обратно.

Вы найдете исходный код этого примера в этом GitHub-репозитории. Он включает в себя очень простой пользовательский интерфейс.


ссылка на оригинал статьи https://habr.com/ru/post/476696/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *