Добрый день, дорогие хабра: жители, читатели, писатели, негативно-комментаторы 🙂
В качестве вводной части и чтобы снять некоторые вопросы немного расскажу о себе.
Меня зовут Тамара. Оужас, я девушка! Кого это пугает — закрывайте статью и не читайте.
Для остальных: у меня за плечам незаконченный лет 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 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.
Как я уже говорила — за исходник я брала базовую конфигурация из туториала. Поэтому тут ничего особенного нет. Ну, кроме, что 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. За орфографию, грамматику, стилистику, некоторую саркастичность заранее прошу прощения, а при указании на что-то из первых трех пунктов — исправлю это 🙂
Важное: конструктивная критика, подсказки, указания на ошибки, неточности в понимании сути — крайне приветствуются. Я буду весьма благодарна если вы потратите немного своего драгоценного на меня.
И огромное вам спасибо, если дочитали этот пост!
ссылка на оригинал статьи https://habrahabr.ru/post/278523/
Добавить комментарий