Rumah >hujung hadapan web >tutorial js >Artikel yang menganalisis sistem modul dalam nod
Saya menulis artikel dua tahun lalu untuk memperkenalkan sistem modul: Memahami konsep modul bahagian hadapan: CommonJs dan ES6Module. Pengetahuan dalam artikel ini bertujuan untuk pemula dan agak mudah. Saya juga membetulkan beberapa ralat dalam artikel di sini:
Pengetahuan asas tentang sistem modul hampir dibincangkan dalam artikel sebelum ini, jadi artikel ini akan memberi tumpuan kepada prinsip dalaman sistem modul dan pengenalan yang lebih lengkapPerbezaan antara sistem modul yang berbeza, kandungan yang dipaparkan dalam artikel sebelum ini tidak akan diulang di sini.
Tidak semua bahasa pengaturcaraan mempunyai sistem modul terbina dalam Tidak ada sistem modul untuk masa yang lama selepas JavaScript dilahirkan.
Dalam persekitaran penyemak imbas, anda hanya boleh menggunakan tag <script></script>
untuk memperkenalkan fail kod yang tidak digunakan Kaedah ini berkongsi skop global, yang boleh dikatakan penuh dengan masalah, ditambah dengan perkembangan pesat front-end, kaedah ini mempunyai Ia tidak lagi memenuhi keperluan semasa. Sebelum sistem modul rasmi muncul, komuniti bahagian hadapan mencipta sistem modul pihak ketiga sendiri Yang paling biasa digunakan ialah: takrifan modul tak segerak AMD, definisi modul universal UMD, dll. Sudah tentu, yang paling popular ialah Yang paling terkenal ialah CommonJS.
Oleh kerana Node.js ialah persekitaran berjalan JavaScript, ia boleh mengakses terus sistem fail asas. Jadi pembangun meluluskannya dan melaksanakan sistem modul mengikut spesifikasi CommonJS.
Pada mulanya, CommonJS hanya boleh digunakan pada platform Node.js Dengan kemunculan alat pembungkusan modul seperti Browserify dan Webpack, CommonJS akhirnya boleh dijalankan di bahagian penyemak imbas.
Sehingga spesifikasi ECMAScript6 dikeluarkan pada tahun 2015 barulah terdapat standard formal untuk sistem modul Sistem modul yang dibina mengikut piawaian ini dipanggil modul ECMAScriptdirujuk sebagai. [ESM], dan dengan itu ESM menjadi Persekitaran Node.js dan persekitaran pelayar mula disatukan. Sudah tentu, ECMAScript6 hanya menyediakan sintaks dan semantik Bagi pelaksanaan, terpulang kepada vendor perkhidmatan penyemak imbas dan pembangun Node untuk bekerja keras. Itulah sebabnya kami mempunyai artifak babel yang dicemburui oleh bahasa pengaturcaraan lain. Melaksanakan sistem modul bukanlah tugas yang mudah Node.js mempunyai sokongan yang agak stabil hanya dalam versi 13.2. ESM.
Tetapi tidak kira apa pun, ESM ialah "anak" JavaScript, dan tidak salah untuk mempelajarinya!
Dalam era pertanian slash-and-burn, JavaScript digunakan untuk membangunkan aplikasi, dan fail skrip hanya boleh diperkenalkan melalui tag skrip. Salah satu masalah yang lebih serius ialah kekurangan mekanisme ruang nama, yang bermaksud bahawa setiap skrip berkongsi skop yang sama. Terdapat penyelesaian yang lebih baik untuk masalah ini dalam komuniti: Modul Revevaling
const myModule = (() => { const _privateFn = () => {} const _privateAttr = 1 return { publicFn: () => {}, publicAttr: 2 } })() console.log(myModule) console.log(myModule.publicFn, myModule._privateFn)
Hasil yang dijalankan adalah seperti berikut:
Ini Coraknya sangat mudah, gunakan IIFE untuk mencipta skop peribadi, dan gunakan return untuk mendedahkan pembolehubah. Pembolehubah dalaman (seperti _privateFn, _privateAttr) tidak boleh diakses dari skop luar.
[modul mendedahkan] memanfaatkan ciri ini untuk menyembunyikan maklumat peribadi dan mengeksport API yang harus didedahkan kepada dunia luar. Sistem modul seterusnya juga dibangunkan berdasarkan idea ini.
Berdasarkan idea di atas, bangunkan pemuat modul.
Mula-mula tulis fungsi yang memuatkan kandungan modul, bungkus fungsi ini dalam skop peribadi, dan kemudian nilaikannya melalui eval() untuk menjalankan fungsi:
function loadModule (filename, module, require) { const wrappedSrc = `(function (module, exports, require) { ${fs.readFileSync(filename, 'utf8)} }(module, module.exports, require)` eval(wrappedSrc) }
dan [ Suka mendedahkan modul , kod sumber modul dibungkus dalam fungsi Perbezaannya ialah satu siri pembolehubah (modul, modul.eksport, memerlukan) juga dihantar ke fungsi.
Perlu diperhatikan bahawa kandungan modul dibaca melalui [readFileSync]. Secara umumnya, anda tidak seharusnya menggunakan versi yang disegerakkan apabila memanggil API yang melibatkan sistem fail. Tetapi kali ini berbeza, kerana memuatkan modul melalui sistem CommonJs sendiri harus dilaksanakan sebagai operasi segerak untuk memastikan berbilang modul boleh diperkenalkan dalam susunan kebergantungan yang betul.
Kemudian simulasikan fungsi require(), fungsi utama ialah memuatkan modul.
function require(moduleName) { const id = require.resolve(moduleName) if (require.cache[id]) { return require.cache[id].exports } // 模块的元数据 const module = { exports: {}, id } // 更新缓存 require.cache[id] = module // 载入模块 loadModule(id, module, require) // 返回导出的变量 return module.exports } require.cache = {} require.resolve = (moduleName) => { // 根据moduleName解析出完整的模块id }
(1)函数接收到moduleName后,首先解析出模块的完整路径,赋值给id。
(2)如果cache[id]
为true,说明该模块已经被加载过了,直接返回缓存结果
(3)否则,就配置一套环境,用于首次加载。具体来说,创建module对象,包含exports(也就是导出内容),id(作用如上)
(4)将首次加载的module缓存起来
(5)通过loadModule从模块的源文件中读取源代码
(6)最后return module.exports
返回想要导出的内容。
在模拟require函数的时候,有一个很重要的细节:require函数必须是同步的。它的作用仅仅是直接将模块内容返回而已,并没有用到回调机制。Node.js中的require也是如此。所以针对module.exports的赋值操作,也必须是同步的,如果用异步就会出问题:
// 出问题 setTimeout(() => { module.exports = function () {} }, 1000)
require是同步函数这一点对定义模块的方式有着非常重要的影响,因为它迫使我们在定义模块时只能使用同步的代码,以至于Node.js都为此,提供了大多数异步API的同步版本。
早期的Node.js有异步版本的require函数,但很快就移除了,因为这会让函数的功能变得十分复杂。
ESM是ECMAScript2015规范的一部分,该规范给JavaScript语言指定了一套官方的模块系统,以适应各种执行环境。
Node.js默认会把.js后缀的文件,都当成是采用CommonJS语法所写的。如果直接在.js文件中采用ESM语法,解释器会报错。
有三种方法可以在让Node.js解释器转为ESM语法:
1、把文件后缀名改为.mjs;
2、给最近的package.json文件添加type字段,值为“module”;
3、字符串作为参数传入--eval
,或通过STDIN管道传输到node,带有标志--input-type=module
比如:
node --input-type=module --eval "import { sep } from 'node:path'; console.log(sep);"
ESM可以被解析并缓存为URL(这也意味着特殊字符必须是百分比编码)。支持file:
、node:
和data:
等的URL协议
file:URL
如果用于解析模块的import说明符具有不同的查询或片段,则会多次加载模块
// 被认为是两个不同的模块 import './foo.mjs?query=1'; import './foo.mjs?query=2';
data:URL
支持使用MIME类型导入:
text/javascript
用于ES模块application/json
用于JSONapplication/wasm
用于Wasmimport 'data:text/javascript,console.log("hello!");'; import _ from 'data:application/json,"world!"' assert { type: 'json' };
data:URL
仅解析内置模块的裸说明符和绝对说明符。解析相对说明符不起作用,因为data:
不是特殊协议,没有相对解析的概念。
导入断言
这个属性为模块导入语句添加了内联语法,以便在模块说明符旁边传入更多信息。
import fooData from './foo.json' assert { type: 'json' }; const { default: barData } = await import('./bar.json', { assert: { type: 'json' } });
目前只支持JSON模块,而且assert { type: 'json' }
语法是具有强制性的。
导入Wash模块
在--experimental-wasm-modules
标志下支持导入WebAssembly模块,允许将任何.wasm文件作为普通模块导入,同时也支持它们的模块导入。
// index.mjs import * as M from './module.wasm'; console.log(M)
使用如下命令执行:
node --experimental-wasm-modules index.mjs
await关键字可以用在ESM中的顶层。
// a.mjs export const five = await Promise.resolve(5) // b.mjs import { five } from './a.mjs' console.log(five) // 5
前面说过,import语句对模块依赖的解决是静态的,因此有两项著名的限制:
然而,对于某些情况来说,这两项限制无疑是过于严格。就比如说有一个还算是比较常见的需求:延迟加载:
在遇到一个体积很大的模块时,只想在真正需要用到模块里的某个功能时,再去加载这个庞大的模块。
为此,ESM提供了异步引入机制。这种引入操作,可以在程序运行的时候,通过import()
运算符实现。从语法上看,相当于一个函数,接收模块标识符作为参数,并返回一个Promise,待Promise resolve后就能得到解析后的模块对象。
用一个循环依赖的例子来说明ESM的加载过程:
// index.js import * as foo from './foo.js'; import * as bar from './bar.js'; console.log(foo); console.log(bar); // foo.js import * as Bar from './bar.js' export let loaded = false; export const bar = Bar; loaded = true; // bar.js import * as Foo from './foo.js'; export let loaded = false; export const foo = Foo; loaded = true
先看看运行结果:
Ia boleh diperhatikan melalui dimuatkan bahawa kedua-dua modul foo dan bar boleh log maklumat modul lengkap yang dimuatkan. Tetapi CommonJS berbeza mesti ada modul yang tidak dapat mencetak rupanya selepas dimuatkan sepenuhnya.
Mari kita gali proses pemuatan untuk melihat sebab keputusan sedemikian berlaku.
Proses pemuatan boleh dibahagikan kepada tiga peringkat:
Fasa penghuraian: Jurubahasa
bermula daripada fail kemasukan (iaitu, index.js), menghuraikan kebergantungan antara modul dan memaparkannya dalam bentuk rajah , ini graf juga dipanggil graf kebergantungan.
Pada peringkat ini, kami hanya menumpukan pada pernyataan import dan memuatkan kod sumber yang sepadan dengan modul yang ingin diperkenalkan oleh pernyataan ini. Dan dapatkan graf pergantungan akhir melalui analisis mendalam. Ambil contoh di atas untuk menggambarkan:
1 Mula dari index.js, cari pernyataan import * as foo from './foo.js'
dan pergi ke fail foo.js.
2. Teruskan menghuraikan daripada fail foo.js, cari pernyataan import * as Bar from './bar.js'
dan kemudian pergi ke bar.js.
3. Teruskan menghuraikan dari bar.js dan cari pernyataan import * as Foo from './foo.js'
, yang membentuk kebergantungan bulat Walau bagaimanapun, memandangkan penterjemah sedang memproses modul foo.js, ia tidak akan memasukkannya lagi, dan kemudian meneruskan ke. menghuraikan modul bar.
4. Selepas menghuraikan modul bar, kami mendapati tiada pernyataan import, jadi kami kembali ke foo.js dan teruskan menghuraikan. Pernyataan import tidak ditemui lagi sepanjang jalan dan index.js telah dikembalikan.
5. import * as bar from './bar.js'
ditemui dalam index.js, tetapi memandangkan bar.js telah dihuraikan, ia dilangkau dan meneruskan pelaksanaan.
Akhir sekali, graf pergantungan dipaparkan sepenuhnya melalui kaedah depth-first:
Fasa pengisytiharan:
Penterjemah bermula dari Bermula dari graf kebergantungan yang diperoleh, isytiharkan setiap modul mengikut urutan dari bawah ke atas. Khususnya, setiap kali modul dicapai, semua atribut yang akan dieksport oleh modul dicari, dan pengecam nilai yang dieksport diisytiharkan dalam ingatan. Sila ambil perhatian bahawa hanya pengisytiharan dibuat pada peringkat ini dan tiada operasi tugasan dilakukan.
1. Jurubahasa bermula dari modul bar.js dan mengisytiharkan pengecam dimuatkan dan foo.
2. Jejak kembali ke modul foo.js dan isytiharkan pengecam yang dimuatkan dan bar.
3 Modul index.js dicapai, tetapi modul ini tidak mempunyai pernyataan eksport, jadi tiada pengecam diisytiharkan.
Selepas mengisytiharkan semua pengecam eksport, jalani graf pergantungan sekali lagi untuk menyambungkan hubungan antara import dan eksport.
Seperti yang anda lihat, hubungan mengikat yang serupa dengan const diwujudkan antara modul yang diperkenalkan oleh import dan nilai yang dieksport. Bahagian pengimport ialah Hanya boleh membaca tetapi tidak menulis. Selain itu, modul bar dibaca dalam index.js dan modul bar dibaca dalam foo.js pada asasnya adalah contoh yang sama.
Jadi inilah sebabnya hasil penghuraian lengkap dikeluarkan dalam hasil contoh ini.
Ini pada asasnya berbeza daripada kaedah yang digunakan oleh sistem CommonJS. Jika modul mengimport modul CommonJS, sistem akan menyalin keseluruhan objek eksport yang terakhir dan menyalin kandungannya ke modul semasa Dalam kes ini, jika modul yang diimport mengubah pembolehubah salinannya sendiri, maka pengguna tidak dapat melihat nilai baharu .
Fasa pelaksanaan:
Dalam fasa ini, enjin akan melaksanakan kod modul. Graf pergantungan masih diakses dalam susunan bawah ke atas dan fail yang diakses dilaksanakan satu demi satu. Pelaksanaan bermula daripada fail bar.js, ke foo.js, dan akhirnya ke index.js. Dalam proses ini, nilai pengecam dalam jadual eksport ditingkatkan secara beransur-ansur.
Proses ini nampaknya tidak jauh berbeza daripada CommonJS, tetapi sebenarnya terdapat perbezaan besar. Memandangkan CommonJS adalah dinamik, ia menghuraikan graf pergantungan semasa melaksanakan fail berkaitan. Jadi selagi anda melihat pernyataan memerlukan, anda boleh yakin bahawa apabila program datang ke pernyataan ini, semua kod sebelumnya telah dilaksanakan. Oleh itu, pernyataan memerlukan tidak semestinya perlu muncul pada permulaan fail, tetapi boleh muncul di mana-mana, dan pengecam modul juga boleh dibina daripada pembolehubah.
Tetapi ESM berbeza dalam ESM, ketiga-tiga peringkat di atas dipisahkan antara satu sama lain. Ia mesti membina graf pergantungan sepenuhnya sebelum ia boleh melaksanakan kod tersebut. Ia mestilah statik dan tidak boleh menunggu sehingga kod dilaksanakan.
Selain beberapa perbezaan yang dinyatakan di atas, terdapat beberapa perbezaan yang perlu diberi perhatian:
在ESM中使用import关键字解析相对或绝对的说明符时,必须提供文件扩展名,还必须完全指定目录索引('./path/index.js')。而CommonJS的require函数则允许省略这个扩展名。
ESM是默认运行于严格模式之下,而且该严格模式是不能禁用。所以不能使用未声明的变量,也不能使用那些仅仅在非严格模式下才能使用的特性(例如with)。
CommonJS中提供了一些全局变量,这些变量不能在ESM下使用,如果试图使用这些变量会导致ReferenceError错误。包括
require
exports
module.exports
__filename
__dirname
其中__filename
指的是当前这个模块文件的绝对路径,__dirname
则是该文件所在文件夹的绝对路径。这连个变量在构建当前文件的相对路径时很有帮助,所以ESM提供了一些方法去实现两个变量的功能。
在ESM中,可以使用import.meta
对象来获取一个引用,这个引用指的是当前文件的URL。具体来说,就是通过import.meta.url
来获取当前模块的文件路径,这个路径的格式类似file:///path/to/current_module.js
。根据这条路径,构造出__filename
和__dirname
所表达的绝对路径:
import { fileURLToPath } from 'url' import { dirname } from 'path' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename)
而且还能模拟CommonJS中require()函数
import { createRequire } from 'module' const require = createRequire(import.meta.url)
在ESM的全局作用域中,this是未定义(undefined),但是在CommonJS模块系统中,它是一个指向exports的引用:
// ESM console.log(this) // undefined // CommonJS console.log(this === exports) // true
上面提到过在ESM中可以模拟CommonJS的require()函数,以此来加载CommonJS的模块。除此之外,还可以使用标准的import语法引入CommonJS模块,不过这种引入方式只能把默认导出的东西给引进来:
import packageMain from 'commonjs-package' // 完全可以 import { method } from 'commonjs-package' // 出错
而CommonJS模块的require总是将它引用的文件视为CommonJS。不支持使用require加载ES模块,因为ES模块具有异步执行。但可以使用import()
从CommonJS模块中加载ES模块。
虽然ESM已经推出了7年,node.js也已经稳定支持了,我们开发组件库的时候可以只支持ESM。但为了兼容旧项目,对CommonJS的支持也是必不可少的。有两种广泛使用的方法可以使得组件库同时支持两个模块系统的导出。
在CommonJS中编写包或将ES模块源代码转换为CommonJS,并创建定义命名导出的ES模块封装文件。使用条件导出,import使用ES模块封装器,require使用CommonJS入口点。举个例子,example模块中
// package.json { "type": "module", "exports": { "import": "./wrapper.mjs", "require": "./index.cjs" } }
使用显示扩展名.cjs
和.mjs
,因为只用.js
的话,要么是被默认为CommonJS,要么"type": "module"
会导致这些文件都被视为ES模块。
// ./index.cjs export.name = 'name'; // ./wrapper.mjs import cjsModule from './index.cjs' export const name = cjsModule.name;
在这个例子中:
// 使用ESM引入 import { name } from 'example' // 使用CommonJS引入 const { name } = require('example')
这两种方式引入的name都是相同的单例。
package.json文件可以直接定义单独的CommonJS和ES模块入口点:
// package.json { "type": "module", "exports": { "import": "./index.mjs", "require": "./index.cjs" } }
如果包的CommonJS和ESM版本是等效的,则可以做到这一点,例如因为一个是另一个的转译输出;并且包的状态管理被仔细隔离(或包是无状态的)
状态是一个问题的原因是因为包的CommonJS和ESM版本都可能在应用程序中使用;例如,用户的引用程序代码可以importESM版本,而依赖项require CommonJS版本。如果发生这种情况,包的两个副本将被加载到内存中,因此将出现两个不同的状态。这可能会导致难以解决的错误。
除了编写无状态包(例如,如果JavaScript的Math是一个包,它将是无状态的,因为它的所有方法都是静态的),还有一些方法可以隔离状态,以便在可能加载的CommonJS和ESM之间共享它包的实例:
import Date from 'date'; const someDate = new Date(); // someDate 包含状态;Date 不包含
new关键字不是必需的;包的函数可以返回新的对象,或修改传入的对象,以保持包外部的状态。
// index.cjs const state = require('./state.cjs') module.exports.state = state; // index.mjs import state from './state.cjs' export { state }
即使example在应用程序中通过require和import使用example的每个引用都包含相同的状态;并且任一模块系统修改状态将适用二者皆是。
如果本文对你有帮助,就点个赞支持下吧,你的「赞」是我持续进行创作的动力。
本文引用以下资料:
更多node相关知识,请访问:nodejs 教程!
Atas ialah kandungan terperinci Artikel yang menganalisis sistem modul dalam nod. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!