10 baris kode untuk mengurangi rasa sakit proyek Vue Anda

... atau keakraban dengan plugin Vue JS sebagai contoh dari bus acara terintegrasi


Beberapa kata tentang ...


Halo semuanya! Saya akan segera melakukan reservasi. Saya benar-benar menyukai VueJS, saya telah aktif menulis tentang hal ini selama lebih dari 2 tahun dan saya tidak berpikir bahwa pengembangannya dapat menyakitkan setidaknya untuk beberapa tingkat yang signifikan :)
Di sisi lain, kami selalu berusaha menemukan solusi universal yang akan membantu menghabiskan lebih sedikit waktu untuk pekerjaan mekanik dan lebih pada apa yang benar-benar menarik. Terkadang solusinya sangat berhasil. Salah satunya saya ingin berbagi dengan Anda. 10 baris yang akan dibahas (spoiler: pada akhirnya akan ada lebih banyak) lahir dalam proses mengerjakan proyek Cloud Blue-Connect, yang merupakan aplikasi yang cukup besar dengan 400+ komponen. Solusi yang kami temukan sudah terintegrasi ke dalam berbagai titik sistem dan selama lebih dari setengah tahun tidak pernah membutuhkan koreksi, sehingga dapat dengan aman dianggap berhasil diuji stabilitasnya.

Dan yang terakhir. Sebelum melanjutkan langsung ke solusinya, saya ingin sedikit lebih memikirkan deskripsi dari tiga jenis interaksi antara komponen Vue di antara mereka sendiri: prinsip aliran searah, pola toko, dan bus peristiwa. Jika penjelasan ini tidak perlu (atau membosankan) untuk Anda, langsung ke bagian dengan solusinya - semuanya sesingkat dan teknis mungkin.

Sedikit tentang bagaimana komponen Vue berkomunikasi satu sama lain


Mungkin pertanyaan pertama yang ditimbulkan seseorang yang menulis komponen pertamanya adalah bagaimana ia akan menerima data untuk pekerjaan dan bagaimana, pada gilirannya, ia akan mengirimkan data yang ia terima "keluar". Prinsip interaksi yang diadopsi dalam kerangka Vue JS disebut ...

Aliran data searah


Singkatnya, prinsip ini terdengar seperti "properti - turun, peristiwa - naik." Yaitu, untuk menerima data dari luar ("dari atas"), kami mendaftarkan properti khusus di dalam komponen di mana kerangka kerja menulis, jika perlu, data kami diperoleh dari "luar". Untuk mentransfer data "naik", di dalam komponen di tempat yang tepat, kami memanggil metode kerangka kerja $ emit khusus, yang meneruskan data kami ke penangan komponen induk. Pada saat yang sama, di Vue JS kita tidak bisa hanya "menyiarkan" acara hingga kedalaman yang tidak terbatas (seperti misalnya dalam Angular 1.x). Itu "muncul" hanya satu tingkat, ke orang tua langsung. Hal yang sama berlaku untuk acara. Untuk mentransfernya ke tingkat berikutnya, untuk masing-masing Anda juga perlu mendaftarkan antarmuka khusus - properti dan acara yang akan mengirimkan "pesan" kami lebih lanjut.

Ini dapat digambarkan sebagai gedung kantor di mana pekerja hanya dapat bergerak dari lantai mereka ke lantai tetangga - satu naik dan turun satu. Jadi, untuk mentransfer "dokumen untuk penandatanganan" dari lantai lima ke lantai dua, dibutuhkan tiga pekerja yang akan mengirimkannya dari lantai lima ke lantai dua, dan kemudian tiga orang lagi yang akan mengembalikannya ke lantai lima.

"Tapi ini tidak nyaman!" Tentu saja, ini tidak selalu nyaman dari sudut pandang pengembangan, tetapi melihat kode masing-masing komponen, kita bisa melihat apa dan kepada siapa itu dilewati. Kita tidak perlu mengingat seluruh struktur aplikasi untuk memahami apakah komponen kita sedang dalam perjalanan ke acara tersebut atau tidak. Kita dapat melihat ini dari komponen induk.

