Mekanisme Ekstensibilitas yang Dapat Diperpanjang dalam JavaScript

Halo rekan!

Kami mengingatkan Anda bahwa belum lama ini kami menerbitkan edisi ke-3 dari buku legendaris "Expressive JavaScript " (Eloquent JavaScript) - itu dicetak dalam bahasa Rusia untuk pertama kalinya, meskipun terjemahan berkualitas tinggi dari edisi sebelumnya ditemukan di Internet.



Namun, baik JavaScript maupun karya penelitian Mr. Haverbeke, tentu saja, tidak tinggal diam. Melanjutkan tema JavaScript ekspresif, kami menawarkan terjemahan artikel tentang desain ekstensi (menggunakan pengembangan editor teks sebagai contoh), yang diterbitkan di blog penulis pada akhir Agustus 2019


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 bahkan tidak memuat fitur yang tidak Anda butuhkan, yang sangat berguna saat bekerja dengan sistem sisi klien.
  • Kemampuan untuk mengganti dengan implementasi lain fungsi yang tidak memenuhi kebutuhan Anda. Dengan cara ini, tekanan juga berkurang pada modul-modul nuklir, yang sebaliknya harus mencakup semua jenis kasus praktis.
  • Memeriksa antarmuka kernel dalam kondisi nyata - dengan mengimplementasikan fitur-fitur dasar di atas antarmuka yang menghadap klien; Anda harus membuat antarmuka cukup kuat untuk setidaknya mengatasi dukungan fitur-fitur ini - dan memastikan bahwa elemen yang secara fungsional serupa dapat dirakit dari kode pihak ketiga.
  • Isolasi antar komponen sistem. Peserta proyek perlu mencari paket khusus yang menarik minat mereka. Paket dapat diversi, tidak digunakan lagi atau diganti oleh yang lain, dan semua ini tidak akan mempengaruhi kernel.


Pendekatan ini melibatkan biaya-biaya tertentu, yang bermuara pada kompleksitas tambahan. Agar pengguna dapat memulai, Anda dapat menyediakan mereka dengan paket bungkus, yang semuanya termasuk, tetapi pada titik tertentu mereka mungkin harus menghapus bungkus ini dan menangani instalasi dan konfigurasi paket tambahan sendiri, dan ini ternyata lebih sulit daripada menyertakan Fitur baru yang disampaikan di perpustakaan monolitik.
Dalam artikel ini, saya akan mencoba mengeksplorasi berbagai cara untuk merancang mekanisme perluasan yang melibatkan "ekstensibilitas dalam skala besar" dan segera meletakkan poin baru untuk ekspansi di masa depan.

Ekstensibilitas

Apa yang kita butuhkan dari sistem yang dapat diperluas? Pertama, tentu saja, Anda perlu kemampuan untuk membangun perilaku baru di atas kode eksternal.

Namun, ini hampir tidak cukup. Biarkan saya ngelantur, ceritakan satu masalah bodoh yang pernah saya temui. Saya sedang mengembangkan editor teks. Dalam satu versi awal editor kode, klien dapat menentukan tampilan baris tertentu. Itu luar biasa - pengguna dapat memilih jalur ini atau itu secara selektif dengan cara ini.

Selain itu, jika Anda mencoba untuk memulai tata letak garis dari dua fragmen kode yang saling independen, maka mereka mulai menginjak tumit masing-masing. Ekstensi kedua, yang diterapkan pada baris tertentu, menimpa perubahan yang dilakukan melalui baris pertama. Atau, jika beberapa saat kemudian kami mencoba untuk menghapus kode pertama perubahan-perubahan dalam desain yang kami buat dengan bantuannya, maka sebagai hasilnya kami akan menimpa desain yang dibuat dari fragmen kode kedua.

Solusinya adalah memungkinkan kode untuk ditambahkan (dan dihapus ) daripada diinstal, sehingga dua ekstensi dapat berinteraksi dengan baris yang sama tanpa mengganggu pekerjaan masing-masing.

