Русский
Русский
English
Статистика
Реклама

Fullstack

Пишем full stack монолит с помощью Angular Universal NestJS PostgreSQL

10.08.2020 02:14:34 | Автор: admin
Привет, Хабр!

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


Эта статья будет полезна, если вы:


  • Начинающий fullstack-разработчик;
  • Стартапер, который пишет MVP чтобы проверить гипотезу.

Почему выбрал такой стек:


  • Angular: имею много опыта в нем, люблю строгую архитектуру и Typescript из коробки, выходец из .NET
  • NestJS: тот-же язык, та-же архитектура, быстрое написание REST API, возможность в дальнейшем пересесть на Serverless (дешевле виртуалки)
  • PostgreSQL: Собираюсь хоститься в Яндекс.Облаке, на минималках дешевле на 30% чем MongoDB

Прайс яндекса


Прежде чем написать статью, поискал на хабре статьи про подобный кейс, нашел следующее:



Из этого ничего не описывает "скопировал и вставил" или дает ссылки на то что еще нужно дорабатывать.


Оглавление:


1. Создаем Angular приложение и добавляем библиотеку компонентов ng-zorro
2. Устанавливаем NestJS и решаем проблемы с SSR
3. Делаем API на NestJS и подключаем к фронту
4. Подключаем базу данных PostgreSQL



1. Создаем Angular приложение


Установим Angular-CLI чтобы создавать SPA-сайты на Ангуляре:


npm install -g @angular/cli

Создадим Angular приложение с помощью следующей команды:


ng new angular-habr-nestjs

Далее переходим в папку приложения и запускаем, чтобы проверить работоспособность:


cd angular-habr-nestjsng serve --open

Статическое SPA-приложение на Angular


Приложение создалось. Подключаем библиотеку NG-Zorro:


ng add ng-zorro-antd

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


? Enable icon dynamic loading [ Detail: https://ng.ant.design/components/icon/en ] Yes? Set up custom theme file [ Detail: https://ng.ant.design/docs/customize-theme/en ] No? Choose your locale code: ru_RU? Choose template to create project: sidemenu

Эта конфигурация заменит содержимое app.component на дизайн с менюшкой слева, футером и хедером и подключит локализацию на русском языке:


Подключили NG-Zorro


В данной статье мы отобразим список данных для наглядности, поэтому добавим простенькую табличку в компоненте src/app/pages/welcome, который сгенерил NG-Zorro:
Пример взят отсюда:
https://ng.ant.design/components/table/en


// welcome.component.html<nz-table #basicTable [nzData]="items$ | async"> <thead> <tr>  <th>Name</th>  <th>Age</th>  <th>Address</th> </tr> </thead> <tbody> <tr *ngFor="let data of basicTable.data">  <td>{{ data.name }}</td>  <td>{{ data.age }}</td>  <td>{{ data.address }}</td> </tr> </tbody></nz-table>

// welcome.module.tsimport { NgModule } from '@angular/core';import { WelcomeRoutingModule } from './welcome-routing.module';import { WelcomeComponent } from './welcome.component';import { NzTableModule } from 'ng-zorro-antd';import { CommonModule } from '@angular/common';@NgModule({ imports: [  WelcomeRoutingModule,  NzTableModule, // Добавили для таблицы  CommonModule // Добавили для пайпа async ], declarations: [WelcomeComponent], exports: [WelcomeComponent]})export class WelcomeModule {}

// welcome.component.tsimport { Component, OnInit } from '@angular/core';import { Observable, of } from 'rxjs';import { HttpClient } from '@angular/common/http';import { share } from 'rxjs/operators';@Component({ selector: 'app-welcome', templateUrl: './welcome.component.html', styleUrls: ['./welcome.component.scss']})export class WelcomeComponent implements OnInit { items$: Observable<Item[]> = of([  {name: 'Вася', age: 24, address: 'Москва'},  {name: 'Петя', age: 23, address: 'Лондон'},  {name: 'Миша', age: 21, address: 'Париж'},  {name: 'Вова', age: 23, address: 'Сидней'} ]); constructor(private http: HttpClient) { } ngOnInit() { } // Сразу напишем метод к бэку, понадобится позже getItems(): Observable<Item[]> {  return this.http.get<Item[]>('/api/items').pipe(share()); }}interface Item { name: string; age: number; address: string;}

Получилось следующее:


Табличка NG-Zorro



2. Устанавливаем NestJS


Далее установим NestJS таким образом, чтобы он предоставил Angular Universal (Server Side Rendering) из коробки и напишем пару ендпоинтов.


ng add @nestjs/ng-universal

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


npm run serve

И вот уже первый косяк :) У нас появляется следующая ошибка:


TypeError: Cannot read property 'indexOf' of undefined  at D:\Projects\angular-habr-nestjs\node_modules\@nestjs\ng-universal\dist\utils\setup-universal.utils.js:35:43  at D:\Projects\angular-habr-nestjs\dist\server\main.js:107572:13  at View.engine (D:\Projects\angular-habr-nestjs\node_modules\@nestjs\ng-universal\dist\utils\setup-universal.utils.js:30:11)  at View.render (D:\Projects\angular-habr-nestjs\node_modules\express\lib\view.js:135:8)  at tryRender (D:\Projects\angular-habr-nestjs\node_modules\express\lib\application.js:640:10)  at Function.render (D:\Projects\angular-habr-nestjs\node_modules\express\lib\application.js:592:3)  at ServerResponse.render (D:\Projects\angular-habr-nestjs\node_modules\express\lib\response.js:1012:7)  at D:\Projects\angular-habr-nestjs\node_modules\@nestjs\ng-universal\dist\angular-universal.module.js:60:66  at Layer.handle [as handle_request] (D:\Projects\angular-habr-nestjs\node_modules\express\lib\router\layer.js:95:5)  at next (D:\Projects\angular-habr-nestjs\node_modules\express\lib\router\route.js:137:13)

Чтобы решить косяк, зайдем в файл server/app.module.ts и поменяем значение liveReload на false:


import { Module } from '@nestjs/common';import { AngularUniversalModule } from '@nestjs/ng-universal';import { join } from 'path';@Module({ imports: [  AngularUniversalModule.forRoot({   viewsPath: join(process.cwd(), 'dist/browser'),   bundle: require('../server/main'),   liveReload: false  }) ]})export class ApplicationModule {}

Также подтюним конфиг тайпскрипта, так-как эта конфигурация не взлетает с использованием Ivy рендера:


