Близятся первомайские праздники, а значит у нас появится лишний выходной, который можно будет посвятить изучению Angular 4 и GitHub API.
На самом деле, между Angular 4 и Angular 2 не велика разница, поэтому все, что я буду обсуждать здесь, будет работать с Angular 2 (вы, вероятно, знаете, что разработчики стремятся назвывать Angular 4 или Angular 2 просто Angular, но я предпочитаю указывать версию, с которой работаю.
Подготовка к работе
На вашем компьютере должны быть установлены следующие компоненты, желательно последней версии: NodeJs, TypeScript и Angular-CLI. Если вы никогда не работали с TypeScript, то сейчас самое подходящее время для начала. Я рекомендую вам использовать текстовый редактор Atom с установленным typescript-плагином.
Вот скриншот готового приложения:
Создание приложения на Angular 4 с помощью Angular CLI
В принципе, Вы можете создать приложение и без Angular-CLI, но зачем заморачиваться? Существует много вещей, которые потребуется настроить для идеального рабочего процесса разработки. Именно поэтому я и рекомендую вам использовать Angular-CLI. Выполните в консоли следующую команду:
ng new angular4-search-devs
Вы заметите, что Angular-CLI сгенерировал много файлов и структуру папок. Он также установит пакеты из npm-реестра. Если вы установили yarn в качестве менеджера пакетов для NodeJs, то для загрузки пакетов Angular-CLI будет использовать его.
Если вы увидите следующую строку в консоли Project 'angular4-search-devs' successfully created., - знайте, что все готово для работы над проектом. Перейдите в каталог angular4-search-devs и и выполните в консоли команду npm start
. Она будет работать внутри ng serve
. Теперь, если вы перейдете в localhost:4200, то должны увидеть надпись “app works!” (приложение работает!). Это означает, что вы успешно настроили приложение на Angular 4.
Если по какой-либо причине требуется изменить порт, вы можете сделать это через команду ng serve --port 3000
.
Оставьте работать сервер в этой консоли и откройте наш проект на другом терминале. Сейчас самое подходящее время для того, чтобы понять структуру Angular-проекта.
Я не буду подробно анализировать, что делает каждый файл. Но я хотел бы рассказать о самых важных из них. Обратите внимание на папку src/app. Эта папка содержит коды Angular-компонентов, служб, модулей, директив и т.д. tsconfig-файлы действуют так же как и файлы конфигурации typescript-компайлера.
Если вы откроете src/app/app.component.ts, то вы увидите следующий код:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'app works!';
}
Как видете, у нас есть свойство, определяющее заголовок, и которое содержит значение “app works”
. Перейдите теперь в src/app/app.component.html
<h1>
{{title}}
</h1>
Как видите, в этой паре фигурных скобок Angular выводит значение заголовка. Любое Angular-приложение должно иметь один Angular-модуль. В нашем случае у нас есть наш модуль в src/app/app.module.ts. Этот файл является одной из самых важных частей нашего приложения. Вы можете считать его сердцем нашего веб-приложения. Он содержит все импорты и декларации. На самом деле, Angular требуется один Angular-модуль, который будет передан в src/main.ts. main.ts же запускает приложение и загружает в наш Html.
Давайте посмотрим код в src/app/app.module.ts:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Как видите, мы нуждаемся в AppComponent
и объявляем его в @NgModule
. @NgModule
называется декоратором в TypeScript. Он в основном содержит метаданные для следующего класса. Мы также инструктируем Angular для загрузки AppComponent
как корневой компонент в поле начальной загузки декоратора @NgModule
.Любой новый компонент, который мы будем писать, должен быть импортирован здесь и должен быть объявлен в массиве деклараций. Если мы напишем какие-либо сервисы, они войдут в массив providers
.
Создание Angular-компонента с помощью Angular CLI
ng generate component search-users
Здесь мы создаем компонент search-users
. Откройте src/app/app.component.html и содержимое на следующее:
<app-search-users></app-search-users>
Проверьте свой браузер. Он должен показывать, что компонент search-users
работает. Давайте добавим таблицу стилей для bootstarp 3 в наше веб-приложение. Мы можем сделать это тремя способами:
1. добавить CSS-файл в папку assets и подключить его в src/index.html
2. непосредственно вытянуть его из CDN и подключить в src/index.html
3. вставить bootstrap-овский CSS в src/styles.css. Этот CSS будет доступен для всех компонентов.
Я выбрал последний вариант, у которого есть одно небольшое преимущество: нам не нужно будет вручную манипулировать файлом index.html. Webpack будет автоматически подключать styles.css. Теперь наш проект на Angular должен выглядеть следующим образом:
src
├── app
│ ├── app.component.css
│ ├── app.component.html
│ ├── app.component.spec.ts
│ ├── app.component.ts
│ ├── app.module.ts
│ ├── search-users
│ ├── search-users.component.css
│ ├── search-users.component.html
│ ├── search-users.component.spec.ts
│ └── search-users.component.ts
├── assets
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── favicon.ico
├── index.html
├── main.ts
├── polyfills.ts
├── styles.css
├── test.ts
├── tsconfig.app.json
├── tsconfig.spec.json
└── typings.d.ts
<div>
<input placeholder="Название места">
<input placeholder="Язык, например Java">
<button>Поиск</button>
</div>
Здесь нет ничего сложного. У нас есть только два поля ввода и одна кнопка для поиска. Опишем логику в нашем компоненте, который находится в файле src/app/search-users/search-users.component.ts. Вы можете задаться вопросом, как мы будем ссылаться на значения двух полей ввода. Чтобы получить значение из полей ввода в компоненте Angular 4, мы будем использовать #property
для обозначения этого поля ввода. Теперь наш HTML должен выглядеть так:
<div>
<input placeholder="Название места" #place >
<input placeholder="Язык, например Java" #language>
<button>Поиск</button>
</div>
Теперь откроем ваш компонент в typescript-файле и напишем наш метод поиска, который будет взаимодействовать с бэкендом через службы Angular 4.
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'app-search-users',
templateUrl: './search-users.component.html',
styleUrls: ['./search-users.component.css']
})
export class SearchUsersComponent implements OnInit {
place: string;
language: string;
constructor() {}
ngOnInit() {}
search(place: string, language: string) {
this.place = place;
this.language = language;
console.log(this.place, this.language);
}
}
Мы можем использовать search-метод поиска в событии click у кнопки поиска. Воспользуемся атрибутом элемента button (click
).
<div>
<input placeholder="Название места" #place >
<input placeholder="Язык, например Java" #language>
<button (click)="search(place.value, language.value)">Поиск</button>
</div>
Да, нам нужно передать свойство значения указанных полей ввода. Теперь, если мы зайдем в браузер и напишем что-то в поле ввода и нажмем на кнопку, мы увидим это в dev-консоли.
Пишем сервис через Angular 4 – HTTP-сервис c GET-запросом
Пришло время поучиться общаться с API GitHub через службы Angular 4. Мы будем использовать HTTP-сервис Angular, чтобы сделать один GET-запрос к API GitHub. Мы также используем RxJS для использования новой темы, которая находится под наблюдением (Observables). Вы можете использовать Angular CLI для генерации базового кода для нашего сервиса.
Перед написанием сервиса учтите, что любая сервис, который вы пишете, нужно включить декоратор @Injectable()
. С помощью этого декоратора мы описываем, что этот класс может быть добавлен к другому конструктору классов или просто другой класс компонента может использовать его без создания нового экземпляра класса сервиса.
Любой сервис, который мы пишем, мы должны объявить как providers
в файле app.module.ts.
Не пугайтесь, писать и использовать сервисы для Angular 4 вовсе не сложно. Мы можем сгенерировать код для сервисов через:
ng generate service search-users
Теперь откройте файл app.module.ts и замените существующий код на следующий:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { AppComponent } from './app.component';
import { SearchUsersComponent } from './search-users/search-users.component';
import { SerachUsersService } from './serach-users.service';
@NgModule({
declarations: [
AppComponent,
SearchUsersComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule
],
providers: [ SerachUsersService ], // It is important
bootstrap: [ AppComponent ]
})
export class AppModule { }
Теперь вы готовы перейти к написанию кода для сервиса поиска пользователей. У нас будет два основных общедоступных метода. Одним из способов является поиск пользователей через место и язык программирования. Советую посмотреть гид по GitHub API, там много чего интересного.
import { Injectable } from '@angular/core';
import { Http, Headers, Response } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
@Injectable()
export class SerachUsersService {
private searchUsersEndPoint = "https://api.github.com/search/users?q=";
private getUserDetailsEndPoint = "https://api.github.com/users/";
constructor(private http: Http) { }
getUsersByPlaceAndLanguage(place: string, language: string) {
let url;
if (place && !language) {
url = `${this.searchUsersEndPoint}location:${place}`;
} else if (!place && language) {
url = `${this.searchUsersEndPoint}language:${language}`;
} else {
url = `${this.searchUsersEndPoint}location:${place}+language:${language}`;
}
return this.http.get(url)
.map(this.extractData)
.catch(this.handleError);
}
getDetailsByUserName(username: string) {
if (username) {
let url = `${this.getUserDetailsEndPoint}${username}`;
return this.http.get(url)
.map((res: Response) => res.json())
.catch(this.handleError);
}
}
private extractData(res: Response) {
let body = res.json();
return body.items || {};
}
private handleError(error: Response | any) {
// В реальности же, Вам нужно будет использовать удаленный вход в систему
let errMsg: string;
if (error instanceof Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Observable.throw(errMsg);
}
}
import { Component, OnInit, Input } from '@angular/core';
import { SerachUsersService } from '../search-users.service';
@Component({
selector: 'app-search-users',
templateUrl: './search-users.component.html',
styleUrls: ['./search-users.component.css']
})
export class SearchUsersComponent implements OnInit {
place: string;
language: string;
constructor(private serachService: SerachUsersService) {}
ngOnInit() {}
search(place: string, language: string) {
this.place = place;
this.language = language;
console.log(this.place, this.language);
}
}
Как видите, мы передали сервис через наш конструктор. С помощью this.serachService
мы можем получить доступ к публичным методам нашего сервиса. Разметка нашего компонента пока не готова к показу результатов поиска. Давайте сначала подготовим ее, а затем вызовем нашу службу из метода search. Мы также очистим поля ввода, когда нажмем клавишу Enter или кнопку поиска.
Мы также проверим результаты, используя *ngFor
и вызовем метод search когда пользователь нажимает Enter в поле ввода. Для этого мы используем (keyup.enter)
. Переменная results пока не объявлена. Мы это сделаем позже и присвоим ей данные, поступающие от службы.
<div>
<input placeholder="Название места" #place (keyup.enter)="search(place.value, language.value); place.value=''; language.value='';" >
<input placeholder="Язык, например Java" #language (keyup.enter)="search(place.value, language.value); place.value=''; language.value='';">
<button (click)="search(place.value, language.value); place.value=''; language.value='';">Поиск</button>
</div>
<p style="color:red;" *ngIf="error_text">{{ error_text }}</p>
<div>
<div *ngFor="let user of results">
<div>
<img src="{{user.avatar_url}}" width="100" height="100">
</div>
<div>
<a href="{{user.html_url}}">{{ user.login }}</a>
</div>
</div>
</div>
Теперь откроем файл search-users.component.ts и объявим переменные:
...
export class SearchUsersComponent implements OnInit {
place: string;
language: string;
results: any[] = []; // Здесь будут содержаться данные, поступающие от службы
selected: boolean = false; // Флаг, чтобы проверить, было нажатие или нет
selectedUser: any; // presently Selected user details
error_text: string = ""; // Текст сообщения об ошибке для конечного пользователя
constructor(private serachService: SerachUsersService) {}
....
}
Пришло время использовать службу в методе search
. Как я упоминал ранее, мы будем подписываться на Observables. Первый параметр - это результат, а второй параметр выводит ошибку. Мы вызываем службу только в том случае, если какой-либо из двух аргументов метода search задается пользователем. В противном случае, нет смысла вызывать сервис поиска. Теперь, если вы откроете браузер, вы увидите, что все работает.
search(place: string, language: string) {
this.selected = false;
this.error_text = "";
if (place || language) {
this.place = place;
this.language = language;
this.serachService.getUsersByPlaceAndLanguage(place, language).subscribe(
users => {
this.results = users;
},
error => {
this.results = [];
this.error_text = "Сожалею, пользователи не найдены. Попробуй еще раз";
console.error(error);
}
)
}
}
Теперь наши основные функции приложения готовы. Мы сделаем каждый наш результат кликабельным и выведем детали по клику, используя второй метод поисковой службы. Я также добавил несколько стилей бутстрапа к Angular 4, чтобы все выглядело стильно. Вам нужно вставить CSS-код в src/app/search-users/search-users.component.css отсюда.
<div class="container">
<div class="row text-center">
<div class="col-md-12">
<h2>Поиск web-разработчиков</h2>
</div>
</div>
<div class="row">
<div class="col-md-12 col-md-offset-3">
<div class="form-inline">
<input placeholder="Название места" class="form-control input-lg input" #place (keyup.enter)="search(place.value, language.value); place.value=''; language.value='';" />
<input placeholder="Язык, например Java" class="form-control input-lg input" #language (keyup.enter)="search(place.value, language.value); place.value=''; language.value='';" />
<button class="btn btn-primary btn-lg" (click)="search(place.value, language.value); place.value=''; language.value='';">Поиск</button>
</div>
</div>
</div>
<div class="row">
<p class="text-center" style="padding: 1em; color:red;" *ngIf="error_text">{{ error_text }}</p>
<div [ngClass]="{'col-md-9': selected, 'col-md-12': !selected}">
<div class="user col-md-2 text-center" *ngFor="let user of results" (click)="getDetails(user.login)">
<div class="img_container">
<img src="{{user.avatar_url}}" class="img-circle" width="100" height="100">
</div>
<div class="details_container">
<a href="{{user.html_url}}" target="_blank" class="login_name">{{ user.login }}</a>
</div>
</div>
</div>
<div *ngIf="results.length && selected" class="col-md-3">
<div class="box text-center">
<img src="{{selectedUser.avatar_url}}" class="img-responsive img-circle">
<div class="details">
<div *ngIf="selectedUser.name"><a href="{{ selectedUser.html_url }}" target="_blank"><h3>{{ selectedUser.name }} </h3></a></div>
<hr>
<div *ngIf="selectedUser.email">Email: {{ selectedUser.email }}</div>
<hr>
<div *ngIf="selectedUser.location">Место нахождения: {{ selectedUser.location }}</div>
<hr>
<div *ngIf="selectedUser.bio">Биография: {{ selectedUser.bio }}</div>
<hr>
<div *ngIf="selectedUser.updated_at">Последнее посещение: {{ selectedUser.updated_at | date }}</div>
<hr>
<div *ngIf="selectedUser.public_repos">Repos: {{ selectedUser.public_repos }}</div>
<hr>
<div *ngIf="selectedUser.public_gists">Gists: {{ selectedUser.public_gists }}</div>
<hr>
<div *ngIf="selectedUser.followers">Followers: {{ selectedUser.followers }}</div>
<hr>
<div *ngIf="selectedUser.created_at">Присоединился к github: {{ selectedUser.created_at | date }}</div>
<hr>
<button class="btn btn-sm btn-warning" (click)="selected = !selected">Назад</button>
</div>
</div>
</div>
</div>
</div>
Прокручивая массив результатов мы сталкиваемся с текущим повторяющимся объектом, который хранится в пользовательской переменной. Если с GitHub API все в порядке, то user.login
выдаст никнейм пользователя. Поэтому давайте создадим этот метод getDetails
в нашем классе компонентов.
...
getDetails(username: string) {
this.serachService.getDetailsByUserName(username).subscribe(
userDatils => {
this.selectedUser = userDatils;
this.selected = true;
},
error => {
this.selected = false;
console.error(error);
}
)
}
...
Теперь, если вы нажмете на какого-нибудь пользователя, вы сможете просмотреть подробную информацию о нем. Как скрыть ее? ? Просто переключите выбранное значение как мы делаем это здесь.
Надеюсь, теперь вы изучили основы Angular 4 и как его использовать. Ссылка на исходный код.