Extensible Extensions dalam JavaScript

Halo, Habr!

Kami menarik perhatian Anda pada salinan tambahan yang telah lama ditunggu-tunggu dari buku " JavaScript Ekspresif ", yang baru saja datang dari percetakan.


Bagi mereka yang tidak terbiasa dengan karya penulis buku (untuk semua sifat ensiklopedis, pengembang pemula juga akan menyukainya) - kami sarankan Anda membiasakan diri dengan artikel dari blognya; Artikel ini menguraikan pemikiran tentang mengatur ekstensi dalam JavaScript.

Saat ini, telah menjadi mode untuk menyusun sistem besar dalam bentuk banyak paket terpisah. Gagasan mengemudi yang mendasari pendekatan ini adalah bahwa lebih baik tidak membatasi orang ke fitur tertentu (diusulkan oleh Anda) dengan menerapkan fitur, tetapi untuk menyediakan fitur ini sebagai paket terpisah yang dapat diunduh seseorang bersama dengan paket sistem dasar.
Untuk melakukan ini, secara umum, Anda perlu ...

  • Kemampuan untuk tidak mengunduh fitur yang tidak Anda perlukan sangat berguna pada sistem klien.
  • Kemampuan untuk mengganti dengan implementasi lain sepotong fungsi yang tidak memenuhi tujuan Anda. Dengan demikian, beban pada modul kernel juga berkurang - tidak perlu mencakup semua kasus praktis yang mungkin dengan bantuan mereka.
  • Memeriksa antarmuka kernel dalam kondisi nyata - dengan mengimplementasikan fitur dasar di atas antarmuka yang menghadap sisi klien, Anda dipaksa untuk membuat antarmuka ini setidaknya sangat kuat sehingga dapat mendukung fitur-fitur ini. Dengan demikian, Anda dapat yakin bahwa akan ada kemungkinan untuk membangun di atasnya hal serupa yang ditulis oleh pengembang pihak ketiga.
  • Isolasi antar bagian sistem. Peserta proyek dapat dengan mudah mencari paket yang mereka minati. Paket dapat diversi, ditandai sebagai tidak diinginkan, atau diganti tanpa mempengaruhi kode inti.

Pendekatan ini biasanya dikaitkan dengan peningkatan kompleksitas. Agar lebih mudah bagi pengguna untuk memulai, Anda dapat menyediakan mereka dengan paket pembungkus di mana "semuanya termasuk", tetapi cepat atau lambat mereka mungkin harus menyingkirkan shell ini dan menginstal dan mengkonfigurasi paket tambahan tertentu, yang kadang-kadang lebih sulit daripada hanya beralih ke fitur lain di perpustakaan monolitik.

Dalam artikel ini, kami akan mencoba membahas opsi untuk mekanisme ekspansi yang mendukung "ekstensibilitas skala besar" dan memungkinkan Anda untuk membuat titik ekstensi baru di mana ini tidak disediakan.

Ekstensibilitas


Apa yang kita inginkan dari sistem yang dapat diperluas? Pertama-tama, tentu saja, ia harus memiliki kemampuan untuk memperluas kemampuannya sendiri menggunakan kode eksternal.

Tetapi ini hampir tidak cukup. Biarkan saya ngelantur tentang satu masalah bodoh yang pernah saya temui. Saya sedang mengembangkan editor kode. Di salah satu versi editor ini sebelumnya, Anda bisa mengatur gaya untuk baris tertentu dalam kode klien. Itu hebat - tata letak garis selektif.

Kecuali untuk kasus ketika upaya untuk mengubah tampilan garis dilakukan segera dari dua bagian kode - dan upaya ini mulai berakhir. Ekstensi kedua yang diterapkan pada baris menimpa gaya ekstensi pertama. Atau, ketika kode pertama mencoba untuk menghapus desain yang dibuat di suatu tempat di bagian selanjutnya dari kode, itu menimpa gaya yang diperkenalkan oleh fragmen kode kedua.