Dalam formulasi yang lebih umum, perlu untuk memastikan bahwa ekstensi dapat digabungkan, bahkan jika mereka “sama sekali tidak mengetahui” satu sama lain - dan sehingga tidak ada konflik yang timbul selama interaksinya.

Agar ini berfungsi, setiap ekstensi harus terpapar ke sejumlah agen sekaligus. Bagaimana masing-masing efek akan diproses berbeda tergantung pada kasus spesifik. Berikut ini beberapa strategi yang mungkin berguna bagi Anda:

  • Semua perubahan berlaku. Misalnya, jika kita menambahkan kelas CSS ke suatu elemen, atau menampilkan widget pada posisi tertentu dalam teks, dua hal ini dapat dilakukan segera. Tentu saja, beberapa jenis pemesanan akan diperlukan: widget harus ditampilkan dalam urutan yang dapat diprediksi dan didefinisikan dengan baik.
  • Perubahan berbaris di dalam pipa. Contoh dari pendekatan ini adalah penangan yang dapat memfilter perubahan yang dibuat ke dokumen sebelum berlaku. Setiap pawang diberi perubahan yang dibuat oleh pawang sebelumnya, dan pawang selanjutnya dapat melanjutkan modifikasi tersebut. Pemesanan di sini tidak penting, tetapi bisa signifikan.
  • Pendekatan pertama datang pertama dilayani. Pendekatan ini berlaku, misalnya, dengan penangan acara. Setiap pawang mendapat kesempatan untuk bermain-main dengan acara tersebut sampai salah satu pawang mengumumkan bahwa semuanya sudah dilakukan, dan pawang berikutnya pada gilirannya tidak akan terganggu.
  • Terkadang perlu untuk memilih hanya satu nilai, misalnya, untuk menentukan nilai parameter konfigurasi tertentu. Di sini akan tepat untuk menggunakan beberapa jenis operator (misalnya, logis atau, logis dan, minimum, maksimum) untuk mengurangi input potensial ke nilai tunggal. Misalnya, editor dapat beralih ke mode hanya-baca jika setidaknya satu ekstensi memerlukannya, atau panjang maksimum dokumen dapat menjadi minimum dari semua nilai yang disediakan untuk opsi ini.


Dalam banyak situasi seperti itu, ketertiban itu penting. Maksud saya kepatuhan dengan urutan penerapan efek, urutan ini harus dikontrol dan diprediksi.

Ini adalah salah satu situasi di mana sistem ekstensi imperatif biasanya tidak terlalu baik, operasi yang tergantung pada efek samping. Misalnya, operasi addEventListener dari model DOM addEventListener mensyaratkan bahwa penangan peristiwa dipanggil sesuai urutan pendaftarannya. Ini normal jika semua panggilan dikontrol oleh satu sistem, atau jika urutan panggilan tidak begitu penting. Namun, jika Anda memiliki banyak komponen perangkat lunak yang menambahkan penangan secara independen satu sama lain, mungkin akan sangat sulit untuk memprediksi mana yang akan dipanggil.

Pendekatan sederhana

Biarkan saya memberi Anda contoh konkret: Saya pertama kali menerapkan strategi modular seperti itu saat mengembangkan ProseMirror, sebuah sistem untuk mengedit teks kaya. Inti dari sistem ini sendiri pada dasarnya tidak berguna: ia bergantung pada paket tambahan untuk menggambarkan struktur dokumen, untuk mengikat kunci, untuk mempertahankan sejarah pembatalan. Meskipun ini agak sulit untuk menggunakan sistem ini, ia telah menemukan aplikasi dalam program di mana Anda perlu mengonfigurasi hal-hal yang tidak didukung di editor klasik.

Mekanisme ekstensi ProseMirror relatif mudah. Saat membuat editor, satu array objek yang terhubung ditentukan dalam kode klien. Setiap objek plug-in ini dapat memengaruhi berbagai aspek editor dan melakukan hal-hal seperti menambahkan bit data keadaan atau menangani acara antarmuka.
Semua aspek ini dirancang untuk bekerja dengan susunan nilai konfigurasi yang diatur menggunakan salah satu strategi yang dijelaskan di atas. Misalnya, ketika Anda perlu menentukan banyak kamus dengan nilai, prioritas mengikuti contoh ekstensi untuk pengikatan kunci tergantung pada urutan di mana Anda menentukan contoh tersebut. Ekstensi pertama untuk pengikatan kunci, mengetahui apa yang harus dilakukan dengan penekanan tombol ini, akan diproses.

