Frontend Odnoklassniki baru: meluncurkan React in Java. Bagian II



Kami melanjutkan kisah tentang bagaimana, di dalam Odnoklassniki, dengan bantuan GraalVM, kami berhasil berteman dengan Java dan JavaScript dan mulai bermigrasi ke sistem besar dengan banyak kode lawas.

Pada bagian kedua artikel ini, kita akan berbicara secara rinci tentang peluncuran, perakitan, dan integrasi aplikasi pada tumpukan baru, selami spesifik pekerjaan mereka baik pada klien maupun di server, serta membahas kesulitan yang dihadapi dalam perjalanan kami dan menjelaskan solusi untuk membantu mereka mengatasi .

Jika Anda belum membaca bagian pertamaSaya sangat merekomendasikan melakukan ini. Dari sini Anda akan belajar tentang sejarah frontend di Odnoklassniki dan berkenalan dengan fitur-fitur historisnya, melalui jalur menemukan solusi untuk masalah yang telah terakumulasi dalam 13 tahun proyek kami, dan pada akhirnya Anda akan terjun ke fitur teknis dari implementasi server dari keputusan yang kami buat.

Konfigurasi UI


Untuk menulis kode UI, kami memilih alat yang paling canggih: Bereaksi bersama dengan MobX, Modul CSS, ESLint, TypeScript, Lerna. Semua ini dikumpulkan menggunakan Webpack.



Arsitektur aplikasi


Seperti yang ditulis di bagian sebelumnya artikel ini, untuk menerapkan migrasi bertahap, kami akan memasukkan komponen baru di situs dalam elemen DOM dengan nama kustom yang akan berfungsi di dalam tumpukan UI baru, sedangkan untuk sisa situs akan terlihat seperti elemen DOM dengan API-nya. Isi elemen-elemen ini dapat dirender di server.

Apa itu? Di dalamnya ada aplikasi MVC keren, modis, modern yang berjalan di React dan menyediakan API DOM standar: atribut, metode pada elemen DOM ini, dan acara.



Untuk menjalankan komponen seperti itu, kami telah mengembangkan mekanisme khusus. Apa yang dia lakukan? Pertama, ia menginisialisasi aplikasi sesuai dengan deskripsinya. Kedua, ia mengikat komponen ke simpul DOM tertentu di mana ia mulai. Ada juga dua mesin (untuk klien dan untuk server) yang dapat menemukan dan membuat komponen-komponen ini.



Mengapa ini dibutuhkan? Faktanya adalah bahwa ketika seluruh situs dibuat pada React, maka biasanya komponen situs dirender ke dalam elemen root halaman, dan komponen ini tidak masalah apa yang ada di luar, tetapi hanya apa yang ada di dalamnya yang menarik.

Dalam kasus kami, semuanya lebih rumit: sejumlah aplikasi perlu kesempatan untuk memberi tahu halaman kami di situs "Saya, dan ada sesuatu yang berubah dalam diri saya." Sebagai contoh, kalender perlu mengadakan acara yang diklik pengguna pada tombol, dan tanggal telah berubah, atau di luar Anda memerlukan kemampuan sehingga di dalam kalender Anda dapat mengubah tanggal. Untuk ini, mesin aplikasi mengimplementasikan fasad dalam fungsionalitas dasar aplikasi.

Saat mengirimkan komponen ke klien, perlu bahwa mesin situs lama dapat meluncurkan komponen ini. Untuk melakukan ini, selama pembangunan, informasi yang diperlukan untuk peluncurannya dikumpulkan.

{
    "events-calendar": {
        "bundleName": "events-calendar",
        "js": "events-calendar-h4h5m.js",
        "css": "events-calendar-h4h5m.css"
    }
}


Marker khusus ditambahkan ke atribut tag komponen, yang mengatakan bahwa aplikasi ini adalah tipe baru, kodenya dapat diambil dari file JS tertentu. Pada saat yang sama, ia memiliki atribut sendiri yang diperlukan untuk menginisialisasi komponen ini: mereka membentuk keadaan awal komponen di toko.

<events-calendar	data-module="react-loader"
			data-bundle="events-calendar.js"
			date=".."
			marks="[{..}]"
			…