Meskipun keuntungan dari pendekatan ini dapat dipahami, ia juga memiliki kelemahan yang jelas, yaitu, kohesi komponen yang tinggi. Sederhananya, agar kita dapat menempatkan beberapa komponen dalam struktur, kita perlu overlay dengan antarmuka yang diperlukan untuk mengelola kondisinya. Untuk mengurangi konektivitas ini, mereka sering menggunakan "alat manajemen negara". Mungkin alat paling populer untuk Vue adalah ...

Vuex (samping)


Melanjutkan analogi kami dengan gedung kantor, Vuex Stor adalah layanan pos internal. Bayangkan di setiap lantai kantor ada jendela untuk menerbitkan dan menerima paket. Di lantai lima mereka mentransfer dokumen No. 11 untuk ditandatangani, dan di lantai kedua mereka secara berkala bertanya: "Apakah ada dokumen untuk ditandatangani?", Tanda tangani dokumen yang ada dan kembalikan. Pada yang kelima mereka juga bertanya: "Apakah ada penandatangan?" Pada saat yang sama, karyawan dapat pindah ke lantai lain atau ke kamar lain - prinsip kerja tidak akan berubah saat surat berfungsi.

Kira-kira dengan prinsip ini pola yang disebut Toko juga berfungsi. Menggunakan antarmuka Vuex, gudang data global terdaftar dan dikonfigurasikan, dan komponen berlangganannya. Dan tidak masalah pada level apa struktur banding terjadi, toko akan selalu memberikan informasi yang benar.

Tampaknya pada masalah ini semua sudah diselesaikan. Tetapi pada suatu titik di gedung metaforis kami, satu karyawan ingin memanggil yang lain untuk makan siang ... atau melaporkan beberapa jenis kesalahan. Dan di sini yang aneh dimulai. Pesan itu sendiri tidak memerlukan transmisi seperti itu. Tetapi untuk menggunakan surat Anda perlu mentransfer sesuatu. Kemudian karyawan kami membuat kode. Satu bola hijau - pergi makan siang, dua kubus merah - kesalahan aplikasi E-981273 terjadi, tiga koin kuning - periksa surat Anda dan sebagainya.

Mudah ditebak bahwa dengan bantuan metafora canggung ini saya menggambarkan situasi ketika kita perlu memastikan respons komponen kita terhadap peristiwa yang terjadi di komponen lain, yang dengan sendirinya tidak terhubung dengan cara apa pun dengan aliran data. Menyimpan item baru selesai - Anda harus mengambil kembali koleksi. Telah terjadi kesalahan 403 Tidak Resmi - Anda harus memulai logout pengguna dan sebagainya. Praktik yang biasa (dan jauh dari yang terbaik) dalam hal ini adalah membuat bendera di dalam toko atau untuk secara tidak langsung menafsirkan data yang disimpan dan perubahannya. Ini dengan cepat menyebabkan pencemaran baik toko itu sendiri maupun logika komponen di sekitarnya.

Pada tahap ini, kita mulai berpikir tentang cara melewati peristiwa secara langsung, melewati seluruh rantai komponen. Dan, sedikit google atau mencari-cari dalam dokumentasi, kami menemukan sebuah pola ...

Bus acara


Dari sudut pandang teknis, bus peristiwa adalah objek yang memungkinkan menggunakan satu metode khusus untuk meluncurkan "peristiwa" dan berlangganan untuk itu menggunakan yang lain. Dengan kata lain, ketika berlangganan event eventA, objek ini menyimpan fungsi handler yang lewat di dalam strukturnya, yang akan dipanggil ketika metode peluncuran dengan kunci eventA dipanggil di suatu tempat dalam aplikasi. Untuk menandatangani atau menjalankannya sudah cukup untuk mengaksesnya melalui impor atau dengan referensi, dan Anda selesai.