// tsconfig.server.json{ "extends": "./tsconfig.app.json", "compilerOptions": {  "outDir": "./out-tsc/server",  "target": "es2016",  "types": [   "node"  ] }, "files": [  "src/main.server.ts" ], "angularCompilerOptions": {  "enableIvy": false, // Добавили флажок  "entryModule": "./src/app/app.server.module#AppServerModule" }}

После пересоберем приложение командой ng run serve чтобы SSR заработал.


Angular SSR + NestJS


Ура! SSR подрубился, но как видимо в devtools он приходит с кривыми стилями.


Добавим extractCss: true, который позволит выносить стили не в styles.js, а в styles.css:


// angular.json..."architect": {    "build": {     "builder": "@angular-devkit/build-angular:browser",     "options": {      "outputPath": "dist/browser",      "index": "src/index.html",      "main": "src/main.ts",      "polyfills": "src/polyfills.ts",      "tsConfig": "tsconfig.app.json",      "aot": true,      "assets": [       "src/favicon.ico",       "src/assets",       {        "glob": "**/*",        "input": "./node_modules/@ant-design/icons-angular/src/inline-svg/",        "output": "/assets/"       }      ],      "extractCss": true, // Добавили флажок      "styles": [       "./node_modules/ng-zorro-antd/ng-zorro-antd.min.css",       "src/styles.scss"      ],      "scripts": []     },...

Также подключим стили библиотеки в app.component.scss:


// app.component.scss@import "~ng-zorro-antd/ng-zorro-antd.min.css"; // Подключили стили:host { display: flex; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;}.app-layout { height: 100vh;}...

Теперь стили подключены, SSR отдает страничку со стилями, но мы видим что сначала у нас грузится SSR, потом страница моргает и отрисовывается CSR (Client Side Rendering). Это решается следующим способом:


