Organisasi pengembangan aplikasi React skala besar

Posting ini didasarkan pada seri tentang memodernisasi frontend jQuery dengan React. Untuk lebih memahami alasan mengapa materi ini ditulis, disarankan untuk melihat artikel pertama dalam seri ini. Saat ini sangat mudah untuk mengatur pengembangan aplikasi React kecil, atau memulai dari awal. Terutama saat menggunakan create-react-app . Beberapa proyek kemungkinan besar hanya membutuhkan sedikit dependensi (misalnya, untuk mengelola status aplikasi dan menginternasionalkan proyek) dan folder yang berisi setidaknya satu direktori







srccomponents... Saya percaya bahwa ini adalah struktur di mana sebagian besar proyek React dimulai. Biasanya, bagaimanapun, seiring bertambahnya jumlah ketergantungan proyek, pemrogram dihadapkan pada peningkatan jumlah komponen, pereduksi, dan mekanisme yang dapat digunakan kembali lainnya termasuk dalam komposisinya. Terkadang semuanya menjadi sangat tidak nyaman dan sulit untuk dikelola. Apa yang harus dilakukan, misalnya, jika tidak lagi jelas mengapa ketergantungan tertentu diperlukan, dan bagaimana mereka cocok? Atau, bagaimana jika proyek telah mengumpulkan begitu banyak komponen sehingga sulit untuk menemukan komponen yang tepat? Apa yang harus dilakukan jika seorang programmer perlu menemukan komponen tertentu yang namanya telah dilupakan?



Ini hanyalah beberapa contoh dari pertanyaan yang harus kami temukan jawabannya saat mengerjakan ulang tampilan depan di Karify . Kami tahu bahwa jumlah dependensi dan komponen proyek suatu hari nanti bisa lepas kendali. Ini berarti kami harus merencanakan segalanya sehingga, seiring dengan berkembangnya proyek, kami dapat terus mengerjakannya dengan percaya diri. Perencanaan ini termasuk menyetujui struktur file dan folder serta kualitas kode. Ini termasuk deskripsi keseluruhan arsitektur proyek. Dan yang paling penting, itu perlu dibuat agar semua ini dapat dengan mudah dirasakan oleh programmer baru yang datang ke proyek, sehingga mereka, untuk dimasukkan dalam pekerjaan, tidak perlu mempelajari proyek terlalu lama, memahami semua ketergantungannya dan gaya kodenya.



Pada saat penulisan ini, kami memiliki sekitar 1200 file JavaScript dalam proyek kami. 350 di antaranya adalah komponen. Kode ini 80% unit diuji. Karena kami masih mematuhi perjanjian yang telah kami buat dan bekerja dalam kerangka arsitektur proyek yang dibuat sebelumnya, kami memutuskan bahwa akan lebih baik untuk membagikan semua ini dengan masyarakat umum. Beginilah artikel ini muncul. Di sini kita akan berbicara tentang mengatur pengembangan aplikasi React berskala besar, dan pelajaran apa yang kita pelajari dari pengalaman mengerjakannya.



Bagaimana cara mengatur file dan folder?



Kami hanya menemukan cara untuk mengatur material front-end React dengan mudah setelah melalui beberapa tahapan proyek. Awalnya, kami akan meng-host materi proyek di repositori yang sama tempat kode frontend berbasis jQuery disimpan. Namun, karena persyaratan untuk struktur folder yang diberlakukan pada proyek oleh kerangka kerja backend yang kami gunakan, opsi ini tidak berfungsi untuk kami. Selanjutnya, kami berpikir untuk memindahkan kode frontend ke repositori terpisah. Pada awalnya pendekatan ini bekerja dengan baik, tetapi seiring waktu kami mulai berpikir untuk membuat bagian klien lain dari proyek, misalnya, frontend berdasarkan React Native. Ini membuat kami berpikir tentang pustaka komponen. Hasilnya, kami membagi repositori baru menjadi dua repositori terpisah. Satu untuk pustaka komponen, dan yang lainnya untuk frontend React yang baru.Meskipun pada awalnya kami mengira ide ini berhasil, implementasinya menyebabkan komplikasi serius dari prosedur peninjauan kode. Hubungan antara perubahan di dua repositori kami menjadi tidak jelas. Akibatnya, kami memutuskan untuk beralih lagi untuk menyimpan kode dalam satu repositori, tetapi sekarang menjadi repositori mono.



