Karena fakta bahwa aplikasi dimuat dalam iframe, ada masalah dengan tata letak, plugin tidak berfungsi dengan benar, klien masih mengunduh dua bundel dengan Angular, meskipun versi Angular dalam aplikasi dan Frame Manager sama. Dan menggunakan iframe pada tahun 2020 sepertinya tidak sopan. Tetapi bagaimana jika kita melepaskan frame dan memuat semua aplikasi dalam satu jendela?
Ternyata ini mungkin, dan sekarang saya akan memberi tahu Anda cara menerapkannya.
Solusi yang memungkinkan
Spa tunggal : "Router javascript untuk layanan mikro front-end" - seperti yang ditunjukkan di situs web perpustakaan. Memungkinkan Anda menjalankan aplikasi yang ditulis dalam kerangka berbeda di halaman yang sama secara bersamaan. Solusi tersebut tidak berhasil untuk kami: sebagian besar fungsionalitas tidak diperlukan, dan pemuat System.js yang digunakan di dalamnya dalam beberapa kasus menimbulkan masalah saat membuat dengan webpack. Dan menggunakan pemuat modul dengan webpack sepertinya bukan solusi terbaik.
Elemen sudut: Paket ini memungkinkan Anda untuk membungkus komponen Angular dalam komponen web. Anda dapat membungkus seluruh aplikasi. Kemudian Anda harus menambahkan polyfill untuk browser lama, dan membuat komponen web dari seluruh aplikasi dengan peruteannya sendiri terlihat seperti keputusan yang salah secara ideologis.
Implementasi manajer bingkai
Mari kita lihat bagaimana memuat aplikasi tanpa bingkai di Manajer bingkai diimplementasikan menggunakan sebuah contoh.
Pengaturan awal terlihat seperti ini: kami memiliki aplikasi utama - utama. Itu selalu memuat terlebih dahulu dan harus memuat aplikasi lain di dalamnya - app-1 dan app-2. Mari buat tiga aplikasi menggunakan perintah ng <app-name> baru . Selanjutnya, kita akan mengkonfigurasi proxy sehingga file html dan js dari aplikasi yang diperlukan dikirim ke permintaan seperti /<app-name>/*.js , /<app-name>/*.html , dan statika aplikasi utama dikirim ke semua permintaan lainnya.
proxy.conf.js
const cfg = [
{
context: [
'/app1/*.js',
'/app1/*.html'
],
target: 'http://localhost:3001/'
},
{
context: [
'/app2/*.js',
'/app2/*.html'
],
target: 'http://localhost:3002/'
}
];
module.exports = cfg;
Untuk aplikasi app-1 dan app-2, kami akan menentukan baseHref di angular.json app1 dan app2, masing-masing. Kami juga akan mengubah pemilih komponen root menjadi app-1 dan app-2.
Seperti inilah tampilan aplikasi utama
Pertama, mari kita memuat setidaknya satu sub-aplikasi. Untuk melakukan ini, Anda perlu memuat semua file js yang ditentukan di index.html.
Temukan url dari file js: buat permintaan http untuk index.html, parsing string menggunakan DOMParser dan pilih semua tag skrip. Mari kita ubah semuanya menjadi array dan petakan ke array alamat. Alamat yang diperoleh dengan cara ini akan berisi location.origin, jadi kami menggantinya dengan string kosong:
private getAppHTML(): Observable<string> {
return this.http.get(`/${this.currentApp}/index.html`, {responseType: 'text'});
}
private getScriptUrls(html: string): string[] {
const appDocument: Document = new DOMParser().parseFromString(html, 'text/html');
const scriptElements = appDocument.querySelectorAll('script');
return Array.from(scriptElements)
.map(({src}) => src.replace(this.document.location.origin, ''));
}
Ada alamat, sekarang Anda perlu memuat skrip:
private importJs(url: string): Observable<void> {
return new Observable(sub => {
const script = this.document.createElement('script');
script.src = url;
script.onload = () => {
this.document.head.removeChild(script);
sub.next();
sub.complete();
};
script.onerror = e => {
sub.error(e);
};
this.document.head.appendChild(script);
});
}
Kode menambahkan elemen skrip dengan src yang diperlukan ke DOM, dan setelah mengunduh skrip, ia menghapus elemen-elemen ini - solusi yang cukup standar, memuat ke webpack dan system.js diimplementasikan dengan cara yang sama.
Setelah memuat skrip - secara teori - kami memiliki segalanya untuk meluncurkan aplikasi yang disematkan. Namun nyatanya, kita akan mendapatkan inisialisasi ulang aplikasi utama. Sepertinya aplikasi yang sedang dimuat bertentangan dengan yang utama, yang tidak terjadi saat dimuat ke iframe.
Memuat paket webpack
Angular menggunakan webpack untuk memuat modul. Dalam konfigurasi standar, webpack membagi kode ke dalam bundel berikut:
- main.js - semua kode klien;
- polyfills.js - polyfills;
- styles.js - gaya;
- vendor.js - semua pustaka yang digunakan dalam aplikasi, termasuk Angular;
- runtime.js - runtime webpack;
- <module-name> .module.js - modul malas.
Jika Anda membuka salah satu file ini, pada awalnya Anda dapat melihat kode:
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([/.../])
Dan di runtime.js:
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
Ini berfungsi seperti ini: ketika bundel dimuat, itu membuat array webpackJsonp, jika belum ada, dan mendorong isinya ke dalamnya. Runtime webpack mengganti fungsi push dari array ini sehingga Anda nanti dapat memuat bundel baru, dan memproses semua yang sudah ada di dalam array.
Semua ini diperlukan agar urutan pemuatan bundel tidak menjadi masalah.
Jadi, jika Anda memuat aplikasi Angular kedua, ia akan mencoba menambahkan modulnya ke runtime webpack yang sudah ada, yang paling banter akan mengarah pada penginisialisasian ulang aplikasi utama.
Ubah nama webpackJsonp
Untuk menghindari konflik, Anda perlu mengubah nama array webpackJsonp. CLI Angular menggunakan konfigurasi webpacknya sendiri, tetapi dapat diperpanjang jika diinginkan. Untuk melakukan ini, Anda perlu menginstal paket angular-builders / custom-webpack:
npm i -D @ angular-builders / custom-webpack.
Kemudian, di file angular.json dalam konfigurasi proyek, ganti arsitek.build.builder dengan @ angular-builders / custom-webpack: browser , dan tambahkan ke arsitek.build.options :
"customWebpackConfig": {
"path": "./custom-webpack.config.js"
}
Anda juga perlu mengganti arsitek.serve.builder dengan @ angular-builders / custom-webpack: dev-server agar ini bekerja secara lokal dengan server dev.
Sekarang Anda perlu membuat file konfigurasi webpack, yang ditentukan di atas di customWebpackConfig: custom-webpack.config.js
Ini mendefinisikan pengaturan kustom, Anda dapat membaca lebih lanjut di dokumentasi resmi .
Kami tertarik dengan jsonpFunction .
Anda dapat mengatur konfigurasi seperti itu di semua aplikasi yang dimuat untuk menghindari konflik (jika setelah itu konflik masih tetap ada, kemungkinan besar Anda dikutuk):
module.exports = {
output: {
jsonpFunction: Math.random().toString()
},
};
Sekarang, jika kita mencoba memuat semua skrip dengan cara yang dijelaskan di atas, kita akan melihat kesalahan:
Sebelum memuat aplikasi, Anda perlu menambahkan elemen root ke DOM:
private addAppRootElement(appName: string) {
const rootElementSelector = APP_CFG[appName].rootElement;
this.appRootElement = this.document.createElement(rootElementSelector);
this.appContainer.nativeElement.appendChild(this.appRootElement);
}
Ayo coba lagi - hore, aplikasi telah dimuat!
Beralih antar aplikasi
Kami menghapus aplikasi sebelumnya dari DOM dan kami dapat beralih antar aplikasi:
destroyApp () {
if (!this.currentApp) return;
this.appContainer.nativeElement.removeChild(this.appRootElement);
}
Tetapi ada kekurangan di sini: ketika kita membuka app-1 β app-2 β app-1, kita memuat ulang bundel js untuk aplikasi app-1 dan menjalankan kodenya. Selain itu, kami tidak menghancurkan aplikasi yang dimuat sebelumnya, yang menyebabkan kebocoran memori dan konsumsi sumber daya yang tidak perlu.
Jika Anda tidak mengunduh ulang bundel aplikasi, proses bootstrap tidak akan dijalankan dengan sendirinya dan aplikasi tidak akan dimuat. Anda perlu mendelegasikan proses startup bootstrap ke aplikasi utama.
Untuk melakukan ini, mari tulis ulang file main.ts dari aplikasi yang dimuat:
const BOOTSTRAP_FN_NAME = 'ngBootstrap';
const bootstrapFn = (opts?) => platformBrowserDynamic().bootstrapModule(AppModule, opts);
window[BOOTSTRAP_FN_NAME] = bootstrapFn;
Metode bootstrapModule tidak segera dijalankan, tetapi disimpan dalam fungsi pembungkus yang berada dalam variabel global. Di aplikasi utama, Anda dapat mengaksesnya dan menjalankannya bila diperlukan.
Untuk menghancurkan aplikasi dan memperbaiki kebocoran memori, Anda perlu memanggil metode penghancuran modul aplikasi root (AppModule). PlatformBrowserDynamic (). Metode BootstrapModule mengembalikan tautan ke sana, yang berarti fungsi pembungkus kami:
this.getBootstrapFn$().subscribe((bootstrapFn: BootstrapFn) => {
this.zone.runOutsideAngular(() => {
bootstrapFn().then(m => {
this.ngModule = m; //
});
});
});
this.ngModule.destroy(); //
Setelah memanggil destroy () pada modul root, metode ngOnDestroy () dari semua layanan dan komponen aplikasi (jika diterapkan) akan dipanggil.
Semuanya bekerja. Tetapi jika aplikasi yang dimuat berisi modul malas, mereka tidak akan dapat memuat:
Terlihat bahwa jalur aplikasi tidak ada di alamatnya (seharusnya ada /app2/lazy-lazy-module.js ). Untuk mengatasi masalah ini, Anda perlu menyinkronkan href dasar utama dan aplikasi yang dimuat:
private syncBaseHref(appBaseHref: string) {
const base = this.document.querySelector('base');
base.href = appBaseHref;
}
Sekarang semuanya berjalan sebagaimana mestinya.
Hasil
Mari kita lihat berapa lama waktu yang dibutuhkan untuk memuat subaplikasi dengan meletakkan console.time () sebelum memuat skrip di aplikasi utama dan console.timeEnd () di konstruktor komponen root aplikasi utama.
Saat aplikasi app-1 dan app-2 dimuat untuk pertama kali, kami melihat sesuatu seperti ini:
Cukup cepat. Tetapi jika Anda kembali ke aplikasi yang diunduh sebelumnya, Anda dapat melihat angka-angka berikut:
Aplikasi dimuat secara instan, karena semua potongan yang diperlukan sudah ada di memori. Tapi sekarang Anda perlu lebih berhati-hati tentang referensi dan langganan objek yang tidak digunakan, karena meskipun aplikasi dimusnahkan, hal itu dapat menyebabkan kebocoran memori.
Manajer bingkai tanpa bingkai
Solusi yang dijelaskan di atas diimplementasikan di manajer Frame, yang mendukung pemuatan aplikasi dengan atau tanpa iframe. Sekitar seperempat dari semua aplikasi di Tinkoff Business sekarang dimuat tanpa bingkai, dan jumlahnya terus bertambah.
Dan berkat solusi yang dijelaskan, kami belajar cara meraba-raba Angular dan pustaka umum yang digunakan dalam manajer dan aplikasi Frame, yang selanjutnya meningkatkan kecepatan memuat dan bekerja. Kita akan membahasnya di artikel berikutnya.
Repositori dengan kode sampel