Secara metaforis, di "gedung" kami, sebuah bus adalah obrolan yang umum di dalam pembawa pesan. Komponen berlangganan "obrolan umum" dimana komponen lain mengirim pesan. Segera setelah "pesan" muncul di "obrolan", di mana komponen telah berlangganan, pawang akan mulai.

Ada banyak cara berbeda untuk membuat bus acara. Anda bisa menulis sendiri atau menggunakan solusi siap pakai - RxJS yang sama, yang menyediakan fungsionalitas sangat besar untuk bekerja dengan seluruh aliran acara. Tetapi paling sering ketika bekerja dengan VueJS yang mereka gunakan, anehnya, VueJS sendiri. Contoh Vue dibuat melalui konstruktor (Vue baru ()) menyediakan antarmuka acara yang indah dan ringkas, dijelaskan dalam dokumentasi resmi.

Di sini kita mendekati pertanyaan berikutnya ...

Apa yang kita inginkan?


Dan kami ingin membangun sebuah bus acara di aplikasi kami. Tetapi kami memiliki dua persyaratan tambahan:

  1. Itu harus mudah diakses di setiap komponen. Memisahkan impor ke masing-masing dari puluhan komponen tampaknya berlebihan bagi kami.
  2. Itu harus modular. Kami tidak ingin mengingat semua nama acara untuk menghindari situasi ketika "item-made" event memecat handler dari seluruh aplikasi. Oleh karena itu, kami ingin dapat dengan mudah memisahkan fragmen kecil dari komponen pohon menjadi modul terpisah dan menyiarkan acara di dalamnya, dan tidak di luar.

Untuk mengimplementasikan fungsionalitas yang mengesankan seperti itu, kami menggunakan antarmuka plug-in yang kuat yang disediakan oleh VueJS. Anda dapat membiasakan diri dengan lebih detail di sini di halaman ini dengan dokumentasi resmi.

Ayo daftarkan plugin kita terlebih dahulu. Untuk melakukan ini, tepat sebelum titik inisialisasi aplikasi Vue kami (sebelum memanggil Vue. $ Mount ()) kami menempatkan blok berikut:

Vue.use({   
  install(vue) { }, 
});

Faktanya, plugin Vue adalah cara untuk memperluas fungsionalitas framework di seluruh level aplikasi. Antarmuka plugin menyediakan beberapa cara untuk berintegrasi ke dalam komponen, tetapi hari ini kami akan memperkenalkan antarmuka mixin. Metode ini menerima objek yang memperluas keterangan setiap komponen sebelum memulai siklus hidup dalam aplikasi.(Kode komponen yang kami tulis lebih cenderung bukan komponen itu sendiri, tetapi deskripsi perilaku dan enkapsulasi bagian tertentu dari logika yang digunakan kerangka kerja pada berbagai tahap siklus hidupnya. Inisialisasi plug-in berada di luar siklus hidup komponen, sebelum itu, oleh karena itu kami kami mengatakan "deskriptor", bukan komponen, untuk menekankan bahwa kode yang ditulis dalam file kami, dan bukan entitas yang merupakan produk dari kerangka kerja, akan ditransfer ke bagian mixin dari plugin) .

Vue.use({
  install(vue) {     
    vue.mixin({}); // <--
  }, 
});

Objek kosong inilah yang akan berisi ekstensi untuk komponen kami. Tapi sebagai permulaan, berhenti lagi. Dalam kasus kami, kami ingin membuat antarmuka untuk mengakses bus di tingkat setiap komponen. Mari tambahkan bidang '$ broadcast' ke deskriptor kami, ini akan menyimpan tautan ke bus kami. Untuk melakukan ini, gunakan Vue.prototype:

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null; // <--
    vue.mixin({}); 
  }, 
});