Kami memilih repositori mono karena kami ingin memperkenalkan pemisahan antara pustaka komponen dan aplikasi frontend dalam proyek. Perbedaan antara repositori mono kami dan repositori serupa lainnya adalah kami tidak perlu menerbitkan paket di dalam repositori kami. Dalam kasus kami, paket hanyalah sarana untuk memastikan modularitas pengembangan dan alat untuk pemisahan masalah. Sangat berguna untuk memiliki paket yang berbeda untuk varian yang berbeda dari aplikasi Anda, karena ini memungkinkan Anda untuk menentukan dependensi yang berbeda untuk masing-masing dan menerapkan skrip yang berbeda untuk masing-masing.



Kami menyiapkan repositori mono kami menggunakan ruang kerja benang menggunakan konfigurasi berikut di file root package.json:



"workspaces": [
    "app/*",
    "lib/*",
    "tool/*"
]


Sekarang beberapa dari Anda mungkin bertanya-tanya mengapa kami tidak menggunakan folder paket, melakukan hal yang sama seperti di monorepositories lainnya. Ini terutama karena kami ingin memisahkan aplikasi dan pustaka komponen. Selain itu, kami tahu bahwa kami perlu membuat beberapa alat kami sendiri. Hasilnya, kami sampai pada struktur folder di atas. Begini cara folder ini bermain dalam sebuah proyek:



  • app: semua paket dalam folder ini terkait dengan aplikasi frontend seperti Karify frontend dan beberapa frontend internal lainnya. Materi Buku Cerita kami juga disimpan di sini .
  • lib: -, , . , , . , , typography, media primitive.
  • tool: , , Node.js. , , , . , , webpack, , ( Β« Β»).


Semua paket kami, terlepas dari folder penyimpanannya, memiliki subfolder src, dan secara opsional sebuah folder bin. Folder srcpaket, yang disimpan di direktori appdan lib, mungkin berisi beberapa subfolder berikut:



  • actions: Berisi fungsi untuk membuat tindakan yang nilai kembaliannya dapat diteruskan ke fungsi pengiriman dari reduxatau useReducer.
  • components: berisi folder komponen dengan kode, terjemahan, pengujian unit, snapshot, histori (jika berlaku untuk komponen tertentu).
  • constants: folder ini menyimpan nilai yang tidak berubah di lingkungan yang berbeda. Utilitas juga disimpan di sini.
  • fetch: ini adalah tempat definisi jenis disimpan untuk memproses data yang diterima dari API kami, serta tindakan asinkron terkait yang digunakan untuk menerima data tersebut.
  • helpers: , .
  • reducers: , redux useReducer.
  • routes: , react-router history.
  • selectors: , redux-, , API.


Struktur folder ini memungkinkan kita untuk menulis kode yang benar-benar modular, karena ini menciptakan sistem yang jelas untuk membagi tanggung jawab antara berbagai konsep yang ditentukan oleh dependensi kita. Ini membantu kita untuk mencari repositori untuk variabel, fungsi dan komponen, dan, terlebih lagi, terlepas dari apakah orang yang mencarinya mengetahui keberadaannya atau tidak. Selain itu, ini membantu kami untuk menjaga jumlah minimum konten dalam folder terpisah, yang pada gilirannya, membuatnya lebih mudah untuk bekerja dengannya.



Ketika kami mulai menerapkan struktur folder ini, kami dihadapkan pada tantangan untuk memastikan penerapan struktur ini secara konsisten. Saat bekerja dengan paket yang berbeda, pengembang mungkin ingin membuat folder berbeda di folder paket ini, mengatur file di folder ini dengan cara berbeda. Meskipun tidak selalu merupakan hal yang buruk, pendekatan yang tidak teratur seperti itu akan menimbulkan kebingungan. Untuk membantu kami menerapkan struktur di atas secara sistematis, kami telah membuat apa yang disebut "linter sistem file". Kami akan membicarakan ini sekarang.



Bagaimana Anda memastikan bahwa panduan gaya diterapkan?



Kami mengupayakan keseragaman dalam struktur file dan folder dalam proyek kami. Kami ingin mencapai hal yang sama untuk kode. Pada saat itu, kami sudah memiliki pengalaman sukses dalam memecahkan masalah serupa dalam versi proyek jQuery, tetapi kami harus banyak meningkatkan, terutama dalam hal CSS. Akibatnya, kami memutuskan untuk membuat panduan gaya dari awal dan memastikan untuk menggunakannya dengan linter. Aturan yang tidak dapat diterapkan dengan linter dikontrol selama peninjauan kode.



