Materi, terjemahan yang kami terbitkan hari ini, akan fokus pada apa yang harus dilakukan dalam situasi di mana data yang diterima dari server tidak tampak seperti yang dibutuhkan klien. Yaitu, pada awalnya kita akan mempertimbangkan masalah khas semacam ini, dan kemudian kita akan menganalisis beberapa cara untuk menyelesaikannya.

Masalah API server gagal
Mari kita pertimbangkan contoh bersyarat berdasarkan beberapa proyek nyata. Misalkan kita sedang mengembangkan situs web baru untuk organisasi yang telah ada selama beberapa waktu. Dia sudah memiliki titik akhir REST, tetapi itu tidak cukup dirancang untuk apa yang akan kita buat. Di sini kita perlu mengakses server hanya untuk mengotentikasi pengguna, untuk mendapatkan informasi tentang dia dan mengunduh daftar pemberitahuan yang tidak ditinjau dari pengguna ini. Akibatnya, kami tertarik pada titik akhir API server berikut:
/auth
: mengotorisasi pengguna dan mengembalikan token akses./profile
: mengembalikan informasi pengguna dasar./notifications
: memungkinkan Anda untuk mendapatkan notifikasi pengguna yang belum dibaca.
Bayangkan bahwa aplikasi kita selalu perlu menerima semua data ini dalam satu unit, yaitu, idealnya, alangkah baiknya jika alih-alih tiga titik akhir kita hanya akan memiliki satu.
Namun, kita dihadapkan dengan lebih banyak masalah daripada terlalu banyak titik akhir. Secara khusus, kita berbicara tentang fakta bahwa data yang kita terima tidak terlihat sebagai cara terbaik.
Misalnya, titik akhir
/profile
dibuat pada zaman kuno, itu tidak ditulis dalam JavaScript, sebagai hasilnya, nama-nama properti dalam data yang dikembalikan ke sana terlihat tidak biasa untuk aplikasi JS:
{ "Profiles": [ { "id": 1234, "Christian_Name": "David", "Surname": "Gilbertson", "Photographs": [ { "Size": "Medium", "URLS": [ "/images/david.png" ] } ], "Last_Login": "2018-01-01" } ] }
Secara umum - tidak ada yang baik.
Benar, jika Anda melihat apa yang dihasilkan oleh endpoint
/notifications
, maka data di atas dari
/profile
akan tampak cukup bagus:
{ "data": { "msg-1234": { "timestamp": "1529739612", "user": { "Christian_Name": "Alice", "Surname": "Guthbertson", "Enhanced": "True", "Photographs": [ { "Size": "Medium", "URLS": [ "/images/alice.png" ] } ] }, "message_summary": "Hey I like your hair, it re", "message": "Hey I like your hair, it really goes nice with your eyes" }, "msg-5678": { "timestamp": "1529731234", "user": { "Christian_Name": "Bob", "Surname": "Smelthsen", "Photographs": [ { "Size": "Medium", "URLS": [ "/images/smelth.png" ] } ] }, "message_summary": "I'm launching my own cryptocu", "message": "I'm launching my own cryptocurrency soon and many thanks for you to look at and talk about" } } }
Di sini daftar pesan adalah objek, bukan array. Lebih lanjut, ada data pengguna di sini, yang disusun dengan tidak nyaman seperti halnya pada titik akhir
/profile
. Dan - inilah kejutan - properti
timestamp
berisi jumlah detik sejak awal 1970.
Jika saya harus menggambar diagram arsitektur dari sistem yang sangat tidak nyaman yang baru saja kita bicarakan, itu akan terlihat seperti yang ditunjukkan pada gambar di bawah ini. Warna merah digunakan untuk bagian-bagian dari rangkaian ini yang sesuai dengan data yang tidak disiapkan dengan baik untuk pekerjaan lebih lanjut.
Diagram sistemKami, dalam keadaan ini, mungkin tidak berusaha untuk memperbaiki arsitektur sistem ini. Anda cukup memuat data dari ketiga API ini dan menggunakan data ini dalam aplikasi. Misalnya, jika Anda perlu menampilkan nama pengguna lengkap di halaman, kami harus menggabungkan properti
Christian_Name
dan
Surname
.
Di sini saya ingin memberi komentar tentang nama. Gagasan untuk membagi nama lengkap seseorang menjadi nama pribadi dan nama keluarga adalah karakteristik negara-negara Barat. Jika Anda mengembangkan sesuatu yang dirancang untuk penggunaan internasional, cobalah untuk mempertimbangkan nama lengkap orang tersebut sebagai string yang tidak dapat dibagi, dan jangan membuat asumsi tentang bagaimana memecah string ini menjadi bagian-bagian yang lebih kecil untuk menggunakan apa yang terjadi di tempat-tempat di mana perlu singkat atau ingin menarik pengguna dalam gaya informal.
Kembali ke struktur data kami yang tidak sempurna. Masalah jelas pertama yang dapat dilihat di sini dinyatakan dalam kebutuhan untuk menggabungkan data yang berbeda dalam kode antarmuka pengguna. Itu terdiri dari fakta bahwa kita mungkin perlu mengulangi tindakan ini di beberapa tempat. Jika Anda hanya perlu melakukan ini sesekali, masalahnya tidak begitu serius, tetapi jika Anda sering membutuhkannya, itu jauh lebih buruk. Akibatnya, ada fenomena yang tidak diinginkan yang disebabkan oleh ketidaksesuaian tentang bagaimana data yang diterima dari server diatur dan bagaimana mereka digunakan dalam aplikasi.
Masalah kedua adalah kompleksitas kode yang digunakan untuk membentuk antarmuka pengguna. Saya percaya bahwa kode seperti itu harus, pertama, sesederhana mungkin, dan kedua - sejelas mungkin. Semakin banyak transformasi data internal yang harus Anda lakukan pada klien, semakin tinggi kompleksitasnya, dan kode kompleks adalah tempat di mana kesalahan biasanya disembunyikan.
Masalah ketiga menyangkut tipe data. Dari cuplikan kode di atas, Anda dapat melihat bahwa, misalnya, pengidentifikasi pesan adalah string, dan pengidentifikasi pengguna adalah angka. Dari sudut pandang teknis, semuanya baik-baik saja, tetapi hal-hal seperti itu dapat membingungkan programmer. Juga, lihat presentasi tanggal! Tetapi bagaimana dengan kekacauan di bagian data yang berhubungan dengan gambar profil? Lagi pula, yang kita butuhkan adalah URL yang mengarah ke file yang sesuai, dan bukan sesuatu dari mana kita harus membuat URL ini sendiri, mengarungi rimba struktur data bersarang.
Jika kami memproses data ini, meneruskannya ke kode antarmuka pengguna, kemudian, menganalisis modul, kami tidak dapat segera memahami dengan tepat apa yang kami kerjakan di sana. Mengubah struktur data internal dan tipenya ketika bekerja dengannya menciptakan beban tambahan pada programmer. Tetapi tanpa semua kesulitan ini sangat mungkin untuk dilakukan.
Bahkan, sebagai opsi, dimungkinkan untuk mengimplementasikan sistem tipe statis untuk menyelesaikan masalah ini, tetapi pengetikan yang ketat tidak mampu, hanya dengan fakta keberadaannya, untuk membuat kode yang buruk menjadi baik.
Sekarang Anda dapat melihat keseriusan masalah yang kita hadapi, mari kita bicara tentang cara untuk menyelesaikannya.
Solusi # 1: mengubah API server
Jika perangkat yang tidak nyaman dari API yang ada tidak ditentukan oleh beberapa alasan penting, maka tidak ada yang mencegah Anda membuat versi baru yang lebih sesuai dengan kebutuhan proyek dan menemukan versi baru ini, katakanlah di
/v2
. Mungkin pendekatan ini bisa disebut solusi paling sukses untuk masalah di atas. Skema sistem seperti itu disajikan pada gambar di bawah ini, struktur data yang sangat cocok dengan kebutuhan klien disorot dalam warna hijau.
API server baru yang menghasilkan persis apa yang dibutuhkan sisi klien dari sistemMulai mengembangkan proyek baru, API yang menyisakan banyak yang diinginkan, saya selalu tertarik pada kemungkinan menerapkan pendekatan yang baru saja dijelaskan. Namun, kadang-kadang perangkat API, meskipun tidak nyaman, memiliki beberapa tujuan penting, atau mengubah API server sama sekali tidak mungkin. Dalam hal ini, saya menggunakan pendekatan berikut.
Solusi # 2: Pola BFF
Ini adalah pola BFF (
Backend-Untuk-Frontend ) tua yang baik. Dengan menggunakan pola ini, Anda dapat mengambil abstrak dari titik akhir REST universal yang rumit dan memberikan ujung depan persis apa yang dibutuhkan. Berikut ini adalah representasi skematis dari solusi semacam itu.
Menerapkan Pola BFFArti keberadaan lapisan BFF adalah untuk memenuhi kebutuhan frontend. Mungkin dia akan menggunakan titik akhir REST tambahan, atau layanan GraphQL, atau soket web, atau apa pun. Tujuan utamanya adalah untuk melakukan segala yang mungkin untuk kenyamanan sisi klien dari aplikasi.
Arsitektur favorit saya adalah NodeJS BFF, yang menggunakan pengembang front-end dapat melakukan apa yang mereka butuhkan, menciptakan API yang bagus untuk aplikasi klien yang mereka kembangkan. Idealnya, kode yang sesuai berada dalam repositori yang sama dengan kode front-end itu sendiri, yang menyederhanakan berbagi kode, misalnya, untuk memeriksa data yang dikirim, baik pada klien maupun di server.
Selain itu, ini berarti bahwa tugas yang memerlukan perubahan pada bagian klien dari aplikasi dan API servernya dilakukan dalam satu repositori. Agak, seperti kata mereka, tapi bagus.
Namun, BFF mungkin tidak selalu digunakan. Dan fakta ini membawa kita ke solusi lain untuk masalah penggunaan API server yang buruk.
Solusi # 3: Pola BIF
Pola BIF (Backend In the Frontend) menggunakan logika yang sama yang dapat diterapkan menggunakan BFF (menggabungkan beberapa API dan pembersihan data), tetapi logika ini bergerak ke sisi klien. Sebenarnya, ide ini bukanlah hal baru, itu bisa dilihat dua puluh tahun yang lalu, tetapi pendekatan seperti itu dapat membantu dalam bekerja dengan API server yang tidak terorganisir, itu sebabnya kami membicarakannya. Ini tampilannya.
Menerapkan Pola BIF▍ Apa itu BIF?
Seperti yang dapat dilihat dari bagian sebelumnya, BIF adalah sebuah pola, yaitu pendekatan untuk memahami kode dan organisasinya. Penggunaannya tidak mengarah pada kebutuhan untuk menghapus logika apa pun dari proyek. Itu hanya memisahkan logika satu jenis (modifikasi struktur data) dari logika jenis lain (pembentukan antarmuka pengguna). Ini mirip dengan gagasan "pemisahan tanggung jawab," yang didengar semua orang.
Di sini saya ingin mencatat bahwa, meskipun ini tidak dapat disebut bencana, saya sering harus melihat implementasi BIF yang buta huruf. Oleh karena itu, menurut saya banyak yang akan tertarik mendengar cerita tentang bagaimana menerapkan pola ini dengan benar.
Kode BIF harus dianggap sebagai kode yang dapat diambil dan ditransfer ke server Node.js, setelah itu semuanya akan bekerja dengan cara yang sama seperti sebelumnya. Atau bahkan mentransfernya ke paket NPM pribadi, yang akan digunakan dalam beberapa proyek front-end dalam kerangka satu perusahaan, yang sangat menakjubkan.
Ingatlah bahwa kami telah membahas di atas masalah utama yang muncul saat bekerja dengan API server yang gagal. Diantaranya adalah panggilan yang terlalu sering ke API dan fakta bahwa data yang dikembalikan oleh mereka tidak memenuhi kebutuhan frontend.
Kami akan memecah solusi untuk masing-masing masalah ini menjadi blok kode yang terpisah, yang masing-masing akan ditempatkan di file sendiri. Akibatnya, lapisan BIF dari bagian klien dari aplikasi akan terdiri dari dua file. Selain itu, file uji akan dilampirkan padanya.
▍ Menggabungkan panggilan API
Melakukan banyak panggilan ke API server dalam kode klien kami bukanlah masalah serius. Namun, saya ingin mengabstraksikannya, untuk memungkinkannya memenuhi satu "permintaan" (dari kode aplikasi ke lapisan BIF), dan mendapatkan apa yang dibutuhkan sebagai tanggapan.
Tentu saja, dalam kasus kami, tidak ada jalan untuk membuat tiga permintaan HTTP ke server, tetapi aplikasi tidak perlu mengetahuinya.
API lapisan BIF saya direpresentasikan sebagai fungsi. Oleh karena itu, ketika aplikasi membutuhkan beberapa data tentang pengguna, itu akan memanggil fungsi
getUser()
, yang akan mengembalikan data ini ke sana. Seperti inilah fungsi ini:
import parseUserData from './parseUserData'; import fetchJson from './fetchJson'; export const getUser = async () => { const auth = await fetchJson('/auth'); const [ profile, notifications ] = await Promise.all([ fetchJson(`/profile/${auth.userId}`, auth.jwt), fetchJson(`/notifications/${auth.userId}`, auth.jwt), ]); return parseUserData(auth, profile, notifications); };
Di sini, pertama, permintaan dibuat ke layanan otentikasi untuk mendapatkan token, yang dapat digunakan untuk mengotorisasi pengguna (kami tidak akan berbicara tentang mekanisme otentikasi di sini, namun tujuan utama kami adalah BIF).
Setelah menerima token, Anda dapat sekaligus menjalankan dua permintaan yang menerima data profil pengguna dan informasi tentang notifikasi yang belum dibaca.
Ngomong-ngomong, lihat betapa indahnya konstruksi
async/await
ketika bekerja dengannya menggunakan
Promise.all
dan menggunakan tugas destruktif.
Jadi, ini adalah langkah pertama, di sini kami mengambil abstrak dari fakta bahwa akses ke server mencakup tiga permintaan. Namun, kasusnya belum dilakukan. Yaitu, perhatikan panggilan ke fungsi
parseUserData()
, yang, seperti yang Anda dapat menilai dari namanya, merapikan data yang diterima dari server. Mari kita bicarakan dia.
▍ Pembersihan data
Saya ingin segera memberikan satu rekomendasi, yang, saya percaya, dapat secara serius mempengaruhi proyek yang sebelumnya tidak memiliki lapisan BIF, khususnya, proyek baru. Cobalah untuk tidak memikirkan apa yang Anda dapatkan dari server untuk sementara waktu. Alih-alih, fokuslah pada data apa yang dibutuhkan aplikasi Anda.
Selain itu, yang terbaik adalah tidak mencoba, ketika merancang aplikasi, untuk memperhitungkan kemungkinan kebutuhan di masa depan, katakanlah, terkait dengan 2021. Cobalah untuk membuat aplikasi berfungsi persis seperti yang seharusnya hari ini. Faktanya adalah bahwa antusiasme yang berlebihan untuk perencanaan dan upaya untuk memprediksi masa depan adalah alasan utama untuk komplikasi proyek perangkat lunak yang tidak dapat dibenarkan.
Jadi, kembali ke bisnis kita. Sekarang kita tahu seperti apa data yang diterima dari tiga API server, dan kita tahu apa yang harus diubah setelah analisis.
Tampaknya inilah salah satu kasus langka ketika penggunaan TDD benar-benar masuk akal. Oleh karena itu, kami akan menulis tes panjang yang besar untuk fungsi
parseUserData()
:
import parseUserData from './parseUserData'; it('should parse the data', () => { const authApiData = { userId: 1234, jwt: 'the jwt', }; const profileApiData = { Profiles: [ { id: 1234, Christian_Name: 'David', Surname: 'Gilbertson', Photographs: [ { Size: 'Medium', URLS: [ '/images/david.png', ], }, ], Last_Login: '2018-01-01' }, ], }; const notificationsApiData = { data: { 'msg-1234': { timestamp: '1529739612', user: { Christian_Name: 'Alice', Surname: 'Guthbertson', Enhanced: 'True', Photographs: [ { Size: 'Medium', URLS: [ '/images/alice.png' ] } ] }, message_summary: 'Hey I like your hair, it re', message: 'Hey I like your hair, it really goes nice with your eyes' }, 'msg-5678': { timestamp: '1529731234', user: { Christian_Name: 'Bob', Surname: 'Smelthsen', }, message_summary: 'I\'m launching my own cryptocu', message: 'I\'m launching my own cryptocurrency soon and many thanks for you to look at and talk about' }, }, }; const parsedData = parseUserData(authApiData, profileApiData, notificationsApiData); expect(parsedData).toEqual({ jwt: 'the jwt', id: '1234', name: 'David Gilbertson', photoUrl: '/images/david.png', notifications: [ { id: 'msg-1234', dateTime: expect.any(Date), name: 'Alice Guthbertson', premiumMember: true, photoUrl: '/images/alice.png', message: 'Hey I like your hair, it really goes nice with your eyes' }, { id: 'msg-5678', dateTime: expect.any(Date), name: 'Bob Smelthsen', premiumMember: false, photoUrl: '/images/placeholder.jpg', message: 'I\'m launching my own cryptocurrency soon and many thanks for you to look at and talk about' }, ], }); });
Dan ini kode fungsinya sendiri:
const getPhotoFromProfile = profile => { try { return profile.Photographs[0].URLS[0]; } catch (err) { return '/images/placeholder.jpg'; // } }; const getFullNameFromProfile = profile => `${profile.Christian_Name} ${profile.Surname}`; export default function parseUserData(authApiData, profileApiData, notificationsApiData) { const profile = profileApiData.Profiles[0]; const result = { jwt: authApiData.jwt, id: authApiData.userId.toString(), // ID name: getFullNameFromProfile(profile), photoUrl: getPhotoFromProfile(profile), notifications: [], // , }; Object.entries(notificationsApiData.data).forEach(([id, notification]) => { result.notifications.push({ id, dateTime: new Date(Number(notification.timestamp) * 1000), // , , , Unix, name: getFullNameFromProfile(notification.user), photoUrl: getPhotoFromProfile(notification.user), message: notification.message, premiumMember: notification.user.Enhanced === 'True', }) }); return result; }
Saya ingin mencatat bahwa ketika dimungkinkan untuk mengumpulkan di satu tempat dua ratus baris kode yang bertanggung jawab untuk memodifikasi data yang tersebar sebelum ini di seluruh aplikasi, itu menyebabkan perasaan yang luar biasa. Sekarang semua ini ada dalam satu file, unit test ditulis untuk kode ini, dan semua momen ambigu diberikan komentar.
Saya katakan sebelumnya bahwa BFF adalah pendekatan favorit saya untuk menggabungkan dan membersihkan data, tetapi ada satu bidang di mana BIF lebih unggul dari BFF. Yaitu, data yang diterima dari server dapat mencakup objek JavaScript yang tidak mendukung JSON, seperti objek
Date
atau
Map
(mungkin ini adalah salah satu fitur JavaScript yang paling kurang dimanfaatkan). Sebagai contoh, dalam kasus kami, kami harus mengubah tanggal yang datang dari server (dinyatakan dalam detik, bukan milidetik) menjadi objek JS-
Date
-type.
Ringkasan
Jika Anda berpikir bahwa proyek Anda memiliki sesuatu yang sama dengan proyek di mana kami memeriksa masalah API yang tidak berhasil, analisis kode dengan bertanya pada diri sendiri pertanyaan berikut tentang menggunakan data dari server pada klien:
- Apakah Anda harus menggabungkan properti yang tidak pernah digunakan secara terpisah (misalnya, nama depan dan belakang pengguna)?
- Apakah kode JS harus bekerja dengan nama properti yang dibentuk dengan cara yang tidak diterima di JS (sesuatu seperti PascalCase)?
- Apa tipe data dari berbagai pengidentifikasi? Mungkin terkadang ini adalah string, terkadang angka?
- Bagaimana tanggal disajikan dalam proyek Anda? Mungkin terkadang ini adalah objek
Date
JS yang siap digunakan dalam antarmuka, dan terkadang angka, atau bahkan string? - Apakah Anda sering harus memeriksa properti untuk keberadaannya, atau memeriksa apakah suatu entitas adalah array sebelum Anda mulai menghitung elemen-elemen dari entitas ini untuk membentuk beberapa fragmen antarmuka pengguna pada dasarnya? Mungkinkah entitas ini tidak akan menjadi array, bahkan jika kosong?
- Apakah Anda harus mengurutkan atau memfilter array ketika membentuk antarmuka, yang, idealnya, harus sudah diurutkan dan difilter dengan benar?
- Jika ternyata, ketika memeriksa properti untuk keberadaannya, tidak ada properti yang dicari, apakah Anda harus beralih menggunakan beberapa nilai default (misalnya, gunakan gambar standar ketika tidak ada foto pengguna dalam data yang diterima dari server)?
- Apakah properti dinamai secara seragam? Apakah itu terjadi bahwa entitas yang sama dapat memiliki nama yang berbeda, yang mungkin disebabkan oleh penggunaan bersama dari API server "lama" dan "baru"?
- Apakah Anda harus, bersama dengan data yang berguna, mentransfer data di suatu tempat yang tidak pernah digunakan, melakukan ini hanya karena berasal dari server API? Apakah data yang tidak digunakan ini mengganggu proses debug?
Jika Anda dapat dengan positif menjawab satu atau dua pertanyaan dari daftar ini, maka mungkin Anda sebaiknya tidak memperbaiki sesuatu yang sudah berfungsi dengan baik.
Namun, jika Anda, membaca pertanyaan-pertanyaan ini, cari tahu masing-masing dari mereka masalah proyek Anda, jika perangkat kode Anda tidak perlu rumit karena semua ini, jika sulit untuk dilihat dan diuji, jika mengandung kesalahan yang sulit dideteksi, lihat pola BIF.
Pada akhirnya, saya ingin mengatakan bahwa ketika memperkenalkan lapisan BIF ke dalam aplikasi yang ada, hal-hal lebih mudah karena fakta bahwa ini dapat dilakukan secara bertahap, dalam langkah-langkah kecil. Katakanlah versi pertama dari fungsi untuk menyiapkan data, sebut saja
parseData()
, bisa dengan mudah, tanpa perubahan, mengembalikan apa yang datang ke inputnya. Kemudian Anda dapat secara bertahap memindahkan logika dari kode yang bertanggung jawab untuk membuat antarmuka pengguna ke fungsi ini.
Pembaca yang budiman! , BIF?