/>


Untuk rehidrasi, itu bukan pemeran status aplikasi yang digunakan, tetapi atribut, yang memungkinkan penghematan lalu lintas. Mereka datang dalam bentuk yang dinormalisasi, dan, sebagai aturan, lebih kecil dari toko yang dibuat aplikasi. Pada saat yang sama, waktu untuk membuat ulang toko dari atribut pada klien adalah singkat, sehingga biasanya dapat diabaikan.

Misalnya, untuk kalender, atribut hanya memiliki tanggal yang disorot, dan toko sudah memiliki matriks dengan informasi lengkap untuk bulan itu. Jelas, tidak ada gunanya mentransfernya dari server.

Bagaimana cara menjalankan kode?


Konsep ini diuji pada fungsi-fungsi sederhana yang memberikan garis untuk server atau menulis innerHTML untuk klien Tetapi dalam kode nyata ada modul dan TypeScript.

Ada solusi standar untuk klien, misalnya, mengumpulkan kode menggunakan Webpack, yang dengan sendirinya menggiling semuanya dan memberikannya kepada klien dalam bentuk bundel bundel. Dan apa yang harus dilakukan untuk server saat menggunakan GraalVM?



Mari kita pertimbangkan dua opsi. Yang pertama adalah mengetikkan TypeScript dalam JavaScript, seperti yang mereka lakukan untuk Node.js. Sayangnya, opsi ini tidak berfungsi dalam konfigurasi kami ketika JavaScript adalah bahasa tamu di GraalVM. Dalam hal ini, JavaScript tidak memiliki sistem modular, atau bahkan sinkronisasi. Karena modularitas dan bekerja dengan asinkron memberikan runtime spesifik: NodeJS atau browser. Dan dalam kasus kami, server memiliki JavaScript yang hanya dapat menjalankan kode secara serempak.

Opsi kedua - Anda cukup menjalankan kode server dari file yang sama yang dikumpulkan untuk klien. Dan opsi ini berfungsi. Tetapi ada masalah bahwa server membutuhkan implementasi lain untuk sejumlah metode. Misalnya, fungsi renderToString () akan dipanggil pada server untuk membuat komponen, dan ReactDOM.render () pada klien. Atau contoh lain dari artikel sebelumnya: untuk mendapatkan teks dan pengaturan di server, fungsi yang disediakan Java akan dipanggil, dan pada klien itu akan menjadi implementasi di JS.

Sebagai solusi untuk masalah ini, Anda dapat menggunakan alias dari Webpack. Mereka memungkinkan Anda untuk membuat dua implementasi dari kelas yang kita butuhkan: untuk klien dan server. Kemudian, dalam file konfigurasi untuk klien dan server, tentukan implementasi yang sesuai.



Tetapi dua file konfigurasi adalah dua rakitan. Setiap kali, mengumpulkan semuanya secara terpisah untuk server dan untuk klien panjang dan sulit untuk didukung.

Anda harus membuat konfigurasi sedemikian rupa sehingga semuanya terkumpul dalam sekali jalan.

Konfigurasi webpack untuk menjalankan JS di server dan klien


Untuk menemukan solusi untuk masalah ini, mari kita lihat bagian proyek yang terdiri dari:



Pertama, proyek memiliki runtime pihak ketiga (vendor), yang sama untuk klien dan untuk server. Hampir tidak pernah berubah. Rantime dapat diberikan kepada pengguna, dan ia akan di-cache pada klien hingga kami memperbarui versi perpustakaan pihak ketiga.

Kedua, ada runtime kami (inti), yang memastikan peluncuran aplikasi. Ini memiliki metode dengan implementasi yang berbeda untuk klien dan server. Misalnya, mendapatkan teks pelokalan, pengaturan, dan sebagainya. Runtime ini juga jarang berubah.

Ketiga, ada kode komponen. Itu sama untuk klien dan untuk server, yang memungkinkan Anda untuk men-debug kode aplikasi di browser tanpa memulai server sama sekali. Jika ada yang tidak beres pada klien, Anda dapat melihat kesalahan di konsol browser, mengingat semuanya dan pastikan bahwa tidak ada kesalahan saat memulai di server.