Menyiapkan linter dalam repositori mono dilakukan dengan cara yang sama seperti di repositori lainnya. Ini bagus karena memungkinkan Anda untuk memeriksa seluruh repositori dalam satu proses linter. Jika Anda tidak terbiasa dengan linter, saya sarankan untuk melihat ESLint dan Stylelint . Kami menggunakannya dengan tepat.



Menggunakan linter JavaScript terbukti sangat berguna dalam situasi berikut:



  • Memastikan penggunaan komponen yang dibuat dengan mempertimbangkan aksesibilitas konten, bukan komponen HTML-nya. Saat membuat panduan gaya, kami memperkenalkan beberapa aturan terkait aksesibilitas tautan, tombol, gambar, dan ikon. Kemudian kami perlu menegakkan aturan ini dalam kode dan memastikan bahwa kami, di masa mendatang, tidak akan melupakannya. Kami melakukan ini menggunakan aturan react / larangan-elemen dari eslint-plugin-react .


Berikut adalah contoh tampilannya:



'react/forbid-elements': [
    'error',
    {
        forbid: [
            {
                element: 'img',
                message: 'Use "<Image>" instead. This is important for accessibility reasons.',
            },
        ],
    },
],






Selain linting JavaScript dan CSS, kami juga memiliki "linter sistem file" sendiri. Dialah yang memastikan penggunaan seragam dari struktur folder yang telah kita pilih. Karena ini adalah alat yang kami buat sendiri, jika kami memutuskan untuk beralih ke struktur folder lain, kami selalu dapat mengubahnya. Berikut adalah contoh aturan yang kami kontrol saat bekerja dengan file dan folder:



  • Memeriksa struktur folder komponen: memastikan bahwa selalu ada file index.tsdan .tsx.file dengan nama yang sama dengan folder tersebut.
  • Validasi File package.json: Memastikan bahwa ada satu file tersebut per paket dan properti privatediatur trueuntuk mencegah publikasi paket yang tidak disengaja.


Jenis sistem apa yang harus Anda pilih?



Sekarang, jawaban atas pertanyaan dalam judul bagian ini mungkin cukup mudah bagi banyak orang. Anda hanya perlu menggunakan TypeScript . Dalam beberapa kasus, terlepas dari ukuran proyeknya, menerapkan TypeScript dapat memperlambat pengembangan. Namun kami yakin bahwa ini adalah harga yang wajar yang harus dibayar untuk meningkatkan kualitas dan ketelitian kode.



Sayangnya, pada saat kami mulai mengerjakan proyek, sistem tipe prop masih sangat banyak digunakan.... Pada awal pekerjaan kami, ini sudah cukup bagi kami, tetapi seiring dengan berkembangnya proyek, kami mulai kehilangan kemampuan untuk mendeklarasikan tipe untuk entitas yang bukan komponen. Kami telah melihat bahwa ini akan membantu kami meningkatkan, misalnya, pereduksi dan penyeleksi. Tetapi memperkenalkan sistem pengetikan yang berbeda ke dalam sebuah proyek akan membutuhkan banyak pemfaktoran ulang kode untuk mengetik seluruh basis kode.



Pada akhirnya, kami masih melengkapi proyek kami dengan dukungan tipe, tetapi membuat kesalahan dengan mencoba Flow terlebih dahulu.... Bagi kami, Flow lebih mudah diintegrasikan ke dalam proyek. Meskipun demikian, kami secara teratur mengalami segala macam masalah dengan Flow. Sistem ini tidak berintegrasi dengan baik dengan IDE kami, terkadang karena beberapa alasan yang tidak diketahui tidak mendeteksi beberapa bug, dan membuat tipe generik adalah mimpi buruk yang nyata. Untuk alasan ini, kami akhirnya memigrasikan semuanya ke TypeScript. Jika kita tahu apa yang kita ketahui sekarang, kita akan segera memilih TypeScript.



Karena arah pengembangan TypeScript dalam beberapa tahun terakhir, transisi ini cukup mudah bagi kami. Transisi dari TSLint ke ESLint sangat berguna bagi kami .



Bagaimana cara menguji kode?



Saat kami mulai mengerjakan proyek, tidak begitu jelas bagi kami alat pengujian mana yang harus dipilih. Jika saya memikirkannya sekarang, saya akan mengatakan bahwa, untuk pengujian unit dan integrasi, yang terbaik adalah menggunakan lelucon dan cypress, masing-masing . Alat-alat ini didokumentasikan dengan baik dan mudah digunakan. Sayangnya, cypress tidak mendukung Fetch API , hal buruknya adalah API alat ini tidak dirancang untuk menggunakan konstruksi async / await . Kami, setelah mulai menggunakan cemara, tidak segera memahami hal ini. Tapi saya berharap situasi ini akan membaik dalam waktu dekat.