import { NgModule } from '@angular/core';import { Routes, RouterModule } from '@angular/router';const routes: Routes = [ { path: '', pathMatch: 'full', redirectTo: '/welcome' }, { path: 'welcome', loadChildren: () => import('./pages/welcome/welcome.module').then(m => m.WelcomeModule) }];@NgModule({ imports: [RouterModule.forRoot(routes, {initialNavigation: 'enabled', scrollPositionRestoration: 'enabled'})], // Добавили initialNavigation, scrollPositionRestoration exports: [RouterModule]})export class AppRoutingModule { }

  • initialNavigation: 'enabled' дает инструкцию роутингу не отрисовывать страницу, если уже загружена через SSR
  • scrollPositionRestoration: 'enabled' скролит страницу наверх при каждом роутинге.


3. Сделаем пару ендпоинтов на NestJS


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


cd servernest g module itemsnest g controller items --no-spec

// items.module.tsimport { Module } from '@nestjs/common';import { ItemsController } from './items.controller';@Module({ controllers: [ItemsController]})export class ItemsModule {}

// items.controller.tsimport { Controller } from '@nestjs/common';@Controller('items')export class ItemsController {}

Контроллер и модуль создались. Создадим метод на получение списка items и на добавление объекта в список:


// server/src/items/items.controller.tsimport { Body, Controller, Get, Post } from '@nestjs/common';class Item { name: string; age: number; address: string;}@Controller('items')export class ItemsController { // для простоты данные взял из Angular private items: Item[] = [  {name: 'Вася', age: 24, address: 'Москва'},  {name: 'Петя', age: 23, address: 'Лондон'},  {name: 'Миша', age: 21, address: 'Париж'},  {name: 'Вова', age: 23, address: 'Сидней'} ]; @Get() getAll(): Item[] {  return this.items; } @Post() create(@Body() newItem: Item): void {  this.items.push(newItem); }}

Попробуем вызвать GET в Postman:


GET запросы апишки NestJS


Отлично, работает! Обратите внимание, вызываем метод GET items с префиксом api, который ставится автоматически в файле server/main.ts при установке NestJS:


// server/main.tsimport { NestFactory } from '@nestjs/core';import { ApplicationModule } from './app.module';async function bootstrap() { const app = await NestFactory.create(ApplicationModule); app.setGlobalPrefix('api'); // Это префикс await app.listen(4200);}bootstrap();

Теперь прикрутим бэк к фронту. Возвращаемся к файлу welcome.component.ts и делаем запрос списка к бэку:


// welcome.component.tsimport { Component, OnInit } from '@angular/core';import { Observable, of } from 'rxjs';import { HttpClient } from '@angular/common/http';import { share } from 'rxjs/operators';@Component({ selector: 'app-welcome', templateUrl: './welcome.component.html', styleUrls: ['./welcome.component.scss']})export class WelcomeComponent implements OnInit { items$: Observable<Item[]> = this.getItems(); // прикрутили вызов бэка constructor(private http: HttpClient) { } ngOnInit() { } getItems(): Observable<Item[]> {  return this.http.get<Item[]>('/api/items').pipe(share()); }}interface Item { name: string; age: number; address: string;}

Можно увидеть что апиха на фронте дергается, но также дергается и в SSR, причем с ошибкой:


Дергание апихи в SSR


Ошибка при запросе в SSR решается следующим способом:


// welcome.component.tsimport { Component, OnInit } from '@angular/core';import { Observable, of } from 'rxjs';import { HttpClient } from '@angular/common/http';import { share } from 'rxjs/operators';@Component({ selector: 'app-welcome', templateUrl: './welcome.component.html', styleUrls: ['./welcome.component.scss']})export class WelcomeComponent implements OnInit { items$: Observable<Item[]> = this.getItems(); // прикрутили вызов бэка constructor(private http: HttpClient) { } ngOnInit() { } getItems(): Observable<Item[]> {  return this.http.get<Item[]>('http://localhost:4200/api/items').pipe(share()); // Прописали полный путь к апихе чтобы SSR не ругался }}interface Item { name: string; age: number; address: string;}

Чтобы исключить двойной запрос к апихе (один на SSR, другой на фронте), нужно проделать следующее:


  • Установим библиотеку @nguniversal/common:

npm i @nguniversal/common

  • В файле app/app.module.ts добавим модуль для запросов из SSR:

// app.module.tsimport { BrowserModule } from '@angular/platform-browser';import { NgModule } from '@angular/core';import { AppRoutingModule } from './app-routing.module';import { AppComponent } from './app.component';import { IconsProviderModule } from './icons-provider.module';import { NzLayoutModule } from 'ng-zorro-antd/layout';import { NzMenuModule } from 'ng-zorro-antd/menu';import { FormsModule } from '@angular/forms';import { HttpClientModule } from '@angular/common/http';import { BrowserAnimationsModule } from '@angular/platform-browser/animations';import { NZ_I18N } from 'ng-zorro-antd/i18n';import { ru_RU } from 'ng-zorro-antd/i18n';import { registerLocaleData } from '@angular/common';import ru from '@angular/common/locales/ru';import {TransferHttpCacheModule} from '@nguniversal/common';registerLocaleData(ru);@NgModule({ declarations: [  AppComponent ], imports: [  BrowserModule.withServerTransition({ appId: 'serverApp' }),  TransferHttpCacheModule, // Добавили  AppRoutingModule,  IconsProviderModule,  NzLayoutModule,  NzMenuModule,  FormsModule,  HttpClientModule,  BrowserAnimationsModule ], providers: [{ provide: NZ_I18N, useValue: ru_RU }], bootstrap: [AppComponent]})export class AppModule { }

Схожую операцию проделаем с app.server.module.ts:


// app.server.module.tsimport { NgModule } from '@angular/core';import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';import { AppModule } from './app.module';import { AppComponent } from './app.component';@NgModule({ imports: [  AppModule,  ServerModule,  ServerTransferStateModule, // Добавили ], bootstrap: [AppComponent],})export class AppServerModule {}

Хорошо. Теперь получаем данные из апи в SSR, отрисовываем на форме, отдаем на фронт и тот не делает повторных запросов.


Запроса нет, данные есть!



4. Подключим базу PostgreSQL


Подключим библиотеки для работы с PostgreSQL, также будем использовать TypeORM для работы с базой:


npm i pg typeorm @nestjs/typeorm

Внимание: у вас уже должна быть установлена PostgreSQL с базой внутри.


Описываем конфиг подключения к базе в server/app.module.ts:


// server/app.module.tsimport { Module } from '@nestjs/common';import { AngularUniversalModule } from '@nestjs/ng-universal';import { join } from 'path';import { ItemsController } from './src/items/items.controller';import { TypeOrmModule } from '@nestjs/typeorm';@Module({ imports: [  AngularUniversalModule.forRoot({   viewsPath: join(process.cwd(), 'dist/browser'),   bundle: require('../server/main'),   liveReload: false  }),  TypeOrmModule.forRoot({ // Конфиг подключения к базе   type: 'postgres',   host: 'localhost',   port: 5432,   username: 'postgres',   password: 'admin',   database: 'postgres',   entities: ['dist/**/*.entity{.ts,.js}'],   synchronize: true  }) ], controllers: [ItemsController]})export class ApplicationModule {}

Немного про поля конфига:


  • type: указываем название типа базы данных, к которой подключаемся
  • host и port: место где база хостится
  • username и password: аккаунт для этой базы
  • database: название базы
  • entities: путь, откуда будем брать сущности для схемы нашей базы

По последнему пункту, нужно создать сущность Item для мапинга полей в базу:


// server/src/items/item.entity.tsimport { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm/index';@Entity()export class ItemEntity { @PrimaryGeneratedColumn() id: number; @CreateDateColumn() createDate: string; @Column() name: string; @Column() age: number; @Column() address: string;}

Далее свяжем эту сущность с нашей базой.


// items.module.tsimport { Module } from '@nestjs/common';import { TypeOrmModule } from '@nestjs/typeorm';import { ItemEntity } from './item.entity';import { ItemsController } from './items.controller';@Module({ imports: [  TypeOrmModule.forFeature([ItemEntity]) // Подключаем фича-модуль и указываем сущности базы ], controllers: [ItemsController]})export class ItemsModule {}

Теперь укажем в контроллере, что хотим работать с базой, а не кешем:


// items.controller.tsimport { Body, Controller, Get, Post } from '@nestjs/common';import { ItemEntity } from './item.entity';import { InjectRepository } from '@nestjs/typeorm';import { Repository } from 'typeorm/index';interface Item { name: string; age: number; address: string;}@Controller('items')export class ItemsController { constructor(@InjectRepository(ItemEntity)       private readonly itemsRepository: Repository<ItemEntity>) { // Подключили репозиторий } @Get() getAll(): Promise<Item[]> {  return this.itemsRepository.find(); } @Post() create(@Body() newItem: Item): Promise<Item> {  const item = this.itemsRepository.create(newItem);  return this.itemsRepository.save(item); }}

Проверим работу апихи в Postman:


POST к апихе с базой


Работает. Потыкали несколько раз постман, посмотрим что записалось в базе с помощью DBeaver:


Записи в базе


Отлично! В базе есть, посмотрим как выглядит на фронте:


Рабочее fullstack приложение


Готово! Мы сделали fullstack приложение, с которым можно работать дальше.


P.S. Сразу поясню следующее:


  • Вместо Ng-Zorro вы можете использовать любую другую библиотеку, например Angular Material. Мне она лично не зашла из-за сложности разработки;
  • Я знаю, что нужно на бэке использовать сервисы, а не напрямую дергать базу в контроллерах. Эта статья о том, как решив проблемы "влоб" получить MVP с которым можно работать, а не про архитектуру и паттерны;
  • Вместо вписывания на фронте http://localhost:4200/api возможно лучше написать интерсептор и проверять откуда мы стучимся

Полезные ссылки:


Подробнее..

Recovery mode Как я искал работу в Берлине

04.09.2020 04:18:51 | Автор: admin

Обычно рассказывают про уже свершившиеся истории о том как кто-то уже нашел работу и переехал. Я все еще в процессе, так что расскажу с чем сталкивался и к каким выводам пришел. Советы будут в самом конце.

В марте 2020-го приехал я в Берлин на две недели отдохнуть (тогда корону многие всерьез не воспринимали). Через неделю после моего приезда Германия (и все остальные за ней) закрыли все границы и объявили режим ЧП. У меня было окно в день чтобы вернуться в Сербию, но я решил попытать счастье. Что-то мне подсказывало что я смогу воспользоваться ситуацией с короной чтобы получить документы не выезжая. Так и случилось, нашел миграционного адвоката, мы с ним обсудили ситуацию и он взялся за мое дело. Дело было за мной найти работу.

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

Еще будучи в Сербии я пытался искать работу в Германии, но за два года никто так и не отозвался по двум причинам:

  • Номер обратной связи был НЕ немецкий, а сербский, а значит я НЕ в Германии

  • У меня на тот момент не было разрешения на работу (о чем почти все спрашивали в анкете на момент подачи заявления)

Я не оправдываю, но понимаю компании которым эти два пункта важны, ибо визу обычно приходиться ждать месецами а иногда и годами (если позиция не очень востребована) тут все зависит от настроения консульского отдела страны откуда вы подаетесь. Я знаю случаи когда люди ждут своей очереди на термин по 18 месяцев, а есть те кто решает все за 2. Компании не хотят рисковать или терять время.

Как только адвокат получил для меня разрешение на работу в Германии и я стал в анкетах указывать что виза у меня есть и оставлять немецкий номер - то звонки от рекрутеров и HR компаний стали поступать сразу. Только в Августе у меня было 21 интервью.

Конечно из-за Ковида компании начали отвечать только к конце июля. До этого была тишина.

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

Подавался я на позиции Fullstack, Frontend и Backend разработчика, Благо опыта было достаточно и еще я воспользовался пандемией и потратил время но обновление своих знаний, завершил пару курсов на Udemy, получил сертификаты, наполнил свой Github и порешал задачи на HackerRank, как оказалось потом, спрашивали все.

Frontend

Два года назад я начал копать Frontend, то выбор пал на Vue и React, потому что чуть ли не каждый второй пугал тем что Angular уже вымер, и держится он только на том что его поддерживает Google.

Мои наблюдения показали что Стартапы и новые компании ищут в основном React разработчиков.
Серьезные компании ищут Angular разработчика. Который написан на Typescript. на который здесь моляться.
Vue почти никто не интересовался, только одна компания просила меня сделать тест задание на Vue и то на следующий день позвонили и сказали что прерывают процесс рекрутинга на эту позицию так как руководство решило подождать.

Почти все говорили о Typescript так как будто это венец эволюции. Лично я не люблю Typescript. но для поиска работы пришлось овладеть хотя бы базовыми навыками.

Backend

Сразу бросилось в глаза то что почти никто не искал PHP разработчика, на Backend в основном искали Java, Go, RoR, Node или Python. Из этого списка я работал только с Node.js, поэтому и подавался на позиции где указывался Node.

Процесс рекрутинга

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

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

  3. На третьем этапе вам посылают тест задание, слова это хорошо, но надо доказать на деле что вы действительно знаете все о чем вы говорили до этого. Самое частое задание которое меня просили выполнить - это написать конвертор валюты.

  4. Четвертый этап они называют Behavioral Interview - обычно это Project Manager, пытается выявить сможете ли вы следовать правилам выполнения работ (обычно Agile), написания кода и составление документации

  5. Последний Этап - это знакомство с командой. К моему удивлению это было не "Ребятя, познакомьтесь. Это Азиз", а вполне себе серьезный разговор со большинством команды. Каждый задает вопросы в своей сфере и пытается понять как именно вы будете полезны именно им. До этого этапа я дошел только трижды

Советы

  • Agile: Представьте что ваш директор дал вам час на написать определенного кода. Не важно успеете Вы или нет. Вы должны начать писать код тк, чтобы в случает когда вы не успеете и передадите задание другому коллеге, он - взяв незаконченный код, понял что вы хотели сделать. 90% компаний ждали именно этого подхода от меня и этот ответ их полностью удовлетворял.
    На деле это выглядит так: Вы начинаете писать код, создаете функции оставляете их пустыми, дополните потом, или Вы или Ваш коллега, неважно - главное вы начертили структуру. То же самое касается Юнит Тестов. При написании обязательно надо создавать Тесты или до написания кода, или параллельно. Вы должны показать что вы будете членом команды.

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

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

  • Удалите или отредактируйте позиции с предыдущих работ. Или придумайте хороший ответ который их удовлетворит. Если вы были директором предприятия и подаетесь на разработчика - будьте уверены Вас обязательно спросят об этом. Вам могут из-за этого и отказать, предположив что вы будете скучать в роли подчиненного.

Отказы

Не бойтесь получать отказы. Получив отказ не стесняйтесь спрашивать причину объясняя тем что это важно для вас чтобы вы могли поработать над собой. Дело в том что компании иногда бояться говорить о причине отказа чтобы вы не попытались их засудить (бывали и такие случаи).

Из моих отказов один был мотивирован тем что:

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


Благодарю тех кто дочитал до этого момента, тех кто не дочитал тоже благодарю.

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

Подробнее..

Полный стек на примере списка задач (React, Vue, TypeScript, Express, Mongoose)

24.12.2020 12:19:48 | Автор: admin


Доброго времени суток, друзья!

В данном туториале я покажу вам, как создать фуллстек-тудушку.

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

  • добавление новой задачи в список
  • обновление индикатора выполнения задачи
  • обновление текста задачи
  • удаление задачи из списка
  • фильтрация задач: все, активные, завершенные
  • сохранение задач на стороне клиента и в базе данных

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


Для более широкого охвата аудитории клиентская часть приложения будет реализована на чистом JavaScript, серверная на Node.js. В качестве абстракции для ноды будет использован Express.js, в качестве базы данных сначала локальное хранилище (Local Storage), затем индексированная база данных (IndexedDB) и, наконец, облачная MongoDB.

При разработке клиентской части будут использованы лучшие практики, предлагаемые такими фреймворками, как React и Vue: разделение кода на автономные переиспользуемые компоненты, повторный рендеринг только тех частей приложения, которые подверглись изменениям и т.д. При этом, необходимый функционал будет реализован настолько просто, насколько это возможно. Мы также воздержимся от смешивания HTML, CSS и JavaScript.

В статье будут приведены примеры реализации клиентской части на React и Vue, а также фуллстек-тудушки на React + TypeScript + Express + Mongoose.

Исходный код всех рассматриваемых в статье проектов находится здесь.

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

Демо нашего приложения:


Итак, поехали.

Клиент


Начнем с клиентской части.

Создаем рабочую директорию, например, javascript-express-mongoose:

mkdir javascript-express-mongoosecd !$code .

Создаем директорию client. В этой директории будет храниться весь клиентский код приложения, за исключением index.html. Создаем следующие папки и файлы:

client  components    Buttons.js    Form.js    Item.js    List.js  src    helpers.js    idb.js    router.js    storage.js  script.js  style.css

В корне проекта создаем index.html следующего содержания:

<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <title>JS Todos App</title>    <!-- Подключаем стили -->    <link rel="stylesheet" href="client/style.css" />  </head>  <body>    <div id="root"></div>    <!-- Подключаем скрипт -->    <script src="client/script.js" type="module"></script>  </body></html>

Стили (client/style.css):
@import url('https://fonts.googleapis.com/css2?family=Stylish&display=swap');* {  margin: 0;  padding: 0;  box-sizing: border-box;  font-family: stylish;  font-size: 1rem;  color: #222;}#root {  max-width: 512px;  margin: auto;  text-align: center;}#title {  font-size: 2.25rem;  margin: 0.75rem;}#counter {  font-size: 1.5rem;  margin-bottom: 0.5rem;}#form {  display: flex;  margin-bottom: 0.25rem;}#input {  flex-grow: 1;  border: none;  border-radius: 4px;  box-shadow: 0 0 1px inset #222;  text-align: center;  font-size: 1.15rem;  margin: 0.5rem 0.25rem;}#input:focus {  outline-color: #5bc0de;}.btn {  border: none;  outline: none;  background: #337ab7;  padding: 0.5rem 1rem;  border-radius: 4px;  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.5);  color: #eee;  margin: 0.5rem 0.25rem;  cursor: pointer;  user-select: none;  width: 102px;  text-shadow: 0 0 1px rgba(0, 0, 0, 0.5);}.btn:active {  box-shadow: 0 0 1px rgba(0, 0, 0, 0.5) inset;}.btn.info {  background: #5bc0de;}.btn.success {  background: #5cb85c;}.btn.warning {  background: #f0ad4e;}.btn.danger {  background: #d9534f;}.btn.filter {  background: none;  color: #222;  text-shadow: none;  border: 1px dashed #222;  box-shadow: none;}.btn.filter.checked {  border: 1px solid #222;}#list {  list-style: none;}.item {  display: flex;  flex-wrap: wrap;  justify-content: space-between;  align-items: center;}.item + .item {  border-top: 1px dashed rgba(0, 0, 0, 0.5);}.text {  flex: 1;  font-size: 1.15rem;  margin: 0.5rem;  padding: 0.5rem;  background: #eee;  border-radius: 4px;}.completed .text {  text-decoration: line-through;  color: #888;}.disabled {  opacity: 0.8;  position: relative;  z-index: -1;}#modal {  position: absolute;  top: 10px;  left: 10px;  padding: 0.5em 1em;  background: rgba(0, 0, 0, 0.5);  border-radius: 4px;  font-size: 1.2em;  color: #eee;}


Наше приложение будет состоять из следующих частей (основные компоненты выделены зеленым, дополнительные элементы синим):


Основные компоненты: 1) форма, включающая поле для ввода текста задачи и кнопку для добавления задачи в список; 2) контейнер с кнопками для фильтрации задач; 3) список задач. Также в качестве основного компонента мы дополнительно выделим элемент списка для обеспечения возможности рендеринга отдельных частей приложения.

