Отображение средних блогов с использованием угловых элементов

Обзор

Элементы Angular - это компоненты Angular, упакованные как настраиваемые элементы. Angular Elements размещает компонент Angular, обеспечивая мост между данными и логикой, определенными в компоненте и стандартных API-интерфейсах DOM, тем самым обеспечивая способ использования компонентов Angular в средах, отличных от Angular.

Пользовательские элементы позволяют веб-разработчикам определять новые типы элементов HTML, которые являются частью и одной из ключевых функций стандарта веб-компонентов. Пользовательские элементы - это многократно используемые инкапсулированные HTML-теги для использования на веб-страницах и / или веб-приложениях. Их можно использовать на статических веб-сайтах, в любой платформе или библиотеке, использующей HTML. Подводя итог, можно сказать, что настраиваемые элементы - это настраиваемые элементы DOM, обладающие собственными функциями, о которых остальной части страницы не нужно ничего знать.

Спецификация настраиваемых элементов требует, чтобы все настраиваемые элементы содержали в названии дефис (-). Пример: <my-widget>, <abc-element>.

Вы можете использовать любое имя, разделенное дефисом, за исключением:

annotation-xml
color-profile
font-face
font-face-src
font-face-uri
font-face-format
font-face-name
missing-glyph

Веб-компоненты - это набор различных технологий, позволяющих создавать многократно используемые настраиваемые элементы - с их функциональностью, инкапсулированной отдельно от остальной части вашего кода - и использовать их в ваших веб-приложениях.

Начиная

Теперь, когда мы знаем об элементах Angular, давайте создадим настраиваемый веб-элемент многократного использования. Мы собираемся сделать простой компонент для списка всех Средних блогов указанного пользователя.

Создать и настроить проект

Мы собираемся создать новый проект Angular CLI и добавить в него элементы.

Во-первых, убедитесь, что у вас глобально установлен Angular CLI (и убедитесь, что это последняя версия, по крайней мере, 7.1.1 на момент написания):

npm install -g @angular/cli

Или обновите Angular CLI, используя:

npm uninstall -g @angular/cli
npm cache verfiy
npm install -g @angular/cli

Создайте приложение, используя интерфейс командной строки (маршрутизация no и формат таблицы стилей SCSS). После завершения установки перейдите в папку с cd и откройте ее в своем любимом редакторе (я использую Visual Studio Code).

ng new my-medium
cd my-medium
code .

Элементы Angular можно добавить с помощью команды ng add и передачи имени нашего приложения.

ng add @angular/elements --project=my-medium

Эта команда добавит пакеты @angular/elements и все необходимое для создания веб-компонентов, включая polyfill document-register-element (автономная рабочая облегченная версия спецификации W3C Custom Elements) в раздел сценария файла конфигурации Angular CLI angular.json.

ПРИМЕЧАНИЕ. Проблема с этим пакетом. На момент написания этой статьи была установлена ​​версия 1.13.1, которую мне не удалось заставить работать в Chrome 70. После некоторого просмотра репозиториев GitHub для элементов Angular предложение, которое сработало для меня, заключалось в том, чтобы понизить версию версия пакета Document Register Element до версии 1.8.1, поскольку кажется, что она лучше поддерживается в браузерах, поддерживающих пользовательские элементы. Самый простой способ - отредактировать package.json файл и изменить зависимость document-register-element с ^1.7.2 на 1.8.1 , а затем повторно запустить команду npm install. Или вы можете установить его, выполнив команду npm i [email protected].

Создать компонент

Приложение настроено, давайте создадим наш компонент. Мы можем повторно использовать app.component.ts. Селектор не имеет значения, потому что, когда мы настраиваем компонент для экспорта, тогда мы назначаем настраиваемый тег.

app.component.ts: https://github.com/Aks1357/my-medium/blob/master/src/app/app.component.ts