Pada awalnya, sulit bagi kami untuk menemukan cara terbaik untuk menulis tes unit. Seiring waktu, kami telah mencoba pendekatan seperti pengujian snapshot , pengujian renderer , renderer dangkal . Kami mencoba Pustaka Pengujian . Kami berakhir dengan rendering yang dangkal, digunakan untuk menguji output komponen, dan menggunakan pengujian rendering untuk menguji logika internal komponen.



Kami yakin Pustaka Pengujian adalah solusi yang baik untuk proyek kecil. Namun fakta bahwa sistem ini mengandalkan rendering DOM berdampak besar pada performa benchmark. Apalagi kami percaya kritik itupengujian snapshot menggunakan rendering permukaan tidak relevan untuk komponen yang sangat "dalam". Bagi kami, snapshot ternyata sangat berguna dalam memeriksa semua kemungkinan opsi untuk mengeluarkan komponen. Namun, kode komponen tidak boleh terlalu rumit; Anda harus berusaha membuatnya nyaman untuk dibaca. Hal ini dapat toJSONdicapai dengan membuat komponen kecil dan menetapkan metode untuk input komponen yang tidak terkait dengan snapshot.



Agar tidak melupakan pengujian unit, kami menetapkan ambang cakupan kode melalui pengujian... Dengan bercanda, ini sangat mudah dilakukan, dan tidak banyak yang perlu dipikirkan. Cukup dengan menetapkan indikator cakupan kode global dengan tes. Jadi, di awal pekerjaan, kami menetapkan angka ini pada 60%. Seiring waktu, seiring dengan berkembangnya cakupan pengujian basis kode kami, kami meningkatkannya menjadi 80%. Kami puas dengan indikator ini, karena menurut kami tidak perlu mengupayakan cakupan kode 100% dengan tes. Mencapai tingkat cakupan kode ini dengan tes tampaknya tidak realistis bagi kami.



Bagaimana cara menyederhanakan pembuatan proyek baru?



