Добрый день, дорогие хабра: жители, читатели, писатели, негативно-комментаторы :)
В качестве вводной части и чтобы снять некоторые вопросы немного расскажу о себе.
Меня зовут Тамара. Оужас, я девушка! Кого это пугает — закрывайте статью и не читайте.
Для остальных: у меня за плечам незаконченный лет 10 назад МИРЭА, факультет кибернетики. Но все эти 10 лет практики сложились таким образом, что по большей части я занималась рекламой и в перерывах случалось работать в различных стартапах, связанных с интернетом и не только.
В общем, если коротко, то чукча не программист, чукча просто душой и сердцем уважает тех, кто из непонятных строчек кода делает офигенные вещи, которые хорошо работают.
Я покривлю душой, если скажу, что я не могу разобраться в чужом коде. Могу, на java и php могу даже какие-то простые вещи поправить. Но дальше этого мой опыт программирования никогда не уходил.
Но это же все не то, душа просила поэзии с чистого листа. И вот прекратив на некоторое время свою трудовую деятельность и взяв длительный отпуск для души и тела я таки решила попробовать что-то сделать с 0 и самостоятельно. Под "что-то" я понимаю свой маленький проект.
Когда думала и выбирала на чем делать, то для бэкенда остановилась на PHP. А точнее на фреймворке — Laravel.
На нем я остановилась по той причине, что для меня он показался самым низким по порогу вхождения. Мне не нравится в нем документация, так как с моей точки зрения многие моменты не раскрыты и приходится лезть в исходники, чтобы почитать комментарии. Но основные общие моменты разобраны на многих ресурсах. Laracasts как источник обучения весьма грустен. Тейлор там рассматривает все достаточно поверхностно, перескакивая с одного на другое и совершенно не углубляясь. Все по верхам.
Для фронтенда я выбрала Angular 2. Да, я знаю, что он в beta-режиме :), но мне он опять же показался логичным.
Для въезжания в Angular2 я пользуюсь их документацией, исходниками на github, чтения issue там же, stackoverflow — но там как-то все сейчас грустно — задают вопросы в основном ответы на которые есть в документации.
Ну, наверное с вводной частью буду заканчивать.
Перейду теперь к сути. Ниже я поделюсь своим маленьким опытом, что же у меня получилось и за какое время я осилила это сделать.
Тут не будет примеров 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 IlluminateDatabaseSchemaBlueprint;
use IlluminateDatabaseMigrationsMigration;
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 AppModelsCatalog;
use IlluminateDatabaseEloquentModel;
use IlluminateDatabaseEloquentSoftDeletes;
/**
* 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 AppHttpControllersCatalog;
use AppModelsCatalogMainCategory;
use IlluminateHttpRequest;
use AppHttpRequests;
use AppHttpControllersController;
/**
* @api
* @package AppHttpControllersCatalog
* @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.
Как я уже говорила — за исходник я брала базовую конфигурация из туториала. Поэтому тут ничего особенного нет. Ну, кроме, что 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.
<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>
на главной странице.
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
.
import {Component} from "angular2/core";
@Component({
selector: 'home-page',
template: '<h1>Главная страница</h1>'
})
export class HomePageComponent {}
import {Component} from "angular2/core";
@Component({
selector: 'dashboard-main-category',
template: '<h1>Категории</h1>'
})
export class DashboardMainCategoryComponent {}
Так, сделали. Теперь надо пойти в boot.ts и импортировать основной компонент ShopAppComponent
.
boot.ts
Это самый пустой компонент в моем проекте :) У меня он ничего не делает, кроме как загружает все, что нужно из основного компонента с названием app.component.ts
import {bootstrap} from 'angular2/platform/browser'
import {ShopAppComponent} from "./app.component";
bootstrap(ShopAppComponent);
На этом с роутами мы закончили. И, если сейчас сделать npm run start
, то у вас уже будет сайт на котором можно попрыгать между двумя страничками.
Предлагаю перейти к самому вкусному — давайте сделаем так, чтобы у нас загружались данные из базы.
Так, как я не люблю все мешать в одну кучу, то я разные вещи сейчас разношу по разным скриптам. Потом может я приду к тому, что у меня избыток отдельных файликов и буду рефакторить, но пока для своего удобства я делаю так как делаю.
Базовая модель MainCategory
Перво-наперво нам надо сделать простой класс — аналог Модели на php, pojo — на java.
Давайте его обзовем аутентично: 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
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
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
);
}
}
<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 и все проверки уходят из шаблона. Ну это пока то как я для себя поняла эту разницу. Боюсь, что тему я до конца не раскрыла, так как в голове по поводу этих форм еще каша. Честно сказать — второй вариант с формами мне показался проще и чище. Но с ним есть сейчас много своих косяков.
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
);
}
<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
. Теперь он будет выглядеть вот так:
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. За орфографию, грамматику, стилистику, некоторую саркастичность заранее прошу прощения, а при указании на что-то из первых трех пунктов — исправлю это :)
Важное: конструктивная критика, подсказки, указания на ошибки, неточности в понимании сути — крайне приветствуются. Я буду весьма благодарна если вы потратите немного своего драгоценного на меня.
И огромное вам спасибо, если дочитали этот пост!
Автор: 4ertovo4ka