import { Component, ViewEncapsulation, Input, OnInit } from '@angular/core';
import { AppService } from './app.service';
import { UserBlog } from './app.interface';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
  encapsulation: ViewEncapsulation.ShadowDom
})
export class AppComponent implements OnInit {
  @Input()
  username: String = '@aks1357';
  @Input()
  blogs: Boolean = false;
  @Input()
  blogTitle: String = 'Blogs';
  @Input()
  blogClass: String = 'blogs-wrapper';
  @Input()
  author: Boolean = false;
  @Input()
  authorTitle: String = 'Author Info';
  @Input()
  authorClass: String = 'author-wrapper';
  public userBlogs: UserBlog;
  public loading: Boolean = false;
  constructor(private appService: AppService) {
  }
  ngOnInit(): void {
    if (this.blogs || this.author) {
      this.loading = true;
      this.appService.getUserBlogs(this.username).subscribe(response => {
        this.loading = false;
        this.userBlogs = response;
      });
    }
  }
}

Некоторые заметки из AppComponent. Во-первых, некоторые пользовательские импорты: сервис и интерфейс (мы рассмотрим их ниже). Во-вторых, мы установили для компонента инкапсуляцию ViewEncapsulation.ShadowDom. Это гарантирует, что компонент использует собственный Shadow DOM. Определение стратегии инкапсуляции влияет на то, как наши стили применяются к компонентам. По умолчанию это Emulated.

Далее идут 7 входов с установленными значениями по умолчанию. Мы также инициируем AppService и используем его метод getUserBlogs в ngOnInit функции.

Далее просто быстро добавляем интерфейсные и служебные файлы.

app.interface.ts: https://github.com/Aks1357/my-medium/blob/master/src/app/app.interface.ts

export interface BlogFeed {
  url: String;
  title: String;
  link: String;
  author: String;
  description: String;
  image: String;
}
export interface BlogItem {
  title: String;
  pubDate: Date;
  link: String;
  guid: String;
  author: String;
  thumbnail: String;
  description: String;
  enclosure: Object;
  categories: Array<String>;
}
export interface UserBlog {
  status: String;
  feed: BlogFeed;
  items: BlogItem[];
}

Один из основных принципов TypeScript заключается в том, что проверка типов фокусируется на форме, которую имеют значения. Я всегда предпочитаю проверять типы, а не использовать any. Мы будем использовать UserBlog, который состоит из другого набора интерфейсов BlogItem и BlogFeed.

app.service.ts: https://github.com/Aks1357/my-medium/blob/master/src/app/app.service.ts

import { Injectable } from '@angular/core';
import { throwError, Observable } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { UserBlog } from './app.interface';
@Injectable({
  providedIn: 'root'
})
export class AppService {
  constructor(private http: HttpClient) {
  }
  getUserBlogs(username: String): Observable<UserBlog> {
    username = username.replace(/^\@?/, '@');
    const rss2jsonUrl = 'https://api.rss2json.com/v1/api.json?rss_url=';
    const userUrl = 'https://medium.com/feed/' + username;
    return this.http.get<UserBlog>(rss2jsonUrl + userUrl)
      .pipe(
        map(response => {
          return {
            feed: response.feed,
            items: response.items.map(item => {
              item.link = item.link.split('?')[0];
              return { ...item };
            }),
            status: response.status,
          };
        }),
        catchError((error) => this.handleError(error, userUrl))
      );
  }
  private handleError(error: HttpErrorResponse, userUrl: String) {
    if (error.error instanceof ErrorEvent) {
      // A client-side or network error occurred. Handle it accordingly.
      console.error('An error occurred:', error.error.message);
    } else {
      // The backend returned an unsuccessful response code.
      // The response body may contain clues as to what went wrong,
      console.error(
        `Backend returned code ${error.status}, ` +
        `body was: ${error.error.message}\n\n` +
        `Check the username provided is valid.\nRSS URL: ${userUrl}`);
    }
    // return an observable with a user-facing error message
    return throwError(`Something bad happened; please try again later.`);
  }
}

Сервисный файл имеет 2 метода:

1] getUserBlogs, который принимает параметр string username, проверяет, начинается ли он с @, если нет, то префикс username с @, который имеет суффикс кuserUrl. Затем выполняется простой HTTP GET для получения вывода в формате JSON из среднего RSS-канала с использованием rss2json API.

2] handleError, который возвращает ошибку, если username недействителен или URL неверен.

Теперь давайте добавим шаблон и стиль.

app.component.html: https://github.com/Aks1357/my-medium/blob/master/src/app/app.component.html

<div *ngIf="loading">
  Loading...
