Migrasi Frankenstein: Pendekatan Kerangka-Agnostik (Bagian 2)

Diterbitkan: 2022-03-10
Ringkasan singkat Kami baru-baru ini membahas apa itu “Frankenstein Migration”, membandingkannya dengan jenis migrasi konvensional, dan menyebutkan dua blok pembangun utama: layanan mikro dan Komponen Web . Kami juga mendapatkan dasar teoretis tentang cara kerja jenis migrasi ini. Jika Anda tidak membaca atau lupa diskusi itu, Anda mungkin ingin kembali ke Bagian 1 terlebih dahulu karena akan membantu untuk memahami semua yang akan kami bahas di bagian kedua artikel ini.

Dalam artikel ini, kami akan menguji semua teori dengan melakukan migrasi langkah demi langkah aplikasi, mengikuti rekomendasi dari bagian sebelumnya. Untuk mempermudah, mengurangi ketidakpastian, ketidaktahuan, dan tebak-tebakan yang tidak perlu, untuk contoh praktis migrasi, saya memutuskan untuk mendemonstrasikan praktik pada aplikasi yang harus dilakukan sederhana.

Saatnya untuk menguji teori
Saatnya untuk menguji teori. (Pratinjau besar)

Secara umum, saya berasumsi bahwa Anda memiliki pemahaman yang baik tentang cara kerja aplikasi umum yang harus dilakukan. Jenis aplikasi ini sangat sesuai dengan kebutuhan kita: dapat diprediksi, namun memiliki jumlah minimum komponen yang diperlukan untuk menunjukkan berbagai aspek Migrasi Frankenstein. Namun, terlepas dari ukuran dan kompleksitas aplikasi Anda yang sebenarnya, pendekatan ini dapat diskalakan dengan baik dan seharusnya cocok untuk proyek dengan ukuran berapa pun.

Tampilan default aplikasi TodoMVC
Tampilan default aplikasi TodoMVC (Pratinjau besar)

Untuk artikel ini, sebagai titik awal, saya memilih aplikasi jQuery dari proyek TodoMVC — sebuah contoh yang mungkin sudah tidak asing lagi bagi Anda. jQuery cukup warisan, mungkin mencerminkan situasi nyata dengan proyek Anda, dan yang paling penting, memerlukan pemeliharaan dan peretasan yang signifikan untuk memberi daya pada aplikasi dinamis modern. (Ini seharusnya cukup untuk mempertimbangkan migrasi ke sesuatu yang lebih fleksibel.)

Apa yang "lebih fleksibel" ini yang akan kita migrasikan? Untuk menunjukkan kasus yang sangat praktis yang berguna dalam kehidupan nyata, saya harus memilih di antara dua kerangka kerja paling populer saat ini: React dan Vue. Namun, mana pun yang saya pilih, kami akan kehilangan beberapa aspek dari arah lain.

Lebih banyak setelah melompat! Lanjutkan membaca di bawah ini

Jadi di bagian ini, kita akan menjalankan kedua hal berikut:

  • Migrasi aplikasi jQuery ke React , dan
  • Migrasi aplikasi jQuery ke Vue .
Tujuan kami: hasil migrasi ke React dan Vue
Tujuan kami: hasil migrasi ke React dan Vue. (Pratinjau besar)

Repositori Kode

Semua kode yang disebutkan di sini tersedia untuk umum, dan Anda dapat mengaksesnya kapan pun Anda mau. Ada dua repositori yang tersedia untuk Anda mainkan:

  • Frankenstein TodoMVC
    Repositori ini berisi aplikasi TodoMVC dalam kerangka kerja/pustaka yang berbeda. Misalnya, Anda dapat menemukan cabang seperti vue , angularjs , react dan jquery di repositori ini.
  • Demo Frankenstein
    Ini berisi beberapa cabang, yang masing-masing mewakili arah migrasi tertentu antara aplikasi, tersedia di repositori pertama. Ada cabang seperti migration/jquery-to-react dan migration/jquery-to-vue , khususnya, yang akan kita bahas nanti.

Kedua repositori sedang dalam proses dan cabang baru dengan aplikasi baru dan arah migrasi harus ditambahkan secara teratur. ( Anda juga bebas untuk berkontribusi! ) Riwayat komit di cabang migrasi terstruktur dengan baik dan mungkin berfungsi sebagai dokumentasi tambahan dengan lebih banyak detail daripada yang dapat saya bahas dalam artikel ini.

Sekarang, mari kita mengotori tangan kita! Perjalanan kita masih panjang, jadi jangan berharap ini akan menjadi perjalanan yang mulus. Terserah Anda untuk memutuskan bagaimana Anda ingin mengikuti artikel ini, tetapi Anda dapat melakukan hal berikut:

  • Kloning cabang jquery dari repositori Frankenstein TodoMVC dan ikuti semua instruksi di bawah ini dengan ketat.
  • Atau, Anda dapat membuka cabang yang didedikasikan untuk migrasi ke React atau migrasi ke Vue dari repositori Demo Frankenstein dan mengikuti sejarah commit.
  • Atau, Anda dapat bersantai dan terus membaca karena saya akan menyoroti kode yang paling penting di sini, dan jauh lebih penting untuk memahami mekanisme proses daripada kode yang sebenarnya.

Saya ingin menyebutkan sekali lagi bahwa kita akan secara ketat mengikuti langkah-langkah yang disajikan di bagian pertama artikel secara teoritis.

Mari selami!

  1. Identifikasi Layanan Mikro
  2. Izinkan Akses Host-ke-Alien
  3. Tulis Layanan/Komponen Alien
  4. Tulis Pembungkus Komponen Web Di Sekitar Layanan Alien
  5. Ganti Layanan Host Dengan Komponen Web
  6. Bilas & Ulangi Untuk Semua Komponen Anda
  7. Beralih ke Alien

1. Identifikasi Layanan Mikro

Seperti yang disarankan Bagian 1, dalam langkah ini, kita harus menyusun aplikasi kita menjadi layanan independen kecil yang didedikasikan untuk satu pekerjaan tertentu . Pembaca yang penuh perhatian mungkin memperhatikan bahwa aplikasi tugas kami sudah kecil dan independen dan dapat mewakili satu layanan mikro sendiri. Ini adalah bagaimana saya akan memperlakukannya sendiri jika aplikasi ini akan hidup dalam konteks yang lebih luas. Ingat, bagaimanapun, bahwa proses mengidentifikasi layanan mikro sepenuhnya subjektif dan tidak ada satu jawaban yang benar.

Jadi, untuk melihat proses Migrasi Frankenstein lebih detail, kita dapat melangkah lebih jauh dan membagi aplikasi tugas ini menjadi dua layanan mikro independen:

  1. Bidang input untuk menambahkan item baru.
    Layanan ini juga dapat berisi tajuk aplikasi, hanya berdasarkan kedekatan posisi elemen-elemen ini.
  2. Daftar item yang sudah ditambahkan.
    Layanan ini lebih maju, dan bersama dengan daftar itu sendiri, layanan ini juga berisi tindakan seperti pemfilteran, tindakan item daftar, dan sebagainya.
Aplikasi TodoMVC dibagi menjadi dua layanan mikro independen
Aplikasi TodoMVC dibagi menjadi dua layanan mikro independen. (Pratinjau besar)

Tip : Untuk memeriksa apakah layanan yang dipilih benar-benar independen, hapus markup HTML, yang mewakili setiap layanan ini. Pastikan fungsi yang tersisa masih berfungsi. Dalam kasus kami, seharusnya dimungkinkan untuk menambahkan entri baru ke localStorage (yang digunakan aplikasi ini sebagai penyimpanan) dari bidang input tanpa daftar, sementara daftar masih merender entri dari localStorage meskipun bidang input tidak ada. Jika aplikasi Anda menampilkan kesalahan saat Anda menghapus markup untuk layanan mikro potensial, lihat bagian “Refactor Jika Diperlukan” di Bagian 1 untuk contoh cara menangani kasus seperti itu.

Tentu saja, kami dapat melanjutkan dan membagi layanan kedua dan daftar item lebih jauh lagi menjadi layanan mikro independen untuk setiap item tertentu. Namun, mungkin terlalu terperinci untuk contoh ini. Jadi, untuk saat ini, kami menyimpulkan bahwa aplikasi kami akan memiliki dua layanan; mereka independen, dan masing-masing dari mereka bekerja menuju tugas khususnya sendiri. Oleh karena itu, kami telah membagi aplikasi kami menjadi layanan mikro .

2. Izinkan Akses Host-to-Alien