Kami berhasil menemukan solusi, memberikan kemampuan untuk menambah (dan menghapus) ekstensi, daripada mendefinisikannya, sehingga dua ekstensi dapat berinteraksi dengan baris yang sama tanpa menyabot pekerjaan masing-masing.

Dalam arti yang lebih umum, perlu untuk memastikan bahwa ekstensi dapat digabungkan, bahkan jika mereka sama sekali tidak menyadari keberadaan satu sama lain - tanpa menyebabkan konflik di antara mereka.

Untuk melakukan ini, Anda perlu memastikan bahwa sejumlah aktor dapat memengaruhi setiap titik ekspansi. Bagaimana tepatnya beberapa efek akan diproses tergantung pada situasinya. Berikut ini beberapa pendekatan yang mungkin berguna bagi Anda:

  • Semuanya mulai berlaku. Misalnya, saat menambahkan kelas CSS ke elemen atau saat menampilkan widget, kedua fitur ini ditambahkan pada waktu yang bersamaan. Seringkali, mereka masih harus disortir dalam beberapa cara: widget harus ditampilkan dalam urutan yang dapat diprediksi dan didefinisikan dengan baik.
  • Mereka berbaris dalam bentuk konveyor. Contohnya adalah pawang yang bisa memfilter perubahan yang ditambahkan ke dokumen sebelum dibuat. Setiap perubahan pertama kali dimasukkan ke pawang, yang, pada gilirannya, juga dapat mengubahnya. Memesan dalam hal ini tidak kritis, tetapi dapat membuat perbedaan.
  • Anda dapat menerapkan pendekatan first-come-first-serve untuk penangan acara. Setiap pawang memiliki kesempatan untuk melayani acara tersebut sampai salah satu dari mereka mengatakan bahwa mereka sudah menanganinya, setelah itu pawang yang berdiri dalam antrean untuk itu tidak lagi diinterogasi.
  • Ini juga terjadi bahwa Anda benar-benar perlu memilih nilai tertentu - misalnya, menentukan nilai parameter konfigurasi tertentu. Mungkin disarankan untuk menggunakan operator tertentu (katakanlah, logis dan, logis atau, minimum atau maksimum) untuk membatasi jumlah nilai input untuk satu posisi. Misalnya, editor dapat beralih ke mode hanya baca jika ada ekstensi yang memesannya. Anda dapat mengatur nilai maksimum dokumen, atau jumlah nilai minimum yang dilaporkan ke opsi ini.

Dalam banyak kasus seperti itu, ketertiban itu penting. Ini berarti bahwa prioritas efek yang diterapkan harus dapat dikontrol dan diprediksi.

Di front inilah sistem ekstensi imperatif yang didasarkan pada penggunaan efek samping biasanya tidak mampu mengatasinya. Misalnya, operasi addEventListener dilakukan oleh model DOM peramban menyebabkan penangan peristiwa dipanggil persis sesuai urutan pendaftarannya. Ini normal jika semua panggilan dikontrol oleh satu sistem, atau jika urutan operasi benar-benar tidak penting, namun, ketika Anda harus berurusan dengan banyak fragmen perangkat lunak yang secara independen menambahkan penangan, bisa sangat sulit untuk memprediksi yang mana dari mereka yang akan dipanggil.

Pendekatan sederhana


Untuk memberi Anda contoh sederhana: Saya pertama kali menerapkan strategi modular untuk ProseMirror, sebuah sistem untuk mengedit teks kaya. Inti dari sistem ini sendiri, pada prinsipnya, tidak berguna - ini sepenuhnya bergantung pada paket tambahan yang menggambarkan struktur dokumen, kunci yang mengikat, memimpin sejarah pembatalan. Meskipun ini agak sulit untuk menggunakan sistem ini, ia telah diadopsi dalam produk yang memerlukan desain teks khusus, yang tidak tersedia di editor klasik.

Mekanisme ekstensi yang digunakan dalam ProseMirror relatif mudah. Saat membuat editor, kode klien menunjukkan satu array objek yang terhubung. Masing-masing plugin ini entah bagaimana dapat memengaruhi pekerjaan editor, misalnya, menambahkan potongan data status atau menangani acara antarmuka.

