Kursus pengenalan singkat Node.js: masuk untuk belajar
Dua tahun lalu saya menulis artikel yang memperkenalkan sistem modul: Memahami konsep modul front-end: CommonJs dan ES6Module. Pengetahuan dalam artikel ini ditujukan untuk pemula dan relatif sederhana. Di sini kami juga memperbaiki beberapa kesalahan pada artikel:
[Modul] dan [Sistem Modul] adalah dua hal yang berbeda. Modul adalah unit dalam perangkat lunak, dan sistem modul adalah seperangkat sintaks atau alat. Sistem modul memungkinkan pengembang untuk mendefinisikan dan menggunakan modul dalam proyek.
Singkatan dari Modul ECMAScript adalah ESM, atau ESModule, bukan ES6Module.
Pengetahuan dasar tentang sistem modul hampir dibahas pada artikel sebelumnya, jadi artikel ini akan fokus pada prinsip internal sistem modul dan pengenalan lebih lengkap tentang perbedaan antara sistem modul yang berbeda. Isi artikel sebelumnya ada di Ini tidak akan terulang lagi.
Tidak semua bahasa pemrograman memiliki sistem modul bawaan, dan JavaScript tidak memiliki sistem modul dalam waktu lama setelah kelahirannya.
Di lingkungan browser, Anda hanya dapat menggunakan tag <script>
untuk memasukkan file kode yang tidak digunakan. Metode ini memiliki cakupan global, yang bisa dikatakan penuh masalah. Ditambah dengan pesatnya perkembangan front end, metode ini tidak lagi memenuhi kebutuhan saat ini. Sebelum sistem modul resmi muncul, komunitas front-end membuat sistem modul pihak ketiganya sendiri. Yang paling umum digunakan adalah: definisi modul asinkron AMD , UMD definisi modul universal, dll. Tentu saja, yang paling terkenal adalah CommonJS .
Karena Node.js adalah lingkungan runtime JavaScript, Node.js dapat langsung mengakses sistem file yang mendasarinya. Jadi pengembang mengadopsinya dan mengimplementasikan sistem modul sesuai dengan spesifikasi CommonJS.
Pada awalnya CommonJS hanya bisa digunakan di platform Node.js. Dengan munculnya alat pengemasan modul seperti Browserify dan Webpack, CommonJS akhirnya bisa berjalan di sisi browser.
Baru setelah spesifikasi ECMAScript6 dirilis pada tahun 2015, terdapat standar formal untuk sistem modul. Sistem modul yang dibangun sesuai dengan standar ini disebut modul ECMAScript (ESM). Sejak saat itu, ESM mulai menyatu lingkungan Node.js dan lingkungan browser. Tentu saja, ECMAScript6 hanya menyediakan sintaksis dan semantik. Sedangkan untuk implementasinya, bergantung pada berbagai vendor layanan browser dan pengembang Node untuk bekerja keras. Itu sebabnya kami memiliki artefak babel yang membuat iri bahasa pemrograman lain. Mengimplementasikan sistem modul bukanlah tugas yang mudah. Node.js hanya memiliki dukungan yang relatif stabil untuk ESM di versi 13.2.
Namun bagaimanapun juga, ESM adalah "anak" JavaScript, dan tidak ada salahnya mempelajarinya!
Di era pertanian tebang-bakar, JavaScript digunakan untuk mengembangkan aplikasi, dan file skrip hanya dapat dimasukkan melalui tag skrip. Salah satu masalah yang lebih serius adalah kurangnya mekanisme namespace, yang berarti setiap skrip memiliki cakupan yang sama. Ada solusi yang lebih baik untuk masalah ini di komunitas: Modul Revevaling
const modul saya = (() => { const _privateFn = () => {} const_privateAttr = 1 kembali { publikFn: () => {}, attr publik: 2 } })() konsol.log(Modul saya) konsol.log(myModule.publicFn, myModule._privateFn)
Hasil yang berjalan adalah sebagai berikut:
Pola ini sangat sederhana, gunakan IIFE untuk membuat cakupan pribadi, dan gunakan variabel kembalian untuk diekspos. Variabel internal (seperti _privateFn, _privateAttr) tidak dapat diakses dari lingkup luar.
[modul pengungkapan] memanfaatkan fitur ini untuk menyembunyikan informasi pribadi dan mengekspor API yang harus diekspos ke dunia luar. Sistem modul selanjutnya juga dikembangkan berdasarkan ide ini.
Berdasarkan ide di atas, kembangkan pemuat modul.
Pertama tulis fungsi yang memuat konten modul, bungkus fungsi ini dalam lingkup privat, lalu evaluasi melalui eval() untuk menjalankan fungsi:
function loadModule (nama file, modul, kebutuhan) { const dibungkusSrc = `(fungsi (modul, ekspor, memerlukan) { ${fs.readFileSync(nama file, 'utf8)} }(modul, modul.ekspor, memerlukan)` eval(dibungkusSrc) }
Seperti [mengungkapkan modul], kode sumber modul dibungkus dalam suatu fungsi. Perbedaannya adalah serangkaian variabel (modul, module.exports, require) juga diteruskan ke fungsi tersebut.
Perlu dicatat bahwa konten modul dibaca melalui [readFileSync]. Secara umum, Anda tidak boleh menggunakan versi tersinkronisasi saat memanggil API yang melibatkan sistem file. Namun kali ini berbeda, karena memuat modul melalui sistem CommonJs itu sendiri harus diimplementasikan sebagai operasi sinkron untuk memastikan bahwa beberapa modul dapat dimasukkan dalam urutan ketergantungan yang benar.
Kemudian simulasikan fungsi require(), yang fungsi utamanya adalah memuat modul.
fungsi memerlukan(Namamodul) { const id = memerlukan.resolve(namamodul) if (memerlukan.cache[id]) { kembalikan require.cache[id].ekspor } // Modul metadata modul const = { ekspor: {}, PENGENAL } //Perbarui cache require.cache[id] = modul //Muat modul loadModule(id, modul, memerlukan) // Mengembalikan variabel yang diekspor, mengembalikan module.exports } memerlukan.cache = {} memerlukan.resolve = (Namamodul) => { // Parsing id modul lengkap berdasarkan nama modul }
(1) Setelah fungsi menerima Namamodul, fungsi tersebut terlebih dahulu menguraikan jalur lengkap modul dan menugaskannya ke id.
(2) Jika cache[id]
benar, berarti modul telah dimuat, dan hasil cache akan langsung dikembalikan. (3) Jika tidak, lingkungan akan dikonfigurasi untuk pemuatan pertama. Secara khusus, buat objek modul, termasuk ekspor (yaitu konten yang diekspor) dan id (fungsinya seperti di atas)
(4) Cache modul yang dimuat untuk pertama kalinya (5) Baca kode sumber dari file sumber modul melalui loadModule (6) Terakhir, return module.exports
mengembalikan konten yang ingin Anda ekspor.
Saat mensimulasikan fungsi require, ada detail yang sangat penting: fungsi require harus synchronous . Fungsinya hanya untuk mengembalikan konten modul secara langsung, dan tidak menggunakan mekanisme callback. Hal yang sama berlaku untuk kebutuhan di Node.js. Oleh karena itu, operasi penugasan untuk module.exports juga harus sinkron. Jika asinkron digunakan, masalah akan terjadi:
// Ada yang tidak beres setTimeout(() => { modul.ekspor = fungsi () {} }, 1000)
Fakta bahwa require adalah fungsi sinkron memiliki dampak yang sangat penting pada cara mendefinisikan modul, karena memaksa kita untuk hanya menggunakan kode sinkron saat mendefinisikan modul, sehingga Node.js menyediakan versi sinkron dari sebagian besar API asinkron untuk tujuan ini.
Node.js awal memiliki versi asinkron dari fungsi require, namun segera dihapus karena akan membuat fungsinya menjadi sangat rumit.
ESM adalah bagian dari spesifikasi ECMAScript2015, yang menentukan sistem modul resmi untuk bahasa JavaScript untuk beradaptasi dengan berbagai lingkungan eksekusi.
Secara default, Node.js memperlakukan file dengan akhiran .js sebagai ditulis menggunakan sintaks CommonJS. Jika Anda menggunakan sintaks ESM secara langsung di file .js, penerjemah akan melaporkan kesalahan.
Ada tiga cara untuk mengonversi interpreter Node.js ke sintaks ESM:
1. Ubah ekstensi file menjadi .mjs;
2. Tambahkan kolom tipe ke file package.json terbaru dengan nilai "module";
3. String diteruskan ke --eval
sebagai parameter, atau dikirim ke node melalui pipa STDIN dengan flag --input-type=module
Misalnya:
node --input-type=module --eval "impor { sep } dari 'node:path'; konsol.log(sep);"
ESM dapat diurai dan di-cache sebagai URL (yang juga berarti karakter khusus harus dikodekan dalam persen). Mendukung protokol URL seperti file:
node:
dan data:
berkas:URL
Modul dimuat beberapa kali jika penentu impor yang digunakan untuk menyelesaikan modul memiliki kueri atau fragmen yang berbeda
// Dianggap sebagai dua modul berbeda import './foo.mjs?query=1'; impor './foo.mjs?query=2';
data:URL
Mendukung impor menggunakan tipe MIME:
text/javascript
untuk modul ES
application/json
untuk JSON
application/wasm
untuk Wasm
import 'data:text/javascript,console.log("halo!");'; impor _ dari 'data:application/json,"dunia!"' menegaskan { mengetik: 'json' };
data:URL
hanya mem-parsing penentu telanjang dan absolut untuk modul bawaan. Penguraian penentu relatif tidak berfungsi karena data:
bukan protokol khusus dan tidak memiliki konsep penguraian relatif.
Pernyataan Impor <br/>Atribut ini menambahkan sintaks sebaris ke pernyataan impor modul untuk menyampaikan informasi lebih lanjut di sebelah penentu modul.
impor fooData dari './foo.json' menegaskan { ketik: 'json' }; const { default: barData } = menunggu impor('./bar.json', { menegaskan: { mengetik: 'json' } });
Saat ini hanya modul JSON yang didukung, dan sintaksis assert { type: 'json' }
bersifat wajib.
Mengimpor Modul Cuci <br/>Mengimpor modul WebAssembly didukung di bawah flag --experimental-wasm-modules
, yang memungkinkan file .wasm apa pun untuk diimpor sebagai modul normal, sekaligus mendukung impor modulnya.
// indeks.mjs impor * sebagai M dari './module.wasm'; konsol.log(M)
Gunakan perintah berikut untuk mengeksekusi:
simpul --eksperimental-wasm-modules indeks.mjs
Kata kunci menunggu dapat digunakan di level teratas di ESM.
// a.mjs ekspor const lima = menunggu Janji.resolve(5) // b.mjs impor { lima } dari './a.mjs' console.log(lima) // 5
Seperti disebutkan sebelumnya, resolusi ketergantungan modul pernyataan import bersifat statis, sehingga memiliki dua batasan yang terkenal:
Pengidentifikasi modul tidak dapat menunggu hingga waktu proses untuk membuatnya;
Pernyataan impor modul harus ditulis di bagian atas file dan tidak dapat disarangkan dalam pernyataan aliran kontrol;
Namun, untuk beberapa situasi, kedua pembatasan ini tentu saja terlalu ketat. Misalnya, ada persyaratan yang relatif umum: pemuatan lambat :
Saat menemukan modul besar, Anda hanya ingin memuat modul besar ini ketika Anda benar-benar perlu menggunakan fungsi tertentu dalam modul tersebut.
Untuk tujuan ini, ESM menyediakan mekanisme pengenalan asinkron. Operasi pengenalan ini dapat dilakukan melalui operator import()
saat program sedang berjalan. Dari sudut pandang sintaksis, ini setara dengan fungsi yang menerima pengidentifikasi modul sebagai parameter dan mengembalikan Promise. Setelah Promise diselesaikan, objek modul yang diurai dapat diperoleh.
Gunakan contoh ketergantungan melingkar untuk mengilustrasikan proses pemuatan ESM:
// indeks.js impor * sebagai foo dari './foo.js'; impor * sebagai bilah dari './bar.js'; konsol.log(foo); konsol.log(bar); // foo.js impor * sebagai Batang dari './bar.js' ekspor biarkan dimuat = false; ekspor const bar = Batang; dimuat = benar; //bar.js impor * sebagai Foo dari './foo.js'; ekspor biarkan dimuat = false; ekspor const foo = Foo; dimuat = benar
Mari kita lihat hasil runningnya terlebih dahulu:
Dapat diamati melalui pemuatan bahwa modul foo dan bar dapat mencatat informasi modul lengkap yang dimuat. Tapi CommonJS berbeda. Pasti ada modul yang tidak bisa mencetak seperti apa setelah dimuat penuh.
Mari selami proses pemuatan untuk melihat mengapa hasil ini terjadi.
Proses pemuatan dapat dibagi menjadi tiga tahap:
Tahap pertama: analisis
Tahap kedua: deklarasi
Tahap ketiga: eksekusi
Tahap penguraian:
Penerjemah memulai dari file entri (yaitu, index.js), menganalisis ketergantungan antar modul, dan menampilkannya dalam bentuk grafik.
Pada tahap ini, kami hanya fokus pada pernyataan import dan memuat kode sumber yang sesuai dengan modul yang ingin diperkenalkan oleh pernyataan ini. Dan dapatkan grafik ketergantungan akhir melalui analisis mendalam. Ambil contoh di atas untuk menggambarkan:
1. Mulai dari index.js, temukan pernyataan import * as foo from './foo.js'
dan buka file foo.js.
2. Lanjutkan penguraian dari file foo.js dan temukan pernyataan import * as Bar from './bar.js'
, sehingga menuju ke bar.js.
3. Lanjutkan penguraian dari bar.js dan temukan pernyataan import * as Foo from './foo.js'
, yang membentuk ketergantungan melingkar. Namun, karena penerjemah sudah memproses modul foo.js, ia tidak akan memasukinya lagi, lalu lanjutkan. Parsing modul batang.
4. Setelah modul bar diurai, ditemukan tidak ada pernyataan import, sehingga kembali ke foo.js dan melanjutkan parsing. Pernyataan import tidak ditemukan lagi, dan index.js dikembalikan.
5. import * as bar from './bar.js'
ditemukan di index.js, tetapi karena bar.js telah diurai, maka dilewati dan melanjutkan eksekusi.
Terakhir, grafik ketergantungan ditampilkan sepenuhnya melalui pendekatan depth-first:
Fase deklarasi:
Interpreter memulai dari grafik ketergantungan yang diperoleh dan mendeklarasikan setiap modul secara berurutan dari bawah ke atas. Secara khusus, setiap kali modul tercapai, semua properti yang akan diekspor oleh modul dicari dan pengidentifikasi nilai yang diekspor dideklarasikan dalam memori. Harap dicatat bahwa hanya deklarasi yang dibuat pada tahap ini dan tidak ada operasi penugasan yang dilakukan.
1. Interpreter memulai dari modul bar.js dan mendeklarasikan pengidentifikasi Loaded dan Foo.
2. Telusuri kembali ke modul foo.js dan nyatakan pengidentifikasi bilah dan yang dimuat.
3. Kita sampai di modul index.js, tetapi modul ini tidak memiliki pernyataan ekspor, jadi tidak ada pengenal yang dideklarasikan.
Setelah mendeklarasikan semua pengidentifikasi ekspor, telusuri kembali grafik ketergantungan untuk menghubungkan hubungan antara impor dan ekspor.
Terlihat bahwa hubungan pengikatan yang mirip dengan const terjalin antara modul yang diperkenalkan melalui impor dan nilai yang diekspor melalui ekspor. Selain itu, modul bar yang dibaca di index.js dan modul bar yang dibaca di foo.js pada dasarnya adalah contoh yang sama.
Maka dari itu hasil parsing lengkapnya di-output pada hasil contoh ini.
Ini pada dasarnya berbeda dari pendekatan yang digunakan oleh sistem CommonJS. Jika sebuah modul mengimpor modul CommonJS, sistem akan menyalin seluruh objek ekspor modul tersebut dan menyalin isinya ke modul saat ini, dalam kasus ini, jika modul yang diimpor mengubah variabel salinannya sendiri, maka pengguna tidak dapat melihat nilai baru .
Fase eksekusi:
Pada tahap ini, mesin akan mengeksekusi kode modul. Grafik ketergantungan masih diakses dalam urutan bottom-up dan file yang diakses dieksekusi satu per satu. Eksekusi dimulai dari file bar.js, ke foo.js, dan terakhir ke index.js. Dalam proses ini, nilai pengidentifikasi di tabel ekspor ditingkatkan secara bertahap.
Proses ini sepertinya tidak jauh berbeda dengan CommonJS, namun sebenarnya ada perbedaan besar. Karena CommonJS bersifat dinamis, ia mem-parsing grafik ketergantungan saat mengeksekusi file terkait. Jadi selama Anda melihat pernyataan require, Anda dapat yakin bahwa ketika program sampai pada pernyataan ini, semua kode sebelumnya telah dieksekusi. Oleh karena itu, pernyataan require tidak harus muncul di awal file, tetapi dapat muncul di mana saja, dan pengidentifikasi modul juga dapat dibuat dari variabel.
Namun ESM berbeda. Dalam ESM, ketiga tahap di atas dipisahkan satu sama lain. Ia harus membuat grafik ketergantungan terlebih dahulu sebelum dapat mengeksekusi kode. Oleh karena itu, operasi pengenalan modul dan ekspor modul harus bersifat statis. jangan menunggu sampai kode dieksekusi.
Selain beberapa perbedaan yang disebutkan sebelumnya, ada beberapa perbedaan yang perlu diperhatikan:
Saat menggunakan kata kunci import di ESM untuk menyelesaikan penentu relatif atau absolut, ekstensi file harus disediakan dan indeks direktori ('./path/index.js') harus ditentukan sepenuhnya. Fungsi CommonJS require memungkinkan ekstensi ini dihilangkan.
ESM berjalan dalam mode ketat secara default, dan mode ketat ini tidak dapat dinonaktifkan. Oleh karena itu, Anda tidak dapat menggunakan variabel yang tidak dideklarasikan, Anda juga tidak dapat menggunakan fitur yang hanya tersedia dalam mode non-ketat (seperti dengan).
CommonJS menyediakan beberapa variabel global. Variabel ini tidak dapat digunakan di bawah ESM. Jika Anda mencoba menggunakan variabel ini, akan terjadi Kesalahan Referensi. termasuk
require
exports
module.exports
__filename
__dirname
Diantaranya, __filename
mengacu pada jalur absolut dari file modul saat ini, dan __dirname
adalah jalur absolut dari folder tempat file tersebut berada. Kedua variabel ini sangat membantu ketika membangun jalur relatif dari file saat ini, sehingga ESM menyediakan beberapa metode untuk mengimplementasikan fungsi kedua variabel tersebut.
Di ESM, Anda dapat menggunakan objek import.meta
untuk mendapatkan referensi, yang merujuk ke URL file saat ini. Secara khusus, jalur file modul saat ini diperoleh melalui import.meta.url
. Format jalur ini mirip dengan file:///path/to/current_module.js
. Berdasarkan jalur ini, jalur absolut yang dinyatakan dengan __filename
dan __dirname
dibuat:
impor { fileURLToPath } dari 'url' impor { dirname } dari 'jalur' const __nama file = fileURLToPath(import.meta.url) const __namadir = namadir(__namafile)
Itu juga dapat mensimulasikan fungsi require() di CommonJS
impor { createRequire } dari 'modul' const membutuhkan = createRequire(import.meta.url)
Dalam lingkup global ESM, ini tidak ditentukan, tetapi dalam sistem modul CommonJS, ini adalah referensi untuk ekspor:
//ESM console.log(ini) // tidak terdefinisi // CommonJS console.log(ini === ekspor) // benar
Seperti disebutkan di atas, fungsi CommonJS require() dapat disimulasikan di ESM untuk memuat modul CommonJS. Selain itu, Anda juga dapat menggunakan sintaks impor standar untuk memperkenalkan modul CommonJS, namun metode impor ini hanya dapat mengimpor barang yang diekspor secara default:
import packageMain from 'commonjs-package' // Sangat mungkin untuk mengimpor { metode } dari 'commonjs-package' // Kesalahan
Persyaratan modul CommonJS selalu memperlakukan file yang direferensikannya sebagai CommonJS. Memuat modul ES menggunakan require tidak didukung karena modul ES memiliki eksekusi asinkron. Namun Anda dapat menggunakan import()
untuk memuat modul ES dari modul CommonJS.
Meskipun ESM telah diluncurkan selama 7 tahun, node.js juga mendukungnya secara stabil. Saat kami mengembangkan pustaka komponen, kami hanya dapat mendukung ESM. Namun agar kompatibel dengan proyek lama, dukungan untuk CommonJS juga penting. Ada dua metode yang banyak digunakan untuk membuat pustaka komponen mendukung ekspor dari kedua sistem modul.
Tulis paket di CommonJS atau konversikan kode sumber modul ES ke CommonJS dan buat file pembungkus modul ES yang mendefinisikan ekspor bernama. Gunakan ekspor bersyarat, impor menggunakan pembungkus modul ES, dan require menggunakan titik masuk CommonJS. Misalnya pada modul contoh
// paket.json { "tipe": "modul", "ekspor": { "impor": "./wrapper.mjs", "membutuhkan": "./index.cjs" } }
Gunakan ekstensi tampilan .cjs
dan .mjs
, karena hanya menggunakan .js
akan menjadi default CommonJS, atau "type": "module"
akan menyebabkan file-file ini diperlakukan sebagai modul ES.
// ./index.cjs ekspor.nama = 'nama'; // ./wrapper.mjs impor cjsModule dari './index.cjs' ekspor nama const = cjsModule.name;
Dalam contoh ini:
// Gunakan ESM untuk memperkenalkan import { name } from 'example' // Gunakan CommonJS untuk memperkenalkan const { name } = require('example')
Nama yang diperkenalkan dalam kedua cara tersebut adalah nama tunggal yang sama.
File package.json dapat secara langsung menentukan titik masuk modul CommonJS dan ES yang terpisah:
// paket.json { "tipe": "modul", "ekspor": { "impor": "./index.mjs", "membutuhkan": "./index.cjs" } }
Hal ini dapat dilakukan jika versi CommonJS dan ESM dari paket tersebut setara, misalnya karena yang satu merupakan keluaran yang ditranspilasi dari paket lainnya; dan manajemen status paket diisolasi secara hati-hati (atau paket tersebut tidak memiliki kewarganegaraan)
Alasan status menjadi masalah adalah karena versi paket CommonJS dan ESM dapat digunakan dalam aplikasi; misalnya, kode referensi pengguna dapat mengimpor versi ESM, sedangkan ketergantungan memerlukan versi CommonJS. Jika ini terjadi, dua salinan paket akan dimuat ke dalam memori, sehingga akan terjadi dua keadaan berbeda. Hal ini dapat menyebabkan kesalahan yang sulit diatasi.
Selain menulis paket tanpa kewarganegaraan (misalnya, jika Math JavaScript adalah sebuah paket, paket tersebut akan menjadi tanpa kewarganegaraan karena semua metodenya statis), ada cara untuk mengisolasi status sehingga dapat digunakan di CommonJS dan ESM yang berpotensi memuat. contoh paket:
Jika memungkinkan, sertakan semua status dalam objek yang dipakai. Misalnya, Tanggal JavaScript perlu dipakai untuk memuat status; jika berupa paket, maka akan digunakan seperti ini:
impor Tanggal dari 'tanggal'; const someDate = Tanggal baru(); // someDate berisi status; Tanggal tidak
Kata kunci new tidak diperlukan; fungsi paket dapat mengembalikan objek baru atau memodifikasi objek yang diteruskan untuk mempertahankan status di luar paket.
Isolasi status dalam satu atau lebih file CommonJS yang dibagikan antara versi paket CommonJS dan ESM. Misalnya, titik masuk untuk CommonJS dan ESM masing-masing adalah Index.cjs dan Index.mjs:
// indeks.cjs const state = memerlukan('./state.cjs') module.exports.state = negara bagian; // indeks.mjs impor negara dari './state.cjs' ekspor { negara }
Meskipun example digunakan dalam aplikasi melalui require dan import, setiap referensi ke example berisi status yang sama; dan perubahan status oleh sistem modul mana pun akan berlaku untuk keduanya.
Jika artikel ini bermanfaat bagi Anda, silakan beri like dan dukung. "Like" Anda adalah motivasi saya untuk terus berkarya.
Artikel ini mengutip informasi berikut:
dokumentasi resmi node.js
Pola Desain Node.js