Shane Williamson
Shane WilliamsonSoftware Developer @ Vendasta

Angular Elements: dostarczanie bibliotek niezależnych od frameworka

Wykorzystanie Angular Elements do budowania elementów niestandardowych, których można używać poza Angularem.
3.10.20196 min
Angular Elements: dostarczanie bibliotek niezależnych od frameworka

Angular Elements to ciekawa technologia od zespołu Angular. Opisują ją tak:

Angular Elements, czyli komponenty Angulara spakowane jako elementy niestandardowe (zwane również Web Components), są standardem internetowym do definiowania nowych elementów HTML w sposób niezależny od frameworka.

Oznacza to, że jesteśmy w stanie tworzyć komponenty Angular i używać ich w dowolnym miejscu, niezależnie czy używamy zwykłego JS i HTML, czy jakiegokolwiek frameworka, takiego jak Angular, React, Vue itp.

Zbudowaliśmy niedawno z moim zespołem bibliotekę, która działa w naszych aplikacjach angularowych, a także poza nimi. Aby można było użyć biblioteki w różnych projektach, musieliśmy ją zbudować w taki sposób, aby była niezależna od różnic między projektami, w których byłaby wykorzystywana. Angular Elements były idealnym narzędziem do tego scenariusza.

Struktura plików projektu

Droga do łatwej dystrybucji bibliotek rozpoczyna się od przygotowania odpowiedniej struktury plików projektu. Po kilku iteracjach mój zespół doszedł wreszcie do struktury, która dobrze się sprawdza. Okazało się, że jest szczególnie skuteczna w przypadku dostarczania wielu bibliotek z tego samego repozytorium. Struktura jest następująca:

.
├── angular.json
├── package.json
├── projects/
│ └── library1/
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── src/
│ │ ├── elements/
│ │ │ ├── environments/
│ │ │ │ ├── environment.prod.ts
│ │ │ │ └── environment.ts
│ │ │ ├── index.html
│ │ │ ├── index.ts
│ │ │ ├── main.ts
│ │ │ ├── polyfills.ts
│ │ │ ├── library1.module.ts
│ │ │ └── public_api.ts
│ │ ├── index.ts
│ │ ├── lib/
│ │ │ ├── index.ts
│ │ │ ├── my-component/
│ │ │ └── library1.module.ts
│ │ └── public_api.ts
│ ├── tsconfig.elements.json
│ └── tsconfig.lib.json
├── src/
│ ├── app/
│ ├── environments/
│ ├── index.html
│ ├── main.ts
│ └── tsconfig.app.json
└── tsconfig.json


Przykładowa hierarchia plików

Objaśnienie

Pierwszym kluczowym elementem tej struktury jest plik angular.json na najwyższym poziomie projektu. On określa naszą konfigurację obszaru roboczego, w tym konfiguracje kompilacji, które stworzymy dla każdej biblioteki zdefiniowanej w katalogu projects. Zwróć również uwagę na obecność typowych plików package.json i tsconfig.json.

Na najwyższym poziomie są dwa katalogi, projects i src. Katalog projects zawiera wszystkie biblioteki, które będziemy dystrybuować z tego repozytorium. Katalog src zawiera aplikację testową do sprawdzenia naszych bibliotek.

Każda biblioteka w katalogu projects będzie miała tę samą ogólną strukturę. W bibliotece mamy ogólne pliki wysokiego poziomu, takie jak nasz CHANGELOG i plik package.json. W katalogu src biblioteki mamy katalogi lib i elements. Katalog lib to miejsce, w którym będzie się znajdował prawie cały kod bibliotek. Katalog elements będzie zawierał kod odpowiedzialny za używanie i budowanie kodu biblioteki przy pomocy Angular Elements.

Konfiguracja kompilacji

Istnieją dwa rodzaje konfiguracji buildów, które należy przygotować. Jeden build będzie tworzył pakiet NPM, zgodny z Angular Package Format (APF). Drugi to Angular Elements, którego będziemy mogli używać wszędzie.

Oto przykład uproszczonej konfiguracji obszaru roboczego z dwiema konfiguracjami kompilacji, których użyjemy do zbudowania naszej biblioteki zarówno do dystrybucji jako paczki NPM, jak i jako element Angulara:

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "library1": {
      "root": "projects/library1",
      "sourceRoot": "projects/library1/src",
      "projectType": "library",
      "prefix": "library1",
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-ng-packagr:build",
          "options": {
            "tsConfig": "projects/library1/tsconfig.lib.json",
            "project": "projects/library1/ng-package.json"
          }
        },
        "lint": {
          "builder": "@angular-devkit/build-angular:tslint",
          "options": {
            "tsConfig": [
              "projects/library1/tsconfig.lib.json",
              "projects/library1/tsconfig.spec.json"
            ],
            "exclude": [
              "**/node_modules/**",
              "**/polyfills.ts"
            ]
          }
        }
      }
    },
    "library1-elements": {
      "root": "projects/library1",
      "sourceRoot": "projects/library1/src",
      "projectType": "application",
      "prefix": "library1",
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist/library1-elements-tmp",
            "tsConfig": "projects/library1/tsconfig.elements.json",
            "index": "projects/library1/src/elements/index.html",
            "main": "projects/library1/src/elements/main.ts",
            "polyfills": "projects/library1/src/elements/polyfills.ts"
          },
          "configurations": {
            "production": {
              "fileReplacements": [
                {
                  "replace": "projects/library1/src/elements/environments/environment.ts",
                  "with": "projects/library1/src/elements/environments/environment.prod.ts"
                }
              ],
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "extractCss": true,
              "namedChunks": false,
              "extractLicenses": false,
              "vendorChunk": false
            }
          }
        }
      }
    }
  }
}