Дополнительные элементы: 1) заголовок; 2) счетчик количества невыполненных задач.

Приступаем к созданию компонентов (сверху вниз). Компоненты Form и Buttons являются статическими, а List и Item динамическими. В целях дифференциации статические компоненты экспортируются/импортируются по умолчанию, а в отношении динамических компонентов применяется именованный экспорт/импорт.

client/Form.js:

export default /*html*/ `<div id="form">  <input      type="text"      autocomplete="off"      autofocus      id="input"  >  <button    class="btn"    data-btn="add"  >    Add  </button></div>`

/*html*/ обеспечивает подсветку синтаксиса, предоставляемую расширением для VSCode es6-string-html. Атрибут data-btn позволит идентифицировать кнопку в скрипте.

Обратите внимание, что глобальные атрибуты id позволяют обращаться к DOM-элементам напрямую. Дело в том, что такие элементы (с идентификаторами), при разборе и отрисовке документа становятся глобальными переменными (свойствами глобального объекта window). Разумеется, значения идентификаторов должны быть уникальными для документа.

client/Buttons.js:

export default /*html*/ `<div id="buttons">  <button    class="btn filter checked"    data-btn="all"  >    All  </button>  <button    class="btn filter"    data-btn="active"  >    Active  </button>  <button    class="btn filter"    data-btn="completed"  >    Completed  </button></div>`