Biasanya, mekanisme semacam itu ternyata sangat kuat, dan mereka dapat mengubahnya untuk keuntungan mereka. Tetapi cepat atau lambat, sistem ekstensi mencapai kompleksitas sedemikian rupa sehingga tidak nyaman untuk menggunakannya.

  • Jika plugin memiliki banyak efek, maka Anda hanya bisa berharap bahwa mereka semua membutuhkan prioritas yang relatif sama dengan plugin lain, atau Anda harus memecahnya menjadi plugin yang lebih kecil untuk mengatur pesanan mereka dengan benar.
  • Secara umum, pengorganisasian plugin menjadi sangat teliti, karena pengguna akhir tidak selalu jelas plugin mana yang dapat mengganggu plugin lain jika mereka mendapatkan prioritas yang lebih tinggi. Kesalahan yang dibuat dalam kasus seperti itu biasanya hanya terjadi pada saat runtime, ketika peluang tertentu digunakan, sehingga mudah untuk diabaikan.
  • Jika plugin dibangun berdasarkan plugin lain, maka fakta ini harus didokumentasikan dan berharap bahwa pengguna tidak lupa untuk memasukkan dependensi yang sesuai (pada langkah pemesanan, bila perlu).


CodeMirror versi 6 adalah versi editor kode yang ditulis ulang dengan nama yang sama. Dalam proyek ini, saya mencoba mengembangkan pendekatan modular. Untuk melakukan ini, saya memerlukan sistem ekstensi yang lebih ekspresif. Mari kita bahas beberapa tantangan yang harus kita hadapi ketika merancang sistem seperti itu.

Memesan

Sangat mudah untuk merancang sistem yang memberi Anda kontrol penuh atas pemesanan ekstensi. Namun, jauh lebih sulit untuk merancang suatu sistem yang dengannya akan menyenangkan untuk dikerjakan dan yang, pada saat yang sama, akan memungkinkan Anda untuk menggabungkan kode berbagai ekstensi tanpa banyak intervensi dari kategori "sekarang perhatikan tangan Anda".
Ketika datang ke pemesanan, itu terjadi, saya benar-benar ingin resor untuk bekerja dengan nilai-nilai prioritas. Sebagai contoh, properti z-index CSS menunjukkan jumlah posisi yang ditempati oleh elemen ini di kedalaman tumpukan.

Seperti yang Anda lihat pada contoh nilai z-index sangat besar, yang kadang-kadang ditemukan dalam style sheet, cara ini menunjukkan prioritas adalah bermasalah. Modul itu sendiri tidak tahu nilai prioritas apa yang dimiliki modul lain. Opsi hanyalah titik di tengah rentang numerik yang tidak berbeda. Anda dapat menetapkan nilai yang sangat besar (atau sangat negatif) untuk mencoba menjangkau ujung-ujung spektrum ini, tetapi sisa dari pekerjaan tersebut adalah meramal.

Situasi ini dapat sedikit ditingkatkan jika Anda menetapkan sekumpulan kategori prioritas terbatas yang terdefinisi dengan jelas, sehingga ekstensi dapat menandai "level" umum dari prioritas mereka. Selain itu, Anda perlu cara tertentu untuk memutus tautan dalam kategori.

Pengelompokan dan Deduplikasi

Seperti yang saya sebutkan di atas, segera setelah Anda mulai serius mengandalkan ekstensi, situasi mungkin muncul ketika beberapa ekstensi akan menggunakan yang lain saat bekerja. Jika Anda mengelola dependensi secara manual, maka pendekatan ini tidak skala dengan baik; oleh karena itu, alangkah baiknya jika Anda dapat menarik sekelompok ekstensi secara bersamaan.