Semua aspek ini dirancang untuk bekerja dengan berbagai nilai konfigurasi menggunakan strategi yang diuraikan di bagian sebelumnya. Misalnya, ketika menentukan beberapa penugasan kunci, urutan instans plugin keymap ditentukan menentukan prioritasnya. Keymap pertama yang tahu cara menanganinya menerima kunci spesifik dalam pemrosesan.

Biasanya mekanisme ini cukup kuat, dan aktif digunakan. Namun, pada tahap tertentu, menjadi rumit dan menjadi tidak nyaman untuk bekerja dengannya.

  • Jika plugin memiliki banyak efek, maka Anda dapat berharap bahwa dalam urutan itu mereka akan diterapkan ke plugin lain, atau Anda harus memecahnya menjadi plugin yang lebih kecil sehingga Anda dapat mengaturnya dengan benar.
  • Secara umum, pengorganisasian plugin menjadi sangat sensitif, karena pengguna akhir tidak selalu mengerti plugin mana yang dapat memengaruhi operasi plugin lain jika mereka mendapatkan prioritas yang lebih tinggi. Semua kesalahan biasanya muncul hanya pada saat runtime, ketika menggunakan fungsionalitas tertentu - karena itu, mereka mudah untuk dilewatkan.
  • Plugin berdasarkan pada plugin lain harus mendokumentasikan fakta ini - dan masih diharapkan bahwa pengguna tidak akan lupa untuk mengaktifkan dependensi mereka (dalam urutan yang benar).

CodeMirror dalam versi 6 adalah editor yang ditulis ulang dengan nama yang sama . Dalam versi keenam, saya mencoba mengembangkan pendekatan modular. Ini membutuhkan sistem ekstensi yang lebih ekspresif. Mari kita lihat beberapa tantangan yang terkait dengan merancang sistem seperti itu.

Memesan


Sangat mudah untuk merancang sistem yang menyediakan kontrol penuh atas urutan ekstensi. Tetapi sangat sulit untuk merancang sistem seperti itu, yang pada saat yang sama akan menyenangkan untuk digunakan dan memungkinkan Anda untuk menggabungkan kode ekstensi independen tanpa intervensi manual yang luas dan menyeluruh.

Ketika datang ke pemesanan, itu menarik untuk menerapkan nilai-nilai prioritas. Contoh serupa adalah properti z-index CSS, yang memungkinkan Anda menetapkan angka yang menunjukkan seberapa dalam item akan berada di tumpukan.

Karena style sheet terkadang memiliki nilai z-index sangat besar, jelas bahwa cara menunjukkan prioritas ini bermasalah. Modul tertentu secara individual “tidak tahu” nilai prioritas mana yang mengindikasikan modul lain. Opsi hanyalah titik dalam rentang angka yang tidak ditentukan. Anda dapat menentukan nilai tinggi terlarang (atau nilai-nilai sangat negatif), berharap mencapai akhir skala ini, tetapi yang lainnya hanyalah permainan tebak-tebakan.

Situasi ini dapat sedikit ditingkatkan dengan mendefinisikan sekumpulan terbatas kategori prioritas yang terdefinisi dengan jelas sehingga ekstensi dapat diklasifikasikan berdasarkan “level” perkiraan prioritas mereka. Tetapi Anda masih harus entah bagaimana memutuskan ikatan dalam kategori-kategori ini.

Pengelompokan dan Deduplikasi


Seperti yang saya sebutkan di atas, setelah Anda mulai serius mengandalkan ekstensi, situasi dapat muncul di mana beberapa ekstensi akan menggunakan yang lain. Manajemen saling ketergantungan tidak skala dengan baik, jadi alangkah baiknya jika Anda bisa menarik sekelompok ekstensi sekaligus.