Secara total, tiga bagian diperoleh yang perlu dirakit. Kami ingin:
  • Konfigurasikan perakitan masing-masing bagian secara terpisah.
  • Letakkan dependensi di antara mereka sehingga masing-masing bagian tidak jatuh ke dalam apa yang ada di yang lain.
  • Kumpulkan semuanya dalam satu pass.


Bagaimana menjelaskan secara terpisah bagian-bagian yang akan terdiri dari majelis? Ada multikonfigurasi di webpack: Anda cukup memberikan array ekspor modul yang disertakan di setiap bagian.

module.exports = [{
  entry: './vendors.js',
}, {
  entry: './core.js'
}, {
 entry: './app.js'
}];


Semuanya akan baik-baik saja, tetapi di masing-masing bagian ini kode dari modul-modul di mana bagian ini bergantung akan diduplikasi:



Untungnya, dalam set dasar plugin webpack ada DllPlugin , yang memungkinkan Anda untuk mendapatkan daftar modul yang disertakan di dalamnya untuk setiap bagian yang dirakit. Misalnya, untuk vendor, Anda dapat mengetahui modul spesifik mana yang termasuk dalam bagian ini.

Ketika membangun bagian lain, misalnya, perpustakaan inti, kita dapat mengatakan bahwa mereka bergantung pada bagian vendor.



Kemudian, selama perakitan webpack, DllPlugin akan melihat inti tergantung pada beberapa pustaka yang sudah ada di vendor, dan tidak akan menambahkannya ke inti, tetapi cukup tautkan ke dalamnya.

Alhasil, tiga potong dirangkai sekaligus dan saling bergantung. Ketika aplikasi pertama diunduh ke klien, pustaka runtime dan inti akan disimpan dalam cache browser. Dan karena Odnoklassniki adalah situs, tab yang dengannya pengguna dapat membuka "selamanya", berkeliaran akan terjadi sangat jarang. Dalam kebanyakan kasus, dengan rilis versi baru situs, hanya kode aplikasi yang akan diperbarui.

Pengiriman Sumberdaya


Pertimbangkan masalah dengan contoh bekerja dengan teks-teks lokal yang disimpan dalam database terpisah.

Jika sebelumnya di suatu tempat di server Anda membutuhkan teks dalam komponen, Anda bisa memanggil fungsi untuk mendapatkan teks.

const pkg = l10n('smiles');

<div>
    : { pkg.getText('title') }
</div>


Mendapatkan teks di server tidak sulit, karena aplikasi server dapat membuat permintaan cepat ke database atau bahkan men-cache semua teks dalam memori.

Bagaimana cara mendapatkan teks dalam komponen pada reaksi yang diberikan pada server di GraalVM?

Seperti yang telah dibahas di bagian pertama artikel, dalam konteks JS, Anda dapat menambahkan metode ke objek global yang ingin Anda akses dari JavaScript. Diputuskan untuk membuat kelas dengan semua metode yang tersedia untuk JavaScript.

public class ServerMethods {
    
    /**
     *     
     */
    public String getText(String pkg, String key) {
    }
    
}


Kemudian letakkan instance dari kelas ini dalam konteks JavaScript global:

//     Java   
js.putMember("serverMethods", serverMethods);


Akibatnya, dari JavaScript dalam implementasi server, kami cukup memanggil fungsi:

function getText(pkg: string, key: string): string {
    return global.serverMethods.getText(pkg, key);
}


Bahkan, ini akan menjadi pemanggilan fungsi di Jawa yang akan mengembalikan teks yang diminta. Interaksi sinkron langsung dan tidak ada panggilan HTTP.

Pada klien, sayangnya, dibutuhkan waktu yang sangat lama untuk membahas HTTP dan menerima teks untuk setiap panggilan ke fungsi penyisipan teks dalam komponen. Anda dapat mengunduh terlebih dahulu semua teks ke klien, tetapi teks itu sendiri berbobot puluhan megabita, dan ada jenis sumber daya lainnya.



Pengguna akan bosan menunggu sampai semuanya diunduh sebelum memulai aplikasi. Karena itu, metode ini tidak cocok.