Rzućmy okiem na konfigurację do zbudowania pakietu NPM. Jest ona zdefiniowana w sekcji projrcts  jako library1. Aby osiągnąć pożądany Angular Package Format, musimy zadeklarować to jako bibliotekę:

"projectType": "library"


i z użyciem ng-packagr:

"builder": "@angular-devkit/build-ng-packagr:build"


To wszystko, co musimy zrobić, aby skonfigurować build NPM!

Konfiguracja drugiego builda nazwana jako library1-elements będzie dotyczyła budowania jako element Angulara. Zwróć uwagę na różnicę w konfiguracji parametrów projectType i builder.

Musimy zbudować komponenty Angular Elements jako aplikację i użyć domyślnego buildera, używanego w normalnych angularowych aplikacjach.

Należy również zauważyć, że każda konfiguracja builda ma własny plik tsconfig, z opcjami specyficznymi dla każdej z nich.

{
  "compilerOptions": {
    "outDir": "../../out-tsc/lib",
    "target": "es5",
    "module": "es2015",
    "moduleResolution": "node",
    "declaration": true,
    "sourceMap": true,
    "inlineSources": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "importHelpers": true,
    "types": [],
    "lib": [
      "dom",
      "es2018"
    ]
  },
  "angularCompilerOptions": {
    "annotateForClosureCompiler": true,
    "skipTemplateCodegen": true,
    "strictMetadataEmit": true,
    "fullTemplateTypeCheck": true,
    "strictInjectionParameters": true,
    "enableResourceInlining": true,
    "flatModuleId": "AUTOGENERATED",
    "flatModuleOutFile": "AUTOGENERATED"
  },
  "exclude": [
    "src/test.ts",
    "**/*.spec.ts"
  ]
}

tsconfig.lib.json dla NPM

{
  "compilerOptions": {
    "outDir": "../../out-tsc/lib",
    "target": "es2015",
    "module": "es2015",
    "moduleResolution": "node",
    "declaration": true,
    "sourceMap": true,
    "inlineSources": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "importHelpers": true,
    "types": [],
    "lib": [
      "dom",
      "es2018"
    ]
  },
  "angularCompilerOptions": {
    "annotateForClosureCompiler": true,
    "skipTemplateCodegen": true,
    "strictMetadataEmit": true,
    "fullTemplateTypeCheck": true,
    "strictInjectionParameters": true,
    "enableResourceInlining": true
  },
  "exclude": [
    "src/test.ts",
    "**/*.spec.ts"
  ]
}

tsconfig.elements.json dla Angular Elements

Różnice w kompilacji

Proponuję utworzenie dwóch różnych App Module - jednego dla biblioteki NPM, a drugiego dla wersji Angular Elements. W ten sposób dołączasz biblioteki specyficzne dla Angular Elements i elementów webowych, tylko kiedy ich potrzebujesz.

Kompilacja bibliotek

Teraz, gdy opisaliśmy, w jaki sposób mamy zbudować nasze biblioteki, musimy to faktycznie zrobić. Używając ng-packagr, nasz build NPM jest praktycznie gotowy, poza faktycznym opublikowaniem go w NPM. Aby nasze elementy Angulara zbudowały się do tego, co trzeba, potrzebujemy jeszcze trochę czasu.

Po zbudowaniu naszej wersji Angular Elements:

ng build --prod library1-elements


Mamy teraz mieli kilka artefaktów, z którymi możemy pracować.

runtime.js, polyfills.js, main.js


Trzy wymienione powyżej pliki tworzą naszą bibliotekę. Kolejnym opcjonalnym krokiem jest połączenie ich przy pomocy polecenia script w celu łatwiejszej dystrybucji:

const fs = require('fs-extra');
const concat = require('concat');

function handleErr(err) {
    if (err) {
        return console.error(err);
    }
    console.log('success!');
}

(async function build() {
  const library1Files = [
      './dist/library1-elements-tmp/runtime.js',
      './dist/library1-elements-tmp/polyfills.js',
      './dist/library1-elements-tmp/main.js'
    ];

  await fs.ensureDir('./dist/library1-client/elements');
  await concat(library1Files, './dist/library1-client/elements/library1.js');
  await fs.remove('./dist/library1-elements-tmp', handleErr);
})();

Skrypt do łączenia artefaktów kompilacji

Teraz, gdy udało nam się zbudować nasz pojedynczy plik JS, możemy śmiało rozpowszechniać naszą bilbiotekę niezależną od frameworków za pośrednictwem CDN. Gotowe!

Typowe problemy

Chociaż większość przygód mojego zespołu z Angular Elements była bezbolesna, to od czasu do czasu napotykaliśmy pewne problemy.

Build Target

Podczas kompilacji Angular Elements możesz zauważyć, że w bibliotekach niestandardowych elementów webowych występują problemy, gdy ustawimy target kompilacji na >es2015. Można to rozwiązać, ustawiając go na es2015 lub używając adaptera, który obsługuje kolejną wersję. Można również poszukać innych rozwiązań online.

Import skryptu

Podczas importowania i używania Angular Elements może się okazać, że nie działają, dopóki tag script nie znajdzie się za elementem Angulara w DOM. Prostym rozwiązaniem jest po prostu zmiana kolejności tagów.

Sposób zapisu wejścia w elementach

Przy przekazywaniu wartości do elementu Angulara musisz użyć kebab-case dla wszystkich nazw wejść w elemencie DOM.

Np. komponent z wejściem o nazwie myCoolInput:

<my-component my-cool-input="foo" …></my-component>



Oryginalny tekst w języku angielskim przeczytasz tutaj.

<p>Loading...</p>