Кнопки для фильтрации тудушек позволят отображать все, активные (невыполненные) и завершенные (выполненные) задачи.

client/Item.js (самый сложный компонент с точки зрения структуры):

/** * функция принимает на вход задачу, * которая представляет собой объект, * включающий идентификатор, текст и индикатор выполнения * * индикатор выполнения управляет дополнительными классами * и текстом кнопки завершения задачи * * текст завершенной задачи должен быть перечеркнут, * а кнопка для изменения (обновления) текста такой задачи - отключена * * завершенную задачу можно сделать активной*/export const Item = ({ id, text, done }) => /*html*/ `<li  class="item ${done ? 'completed' : ''}"  data-id="${id}">  <button    class="btn ${done ? 'warning' : 'success'}"    data-btn="complete"  >    ${done ? 'Cancel' : 'Complete'}  </button>  <span class="text">    ${text}  </span>  <button    class="btn info ${done ? 'disabled' : ''}"    data-btn="update"  >    Update  </button>  <button    class="btn danger"    data-btn="delete"  >    Delete  </button></li>`

client/List.js:

/** * для формирования списка используется компонент Item * * функция принимает на вход список задач * * если вам не очень понятен принцип работы reduce * https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce*/import { Item } from "./Item.js"export const List = (todos) => /*html*/ `  <ul id="list">    ${todos.reduce(      (html, todo) =>        (html += `            ${Item(todo)}        `),      ''    )}  </ul>`

С компонентами закончили.

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

src/helpers.js:

/** * данная функция будет использоваться * для визуализации нажатия одной из кнопок * для фильтрации задач * * она принимает элемент - нажатую кнопку и класс - в нашем случае checked * * основной контейнер имеет идентификатор root, * поэтому мы можем обращаться к нему напрямую * из любой части кода, в том числе, из модулей*/export const toggleClass = (element, className) => {  root.querySelector(`.${className}`).classList.remove(className)  element.classList.add(className)}// примерные задачиexport const todosExample = [  {    id: '1',    text: 'Learn HTML',    done: true  },  {    id: '2',    text: 'Learn CSS',    done: true  },  {    id: '3',    text: 'Learn JavaScript',    done: false  },  {    id: '4',    text: 'Stay Alive',    done: false  }]

Создадим базу данных (пока в форме локального хранилища).

src/storage.js:

