Artikel ini akan menggunakan metode eval
KOTOR, tidak aman, "kruk", menakutkan, dll. Pingsan hati tidak membaca!
Saya harus mengatakan segera bahwa itu tidak mungkin untuk memecahkan beberapa masalah kegunaan: Anda tidak dapat menggunakan penutupan dalam kode yang akan diteruskan ke pekerja.

Kita semua menyukai teknologi baru, dan kami menyukainya ketika nyaman untuk menggunakan teknologi ini. Tetapi dalam kasus pekerja, ini tidak sepenuhnya benar. Pekerja bekerja dengan file atau tautan file, tetapi ini tidak nyaman. Saya ingin dapat melakukan tugas apa pun pada pekerja, dan bukan hanya kode yang direncanakan khusus.
Apa yang dibutuhkan untuk membuat bekerja dengan pekerja lebih nyaman? Menurut pendapat saya, berikut ini:
- Kemampuan untuk menjalankan kode arbitrer pada pekerja kapan saja
- Kemampuan untuk meneruskan data yang kompleks ke pekerja (instance kelas, fungsi)
- Kemampuan untuk mendapatkan Janji dengan respons dari pekerja.
Pertama, kita membutuhkan protokol komunikasi antara pekerja dan jendela utama. Secara umum, protokol hanyalah struktur dan tipe data yang dengannya jendela browser dan pekerja akan berkomunikasi. Tidak ada yang rumit. Anda dapat menggunakan sesuatu seperti ini atau menulis versi Anda sendiri. Dalam setiap pesan kami akan memiliki ID, dan data khusus untuk jenis pesan tertentu. Untuk mulai dengan, kami akan memiliki dua jenis pesan untuk pekerja:
- menambahkan perpustakaan / file ke pekerja
- peluncuran pekerjaan
File di dalam pekerja
Sebelum Anda mulai membuat pekerja, Anda perlu menjelaskan file yang akan berfungsi pada pekerja dan mendukung protokol yang dijelaskan oleh kami. Saya suka OOP , jadi itu akan menjadi kelas yang disebut WorkerBody. Kelas ini harus berlangganan ke acara dari jendela induk.
self.onmessage = (message) => { this.onMessage(message.data); };
Sekarang kita dapat mendengarkan acara dari jendela induk. Kami memiliki dua jenis peristiwa: peristiwa yang jawabannya tersirat, dan yang lainnya. Kami memproses acara.
Menambahkan pustaka dan file ke pekerja dilakukan menggunakan API importScripts .
Dan hal terburuk: kita akan menggunakan eval untuk menjalankan fungsi arbitrer.
... onMessage(message) { switch (message.type) { case MESSAGE_TYPE.ADD_LIBS: this.addLibs(message.libs); break; case MESSAGE_TYPE.WORK: this.doWork(message); break; } } doWork(message) { try { const processor = eval(message.job); const params = this._parser.parse(message.params); const result = processor(params); if (result && result.then && typeof result.then === 'function') { result.then((data) => { this.send({ id: message.id, state: true, body: data }); }, (error) => { if (error instanceof Error) { error = String(error); } this.send({ id: message.id, state: false, body: error }); }); } else { this.send({ id: message.id, state: true, body: result }); } } catch (e) { this.send({ id: message.id, state: false, body: String(e) }); } } send(data) { data.body = this._serializer.serialize(data.body); try { self.postMessage(data); } catch (e) { const toSet = { id: data.id, state: false, body: String(e) }; self.postMessage(toSet); } }
Metode onMessage
bertanggung jawab untuk menerima pesan dan memilih handler, doWork
- memulai fungsi yang diteruskan, dan send
mengirimkan respons ke jendela induk.
Parser dan serializer
Sekarang kita memiliki konten pekerja, kita perlu belajar bagaimana membuat cerita bersambung dan menguraikan data apa pun untuk meneruskannya kepada pekerja. Mari kita mulai dengan serializer. Kami ingin dapat memberikan data apa pun kepada pekerja, termasuk instance kelas, kelas, dan fungsi. Tetapi dengan fitur asli pekerja, kami hanya dapat mengirimkan data seperti JSON. Untuk mengatasi larangan ini, kita perlu eval . Segala sesuatu yang tidak dapat menerima JSON, kami akan membungkus konstruksi string yang sesuai dan berjalan di sisi lain. Untuk menjaga keabadian, data yang diperoleh dikloning dengan cepat, dan yang tidak dapat diserialisasi dengan metode konvensional digantikan oleh objek layanan, dan mereka, pada gilirannya, diganti kembali oleh parser di sisi lain. Sekilas, sepertinya tugas ini tidak sulit, tetapi ada banyak jebakan. Keterbatasan terburuk dari pendekatan ini adalah ketidakmampuan untuk menggunakan penutupan, yang membawa gaya penulisan kode yang sedikit berbeda. Mari kita mulai dengan yang paling sederhana, dengan fungsi. Pertama, Anda perlu mempelajari cara membedakan fungsi dari konstruktor kelas.
Mari kita coba bedakan:
static isFunction(Factory){ if (!Factory.prototype) {
Pertama, kita akan mengetahui apakah fungsi tersebut memiliki prototipe. Jika tidak, ini jelas sebuah fungsi. Kemudian kita melihat jumlah properti dalam prototipe, dan jika dalam prototipe hanya konstruktor dan fungsi bukan pewaris kelas lain, kami menganggap ini sebagai fungsi.
Setelah menemukan fungsi, kita cukup menggantinya dengan objek layanan dengan bidang __type = "serialized-function"
dan template
, yang sama dengan templat fungsi ini ( func.toString()
).
Untuk saat ini, lewati kelas dan parsing instance kelas. Lebih lanjut dalam data kita perlu membedakan objek biasa dari instance kelas.
static isInstance(some) { const constructor = some.constructor; if (!constructor) { return false; } return !Serializer.isNative(constructor); } static isNative(data) { return /function .*?\(\) \{ \[native code\] \}/.test(data.toString()); }
Kami menganggap objek sebagai hal biasa jika tidak memiliki konstruktor atau konstruktornya adalah fungsi asli. Setelah mengidentifikasi instance kelas, kami menggantinya dengan objek layanan dengan bidang:
__type
- 'serialized-instance'data
- data yang ada di instanceindex
- index
kelas dari instance ini dalam daftar layanan kelas.
Untuk mentransfer data, kita perlu membuat bidang tambahan: di dalamnya kita akan menyimpan daftar semua kelas unik yang kita lewati. Bagian yang paling sulit adalah ketika kelas terdeteksi, ambil tidak hanya templatnya, tetapi juga templat semua kelas induk dan simpan sebagai kelas yang terpisah - sehingga setiap "induk" dilewatkan tidak lebih dari sekali - dan simpan centang pada instanceof. Mendefinisikan kelas itu mudah: ini adalah fungsi yang tidak lulus uji Serializer.isFunction kami. Saat menambahkan kelas, kami memeriksa keberadaan kelas tersebut dalam daftar data berseri dan hanya menambahkan yang unik. Kode yang mengumpulkan kelas ke dalam templat cukup besar dan terletak di sini .
Dalam parser, pertama-tama kita berkeliling semua kelas yang dilewati untuk kita dan kompilasi mereka jika mereka belum lulus sebelumnya. Kemudian kami secara rekursif melintasi setiap bidang data dan mengganti objek layanan dengan data yang dikompilasi. Hal yang paling menarik adalah pada instance kelas. Kami memiliki kelas dan ada data yang ada dalam instansinya, tetapi kami tidak dapat hanya membuat instantiate, karena panggilan ke konstruktor mungkin memiliki parameter yang tidak kami miliki. Metode Object.create yang hampir terlupakan datang ke bantuan kami, yang mengembalikan objek dengan prototipe yang diberikan. Jadi kami menghindari memanggil konstruktor dan mendapatkan instance kelas, dan kemudian hanya menulis ulang properti menjadi instance.
Pekerja penciptaan
Agar pekerja dapat bekerja dengan sukses, kita perlu memiliki parser dan serializer di dalam pekerja dan di luar, jadi kami mengambil serializer dan mengubah serializer, parser dan tubuh pekerja menjadi templat. Kami membuat gumpalan dari template dan membuat tautan unduhan melalui URL.createObjectURL (metode ini mungkin tidak berfungsi dengan beberapa "Kebijakan Keamanan Konten"). Metode ini juga cocok untuk menjalankan kode arbitrer dari string.
_createWorker(customWorker) { const template = `var MyWorker = ${this._createTemplate(customWorker)};`; const blob = new Blob([template], { type: 'application/javascript' }); return new Worker(URL.createObjectURL(blob)); } _createTemplate(WorkerBody) { const Name = Serializer.getFnName(WorkerBody); if (!Name) { throw new Error('Unnamed Worker Body class! Please add name to Worker Body class!'); } return [ '(function () {', this._getFullClassTemplate(Serializer, 'Serializer'), this._getFullClassTemplate(Parser, 'Parser'), this._getFullClassTemplate(WorkerBody, 'WorkerBody'), `return new WorkerBody(Serializer, Parser)})();` ].join('\n'); }
Hasil
Dengan demikian, kami telah memperoleh pustaka yang mudah digunakan yang dapat menjalankan kode arbitrer pada pekerja. Ini mendukung kelas dari TypeScript. Sebagai contoh:
const wrapper = workerWrapper.create(); wrapper.process((params) => {
Rencana selanjutnya
Perpustakaan ini, sayangnya, masih jauh dari ideal. Penting untuk menambahkan dukungan untuk setter dan getter pada kelas, objek, prototipe, properti statis. Kami juga ingin menambahkan caching, membuat skrip alternatif berjalan tanpa eval
melalui URL.createObjectURL dan menambahkan file dengan konten pekerja ke majelis (jika kreasi tidak tersedia dengan cepat). Datanglah ke repositori !