</div>
<div *ngIf="!loading">
  <div *ngIf="!blogs && !author">
    <p>No blog found!</p>
  </div>
  <div [ngClass]="blogClass" *ngIf="blogs">
    <h1 class="title">{{blogTitle}}</h1>
    <div *ngFor="let x of userBlogs.items; last as last;">
      <h3 class="link">
        <a href="{{x.link}}" target="_blank">{{x.title}}</a>
      </h3>
      <span class="author">Author: {{x.author}} </span> |
      <span class="pubDate">Published On: {{x.pubDate | date}} </span>
      <ul class="categories">
        <li *ngFor="let y of x.categories">
          <span>{{y}}</span>
        </li>
      </ul>
      <hr class="separator">
    </div>
  </div>
<div [ngClass]="authorClass" *ngIf="author">
    <h1 class="title">{{authorTitle}}</h1>
    <div>
      <h3 class="link">
        <a href="{{userBlogs.feed.url}}" target="_blank">{{userBlogs.feed.author || userBlogs.feed.title}}</a>
      </h3>
      <span>
        <img src="{{userBlogs.feed.image}}" alt="{{userBlogs.feed.title}}" />
        <p class="description">{{userBlogs.feed.description}}</p>
      </span>
    </div>
  </div>
</div>

app.component.scss: https://github.com/Aks1357/my-medium/blob/master/src/app/app.component.scss

.blogs-wrapper {
  margin: 10px;
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 10px;
  -moz-border-radius: 10px;
  -webkit-border-radius: 10px;
  .title {
    margin: 0;
    padding: 0 0 10px 0;
    border-bottom: 1px solid #ccc;
  }
  .link {
    a {
      text-decoration: none;
      color: #000000;
    }
    a:hover {
      text-decoration: underline;
    }
  }
  .categories {
    list-style-type: none;
    padding: 0;
    margin: 0;
    li {
      display: inline-block;
      border: 1px solid #ccc;
      border-radius: 25%;
      padding: 5px;
      font-size: 12px;
      margin: 5px;
    }
  }
  .categories> :first-child {
    margin-left: 0 !important;
  }
  .separator {
    border-style: ridge;
    border-width: thin;
    border-color: #efefef;
  }
  .separator> :last-child {
    display: none;
  }
}
.author-wrapper {
  margin: 10px;
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 10px;
  -moz-border-radius: 10px;
  -webkit-border-radius: 10px;
  .title {
    margin: 0;
    padding: 0 0 10px 0;
    border-bottom: 1px solid #ccc;
  }
  .link {
    a {
      text-decoration: none;
      color: #000000;
    }
    a:hover {
      text-decoration: underline;
    }
  }
}

Создание настраиваемого элемента

Теперь у нас есть компонент. Все, что нам нужно сделать, это сказать Angular, что это элемент Angular, и упаковать его, чтобы его можно было использовать повторно!

Чтобы убедиться, что Angular знает, что это повторно используемый компонент Angular Element, нам нужно внести некоторые изменения.

Итак, давайте обновим наш файл AppModule.

app.module.ts: https://github.com/Aks1357/my-medium/blob/master/src/app/app.module.ts

import { NgModule, Injector } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { createCustomElement } from '@angular/elements';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
  ],
  providers: [],
  entryComponents: [AppComponent]
})
export class AppModule {
  constructor(private injector: Injector) {
    const el = createCustomElement(AppComponent, { injector });
    customElements.define('aks-my-medium', el);
  }
  ngDoBootstrap() { }
}

Первое, что вы можете заметить в новом коде модуля, это то, что он добавляет массив entryComponents к объявлению @NgModule . Это сообщает Angular, что вместо того, чтобы загружать приложение Angular из AppComponent, мы собираемся его скомпилировать и упаковать для использования в качестве веб-компонента. Кроме того, AppModule теперь имеет конструктор для настройки функции createCustomElement(), которая принимает AppComponent и инжектор. Инжектор будет использоваться для создания новых экземпляров компонента, которые живут независимо друг от друга. Затем мы определяем настраиваемый элемент и селектор aks-my-medium , который будет определен для его использования в других приложениях. Последний ngDoBootstrap() метод отменяет естественную загрузку элемента, поскольку это не будет обычное приложение Angular.

Упаковка углового элемента