/** * база данных имеет два метода * get - для получения тудушек * set - для записи (сохранения) тудушек*/export default (() => ({  get: () => JSON.parse(localStorage.getItem('todos')),  set: (todos) => { localStorage.setItem('todos', JSON.stringify(todos)) }}))()

Побаловались и хватит. Приступаем к делу.

src/script.js:

// импортируем компоненты, вспомогательную функцию, примерные задачи и хранилищеimport Form from './components/Form.js'import Buttons from './components/Buttons.js'import { List } from './components/List.js'import { Item } from './components/Item.js'import { toggleClass, todosExample } from './src/helpers.js'import storage from './src/storage.js'// функция принимает контейнер и список задачconst App = (root, todos) => {  // формируем разметку с помощью компонентов и дополнительных элементов  root.innerHTML = `    <h1 id="title">      JS Todos App    </h1>    ${Form}    <h3 id="counter"></h3>    ${Buttons}    ${List(todos)}  `  // обновляем счетчик  updateCounter()  // получаем кнопку добавления задачи в список  const $addBtn = root.querySelector('[data-btn="add"]')  // основной функционал приложения  // функция добавления задачи в список  function addTodo() {    if (!input.value.trim()) return    const todo = {      // такой способ генерации идентификатора гарантирует его уникальность и соответствие спецификации      id: Date.now().toString(16).slice(-4).padStart(5, 'x'),      text: input.value,      done: false    }    list.insertAdjacentHTML('beforeend', Item(todo))    todos.push(todo)    // очищаем поле и устанавливаем фокус    clearInput()    updateCounter()  }  // функция завершения задачи  // принимает DOM-элемент списка  function completeTodo(item) {    const todo = findTodo(item)    todo.done = !todo.done    // рендерим только изменившийся элемент    renderItem(item, todo)    updateCounter()  }  // функция обновления задачи  function updateTodo(item) {    item.classList.add('disabled')    const todo = findTodo(item)    const oldValue = todo.text    input.value = oldValue    // тонкий момент: мы используем одну и ту же кнопку    // для добавления задачи в список и обновления текста задачи    $addBtn.textContent = 'Update'    // добавляем разовый обработчик    $addBtn.addEventListener(      'click',      (e) => {        // останавливаем распространение события для того,        // чтобы нажатие кнопки не вызвало функцию добавления задачи в список        e.stopPropagation()        const newValue = input.value.trim()        if (newValue && newValue !== oldValue) {          todo.text = newValue        }        renderItem(item, todo)        clearInput()        $addBtn.textContent = 'Add'      },      { once: true }    )  }  // функция удаления задачи  function deleteTodo(item) {    const todo = findTodo(item)    item.remove()    todos.splice(todos.indexOf(todo), 1)    updateCounter()  }  // функция поиска задачи  function findTodo(item) {    const { id } = item.dataset    const todo = todos.find((todo) => todo.id === id)    return todo  }  // дополнительный функционал  // функция фильтрации задач  // принимает значение кнопки  function filterTodos(value) {    const $items = [...root.querySelectorAll('.item')]    switch (value) {      // отобразить все задачи      case 'all':        $items.forEach((todo) => (todo.style.display = ''))        break      // активные задачи      case 'active':        // отобразить все и отключить завершенные        filterTodos('all')        $items          .filter((todo) => todo.classList.contains('completed'))          .forEach((todo) => (todo.style.display = 'none'))        break      // завершенные задачи      case 'completed':        // отобразить все и отключить активные        filterTodos('all')        $items          .filter((todo) => !todo.classList.contains('completed'))          .forEach((todo) => (todo.style.display = 'none'))        break    }  }  // функция обновления счетчика  function updateCounter() {    // считаем количество невыполненных задач    const count = todos.filter((todo) => !todo.done).length    counter.textContent = `      ${count > 0 ? `${count} todo(s) left` : 'All todos completed'}    `    if (!todos.length) {      counter.textContent = 'There are no todos'      buttons.style.display = 'none'    } else {      buttons.style.display = ''    }  }  // функция повторного рендеринга изменившегося элемента  function renderItem(item, todo) {    item.outerHTML = Item(todo)  }  // функция очистки инпута  function clearInput() {    input.value = ''    input.focus()  }  // делегируем обработку событий корневому узлу  root.onclick = ({ target }) => {    if (target.tagName !== 'BUTTON') return    const { btn } = target.dataset    if (target.classList.contains('filter')) {      filterTodos(btn)      toggleClass(target, 'checked')    }    const item = target.parentElement    switch (btn) {      case 'add':        addTodo()        break      case 'complete':        completeTodo(item)        break      case 'update':        updateTodo(item)        break      case 'delete':        deleteTodo(item)        break    }  }  // обрабатываем нажатие Enter  document.onkeypress = ({ key }) => {    if (key === 'Enter') addTodo()  }  // оптимизация работы с хранилищем  window.onbeforeunload = () => {    storage.set(todos)  }}// инициализируем приложения;(() => {  // получаем задачи из хранилища  let todos = storage.get('todos')  // если в хранилище пусто  if (!todos || !todos.length) todos = todosExample  App(root, todos)})()

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

Однако, с использованием локального хранилища в качестве базы данных сопряжено несколько проблем: 1) ограниченный размер около 5 Мб, зависит от браузера; 2) потенциальная возможность потери данных при очистке хранилищ браузера, например, при очистке истории просмотра страниц, нажатии кнопки Clear site data вкладки Application Chrome DevTools и т.д.; 3) привязка к браузеру невозможность использовать приложение на нескольких устройствах.

Первую проблему (ограниченность размера хранилища) можно решить с помощью IndexedDB.

Индексированная база данных имеет довольно сложный интерфейс, поэтому воспользуемся абстракцией Jake Archibald idb-keyval. Копируем этот код и записываем его в файл src/idb.js.

Вносим в src/script.js следующие изменения:

// import storage from './src/storage.js'import { get, set } from './src/idb.js'window.onbeforeunload = () => {  // storage.set(todos)  set('todos', todos)}// обратите внимание, что функция инициализации приложения стала асинхронной;(async () => {  // let todos = storage.get('todos')  let todos = await get('todos')  if (!todos || !todos.length) todos = todosExample  App(root, todos)})()

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

React, Vue

Ниже приводятся примеры реализации клиентской части тудушки на React и Vue.

React:


Vue:


