
Setiap proyek client-server menyiratkan pemisahan yang jelas dari basis kode menjadi 2 bagian (kadang-kadang lebih) - klien dan server. Seringkali, setiap bagian tersebut dieksekusi dalam bentuk proyek independen yang terpisah, didukung oleh tim pengembangnya sendiri.
Pada artikel ini, saya mengusulkan pandangan kritis pada pembagian kode standar menjadi backend dan frontend. Dan pertimbangkan alternatif di mana kode tidak memiliki garis yang jelas antara klien dan server.
Kontra dari pendekatan standar
Kerugian utama dari pemisahan standar proyek menjadi 2 bagian adalah erosi logika bisnis antara klien dan server. Kami mengedit data dalam formulir di browser, memverifikasinya dalam kode klien dan mengirimkannya ke desa kakek (ke server). Server sudah merupakan proyek lain. Di sana, Anda juga perlu memeriksa kebenaran data yang diterima (mis., Menduplikasi fungsi klien), melakukan beberapa manipulasi tambahan (simpan dalam database, kirim email, dll.).
Dengan demikian, untuk melacak seluruh jalur informasi dari formulir di browser ke database di server, kita harus mempelajari dua sistem yang beragam. Jika peran dibagi dalam tim dan spesialis berbeda bertanggung jawab untuk backend dan frontend, masalah organisasi tambahan muncul terkait dengan sinkronisasi mereka.
Mari bermimpi
Misalkan kita dapat menggambarkan seluruh jalur data dari formulir di klien ke database di server dalam satu model. Dalam kode, mungkin terlihat seperti ini (kode tidak berfungsi):
class MyDataModel {
Dengan demikian, seluruh logika bisnis model ada di depan mata kita. Mempertahankan kode seperti itu lebih mudah. Berikut adalah keuntungan yang dapat memadukan metode client-server dalam satu model:
- Logika bisnis terkonsentrasi di satu tempat, tidak perlu membaginya antara klien dan server.
- Anda dapat dengan mudah mentransfer fungsionalitas dari server ke klien atau dari klien ke server selama pengembangan proyek.
- Tidak perlu menduplikasi metode yang sama untuk backend dan frontend.
- Satu set tes untuk seluruh logika bisnis proyek.
- Mengganti garis horizontal garis batas tanggung jawab dalam proyek dengan garis vertikal.
Saya akan mengungkapkan poin terakhir secara lebih rinci. Bayangkan aplikasi client-server biasa dalam bentuk skema seperti itu:

Vasya bertanggung jawab untuk frontend, Fedya - untuk backend. Garis batas tanggung jawab berjalan secara horizontal. Skema ini memiliki kelemahan dari setiap struktur vertikal - sulit untuk skala dan memiliki toleransi kesalahan yang rendah. Jika proyek ini berkembang, Anda harus membuat pilihan yang agak sulit: siapa yang memperkuat Vasya atau Fedya? Atau jika Fedya sakit atau berhenti, Vasya tidak akan bisa menggantikannya.
Pendekatan yang diusulkan di sini memungkinkan Anda untuk memperluas garis pembagian tanggung jawab sebesar 90 derajat dan mengubah arsitektur vertikal menjadi horizontal.

Arsitektur seperti itu jauh lebih mudah untuk diukur dan lebih toleran terhadap kesalahan. Vasya dan Fedya menjadi dipertukarkan.
Secara teori, ini terlihat bagus, mari kita coba menerapkan semua ini dalam praktiknya, tanpa kehilangan semua yang memberi kita keberadaan terpisah dari klien dan server di sepanjang jalan.
Pernyataan masalah
Kami benar-benar tidak harus memiliki server klien terintegrasi dalam produk. Sebaliknya, keputusan seperti itu akan sangat berbahaya dari semua sudut pandang. Tugasnya adalah bahwa dalam proses pengembangan kita akan memiliki basis kode tunggal untuk model data untuk backend dan frontend, tetapi output akan menjadi klien dan server independen. Dalam hal ini, kita akan mendapatkan semua keuntungan dari pendekatan standar dan mendapatkan fasilitas yang tercantum di atas untuk pengembangan dan dukungan proyek.
Solusi
Saya telah bereksperimen dengan integrasi klien dan server dalam satu file untuk beberapa waktu. Sampai baru-baru ini, masalah utama adalah bahwa di JS standar, koneksi modul pihak ketiga pada klien dan server terlalu berbeda: memerlukan (...) di node.js, semua sihir AJAX pada klien. Semuanya telah berubah dengan munculnya modul-ES. Di browser modern, "impor" telah didukung sejak lama. Node.js sedikit ketinggalan dalam hal ini dan ES-modul hanya didukung dengan flag - Eksperimental-modul diaktifkan. Diharapkan di masa mendatang, modul akan bekerja di luar kotak di node.js. Selain itu, tidak mungkin sesuatu akan banyak berubah, karena di browser, fungsi ini telah berfungsi secara default sejak lama. Saya pikir sekarang Anda dapat menggunakan modul-ES tidak hanya pada klien tetapi juga di sisi server (jika Anda memiliki kontra-argumen tentang hal ini, tulis di komentar).
Skema solusi terlihat seperti ini:

Proyek ini berisi tiga katalog utama:
dilindungi - backend;
public -frontend;
shared - model client-server bersama.
Proses pengamat yang terpisah memonitor file dalam direktori bersama dan, dengan perubahan apa pun, membuat versi file yang diubah secara terpisah untuk klien dan secara terpisah untuk server (dalam direktori yang dilindungi / dibagikan dan publik / bersama).
Implementasi
Pertimbangkan contoh seorang kurir real-time yang sederhana. Kita perlu node.js baru (saya punya versi 11.0.0) dan Redis (menginstalnya tidak dibahas di sini).
Klon contoh:
git clone https://github.com/Kolbaskin/both-example cd ./both-example npm i
Instal dan jalankan proses pengamat (pengamat dalam diagram):
npm i both-js -g both ./index.mjs
Jika semuanya beres, pengamat akan meluncurkan server web dan mulai memantau perubahan pada file di direktori bersama dan dilindungi. Ketika perubahan dibuat untuk dibagikan, versi model data yang sesuai untuk klien dan server dibuat. Setelah perubahan dilindungi, pengamat akan memulai kembali server web secara otomatis.
Anda dapat melihat kinerja messenger di browser dengan mengklik tautan
http://localhost:3000/index.html?token=123&user=Vasya
(token dan pengguna sewenang-wenang). Untuk meniru beberapa pengguna, buka halaman yang sama di browser lain dengan menentukan token dan pengguna yang berbeda.
Sekarang sedikit kode.
Server web
protected / server.mjs
import express from 'express'; import bodyParser from 'body-parser';
Ini adalah server kilat biasa, tidak ada yang menarik di sini. Ekstensi mjs diperlukan untuk modul ES di node.js. Untuk konsistensi, kami akan menggunakan ekstensi ini untuk klien.
Pelanggan
publik / index.html
<!DOCTYPE html> <html lang="en"> <head> ... <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="/main.mjs" type="module"></script> </head> <body> ... <ul id="users"> <li v-for="user in users"> {{ user.name }} ({{user.id}}) </li> </ul> <div id="messages"> <div> <input type="text" v-model="msg" /> <button v-on:click="sendMessage()"></button> </div> <ul> <li v-for="message in messages">[{{ message.date }}] <strong>{{ message.text }}</strong></li> </ul> </div> </body> </html>
Sebagai contoh, saya menggunakan Vue pada klien, tetapi ini tidak mengubah esensi. Alih-alih Vue, bisa ada apa pun di mana Anda dapat memisahkan model data menjadi kelas yang terpisah (sistem gugur, sudut).
public / main.mjs
main.mjs adalah skrip yang mengaitkan model data dengan tampilan yang sesuai. Untuk menyederhanakan kode, contoh representasi untuk daftar pengguna aktif dan umpan pesan dibangun langsung ke index.html
Model data
shared / messages / model / dataModel.mjs
Beberapa metode ini menerapkan semua fungsi pengiriman dan penerimaan pesan secara real time. Arahan! #Client dan! #Server memberi tahu proses pengamat metode untuk bagian mana (klien atau server) yang dimaksudkan. Jika tidak ada arahan ini sebelum mendefinisikan metode, metode seperti itu tersedia pada klien dan server. Garis miring komentar sebelum arahan adalah opsional dan hanya ada untuk mencegah IDE standar bersumpah pada kesalahan sintaksis.
Baris pertama jalur menggunakan pencarian & root. Saat membuat versi klien dan server, & root akan diganti masing-masing dengan jalur relatif ke direktori publik dan yang dilindungi.
Poin penting lainnya: dari metode klien, Anda hanya dapat memanggil metode server, yang namanya dimulai dengan "$":
...
Ini dilakukan untuk alasan keamanan: dari luar Anda hanya dapat beralih ke metode yang dirancang khusus untuk ini.
Mari kita lihat versi model data yang dihasilkan pengamat untuk klien dan server.
Klien (publik / bersama / pesan / model / dataModel.mjs)
import Base from '/lib/Base.mjs'; export default class dataModel extends Base { __getFilePath__() {return "messages/model/dataModel.mjs"}
Di sisi klien, model adalah turunan dari kelas Vue (via Base.mjs). Dengan demikian, Anda dapat bekerja dengannya seperti dengan model data Vue biasa. Pengamat menambahkan metode __getFilePath__ ke versi klien dari model, yang mengembalikan path ke file kelas dan mengganti kode metode server $ sendMessage dengan konstruk yang, pada dasarnya, akan memanggil metode yang kita butuhkan pada server melalui mekanisme rpc (__runSharedFunction didefinisikan dalam kelas induk).
Server (dilindungi / dibagikan / pesan / model / dataModel.mjs)
import Base from '../../lib/Base.mjs'; export default class dataModel extends Base { __getFilePath__() {return "messages/model/dataModel.mjs"} ... ...
Dalam versi server, metode __getFilePath__ juga ditambahkan dan metode klien yang ditandai dengan arahan dihapus! #Client
Di kedua versi model yang dibuat, semua baris yang dihapus diganti dengan yang kosong. Ini dilakukan agar pesan kesalahan pada debugger dapat dengan mudah menemukan baris bermasalah dalam kode sumber model.
Interaksi klien-server
Ketika kita perlu memanggil beberapa metode server pada klien, kita lakukan saja.
Jika panggilan itu dalam model yang sama, maka semuanya sederhana:
... !#client async sendMessage(e) { await this.$sendMessage(this.msg); this.msg = ''; } !#server async $sendMessage(msg) {
Anda dapat "menarik" model lain:
import dataModel from "/shared/messages/model/dataModel.mjs"; var msg = new dataModel(); msg.$sendMessage('blah-blah-blah');
Di arah yang berlawanan, mis. Memanggil beberapa metode klien di server tidak berfungsi. Secara teknis, ini layak, tetapi dari sudut pandang praktis tidak masuk akal, karena server adalah satu, tetapi ada banyak klien. Jika kami perlu melakukan beberapa tindakan pada server pada klien, kami menggunakan mekanisme acara:
Metode fireEvent mengambil 3 parameter: nama acara, kepada siapa itu ditujukan, dan data. Anda dapat mengatur penerima dengan beberapa cara: kata kunci “semua” - acara akan dikirim ke semua pengguna atau dalam larik untuk mencantumkan token sesi klien yang kepadanya acara tersebut ditangani.
Acara ini tidak terikat pada contoh spesifik dari kelas model data dan penangan akan memecat dalam semua contoh kelas di mana fireEvent dipanggil.
Penskalaan backend horisontal
Monolithicity model client-server dalam implementasi yang diusulkan, pada pandangan pertama, harus memaksakan pembatasan yang signifikan pada kemungkinan penskalaan horizontal dari bagian server. Tapi ini tidak begitu: secara teknis, server tidak tergantung pada klien. Anda dapat menyalin direktori "publik" di mana saja dan memberikan kontennya melalui server web lain (nginx, apache, dll.).
Sisi server dapat dengan mudah diperluas dengan meluncurkan instance backend baru. Redis dan sistem antrian Kue digunakan untuk berinteraksi dengan instance individual.
API dan klien berbeda untuk satu backend
Dalam proyek nyata, beragam klien server dapat menggunakan satu server API - situs web, aplikasi seluler, layanan pihak ketiga. Dalam solusi yang diusulkan, semua ini tersedia tanpa tarian tambahan. Di bawah kap server metode panggilan adalah rpc tua yang baik. Server web itu sendiri adalah aplikasi ekspres klasik. Cukup menambahkan pembungkus di sana untuk rute dengan memanggil metode yang diperlukan dari model data yang sama.
Posting scriptum
Pendekatan yang diusulkan dalam artikel tidak berpura-pura dengan perubahan revolusioner dalam aplikasi client-server. Itu hanya menambah sedikit kenyamanan pada proses pengembangan, memungkinkan Anda untuk fokus pada logika bisnis yang dirangkai di satu tempat.
Proyek ini eksperimental, tulis di komentar apakah, menurut pendapat Anda, layak untuk melanjutkan percobaan ini.