Namun, tidak hanya itu, dalam hal ini, masalah pemesanan akan semakin memburuk; masalah lain akan muncul. Banyak ekstensi lain dapat bergantung pada ekstensi tertentu sekaligus, dan jika Anda menyatakannya sebagai nilai, maka situasi dengan beberapa unduhan dari ekstensi yang sama mungkin muncul. Dalam beberapa kasus, misalnya, ketika menetapkan kunci atau menangani penangan acara, ini normal. Di negara lain, misalnya, ketika melacak membatalkan riwayat atau bekerja dengan perpustakaan tooltip, pendekatan seperti itu akan menjadi pemborosan sumber daya dengan risiko melanggar sesuatu.

Oleh karena itu, memungkinkan komposisi ekstensi, kami dipaksa untuk beralih ke bagian sistem ekstensi dari kompleksitas yang terkait dengan pengelolaan dependensi. Anda harus dapat mengenali ekstensi yang tidak boleh diduplikasi, dan hanya mengunduh satu contoh dari masing-masing ekstensi tersebut.

Namun, karena dalam kebanyakan kasus, ekstensi dapat dikonfigurasikan, dan semua contoh ekstensi tertentu akan agak berbeda satu sama lain, kami tidak dapat hanya mengambil satu contoh ekstensi dan menggunakannya - kami harus menggabungkannya dengan cara yang bermakna (atau melaporkan kesalahan, saat ini tidak memungkinkan).

Proyek


Di sini saya akan menjelaskan apa yang telah dilakukan di CodeMirror 6. Saya mengusulkan contoh ini sebagai solusi, dan bukan sebagai satu-satunya solusi yang benar. Ada kemungkinan bahwa sistem ini akan berkembang lebih jauh saat perpustakaan stabil.

Primitif utama dalam pendekatan ini disebut perilaku. Perilaku hanyalah hal-hal yang dapat Anda kembangkan dengan menunjukkan nilai. Sebagai contoh, perhatikan perilaku bidang negara, di mana dengan bantuan ekstensi Anda bisa menambahkan bidang baru, memberikan deskripsi setiap bidang. Atau perilaku pengendali acara berbasis browser, tempat Anda dapat menambahkan penangan Anda sendiri menggunakan ekstensi.

Dari sudut pandang perilaku konsumen, perilaku-perilaku yang dikonfigurasikan dalam contoh editor tertentu memberikan urutan nilai yang terurut, di mana nilai-nilai dengan prioritas lebih tinggi didahulukan. Setiap perilaku memiliki tipe, dan nilai untuk itu harus cocok dengan tipe itu.

Perilaku direpresentasikan sebagai nilai yang digunakan untuk mendeklarasikan instance dari perilaku dan untuk merujuk pada nilai-nilai yang mungkin dimiliki oleh perilaku tersebut. Misalnya, ekstensi yang menentukan latar belakang nomor baris dapat menentukan perilaku yang memungkinkan kode lain untuk menambahkan penanda baru ke latar belakang ini.

Ekstensi adalah nilai yang dapat digunakan saat mengonfigurasi editor. Array ekstensi dilaporkan selama inisialisasi. Setiap ekstensi diizinkan dalam perilaku nol atau lebih.

Jenis ekstensi paling sederhana adalah contoh perilaku. Dengan menetapkan nilai untuk perilaku ini, kami mendapatkan respons nilai ekstensi yang mengimplementasikan perilaku ini.
Urutan ekstensi juga dapat dikelompokkan menjadi satu ekstensi. Misalnya, dalam konfigurasi editor untuk bahasa pemrograman yang diberikan, sejumlah ekstensi lain dapat ditarik, khususnya, tata bahasa untuk parsing dan menyoroti bahasa, informasi tentang cara indentasi, dan juga sumber informasi tentang penyelesaian yang secara cerdas melengkapi kode dalam bahasa itu. Jadi Anda mendapatkan ekstensi bahasa, yang hanya mengumpulkan semua ekstensi yang diperlukan yang memberikan nilai kumulatif.

