Поріг входження в 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 {Вами} from "rxjs/Вами";
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) : Вами<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);
return Вами.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 {Ctrl} 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. За орфографію, граматику, стилістику, деяку саркастичность заздалегідь прошу вибачення, а при вказівці на що-то з перших трьох пунктів — виправлю :)

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

І величезне вам спасибі, якщо дочитали цей пост!

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.