Namun, pendekatan ini tidak hanya semakin memperburuk masalah prioritas, tetapi juga memperkenalkan masalah lain: banyak ekstensi lain dapat bergantung pada ekstensi tertentu, dan jika ekstensi disajikan sebagai nilai, mungkin ekstensi yang sama akan dimuat beberapa kali. . Untuk beberapa jenis ekstensi, seperti penangan acara, ini normal. Dalam kasus lain, seperti dengan riwayat pembatalan dan pustaka tooltip, pendekatan ini akan sia-sia dan bahkan dapat merusak segalanya.

Jadi, jika kami mengizinkan tata letak ekstensi, ini memperkenalkan beberapa kompleksitas tambahan ke sistem kami yang terkait dengan manajemen ketergantungan. Anda harus dapat mengenali ekstensi tersebut yang tidak boleh digandakan, dan mengunduhnya secara tepat satu per satu.

Namun, karena dalam sebagian besar kasus, ekstensi dapat dikonfigurasikan, dan oleh karena itu tidak semua instance dari ekstensi yang sama persis sama, kami tidak dapat hanya mengambil satu instance dan bekerja dengannya. Kami harus mempertimbangkan beberapa penggabungan yang berarti dari kejadian-kejadian seperti itu (atau melaporkan kesalahan jika penggabungan kepentingan dengan kami tidak memungkinkan).

Proyek

Di sini saya akan menjelaskan secara umum apa yang kami lakukan di CodeMirror 6. Ini hanya sketsa, bukan Solusi Gagal. Ada kemungkinan bahwa sistem ini akan berkembang lebih jauh ketika perpustakaan stabil.

Primitif utama yang digunakan dalam pendekatan ini disebut perilaku. Perilaku hanyalah fitur yang dapat Anda bangun dengan ekstensi, yang menentukan nilai untuknya. Contohnya adalah perilaku bidang negara di mana, dengan bantuan ekstensi, Anda bisa menambahkan bidang baru, memberikan deskripsi bidang tersebut. Contoh lain adalah perilaku penangan acara di browser; dalam hal ini, dengan bantuan ekstensi, kami dapat menambahkan penangan kami sendiri.

Dari sudut pandang konsumen perilaku, perilaku itu sendiri, dikonfigurasi dengan cara tertentu dalam contoh khusus editor, memberikan urutan nilai yang dipesan, dan nilai-nilai yang datang sebelumnya memiliki prioritas yang lebih tinggi. Setiap perilaku memiliki tipe, dan nilai yang disediakan untuk itu harus cocok dengan tipe itu.
Perilaku direpresentasikan sebagai nilai yang digunakan untuk mendeklarasikan instance perilaku dan mengakses nilai yang dimiliki perilaku tersebut. Ada sejumlah perilaku bawaan di perpustakaan, tetapi kode eksternal dapat menentukan perilaku itu sendiri. Misalnya, dalam ekstensi yang mendefinisikan interval antara nomor baris, perilaku dapat didefinisikan yang memungkinkan kode lain untuk menambahkan penanda tambahan dalam interval ini.

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

Perpanjangan sederhana semacam itu dapat dianggap sebagai contoh perilaku. Jika kami menetapkan nilai untuk perilaku, maka kode mengembalikan nilai ekstensi yang menghasilkan perilaku ini kepada kami.

Urutan ekstensi juga dapat dikelompokkan menjadi satu ekstensi. Misalnya, dalam konfigurasi editor untuk bekerja dengan bahasa pemrograman tertentu, Anda dapat menarik beberapa ekstensi lainnya - misalnya, tata bahasa untuk penguraian dan penyorotan teks, informasi tentang lekukan yang diperlukan, sumber pelengkapan otomatis yang akan dengan benar menampilkan permintaan untuk menyelesaikan baris dalam bahasa ini. Dengan demikian, kita dapat membuat ekstensi bahasa tunggal di mana kita mengumpulkan semua ekstensi yang sesuai ini dan mengelompokkannya bersama, menghasilkan nilai tunggal.

Membuat versi sederhana dari sistem seperti itu, kita bisa berhenti pada ini hanya dengan menyelaraskan semua ekstensi bersarang dalam satu array ekstensi perilaku. Kemudian mereka dapat dikelompokkan berdasarkan jenis perilaku dan kemudian membangun urutan nilai-nilai perilaku yang teratur.