Biasanya awal bekerja pada Bereaksi-aplikasi yang sangat sederhana: ReactDOM.render(<App />, document.getElementById(β€˜#root’));. Namun jika Anda perlu mendukung SSR (Server-Side Rendering), tugas ini menjadi lebih rumit. Selain itu, jika dependensi aplikasi Anda mencakup lebih dari sekadar React, kode klien dan server Anda mungkin perlu menggunakan parameter yang berbeda. Misalnya, kami menggunakan react-intl untuk internasionalisasi, react-redux untuk manajemen keadaan global , react-router untuk perutean , dan redux-saga untuk mengelola tindakan asinkron . Dependensi ini membutuhkan beberapa penyesuaian. Proses mengonfigurasi dependensi ini bisa jadi rumit.



Solusi kami untuk masalah ini didasarkan pada pola desain " Strategi " dan " Pabrik Abstrak ". Kami biasa membuat dua kelas berbeda (dua strategi berbeda): satu untuk konfigurasi klien dan satu lagi untuk konfigurasi server. Kedua kelas ini menerima parameter dari aplikasi yang dibuat, yang meliputi nama, logo, reduksi, rute, bahasa default, saga (untuk redux-saga), dan sebagainya. Pereduksi, rute, dan saga dapat diambil dari berbagai paket mono-repositori kami. Konfigurasi ini kemudian digunakan untuk membuat redux store, sagas middleware, objek riwayat router. Ini juga digunakan untuk memuat terjemahan dan merender aplikasi. Misalnya, berikut adalah tanda tangan dari klien dan strategi server:



type BootstrapConfiguration = {
  logo: string,
  name: string,
  reducers: ReducersMapObject,
  routes: Route[],
  sagas: Saga[],
};
class AbstractBootstrap {
  configuration: BootstrapConfiguration;
  intl: IntlShape;
  store: Store;
  rootSaga: Task;
abstract public run(): void;
  abstract public render<T>(): T;
  abstract protected createIntl(): IntlShape;
  abstract protected createRootSaga(): Task;
  abstract protected createStore(): Store;
}
//   
class WebBootstrap extends AbstractBootstrap {
  constructor(config: BootstrapConfiguration);
  public render<ReactNode>(): ReactNode;
}
//   
class ServerBootstrap extends AbstractBootstrap {
  constructor(config: BootstrapConfiguration);
  public render<string>(): string;
}


Kami menemukan bahwa pemisahan strategi ini berguna, karena ada beberapa perbedaan dalam menyiapkan penyimpanan, saga, objek internasionalisasi, dan riwayat, bergantung pada lingkungan tempat kode dijalankan. Misalnya, penyimpanan redux di klien dibuat menggunakan data yang dimuat sebelumnya dari server dan menggunakan ekstensi redux-devtools . Semua ini tidak diperlukan di server. Contoh lain adalah objek internasionalisasi yang, pada klien, mendapatkan bahasa saat ini dari navigator.languages , dan di server dari header HTTP Terima-Bahasa .



Penting untuk dicatat bahwa kami telah mengambil keputusan ini sejak lama. Meskipun kelas masih banyak digunakan dalam aplikasi React, tidak ada alat sederhana untuk melakukan rendering aplikasi di sisi server. Seiring waktu, pustaka React mengambil langkah menuju gaya fungsional dan proyek seperti Next.js muncul . Dengan pemikiran ini, jika Anda mencari solusi untuk masalah serupa, kami menyarankan Anda untuk meneliti teknologi terkini. Ini, sangat mungkin, akan memungkinkan kita menemukan sesuatu yang akan lebih sederhana dan lebih fungsional daripada yang kita gunakan.



Bagaimana cara menjaga kualitas kode pada level tinggi?



Linters, tes, pemeriksaan jenis - semua ini memiliki efek menguntungkan pada kualitas kode. Tetapi seorang programmer dapat dengan mudah lupa untuk menjalankan pemeriksaan yang sesuai sebelum memasukkan kode ke dalam sebuah cabang master. Hal terbaik untuk dilakukan adalah menjalankan pemeriksaan tersebut secara otomatis. Beberapa orang lebih suka melakukan ini pada setiap komit menggunakan Git hooks., yang tidak memungkinkan Anda untuk melakukan hingga kode telah melewati semua pemeriksaan. Tetapi kami percaya bahwa dengan pendekatan ini, sistem terlalu banyak mengganggu pekerjaan pemrogram. Lagi pula, misalnya, pengerjaan di cabang tertentu mungkin memerlukan beberapa hari, dan selama ini tidak akan dianggap cocok untuk dikirim ke repositori. Oleh karena itu, kami memeriksa komit menggunakan sistem integrasi berkelanjutan. Hanya kode cabang yang terkait dengan permintaan penggabungan yang diperiksa. Hal ini memungkinkan kita untuk menghindari menjalankan pemeriksaan yang dijamin akan gagal, karena kita paling sering membuat permintaan untuk memasukkan hasil pekerjaan kita ke dalam kode utama proyek ketika kita yakin bahwa hasil tersebut dapat melewati semua pemeriksaan.



Alur validasi kode otomatis dimulai dengan menginstal dependensi. Ini diikuti dengan pemeriksaan jenis, menjalankan linter, menjalankan pengujian unit, membuat aplikasi, menjalankan pengujian cypress. Hampir semua tugas ini dilakukan secara paralel. Jika terjadi kesalahan pada salah satu langkah ini, seluruh proses pembayaran akan gagal dan cabang terkait tidak dapat disertakan dalam kode proyek utama. Berikut adalah contoh sistem tinjauan kode yang berfungsi.





Verifikasi Kode Otomatis



Kesulitan utama yang kami temui saat menyiapkan sistem ini adalah untuk mempercepat pelaksanaan pemeriksaan. Tugas ini masih relevan. Kami melakukan banyak pengoptimalan dan sekarang semua pemeriksaan ini stabil dalam waktu sekitar 20 menit. Mungkin indikator ini dapat ditingkatkan dengan memparalelkan pelaksanaan beberapa tes cemara, tetapi untuk saat ini cocok untuk kita.



Hasil



Mengorganisir pengembangan aplikasi React skala besar bukanlah tugas yang mudah. Untuk mengatasinya, seorang programmer perlu membuat banyak keputusan, banyak alat yang perlu dikonfigurasi. Pada saat yang sama, tidak ada jawaban yang benar untuk pertanyaan tentang bagaimana mengembangkan aplikasi semacam itu.



Sistem kami cocok untuk kami sejauh ini. Kami berharap dengan menceritakannya akan membantu programmer lain yang dihadapkan dengan tugas yang sama dengan yang kami hadapi. Jika Anda memutuskan untuk mengikuti contoh kami, pertama-tama pastikan bahwa apa yang dibahas di sini tepat untuk Anda dan perusahaan Anda. Yang terpenting, berusahalah untuk minimalis. Jangan memperumit aplikasi dan perangkat yang Anda gunakan untuk membuatnya.



Bagaimana Anda mendekati tugas mengatur pengembangan proyek React skala besar?






All Articles