Это продолжение. Первая часть. Вторая часть.
Скорость работы
А теперь взглянем на страничку запросов, которые приходят при загрузке главной страницы приложения.
Ух ты, 31 запрос и 20.8КБ передано за 546 мс. Это почти в пять раз медленне загрузки главной страницы Hacker News и вдвое больший объём данных при загрузке топиков. Это очень медленно. Даже приняв во внимание, что мы грузим главную страницу один раз и можем смириться с полусекундной задержкой, то загрузка комментариев к популярной новости займёт очень много времени!
Вы можете увидеть как я загружаю новость с 2000 комментариев тут. Если вам лень смотреть гифку, то вот статистика: там 741 запрос, 1,5 МБ и 90 сек для загрузки примерно 700 комментариев (я не стал ждать пока все комментарии загрузятся).
Вносим коррективы
Теперь мы видим, что выбрали не лучшее решение, используя множественные сетевые запросы для загрузки элементов и их содержимого. После недолгого поиска, я нашёл неофициальный API, возвращающий элемент и его детали одним запросом.
Вот пример ответа по запросу списка популярных историй:
// https://node-hnapi.herokuapp.com/news?page=1
[
{
"id": 12469856,
"title": "Owl Lisp – A purely functional Scheme that compiles to C",
"points": 57,
"user": "rcarmo",
"time": 1473524669,
"time_ago": "2 hours ago",
"comments_count": 9,
"type": "link",
"url": "https://github.com/aoh/owl-lisp",
"domain": "github.com"
},
{
"id": 12469823,
"title": "How to Write Articles and Essays Quickly and Expertly",
"points": 52,
"user": "bemmu",
"time": 1473524142,
"time_ago": "2 hours ago",
"comments_count": 6,
"type": "link",
"url": "https://www.downes.ca/post/38526",
"domain": "downes.ca"
},
...
]
Мы видим, что здесь есть и такие атрибуты, как domain и time_ago, это очень круто. И это значит, что мы можем выкинуть файл domain.pipe.ts, созданный ранее, а также удалить библиотеку angular2-moment. И доработаем нашу службу:
// hackernews-api.service.ts
export class HackerNewsAPIService {
baseUrl: string;
constructor(private http: Http) {
this.baseUrl = 'https://node-hnapi.herokuapp.com';
}
fetchStories(storyType: string, page: number): Observable {
return this.http.get(`${this.baseUrl}/${storyType}?page=${page}`)
.map(response => response.json());
}
}
Так как API не загружает все 500 топиков, нам необходимо будет добавить номер страницы как аргумент. Обратите внимание также на то, как мы передаём storyType — это позволит нам показывать разные типы топиков, в зависимости от запроса пользователя.
Доработаем компонент stories. Можно начать лишь передав ‘news’ и номер страницы 1 в вызов службы для получения топа:
// stories.component.ts
export class StoriesComponent implements OnInit {
items;
constructor(private _hackerNewsAPIService: HackerNewsAPIService) {}
ngOnInit() {
this._hackerNewsAPIService.fetchStories('news', 1)
.subscribe(
items => this.items = items,
error => console.log('Error fetching stories'));
}
}
Соответствующая разметка:
И нам нужно ещё добавить индикатор загрузки.
Доработаем ItemComponent — в файле item.component.ts уже не нужен HackerNewsService:
// item.component.ts
export class ItemComponent implements OnInit {
@Input() item;
constructor() {}
ngOnInit() {
}
}
Разметка:
И посмотрим что у нас получилось:
Всё работает намного быстрее! Исходный код этого этапа можно скачать здесь.
Роутинг
Мы уже много сделали, но сделаем паузу и нарисуем структуру компонентов нашего приложения. Простите за моё неумение работать в Powerpoint.
Начнём с того, что мы уже сделали:
Также обрисуем компоненты, показывающие переход к странице комментариев:
Чтобы пользователь мог переходить между этими страницами, нам нужен небольшой роутинг. Создадим компонент:
ng g component ItemComments
А теперь создадим файл app.routes.ts в папке app.
// app.routes.ts
import { Routes, RouterModule } from '@angular/router';
import { StoriesComponent } from './stories/stories.component';
import { ItemCommentsComponent } from './item-comments/item-comments.component';
const routes: Routes = [
{path: '', redirectTo: 'news/1', pathMatch : 'full'},
{path: 'news/:page', component: StoriesComponent, data: {storiesType: 'news'}},
{path: 'newest/:page', component: StoriesComponent, data: {storiesType: 'newest'}},
{path: 'show/:page', component: StoriesComponent, data: {storiesType: 'show'}},
{path: 'ask/:page', component: StoriesComponent, data: {storiesType: 'ask'}},
{path: 'jobs/:page', component: StoriesComponent, data: {storiesType: 'jobs'}},
{path: 'item/:id', component: ItemCommentsComponent}
];
export const routing = RouterModule.forRoot(routes);
Вот что мы сделали:
- Мы создали массив роутов, с указанием относительного пути и привязкой к конкретному компоненту
- Ссылки в шапке страницы будут указывать на разные пути: news, newest, show, ask и jobs. Все эти пути привязаны к StoriesComponent
- С корневого пути мы сделаем редирект на news, возвращающий топ историй
- При привязке StoriesComponent мы передаём storiesType как параметр свойства data.
- :page используем как токен, поэтому StoriesComponent может получать список топиков определённой страницы
- :id используется также, поэтому ItemCommentsComponent получает все комментарии к нужному топику
С роутингом можно сделать ещё много интересного, но этой основы нам пока достаточно. Откроем app.module.ts и зарегистрируем наш роутинг:
// app.module.ts
// ...
import { routing } from './app.routes';
@NgModule({
declarations: [
//...
],
imports: [
//...
routing
],
providers: [HackerNewsAPIService],
bootstrap: [AppComponent]
})
export class AppModule { }
Чтобы Angular знал, куда загружать нужный компонент, нам нужен RouterOutlet.
Навигация по топикам
Привяжем навигационные ссылки в HeaderComponent к соответствующим роутам:
Директива RouterLink ответственна за привязку определённого элемента к роуту. Теперь обновим StoriesComponent:
// stories.component.ts
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { ActivatedRoute } from '@angular/router';
import { HackerNewsAPIService } from '../hackernews-api.service';
@Component({
selector: 'app-stories',
templateUrl: './stories.component.html',
styleUrls: ['./stories.component.scss']
})
export class StoriesComponent implements OnInit {
typeSub: any;
pageSub: any;
items;
storiesType;
pageNum: number;
listStart: number;
constructor(
private _hackerNewsAPIService: HackerNewsAPIService,
private route: ActivatedRoute
) {}
ngOnInit() {
this.typeSub = this.route
.data
.subscribe(data => this.storiesType = (data as any).storiesType);
this.pageSub = this.route.params.subscribe(params => {
this.pageNum = +params['page'] ? +params['page'] : 1;
this._hackerNewsAPIService.fetchStories(this.storiesType, this.pageNum)
.subscribe(
items => this.items = items,
error => console.log('Error fetching' + this.storiesType + 'stories'),
() => this.listStart = ((this.pageNum - 1) * 30) + 1);
});
}
}
Опишем вкратце, что мы сделали. Вначале мы импортировали ActivatedRoute — это служба, обеспечивающая доступ к информации в роуте.
import { ActivatedRoute } from '@angular/router';
@Component({
//...
})
export class StoriesComponent implements OnInit {
//..
constructor(
private route: ActivatedRoute
) {}
//...
}
Затем мы подписываемся на свойства данных роута и сохраняем storiesType в переменной компонента в хуке ngOnInit.
ngOnInit() {
this.typeSub = this.route
.data
.subscribe(data => this.storiesType = (data as any).storiesType);
// ...
}
И, наконец, мы подписываемся на параметры роута и получаем номер страницы. Затем получаем список топиков:
ngOnInit() {
// ...
this.pageSub = this.route.params.subscribe(params => {
this.pageNum = +params['page'] ? +params['page'] : 1;
this._hackerNewsAPIService.fetchStories(this.storiesType, this.pageNum)
.subscribe(
items => this.items = items,
error => console.log('Error fetching' + this.storiesType + 'stories'),
() => {
this.listStart = ((this.pageNum - 1) * 30) + 1;
window.scrollTo(0, 0);
});
});
}
Для подтверждения завершения, мы используем onCompleted() для обновления переменной listStart, которую используем как начальное значение для нашего упорядоченного списка (его вы увидите в разметке ниже). Также мы прокручиваем страницу вверх, чтобы пользователь не застрял внизу страницы при переходе на другую страницу.
Главная страница готова, у нас есть навигация и пагинация. А лучше сами проверьте как работает приложение.
Комментарии
Мы почти закончили! Перед тем, как начать добавлять компоненты комментариев, обновим ссылки в ItemComponent для работы роутинга:
Запустите приложение и кликните на комментарии топика:
Красота. Роутинг к ItemCommentsComponent работает. Теперь создадим остальные компоненты.
ng g component CommentTree
ng g component Comment
Добавим новый GET-запрос в нашу службу для получения комментариев.
// hackernews.api.service.ts
//...
fetchComments(id: number): Observable {
return this.http.get(`${this.baseUrl}/item/${id}`)
.map(response => response.json());
}
И заполним наши компоненты
// item-comments.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { HackerNewsAPIService } from '../hackernews-api.service';
@Component({
selector: 'app-item-comments',
templateUrl: './item-comments.component.html',
styleUrls: ['./item-comments.component.scss']
})
export class ItemCommentsComponent implements OnInit {
sub: any;
item;
constructor(
private _hackerNewsAPIService: HackerNewsAPIService,
private route: ActivatedRoute
) {}
ngOnInit() {
this.sub = this.route.params.subscribe(params => {
let itemID = +params['id'];
this._hackerNewsAPIService.fetchComments(itemID).subscribe(data => {
this.item = data;
}, error => console.log('Could not load item' + itemID));
});
}
}
Также как в StoriesComponent, мы сделаем подписку к параметрам роута, получим id элемента и по нему получим нужные комментарии.
0 || item.type === 'job'" [class.head-margin]="item.text">
{{item.title}}
({{item.domain}})
В начале компонента мы выводим детали элемента, идущие за его описанием (item.content). Затем вводим объект комментариев (item.comments) в app-comment-tree, селектор для CommentTreeComponent. Стили для этого компонента можно скачать здесь.
Теперь доработаем CommentTreeComponent.
// comment-tree.component.ts
import { Component, Input, OnInit } from '@angular/core';
@Component({
selector: 'app-comment-tree',
templateUrl: './comment-tree.component.html',
styleUrls: ['./comment-tree.component.scss']
})
export class CommentTreeComponent implements OnInit {
@Input() commentTree;
constructor() {}
ngOnInit() {
}
}
Мы выводим список комментариев директивой ngFor. Здесь можно скачать стили.
Доработаем CommentComponent, отвечающий за конкретный комментарий:
// comment.component.ts
import { Component, Input, OnInit } from '@angular/core';
@Component({
selector: 'app-comment',
templateUrl: './comment.component.html',
styleUrls: ['./comment.component.scss']
})
export class CommentComponent implements OnInit {
@Input() comment;
collapse: boolean;
constructor() {}
ngOnInit() {
this.collapse = false;
}
}
[deleted] | Comment Deleted
Запустим приложение и сможем увидеть комментарии где и положено:
Исходный код этого этапа можно взять здесь.
Профили пользователей
Остались только профили пользователей. Их мы сделаем абсолютно так же, как и предыдущие элементы, поэтому описание этого этапа мы пропустим. Вот что нужно сделать:
- Добавить ещё один запрос в службу данных, работающий конечную точку по пользователям.
- Создать компонент для этого.
- Добавить поле в файл с роутами.
- Обновить во всех компонентах ссылки, указывающие на пользователя.
Вот готовый код этой части.
Заключение
Мы закончили. Для сборки приложения можно запустить
ng build --prod
или
ng serve --prod
Источник: «building hacker news with angular 2 cli, rxjs and webpack»