Prolog
Saat ini, saya sedang mengembangkan editor skema Javascript, dan dalam proses pekerjaan ini, saya menemukan masalah yang akan menjadi fokus artikel ini, yaitu serialisasi dan deserialisasi objek data yang kompleks.
Tanpa membahas detail proyek, saya perhatikan bahwa menurut ide saya, skema adalah array elemen (simpul) yang diwarisi dari kelas dasar. Dengan demikian, setiap kelas anak mengimplementasikan logikanya sendiri. Selain itu, simpul berisi tautan satu sama lain (panah), yang juga perlu dipertahankan. Secara teoritis, simpul dapat merujuk ke diri mereka sendiri secara langsung atau melalui simpul lainnya. Standard JSON.stringify tidak dapat membuat serialisasi array seperti itu, jadi saya memutuskan untuk membuat serializer sendiri yang memecahkan dua masalah yang dijelaskan:
- Kemampuan untuk menyimpan informasi kelas selama serialisasi dan mengembalikannya selama deserialisasi.
- Kemampuan untuk menyimpan dan mengembalikan tautan ke objek, termasuk siklik.
Baca lebih lanjut tentang pernyataan masalah dan solusinya di bawah potongan.
Proyek serializer Github
Tautan ke proyek github: tautan .
Contoh kompleks juga ada di folder test-src .
Serializer rekursif: tautan .
Serializer datar: tautan .
Pernyataan masalah
Seperti yang sudah saya catat, tugas awalnya adalah membuat serial rangkaian arbitrer untuk editor. Agar tidak membuang waktu menggambarkan editor, kami mengatur tugas lebih mudah. Misalkan kita ingin membuat deskripsi formal dari skema algoritma sederhana menggunakan kelas Javascript ES6, dan kemudian membuat serial dan deserialize skema ini.
Di Internet, saya menemukan gambar yang cocok dari algoritma paling sederhana untuk menentukan maksimum dua nilai:

Di sini saya harus mengatakan bahwa saya bukan pengembang Javascript, dan bahasa "asli" saya adalah C #, jadi pendekatan untuk menyelesaikan masalah ditentukan oleh pengalaman pengembangan berorientasi objek dalam C #. Melihat diagram ini, saya melihat simpul dari tipe berikut (nama kondisional dan peran khusus tidak dimainkan):
- Mulai simpul (Mulai)
- Puncak akhir (Selesai)
- Team Top (Command)
- Penugasan Vertex (Biarkan)
- Verifikasi Verifikasi Atas (Jika)
Verteks ini memiliki beberapa perbedaan dari satu sama lain dalam kumpulan data atau semantik mereka, tetapi mereka semua diwarisi dari basis simpul (Node). Di tempat yang sama, di kelas Node, bidang tautan dijelaskan, yang berisi tautan ke simpul lain, dan metode addLink memungkinkan tautan ini ditambahkan. Kode lengkap dari semua kelas dapat ditemukan di sini .
Mari kita menulis kode yang mengumpulkan rangkaian dari gambar, dan mencoba membuat serialisasi hasilnya.
Jika kami membuat serialisasi skema ini menggunakan JSON.stringify, kami mendapatkan sesuatu yang buruk. Saya akan memberikan beberapa baris pertama dari hasilnya, di mana saya menambahkan komentar saya:
JSON.stringify hasil [ { "id": "d9c8ab69-e4fa-4433-80bb-1cc7173024d6", "name": "Start", "links": { "2e3d482b-187f-4c96-95cd-b3cde9e55a43": { "id": "2e3d482b-187f-4c96-95cd-b3cde9e55a43", "target": { "id": "f87a3913-84b0-4b70-8927-6111c6628a1f", "name": "Command", "links": { "4f623116-1b70-42bf-8a47-da1e9be5e4b2": { "id": "4f623116-1b70-42bf-8a47-da1e9be5e4b2", "target": { "id": "94a47403-13ab-4c83-98fe-3b201744c8f2", "name": "If", "links": { ...
Karena verteks pertama berisi tautan ke yang kedua, dan ke yang berikutnya, kemudian sebagai akibat dari serialisasinya, seluruh rangkaian diserialisasi. Kemudian puncak kedua adalah serial dan segala sesuatu yang bergantung padanya, dan seterusnya. Anda dapat mengembalikan tautan asli dari hash ini hanya dengan pengidentifikasi, tetapi mereka tidak akan membantu jika salah satu dari simpul merujuk ke dirinya sendiri secara langsung atau melalui simpul lainnya. Dalam hal ini, serializer akan membuang UnEught TypeError: Mengubah struktur lingkaran menjadi kesalahan JSON . Jika tidak jelas, maka di sini adalah contoh paling sederhana yang menghasilkan kesalahan ini: https://jsfiddle.net/L4guo86w/ .
Selain itu, JSON tidak mengandung informasi apa pun tentang kelas sumber, sehingga tidak ada cara untuk memahami tipe setiap titik sebelum serialisasi.
Menyadari masalah ini, saya online dan mulai mencari solusi yang sudah jadi. Ada banyak, tetapi sebagian besar sangat besar atau memerlukan deskripsi khusus tentang kelas serial, jadi diputuskan untuk membuat sepeda sendiri. Dan ya, saya suka sepeda.
Konsep Serializer
Bagian ini adalah untuk mereka yang ingin berpartisipasi dalam membuat algoritma serialisasi dengan saya, meskipun secara virtual.
Salah satu masalah dengan Javascript adalah kurangnya metadata yang dapat bekerja luar biasa dalam bahasa seperti C # atau Java (atribut dan refleksi). Di sisi lain, saya tidak perlu serialisasi super kompleks dengan kemampuan untuk mendefinisikan daftar bidang serializable, validasi dan chip lainnya. Oleh karena itu, ide utamanya adalah menambahkan informasi tentang tipenya ke objek dan membuat serial dengan JSON.stringify biasa.
Saat mencari solusi, saya menemukan sebuah artikel menarik yang judulnya diterjemahkan sebagai "6 Cara Salah untuk Menambahkan Jenis Informasi di JSON" . Sebenarnya, metodenya sangat bagus, dan saya memilih yang ada di nomor 5. Jika Anda terlalu malas untuk membaca artikel, tapi saya sangat menyarankan melakukannya, maka saya akan menjelaskan secara singkat metode ini: ketika membuat serial suatu objek, kami membungkusnya di objek lain dengan satu-satunya. bidang yang namanya dalam format "@<type>"
, dan nilainya adalah data objek. Selama deserialisasi, kami mengekstrak nama jenis, membuat ulang objek dari konstruktor, dan membaca data bidangnya.
Jika kami menghapus tautan dari contoh kami di atas, maka JSON standar.stringify data berseri seperti ini:
JSON.stringify [ { "id": "d04d6a58-7215-4102-aed0-32122e331cf4", "name": "Start", "links": {} }, { "id": "5c58c3fc-8ce1-45a5-9e44-90d5cebe11d3", "name": "Command", "links": {}, "command": " A, B" }, ... }
Dan serializer kami akan membungkusnya seperti ini:
Hasil serialisasi [ { "@Schema.Start": { "id": "d04d6a58-7215-4102-aed0-32122e331cf4", "name": "Start", "links": {} } }, { "@Schema.Command": { "id": "5c58c3fc-8ce1-45a5-9e44-90d5cebe11d3", "name": "Command", "links": {}, "command": " A, B" } }, ... }
Tentu saja, ada kekurangannya: serializer harus tahu tentang jenis-jenis yang dapat diserialisasi, dan objek itu sendiri tidak boleh berisi bidang yang namanya dimulai dengan anjing. Namun, masalah kedua diselesaikan dengan kesepakatan dengan pengembang atau dengan mengganti simbol anjing dengan sesuatu yang lain, dan masalah pertama diselesaikan dalam satu baris kode (di bawah ini akan menjadi contoh). Kami tahu persis apa yang akan kami serialkan, kan?
Memecahkan masalah tautan
Ini masih lebih sederhana dalam hal algoritma, tetapi lebih sulit untuk diimplementasikan.
Saat membuat serial dari kelas-kelas yang terdaftar dalam serializer, kami akan menyimpannya di cache dan memberikan nomor seri. Jika di masa depan kita bertemu contoh ini lagi, maka dalam definisi pertama kita akan menambahkan nomor ini (nama bidang akan mengambil bentuk "@<type>|<index>"
), dan di tempat serialisasi kita akan memasukkan tautan sebagai objek
{ "@<type>": <index> }
Jadi, selama deserialisasi, kita melihat apa sebenarnya nilai dari bidang tersebut. Jika ini adalah angka, maka kami mengekstrak objek dari cache dengan nomor ini. Kalau tidak, ini adalah definisi pertamanya.
Mari kembalikan tautan dari bagian atas skema ke yang kedua dan lihat hasilnya:
Hasil serialisasi [ { "@Schema.Start": { "id": "a26a3a29-9462-4c92-8d24-6a93dd5c819a", "name": "Start", "links": { "25fa2c44-0446-4471-a013-8b24ffb33bac": { "@Schema.Link": { "id": "25fa2c44-0446-4471-a013-8b24ffb33bac", "target": { "@Schema.Command|1": { "id": "4f4f5521-a2ee-4576-8aec-f61a08ed38dc", "name": "Command", "links": {}, "command": " A, B" } } } } } } }, { "@Schema.Command": 1 }, ... }
Sekilas tidak terlihat jelas, karena titik kedua didefinisikan pertama di dalam yang pertama di objek komunikasi Tautan, tetapi penting bahwa pendekatan ini bekerja. Selain itu, saya membuat versi kedua dari serializer, yang memotong pohon tidak di kedalaman, tetapi lebarnya, yang menghindari "tangga" seperti itu.
Buat serializer
Bagian ini ditujukan bagi mereka yang tertarik dalam mengimplementasikan ide-ide yang dijelaskan di atas.
Serializer kosong
Seperti yang lain, serializer kami akan memiliki dua metode utama - serialisasi dan deserialize. Selain itu, kita akan memerlukan metode yang memberi tahu serializer tentang kelas-kelas yang harus di-serialisasi (didaftarkan) dan kelas-kelas yang tidak boleh (abaikan). Yang terakhir ini diperlukan agar tidak membuat serial elemen DOM, objek JQuery, atau tipe data lainnya yang tidak dapat diserialisasi atau yang tidak perlu diserialisasi. Misalnya, di editor saya, saya menyimpan elemen visual yang sesuai dengan simpul atau tautan. Itu dibuat selama inisialisasi dan, tentu saja, tidak boleh jatuh ke dalam database.
Kode shell Serializer export default class Serializer { constructor() { this._nameToCtor = [];
Penjelasan
Untuk mendaftarkan kelas, Anda harus meneruskan konstruktornya ke metode pendaftaran dengan salah satu dari dua cara berikut:
- daftar (MyClass)
- daftar ('MyNamespace.MyClass', MyClass)
Dalam kasus pertama, nama kelas akan diekstraksi dari nama fungsi konstruktor (tidak didukung di IE), di saat Anda menentukan nama sendiri. Metode kedua lebih disukai, karena memungkinkan Anda untuk menggunakan ruang nama, dan yang pertama, dengan desain, dirancang untuk mendaftarkan tipe-tipe Javascript bawaan dengan logika serialisasi yang didefinisikan ulang.
Sebagai contoh kami, inisialisasi serializer adalah sebagai berikut:
import Schema from './schema'; ...
Objek Skema berisi deskripsi dari semua kelas simpul, sehingga kode pendaftaran kelas cocok menjadi satu baris.
Konteks serialisasi dan deserialisasi
Anda mungkin telah memperhatikan kelas SerializationContext dan DeserializationContext cryptic. Merekalah yang melakukan semua pekerjaan, dan dibutuhkan terutama untuk memisahkan data dari proses serialisasi / deserialisasi yang berbeda, karena untuk setiap panggilan mereka perlu menyimpan informasi antara - cache objek berseri dan nomor seri untuk tautan.
Konteks Serialisasi
Saya akan menganalisis secara rinci hanya serializer rekursif, karena pasangan "datar" mereka agak lebih rumit, dan hanya berbeda dalam pendekatannya untuk memproses pohon objek.
Mari kita mulai dengan konstruktor:
constructor(ser) { this.__proto__.__proto__ = ser; this.cache = [];
Saya this.__proto__.__proto__ = ser;
menjelaskan garis misterius this.__proto__.__proto__ = ser;
Atas masukan konstruktor, kami menerima objek dari serializer itu sendiri, dan baris ini mewarisi kelas kami darinya. Ini memungkinkan akses ke data serializer melalui this
.
Sebagai contoh, this._ignore
merujuk ke daftar kelas yang diabaikan dari serializer itu sendiri ("daftar hitam"), yang sangat berguna. Kalau tidak, kita harus menulis sesuatu seperti this._serializer._ignore
.
Metode serialisasi utama:
serialize(val) { if (Array.isArray(val)) {
Perlu dicatat bahwa ada tiga tipe dasar data yang kami proses: array, objek, dan nilai sederhana. Jika konstruktor suatu objek ada di "daftar hitam", maka objek ini tidak bersambung.
Serialisasi serialisasi:
serializeArray(val) { let res = []; for (let item of val) { let e = this.serialize(item); if (typeof e !== 'undefined') res.push(e); } return res; }
Anda dapat menulis lebih pendek melalui peta, tetapi ini tidak penting. Hanya satu hal yang penting - memeriksa nilai untuk undefined. Jika ada kelas non-serializable dalam array, maka tanpa pemeriksaan ini akan jatuh ke dalam array sebagai tidak terdefinisi, yang tidak terlalu baik. Juga dalam implementasi saya, array adalah serial tanpa kunci. Secara teoritis, Anda dapat memperbaiki algoritma untuk serialisasi array asosiatif, tetapi untuk tujuan ini saya lebih suka menggunakan objek. Selain itu, JSON.stringify juga tidak suka array asosiatif.
Serialisasi objek:
Kode serializeObject(val) { let name = this._ctorToName[val.constructor]; if (name) {
Jelas, ini adalah bagian tersulit dari serializer, hatinya. Mari kita pisahkan.
Untuk mulai dengan, kami memeriksa apakah konstruktor kelas terdaftar di serializer. Jika tidak, maka ini adalah objek sederhana yang disebut metode utilitas serializeObjectInner
.
Jika tidak, kami memeriksa apakah objek tersebut diberi pengenal unik __uuid . Ini adalah variabel penghitung sederhana yang umum untuk semua serializer, dan digunakan untuk menyimpan referensi ke instance kelas dalam cache. Anda bisa melakukannya tanpa itu, dan menyimpan instance itu sendiri tanpa kunci dalam cache, tetapi kemudian untuk memeriksa apakah objek disimpan dalam cache, Anda harus melalui seluruh cache, dan di sini cukup untuk memeriksa kunci. Saya pikir ini lebih cepat dalam hal implementasi internal objek di browser. Selain itu, saya sengaja tidak membuat serial bidang yang dimulai dengan dua garis bawah, sehingga bidang __uuid tidak akan jatuh ke json yang dihasilkan, seperti bidang kelas swasta lainnya. Jika ini tidak dapat diterima untuk tugas Anda, Anda dapat mengubah logika ini.
Selanjutnya, dengan nilai __uuid, kami mencari objek yang menggambarkan instance kelas dalam cache (di- cache ).
Jika objek seperti itu ada, maka nilainya telah diserialisasi sebelumnya. Dalam hal ini, kami menetapkan nomor seri ke objek, jika ini belum pernah dilakukan sebelumnya:
if (!cached.index) {
Kode terlihat membingungkan, dan dapat disederhanakan dengan memberikan nomor ke semua kelas yang kita serialkan. Tetapi untuk debugging dan merasakan hasilnya, lebih baik ketika nomor ditugaskan hanya untuk kelas-kelas yang ada tautan di masa depan.
Ketika nomor diberikan, kami mengembalikan tautan sesuai dengan algoritma:
Jika objek diserialisasi untuk pertama kalinya, kami membuat instance cache-nya:
let res; let cached = { ref: { [`@${name}`]: {} } }; this.cache[val.__uuid] = cached;
Dan kemudian membuat cerita bersambung:
if (typeof val.serialize === 'function') {
Ada pemeriksaan untuk implementasi antarmuka serialisasi oleh kelas (yang akan dibahas nanti), serta pembangunan Object.keys(cached.ref)[0]
. Faktanya adalah bahwa cached.ref menyimpan tautan ke objek wrapper { "@<type>[|<index>]": <> }
, tetapi nama bidang objek tidak diketahui oleh kami, karena pada tahap ini, kita belum tahu apakah nama itu akan berisi nomor objek (indeks). Konstruk ini hanya mengekstraksi bidang objek pertama dan satu-satunya.
Akhirnya, metode utilitas serialisasi objek internal:
serializeObjectInner(val) { let res = {}; for (let key of Object.getOwnPropertyNames(val)) { if (!(isString(key) && key.startsWith('__'))) {
Kami membuat objek baru dan menyalin bidang dari yang lama ke dalamnya.
Konteks Deserialisasi
Proses deserialisasi bekerja dalam urutan terbalik dan tidak memerlukan komentar khusus.
Kode /** * */ class DeserializationContext { /** * * @param {Serializer} ser */ constructor(ser) { this.__proto__.__proto__ = ser; this.cache = []; // } /** * json * @param {any} val json * @returns {any} */ deserialize(val) { if (Array.isArray(val)) { // return this.deserializeArray(val); } else if (isObject(val)) { // return this.deserializeObject(val); } else { // return val; } } /** * * @param {Object} val * @returns {Object} */ deserializeArray(val) { return val.map(item => this.deserialize(item)); } /** * * @param {Array} val * @returns {Array} */ deserializeObject(val) { let res = {}; for (let key of Object.getOwnPropertyNames(val)) { let data = val[key]; if (isString(key) && key.startsWith('@')) { // if (isInteger(data)) { // res = this.cache[data]; if (res) { return res; } else { console.error(` ${data}`); return data; } } else { // let [name, id] = key.substr(1).split('|'); let ctor = this._nameToCtor[name]; if (ctor) { // res = new ctor(); // , if (id) this.cache[id] = res; if (typeof res.deserialize === 'function') { // res.deserialize(data); } else { // for (let key of Object.getOwnPropertyNames(data)) { res[key] = this.deserialize(data[key]); } } return res; } else { // console.error(` "${name}" .`); return val[key]; } } } else { // res[key] = this.deserialize(val[key]); } } return res; } }
Fitur tambahan
Antarmuka serialisasi
Tidak ada dukungan antarmuka dalam Javascript, tetapi kami dapat setuju bahwa jika kelas mengimplementasikan metode serialisasi dan deserialize, maka metode ini akan digunakan untuk serialisasi / deserialisasi, masing-masing.
Selain itu, Javascript memungkinkan Anda menerapkan metode ini untuk tipe bawaan, misalnya, untuk Tanggal:
Serialisasi tanggal ke format ISO Date.prototype.serialize = function () { return this.toISOString(); }; Date.prototype.deserialize = function (val) { let date = new Date(val); this.setDate(date.getDate()); this.setTime(date.getTime()); };
Yang terpenting adalah ingat untuk mendaftarkan tipe Tanggal: serializer.register(Date);
.
Hasil:
{ "@Date": "2018-06-02T20:41:06.861Z" }
Satu-satunya batasan: hasil serialisasi tidak boleh bilangan bulat, karena dalam hal ini, itu akan ditafsirkan sebagai referensi ke objek.
Demikian pula, Anda bisa membuat serialkan kelas-kelas sederhana menjadi string. Contoh serialisasi kelas Warna, yang menggambarkan warna, ke baris #rrggbb
ada di github .
Serializer datar
Khusus untuk Anda, pembaca yang budiman, saya menulis versi kedua dari serializer , yang melintasi pohon objek tidak secara mendalam secara rekursif, tetapi secara lebar lebarnya menggunakan antrian.
Sebagai perbandingan, saya akan memberikan contoh serialisasi dari dua simpul pertama dari skema kami dalam kedua kasus.
Serializer rekursif (serialisasi mendalam) [ { "@Schema.Start": { "id": "5ec74f26-9515-4789-b852-12feeb258949", "name": "Start", "links": { "102c3dca-8e08-4389-bc7f-68862f2061ef": { "@Schema.Link": { "id": "102c3dca-8e08-4389-bc7f-68862f2061ef", "target": { "@Schema.Command|1": { "id": "447f6299-4bd4-48e4-b271-016a0d47fc0e", "name": "Command", "links": {}, "command": " A, B" } } } } } } }, { "@Schema.Command": 1 } ]
Serializer datar (lebar serialisasi) [ { "@Schema.Start": { "id": "1412603f-24c2-4513-836e-f2b0c0392483", "name": "Start", "links": { "b94ac7e5-d75f-44c1-960f-a02f52c994da": { "@Schema.Link": { "id": "b94ac7e5-d75f-44c1-960f-a02f52c994da", "target": { "@Schema.Command": 1 } } } } } }, { "@Schema.Command|1": { "id": "a93e452e-4276-4d6a-86a1-0681226d79f0", "name": "Command", "links": {}, "command": " A, B" } } ]
Secara pribadi, saya suka opsi kedua bahkan lebih dari yang pertama, tetapi harus diingat bahwa memilih salah satu opsi, Anda tidak dapat menggunakan yang lain. Ini semua tentang tautan. Perhatikan bahwa dalam serializer datar, tautan ke simpul kedua berjalan sebelum deskripsinya.
Pro dan kontra dari serializer
Pro:
- Kode serializer cukup sederhana dan ringkas (sekitar 300 baris, setengahnya adalah komentar).
- Serializer mudah digunakan dan tidak memerlukan perpustakaan pihak ketiga.
- Ada dukungan built-in untuk antarmuka serialisasi untuk serialisasi arbitrer kelas.
- Hasilnya menyenangkan mata (IMHO).
- Mengembangkan serializer / deserializer serupa dalam bahasa lain bukanlah masalah. Ini mungkin diperlukan jika hasil serialisasi diproses di bagian belakang.
Cons:
- Serializer memerlukan pendaftaran kelas yang dapat diserialisasi.
- Ada sedikit batasan pada nama bidang objek.
- Serializer ditulis noob dalam Javascript, sehingga mungkin mengandung bug dan kesalahan.
- Performa pada sejumlah besar data mungkin menderita.
Juga minus adalah bahwa kode ini ditulis dalam ES6. Tentu saja, dimungkinkan untuk mengkonversi ke versi Javascript sebelumnya, tetapi saya tidak memeriksa kompatibilitas kode yang dihasilkan dengan browser yang berbeda.
Publikasi saya yang lain
- Lokalisasi proyek di .NET dengan juru fungsi
- Mengisi templat teks dengan data berbasis model. Implementasi .NET menggunakan fungsi bytecode dinamis (IL)