Bagaimana Fiverr menyeimbangkan pembagian sumber daya dan independensi komponen dalam sistem Front End yang terdesentralisasi.

Arsitektur ujung depan kami di Fiverr menentukan beberapa aplikasi gateway, menyajikan pengalaman “vertikal”. Setiap vertikal membuat laman web dari beberapa komponen, dari berbagai sumber, yang berbagi satu lingkungan waktu proses — jendela, atau cakupan global.

Di organisasi kami, banyak dari komponen-komponen ini memiliki tumpukan teknologi yang sangat mirip (React, Redux, Lodash…) namun kami tetap ingin menjaga komponen-komponen tersebut tetap independen, dan tidak bergantung pada sumber daya di luar kendali mereka. Beberapa komponen mungkin tetap seperti apa adanya tanpa pemeliharaan dalam waktu lama, sementara komponen lainnya sering diubah — komponen dan fitur baru selalu dibuat. Kami ingin mengakomodasi mereka semua.

Akan selalu ada konflik antara lingkungan yang mendukung kode lama dan keinginan untuk berinovasi dengan teknologi baru.

Arsitektur platform kami telah berkembang menjadi struktur yang menurut saya pribadi dapat dipelihara, berkinerja baik, dan memungkinkan pengembangan yang mudah.

Di seluruh sistem, kami telah mengenali sumber daya yang paling umum digunakan dan mengelompokkannya ke dalam 3 kelompok berbeda:

  1. Vendor — Perpustakaan dan kerangka kerja berukuran besar, biasanya merupakan proyek sumber terbuka yang terkenal, yang memiliki siklus rilis versi utama yang cukup rendah.
  2. Keberulangan Tinggi—Modul yang telah disesuaikan dengan kebutuhan pengembangan kami dengan baik sehingga telah menjadi kebiasaan bagi pengembang dan diharapkan tersedia di semua lingkungan, di antaranya i18n kami solusi, dan reporter statistik seperti pemantauan teknis dan bisnis.
  3. Lain-lain Utilitas — Utilitas ini mungkin memiliki tingkat penggunaan kembali yang berbeda-beda. Hal ini mencakup fungsi pembantu, kelas yang berguna, logika bisnis yang dapat diulang, atau operasi fungsional teknis, yang tidak terkait secara pasti dengan fitur atau domain apa pun.

1. Vendor

Pendekatan kami terhadap sumber daya vendor sederhana saja: Sangat mungkin satu halaman berisi beberapa program yang memerlukan ketergantungan yang sama, seringkali dalam jumlah besar. Kami ingin memastikan mereka dapat menggunakan sumber daya yang sama, dan untuk melakukannya kami perlu menyediakannya secara global. Pendekatan ini dimaksudkan untuk menciptakan cache browser yang optimal dan kompilasi minimal (JIT).

Komponen mendeklarasikan dependensi yang ingin mereka gunakan dari daftar yang telah ditentukan sebelumnya (daftar itu sendiri dipertahankan sebagai dependensi umum). Ini adalah jenis kontrak antara komponen dan layanan. Misalnya; komponen A akan mendeklarasikan: react, react-dom, lodash dan komponen B akan mendeklarasikan react, react-dom, redux. Mereka juga memastikan untuk menyertakan dependensi tersebut sebagai Webpack eksternal untuk masing-masing build yang ditargetkan browsernya, sehingga dependensi tersebut dihilangkan dari solusi kode yang dibundel. Terakhir, mereka harus berasumsi bahwa peningkatan di masa depan dalam versi utama berada di luar kendali mereka, sehingga fitur eksperimental biasanya tidak dapat dipertimbangkan.

vendors.json

{
  "lodash": "_",
  "react": "React",
  "react-dom": "ReactDOM",
  "react-redux": "ReactRedux",
  "redux": "Redux"
}

eksternal.js

module.exports = Object.entries(require('./vendors.json'))
  .reduce(
    (collection, [route, name]) => Object.assign(
      collection,
      {
        [route]: {
          'commonjs': route,
          'commonjs2': route,
          'amd': route,
          'root': name,
          'var': name,
        },
      }
    ),
    {}
  );

Aplikasi gateway yang menggunakan komponen ini adalah pemilik runtime; mereka memalsukan dokumen HTML. Mereka mengetahui semua vendor dengan menggunakan paket “vendor” yang sama. Mereka juga dapat mengupgrade patch dan versi minor secara bebas, dengan asumsi tidak akan terjadi kerusakan serius. Mereka akan memperhitungkan persyaratan semua komponen, hanya akan memuat skrip vendor yang diperlukan, dan mengeksposnya di jendela atau di global.

Apa yang kami temukan sangat berguna dalam pendekatan ini adalah, karena kami menyalurkan representasi semua skrip vendor dari satu lokasi, kami dapat menempatkan nama global di atasnya. Awalnya kami mengekspos React sebagai React, tapi kami pikir sebaiknya kami mengekspos versi utama mana yang kami gunakan, jadi React akan direpresentasikan sebagai window.React15.

Karena semua komponen menggunakan eksternal Webpack, nama globalnya menjadi ambigu pada kode itu sendiri — hanya import React from 'react’, dan konfigurasinya akan mengarah ke versi yang diinginkan. Pendekatan ini memungkinkan kita untuk bermigrasi secara bertahap dan membuat halaman yang sama melayani kedua versi React tanpa benturan. Namun, kami telah memutuskan untuk melakukan migrasi tenggat waktu di perusahaan, untuk menghindari skrip vendor membengkak seiring berjalannya waktu.

2. Kekambuhan Tinggi