Мы создали компонент и превратили его в настраиваемый веб-элемент. Итак, теперь нам нужно упаковать эти вещи. Есть несколько способов сделать это, мы будем использовать подход Node.

Если мы используем команду сборки Angular CLI: ng build --prod. Он создаст кучу файлов под dist с хешами в именах файлов, которые трудно распространить. Итак, чтобы создать плоскую файловую структуру, которая объединяется в один файл, мы сделаем следующее.

Начнем с добавления npm зависимостей:

npm install --save-dev concat fs-extra

Затем в корне нашего проекта создайте файл с именем elements-build.js и вставьте следующее:

elements-build.js: https://github.com/Aks1357/my-medium/blob/master/build-elements.js

const fs = require('fs-extra');
const concat = require('concat');
(async function build() {
  // NOTE: Have changed angular.json file, 'outputPath' to 'dist' rather than 'dist/<application-name>'. If you are using default angular.json then for file paths below, add <application-name> in file path. Example - './dist/my-medium/runtime.js', do the same for all.
  const files = [
    './dist/runtime.js',
    './dist/polyfills.js',
    './dist/scripts.js',
    './dist/main.js'
  ];
  await fs.ensureDir('elements');
  await concat(files, 'elements/aks-my-medium.js');
  // NOTE: Below lines are for testing, update the path according to your test application or comment/remove the below code before running the 'npm run build:elements' command.
  if (fs.existsSync('../aks-my-medium-test/aks-my-medium.js')) {
    fs.unlinkSync('../aks-my-medium-test/aks-my-medium.js');
  }
  fs.copyFileSync('elements/aks-my-medium.js', '../aks-my-medium-test/aks-my-medium.js');
})();

Теперь откройте package.json и добавьте новый скрипт:

"build:elements": "ng build --prod --output-hashing none && node elements-build.js"

Это запустит этот ng build --prod с отключенным выходным хешированием, так что файлы будут называться не main.936cea5fb159f3b5f87d.js, а просто main.js, что упрощает build-elements.js скрипту их поиск и объединение.

Соберите приложение, запустив:

npm run build:elements

Вы увидите новую папку с именем elements в корневой папке с одним файлом: aks-my-medium.js, который вы можете легко распространять или использовать где угодно.

Тестирование пользовательского элемента Angular

Вы можете протестировать элемент, создав HTML-файл во вновь созданной папке elements. Я предпочел создать новый проект (aks-my-medium-test), как показано в build-elements.js. Итак, в папке elements создайте файл index.html.

index.html :

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Angular Element: My Medium Test</title>
  <base href="/">
</head>
<body>
  <aks-my-medium username="aks1357" blog-title="List of Blogs" blogs="true" author="true" author-title="Blog By"></aks-my-medium>
  <script type="text/javascript" src="aks-my-medium.js"></script>
</body>
</html>

Вы можете быстро проверить это, используя простой локальный HTTP-сервер. http-server - это простой пакет, который обслуживает любую папку, в которой он запущен, как веб-папку. Команда для установки http-server:

npm install -g http-server

Затем запустите http-server elements для обслуживания файлов в папке elements.

Теперь перейдите по адресу localhost: 8080 (в браузере, который поддерживает настраиваемые элементы, например Chrome), вы должны увидеть настраиваемый элемент - список блогов!

Заключение

Обратите внимание, что размер объединенного файла .js составляет около 275 КБ. Это довольно много для компонента, который просто отображает список блогов. Тем не менее, мы также должны помнить, что Angular Elements все еще имеют уровень зависимости от Angular под капотом - вместе с некоторыми полифилами, которые либо увеличивают размер пакета.

Итак, теперь у нас есть многоразовый настраиваемый элемент. Кроме того, это простой способ добавить список блогов Medium на ваш сайт, создав свой собственный элемент (код github) или просто используя пользовательский элемент, опубликованный мной.

Прямое использование

Тег сценария

<script type="text/javascript" src="//unpkg.com/[email protected]/index.js"></script>

OR

НПМ

npm i --save aks-my-medium

JS

import 'aks-my-medium';

HTML

<aks-my-medium username="aks1357" blogs="true" blog-title="List of Blogs" author="true" author-title="Blog By"></aks-my-medium>

Полный код и подробности прямого использования на Github