TL; DR
- Artikel ini tidak membahas kerangka kerja JS dari daftar TOP-3.
- Ketika mengembangkan kerangka JS non-TOP-3, Anda harus menyelesaikan urutan besarnya lebih banyak masalah teknis daripada yang diharapkan pada awal pengembangan
- Kisah ini didasarkan pada peristiwa nyata.
Cerita dimulai dengan satu proyek kecil, yang awalnya dikembangkan berdasarkan perpustakaan backbone.js dan marionette.js. Ini, tentu saja, adalah perpustakaan hebat yang memulai sejarah pengembangan aplikasi satu halaman. Tetapi pada saat itu mereka lebih cenderung memiliki nilai historis daripada nilai praktis. Saya hanya akan menyebutkan fakta bahwa untuk menampilkan tabel sederhana perlu dibuat: 1) modul dengan deskripsi model, 2) modul dengan deskripsi koleksi, 3) modul dengan definisi model tampilan, 4) modul dengan definisi koleksi tampilan, 4) template baris tabel, 5) tabel templat; 6) modul pengontrol. Memiliki sekitar 10 entitas dalam aplikasi kecil - pada tahap awal Anda sudah memiliki lebih dari lima puluh modul kecil. Dan ini baru permulaan. Tapi sekarang bukan tentang itu.
Pada titik tertentu, setelah enam bulan aplikasi, itu masih tidak muncul di hasil pencarian. Menambahkan
prerender.io ke proyek (yang kemudian menggunakan mesin phantom.js) membantu, tetapi tidak sepenting yang diharapkan. Awan mulai berkumpul di atas aplikasi dan di atas saya, setelah itu saya menyadari bahwa saya perlu melakukan sesuatu dengan sangat cepat dan efisien, lebih disukai hari ini. Tujuan yang saya tetapkan adalah ini: beralih ke rendering server. Dengan backbone.js dan marionettejs, ini hampir mustahil. Bagaimanapun, proyek rendr di backbone.js, yang dikembangkan di bawah arahan Spike Brehm (penulis gagasan aplikasi isomorfik / universal), mengumpulkan 58 kontributor dan 4.184 suka di github.com, dihentikan pada 2015, dan jelas tidak dimaksudkan untuk blitz satu hari . Saya mulai mencari alternatif. Saya mengecualikan kerangka JS TOP-3 dari pertimbangan segera, karena saya tidak punya cadangan waktu untuk pengembangan mereka. Setelah pencarian singkat, saya menemukan kerangka kerja JS yang berkembang pesat, riot.js
github.com/riot/riot , yang saat ini memiliki 13.704 suka di github.com, dan, seperti yang saya harapkan, itu bisa saja mencapai yang pertama posisi (yang, bagaimanapun, tidak terjadi).
Dan ya, saya menutup pertanyaan (meskipun tidak pada hari yang sama, tetapi untuk dua hari berikutnya dalam sehari) dengan mentransfer aplikasi ke server rendering menggunakan riot.js. Dan pada hari yang sama ketika ini terjadi, situs tersebut pindah ke sepuluh halaman pertama hasil pencarian. Dan dalam seminggu saya pergi ke halaman pertama, dari tempat itu tidak pergi selama beberapa tahun.
Ini mengakhiri kisah sukses dan memulai sejarah kekalahan. Proyek selanjutnya jauh lebih rumit. Benar, ada poin positif yaitu 99% layar aplikasi ada di akun pribadi pengguna, jadi tidak perlu rendering server. Terinspirasi oleh pengalaman sukses pertama menggunakan riot.js, saya mulai mempromosikan gagasan untuk mengkonsolidasikan kesuksesan dan menerapkan riot.js di frontend. Kemudian bagi saya tampaknya, akhirnya, sebuah solusi ditemukan yang menggabungkan kesederhanaan dan fungsionalitas, seperti yang dijanjikan oleh dokumentasi riot.js. Betapa salahnya aku!
Masalah pertama yang saya temui ketika perlu menyediakan desainer tata letak HTML dengan semua alat pengembangan yang diperlukan. Secara khusus, kami membutuhkan plug-in untuk editor kode, sebuah mesin yang memungkinkan untuk menempatkan komponen dan segera mengamati hasilnya, termasuk dengan kelebihan beban komponen (hot-reload). Semua ini harus diberikan dalam bentuk siap untuk operasi industri dalam waktu dekat, dan semua ini tidak. Akibatnya, tata letak aplikasi dimulai pada salah satu mesin templat tradisional, yang akibatnya menyebabkan tahap tidak berterima kasih menerjemahkan dokumen HTML ke riot.js.
Namun, masalah utama yang muncul pada tahap ini bahkan tidak terkait dengan terjemahan tata letak dari format template ke komponen riot.js. Tiba-tiba, ternyata riot.js memberikan pesan yang benar-benar tidak informatif tentang kesalahan kompilasi template, serta kesalahan runtime (ini berlaku untuk semua versi riot.js hingga versi 4.0, yang telah sepenuhnya didesain ulang). Tidak ada informasi tidak hanya tentang baris di mana kesalahan terjadi, tetapi bahkan tentang nama file atau komponen di mana kesalahan terjadi. Dimungkinkan untuk mencari kesalahan selama berjam-jam berturut-turut, dan tetap saja itu tidak dapat ditemukan. Dan kemudian saya harus memutar kembali semua perubahan ke kondisi kerja terakhir.
Berikut ini adalah masalah dengan perutean. Routing di riot.js hampir keluar dari kotak
github.com/riot/route - setidaknya dari pengembang yang sama. Ini membuatnya berharap operasi bebas masalah. Tetapi pada titik tertentu saya perhatikan bahwa beberapa halaman kelebihan beban tidak terduga. Artinya, suatu kali transisi ke rute baru dapat terjadi dalam mode aplikasi satu halaman, dan lain kali transisi yang sama membebani seluruh dokumen HTML, seperti ketika bekerja dengan aplikasi web klasik. Dalam kasus ini, tentu saja, keadaan internal hilang jika belum disimpan di server. (Saat ini, pengembangan perpustakaan ini dihentikan dan tidak digunakan dengan riot.js 4.0.)
Satu-satunya komponen sistem yang berfungsi seperti yang diharapkan adalah state manager minim flux-like
github.com/jimsparkman/RiotControl . Benar, untuk bekerja dengan komponen ini, perlu menunjuk dan membatalkan pendengar perubahan negara lebih sering daripada yang kita inginkan.
Ide awal dari artikel ini adalah ini: untuk menunjukkan dengan contoh pengalaman kami sendiri dengan kerangka kerja kerusuhan.js tugas-tugas yang harus diselesaikan oleh pengembang, yang memutuskan (memutuskan) untuk mengembangkan aplikasi pada kerangka kerja JS bukan dari daftar TOP-3. Namun, dalam proses persiapan, saya memutuskan untuk me-refresh beberapa halaman dari dokumentasi riot.js di ingatan saya, dan jadi saya belajar bahwa versi baru riot.js 4.0 dirilis, yang sepenuhnya (dari awal) didesain ulang, yang dapat ditemukan di artikel pengembang kerusuhan. js di medium.com:
medium.com/@gianluca.guarini/every-revolution-begins-with-a-riot-js-first-6c6a4b090ee . Dari artikel ini saya belajar bahwa semua masalah utama yang membuat saya khawatir, dan yang akan saya bicarakan dalam artikel ini, telah dieliminasi. Secara khusus, dalam riot.js versi 4.0:
- kompiler sepenuhnya ditulis ulang (atau lebih tepatnya, itu pertama kali ditulis karena mesin yang digunakan untuk bekerja pada ekspresi reguler sebelumnya) - ini mempengaruhi, khususnya, isi informasi dari pesan kesalahan
- selain rendering server, hidrat ditambahkan pada klien - ini akhirnya memungkinkan kami untuk mulai menulis aplikasi universal tanpa rendering ganda (pertama kali di server dan di sana pada klien karena kurangnya fungsi hydrate () dalam versi yang lebih lama)
- menambahkan plugin untuk komponen overloading panas github.com/riot/hot-reload
- dan banyak perubahan bermanfaat lainnya
Ini bukan iklan, hanya latar belakang. Bahkan, kesimpulan saya akan sangat ambigu dalam hal rekomendasi untuk digunakan, dan artikel secara umum tidak dikhususkan untuk kerangka kerja atau perpustakaan tertentu, tetapi untuk tugas-tugas yang perlu diselesaikan selama proses pengembangan.
Sayangnya, pekerjaan yang dilakukan oleh pengembang riot.js belum dievaluasi dengan baik oleh komunitas. Misalnya, server render perpustakaan
github.com/riot/ssr selama enam bulan yang telah berlalu sejak awal pengembangannya telah mengumpulkan tiga ko-kontributor dan tiga suka di github.com (tidak semua suka dibuat oleh kontributor, meskipun satu suka sama dengan kontributor).
Oleh karena itu, dalam perjalanan lakon itu, dia mengubah arah artikel, dan bukannya memoir, dia mencoba untuk melakukan semua jalan lagi, memiliki sedikit lebih banyak pengetahuan dan pengalaman, versi perpustakaan yang lebih maju dan waktu luang tanpa batas.
Jadi di sini kita mulai. Misalnya, implementasi aplikasi
github.com/gothinkster/realworld dibuat. Proyek ini telah dibahas lebih dari sekali tentang Habré. Bagi mereka yang tidak mengenalnya, saya akan menjelaskan idenya secara singkat. Pengembang proyek ini dengan bantuan bahasa dan kerangka kerja yang berbeda (atau tanpa mereka) memecahkan masalah yang sama: pengembangan mesin blog yang terlihat seperti versi sederhana dari medium.com dalam fungsi. Ini adalah kompromi antara kompleksitas aplikasi nyata yang harus kita kembangkan setiap hari dan todo.app, yang tidak selalu memungkinkan kita untuk benar-benar mengevaluasi pekerjaan dengan pustaka atau kerangka kerja. Proyek ini dihormati oleh pengembang. Untuk mendukung hal di atas, saya dapat mengatakan bahwa bahkan ada satu implementasi dari Rich Harris (pengembang utama sveltejs)
github.com/sveltejs/realworld .
Lingkungan pengembangan
Tentu saja Anda siap untuk berperang, tetapi pikirkan tentang pengembang di sekitar Anda. Dalam hal ini, fokuslah pada pertanyaan di mana lingkungan pengembangan rekan kerja Anda. Jika tidak ada plugin untuk lingkungan pengembangan utama dan editor kode program untuk kerangka kerja yang akan Anda gunakan, maka Anda tidak akan didukung. Sebagai contoh, saya menggunakan editor Atom untuk pengembangan. Baginya ada plugin riot-tag
github.com/riot/syntax-highlight/tree/legacy , yang belum diperbarui selama tiga tahun terakhir. Dan di repositori yang sama ada sebuah plugin untuk
github.com/riot/syntax-highlight yang sublim - ini saat ini dan mendukung versi saat ini riot.js 4.0.
Namun, komponen riot.js adalah fragmen yang valid dari dokumen HTML di mana kode JS terkandung dalam tubuh elemen skrip. Jadi semuanya hanya berfungsi jika Anda menambahkan tipe dokumen html untuk ekstensi * .riot. Tentu saja, ini adalah keputusan yang dipaksakan, karena kalau tidak, mustahil untuk melanjutkan lebih jauh di sini.
Kami mendapat sorotan sintaksis dalam editor teks, dan sekarang kami membutuhkan fungsionalitas yang lebih maju, yang biasa kami peroleh dari eslint. Dalam kasus kami, komponen kode JS terdapat di badan elemen skrip, saya berharap menemukan dan menemukan plug-in untuk mengekstraksi kode JS dari dokumen HTML -
github.com/BenoitZugmeyer/eslint-plugin-html . Setelah itu, konfigurasi eslint saya mulai terlihat seperti ini:
{ "parser": "babel-eslint", "plugins": [ "html" ], "settings": { "html/html-extensions": [".html", ".riot"] }, "env": { "browser": true, "node": true, "es6": true }, "extends": "standard", "globals": { "Atomics": "readonly", "SharedArrayBuffer": "readonly" }, "parserOptions": { "ecmaVersion": 2018, "sourceType": "module" }, "rules": { } }
Kehadiran plug-in untuk penyorotan sintaks dan eslint mungkin bukan hal pertama yang mulai dipikirkan pengembang ketika memilih kerangka kerja JS. Sementara itu, tanpa alat-alat ini, Anda mungkin menghadapi oposisi dari kolega dan eksodus massal mereka untuk alasan "baik" dari proyek. Meskipun satu-satunya alasan yang benar-benar valid adalah mereka tidak nyaman bekerja tanpa arsenal pengembang penuh. Dalam kasus riot.js, masalah diselesaikan dengan metode Columbus. Dalam arti bahwa sebenarnya tidak ada plugin untuk riot.js, tetapi karena kekhasan sintaks templat riot.js, yang terlihat seperti sebuah fragmen dari dokumen HTML biasa, kami mencakup 99% fungsionalitas yang diperlukan menggunakan alat untuk bekerja dengan dokumen HTML.
Selain sarana untuk menulis dan memvalidasi kode yang baru saja kami periksa, alat pengembang mencakup alat untuk perakitan cepat dan debugging proyek, pemuatan ulang komponen dan modul di browser web saat membuat perubahan pada proyek. Kami akan mempertimbangkan bagian ini di bagian selanjutnya.
Perakitan proyek
Kami berhasil membiasakan diri dengan sebagian besar fitur yang diperlukan ketika membangun proyek, dan bahkan berhenti memikirkan apa yang mungkin berbeda. Tapi bisa jadi sebaliknya. Dan, jika Anda telah memilih kerangka kerja JS baru, disarankan untuk terlebih dahulu memastikan bahwa semuanya berfungsi seperti yang Anda harapkan. Sebagai contoh, seperti yang telah saya sebutkan, masalah terbesar ketika mengembangkan versi riot.js yang lebih lama adalah kurangnya kompilasi pesan kesalahan dan informasi runtime tentang komponen di mana kesalahan ini terjadi. Juga penting adalah kecepatan kompilasi. Sebagai aturan, untuk mempercepat kecepatan kompilasi, dalam kerangka kerja yang dibangun dengan benar hanya bagian yang diubah yang dikompilasi ulang, sebagai akibatnya, waktu reaksi terhadap perubahan teks komponen minimal. Ya, sangat bagus jika memuat ulang komponen yang panas didukung tanpa memuat ulang penuh laman di peramban web.
Oleh karena itu, saya akan mencoba mendaftar daftar periksa, apa yang perlu Anda perhatikan ketika menganalisis alat membangun proyek:
1. Kehadiran mode pengembangan dan aplikasi yang berfungsi
Dalam mode pengembang:
2. Pesan informatif tentang kesalahan kompilasi proyek (nama file sumber, nomor baris dalam file sumber, deskripsi kesalahan)
3. Pesan informatif tentang kesalahan runtime (nama file sumber, nomor baris dalam file sumber, deskripsi kesalahan)
4. Pemasangan kembali modul yang dimodifikasi dengan cepat
5. Panas kelebihan komponen di browser
Dalam mode operasi:
6. Kehadiran versi dalam nama file (misalnya 4a8ee185040ac59496a2.main.js)
7. Tata letak modul kecil dalam satu atau lebih modul (potongan)
8. Pemecah kode menjadi potongan menggunakan impor dinamis
Dalam riot.js versi 4.0, modul
github.com/riot/webpack-loader muncul, yang sepenuhnya sesuai dengan daftar periksa yang diberikan. Saya tidak akan mencantumkan semua fitur konfigurasi perakitan. Satu-satunya hal yang saya ingin menarik perhatian Anda adalah bahwa dalam proyek tersebut saya menggunakan modul untuk express.js: webpack-dev-middleware dan webpack-hot-middleware, yang memungkinkan Anda untuk bekerja pada server yang berfungsi penuh segera, dari saat tata letak. Ini, khususnya, memungkinkan pengembangan aplikasi web universal / isomorfik. Saya menarik perhatian Anda pada fakta bahwa modul kelebihan modul hanya valid untuk browser web. Pada saat yang sama, komponen yang diberikan di sisi server tetap tidak berubah. Oleh karena itu, perlu untuk mendengarkan perubahannya, dan pada saat yang tepat, hapus semua kode yang di-cache oleh server dan muat kode dari modul yang diubah. Cara melakukan ini untuk waktu yang lama, jadi saya hanya akan memberikan tautan ke implementasinya:
github.com/apapacy/realworld-riotjs-effector-universal-hot/blob/master/src/dev_server.jsRouting
Mengutip Leo Tolstoy sedikit, kita dapat mengatakan bahwa semua mesin kerangka JS mirip satu sama lain, sementara semua perutean yang melekat padanya bekerja dengan cara mereka sendiri. Saya sering menemukan klasifikasi kondisional dari router menjadi dua jenis: deklaratif dan imperatif. Sekarang saya akan mencoba mencari tahu bagaimana klasifikasi seperti itu dibenarkan.
Mari kita bertamasya singkat ke dalam sejarah. Pada awal Internet, URL / URI cocok dengan nama file yang dihosting di server. Kami membuka beberapa halaman sejarah sekaligus dan kami akan belajar tentang kedatangan Arsitektur Model 2 (MVC). Dalam arsitektur ini, pengontrol depan muncul, yang melakukan fungsi routing. Saya bertanya-tanya siapa yang pertama kali memutuskan dari pengontrol depan untuk memilih fungsi perutean di blok terpisah, yang kemudian mengirim permintaan ke salah satu dari banyak pengontrol dan belum menemukan jawaban. Tampaknya mereka mulai melakukannya sekaligus.
Yaitu, routing menentukan tindakan yang harus dilakukan pada server dan (secara transitif melalui controller) tampilan yang akan dihasilkan oleh controller. Saat mentransfer perutean ke sisi klien (browser web), ide fungsi perutean dalam aplikasi sudah terbentuk. Dan mereka yang terutama berfokus pada fakta bahwa perutean menentukan tindakan, mengembangkan perutean imperatif, dan mereka yang memperhatikan bahwa, pada akhirnya, perutean menentukan pandangan yang harus ditunjukkan kepada pengguna, mengembangkan perutean deklaratif.
Yaitu, ketika mentransfer dari server ke klien untuk perutean, mereka "menggantung" dua fungsi yang menjadi ciri perutean server (memilih tindakan dan memilih tampilan). Selain itu, tugas baru telah muncul - ini adalah navigasi melalui aplikasi satu halaman tanpa sepenuhnya memuat ulang dokumen HTML, bekerja dengan sejarah kunjungan dan banyak lagi. Sebagai ilustrasi, saya akan memberikan kutipan dari dokumentasi router dari satu framework mega-popular:
... membuatnya mudah untuk membuat aplikasi SPA. Termasuk fitur-fitur berikut
- Rute / Tampilan Bersarang
- Konfigurasi Router Modular
- Akses ke parameter rute, kueri, wildcard
- Lihat animasi transisi berdasarkan Vue.js
- Kontrol navigasi yang nyaman
- Pembubuhan otomatis kelas CSS aktif untuk tautan
- Mode atau hash riwayat HTML5, dengan sakelar otomatis di IE9
- Perilaku gulir khusus
Dalam opsi ini, perutean jelas kelebihan beban dengan fungsi dan perlu memikirkan kembali tugasnya di sisi klien. Saya mulai mencari solusi yang cocok untuk tugas saya. Sebagai kriteria utama, saya memperhitungkan perutean itu:
- harus bekerja sama pada sisi klien web dan sisi server web untuk aplikasi web universal / isomorfik;
- harus bekerja dengan kerangka kerja apa pun (termasuk yang saya pilih) atau tanpa itu.
Dan saya menemukan perpustakaan seperti itu, ini
github.com/kriasoft/universal-router . Jika kami menjelaskan secara singkat ide perpustakaan ini, ia mengkonfigurasi rute yang mengambil string URL pada input dan memanggil fungsi asinkron pada output, yang meneruskan URL yang diuraikan sebagai parameter aktual. Jujur, saya ingin bertanya: apakah hanya itu? Dan bagaimana seharusnya semua orang bekerja dengan ini? Dan kemudian saya menemukan artikel di medium.com
medium.com/@ippei.tanaka/universal-router-history-react-97ec79464573 , di mana opsi yang cukup baik diusulkan, dengan pengecualian mungkin menulis ulang metode sejarah push (), yang tidak Saya perlu dan yang saya hapus dari kode saya. Akibatnya, operasi router di sisi klien didefinisikan kira-kira seperti ini:
const routes = new UniversalRouter([ { path: '/sign-in', action: () => ({ page: 'login', data: { action: 'sign-in' } }) }, { path: '/sign-up', action: () => ({ page: 'login', data: { action: 'sign-up' } }) }, { path: '/', action: (req) => ({ page: 'home', data: { req, action: 'home' } }) }, { path: '/page/:page', action: (req) => ({ page: 'home', data: { req, action: 'home' } }) }, { path: '/feed', action: (req) => ({ page: 'home', data: { req, action: 'feed' } }) }, { path: '/feed/page/:page', action: (req) => ({ page: 'home', data: { req, action: 'feed' } }) }, ... { path: '(.*)', action: () => ({ page: 'notFound', data: { action: 'not-found' } }) } ]) const root = getRootComponent() const history = createBrowserHistory() const render = async (location) => { const route = await router.resolve(location) const component = await import(`./riot/pages/${route.page}.riot`) riot.register(route.page, component.default || component) root.update(route, root) } history.listen(render)
Sekarang panggilan ke history.push () akan memulai perutean. Untuk bernavigasi di dalam aplikasi, Anda juga perlu membuat komponen yang membungkus elemen HTML standar a (jangkar), tidak lupa untuk membatalkan perilaku standarnya:
<navigation-link href={ props.href } onclick={ action }> <slot/> <script> import history from '../history' export default { action (e) { e.preventDefault() history.push(this.props.href) if (this.props.onclick) { this.props.onclick.call(this, e) } e.stopPropagation() } } </script> </navigation-link>
Manajemen status aplikasi
Awalnya, saya menyertakan perpustakaan mobx dalam proyek. Semuanya bekerja seperti yang diharapkan. Kecuali bahwa itu tidak cukup sesuai dengan tugas - studi yang saya atur di awal artikel. Jadi saya beralih ke
github.com/zerobias/effector . Ini adalah proyek yang sangat kuat. Ini memberikan 100% fungsionalitas redux (hanya tanpa overhead besar) dan fungsionalitas 100% mobx (walaupun dalam hal ini akan diperlukan untuk menyandikan lebih sedikit, tetapi masih lebih sedikit jika dibandingkan dengan mobx tanpa dekorator)
Deskripsi toko terlihat seperti ini:
import { createStore, createEvent } from 'effector' import { request } from '../agent' import { parseError } from '../utils' export default class ProfileStore { get store () { return this.profileStore.getState() } constructor () { this.success = createEvent() this.error = createEvent() this.updateError = createEvent() this.init = createEvent() this.profileStore = createStore(null) .on(this.init, (state, store) => ({ ...store })) .on(this.success, (state, data) => ({ data })) .on(this.error, (state, error) => ({ error })) .on(this.updateError, (state, error) => ({ ...state, error })) } getProfile ({ req, author }) { return request(req, { method: 'get', url: `/profiles/${decodeURIComponent(author)}` }).then( response => this.success(response.data.profile), error => this.error(parseError(error)) ) } follow ({ author, method }) { return request(undefined, { method, url: `/profiles/${author}/follow` }).then( response => this.success(response.data.profile), error => this.error(parseError(error)) ) } }
Perpustakaan ini menggunakan reduksi penuh (mereka disebut dalam dokumentasi efektor.js), yang banyak orang kekurangan dalam mobx, tetapi dengan upaya pengkodean yang jauh lebih sedikit daripada redux. Tapi yang utama bukan itu. Setelah menerima 100% fungsionalitas redux dan mobx, saya hanya menggunakan sepersepuluh dari fungsionalitas yang melekat pada effector.js. Dari mana kita dapat menyimpulkan bahwa penggunaannya dalam proyek-proyek kompleks dapat secara signifikan memperkaya dana pengembang.
Pengujian
TodoKesimpulan
Jadi, pekerjaannya selesai. Hasilnya disajikan dalam repositori
github.com/apapacy/realworld-riotjs-effector-universal-hot dan dalam artikel ini tentang Habré.
Situs demo di sekarang
realworld-riot-effector-universal-hot-pnujtmugam.now.shDan pada akhirnya saya akan membagikan kesan saya tentang perkembangan tersebut. Mengembangkan riot.js versi 4.0 cukup nyaman. Banyak konstruk lebih mudah ditulis daripada di Bereaksi. Butuh waktu tepat dua minggu untuk berkembang tanpa fanatisme di jam-jam sesudahnya dan di akhir pekan. Tapi ... Satu kecil tapi ... Keajaiban itu tidak terjadi lagi. Perenderan server di Bereaksi adalah 20-30 kali lebih cepat. Perusahaan menang lagi. Namun, dua perutean menarik dan perpustakaan manajer negara diuji.
apapacy@gmail.com
17 Juni 2019