Menggambarkan versi sederhana dari sistem seperti itu, kita bisa berhenti di sini dan cukup memasukkan ekstensi bersarang ke dalam satu array ekstensi untuk perilaku. Kemudian mereka dapat dikelompokkan berdasarkan jenis perilaku dan mendapatkan urutan nilai-nilai perilaku.

Namun, kami masih belum menemukan deduplikasi, dan kami perlu kontrol lebih lengkap atas pemesanan.

Nilai dari tipe ketiga termasuk ekstensi unik , ini adalah mekanisme untuk memastikan deduplikasi. Ekstensi yang tidak ingin Anda instantiate dua kali di editor yang sama hanyalah itu. Untuk mendefinisikan ekstensi seperti itu, tipe spesifikasi ditentukan, yaitu, tipe nilai konfigurasi yang diharapkan oleh konstruktor ekstensi, dan fungsi instantiasi yang mengambil larik nilai spek tersebut dan mengembalikan ekstensi.
Ekstensi unik mempersulit proses penyelesaian koleksi ekstensi menjadi serangkaian perilaku. Selama ada ekstensi unik dalam rangkaian ekstensi yang diselaraskan, mekanisme resolusi harus memilih jenis ekstensi unik, mengumpulkan semua instansinya, memanggil fungsi instantiasi dengan nilai speknya dan menggantinya dengan hasilnya (dalam satu contoh).

(Ada satu halangan lagi - mereka harus diselesaikan dalam urutan tertentu. Jika Anda pertama-tama mengaktifkan ekstensi unik X, tetapi kemudian ekstensi Y memutuskan ke X lain, ini akan menjadi kesalahan, karena semua instance X harus digabungkan bersama. Karena instantiating ekstensi adalah operasi murni, sistem, dihadapkan dengan itu, menjalankannya dengan coba-coba, memulai kembali proses - dan merekam informasi yang diklarifikasi.)
Akhirnya, mari kita bicara tentang prioritas. Pendekatan dasar dalam hal ini adalah untuk menjaga urutan ekstensi dilaporkan. Ekstensi majemuk disejajarkan dan dimasukkan ke dalam pesanan ini tepat pada posisi di mana mereka pertama kali bertemu. Hasil penyelesaian ekstensi unik juga disisipkan di tempat pertama kali terjadi.

Tetapi ekstensi dapat menetapkan beberapa subekstensi mereka ke kategori dengan prioritas berbeda. Sistem menentukan jenis kategori tersebut: rollback (berlaku setelah hal-hal lain terjadi), secara default, memperluas (prioritas lebih tinggi daripada bulk) dan mendefinisikan ulang (mungkin harus ditempatkan di bagian paling atas). Pemesanan aktual dilakukan pertama berdasarkan kategori, dan kemudian dengan posisi awal.

Jadi, ekstensi dengan penugasan kunci prioritas rendah dan penangan acara dengan prioritas biasa dapat memberi kami ekstensi majemuk yang dibangun atas dasar ekstensi dengan penugasan utama (dalam hal ini, Anda tidak perlu tahu perilaku apa yang termasuk di dalamnya, dengan prioritas "kembalikan" ditambah contoh perilaku pengendali acara.

Tampaknya, pencapaian utama adalah bahwa kami telah memperoleh kemampuan untuk menggabungkan ekstensi, terlepas dari apa yang dilakukan di dalamnya. Dalam ekstensi yang telah kami modelkan sejauh ini, di antaranya: dua sistem parsing dengan perilaku sintaksis yang sama, penyorotan sintaksis, layanan indentasi pintar, riwayat pembatalan, latar belakang nomor baris, penutupan otomatis tanda kurung, penetapan kunci dan pemilihan ganda - semuanya bekerja dengan baik.

Untuk menggunakan sistem seperti itu, Anda benar-benar harus menguasai beberapa konsep baru, dan itu jelas lebih rumit daripada sistem imperatif tradisional yang diterima dalam komunitas JavaScript (panggil metode untuk menambah / menghapus efek). Namun, kemampuan untuk menautkan ekstensi dengan benar tampaknya membenarkan biaya ini.

Source: https://habr.com/ru/post/id484470/


All Articles