База данных


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

  1. Создаем аккаунт в MongoDB Atlas
  2. Во вкладке Projects нажимаем на кнопку New Project
  3. Вводим название проекта, например, todos-db, и нажимаем Next
  4. Нажимаем Create Project
  5. Нажимаем Build a Cluster
  6. Нажимаем Create a cluster (FREE)
  7. Выбираем провайдера и регион, например, Azure и Hong Kong, и нажимаем Create Cluster
  8. Ждем завершения создания кластера и нажимаем connect
  9. В разделе Add a connection IP address выбираем либо Add Your Current IP Address, если у вас статический IP, либо Allow Access from Anywhere, если у вас, как в моем случае, динамический IP (если сомневаетесь, выбирайте второй вариант)
  10. Вводим имя пользователя и пароль, нажимаем Create Database User, затем нажимаем Choose a connection method
  11. Выбираем Connect your application
  12. Копируем строку из раздела Add your connection string into your application code
  13. Нажимаем Close












В корневой директории создаем файл .env и вставляем в него скопированную строку (меняем <username>, <password> и <dbname> на свои данные):

MONGO_URI=mongodb+srv://<username>:<password>@cluster0.hfvcf.mongodb.net/<dbname>?retryWrites=true&w=majority

Сервер


Находясь в корневой директории, инициализируем проект:

npm init -y// илиyarn init -yp

Устанавливаем основные зависимости:

yarn add cors dotenv express express-validator mongoose

  • cors отключает политику общего происхождения (одного источника)
  • dotenv предоставляет доступ к переменным среды в файле .env
  • express облегчает создание сервера на Node.js
  • express-validator служит для проверки (валидации) данных
  • mongoose облегчает работу с MongoDB

Устанавливаем зависимости для разработки:

yarn add -D nodemon open-cli morgan

  • nodemon запускает сервер и автоматически перезагружает его при внесении изменений в файл
  • open-cli открывает вкладку браузера по адресу, на котором запущен сервер
  • morgan логгер HTTP-запросов

Далее добавляем в package.json скрипты для запуска сервера (dev для запуска сервера для разработки и start для продакшн-сервера):

"scripts": {  "start": "node index.js",  "dev": "open-cli http://localhost:1234 && nodemon index.js"},

Отлично. Создаем файл index.js следующего содержания:

// подключаем библиотекиconst express = require('express')const mongoose = require('mongoose')const cors = require('cors')const morgan = require('morgan')require('dotenv/config')// инициализируем приложение и получаем роутерconst app = express()const router = require('./server/router')// подключаем промежуточное ПОapp.use(express.json())app.use(express.urlencoded({ extended: false }))app.use(cors())app.use(morgan('dev'))// указываем, где хранятся статические файлыapp.use(express.static(__dirname))// подлючаемся к БДmongoose.connect(  process.env.MONGO_URI,  {    useNewUrlParser: true,    useUnifiedTopology: true,    useFindAndModify: false,    useCreateIndex: true  },  () => console.log('Connected to database'))// возвращаем index.html в ответ на запрос к корневому узлуapp.get('/', (_, res) => {  res.sendFile(__dirname + '/index.html')})// при запросе к api передаем управление роутеруapp.use('/api', router)// определяем порт и запускаем серверconst PORT = process.env.PORT || 1234app.listen(PORT, () => console.log(`Server is running`))

Тестируем сервер:

yarn dev// илиnpm run dev



Прекрасно, сервер работает. Теперь займемся маршрутизацией. Но перед этим определим схему данных, которые мы будем получать от клиента. Создаем директорию server для хранения серверных файлов. В этой директории создаем файлы Todo.js и router.js.

Структура проекта на данном этапе:

client  components    Buttons.js    Form.js    Item.js    List.js  src    helpers.js    idb.js    storage.js  script.js  style.cssserver  Todo.js  router.js.envindex.htmlindex.jspackage.jsonyarn.lock (либо package-lock.json)

Определяем схему в src/Todo.js:

const { Schema, model } = require('mongoose')const todoSchema = new Schema({  id: {    type: String,    required: true,    unique: true  },  text: {    type: String,    required: true  },  done: {    type: Boolean,    required: true  }})// экспорт модели данныхmodule.exports = model('Todo', todoSchema)

Настраиваем маршрутизацию в src/router.js:

// инициализируем роутерconst router = require('express').Router()// модель данныхconst Todo = require('./Todo')// средства валидацииconst { body, validationResult } = require('express-validator')/** * наш интерфейс (http://personeltest.ru/away/localhost:1234/api) * будет принимать и обрабатывать 4 запроса * GET-запрос /get - получение всех задач из БД * POST /add - добавление в БД новой задачи * DELETE /delete/:id - удаление задачи с указанным идентификатором * PUT /update - обновление текста или индикатора выполнения задачи * * для работы с БД используется модель Todo и методы * find() - для получения всех задач * save() - для добавления задачи * deleteOne() - для удаления задачи * updateOne() - для обновления задачи * * ответ на запрос - объект, в свойстве message которого * содержится сообщение либо об успехе операции, либо об ошибке*/// получение всех задачrouter.get('/get', async (_, res) => {  const todos = (await Todo.find()) || []  return res.json(todos)})// добавление задачиrouter.post(  '/add',  // пример валидации  [    body('id').exists(),    body('text').notEmpty().trim().escape(),    body('done').toBoolean()  ],  async (req, res) => {    // ошибки - это результат валидации    const errors = validationResult(req)    if (!errors.isEmpty()) {      return res.status(400).json({ message: errors.array()[0].msg })    }    const { id, text, done } = req.body    const todo = new Todo({      id,      text,      done    })    try {      await todo.save()      return res.status(201).json({ message: 'Todo created' })    } catch (error) {      return res.status(500).json({ message: `Error: ${error}` })    }  })// удаление задачиrouter.delete('/delete/:id', async (req, res) => {  try {    await Todo.deleteOne({      id: req.params.id    })    res.status(201).json({ message: 'Todo deleted' })  } catch (error) {    return res.status(500).json({ message: `Error: ${error}` })  }})// обновление задачиrouter.put(  '/update',  [    body('text').notEmpty().trim().escape(),    body('done').toBoolean()  ],  async (req, res) => {    const errors = validationResult(req)    if (!errors.isEmpty()) {      return res.status(400).json({ message: errors.array()[0].msg })    }    const { id, text, done } = req.body    try {      await Todo.updateOne(        {          id        },        {          text,          done        }      )      return res.status(201).json({ message: 'Todo updated' })    } catch (error) {      return res.status(500).json({ message: `Error: ${error}` })    }})// экспорт роутераmodule.exports = router

Интеграция


