Saat kita menggunakan Nodejs untuk pengembangan sehari-hari, kita sering menggunakan require untuk mengimpor dua jenis modul. Yang pertama adalah文件模块
yang kita tulis sendiri atau modul pihak ketiga yang diinstal menggunakan npm adalah Ini adalah modul bawaan Node yang disediakan untuk kita gunakan, seperti os
, fs
dan核心模块
lainnya.
Perlu dicatat bahwa perbedaan antara modul file dan modul inti tidak hanya terletak pada apakah modul tersebut dibangun oleh Node, tetapi juga pada lokasi file, kompilasi dan proses eksekusi modul. Terdapat perbedaan yang jelas antara keduanya . Tidak hanya itu, modul file juga dapat dibagi lagi menjadi modul file biasa, modul khusus atau modul ekstensi C/C++, dll. Modul yang berbeda juga memiliki banyak detail yang berbeda dalam pemosisian file, kompilasi, dan proses lainnya.
Artikel ini akan membahas masalah ini dan memperjelas konsep modul file dan modul inti serta proses spesifiknya dan detail yang perlu diperhatikan dalam lokasi file, kompilasi, atau eksekusi.
Mari kita mulai dengan modul file.
Apa itu modul file?
Di Node, modul yang diperlukan menggunakan pengidentifikasi modul yang dimulai dengan .、.. 或/
(yaitu, menggunakan jalur relatif atau jalur absolut) akan diperlakukan sebagai modul file. Selain itu, ada jenis modul khusus. Meskipun tidak berisi jalur relatif atau jalur absolut, dan ini bukan modul inti, ia menunjuk ke sebuah paket. Ketika Node menemukan modul jenis ini, ia akan模块路径
untuk mencari modul satu per satu. Modul jenis ini disebut modul khusus.
Oleh karena itu, modul file mencakup dua jenis, satu adalah modul file biasa dengan jalur, dan yang lainnya adalah modul khusus tanpa jalur.
Modul file dimuat secara dinamis saat runtime, yang memerlukan lokasi file lengkap, proses kompilasi dan eksekusi, dan lebih lambat dibandingkan modul inti.
Untuk pemosisian file, Node menangani kedua jenis modul file ini secara berbeda. Mari kita lihat lebih dekat proses pencarian kedua jenis modul file ini.
Untuk modul file biasa, karena jalur yang dibawanya sangat jelas, pencarian tidak akan memakan waktu lama, sehingga efisiensi pencarian lebih tinggi daripada modul khusus yang diperkenalkan di bawah ini. Namun, masih ada dua hal yang perlu diperhatikan.
Pertama, dalam keadaan normal, ketika menggunakan require untuk memperkenalkan modul file, ekstensi file umumnya tidak ditentukan, misalnya:
const math = require("math");
Karena ekstensi tidak ditentukan, Node tidak dapat menentukan file akhir. Dalam hal ini, Node akan menyelesaikan ekstensi dalam urutan .js、.json、.node
, dan mencobanya satu per satu. Proses ini disebut文件扩展名分析
.
Perlu diperhatikan juga bahwa dalam pengembangan sebenarnya, selain memerlukan file tertentu, biasanya kita juga menentukan direktori, seperti:
const axios = require("../network");
Dalam hal ini, Node akan menjalankan file terlebih dahulu analisis ekstensi. Jika file yang sesuai tidak ditemukan, tetapi direktori diperoleh, Node akan memperlakukan direktori tersebut sebagai sebuah paket.
Secara khusus, Node akan mengembalikan file yang ditunjuk oleh field main
package.json
di direktori sebagai hasil pencarian. Jika file yang ditunjuk oleh main salah, atau file package.json
tidak ada sama sekali, Node akan menggunakan index
sebagai nama file default, lalu menggunakan .js
dan .node
untuk melakukan analisis ekstensi dan mencari file target satu per satu. Jika tidak ditemukan, maka akan terjadi kesalahan.
(Tentu saja karena Node memiliki dua jenis sistem modul, CJS dan ESM, selain mencari field utama, Node juga akan menggunakan metode lain. Karena berada di luar cakupan artikel ini, saya tidak akan menjelaskannya secara detail. )
baru saja disebutkan, Ketika Node mencari modul khusus, ia akan menggunakan jalur modul.
Teman-teman yang sudah familiar dengan parsing modul harus tahu bahwa path modul adalah array yang terdiri dari path. Nilai spesifiknya dapat dilihat pada contoh berikut:
// example.js console.log(module.paths)
;
Seperti yang Anda lihat, modul di Node memiliki array jalur modul, yang disimpan di module.paths
dan digunakan untuk menentukan bagaimana Node menemukan modul khusus yang direferensikan oleh modul saat ini.
Secara khusus, Node akan melintasi larik jalur modul, mencoba setiap jalur satu per satu, dan mencari tahu apakah ada modul khusus yang ditentukan di direktori node_modules
yang sesuai dengan jalur tersebut. Jika tidak, modul tersebut akan berulang ke atas selangkah demi selangkah hingga mencapai direktori node_modules
di direktori root. Sampai modul target ditemukan, kesalahan akan terjadi jika tidak ditemukan.
Dapat dilihat bahwa pencarian direktori node_modules
secara rekursif langkah demi langkah adalah strategi Node untuk menemukan modul khusus, dan jalur modul adalah implementasi spesifik dari strategi ini.
Pada saat yang sama, kami juga sampai pada kesimpulan bahwa ketika mencari modul khusus, semakin dalam levelnya, semakin memakan waktu pencarian terkait. Oleh karena itu, dibandingkan dengan modul inti dan modul file biasa, kecepatan pemuatan modul khusus adalah yang paling lambat.
Tentu saja yang ditemukan berdasarkan jalur modul hanyalah direktori, bukan file tertentu. Setelah menemukan direktori tersebut, Node juga akan mencari sesuai dengan proses pemrosesan paket yang dijelaskan di atas.
Di atas adalah proses pemosisian file dan detail yang perlu diperhatikan untuk modul file biasa dan modul khusus. Selanjutnya, mari kita lihat bagaimana kedua jenis modul tersebut dikompilasi dan dijalankan.
Ketikadan file yang ditunjuk oleh require berada, pengidentifikasi modul biasanya tidak memiliki ekstensi. Berdasarkan analisis ekstensi file yang disebutkan di atas, kita dapat mengetahui bahwa Node mendukung kompilasi dan eksekusi file dengan tiga ekstensi. :
file JavaScript. File dibaca secara sinkron melalui modul fs
dan kemudian dikompilasi dan dieksekusi. Kecuali file .node
dan .json
, file lain akan dimuat sebagai file .js
.
File .node
, yang merupakan file ekstensi yang dikompilasi dan dihasilkan setelah ditulis dalam C/C++. Node memuat file melalui metode process.dlopen()
.
file json, setelah membaca file secara sinkron melalui modul fs
, gunakan JSON.parse()
untuk mengurai dan mengembalikan hasilnya.
Sebelum mengkompilasi dan mengeksekusi modul file, Node akan membungkusnya menggunakan pembungkus modul seperti yang ditunjukkan di bawah ini:
(function(exports, require, module, __filename, __dirname) { //Module code});
Dapat dilihat bahwa melalui pembungkus modul, Node mengemas modul ke dalam lingkup fungsi dan mengisolasinya dari lingkup lain untuk menghindari masalah seperti konflik penamaan variabel dan kontaminasi lingkup global waktu, dengan memasukkan parameter ekspor dan persyaratan memungkinkan modul memiliki kemampuan impor dan ekspor yang diperlukan. Ini adalah implementasi modul Node.
Setelah memahami module wrapper, mari kita lihat dulu proses kompilasi dan eksekusi file json.
Kompilasi dan eksekusi file json adalah yang paling sederhana. Setelah membaca konten file JSON secara sinkron melalui modul fs
, Node akan menggunakan JSON.parse() untuk mengurai objek JavaScript, kemudian menetapkannya ke objek ekspor modul, dan akhirnya mengembalikannya ke modul yang mereferensikannya . Prosesnya sangat sederhana dan kasar.
. Setelah menggunakan pembungkus modul untuk membungkus file JavaScript, kode yang dibungkus akan dieksekusi melalui metode runInThisContext()
(mirip dengan eval) dari modul vm
, mengembalikan objek fungsi.
Kemudian, ekspor, kebutuhan, modul, dan parameter lain dari modul JavaScript diteruskan ke fungsi ini untuk dieksekusi. Setelah eksekusi, atribut ekspor modul dikembalikan ke pemanggil.
Sebelum menjelaskan kompilasi dan eksekusi modul ekstensi C/C++, mari kita perkenalkan terlebih dahulu apa itu modul ekstensi C/C++.
Modul ekstensi C/C++ termasuk dalam kategori modul file. Seperti namanya, modul ini ditulis dalam C/C++. Perbedaannya dengan modul JavaScript adalah modul tersebut tidak perlu dikompilasi setelah dimuat setelah dieksekusi secara langsung, sehingga dimuat Sedikit lebih cepat dari modul JavaScript. Dibandingkan dengan modul file yang ditulis dalam JS, modul ekstensi C/C++ memiliki keunggulan kinerja yang jelas. Untuk fungsi yang tidak dapat dicakup oleh modul inti Node atau memiliki persyaratan kinerja tertentu, pengguna dapat menulis modul ekstensi C/C++ untuk mencapai tujuannya.
Jadi apa itu file .node
, dan apa hubungannya dengan modul ekstensi C/C++?
Faktanya, setelah modul ekstensi C/C++ tertulis dikompilasi, file .node
dibuat. Dengan kata lain, sebagai pengguna modul, kami tidak secara langsung memperkenalkan kode sumber modul ekstensi C/C++, tetapi file biner terkompilasi dari modul ekstensi C/C++. Oleh karena itu, file .node
tidak perlu dikompilasi. Setelah Node menemukan file .node
, ia hanya perlu memuat dan mengeksekusi file tersebut. Selama eksekusi, objek ekspor modul diisi dan dikembalikan ke pemanggil.
Perlu dicatat bahwa file .node
yang dihasilkan dengan mengkompilasi modul ekstensi C/C++ memiliki bentuk yang berbeda pada platform yang berbeda: pada sistem *nix
, modul ekstensi C/C++ dikompilasi menjadi file objek bersama tautan dinamis oleh kompiler seperti g++/gcc. Ekstensinya adalah .so
; di bawah Windows
ekstensi tersebut dikompilasi menjadi file pustaka tautan dinamis oleh kompiler Visual C++, dan ekstensinya adalah .dll
. Namun ekstensi yang kami gunakan sebenarnya adalah .node
. Faktanya, ekstensi .node
hanya agar terlihat lebih alami. Faktanya, ini adalah file .dll di Windows
dan file .dll
di bawah file *nix
.so
.
Setelah Node menemukan file .node
yang diperlukan, ia memanggil metode process.dlopen()
untuk memuat dan mengeksekusi file. Karena file .node
memiliki bentuk file berbeda pada platform berbeda, untuk mencapai implementasi lintas platform, metode dlopen()
memiliki implementasi berbeda pada platform Windows
dan *nix
, dan kemudian dienkapsulasi melalui lapisan kompatibilitas libuv
. Gambar berikut menunjukkan proses kompilasi dan pemuatan modul ekstensi C/C++ pada platform berbeda:
Modul inti dikompilasi menjadi file biner yang dapat dieksekusi selama proses kompilasi kode sumber Node. Ketika proses Node dimulai, beberapa modul inti dimuat langsung ke dalam memori. Oleh karena itu, ketika modul inti ini diperkenalkan, dua langkah lokasi file dan kompilasi serta eksekusi dapat dihilangkan, dan akan dinilai sebelum modul file berada di jalur. analisis. Jadi kecepatan pemuatannya adalah yang tercepat.
Modul inti sebenarnya dibagi menjadi dua bagian yang ditulis dalam C/C++ dan JavaScript. File C/C++ disimpan di direktori src proyek Node, dan file JavaScript disimpan di direktori lib. Jelas sekali, proses kompilasi dan eksekusi kedua bagian modul ini berbeda.
Untuk kompilasi modul inti JavaScript, selama proses kompilasi kode sumber Node, Node akan menggunakan alat js2c.py yang disertakan dengan V8 untuk mengonversi semua kode JavaScript bawaan, termasuk modul inti JavaScript, ke dalam C++ Array, kode JavaScript disimpan di namespace node sebagai string. Saat memulai proses Node, kode JavaScript dimuat langsung ke memori.
Saat modul inti JavaScript diperkenalkan, Node akan memanggil process.binding()
untuk menemukan lokasinya di memori melalui analisis pengidentifikasi modul dan mengambilnya. Setelah dikeluarkan, modul inti JavaScript juga akan dibungkus oleh pembungkus modul, kemudian dieksekusi, objek ekspor akan diekspor, dan dikembalikan ke pemanggil.
dalam modul inti. Beberapa modul semuanya ditulis dalam C/C++, beberapa modul memiliki bagian inti yang diselesaikan oleh C/C++, dan bagian lain dikemas atau diekspor oleh JavaScript untuk memenuhi persyaratan kinerja. . Modul seperti buffer
, fs
, os
, dll. sebagian ditulis dalam C/C++. Model di mana modul C++ mengimplementasikan inti di dalam bagian utama dan modul JavaScript mengimplementasikan enkapsulasi di luar bagian utama adalah cara umum bagi Node untuk meningkatkan kinerja.
Bagian dari modul inti yang ditulis dalam C/C++ murni disebut modul bawaan, seperti node_fs
, node_os
, dll. Biasanya tidak dipanggil langsung oleh pengguna, tetapi bergantung langsung pada modul inti JavaScript. Oleh karena itu, dalam proses pengenalan modul inti Node, terdapat rantai referensi seperti ini:
Jadi bagaimana modul inti JavaScript memuat modul bawaan?
Ingat metode process.binding()
? Node menghapus modul inti JavaScript dari memori dengan memanggil metode ini. Metode ini juga berlaku untuk modul inti JavaScript untuk membantu memuat modul bawaan.
Khusus untuk implementasi metode ini, saat memuat modul bawaan, pertama-tama buat objek ekspor kosong, lalu panggil metode get_builtin_module()
untuk mengeluarkan objek modul bawaan, isi objek ekspor dengan mengeksekusi register_func()
, dan terakhir mengembalikannya ke pemanggil untuk menyelesaikan ekspor. Ini adalah proses memuat dan mengeksekusi modul bawaan.
Melalui analisa di atas, untuk pengenalan rantai referensi seperti modul inti, dengan mengambil contoh modul os, proses umumnya adalah sebagai berikut:
Singkatnya, proses pengenalan modul os melibatkan pengenalan modul file JavaScript, pemuatan dan eksekusi modul inti JavaScript, serta pemuatan dan eksekusi modul bawaan. Prosesnya sangat rumit dan rumit, namun untuk pemanggil modul, karena perlindungan yang mendasarinya. Untuk implementasi dan detail yang kompleks, seluruh modul dapat diimpor hanya melalui require(), yang sangat sederhana. ramah.
Artikel ini memperkenalkan konsep dasar modul file dan modul inti serta proses spesifik dan detailnya yang perlu diperhatikan dalam lokasi file, kompilasi, atau eksekusi. Khususnya:
modul file dapat dibagi menjadi modul file biasa dan modul khusus sesuai dengan proses pemosisian file yang berbeda. Modul file biasa dapat langsung ditemukan karena jalurnya yang jelas, terkadang melibatkan proses analisis ekstensi file dan analisis direktori modul khusus akan mencari berdasarkan jalur modul, dan setelah pencarian berhasil, lokasi file akhir akan dilakukan melalui analisis direktori; .
Modul file dapat dibagi menjadi modul JavaScript dan modul ekstensi C/C++ sesuai dengan proses kompilasi dan eksekusi yang berbeda. Setelah modul JavaScript dikemas oleh pembungkus modul, modul tersebut dieksekusi melalui metode runInThisContext
dari modul vm
; karena modul ekstensi C/C++ sudah menjadi file yang dapat dieksekusi yang dihasilkan setelah kompilasi, modul tersebut dapat dieksekusi secara langsung dan objek yang diekspor dikembalikan. kepada penelepon.
Modul inti dibagi menjadi modul inti JavaScript dan modul bawaan. Modul inti JavaScript dimuat ke dalam memori ketika proses Node dimulai. Modul ini dapat dikeluarkan dan kemudian dieksekusi melalui metode process.binding()
; kompilasi dan eksekusi modul bawaan akan melalui process.binding()
, get_builtin_module()
dan register_func()
pemrosesan fungsi.
Selain itu, kami juga menemukan rantai referensi untuk Node untuk memperkenalkan modul inti, yaitu modul file-->modul inti JavaScript-->modul bawaan modul mengimplementasikan enkapsulasi secara eksternal.