Biarkan saya secara singkat mengingatkan Anda tentang apa ini.

  • Tuan rumah
    Inilah yang disebut aplikasi kita saat ini. Itu ditulis dengan kerangka dari mana kita akan menjauh . Dalam kasus khusus ini, aplikasi jQuery kami.
  • Asing
    Sederhananya, ini adalah penulisan ulang Host secara bertahap pada kerangka kerja baru yang akan kita pindahkan . Sekali lagi, dalam kasus khusus ini, ini adalah aplikasi React atau Vue.

Aturan praktis saat memisahkan Host dan Alien adalah Anda harus dapat mengembangkan dan menerapkan salah satu dari mereka tanpa merusak yang lain — kapan saja.

Menjaga Host dan Alien independen satu sama lain sangat penting untuk Migrasi Frankenstein. Namun, ini membuat mengatur komunikasi antara keduanya sedikit menantang. Bagaimana kami mengizinkan Host mengakses Alien tanpa menghancurkan keduanya?

Menambahkan Alien Sebagai Submodule Host Anda

Meskipun ada beberapa cara untuk mencapai penyiapan yang kami butuhkan, bentuk paling sederhana dari mengatur proyek Anda untuk memenuhi kriteria ini mungkin adalah submodul git. Inilah yang akan kita gunakan dalam artikel ini. Saya akan menyerahkan kepada Anda untuk membaca dengan seksama tentang bagaimana submodul di git bekerja untuk memahami batasan dan gotcha dari struktur ini.

Prinsip umum arsitektur proyek kami dengan submodul git akan terlihat seperti ini:

  • Host dan Alien keduanya independen dan disimpan di repositori git terpisah;
  • Host mereferensikan Alien sebagai submodul. Pada tahap ini, Host memilih status tertentu (komit) dari Alien dan menambahkannya sebagai, seperti apa, subfolder dalam struktur folder Host.
Bereaksi TodoMVC ditambahkan sebagai submodule git ke dalam aplikasi jQuery TodoMVC
React TodoMVC ditambahkan sebagai submodule git ke dalam aplikasi jQuery TodoMVC. (Pratinjau besar)

Proses penambahan submodul sama untuk aplikasi apa pun. Mengajarkan git submodules berada di luar cakupan artikel ini dan tidak terkait langsung dengan Migrasi Frankenstein itu sendiri. Jadi mari kita lihat sekilas contoh yang mungkin.

Dalam cuplikan di bawah ini, kami menggunakan arah React sebagai contoh. Untuk arah migrasi lainnya, ganti react dengan nama cabang dari Frankenstein TodoMVC atau sesuaikan dengan nilai khusus jika diperlukan.

Jika Anda mengikuti menggunakan aplikasi jQuery TodoMVC asli:

 $ git submodule add -b react [email protected]:mishunov/frankenstein-todomvc.git react $ git submodule update --remote $ cd react $ npm i

Jika Anda mengikuti cabang migration/jquery-to-react (atau arah migrasi lainnya) dari repositori Demo Frankenstein, aplikasi Alien seharusnya sudah ada di sana sebagai git submodule , dan Anda akan melihat folder masing-masing. Namun, folder tersebut kosong secara default, dan Anda perlu memperbarui dan menginisialisasi submodul yang terdaftar.

Dari root proyek Anda (Host Anda):

 $ git submodule update --init $ cd react $ npm i

Perhatikan bahwa dalam kedua kasus kami menginstal dependensi untuk aplikasi Alien, tetapi dependensi tersebut menjadi kotak pasir ke subfolder dan tidak akan mencemari Host kami.

Setelah menambahkan aplikasi Alien sebagai submodule dari Host Anda, Anda mendapatkan aplikasi Alien dan Host yang independen (dalam hal layanan mikro). Namun, Host menganggap Alien sebagai subfolder dalam kasus ini, dan jelas, yang memungkinkan Host mengakses Alien tanpa masalah.

3. Tulis Layanan/Komponen Alien

Pada langkah ini, kita harus memutuskan layanan mikro mana yang akan dimigrasikan terlebih dahulu dan menulis/menggunakannya di sisi Alien. Mari ikuti urutan layanan yang sama yang kami identifikasi di Langkah 1 dan mulai dengan yang pertama: bidang input untuk menambahkan item baru. Namun, sebelum kita mulai, mari kita sepakati bahwa di luar titik ini, kita akan menggunakan komponen istilah yang lebih disukai daripada layanan mikro atau layanan saat kita bergerak menuju tempat kerangka kerja frontend dan komponen istilah mengikuti definisi hampir semua modern kerangka.

Cabang dari repositori Frankenstein TodoMVC berisi komponen yang dihasilkan yang mewakili layanan pertama "Bidang input untuk menambahkan item baru" sebagai komponen Header:

  • Komponen header di React
  • Komponen header di Vue

Menulis komponen dalam kerangka pilihan Anda berada di luar cakupan artikel ini dan bukan merupakan bagian dari Migrasi Frankenstein. Namun, ada beberapa hal yang perlu diingat saat menulis komponen Alien.

Kemerdekaan

Pertama-tama, komponen di Alien harus mengikuti prinsip independensi yang sama, yang sebelumnya diatur di sisi Host: komponen tidak boleh bergantung pada komponen lain dengan cara apa pun.

Interoperabilitas

Berkat independensi layanan, kemungkinan besar, komponen di Host Anda berkomunikasi dalam beberapa cara yang mapan baik itu sistem manajemen negara, komunikasi melalui beberapa penyimpanan bersama atau, langsung melalui sistem acara DOM. “Interoperabilitas” komponen Alien berarti bahwa komponen tersebut harus dapat terhubung ke sumber komunikasi yang sama, yang dibuat oleh Host, untuk mengirimkan informasi tentang perubahan statusnya dan mendengarkan perubahan pada komponen lain. Dalam praktiknya, ini berarti bahwa jika komponen di Host Anda berkomunikasi melalui peristiwa DOM, membuat komponen Alien Anda secara eksklusif dengan mempertimbangkan manajemen status tidak akan bekerja dengan sempurna untuk jenis migrasi ini, sayangnya.

Sebagai contoh, lihat file js/storage.js yang merupakan saluran komunikasi utama untuk komponen jQuery kami:

 ... fetch: function() { return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]"); }, save: function(todos) { localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)); var event = new CustomEvent("store-update", { detail: { todos } }); document.dispatchEvent(event); }, ...

Di sini, kami menggunakan localStorage (karena contoh ini tidak kritis terhadap keamanan) untuk menyimpan item yang harus dilakukan, dan setelah perubahan pada penyimpanan dicatat, kami mengirimkan peristiwa DOM khusus pada elemen document yang dapat didengarkan oleh komponen mana pun.

Pada saat yang sama, di sisi Alien (katakanlah Bereaksi) kita dapat mengatur komunikasi manajemen negara yang kompleks seperti yang kita inginkan. Namun, mungkin bijaksana untuk menyimpannya untuk masa depan: untuk berhasil mengintegrasikan komponen Alien React kita ke dalam Host, kita harus terhubung ke saluran komunikasi yang sama yang digunakan oleh Host. Dalam hal ini, ini adalah localStorage . Untuk mempermudah, kami hanya menyalin file penyimpanan Host ke Alien dan menghubungkan komponen kami ke sana:

 import todoStorage from "../storage"; class Header extends Component { constructor(props) { this.state = { todos: todoStorage.fetch() }; } componentDidMount() { document.addEventListener("store-update", this.updateTodos); } componentWillUnmount() { document.removeEventListener("store-update", this.updateTodos); } componentDidUpdate(prevProps, prevState) { if (prevState.todos !== this.state.todos) { todoStorage.save(this.state.todos); } } ... }

Sekarang, komponen Alien kami dapat berbicara bahasa yang sama dengan komponen Host dan sebaliknya.

4. Tulis Pembungkus Komponen Web Di Sekitar Layanan Alien

Walaupun saat ini kita baru berada di langkah keempat, sudah cukup banyak yang kita capai:

  • Kami telah membagi aplikasi Host kami menjadi layanan independen yang siap digantikan oleh layanan Alien;
  • Kami telah menyiapkan Host dan Alien untuk sepenuhnya independen satu sama lain, namun terhubung dengan sangat baik melalui git submodules ;
  • Kami telah menulis komponen Alien pertama kami menggunakan kerangka kerja baru.

Sekarang saatnya untuk mengatur jembatan antara Host dan Alien agar komponen Alien yang baru dapat berfungsi di Host.