Sekarang kita perlu membuat bus itu sendiri, tetapi pertama-tama mari kita ingat persyaratan modularitas dan menganggap bahwa dalam deskriptor komponen kita akan mendeklarasikan modul baru dengan bidang "$ module" dengan beberapa nilai teks (kita akan membutuhkannya sedikit kemudian). Jika bidang $ module ditentukan dalam komponen itu sendiri, kami akan membuat bus baru untuk itu, jika tidak, kami akan meneruskan tautan ke induk melalui bidang $ induk. Perhatikan bahwa bidang deskriptor akan tersedia bagi kami melalui bidang $ options.

Kami akan menempatkan pembuatan bus kami pada tahap sedini mungkin - di hook sebelum Membuat.

Vue.use({
  install(vue) { 
    vue.prototype.$broadcast = null; 
    vue.mixin({
      beforeCreate() {  // <--
        if (this.$options.$module) {  // <--
         
 	} else if (this.$parent && this.$parent.$broadcast) {  // <--
         
        } 
      }, 
    }); 
  }, 
});

Akhirnya, mari kita isi cabang logis. Jika deskriptor berisi deklarasi modul baru, buat instance bus baru, jika tidak, ambil tautan dari $ parent.

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null; 
    vue.mixin({
      beforeCreate() { 
        if (this.$options.$module) {
          this.$broadcast = new Vue();  // <--
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$broadcast = this.$parent.$broadcast;  // <--
        } 
      }, 
    }); 
  }, 
});

Kami membuang pengumuman plugin, kami menganggap ... 1, 2, 3, 4 ... 10 baris, seperti yang saya janjikan!

Bisakah kita melakukannya dengan lebih baik?


Tentu saja kita bisa. Kode ini mudah diperluas. Misalnya, dalam kasus kami, selain $ broadcast, kami memutuskan untuk menambahkan antarmuka $ rootBroadcast, yang memberikan akses ke satu bus untuk seluruh aplikasi. Acara yang dijalankan oleh pengguna pada $ broadcast bus digandakan pada $ rootBroadcast bus sehingga Anda dapat berlangganan semua acara modul tertentu (dalam hal ini, nama acara akan diteruskan ke penangan sebagai argumen pertama) atau ke semua acara aplikasi secara umum (kemudian nama modul akan diteruskan ke handler dengan argumen pertama, nama acara dengan yang kedua, dan data yang dikirimkan dengan peristiwa tersebut akan diteruskan dengan argumen berikut). Desain ini akan memungkinkan kita untuk membangun interaksi antara modul, serta menggantung satu penangan tunggal pada acara-acara modul yang berbeda.

// This one emits event  
this.$broadcast.$emit(‘my-event’, ‘PARAM_A’); 
// This is standard subscription inside module 
this.$broadcast.$on(‘my-event’, (paramA) => {…}); 
// This subscription will work for the same event 
this.$rootBroadcast.$on(‘my-event’, (module, paramA) => {…}); 
// This subscription will also work for the same event 
this.$rootBroadcast.$on(‘*’, (event, module, paramA) => {…});

Mari kita lihat bagaimana kita bisa mencapai ini:

Pertama, buat satu bus, yang akan diatur melalui $ rootBroadcast, dan isian itu sendiri dengan tautan:

const $rootBus = new Vue(); // <--

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus; // <--
        if (this.$options.$module) {
          this.$broadcast = new Vue(); 
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$broadcast = this.$parent.$broadcast; 
        } 
      }, 
    }); 
  }, 
});

Sekarang kita memerlukan keanggotaan modul di setiap komponen, jadi mari kita memperluas definisi modularitas seperti ini:

const $rootBus = new Vue();

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus;
        if (this.$options.$module) {
          this.$module = this.$options.$module;  // <--
          this.$broadcast = new Vue(); 
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$module = this.$parent.$module;  // <--
          this.$broadcast = this.$parent.$broadcast; 
        } 
      }, 
    }); 
  }, 
});