Pada akhirnya, kami memutuskan untuk memecah kelompok ini dan membagi alat antara dua solusi yang ada — “Vendor” dan “Miscellaneous Utilities” — berdasarkan ukuran, kematangan, dan kemungkinan dari perubahan yang melanggar. Pada saat penulisan ini, “Vendor” hanya menyertakan dua dependensi kami yang sebelumnya kami pertimbangkan dari grup “Pengulangan Tinggi”, solusi i18n kami, dan metode dekorator untuk API pengambilan browser.

Utilitas kami yang lain, betapapun seringnya digunakan, telah digabungkan ke dalam pustaka utilitas, untuk digunakan, dan digabungkan menjadi komponen individual (lihat di bawah).

3. Utilitas Lain-Lain

Karena kami ingin agar setiap paket mengonsumsi apa yang dibutuhkannya, tanpa bloatware, tantangannya terletak pada pembuatan pustaka utilitas tunggal yang dapat disesuaikan dengan kebutuhan dasar dan tidak mengandung pengulangan sekaligus berfungsi sebagai satu titik pencarian untuk operasi pembantu umum .

Kami berasumsi modul umum akan diperlukan dari metode dalam pustaka utilitas itu sendiri, namun jika kami mengekspor setiap modul satu per satu sebagai solusi gabungan, kami akan menyebabkan duplikasi kode di dalam komponen. Paket kecil dan individual menciptakan duplikasi tetapi juga cenderung menimbulkan perkalian versi. Kami pasti ingin menghindari hal ini.

Solusi kami menawarkan perpustakaan utilitas dalam bentuk modul Harmony mentah. Setiap konsumen menggabungkan semua yang mereka perlukan dari modul-modul tersebut, dan hubungan internal antar modul juga tetap utuh. Hal ini memberikan solusi bundling yang optimal untuk masing-masing komponen.

Solusi ini memungkinkan kami untuk memperkenalkan perubahan yang dapat menyebabkan gangguan pada program utilitas kami dengan cara yang tidak merusak. Konsumen memperbarui dengan kecepatan mereka sendiri. Fakta bahwa semua utilitas dipelihara dalam satu perpustakaan menciptakan lingkungan yang nyaman di mana konsumen dan utilitas lainnya akan selalu menggunakan versi yang sama untuk setiap program utilitas, dalam satu bundel (penutupan). Terakhir, pendekatan ini memungkinkan evolusi metode bundling untuk menjaga konsistensi dalam modul yang digunakan: penghentian polyfill, ekspor versi ES yang berbeda, dan bundling terpotong berada dalam kendali konsumen. Misalnya, keputusan untuk menyajikan kode ES6 ke browser pendukung kini mencakup semua utilitas juga.

Terdapat batasan yang diterapkan pada pustaka utilitas — pustaka utilitas hanya boleh menggunakan fitur Javascript yang didukung secara luas, fitur eksperimental mungkin tidak tercakup dalam proses “transpile” konsumen (misalnya plugin babel).

Sebuah utilitas

export const resolve = (string = '', context = global) =>
  string
  .split('.')
  .reduce((prev, current) =>
    typeof prev === 'object' ? prev[current] : prev, context);

Akar utilitas

export * from './deepAssign';
export * from './env';
export * from './inTimeRange';
export * from './multiEventListener';
export * from './pluck';
export * from './resolve';
export * from './select';
export * from './sendEvents';

Semua modul diekspor sebagai modul bernama. Menyimpan nama metode berguna untuk pemeliharaan lintas proyek dan penting bagi kemampuan pengembang untuk masuk dan keluar dari proyek. Ini juga mencegah tabrakan penamaan.

Karena alasan ini, dan untuk menyederhanakan konsumsi, apakah mereka diberi namespace atau bersarang di dalam direktori, modul diekspos di root perpustakaan utilitas.

import { inTimeRange, env, resolve } from '@fiverr/util';

Pustaka utilitas dioptimalkan untuk membuat unit fungsional, dan membersihkan kode fitur dari operasi teknis. Repositori ini juga dilindungi oleh lingkungan pengujian dengan browser nyata (termasuk edge!), membuat dokumentasi otomatis, dan umumnya dibuat optimal untuk mengembangkan unit fungsional kode. Pengembang didorong untuk menggunakannya bahkan untuk modul yang tidak memiliki potensi penggunaan kembali.

Kami merasa kami telah menemukan keseimbangan yang baik, di mana dependensi besar menikmati caching dan berbagi yang optimal, sementara perpustakaan utilitas yang dapat dipelihara menyediakan lingkungan pengembangan yang cepat dengan kemampuan untuk memperkenalkan perubahan yang dapat menyebabkan gangguan dengan aman.

Dunia aplikasi web yang kompleks berkembang pesat, saya yakin semakin banyak organisasi yang menghadapi tantangan serupa. Kami berharap ilustrasi solusi Fiverr ini akan membantu orang lain menyesuaikan solusi mereka sendiri.

Bonus Gotcha — Kecualikan / sertakan Webpack

Output dependensi dari proses “transpile” biasanya dikecualikan (misalnya babel). Konsumen modul Harmony mentah harus “membatalkan pengecualian” utilitas dari prosesnya. Poin ini juga berlaku untuk setelan Jest, dan transpiler lainnya.

const MODULES_TO_INCLUDE = ['@fiverr/util'];
const exclude = new RegExp(`node_modules/(?!(${MODULES_TO_INCLUDE.join('|')})/).*`);
...
  module: {
    rules: [{
      loader: 'babel-loader',
      exclude,