Pengingat dari Bagian 1 : Pastikan Host Anda memiliki paket bundler yang tersedia. Dalam artikel ini, kami mengandalkan Webpack, tetapi itu tidak berarti bahwa teknik ini tidak akan bekerja dengan Rollup atau bundler lain pilihan Anda. Namun, saya menyerahkan pemetaan dari Webpack ke eksperimen Anda.

Konvensi penamaan

Seperti yang disebutkan dalam artikel sebelumnya, kita akan menggunakan Komponen Web untuk mengintegrasikan Alien ke dalam Host. Di sisi Host, kami membuat file baru: js/frankenstein-wrappers/Header-wrapper.js . (Ini akan menjadi pembungkus Frankenstein pertama kami.) Ingatlah bahwa ada baiknya untuk memberi nama pembungkus Anda sama dengan komponen Anda di aplikasi Alien, misalnya hanya dengan menambahkan akhiran “ -wrapper ”. Anda akan melihat nanti mengapa ini adalah ide yang bagus, tetapi untuk saat ini, mari kita sepakati bahwa ini berarti bahwa jika komponen Alien disebut Header.js (di React) atau Header.vue (di Vue), pembungkus yang sesuai pada Sisi host harus disebut Header-wrapper.js .

Dalam pembungkus pertama kami, kami mulai dengan pelat dasar dasar untuk mendaftarkan elemen khusus:

 class FrankensteinWrapper extends HTMLElement {} customElements.define("frankenstein-header-wrapper", FrankensteinWrapper);

Selanjutnya, kita harus menginisialisasi Shadow DOM untuk elemen ini.

Silakan merujuk ke Bagian 1 untuk mendapatkan alasan mengapa kami menggunakan Shadow DOM.

 class FrankensteinWrapper extends HTMLElement { connectedCallback() { this.attachShadow({ mode: "open" }); } }

Dengan ini, kami memiliki semua bagian penting dari Komponen Web yang disiapkan, dan inilah saatnya untuk menambahkan komponen Alien kami ke dalam campuran. Pertama-tama, di awal pembungkus Frankenstein kita, kita harus mengimpor semua bit yang bertanggung jawab untuk rendering komponen Alien.

 import React from "../../react/node_modules/react"; import ReactDOM from "../../react/node_modules/react-dom"; import HeaderApp from "../../react/src/components/Header"; ...

Di sini kita harus berhenti sejenak. Perhatikan bahwa kami tidak mengimpor dependensi Alien dari node_modules Host. Semuanya berasal dari Alien itu sendiri yang berada di react/ subfolder. Itulah mengapa Langkah 2 sangat penting, dan sangat penting untuk memastikan Host memiliki akses penuh ke aset Alien.

Sekarang, kita dapat merender komponen Alien kita di dalam Shadow DOM Komponen Web:

 ... connectedCallback() { ... ReactDOM.render(<HeaderApp />, this.shadowRoot); } ...

Catatan : Dalam hal ini, React tidak membutuhkan yang lain. Namun, untuk merender komponen Vue, Anda perlu menambahkan simpul pembungkus untuk memuat komponen Vue Anda seperti berikut:

 ... connectedCallback() { const mountPoint = document.createElement("div"); this.attachShadow({ mode: "open" }).appendChild(mountPoint); new Vue({ render: h => h(VueHeader) }).$mount(mountPoint); } ...

Alasan untuk ini adalah perbedaan dalam cara React dan Vue merender komponen: React menambahkan komponen ke node DOM yang direferensikan, sementara Vue menggantikan node DOM yang direferensikan dengan komponen. Oleh karena itu, jika kita melakukan .$mount(this.shadowRoot) untuk Vue, itu pada dasarnya menggantikan Shadow DOM.

Itu saja yang harus kita lakukan untuk pembungkus kita untuk saat ini. Hasil saat ini untuk pembungkus Frankenstein di kedua arah migrasi jQuery-to-React dan jQuery-to-Vue dapat ditemukan di sini:

  • Frankenstein Wrapper untuk komponen React
  • Frankenstein Wrapper untuk komponen Vue

Untuk meringkas mekanisme pembungkus Frankenstein:

  1. Buat elemen khusus,
  2. Mulai DOM Bayangan,
  3. Impor semua yang diperlukan untuk merender komponen Alien,
  4. Render komponen Alien di dalam Shadow DOM elemen kustom.

Namun, ini tidak membuat Alien in Host kita secara otomatis. Kami harus mengganti markup Host yang ada dengan pembungkus Frankenstein baru kami.

Kencangkan sabuk pengaman Anda, itu mungkin tidak semudah yang diharapkan!

5. Ganti Layanan Host Dengan Komponen Web

Mari lanjutkan dan tambahkan file Header-wrapper.js baru ke index.html dan ganti markup header yang ada dengan elemen kustom <frankenstein-header-wrapper> yang baru dibuat.

 ... <!-- <header class="header">--> <!-- <h1>todos</h1>--> <!-- <input class="new-todo" placeholder="What needs to be done?" autofocus>--> <!-- </header>--> <frankenstein-header-wrapper></frankenstein-header-wrapper> ... <script type="module" src="js/frankenstein-wrappers/Header-wrapper.js"></script>

Sayangnya, ini tidak akan bekerja sesederhana itu. Jika Anda membuka browser dan memeriksa konsol, ada Uncaught SyntaxError yang menunggu Anda. Bergantung pada browser dan dukungannya untuk modul ES6, itu akan terkait dengan impor ES6 atau dengan cara komponen Alien dirender. Either way, kita harus melakukan sesuatu tentang hal itu, tetapi masalah dan solusinya harus akrab dan jelas bagi sebagian besar pembaca.

5.1. Perbarui Webpack dan Babel jika diperlukan

Kita harus melibatkan beberapa keajaiban Webpack dan Babel sebelum mengintegrasikan pembungkus Frankenstein kita. Perdebatan alat-alat ini berada di luar cakupan artikel, tetapi Anda dapat melihat komit yang sesuai di repositori Demo Frankenstein:

  • Konfigurasi untuk migrasi ke React
  • Konfigurasi untuk migrasi ke Vue

Pada dasarnya, kami mengatur pemrosesan file serta frankenstein titik masuk baru dalam konfigurasi Webpack untuk memuat semua yang terkait dengan pembungkus Frankenstein di satu tempat.

Setelah Webpack di Host mengetahui cara memproses komponen Alien dan Komponen Web, kami siap mengganti markup Host dengan pembungkus Frankenstein yang baru.

5.2. Penggantian Komponen Sebenarnya

Penggantian komponen harus mudah sekarang. Di index.html Host Anda, lakukan hal berikut:

  1. Ganti elemen DOM <header class="header"> dengan <frankenstein-header-wrapper> ;
  2. Tambahkan skrip baru frankenstein.js . Ini adalah titik masuk baru di Webpack yang berisi semua yang terkait dengan pembungkus Frankenstein.
 ... <!-- We replace <header class="header"> --> <frankenstein-header-wrapper></frankenstein-header-wrapper> ... <script src="./frankenstein.js"></script>

Itu dia! Mulai ulang server Anda jika diperlukan dan saksikan keajaiban komponen Alien yang terintegrasi ke dalam Host.

Namun, sepertinya masih ada yang kurang. Komponen Alien dalam konteks Host tidak terlihat sama seperti dalam konteks aplikasi Alien mandiri. Ini hanya unstyled.

Komponen Reaksi Alien Tanpa Gaya setelah diintegrasikan ke dalam Host
Komponen Alien React tanpa gaya setelah diintegrasikan ke dalam Host (Pratinjau besar)

Kenapa gitu? Bukankah seharusnya gaya komponen diintegrasikan dengan komponen Alien ke dalam Host secara otomatis? Saya berharap mereka akan melakukannya, tetapi seperti dalam banyak situasi, itu tergantung. Kita memasuki bagian yang menantang dari Migrasi Frankenstein.

5.3. Informasi Umum Tentang Styling Komponen Alien

Pertama-tama, ironisnya adalah tidak ada bug dalam cara kerja segala sesuatunya. Semuanya seperti yang dirancang untuk bekerja. Untuk menjelaskan hal ini, mari kita sebutkan secara singkat berbagai cara komponen penataan gaya.

Gaya Global

Kita semua akrab dengan ini: gaya global dapat (dan biasanya) didistribusikan tanpa komponen tertentu dan diterapkan ke seluruh halaman. Gaya global memengaruhi semua simpul DOM dengan pemilih yang cocok.

Beberapa contoh gaya global adalah <style> dan <link rel="stylesheet"> yang ditemukan di index.html Anda. Sebagai alternatif, stylesheet global dapat diimpor ke beberapa modul root JS sehingga semua komponen juga dapat mengaksesnya.