Selanjutnya, kita perlu membuat acara di bus lokal modular mencerminkan cara kita perlu ke root. Untuk melakukan ini, pertama-tama kita harus membuat antarmuka proxy yang sederhana dan menempatkan bus itu sendiri di properti pribadi kondisional $ bus:

const $rootBus = new Vue();

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus;
        if (this.$options.$module) {
          this.$module = this.$options.$module;
          this.$broadcast = { $bus: new Vue() };  // <--
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$module = this.$parent.$module;
          this.$broadcast = { $bus: this.$parent.$broadcast.$bus };  // <--
        } 
      }, 
    }); 
  }, 
});

Dan akhirnya, tambahkan metode proxy ke objek - karena sekarang bidang $ broadcast tidak menyediakan akses langsung ke bus:

const $rootBus = new Vue();

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus;
        if (this.$options.$module) {
          this.$module = this.$options.$module;
          this.$broadcast = { $bus: new Vue() };  
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$module = this.$parent.$module;
          this.$broadcast = { $bus: this.$parent.$broadcast.$bus };
        } 
        // >>>
        this.$broadcast.$emit = (…attrs) => {
          this.$broadcast.$bus.$emit(…attrs);           
          const [event, …attributes] = attrs; 
          this.$rootBroadcast.$emit(event, this.$module, …attributes)); 
          this.$rootBroadcast.$emit(‘*’, event, this.$module, …attributes)
        };
        
        this.$broadcast.$on = (…attrs) => {           
          this.$broadcast.$bus.$on(…attrs);
        };
        // <<<
      }, 
    }); 
  }, 
});

Nah, sebagai sentuhan terakhir, mari kita ingat bahwa kita mendapatkan akses ke bus dengan menutup, yang berarti bahwa penangan yang ditambahkan sekali tidak akan dihapus dengan komponen, tetapi akan hidup selama seluruh waktu bekerja dengan aplikasi. Ini dapat menyebabkan efek samping yang tidak menyenangkan, jadi mari kita tambahkan fungsi pembersihan pendengar ke bus kami di akhir siklus hidup komponen:

const $rootBus = new Vue();

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeDestroy() {                               // <--
        this.$broadcast.$off(this.$broadcastEvents);  // <--
      },

      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus;
        this.$broadcastEvents = [];  // <--
        if (this.$options.$module) {
          this.$module = this.$options.$module;
          this.$broadcast = { $bus: new Vue() };  
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$module = this.$parent.$module;
          this.$broadcast = { $bus: this.$parent.$broadcast.$bus };
        } 

        this.$broadcast.$emit = (…attrs) => {
          this.$broadcastEvents.push(attrs[0]);   // <--
          this.$broadcast.$bus.$emit(…attrs);           
          const [event, …attributes] = attrs; 
          this.$rootBroadcast.$emit(event, this.$module, …attributes)); 
          this.$rootBroadcast.$emit(‘*’, event, this.$module, …attributes)
        };
        
        this.$broadcast.$on = (…attrs) => {           
          this.$broadcast.$bus.$on(…attrs);
        };

        this.$broadcast.$off =: (...attrs) => {  // <--
          this.$broadcast.$bus.$off(...attrs);   // <--
        };
      }, 
    }); 
  }, 
});

Dengan demikian, opsi ini memberikan fungsionalitas yang lebih menarik, meski kurang ringkas. Dengannya, Anda dapat menerapkan sistem komunikasi alternatif yang lengkap antar komponen. Selain itu, ia sepenuhnya di bawah kendali kami dan tidak membawa ketergantungan eksternal ke dalam proyek kami.

Saya harap setelah membaca Anda memperoleh atau menyegarkan kembali pengetahuan Anda tentang plugin Vue, dan mungkin lain kali Anda perlu menambahkan beberapa fungsionalitas generik ke aplikasi Anda, Anda dapat mengimplementasikannya lebih efisien - tanpa menambahkan dependensi eksternal.

All Articles