Saya hanya ingin menerima teks-teks yang diperlukan dalam aplikasi tertentu. Teks kami dipecah menjadi beberapa paket. Oleh karena itu, Anda dapat mengumpulkan paket-paket yang diperlukan untuk aplikasi dan mengunduhnya bersama bundel. Saat aplikasi dimulai, semua teks sudah akan berada di cache klien.

Bagaimana cara mengetahui teks mana yang dibutuhkan aplikasi?

Kami menandatangani perjanjian bahwa paket teks dalam kode diperoleh dengan memanggil fungsi l10n (), di mana nama paket ditransmisikan HANYA dalam bentuk string literal:

const pkg = l10n('smiles');

<div>
    { pkg.getLMsg('title') }
</div>


Kami menulis sebuah plugin webpack yang, dengan menganalisis pohon kode komponen AST, menemukan semua panggilan ke fungsi l10n () dan mengumpulkan nama paket dari argumen. Demikian pula, plugin mengumpulkan informasi tentang jenis sumber daya lain yang dibutuhkan oleh aplikasi.

Pada output setelah perakitan untuk setiap aplikasi, kami mendapatkan konfigurasi dengan sumber dayanya:

{
    "events-calendar": {
       "pkg":  [
           "calendar",
           "dates"
       ],
       "cfg":  [
           "config1",
           "config2"
       ],
       "bundleName":  "events-calendar",
       "js":  "events-calendar.js",
       "css":  "events-calendar.css",
    }
}


Dan tentu saja, kita tidak boleh lupa tentang memperbarui teks. Karena di server semua teks selalu mutakhir, dan klien memerlukan mekanisme pembaruan cache yang terpisah, misalnya, pengamat atau pendorong.

Kode lama dalam baru


Dengan transisi yang mulus, timbul masalah untuk menggunakan kembali kode lama dalam komponen baru, karena ada komponen besar dan kompleks (misalnya, pemutar video), menulis ulang yang akan memakan banyak waktu, dan Anda harus menggunakannya dalam tumpukan baru sekarang.



Apa masalahnya?

  • Situs lama dan aplikasi Bereaksi baru memiliki siklus hidup yang sangat berbeda.
  • Jika Anda menempelkan kode sampel lama di dalam aplikasi Bereaksi, maka kode ini tidak akan memulai, karena Bereaksi tidak tahu cara mengaktifkannya.
  • Karena siklus hidup yang berbeda, Bereaksi dan mesin yang lama mungkin secara bersamaan mencoba untuk mengubah isi kode lama, yang dapat menyebabkan efek samping yang tidak menyenangkan.


Untuk mengatasi masalah ini, kelas dasar umum dialokasikan untuk komponen yang berisi kode lama. Kelas memungkinkan pewaris untuk mengoordinasikan siklus hidup aplikasi Bereaksi dan gaya lama.

export class OldCodeBase<T> extends React.Component<T> {

    ref: React.RefObject<HTMLElement> = React.createRef();

    componentDidMount() {
        //       DOM
        this.props.activate(this.ref.current!); 
    }

    componentWillUnmount() {
        //       DOM
        this.props.deactivate(this.ref.current!); 
    }

    shouldComponentUpdate() {
        // React     , 
        //   React-. 
        //     .
        return false;
    }

    render() {
        return (
            <div ref={this.ref}></div>
        );
    }
}


Kelas memungkinkan Anda untuk membuat potongan kode yang bekerja dengan cara lama, atau menghancurkan, sementara tidak akan ada interaksi simultan dengan mereka.

Rekatkan kode lama di server


Dalam praktiknya, ada kebutuhan untuk komponen pembungkus (misalnya, pop-up), yang isinya bisa apa saja, termasuk yang dibuat menggunakan teknologi lama. Anda perlu mengetahui cara menanamkan kode apa pun di server di dalam komponen tersebut.

Dalam artikel sebelumnya, kami berbicara tentang menggunakan atribut untuk meneruskan parameter ke komponen baru di klien dan server.

<cool-app users="[1,2,3]" />