Masalah penerapan gaya dengan cara ini jelas: mempertahankan lembar gaya monolitik untuk aplikasi besar menjadi sangat sulit. Juga, seperti yang kita lihat di artikel sebelumnya, gaya global dapat dengan mudah memecah komponen yang dirender langsung di pohon DOM utama seperti di React atau Vue.

Gaya yang Dibundel

Gaya ini biasanya digabungkan erat dengan komponen itu sendiri dan jarang didistribusikan tanpa komponen. Gaya biasanya berada di file yang sama dengan komponen. Contoh bagus dari jenis gaya ini adalah komponen gaya dalam Modul React atau CSS dan Scoped CSS dalam komponen file tunggal di Vue. Namun, terlepas dari berbagai alat untuk menulis gaya yang dibundel, prinsip yang mendasari sebagian besar adalah sama: alat menyediakan mekanisme pelingkupan untuk mengunci gaya yang ditentukan dalam suatu komponen sehingga gaya tidak merusak komponen lain atau global gaya.

Mengapa Scoped Styles Bisa Rapuh?

Di Bagian 1, saat membenarkan penggunaan Shadow DOM di Frankenstein Migration, kami membahas secara singkat topik pelingkupan vs. enkapsulasi) dan bagaimana enkapsulasi Shadow DOM berbeda dari alat penataan pelingkupan. Namun, kami tidak menjelaskan mengapa alat pelingkupan memberikan gaya yang rapuh untuk komponen kami, dan sekarang, ketika kami menghadapi komponen Alien yang tidak ditata, itu menjadi penting untuk dipahami.

Semua alat pelingkupan untuk kerangka kerja modern bekerja dengan cara yang sama:

  • Anda menulis gaya untuk komponen Anda dalam beberapa cara tanpa berpikir banyak tentang ruang lingkup atau enkapsulasi;
  • Anda menjalankan komponen Anda dengan stylesheet yang diimpor/disematkan melalui beberapa sistem bundling, seperti Webpack atau Rollup;
  • Bundeler menghasilkan kelas CSS unik atau atribut lain, membuat dan memasukkan penyeleksi individual untuk HTML Anda dan lembar gaya yang sesuai;
  • Bundeler membuat entri <style> di <head> dokumen Anda dan menempatkan gaya komponen Anda dengan pemilih campuran unik di sana.

Itu cukup banyak. Ini berfungsi dan berfungsi dengan baik dalam banyak kasus. Kecuali jika tidak: ketika gaya untuk semua komponen hidup dalam lingkup gaya global, menjadi mudah untuk memecahkannya, misalnya, menggunakan spesifisitas yang lebih tinggi. Ini menjelaskan potensi kerapuhan alat pelingkupan, tetapi mengapa komponen Alien kami sama sekali tidak ditata?

Mari kita lihat Host saat ini menggunakan DevTools. Saat memeriksa pembungkus Frankenstein yang baru ditambahkan dengan komponen Alien React, misalnya, kita dapat melihat sesuatu seperti ini:

Bungkus Frankenstein dengan komponen Alien di dalamnya. Perhatikan kelas CSS unik pada node Alien.
Bungkus Frankenstein dengan komponen Alien di dalamnya. Perhatikan kelas CSS unik pada node Alien. (Pratinjau besar)

Jadi, Webpack memang menghasilkan kelas CSS unik untuk komponen kita. Besar! Di mana gaya-gaya itu? Nah, gayanya persis seperti yang dirancang — di <head> dokumen.

Sementara komponen Alien ada di dalam pembungkus Frankenstein, gayanya ada di kepala dokumen.
Sementara komponen Alien ada di dalam pembungkus Frankenstein, gayanya ada di <head> dokumen. (Pratinjau besar)

Jadi semuanya berjalan sebagaimana mestinya, dan ini adalah masalah utama. Karena komponen Alien kami berada di Shadow DOM, dan seperti yang dijelaskan di Bagian #1, Shadow DOM menyediakan enkapsulasi penuh komponen dari sisa halaman dan gaya global, termasuk stylesheet yang baru dibuat untuk komponen yang tidak dapat melewati batas bayangan dan sampai ke komponen Alien. Oleh karena itu, komponen Alien dibiarkan tanpa gaya. Namun, sekarang, taktik untuk memecahkan masalah harus jelas: entah bagaimana kita harus menempatkan gaya komponen di Shadow DOM yang sama di mana komponen kita berada (bukan <head> ).

5.4. Memperbaiki Gaya Untuk Komponen Alien

Hingga saat ini, proses migrasi ke kerangka kerja apa pun adalah sama. Namun, hal-hal mulai berbeda di sini: setiap kerangka kerja memiliki rekomendasi tentang cara menata komponen, dan karenanya, cara mengatasi masalah berbeda. Di sini, kita membahas kasus yang paling umum tetapi, jika kerangka kerja yang Anda gunakan menggunakan beberapa cara unik untuk menata komponen, Anda perlu mengingat taktik dasar seperti memasukkan gaya komponen ke dalam Shadow DOM alih-alih <head> .

Dalam bab ini, kami membahas perbaikan untuk:

  • Gaya yang dibundel dengan Modul CSS di Vue (taktik untuk Scoped CSS adalah sama);
  • Kumpulan gaya dengan komponen gaya di React;
  • Modul CSS Generik dan gaya global. Saya menggabungkan ini karena Modul CSS, secara umum, sangat mirip dengan stylesheet global dan dapat diimpor oleh komponen apa pun yang membuat gaya terputus dari komponen tertentu.

Kendala terlebih dahulu: apa pun yang kita lakukan untuk memperbaiki gaya tidak boleh merusak komponen Alien itu sendiri . Jika tidak, kami kehilangan independensi sistem Alien dan Host kami. Jadi, untuk mengatasi masalah gaya, kita akan mengandalkan konfigurasi bundler atau pembungkus Frankenstein.

Gaya yang Dibundel Di Vue Dan Shadow DOM

Jika Anda sedang menulis aplikasi Vue, kemungkinan besar Anda menggunakan komponen file tunggal. Jika Anda juga menggunakan Webpack, Anda harus terbiasa dengan dua pemuat vue-loader dan vue-style-loader . Yang pertama memungkinkan Anda untuk menulis komponen file tunggal tersebut sementara yang kedua secara dinamis menyuntikkan CSS komponen ke dalam dokumen sebagai tag <style> . Secara default, vue-style-loader menyuntikkan gaya komponen ke dalam <head> dokumen. Namun, kedua paket menerima opsi shadowMode dalam konfigurasi yang memungkinkan kita untuk dengan mudah mengubah perilaku default dan menyuntikkan gaya (seperti yang tersirat dari nama opsi) ke dalam Shadow DOM. Mari kita lihat aksinya.

Konfigurasi Paket Web

Minimal, file konfigurasi Webpack harus berisi yang berikut ini:

 const VueLoaderPlugin = require('vue-loader/lib/plugin'); ... module: { rules: [ { test: /\.vue$/, loader: 'vue-loader', options: { shadowMode: true } }, { test: /\.css$/, include: path.resolve(__dirname, '../vue'), use: [ { loader:'vue-style-loader', options: { shadowMode: true } }, 'css-loader' ] } ], plugins: [ new VueLoaderPlugin() ] }

Dalam aplikasi nyata, blok test: /\.css$/ Anda akan lebih canggih (mungkin melibatkan aturan oneOf ) untuk memperhitungkan konfigurasi Host dan Alien. Namun, dalam kasus ini, jQuery kami ditata dengan <link rel="stylesheet"> sederhana di index.html , jadi kami tidak membuat gaya untuk Host melalui Webpack, dan aman untuk melayani Alien saja.

Konfigurasi Pembungkus

Selain konfigurasi Webpack, kita juga perlu memperbarui pembungkus Frankenstein kita, mengarahkan Vue ke Shadow DOM yang benar. Dalam Header-wrapper.js kami, rendering komponen Vue harus menyertakan properti shadowRoot yang mengarah ke shadowRoot dari pembungkus Frankenstein kami:

 ... new Vue({ shadowRoot: this.shadowRoot, render: h => h(VueHeader) }).$mount(mountPoint); ...

Setelah Anda memperbarui file dan memulai ulang server Anda, Anda akan mendapatkan sesuatu seperti ini di DevTools Anda:

Gaya yang dibundel dengan komponen Alien Vue ditempatkan di dalam pembungkus Frankenstein dengan semua kelas CSS unik yang dipertahankan.
Gaya yang dibundel dengan komponen Alien Vue ditempatkan di dalam pembungkus Frankenstein dengan semua kelas CSS unik yang dipertahankan. (Pratinjau besar)