Возвращаемся к клиентской части. Для того, чтобы абстрагировать отправляемые клиентом запросы мы также прибегнем к помощи роутера. Создаем файл client/src/router.js:

/** * наш роутер - это обычная функция, * принимающая адрес конечной точки в качестве параметра (url) * * функция возвращает объект с методами: * get() - для получения всех задач из БД * set() - для добавления в БД новой задачи * update() - для обновления текста или индикатора выполнения задачи * delete() - для удаления задачи с указанным идентификатором * * все методы, кроме get(), принимают на вход задачу * * методы возвращают ответ от сервера в формате json * (объект со свойством message)*/export const Router = (url) => ({  // получение всех задач  get: async () => {    const response = await fetch(`${url}/get`)    return response.json()  },  // добавление задачи  set: async (todo) => {    const response = await fetch(`${url}/add`, {      method: 'POST',      headers: {        'Content-Type': 'application/json'      },      body: JSON.stringify(todo)    })    return response.json()  },  // обновление задачи  update: async (todo) => {    const response = await fetch(`${url}/update`, {      method: 'PUT',      headers: {        'Content-Type': 'application/json'      },      body: JSON.stringify(todo)    })    return response.json()  },  // удаление задачи  delete: async ({ id }) => {    const response = await fetch(`${url}/delete/${id}`, {      method: 'DELETE'    })    return response.json()  }})

Для того, чтобы сообщать пользователю о результате выполнения CRUD-операции (create, read, update, delete создание, чтение, обновление, удаление), добавим в src/helpers.js еще одну вспомогательную функцию:

// функция создает модальное окно с сообщением о результате операции// и удаляет его через две секундыexport const createModal = ({ message }) => {  root.innerHTML += `<div data-id="modal">${message}</div>`  const timer = setTimeout(() => {    root.querySelector('[data-id="modal"]').remove()    clearTimeout(timer)  }, 2000)}

Вот как выглядит итоговый вариант client/script.js:

import Form from './components/Form.js'import Buttons from './components/Buttons.js'import { List } from './components/List.js'import { Item } from './components/Item.js'import { toggleClass, createModal, todosExample } from './src/helpers.js'// импортируем роутер и передаем ему адрес конечной точкиimport { Router } from './src/router.js'const router = Router('http://localhost:1234/api')const App = (root, todos) => {  root.innerHTML = `    <h1 id="title">      JS Todos App    </h1>    ${Form}    <h3 id="counter"></h3>    ${Buttons}    ${List(todos)}  `  updateCounter()  const $addBtn = root.querySelector('[data-btn="add"]')  // основной функционал  async function addTodo() {    if (!input.value.trim()) return    const todo = {      id: Date.now().toString(16).slice(-4).padStart(5, 'x'),      text: input.value,      done: false    }    list.insertAdjacentHTML('beforeend', Item(todo))    todos.push(todo)    // добавляем в БД новую задачу и сообщаем о результате операции пользователю    createModal(await router.set(todo))    clearInput()    updateCounter()  }  async function completeTodo(item) {    const todo = findTodo(item)    todo.done = !todo.done    renderItem(item, todo)    // обновляем индикатор выполнения задачи    createModal(await router.update(todo))    updateCounter()  }  function updateTodo(item) {    item.classList.add('disabled')    const todo = findTodo(item)    const oldValue = todo.text    input.value = oldValue    $addBtn.textContent = 'Update'    $addBtn.addEventListener(      'click',      async (e) => {        e.stopPropagation()        const newValue = input.value.trim()        if (newValue && newValue !== oldValue) {          todo.text = newValue        }        renderItem(item, todo)        // обновляем текст задачи        createModal(await router.update(todo))        clearInput()        $addBtn.textContent = 'Add'      },      { once: true }    )  }  async function deleteTodo(item) {    const todo = findTodo(item)    item.remove()    todos.splice(todos.indexOf(todo), 1)    // удаляем задачу    createModal(await router.delete(todo))    updateCounter()  }  function findTodo(item) {    const { id } = item.dataset    const todo = todos.find((todo) => todo.id === id)    return todo  }  // дальше все тоже самое  // за исключением window.onbeforeunload  function filterTodos(value) {    const $items = [...root.querySelectorAll('.item')]    switch (value) {      case 'all':        $items.forEach((todo) => (todo.style.display = ''))        break      case 'active':        filterTodos('all')        $items          .filter((todo) => todo.classList.contains('completed'))          .forEach((todo) => (todo.style.display = 'none'))        break      case 'completed':        filterTodos('all')        $items          .filter((todo) => !todo.classList.contains('completed'))          .forEach((todo) => (todo.style.display = 'none'))        break    }  }  function updateCounter() {    const count = todos.filter((todo) => !todo.done).length    counter.textContent = `      ${count > 0 ? `${count} todo(s) left` : 'All todos completed'}    `    if (!todos.length) {      counter.textContent = 'There are no todos'      buttons.style.display = 'none'    } else {      buttons.style.display = ''    }  }  function renderItem(item, todo) {    item.outerHTML = Item(todo)  }  function clearInput() {    input.value = ''    input.focus()  }  root.onclick = ({ target }) => {    if (target.tagName !== 'BUTTON') return    const { btn } = target.dataset    if (target.classList.contains('filter')) {      filterTodos(btn)      toggleClass(target, 'checked')    }    const item = target.parentElement    switch (btn) {      case 'add':        addTodo()        break      case 'complete':        completeTodo(item)        break      case 'update':        updateTodo(item)        break      case 'delete':        deleteTodo(item)        break    }  }  document.onkeypress = ({ key }) => {    if (key === 'Enter') addTodo()  }};(async () => {  // получаем задачи из БД  let todos = await router.get()  if (!todos || !todos.length) todos = todosExample  App(root, todos)})()

Поздравляю, вы только что создали полноценную фуллстек-тудушку.

TypeScript

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



Заключение


Подведем краткие итоги.

Мы с вами реализовали полноценное клиент-серверное приложение для добавления, редактирования и удаления задач из списка, интегрированное с настоящей базой данных. На клиенте мы использовали самый современный (чистый) JavaScript, на сервере Node.js сквозь призму Express.js, для взаимодействия с БД Mongoose. Мы рассмотрели парочку вариантов хранения данных на стороне клиента (local storage, indexeddb idb-keyval). Также мы увидели примеры реализации клиентской части на React (+TypeScript) и Vue. По-моему, очень неплохо для одной статьи.

Буду рад любой форме обратной связи. Благодарю за внимание и хорошего дня.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru