Порог вхождения в Angular 2 — теория и практика

от автора

Добрый день, дорогие хабра: жители, читатели, писатели, негативно-комментаторы 🙂

В качестве вводной части и чтобы снять некоторые вопросы немного расскажу о себе.
Меня зовут Тамара. Оужас, я девушка! Кого это пугает — закрывайте статью и не читайте.

Для остальных: у меня за плечам незаконченный лет 10 назад МИРЭА, факультет кибернетики. Но все эти 10 лет практики сложились таким образом, что по большей части я занималась рекламой и в перерывах случалось работать в различных стартапах, связанных с интернетом и не только.
image
В общем, если коротко, то чукча не программист, чукча просто душой и сердцем уважает тех, кто из непонятных строчек кода делает офигенные вещи, которые хорошо работают.

Я покривлю душой, если скажу, что я не могу разобраться в чужом коде. Могу, на java и php могу даже какие-то простые вещи поправить. Но дальше этого мой опыт программирования никогда не уходил.
image
Но это же все не то, душа просила поэзии с чистого листа. И вот прекратив на некоторое время свою трудовую деятельность и взяв длительный отпуск для души и тела я таки решила попробовать что-то сделать с 0 и самостоятельно. Под "что-то" я понимаю свой маленький проект.

Когда думала и выбирала на чем делать, то для бэкенда остановилась на PHP. А точнее на фреймворке — Laravel.
На нем я остановилась по той причине, что для меня он показался самым низким по порогу вхождения. Мне не нравится в нем документация, так как с моей точки зрения многие моменты не раскрыты и приходится лезть в исходники, чтобы почитать комментарии. Но основные общие моменты разобраны на многих ресурсах. Laracasts как источник обучения весьма грустен. Тейлор там рассматривает все достаточно поверхностно, перескакивая с одного на другое и совершенно не углубляясь. Все по верхам.

Для фронтенда я выбрала Angular 2. Да, я знаю, что он в beta-режиме :), но мне он опять же показался логичным.
Для въезжания в Angular2 я пользуюсь их документацией, исходниками на github, чтения issue там же, stackoverflow — но там как-то все сейчас грустно — задают вопросы в основном ответы на которые есть в документации.

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

image

Тут не будет примеров todo и helloworld.
Я покажу маленький пример того, что я сейчас ковыряю и как у меня это работает.
В кусочке будет получение данных через api, вывод их, и отправка формы.

Настройка Angular 2 и Laravel.

Я не буду заострять на этом внимание. Для Angular 2 — вся базовая настройка проекта написана в их 5-и минутном туториале HelloWorld.
С Laravel тоже базовое создание проекта описано в документации.

Остановлюсь поподробнее только на том моменте, который меня на старте поставили в тупик.

Когда я начинала проект меня волновал вопрос взаимодействия этих товарищей в плане роутинга. А именно, если грузить Angular в папку public, то у меня лично возникли проблемы с роутингом. Так как у Laravel свой роутинг, который с роутингом Angular у меня вообще никак не совпадал, а манипуляции c отдачей нужных роутов не привели к нужному результату. При возврате через браузер на предыдущую страницу мне постоянно выбрасывалась laravelевская страница с ошибкой. Убив пару часов, чтобы подружить этих товарищей я приняла решение разнести по разным доменам api(бэкенд) и фронтенд. Как по мне, так в случае замены одной или другой части целого я не буду зависеть от незаменяемой части.
Так, что, условно сейчас я имею два проекта. Один, условно, крутится на домене: api.proect.dev, а второй на: proect.dev

Так как я все-таки заявила в заголовке, про порог вхождения именно в Angular, то я не буду подробно останавливаться на API.

Быстренько сделаем бэкенд

Если коротко, то наша работа во фронтенде будет по 2 запросам к бэкенду. По одному запросу мы получаем данные из таблицы, по второму мы туда их записываем 🙂 Элементарно, Ватсон 🙂
Далее я просто приведу куски кода бэкенда с комментариями в самом же коде, чтобы нам дальше двигаться.

Кому это надо — заглядывайте

php artisan make:model MainCategory -m

Эта команда создаст нам модель MainСategory и миграцию для этой модели.
В миграцию вставляем нужные нам строчки.

Миграция — как она выглядит

2016_02_22_135455_create_main_categories_table.php

<?php  use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration;  class CreateMainCategoriesTable extends Migration {     /**      * Run the migrations.      *      * @return void      */     public function up()     {         Schema::create('main_categories', function (Blueprint $table) {             $table->increments('id');             $table->string('name', 255)->unique(); //это у меня будет название категории.              $table->string('slug', 255)->unique(); //это ссылка на эту категорию             $table->boolean('show')->default(0); // тут статус публикации категории на сайте. Если true(1) - тогда показываем, если false(0) - нет.             $table->timestamps();             $table->softDeletes();         });     }      /**      * Reverse the migrations.      *      * @return void      */     public function down()     {         Schema::drop('main_categories');     } }

Модель — как она выглядит

MainCategory.php

<?php namespace App\Models\Catalog;  use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes;  /**  * Class MainCategory  *  * @package App  *  * @property integer  $id $primaryKey $autoincrement  * @property string   $name $unique  * @property string   slug $unique  * @property boolean  show  * @property datetime created_at  * @property datetime updated_at  * @property datetime deleted_at  */ class MainCategory extends Model {     use SoftDeletes;      protected $fillable = ['name', 'slug', 'show'];      protected $guarded = ['id'];      protected $dates = ['created_at', 'updated_at', 'deleted_at'];  }

Ну и собственно контроллер, который со стороны php будет определять в каком виде данные получать, как их из базы вытаскивать, как запихивать их обратно. Он создается командой php artisan make:controller MainCategoryController
У меня он лежит в своей папочке с названием Catalog, обращаю на это внимание, так как дальше в роутах он обязательно проскользнет.
Так, как чтобы со стороны бэкенда не плодить ненужны папки-подпапки я решила, что в тематическом контроллере под разными названиями плодить нужные мне запросы 🙂

Контроллер — как он выглядит

MainCategoryController.php

<?php  namespace App\Http\Controllers\Catalog;  use App\Models\Catalog\MainCategory; use Illuminate\Http\Request;  use App\Http\Requests; use App\Http\Controllers\Controller;  /**  * @api  * @package     App\Http\Controllers\Catalog  * @class    MainCategoryController  */ class MainCategoryController extends Controller {     /**      * Возвращает список всех категорий каталога со всеми полями      * @function indexAdmin      * @return mixed $main_categories      */     public function indexAdmin()     {         $main_categories = MainCategory::all();         return $main_categories;     }      /**      * @function createAdmin      * Создание новой категории каталога. Доступно только в административном функционале      *      * @param Request $request      */     public function createAdmin(Request $request)     {         $main_category = new MainCategory;         $main_category->name = $request->name;         $main_category->slug = $request->slug;         $main_category->show = $request->show;         $main_category->save();     } }

Ну и последнее, что осталось сделать — это прописать пути. Вот кусочек route.php и 2 пути по которым мы и будем запрашивать нужную нам информацию.

Пути

Route::group(['middleware' => 'cors'], function() {     Route::group(['middleware' => 'api'], function () {             Route::group(['prefix' => 'backend'], function () {                 Route::group(['namespace' => 'Catalog', 'prefix' => 'catalog'], function () {                     Route::get('/main-categories', 'MainCategoryController@indexAdmin');                     Route::post('/main-category/create', 'MainCategoryController@createAdmin');                 });             });     }); });

На выходе мы на самом деле получаем 2 ссылки:

get: http://api.project.dev/backend/catalog/main-categories post: http://api.project.dev/backend/catalog/main-category/create

На этом миссия по настройке бэкенд завершена.

Ура! Обещанный Angular 2.

Ну теперь начинается самое интересное.
Так как я пока еще не определилась окончательно со структурой в самом проекте и что и как на страницах буду отображать, то вот скрин того, как это сейчас у меня выглядит. Единственное, что для habra я кусочки шаблонов внесу в сами .ts скрипты, хотя у меня они сейчас вынесены в отдельные html.
image

Как я уже говорила — за исходник я брала базовую конфигурация из туториала. Поэтому тут ничего особенного нет. Ну, кроме, что main.ts я переименовала для себя в boot.ts 🙂

index.html
Единственное, на что здесь стоит обратить внимание, так это на то, что к базовым скриптам добавлены

<script src="node_modules/angular2/bundles/router.dev.js"></script> <script src="node_modules/angular2/bundles/http.dev.js"></script>

Без этих товарищей не будут работать роуты и запросы-ответы к API.

Полный вариант index.html

<html> <head>     <base href="/">     <title>Angular 2 QuickStart</title>     <meta name="viewport" content="width=device-width, initial-scale=1">      <!-- 1. Load libraries -->     <!-- IE required polyfills, in this exact order -->     <script src="node_modules/es6-shim/es6-shim.js"></script>     <script src="node_modules/systemjs/dist/system-polyfills.js"></script>     <script src="node_modules/angular2/es6/dev/src/testing/shims_for_IE.js"></script>     <script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>     <script src="node_modules/systemjs/dist/system.src.js"></script>     <script src="node_modules/rxjs/bundles/Rx.js"></script>     <script src="node_modules/angular2/bundles/angular2.dev.js"></script>     <script src="node_modules/angular2/bundles/router.dev.js"></script>     <script src="node_modules/angular2/bundles/http.dev.js"></script>     <!-- 2. Configure SystemJS -->     <script>         System.config({             packages: {                 app: {                     format: 'register',                     defaultExtension: 'js'                 }             }         });         System.import('app/boot')                 .then(null, console.error.bind(console));     </script> </head> <!-- 3. Display the application --> <body> <shop-app>Loading...</shop-app> </body> </html>

В приложении сейчас есть 2 роута: это главная страница, на которую можно вернуться и это страница с отображением всех категорий и добавлением новой.

Роуты у меня расположены в app.component.ts. И, соответственно он же у меня является тем самым входным компонентом, который и видно в виде тэгов <shop-app></shop-app> на главной странице.

Полный вариант app.component.ts

import {Component} from 'angular2/core'; import {RouteConfig, ROUTER_DIRECTIVES, ROUTER_PROVIDERS} from "angular2/router"; import {HomePageComponent} from "./home-page/home-page.component" import {DashboardMainCategoryComponent} from "./dashboard/catalog/main-category/main-category.root.component";  @Component({     selector: 'shop-app',     template: `     <a [routerLink]="['HomePage']">Главная</a>     <a [routerLink]="['/DashboardMainCategory']">Категории</a>     <router-outlet></router-outlet>     `,     directives: [ROUTER_DIRECTIVES],     providers: [ROUTER_PROVIDERS] })  @RouteConfig([     {         path: '/',         name: 'HomePage',         component: HomePageComponent,         useAsDefault: true     },      {         path: '/main-category',         name: 'DashboardMainCategory',         component: DashboardMainCategoryComponent     }  ]) export class ShopAppComponent { }

Собственно, чтобы роуты заработали нам осталось всего-ничего — добавить соответствующие компоненты: HomePageComponent и DashboardMainCategoryComponent.

Полный вариант HomePageComponent — home-page.component.ts

import {Component} from "angular2/core";  @Component({     selector: 'home-page',     template: '<h1>Главная страница</h1>' })  export class HomePageComponent {}

Полный вариант DashboardMainCategoryComponent — main-category.root.component.ts

import {Component} from "angular2/core";  @Component({     selector: 'dashboard-main-category',     template: '<h1>Категории</h1>' })  export class DashboardMainCategoryComponent {}

Так, сделали. Теперь надо пойти в boot.ts и импортировать основной компонент ShopAppComponent.

boot.ts
Это самый пустой компонент в моем проекте 🙂 У меня он ничего не делает, кроме как загружает все, что нужно из основного компонента с названием app.component.ts

Полный вариант boot.ts

import {bootstrap} from 'angular2/platform/browser' import {ShopAppComponent} from "./app.component";  bootstrap(ShopAppComponent);

На этом с роутами мы закончили. И, если сейчас сделать npm run start, то у вас уже будет сайт на котором можно попрыгать между двумя страничками.

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

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

Базовая модель MainCategory

Перво-наперво нам надо сделать простой класс — аналог Модели на php, pojo — на java.
Давайте его обзовем аутентично: main-category.ts

Полный вариант main-category.ts

export class MainCategory{     constructor(         public id: number,         public name: string,         public slug: string,         public show: boolean,         public created_at: string,         public updated_at: string,         public deleted_at: string     ) {} }

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

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

MainCategoryService

Ладно, первый шаг сделали. Потопали дальше. Если заглянуть в ARCHITECTURE OVERVIEW Angular2, там они предлагают придерживаться той идеи, что ту часть приложения, которая что-то делает (например, авторизация, логгирование, калькулятор пошлины или, как в нашем случае — общение по API) надо называть service и выносить в отдельный файл, который мы потом будем импортировать туда, куда надо. Это необязательно, но желательно. Я так и поступила. Отсюда у меня появился main-category.service.ts

Полный вариант main-category.service.ts

import {Injectable} from "angular2/core"; import {Http, Headers, RequestOptions, Response} from "angular2/http"; import {Observable} from "rxjs/Observable"; import 'rxjs/Rx'; //без этого импорта у нас любое общение с API будет заканчиваться ошибками. Временная фича, которую обещают найти и устранить import {MainCategory} from "./main-category";  //@Injectable - декоратор, который передает данные о нашем сервисе. @Injectable() export class MainCategoryService {      constructor (private http: Http) {}      //так как у меня по разным ссылкам запрос и отправка данных, то я сделала 2 переменные с их указанием. Если вдруг что поменяется в ссылках, то мне не надо будет разыскивать по всему документу :) Удобно     private _getAdminMainCategories = 'http://api.shops.dev:8080/backend/catalog/main-categories';     private _createAdminMainCategory = 'http://api.shops.dev:8080/backend/catalog/main-category/create';     //запрашиваем все категории каталога     getAdminMainCategories() {         //обращаемся к API через get         return this.http.get(this._getAdminMainCategories)                         //тут мы принимаем событие и возвращаем некоторые данные. В нашем случае - массив категорий в json формате                         .map(res => <MainCategory[]> res.json())                         .catch(this.handleError);     }      //создаем категорию каталога. Так как мы заранее знаем какие данные и в каком виде нам приходят, то мы указываем, что будем получать и передавать     createAdminMainCategory(name:String, slug:String, show:boolean) : Observable<MainCategory> {         //преобразуем данные в JSON-строку. Обещают, что потом нам эта строчка не будет нужна         let body = JSON.stringify({name, slug, show});         //устанавливаем нужный нам заголовок         let headers = new Headers({ 'Content-Type': 'application/json' });         let options = new RequestOptions({ headers: headers });          //отправляем данные         return this.http.post(this._createAdminMainCategory, body, options)             .map(res =>  <MainCategory> res.json())             .catch(this.handleError)     }      private handleError (error: Response) {         //in a real world app, we may send the error to some remote logging infrastructure         //instead of just logging it to the console         console.error(error);         return Observable.throw(error.json().error || 'Server error');     } }

На этом основное взаимодействие с сервером мы описали. Осталась сущая ерунда — пара компонентов и дело в шляпе!

GetMainCategories

Начнем с компонента, который получает данные: main-category.get.component.ts

Полный вариант main-category.get.component.ts`

import {Component} from "angular2/core"; import {MainCategoryService} from "./main-category.service"; import {OnInit} from "angular2/core"; import {MainCategory} from "./main-category";  @Component({     selector: 'backend-get-main-categories',     templateUrl: 'app/dashboard/catalog/main-category/main-category.get.template.html',     providers: [MainCategoryService] //в качестве провайдера как раз указываем созданный нами сервис })  export class BackendGetMainCategories implements OnInit {      constructor (private _mainCategoryService: MainCategoryService) {}      errorMessage: string;     mainCategories: MainCategory[];      ngOnInit() {         this.getAdminMainCategories();     }     //обращаемся к созданному нами сервису, конкретно к getAdminMainCategories     getAdminMainCategories() {         this._mainCategoryService.getAdminMainCategories()                                 .subscribe(                                     mainCategories => this.mainCategories = mainCategories,                                     error => this.errorMessage = <any>error                                 );     } }

Полный вариант шаблона main-category.get.template.html

<h1>Категории каталога</h1> <table>     <thead>     <tr>         <th>id</th>         <th>name</th>         <th>slug</th>         <th>show</th>         <th>created_at</th>         <th>updated_at</th>         <th>deleted_at</th>     </tr>     </thead>     <tbody>    <!--Angular повторяет строку до тех пор пока у нас данные не закончатся :)-->     <tr *ngFor="#mainCategory of mainCategories">         <td>{{ mainCategory.id }}</td>         <td>{{ mainCategory.name }}</td>         <td>{{ mainCategory.slug }}</td>         <td>{{ mainCategory.show }}</td>         <td>{{ mainCategory.created_at }}</td>         <td>{{ mainCategory.updated_at }}</td>         <td>{{ mainCategory.deleted_at }}</td>     </tr>     </tbody> </table>

PostMainCategory

В Angular2 есть два способа создания форм — template и data-driven. Принципиальное отличие у них в том, что в template — все проверки пишутся в самом шаблоне. Т.е. это более близко к Angular1. Data-driven — это нововведение в Angular2 и все проверки уходят из шаблона. Ну это пока то как я для себя поняла эту разницу. Боюсь, что тему я до конца не раскрыла, так как в голове по поводу этих форм еще каша. Честно сказать — второй вариант с формами мне показался проще и чище. Но с ним есть сейчас много своих косяков.

Полный вариант шаблона main-category.create.component.html

import {Component} from "angular2/core"; import {MainCategoryService} from "./main-category.service"; import {OnInit} from "angular2/core"; import {FORM_DIRECTIVES} from "angular2/common"; import {FORM_PROVIDERS} from "angular2/common"; import {ControlGroup} from "angular2/common"; import {FormBuilder} from "angular2/common"; import {Validators} from "angular2/common"; import {MainCategory} from "./main-category"; import {HTTP_PROVIDERS} from "angular2/http";  @Component({     selector: 'backend-create-main-category',     templateUrl: 'app/dashboard/catalog/main-category/main-category.create.component.html',     providers: [MainCategoryService, FORM_PROVIDERS, HTTP_PROVIDERS],     directives: [FORM_DIRECTIVES] })  export class BackendCreateMainCategory implements OnInit {     //сообщаем что у нас есть группа контроллеров в нашей форме и она одна :)      createMainCategoryForm: ControlGroup;     mainCategories:MainCategory[];     errorMessage: string;      constructor( private _formBuilder: FormBuilder, private _mainCategoryService: MainCategoryService) {}        //то о чем я писала - наши проверки вынесены из шаблона       ngOnInit() {         this.createMainCategoryForm = this._formBuilder.group({             'name': ['', Validators.required],             'slug': ['', Validators.required],             'show': [false]         });     }       //при сабмите формы отправляем данные на сервер      onSubmit() {         var name = this.createMainCategoryForm.value.name;         var slug = this.createMainCategoryForm.value.slug;         var show = this.createMainCategoryForm.value.show;         this._mainCategoryService.createAdminMainCategory(name, slug, show).subscribe(           main_category => this.mainCategories.push(main_category),             error => this.errorMessage = <any>error         );      }

Полный вариант шаблона main-category.create.template.html

<h1>Создать категорию каталога</h1>  <form [ngFormModel]="createMainCategoryForm" (ngSubmit)="onSubmit()">     <div>         <label for="name">Название</label>         <input type="text" id="name" [ngFormControl]="createMainCategoryForm.controls['name']">     </div>     <div>         <label for="slug">Ссылка</label>         <input type="text" id="slug" [ngFormControl]="createMainCategoryForm.controls['slug']">     </div>     <div>         <label for="show">Опубликовать?</label>         <input type="checkbox" id="show" [ngFormControl]="createMainCategoryForm.controls['show']">     </div>     <button type="submit">Сохранить</button> </form>

К сожалению radiobutton пока шалит в Angular2 и работать может, но только после длительных плясок с бубном, так, что для своих нужд я остановилась пока на checkbox.

Осталось все нужное импортировать в наш класс DashboardMainCategoryComponent. Теперь он будет выглядеть вот так:

Полный вариант main-category.root.component.ts

import {Component} from "angular2/core"; import {FORM_DIRECTIVES} from "angular2/common"; import {ControlGroup} from "angular2/common"; import {Control} from "angular2/common"; import {FormBuilder} from "angular2/common"; import {Validators} from "angular2/common"; import {MainCategoryService} from "./main-category.service"; import {HTTP_PROVIDERS} from "angular2/http"; import {BackendGetMainCategories} from "./main-category.get.component"; import {BackendCreateMainCategory} from "./main-category.create.component"; @Component({     selector: 'dashboard-main-category',     template:`     <h1>Категории</h1>     <backend-get-main-categories></backend-get-main-categories>      <backend-create-main-category></backend-create-main-category>     `,     directives: [         FORM_DIRECTIVES,         BackendGetMainCategories,         BackendCreateMainCategory],     providers: [MainCategoryService, HTTP_PROVIDERS] })  export class DashboardMainCategoryComponent {}

На этом мы имеем простое приложение с получением и отправкой данных на сервер.

Итоги

Если взять чистое время, которое у меня заняло написать то, что я выложила выше и заставить это работать:
Backend — 1 час 17 минут. Это не совсем чистое время, а вместе с загрузкой PhpStorm, хождениями на перекуры и отвлечениями на телефонные разговоры. Для меня это достаточно просто, так как все таки php я не первый раз вижу.
С Angular2 все сложнее.
Я никогда не копалась в JS. Нет, скриптик подключить я могла по инструкции, а вот дальше — для меня это был темный лес, в который я нос не совала. В итоге на курение доков по Angular2, JavaScript, TypeScript, вникание, написание, перепроверки, переделки у меня ушло чистых 12 часов 48 минут. Перекуры, разговоры, загрузки-перезагрузки IDE в этом времени не учтены.

Итого: IMHO Angular2 весьма опасен тем, что туда могут вот так вот, достаточно просто влезть такие блондинки как я, и даже потратив не так много времени сделать что-то большее, чем HelloWorld или же ToDo-список.

P.S. Тема статьи родилась из прочтения одного твита, где задавали вопрос — насколько высок порог вхождения в Angular2. Ну что же, можно сказать, что невысок. Все гуру могут хвататься за голову и предрекать наступление краха из-за того, что скоро полезут недоучки, которые будут писать полную ерунду, а им потом разгребать это.

P.P.S. За орфографию, грамматику, стилистику, некоторую саркастичность заранее прошу прощения, а при указании на что-то из первых трех пунктов — исправлю это 🙂

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

И огромное вам спасибо, если дочитали этот пост!

ссылка на оригинал статьи https://habrahabr.ru/post/278523/