Terakhir, gaya untuk komponen Vue ada di dalam Shadow DOM kita. Pada saat yang sama, aplikasi Anda akan terlihat seperti ini:

Komponen header mulai terlihat lebih seperti seharusnya. Namun, masih ada yang kurang.
Komponen header mulai terlihat lebih seperti seharusnya. Namun, masih ada yang kurang. (Pratinjau besar)

Kami mulai mendapatkan sesuatu yang menyerupai aplikasi Vue kami: gaya yang dibundel dengan komponen, disuntikkan ke dalam DOM Shadow pembungkus, tetapi komponen masih terlihat tidak seperti yang seharusnya. Alasannya adalah bahwa dalam aplikasi Vue asli, komponen ditata tidak hanya dengan gaya yang dibundel tetapi juga sebagian dengan gaya global. Namun, sebelum memperbaiki gaya global, kita harus mendapatkan integrasi React kita ke status yang sama dengan Vue.

Gaya yang Dibundel Dalam React Dan Shadow DOM

Karena ada banyak cara seseorang dapat mendesain komponen React, solusi khusus untuk memperbaiki komponen Alien di Frankenstein Migration bergantung pada cara kita mendesain komponen di tempat pertama. Mari kita bahas secara singkat alternatif yang paling umum digunakan.

gaya-komponen

styled-components adalah salah satu cara paling populer untuk menata komponen React. Untuk komponen Header React, komponen gaya adalah persis seperti yang kita gayakan. Karena ini adalah pendekatan CSS-in-JS klasik, tidak ada file dengan ekstensi khusus yang dapat kami kaitkan dengan bundler kami seperti yang kami lakukan untuk file .css atau .js , misalnya. Untungnya, komponen bergaya memungkinkan injeksi gaya komponen ke dalam simpul khusus (Shadow DOM dalam kasus kami) alih-alih head dokumen dengan bantuan komponen bantuan StyleSheetManager . Ini adalah komponen yang telah ditentukan sebelumnya, diinstal dengan paket styled-components yang menerima properti target , mendefinisikan "node DOM alternatif untuk menyuntikkan info gaya". Persis apa yang kita butuhkan! Selain itu, kami bahkan tidak perlu mengubah konfigurasi Webpack kami: semuanya tergantung pada pembungkus Frankenstein kami.

Kita harus memperbarui Header-wrapper.js yang berisi komponen React Alien dengan baris berikut:

 ... import { StyleSheetManager } from "../../react/node_modules/styled-components"; ... const target = this.shadowRoot; ReactDOM.render( <StyleSheetManager target={target}> <HeaderApp /> </StyleSheetManager>, appWrapper ); ...

Di sini, kita mengimpor komponen StyleSheetManager (dari Alien, dan bukan dari Host) dan membungkus komponen React kita dengannya. Pada saat yang sama, kami mengirim properti target yang menunjuk ke shadowRoot kami. Itu dia. Jika Anda me-restart server, Anda harus melihat sesuatu seperti ini di DevTools Anda:

Gaya yang dibundel dengan komponen React Alien ditempatkan di dalam pembungkus Frankenstein dengan semua kelas CSS unik yang dipertahankan.
Gaya yang dibundel dengan komponen React Alien ditempatkan di dalam pembungkus Frankenstein dengan semua kelas CSS unik yang dipertahankan. (Pratinjau besar)

Sekarang, gaya komponen kita ada di Shadow DOM bukan <head> . Dengan cara ini, rendering aplikasi kita sekarang menyerupai apa yang telah kita lihat dengan aplikasi Vue sebelumnya.

Setelah memindahkan gaya yang dibundel ke dalam pembungkus Frankenstein, komponen Alien React mulai terlihat lebih baik. Namun, kami belum sampai di sana.
Setelah memindahkan gaya yang dibundel ke dalam pembungkus Frankenstein, komponen Alien React mulai terlihat lebih baik. Namun, kami belum sampai di sana. (Pratinjau besar)

Cerita yang sama: styled-components bertanggung jawab hanya untuk bagian yang dibundel dari komponen React styles , dan global styles mengelola bit yang tersisa. Kami kembali ke gaya global sebentar lagi setelah kami meninjau satu lagi jenis komponen gaya.

Modul CSS

Jika Anda melihat lebih dekat pada komponen Vue yang telah kami perbaiki sebelumnya, Anda mungkin memperhatikan bahwa Modul CSS persis seperti cara kami menata komponen itu. However, even if we style it with Scoped CSS (another recommended way of styling Vue components) the way we fix our unstyled component doesn't change: it is still up to vue-loader and vue-style-loader to handle it through shadowMode: true option.

When it comes to CSS Modules in React (or any other system using CSS Modules without any dedicated tools), things get a bit more complicated and less flexible, unfortunately.

Let's take a look at the same React component which we've just integrated, but this time styled with CSS Modules instead of styled-components. The main thing to note in this component is a separate import for stylesheet:

 import styles from './Header.module.css'

The .module.css extension is a standard way to tell React applications built with the create-react-app utility that the imported stylesheet is a CSS Module. The stylesheet itself is very basic and does precisely the same our styled-components do.

Integrating CSS modules into a Frankenstein wrapper consists of two parts:

  • Enabling CSS Modules in bundler,
  • Pushing resulting stylesheet into Shadow DOM.

I believe the first point is trivial: all you need to do is set { modules: true } for css-loader in your Webpack configuration. Since, in this particular case, we have a dedicated extension for our CSS Modules ( .module.css ), we can have a dedicated configuration block for it under the general .css configuration:

 { test: /\.css$/, oneOf: [ { test: /\.module\.css$/, use: [ ... { loader: 'css-loader', options: { modules: true, } } ] } ] }

Note : A modules option for css-loader is all we have to know about CSS Modules no matter whether it's React or any other system. When it comes to pushing resulting stylesheet into Shadow DOM, however, CSS Modules are no different from any other global stylesheet.

By now, we went through the ways of integrating bundled styles into Shadow DOM for the following conventional scenarios:

  • Vue components, styled with CSS Modules. Dealing with Scoped CSS in Vue components won't be any different;
  • React components, styled with styled-components;
  • Components styled with raw CSS Modules (without dedicated tools like those in Vue). For these, we have enabled support for CSS modules in Webpack configuration.

However, our components still don't look as they are supposed to because their styles partially come from global styles . Those global styles do not come to our Frankenstein wrappers automatically. Moreover, you might get into a situation in which your Alien components are styled exclusively with global styles without any bundled styles whatsoever. So let's finally fix this side of the story.

Global Styles And Shadow DOM

Having your components styled with global styles is neither wrong nor bad per se: every project has its requirements and limitations. However, the best you can do for your components if they rely on some global styles is to pull those styles into the component itself. This way, you have proper easy-to-maintain self-contained components with bundled styles.

Nevertheless, it's not always possible or reasonable to do so: several components might share some styling, or your whole styling architecture could be built using global stylesheets that are split into the modular structure, and so on.

So having an opportunity to pull in global styles into our Frankenstein wrappers wherever it's required is essential for the success of this type of migration. Before we get to an example, keep in mind that this part is the same for pretty much any framework of your choice — be it React, Vue or anything else using global stylesheets!

Let's get back to our Header component from the Vue application. Take a look at this import:

 import "todomvc-app-css/index.css";

This import is where we pull in the global stylesheet. In this case, we do it from the component itself. It's only one way of using global stylesheet to style your component, but it's not necessarily like this in your application.

Some parent module might add a global stylesheet like in our React application where we import index.css only in index.js , and then our components expect it to be available in the global scope. Your component's styling might even rely on a stylesheet, added with <style> or <link> to your index.html . Tidak masalah. What matters, however, is that you should expect to either import global stylesheets in your Alien component (if it doesn't harm the Alien application) or explicitly in the Frankenstein wrapper. Otherwise, the wrapper would not know that the Alien component needs any stylesheet other than the ones already bundled with it.

Caution . If there are many global stylesheets to be shared between Alien components and you have a lot of such components, this might harm the performance of your Host application under the migration period.

Here is how import of a global stylesheet, required for the Header component, is done in Frankenstein wrapper for React component:

 // we import directly from react/, not from Host import '../../react/node_modules/todomvc-app-css/index.css'

Nevertheless, by importing a stylesheet this way, we still bring the styles to the global scope of our Host, while what we need is to pull in the styles into our Shadow DOM. Bagaimana kita melakukan ini?

Webpack configuration for global stylesheets & Shadow DOM