Dan sekarang kita masih ingin menyisipkan sepotong markup di sana, yang artinya bukan atribut. Untuk ini, diputuskan untuk menggunakan sistem slot.

<cool-app>
    <ui:part id="old-code">
        <div>old component</div>
    </ui:part>
</cool-app>


Seperti yang Anda lihat dalam contoh di atas, di dalam kode komponen aplikasi keren, slot kode lama yang mengandung komponen lama dijelaskan. Kemudian, di dalam komponen reaksi, tempat di mana Anda ingin menempelkan isi slot ini ditunjukkan:

render() {
    return (
        <div>
            <UiPart id="old-code" />
        </div>
    );
}


Mesin server membuat komponen reaksi ini dan membingkai konten slot di tag <ui-part>, menetapkan atribut data-part-id = "old-code" ke dalamnya.

<cool-app>
    <div>
        <ui-part data-part-id="old-code">
            old code
        </ui-part>
    </div>
</cool-app>


Jika rendering sisi server JS di GraalVM tidak cocok dengan batas waktu, maka kami melakukan fallback ke rendering klien. Untuk melakukan ini, mesin di server hanya memberikan slot, membingkai mereka di tag templat sehingga browser tidak berinteraksi dengan kode mereka.

<cool-app>
    <template>
        <ui-part data-part-id="old-code">
            old code
        </ui-part>
    </template>
</cool-app>


Apa yang terjadi pada klien? Mesin klien hanya memindai kode komponen, mengumpulkan tag <ui-part>, menerima isinya dalam bentuk string, dan meneruskannya ke fungsi rendering bersama dengan parameter lainnya.

var tagName = 'cool-app';
var reactComponent = components[tagName];
reactComponent.render({
       tagName: tagName,
       attrs: attrs,
       parts: parts,
       node: element
});


Kode komponen yang memasukkan slot ke lokasi yang diinginkan adalah sebagai berikut:

export class UiPart extends OldCodeBase<IProps> {

	render() {
		const id = this.props.id;
		const parts = this.props.parts;

		if (!parts.hasOwnProperty(id)) {
			return null;
		}

		return React.createElement('ui-part', {
			'data-part-id': id,
			ref: this.ref,
			dangerouslySetInnerHTML: { __html: parts[id] }
		});
	}
}


Pada saat yang sama, ini diwarisi dari kelas OldCodeBase, yang memecahkan masalah interaksi antara tumpukan yang lama dan yang baru.



Sekarang Anda dapat menulis pop-up dan mengisinya menggunakan tumpukan baru atau permintaan dari server menggunakan pendekatan lama. Dalam hal ini, komponen akan berfungsi dengan benar.

Ini memungkinkan Anda untuk secara bertahap memigrasi komponen situs ke tumpukan baru.
Hanya ini adalah salah satu persyaratan utama untuk frontend baru.

Ringkasan


Semua orang bertanya-tanya seberapa cepat GraalVM bekerja. Pengembang Odnoklassniki melakukan berbagai tes dengan Bereaksi aplikasi.

Fungsi sederhana yang mengembalikan string setelah pemanasan membutuhkan waktu sekitar 1 mikrodetik.

Komponen (lagi setelah pemanasan) - dari 0,5 hingga 6 milidetik, tergantung pada ukurannya.

GraalVM berakselerasi lebih lambat dari V8. Tetapi untuk saat pemanasannya, situasinya menjadi lancar berkat kemunduran rendering klien. Karena ada begitu banyak pengguna, mesin virtual memanas dengan cepat.

Apa yang berhasil Anda lakukan



  • Jalankan JavaScript di server di dunia Java Classmates.
  • Buat kode isomorfik untuk UI.
  • Gunakan tumpukan modern yang diketahui semua vendor front-end.
  • Buat platform umum dan pendekatan tunggal untuk menulis UI.
  • Mulai transisi yang lancar tanpa mempersulit operasi dan tidak memperlambat rendering server.


Kami berharap pengalaman Odnoklassniki dan contoh-contohnya akan bermanfaat bagi Anda dan Anda akan menemukannya untuk digunakan dalam pekerjaan Anda.

Source: https://habr.com/ru/post/undefined/


All Articles