Namun, tetap berurusan dengan deduplikasi dan memberikan kontrol yang lebih baik atas pemesanan.

Nilai ekstensi terkait dengan tipe ketiga, ekstensi unik, hanya membantu mencapai deduplikasi. Ekstensi yang seharusnya tidak dipakai dua kali dalam editor yang sama adalah jenis ini. Untuk menentukan ekstensi seperti itu, Anda perlu menentukan tipe spesifikasi , yaitu, tipe nilai konfigurasi yang diharapkan oleh konstruktor ekstensi, dan juga menentukan fungsi instantiasi yang mengambil larik dari nilai yang ditentukan tersebut dan mengembalikan ekstensi.

Ekstensi unik mempersulit proses penyelesaian serangkaian ekstensi menjadi serangkaian perilaku. Jika ada ekstensi unik dalam rangkaian ekstensi yang disejajarkan, maka mekanisme penyelesaian harus memilih jenis ekstensi unik, mengumpulkan semua instansinya dan memanggil fungsi instansiasi yang sesuai bersama dengan spesifikasi, dan kemudian menggantinya semua dengan hasilnya (dalam satu salinan).

(Ada tangkapan lain: mereka harus menyelesaikan dalam urutan yang benar. Jika Anda pertama kali mengaktifkan ekstensi unik X, tetapi kemudian mendapatkan X lain sebagai hasil dari resolusi, maka ini akan salah, karena semua instance X harus disatukan. Karena fungsi instantiation ekspansi bersih, sistem mengatasi situasi ini dengan coba-coba, memulai kembali proses dan merekam informasi tentang apa yang mungkin dipelajari, berada dalam situasi ini.)

Akhirnya, Anda harus menyelesaikan masalah dengan aturan untuk mengikuti. Pendekatan dasar tetap sama: menjaga urutan usulan ekstensi. Ekstensi gabungan meluruskan dalam urutan yang sama pada titik di mana mereka terjadi. Hasil penyelesaian ekstensi unik dimasukkan saat pertama kali dihidupkan.

Namun, ekstensi dapat menghubungkan beberapa sub ekstensi dengan kategori yang memiliki prioritas berbeda. Sistem menyediakan empat kategori seperti itu: fallback (berlaku setelah hal-hal lain terjadi), default (default), extended (prioritas lebih tinggi daripada bulk) dan override (mungkin harus diutamakan). Dalam prakteknya, penyortiran dilakukan pertama berdasarkan kategori, dan kemudian dengan memulai posisi.

Jadi, ekstensi penjilidan kunci, yang memiliki prioritas rendah, dan penangan acara dengan prioritas reguler, didasarkan pada mereka bahwa itu modis untuk mendapatkan ekstensi gabungan yang dibangun dari hasil ekstensi penjilidan kunci (tidak perlu mengetahui perilaku apa itu terdiri) dengan tingkat prioritas mundur dan dari sebuah instance dengan perilaku event handler.

Pendekatan ini, yang memungkinkan Anda untuk menggabungkan ekstensi tanpa memikirkan apa yang mereka lakukan "di dalam," tampaknya merupakan pencapaian yang luar biasa. Ekstensi yang kami modelkan sebelumnya dalam artikel ini meliputi: dua sistem parsing yang menunjukkan perilaku yang sama di tingkat sintaksis, layanan penyorotan sintaks, layanan indentasi pintar, riwayat pembatalan, layanan penspasian garis, kurung tutup otomatis, penjilidan kunci dan banyak pilihan - semua berfungsi dengan baik.

Ada beberapa konsep baru yang harus dipelajari pengguna untuk menggunakan sistem ini. Selain itu, bekerja dengan sistem seperti itu memang sedikit lebih rumit daripada dengan sistem imperatif tradisional yang diadopsi dalam komunitas JavaScript (kami memanggil metode untuk menambah / menghapus efek). Namun, jika ekstensi diatur dengan benar, maka manfaatnya lebih besar daripada biaya yang terkait.

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


All Articles