First of all, you might want to add an explicit test to make sure that we process only the stylesheets coming from our Alien. In case of our React migration, it will look similar to this:

 test: /\.css$/, oneOf: [ // this matches stylesheets coming from /react/ subfolder { test: /\/react\//, use: [] }, ... ]

In case of Vue application, obviously, you change test: /\/react\// with something like test: /\/vue\// . Apart from that, the configuration will be the same for any framework. Next, let's specify the required loaders for this block.

 ... use: [ { loader: 'style-loader', options: { ... } }, 'css-loader' ]

Two things to note. First, you have to specify modules: true in css-loader 's configuration if you're processing CSS Modules of your Alien application.

Second, we should convert styles into <style> tag before injecting those into Shadow DOM. In the case of Webpack, for that, we use style-loader . The default behavior for this loader is to insert styles into the document's head. Typically. And this is precisely what we don't want: our goal is to get stylesheets into Shadow DOM. However, in the same way we used target property for styled-components in React or shadowMode option for Vue components that allowed us to specify custom insertion point for our <style> tags, regular style-loader provides us with nearly same functionality for any stylesheet: the insert configuration option is exactly what helps us achieve our primary goal. Kabar baik! Let's add it to our configuration.

 ... { loader: 'style-loader', options: { insert: 'frankenstein-header-wrapper' } }

However, not everything is so smooth here with a couple of things to keep in mind.

Stylesheet global dan opsi insert dari style-loader

Jika Anda memeriksa dokumentasi untuk opsi ini, Anda perhatikan, bahwa opsi ini membutuhkan satu pemilih per konfigurasi. Ini berarti bahwa jika Anda memiliki beberapa komponen Alien yang memerlukan gaya global yang ditarik ke dalam pembungkus Frankenstein, Anda harus menentukan style-loader untuk setiap pembungkus Frankenstein. Dalam praktiknya, ini berarti Anda, mungkin, harus bergantung pada aturan oneOf di blok konfigurasi Anda untuk melayani semua pembungkus.

 { test: /\/react\//, oneOf: [ { test: /1-TEST-FOR-ALIEN-FILE-PATH$/, use: [ { loader: 'style-loader', options: { insert: '1-frankenstein-wrapper' } }, `css-loader` ] }, { test: /2-TEST-FOR-ALIEN-FILE-PATH$/, use: [ { loader: 'style-loader', options: { insert: '2-frankenstein-wrapper' } }, `css-loader` ] }, // etc. ], }

Tidak terlalu fleksibel, saya setuju. Namun demikian, itu bukan masalah besar selama Anda tidak memiliki ratusan komponen untuk dimigrasikan. Jika tidak, mungkin membuat konfigurasi Webpack Anda sulit untuk dipertahankan. Masalah sebenarnya, bagaimanapun, adalah bahwa kita tidak dapat menulis pemilih CSS untuk Shadow DOM.

Mencoba menyelesaikan ini, kami mungkin mencatat bahwa opsi insert juga dapat mengambil fungsi alih-alih pemilih biasa untuk menentukan logika yang lebih maju untuk penyisipan. Dengan ini, kita dapat menggunakan opsi ini untuk menyisipkan stylesheet langsung ke Shadow DOM! Dalam bentuk yang disederhanakan mungkin terlihat seperti ini:

 insert: function(element) { var parent = document.querySelector('frankenstein-header-wrapper').shadowRoot; parent.insertBefore(element, parent.firstChild); }

Menggoda, bukan? Namun, ini tidak akan berhasil untuk skenario kami atau akan bekerja jauh dari optimal. <frankenstein-header-wrapper> kami memang tersedia dari index.html (karena kami menambahkannya di Langkah 5.2). Tetapi ketika Webpack memproses semua dependensi (termasuk stylesheet) baik untuk komponen Alien atau wrapper Frankenstein, Shadow DOM belum diinisialisasi dalam wrapper Frankenstein: impor diproses sebelum itu. Oleh karena itu, mengarahkan insert langsung ke shadowRoot akan menghasilkan kesalahan.

Hanya ada satu kasus ketika kami dapat menjamin bahwa Shadow DOM diinisialisasi sebelum Webpack memproses ketergantungan stylesheet kami. Jika komponen Alien tidak mengimpor stylesheet itu sendiri dan tergantung pada pembungkus Frankenstein untuk mengimpornya, kami mungkin menggunakan impor dinamis dan mengimpor stylesheet yang diperlukan setelah kami menyiapkan Shadow DOM:

 this.attachShadow({ mode: "open" }); import('../vue/node_modules/todomvc-app-css/index.css');

Ini akan berfungsi: impor seperti itu, dikombinasikan dengan konfigurasi insert di atas, memang akan menemukan Shadow DOM yang benar dan memasukkan <style> ke dalamnya. Namun demikian, mendapatkan dan memproses stylesheet akan memakan waktu, yang berarti pengguna Anda pada koneksi yang lambat atau perangkat yang lambat mungkin menghadapi momen komponen yang tidak ditata sebelum stylesheet Anda berada di tempatnya di dalam Shadow DOM pembungkus.

Komponen Alien yang tidak diberi gaya akan dirender sebelum lembar gaya global diimpor dan ditambahkan ke Shadow DOM.
Komponen Alien yang tidak diberi gaya akan dirender sebelum lembar gaya global diimpor dan ditambahkan ke Shadow DOM. (Pratinjau besar)

Jadi secara keseluruhan, meskipun fungsi insert menerima, sayangnya, itu tidak cukup bagi kami, dan kami harus kembali ke pemilih CSS biasa seperti frankenstein-header-wrapper . Namun, ini tidak menempatkan lembar gaya ke dalam Shadow DOM secara otomatis, dan lembar gaya berada di <frankenstein-header-wrapper> di luar Shadow DOM.

style-loader menempatkan stylesheet yang diimpor ke dalam pembungkus Frankenstein, tetapi di luar Shadow DOM.
style-loader menempatkan stylesheet yang diimpor ke dalam pembungkus Frankenstein, tetapi di luar Shadow DOM. (Pratinjau besar)

Kami membutuhkan satu bagian lagi dari teka-teki.

Konfigurasi pembungkus untuk stylesheet global & Shadow DOM

Untungnya, perbaikannya cukup mudah di sisi pembungkus: ketika Shadow DOM diinisialisasi, kita perlu memeriksa setiap lembar gaya yang tertunda di pembungkus saat ini dan menariknya ke dalam Shadow DOM.

Status impor global stylesheet saat ini adalah sebagai berikut:

  • Kami mengimpor stylesheet yang harus ditambahkan ke Shadow DOM. Stylesheet dapat diimpor baik dalam komponen Alien itu sendiri atau, secara eksplisit di pembungkus Frankenstein. Dalam kasus migrasi ke React, misalnya, impor diinisialisasi dari pembungkus. Namun, dalam migrasi ke Vue, komponen serupa itu sendiri mengimpor stylesheet yang diperlukan, dan kita tidak perlu mengimpor apa pun di pembungkusnya.
  • Seperti yang ditunjukkan di atas, ketika Webpack memproses impor .css untuk komponen Alien, berkat opsi insert style-loader , stylesheet disuntikkan ke dalam pembungkus Frankenstein, tetapi di luar Shadow DOM.

Inisialisasi Shadow DOM yang disederhanakan dalam pembungkus Frankenstein, seharusnya saat ini (sebelum kita menarik lembar gaya apa pun) terlihat mirip dengan ini:

 this.attachShadow({ mode: "open" }); ReactDOM.render(); // or `new Vue()`

Sekarang, untuk menghindari kedipan komponen yang tidak diberi gaya, yang perlu kita lakukan sekarang adalah menarik semua lembar gaya yang diperlukan setelah inisialisasi Shadow DOM, tetapi sebelum rendering komponen Alien.

 this.attachShadow({ mode: "open" }); Array.prototype.slice .call(this.querySelectorAll("style")) .forEach(style => { this.shadowRoot.prepend(style); }); ReactDOM.render(); // or new Vue({})

Itu adalah penjelasan yang panjang dengan banyak detail, tetapi terutama, semua yang diperlukan untuk menarik stylesheet global ke dalam Shadow DOM:

  • Dalam konfigurasi Webpack tambahkan style-loader dengan opsi insert yang menunjuk ke pembungkus Frankenstein yang diperlukan.
  • Di pembungkus itu sendiri, tarik lembar gaya "tertunda" setelah inisialisasi Shadow DOM, tetapi sebelum rendering komponen Alien.

Setelah menerapkan perubahan ini, komponen Anda harus memiliki semua yang dibutuhkannya. Satu-satunya hal yang mungkin Anda ingin (ini bukan keharusan) untuk ditambahkan adalah beberapa CSS khusus untuk menyempurnakan komponen Alien di lingkungan Host. Anda bahkan mungkin menata komponen Alien Anda sama sekali berbeda saat digunakan di Host. Ini melampaui poin utama artikel, tetapi Anda melihat kode akhir untuk pembungkus, di mana Anda dapat menemukan contoh bagaimana mengganti gaya sederhana pada tingkat pembungkus.

  • Pembungkus Frankenstein untuk komponen React
  • Pembungkus Frankenstein untuk komponen Vue

Anda juga dapat melihat konfigurasi Webpack pada langkah migrasi ini:

  • Migrasi untuk Bereaksi dengan komponen gaya
  • Migrasi untuk Bereaksi dengan Modul CSS
  • Migrasi ke Vue

Dan akhirnya, komponen kita terlihat persis seperti yang kita inginkan.

Hasil migrasi komponen Header yang ditulis dengan Vue dan React. Daftar item yang harus dilakukan masih aplikasi jQuery.
Hasil migrasi komponen Header yang ditulis dengan Vue dan React. Daftar item yang harus dilakukan masih aplikasi jQuery. (Pratinjau besar)

5.5. Ringkasan gaya pemasangan untuk komponen Alien

Ini adalah saat yang tepat untuk menyimpulkan apa yang telah kita pelajari dalam bab ini sejauh ini. Sepertinya kami harus melakukan pekerjaan besar untuk memperbaiki gaya komponen Alien; namun, semuanya bermuara pada:

  • Memperbaiki gaya yang dibundel yang diimplementasikan dengan komponen-gaya dalam modul React atau CSS dan Scoped CSS di Vue semudah beberapa baris dalam konfigurasi Frankenstein wrapper atau Webpack.
  • Memperbaiki gaya, diimplementasikan dengan Modul CSS, dimulai dengan hanya satu baris dalam konfigurasi css-loader . Setelah itu, Modul CSS diperlakukan sebagai stylesheet global.
  • Memperbaiki stylesheet global memerlukan konfigurasi paket style-loader dengan opsi insert di Webpack, dan memperbarui wrapper Frankenstein untuk menarik stylesheet ke dalam Shadow DOM pada saat yang tepat dari siklus hidup wrapper.

Bagaimanapun, kami telah mendapatkan komponen Alien yang ditata dengan benar yang dimigrasikan ke Host. Hanya ada satu hal yang mungkin atau mungkin tidak mengganggu Anda tergantung pada kerangka kerja tempat Anda bermigrasi.

Kabar baik pertama: Jika Anda bermigrasi ke Vue , demo seharusnya berfungsi dengan baik, dan Anda harus dapat menambahkan item tugas baru dari komponen Vue yang dimigrasikan. Namun, jika Anda bermigrasi ke React , dan mencoba menambahkan item tugas baru, Anda tidak akan berhasil. Menambahkan item baru tidak akan berhasil, dan tidak ada entri yang ditambahkan ke daftar. Tapi kenapa? Apa masalahnya? Tidak ada prasangka, tetapi React memiliki pendapatnya sendiri tentang beberapa hal.

5.6. Bereaksi Dan Acara JS Di Shadow DOM

Tidak peduli apa yang dikatakan oleh dokumentasi React kepada Anda, React tidak terlalu bersahabat dengan Komponen Web. Kesederhanaan contoh dalam dokumentasi tidak menerima kritik apa pun, dan sesuatu yang lebih rumit daripada merender tautan di Komponen Web memerlukan beberapa penelitian dan penyelidikan.

Seperti yang telah Anda lihat saat memperbaiki gaya untuk komponen Alien kami, bertentangan dengan Vue di mana segala sesuatunya sesuai dengan Komponen Web hampir di luar kotak, React belum siap untuk Komponen Web. Untuk saat ini, kami memiliki pemahaman tentang bagaimana membuat komponen React setidaknya terlihat bagus di dalam Komponen Web, tetapi ada juga fungsionalitas dan event JavaScript yang harus diperbaiki.

Singkat cerita: Shadow DOM merangkum peristiwa dan menargetkannya kembali, sementara React tidak mendukung perilaku Shadow DOM ini secara asli dan karenanya tidak menangkap peristiwa yang berasal dari dalam Shadow DOM. Ada alasan yang lebih dalam untuk perilaku ini, dan bahkan ada masalah terbuka di pelacak bug React jika Anda ingin menyelami lebih banyak detail dan diskusi.

Untungnya, orang pintar menyiapkan solusi untuk kita. @josephnvu memberikan dasar untuk solusi, dan Lukas Bombach mengubahnya menjadi modul npm react-shadow-dom-retarget-events . Jadi Anda dapat menginstal paket, ikuti instruksi pada halaman paket, perbarui kode pembungkus Anda dan komponen Alien Anda akan mulai bekerja secara ajaib:

 import retargetEvents from 'react-shadow-dom-retarget-events'; ... ReactDOM.render( ... ); retargetEvents(this.shadowRoot);

Jika Anda ingin membuatnya lebih berperforma, Anda dapat membuat salinan paket lokal (lisensi MIT mengizinkannya) dan membatasi jumlah acara untuk didengarkan seperti yang dilakukan di repositori Demo Frankenstein. Untuk contoh ini, saya tahu acara apa yang perlu saya targetkan ulang dan hanya menentukannya.

Dengan ini, kami akhirnya (saya tahu itu adalah proses yang panjang) dilakukan dengan migrasi yang tepat dari komponen Alien yang pertama dan berfungsi penuh. Dapatkan sendiri minuman yang baik. Anda layak mendapatkannya!

6. Bilas & Ulangi Untuk Semua Komponen Anda

Setelah kita memigrasikan komponen pertama, kita harus mengulangi proses untuk semua komponen kita. Dalam kasus Demo Frankenstein, hanya ada satu yang tersisa: satu, yang bertanggung jawab untuk membuat daftar item yang harus dilakukan.

Pembungkus Baru Untuk Komponen Baru

Mari kita mulai dengan menambahkan pembungkus baru. Mengikuti konvensi penamaan, yang dibahas di atas (karena komponen React kami disebut MainSection.js ), pembungkus yang sesuai dalam migrasi ke React harus disebut MainSection-wrapper.js . Pada saat yang sama, komponen serupa di Vue disebut Listing.vue , maka pembungkus yang sesuai dalam migrasi ke Vue harus disebut Listing-wrapper.js . Namun, apa pun konvensi penamaannya, pembungkus itu sendiri akan hampir identik dengan yang sudah kita miliki:

  • Pembungkus untuk daftar Bereaksi
  • Pembungkus untuk daftar Vue

Hanya ada satu hal menarik yang kami perkenalkan pada komponen kedua ini di aplikasi React. Terkadang, untuk alasan itu atau lainnya, Anda mungkin ingin menggunakan beberapa plugin jQuery di komponen Anda. Dalam hal komponen React kami, kami memperkenalkan dua hal:

  • Plugin tooltip dari Bootstrap yang menggunakan jQuery,
  • Pengalih untuk kelas CSS seperti .addClass() dan .removeClass() .

    Catatan : Penggunaan jQuery untuk menambah/menghapus kelas ini murni ilustrasi. Tolong jangan gunakan jQuery untuk skenario ini dalam proyek nyata — sebagai gantinya mengandalkan JavaScript biasa.

Tentu saja, mungkin terlihat aneh untuk memperkenalkan jQuery dalam komponen Alien ketika kami bermigrasi dari jQuery, tetapi Host Anda mungkin berbeda dari Host dalam contoh ini — Anda mungkin bermigrasi dari AngularJS atau apa pun. Juga, fungsi jQuery dalam komponen dan jQuery global tidak selalu sama.

Namun, masalahnya adalah bahwa meskipun Anda mengonfirmasi bahwa komponen berfungsi dengan baik dalam konteks aplikasi Alien Anda, saat Anda memasukkannya ke dalam Shadow DOM, plugin jQuery dan kode lain yang mengandalkan jQuery tidak akan berfungsi.

jQuery Dalam Bayangan DOM

Mari kita lihat inisialisasi umum dari plugin jQuery acak:

 $('.my-selector').fancyPlugin();

Dengan cara ini, semua elemen dengan .my-selector akan diproses oleh fancyPlugin . Bentuk inisialisasi ini mengasumsikan bahwa .my-selector hadir di DOM global. Namun, begitu elemen tersebut dimasukkan ke dalam Shadow DOM, seperti halnya dengan gaya, batas bayangan mencegah jQuery menyelinap ke dalamnya. Akibatnya, jQuery tidak dapat menemukan elemen di dalam Shadow DOM.

Solusinya adalah memberikan parameter kedua opsional ke pemilih yang mendefinisikan elemen root untuk pencarian jQuery. Dan di sinilah kami dapat menyediakan shadowRoot kami.

 $('.my-selector', this.shadowRoot).fancyPlugin();

Dengan cara ini, pemilih jQuery dan, sebagai hasilnya, plugin akan berfungsi dengan baik.

Perlu diingat bahwa komponen Alien dimaksudkan untuk digunakan baik: di Alien tanpa shadow DOM, dan di Host dalam Shadow DOM. Oleh karena itu kami membutuhkan solusi yang lebih terpadu yang tidak akan mengasumsikan keberadaan Shadow DOM secara default.

Menganalisis komponen MainSection dalam aplikasi React kami, kami menemukan bahwa ia menetapkan properti documentRoot .

 ... this.documentRoot = this.props.root? this.props.root: document; ...

Jadi, kami memeriksa properti root yang diteruskan, dan jika ada, inilah yang kami gunakan sebagai documentRoot . Jika tidak, kita kembali ke document .

Berikut inisialisasi plugin tooltip yang menggunakan properti ini:

 $('[data-toggle="tooltip"]', this.documentRoot).tooltip({ container: this.props.root || 'body' });

Sebagai bonus, kami menggunakan properti root yang sama untuk mendefinisikan wadah untuk menginjeksi tooltip dalam kasus ini.

Sekarang, ketika komponen Alien siap menerima properti root , kami memperbarui rendering komponen dalam pembungkus Frankenstein yang sesuai:

 // `appWrapper` is the root element within wrapper's Shadow DOM. ReactDOM.render(<MainApp root={ appWrapper } />, appWrapper);

Dan itu saja! Komponen berfungsi dengan baik di Shadow DOM seperti halnya di DOM global.

Konfigurasi Webpack untuk skenario multi-pembungkus

Bagian yang menarik terjadi dalam konfigurasi Webpack saat menggunakan beberapa pembungkus. Tidak ada perubahan untuk gaya yang dibundel seperti Modul CSS di komponen Vue, atau komponen gaya di React. Namun, gaya global harus mendapatkan sedikit sentuhan sekarang.

Ingat, kami mengatakan bahwa style-loader (bertanggung jawab untuk menyuntikkan lembar gaya global ke dalam Shadow DOM yang benar) tidak fleksibel karena hanya membutuhkan satu pemilih pada satu waktu untuk opsi insert . Ini berarti bahwa kita harus membagi aturan .css di Webpack untuk memiliki satu sub-aturan per pembungkus menggunakan aturan oneOf atau yang serupa, jika Anda menggunakan bundler selain Webpack.

Itu selalu lebih mudah untuk dijelaskan dengan menggunakan sebuah contoh, jadi mari kita bicara tentang yang dari migrasi ke Vue kali ini (yang dalam migrasi ke React, hampir identik):

 ... oneOf: [ { issuer: /Header/, use: [ { loader: 'style-loader', options: { insert: 'frankenstein-header-wrapper' } }, ... ] }, { issuer: /Listing/, use: [ { loader: 'style-loader', options: { insert: 'frankenstein-listing-wrapper' } }, ... ] }, ] ...

Saya telah mengecualikan css-loader karena konfigurasinya sama di semua kasus. Mari kita bicara tentang style-loader sebagai gantinya. Dalam konfigurasi ini, kami memasukkan <style> ke dalam *-header-* atau *-listing-* , tergantung pada nama file yang meminta stylesheet tersebut ( aturan issuer di Webpack). Tetapi kita harus ingat bahwa stylesheet global yang diperlukan untuk merender komponen Alien mungkin diimpor di dua tempat:

  • Komponen Alien itu sendiri,
  • Bungkus Frankenstein.

Dan di sini, kita harus menghargai konvensi penamaan untuk pembungkus, yang dijelaskan di atas, ketika nama komponen Alien dan pembungkus yang sesuai cocok. Jika, misalnya, kita memiliki stylesheet, yang diimpor dalam komponen Vue bernama Header.vue , itu akan memperbaiki *-header-* wrapper. Pada saat yang sama, jika kita, sebagai gantinya, mengimpor stylesheet di pembungkus, stylesheet tersebut mengikuti aturan yang sama persis jika pembungkusnya disebut Header-wrapper.js tanpa perubahan apa pun dalam konfigurasi. Hal yang sama untuk komponen Listing.vue dan pembungkusnya yang sesuai Listing-wrapper.js . Menggunakan konvensi penamaan ini, kami mengurangi konfigurasi di bundler kami.

Setelah semua komponen Anda bermigrasi, saatnya untuk langkah terakhir dari migrasi.

7. Beralih Ke Alien

Pada titik tertentu, Anda menemukan bahwa komponen yang Anda identifikasi pada langkah pertama migrasi, semuanya diganti dengan pembungkus Frankenstein. Tidak ada aplikasi jQuery yang tersisa dan apa yang Anda miliki, pada dasarnya, adalah aplikasi Alien yang direkatkan menggunakan sarana Host.

Misalnya, bagian konten index.html di aplikasi jQuery — setelah migrasi kedua layanan mikro — terlihat seperti ini sekarang:

 <section class="todoapp"> <frankenstein-header-wrapper></frankenstein-header-wrapper> <frankenstein-listing-wrapper></frankenstein-listing-wrapper> </section>

Saat ini, tidak ada gunanya menyimpan aplikasi jQuery kita: sebagai gantinya, kita harus beralih ke aplikasi Vue dan melupakan semua pembungkus, Shadow DOM, dan konfigurasi Webpack yang mewah. Untuk melakukan ini, kami memiliki solusi yang elegan.

Mari kita bicara tentang permintaan HTTP. Saya akan menyebutkan konfigurasi Apache di sini, tetapi ini hanya detail implementasi: melakukan sakelar di Nginx atau apa pun harus sepele seperti di Apache.

Bayangkan situs Anda dilayani dari folder /var/www/html di server Anda. Dalam hal ini, httpd.conf atau httpd-vhost.conf Anda harus memiliki entri yang mengarah ke folder itu seperti:

 DocumentRoot "/var/www/html"

Untuk mengganti aplikasi Anda setelah migrasi Frankenstein dari jQuery ke React, yang perlu Anda lakukan hanyalah memperbarui entri DocumentRoot ke sesuatu seperti:

 DocumentRoot "/var/www/html/react/build"

Bangun aplikasi Alien Anda, restart server Anda, dan aplikasi Anda dilayani langsung dari folder Alien: aplikasi React dilayani dari folder react/ . Namun, hal yang sama berlaku untuk Vue, tentu saja, atau kerangka kerja lain yang telah Anda migrasikan juga. Inilah mengapa sangat penting untuk menjaga Host dan Alien sepenuhnya independen dan berfungsi kapan saja karena Alien Anda menjadi Host Anda pada langkah ini.

Sekarang Anda dapat dengan aman menghapus semua yang ada di sekitar folder Alien Anda, termasuk semua Shadow DOM, pembungkus Frankenstein, dan artefak terkait migrasi lainnya. Itu adalah jalan yang sulit pada saat-saat tertentu, tetapi Anda telah memigrasikan situs Anda. Selamat!

Kesimpulan

Kami pasti melewati medan yang agak kasar dalam artikel ini. Namun, setelah kami memulai dengan aplikasi jQuery, kami telah berhasil memigrasikannya ke Vue dan React. Kami telah menemukan beberapa masalah tak terduga dan tidak terlalu sepele di sepanjang jalan: kami harus memperbaiki gaya, kami harus memperbaiki fungsionalitas JavaScript, memperkenalkan beberapa konfigurasi bundler, dan banyak lagi. Namun, itu memberi kami gambaran yang lebih baik tentang apa yang diharapkan dalam proyek nyata. Pada akhirnya, kami mendapatkan aplikasi kontemporer tanpa sisa bit dari aplikasi jQuery meskipun kami memiliki semua hak untuk skeptis tentang hasil akhir saat migrasi sedang berlangsung.

Setelah beralih ke Alien, Frankenstein bisa pensiun.
Setelah beralih ke Alien, Frankenstein bisa pensiun. (Pratinjau besar)

Migrasi Frankenstein bukanlah peluru perak atau proses yang menakutkan. Ini hanya algoritme yang ditentukan, yang berlaku untuk banyak proyek, yang membantu mengubah proyek menjadi sesuatu yang baru dan kuat dengan cara yang dapat diprediksi.