หลักสูตรแนะนำ Node.js ฉบับย่อ: เข้าเรียนเพื่อเรียนรู้
สองปีที่แล้ว ฉันเขียนบทความแนะนำระบบโมดูล: การทำความเข้าใจแนวคิดของโมดูลส่วนหน้า: CommonJs และ ES6Module ความรู้ในบทความนี้มุ่งเป้าไปที่ผู้เริ่มต้นและค่อนข้างง่าย ที่นี่เรายังแก้ไขข้อผิดพลาดบางประการในบทความด้วย:
[โมดูล] และ [ระบบโมดูล] เป็นสองสิ่งที่แตกต่างกัน โมดูลคือหน่วยหนึ่งในซอฟต์แวร์ และระบบโมดูลคือชุดของไวยากรณ์หรือเครื่องมือ ระบบโมดูลช่วยให้นักพัฒนาสามารถกำหนดและใช้โมดูลในโครงการได้
ตัวย่อของโมดูล ECMAScript คือ ESM หรือ ESModule ไม่ใช่ ES6Module
ความรู้พื้นฐานเกี่ยวกับระบบโมดูลเกือบจะครอบคลุมอยู่ในบทความที่แล้ว ดังนั้นบทความนี้จะเน้นไปที่หลักการภายในของระบบโมดูลและการแนะนำที่สมบูรณ์ยิ่งขึ้นเกี่ยวกับความแตกต่างระหว่างระบบโมดูลต่างๆ เนื้อหาของบทความก่อนหน้านี้อยู่ในนี้ จะไม่ถูกทำซ้ำอีกครั้ง
ภาษาการเขียนโปรแกรมบางภาษามีระบบโมดูลในตัวและ JavaScript ไม่มีระบบโมดูลมาเป็นเวลานานหลังจากที่มันเกิด
ในสภาพแวดล้อมของเบราว์เซอร์ คุณสามารถใช้แท็ก <script>
เพื่อแนะนำไฟล์โค้ดที่ไม่ได้ใช้เท่านั้น วิธีการนี้ใช้ขอบเขตร่วมกันซึ่งอาจกล่าวได้ว่าเต็มไปด้วยปัญหา ควบคู่ไปกับการพัฒนาอย่างรวดเร็วของส่วนหน้า วิธีนี้ไม่ใช่ ตอบโจทย์ความต้องการในปัจจุบันได้ยาวนานยิ่งขึ้น ก่อนที่ระบบโมดูลอย่างเป็นทางการจะปรากฏขึ้น ชุมชนส่วนหน้าได้สร้างระบบโมดูลของบุคคลที่สามขึ้นมาเอง ระบบที่ใช้กันมากที่สุดคือ: คำจำกัดความของโมดูลอะซิงโครนัส AMD , คำจำกัดความโมดูลสากล UMD ฯลฯ แน่นอนว่าระบบที่มีชื่อเสียงที่สุดคือ CommonJS
เนื่องจาก Node.js เป็นสภาพแวดล้อมรันไทม์ JavaScript จึงสามารถเข้าถึงระบบไฟล์ที่เกี่ยวข้องได้โดยตรง ดังนั้นนักพัฒนาจึงนำมันมาใช้และใช้งานระบบโมดูลตามข้อกำหนดของ CommonJS
ในตอนแรก CommonJS สามารถใช้ได้บนแพลตฟอร์ม Node.js เท่านั้น ด้วยการเกิดขึ้นของเครื่องมือบรรจุภัณฑ์โมดูล เช่น Browserify และ Webpack ในที่สุด CommonJS ก็สามารถทำงานบนฝั่งเบราว์เซอร์ได้
จนกระทั่งมีการเปิดตัวข้อกำหนด ECMAScript6 ในปี 2558 จึงมีมาตรฐานอย่างเป็นทางการสำหรับระบบโมดูล ระบบโมดูลที่สร้างขึ้นตามมาตรฐานนี้เรียกโดยย่อว่า ECMAScript module (ESM) จากนั้นเป็นต้นมา ESM ก็เริ่มรวมเป็นหนึ่งเดียว สภาพแวดล้อม Node.js และสภาพแวดล้อมของเบราว์เซอร์ แน่นอนว่า ECMAScript6 จัดเตรียมเฉพาะไวยากรณ์และความหมายเท่านั้น สำหรับการนำไปใช้งานนั้น ขึ้นอยู่กับผู้จำหน่ายบริการเบราว์เซอร์และนักพัฒนาโหนดต่างๆ ที่จะทำงานหนัก นั่นเป็นเหตุผลว่าทำไมเราถึงมี Babel Artifact ที่น่าอิจฉาสำหรับภาษาโปรแกรมอื่นๆ การใช้ระบบโมดูลจึงไม่ใช่เรื่องง่าย Node.js มีเพียงการรองรับ ESM ที่ค่อนข้างเสถียรในเวอร์ชัน 13.2 เท่านั้น
แต่ไม่ว่าอะไรจะเกิดขึ้น ESM ก็คือ "ลูก" ของ JavaScript และไม่มีอะไรผิดในการเรียนรู้มัน!
ในยุคของการทำฟาร์มแบบสแลชแอนด์เบิร์น JavaScript ถูกใช้เพื่อพัฒนาแอปพลิเคชัน และไฟล์สคริปต์สามารถใช้ได้ผ่านแท็กสคริปต์เท่านั้น ปัญหาที่ร้ายแรงประการหนึ่งคือการไม่มีกลไกเนมสเปซ ซึ่งหมายความว่าแต่ละสคริปต์ใช้ขอบเขตเดียวกัน มีวิธีแก้ไขปัญหานี้ที่ดีกว่าในชุมชน: โมดูลการเปิดเผย
const myModule = (() => { const _privateFn = () => {} const_privateAttr = 1 กลับ { publicFn: () => {}, สาธารณะAttr: 2 - - console.log (โมดูลของฉัน) console.log(myModule.publicFn, myModule._privateFn)
ผลการวิ่งมีดังนี้:
รูปแบบนี้ง่ายมาก ใช้ IIFE เพื่อสร้างขอบเขตส่วนตัว และใช้ตัวแปรส่งคืนเพื่อแสดง ไม่สามารถเข้าถึงตัวแปรภายใน (เช่น _privateFn, _privateAttr) จากขอบเขตภายนอก
[โมดูลการเปิดเผย] ใช้ประโยชน์จากคุณสมบัติเหล่านี้เพื่อซ่อนข้อมูลส่วนตัวและส่งออก API ที่ควรเปิดเผยสู่โลกภายนอก ระบบโมดูลต่อมาก็ได้รับการพัฒนาตามแนวคิดนี้เช่นกัน
จากแนวคิดข้างต้น ให้พัฒนาตัวโหลดโมดูล
ขั้นแรกให้เขียนฟังก์ชันที่โหลดเนื้อหาโมดูล ล้อมฟังก์ชันนี้ไว้ในขอบเขตส่วนตัว จากนั้นประเมินผ่าน eval() เพื่อเรียกใช้ฟังก์ชัน:
ฟังก์ชั่น loadModule (ชื่อไฟล์, โมดูล, ต้องการ) { const wrapSrc = `(ฟังก์ชั่น (โมดูล, ส่งออก, ต้องการ) { ${fs.readFileSync(ชื่อไฟล์, 'utf8)} }(โมดูล, module.exports, ต้องการ)` eval (ห่อ Src) -
เช่นเดียวกับ [เปิดเผยโมดูล] ซอร์สโค้ดของโมดูลถูกรวมไว้ในฟังก์ชัน ความแตกต่างก็คือชุดของตัวแปร (โมดูล, module.exports, ต้องการ) จะถูกส่งผ่านไปยังฟังก์ชันด้วย
เป็นที่น่าสังเกตว่าเนื้อหาโมดูลถูกอ่านผ่าน [readFileSync] โดยทั่วไป คุณไม่ควรใช้เวอร์ชันที่ซิงโครไนซ์เมื่อเรียก API ที่เกี่ยวข้องกับระบบไฟล์ แต่ครั้งนี้แตกต่างออกไป เนื่องจากการโหลดโมดูลผ่านระบบ CommonJs เองควรถูกนำมาใช้เป็นการดำเนินการแบบซิงโครนัสเพื่อให้แน่ใจว่าโมดูลหลายโมดูลสามารถถูกนำมาใช้ในลำดับการพึ่งพาที่ถูกต้อง
จากนั้นจำลองฟังก์ชัน need() ซึ่งมีหน้าที่หลักในการโหลดโมดูล
ฟังก์ชั่นต้องการ (ชื่อโมดูล) { const id = need.resolve (ชื่อโมดูล) ถ้า (require.cache[id]) { กลับ need.cache[id].exports - // โมดูล const ข้อมูลเมตาของโมดูล = { ส่งออก: {}, บัตรประจำตัวประชาชน - //อัพเดตแคช need.cache[id] = module //โหลดโมดูล loadModule(id, โมดูล, ต้องการ) // ส่งคืนตัวแปรที่ส่งออก return module.exports - need.แคช = {} need.resolve = (ชื่อโมดูล) => { // แยกรหัสโมดูลทั้งหมดออกตามชื่อโมดูล -
(1) หลังจากที่ฟังก์ชันได้รับ moduleName แล้ว ขั้นแรกฟังก์ชันจะแยกวิเคราะห์เส้นทางที่สมบูรณ์ของโมดูลและกำหนดให้กับ id
(2) หาก cache[id]
เป็นจริง แสดงว่าโหลดโมดูลแล้ว และผลลัพธ์ของแคชจะถูกส่งกลับโดยตรง (3) มิฉะนั้น สภาพแวดล้อมจะถูกกำหนดค่าสำหรับการโหลดครั้งแรก โดยเฉพาะ สร้างอ็อบเจ็กต์โมดูล รวมถึงการส่งออก (นั่นคือ เนื้อหาที่ส่งออก) และ id (ฟังก์ชันดังที่กล่าวข้างต้น)
(4) แคชโมดูลที่โหลดเป็นครั้งแรก (5) อ่านซอร์สโค้ดจากไฟล์ต้นฉบับของโมดูลผ่าน loadModule (6) สุดท้าย return module.exports
ส่งคืนเนื้อหาที่คุณต้องการส่งออก
เมื่อจำลองฟังก์ชัน need มีรายละเอียดที่สำคัญมาก: ฟังก์ชัน need จะต้องเป็นแบบซิงโครนัส ฟังก์ชันนี้มีไว้เพื่อส่งคืนเนื้อหาโมดูลโดยตรงเท่านั้น และไม่ใช้กลไกการเรียกกลับ เช่นเดียวกับ need ใน Node.js ดังนั้น การดำเนินการกำหนดสำหรับ module.exports จะต้องเป็นแบบซิงโครนัสด้วย หากใช้แบบอะซิงโครนัส ปัญหาจะเกิดขึ้น:
// มีบางอย่างผิดพลาด setTimeout(() => { module.exports = ฟังก์ชัน () {} }, 1,000)
ข้อเท็จจริงที่จำเป็นต้องมีคือฟังก์ชันซิงโครนัสมีผลกระทบที่สำคัญมากต่อวิธีการกำหนดโมดูล เนื่องจากบังคับให้เราใช้โค้ดซิงโครนัสเมื่อกำหนดโมดูลเท่านั้น ดังนั้น Node.js จึงจัดเตรียม API แบบอะซิงโครนัสส่วนใหญ่ในเวอร์ชันซิงโครนัสเพื่อจุดประสงค์นี้
Early Node.js มีฟังก์ชัน need เวอร์ชันอะซิงโครนัส แต่ถูกลบออกอย่างรวดเร็วเนื่องจากจะทำให้ฟังก์ชันซับซ้อนมาก
ESM เป็นส่วนหนึ่งของข้อกำหนด ECMAScript2015 ซึ่งระบุระบบโมดูลอย่างเป็นทางการสำหรับภาษา JavaScript เพื่อปรับให้เข้ากับสภาพแวดล้อมการดำเนินการต่างๆ
ตามค่าเริ่มต้น Node.js จะถือว่าไฟล์ที่มีส่วนต่อท้าย .js เหมือนกับถูกเขียนโดยใช้ไวยากรณ์ CommonJS หากคุณใช้ไวยากรณ์ ESM โดยตรงในไฟล์ .js ล่ามจะรายงานข้อผิดพลาด
มีสามวิธีในการแปลงล่าม Node.js เป็นไวยากรณ์ ESM:
1. เปลี่ยนนามสกุลไฟล์เป็น .mjs;
2. เพิ่มฟิลด์ประเภทลงในไฟล์ package.json ล่าสุดด้วยค่า "โมดูล"
3. สตริงถูกส่งผ่านไปยัง --eval
เป็นพารามิเตอร์ หรือส่งไปยังโหนดผ่านไปป์ STDIN ด้วยแฟล็ก --input-type=module
ตัวอย่างเช่น:
โหนด --input-type = โมดูล --eval "นำเข้า { กันยายน } จาก 'โหนด: เส้นทาง'; console.log(กันยายน);"
ESM สามารถแยกวิเคราะห์และแคชเป็น URL ได้ (ซึ่งหมายความว่าอักขระพิเศษจะต้องเข้ารหัสเป็นเปอร์เซ็นต์) รองรับโปรโตคอล URL เช่น file:
node:
และ data:
ไฟล์:URL
โมดูลจะถูกโหลดหลายครั้งหากตัวระบุการนำเข้าที่ใช้ในการแก้ไขโมดูลมีการสืบค้นหรือส่วนที่แตกต่างกัน
// ถือว่าเป็นสองโมดูลที่แตกต่างกัน import './foo.mjs?query=1'; นำเข้า './foo.mjs?query=2';
ข้อมูล:URL
รองรับการนำเข้าโดยใช้ประเภท MIME:
text/javascript
สำหรับโมดูล ES
application/json
สำหรับ JSON
application/wasm
สำหรับ Wasm
นำเข้า 'data:text/javascript,console.log("hello!");'; นำเข้า _ จาก 'data:application/json,"world!"' assert { type: 'json' };
data:URL
จะแยกวิเคราะห์เฉพาะตัวระบุแบบเปลือยและแบบสัมบูรณ์สำหรับโมดูลในตัว การแยกวิเคราะห์ตัวระบุแบบสัมพันธ์ไม่ทำงานเนื่องจาก data:
ไม่ใช่โปรโตคอลพิเศษ และไม่มีแนวคิดเกี่ยวกับการแยกวิเคราะห์แบบสัมพันธ์
การยืนยันการนำเข้า <br/>แอตทริบิวต์นี้จะเพิ่มไวยากรณ์แบบอินไลน์ให้กับคำสั่งการนำเข้าโมดูลเพื่อส่งข้อมูลเพิ่มเติมถัดจากตัวระบุโมดูล
นำเข้า fooData จาก './foo.json' ยืนยัน { ประเภท: 'json' }; const { ค่าเริ่มต้น: barData } = รอการนำเข้า ('./bar.json', { ยืนยัน: { ประเภท: 'json' } });
ขณะนี้รองรับเฉพาะโมดูล JSON และจำเป็นต้องมีไวยากรณ์ assert { type: 'json' }
การนำเข้าโมดูลการซัก <br/>การนำเข้าโมดูล WebAssembly ได้รับการสนับสนุนภายใต้แฟล็ก --experimental-wasm-modules
ซึ่งช่วยให้สามารถนำเข้าไฟล์ .wasm ใดๆ ก็ได้เป็นโมดูลปกติ ขณะเดียวกันก็สนับสนุนการนำเข้าโมดูลด้วย
//ดัชนี.mjs นำเข้า * เป็น M จาก './module.wasm'; console.log(M)
ใช้คำสั่งต่อไปนี้เพื่อดำเนินการ:
โหนด -- ทดลอง-wasm-โมดูล index.mjs
await สามารถใช้ที่ระดับบนสุดใน ESM
// a.mjs ส่งออก const five = รอ Promise.resolve (5) // b.mjs นำเข้า { ห้า } จาก './a.mjs' console.log(ห้า) // 5
ตามที่กล่าวไว้ก่อนหน้านี้ ความละเอียดของคำสั่ง import ของการขึ้นต่อกันของโมดูลเป็นแบบคงที่ ดังนั้นจึงมีข้อจำกัดที่มีชื่อเสียงสองประการ:
ตัวระบุโมดูลไม่สามารถรอจนกว่ารันไทม์จะสร้างได้
คำสั่งการนำเข้าโมดูลจะต้องเขียนที่ด้านบนของไฟล์ และไม่สามารถซ้อนกันในคำสั่งโฟลว์ควบคุมได้
อย่างไรก็ตาม ในบางสถานการณ์ ข้อจำกัดทั้งสองนี้เข้มงวดเกินไปอย่างไม่ต้องสงสัย ตัวอย่างเช่น มีข้อกำหนดที่ค่อนข้างทั่วไป: การโหลดแบบ Lazy Loading :
เมื่อพบกับโมดูลขนาดใหญ่ คุณจะต้องโหลดโมดูลขนาดใหญ่นี้เฉพาะเมื่อคุณต้องการใช้ฟังก์ชันบางอย่างในโมดูลจริงๆ เท่านั้น
เพื่อจุดประสงค์นี้ ESM จัดให้มีกลไกการแนะนำแบบอะซิงโครนัส การดำเนินการแนะนำนี้สามารถทำได้ผ่านตัวดำเนินการ import()
เมื่อโปรแกรมกำลังทำงาน จากมุมมองทางวากยสัมพันธ์ จะเทียบเท่ากับฟังก์ชันที่ได้รับตัวระบุโมดูลเป็นพารามิเตอร์และส่งคืนสัญญา หลังจากแก้ไขสัญญาแล้ว สามารถรับออบเจ็กต์โมดูลที่แยกวิเคราะห์ได้
ใช้ตัวอย่างการขึ้นต่อกันแบบวงกลมเพื่อแสดงกระบวนการโหลด ESM:
//index.js นำเข้า * เป็น foo จาก './foo.js'; นำเข้า * เป็นแถบจาก './bar.js'; console.log(ฟู); console.log(บาร์); //foo.js นำเข้า * เป็น Bar จาก './bar.js' ส่งออกให้โหลด = false; ส่งออกแถบ const = บาร์; โหลด = จริง; //bar.js นำเข้า * เป็น Foo จาก './foo.js'; ส่งออกให้โหลด = false; ส่งออก const foo = Foo; โหลดแล้ว = จริง
มาดูผลการวิ่งกันก่อน:
สังเกตได้จากการโหลดว่าทั้งโมดูล foo และ bar สามารถบันทึกข้อมูลโมดูลทั้งหมดที่โหลดได้ แต่ CommonJS นั้นแตกต่างออกไป จะต้องมีโมดูลที่ไม่สามารถพิมพ์ออกมาได้ว่าจะเป็นอย่างไรหลังจากโหลดจนเต็มแล้ว
มาดำดิ่งสู่กระบวนการโหลดเพื่อดูว่าเหตุใดผลลัพธ์นี้จึงเกิดขึ้น
กระบวนการโหลดสามารถแบ่งออกเป็นสามขั้นตอน:
ขั้นแรก: การวิเคราะห์
ขั้นตอนที่สอง: การประกาศ
ขั้นตอนที่สาม: การประหารชีวิต
ขั้นตอนการแยกวิเคราะห์:
ล่ามเริ่มต้นจากไฟล์รายการ (นั่นคือ index.js) วิเคราะห์การขึ้นต่อกันระหว่างโมดูล และแสดงในรูปแบบของกราฟ กราฟนี้เรียกอีกอย่างว่ากราฟการพึ่งพา
ในขั้นตอนนี้ เรามุ่งเน้นเฉพาะคำสั่งนำเข้าและโหลดซอร์สโค้ดที่สอดคล้องกับโมดูลที่คำสั่งเหล่านี้ต้องการแนะนำ และรับกราฟการพึ่งพาสุดท้ายผ่านการวิเคราะห์เชิงลึก ใช้ตัวอย่างข้างต้นเพื่อแสดงให้เห็นว่า:
1. เริ่มต้นจาก index.js ค้นหาคำสั่ง import * as foo from './foo.js'
แล้วไปที่ไฟล์ foo.js
2. แยกวิเคราะห์ไฟล์ foo.js ต่อไป และค้นหาคำสั่ง import * as Bar from './bar.js'
จากนั้นไปที่ bar.js
3. แยกวิเคราะห์จาก bar.js ต่อไป และค้นหาคำสั่ง import * as Foo from './foo.js'
ซึ่งก่อให้เกิดการพึ่งพาแบบวงกลม อย่างไรก็ตาม เนื่องจากล่ามกำลังประมวลผลโมดูล foo.js อยู่แล้ว จึงไม่สามารถป้อนได้ อีกครั้ง แล้วดำเนินการต่อ
4. หลังจากแยกวิเคราะห์โมดูล bar พบว่าไม่มีคำสั่งนำเข้า จึงกลับไปที่ foo.js และแยกวิเคราะห์ต่อไป ไม่พบคำสั่งการนำเข้าอีกเลย และดัชนี js ก็ถูกส่งคืน
5. พบ import * as bar from './bar.js'
ใน index.js แต่เนื่องจาก bar.js ได้ถูกแยกวิเคราะห์แล้ว จึงถูกข้ามและดำเนินการต่อไป
สุดท้าย กราฟการขึ้นต่อกันจะแสดงอย่างสมบูรณ์โดยอาศัยแนวทางเชิงลึกเป็นอันดับแรก:
ขั้นตอนการประกาศ:
ล่ามเริ่มต้นจากกราฟการพึ่งพาที่ได้รับและประกาศแต่ละโมดูลตามลำดับจากล่างขึ้นบน โดยเฉพาะอย่างยิ่งทุกครั้งที่เข้าถึงโมดูล คุณสมบัติทั้งหมดที่โมดูลส่งออกจะถูกค้นหาและตัวระบุของค่าที่ส่งออกจะถูกประกาศในหน่วยความจำ โปรดทราบว่าในขั้นตอนนี้จะมีเพียงการประกาศเท่านั้นและไม่มีการดำเนินการมอบหมายใดๆ
1. ล่ามเริ่มต้นจากโมดูล bar.js และประกาศตัวระบุของ load และ foo
2. ติดตามย้อนกลับไปยังโมดูล foo.js และประกาศตัวระบุที่โหลดและแถบ
3. เรามาถึงโมดูล index.js แล้ว แต่โมดูลนี้ไม่มีคำสั่งการส่งออก ดังนั้นจึงไม่มีการประกาศตัวระบุ
หลังจากประกาศตัวระบุการส่งออกทั้งหมดแล้ว โปรดดูกราฟอ้างอิงอีกครั้งเพื่อเชื่อมโยงความสัมพันธ์ระหว่างการนำเข้าและการส่งออก
จะเห็นได้ว่าความสัมพันธ์ที่คล้ายกับ const ถูกสร้างขึ้นระหว่างโมดูลที่แนะนำโดยการนำเข้าและค่าที่ส่งออกโดยการส่งออก ฝั่งนำเข้าสามารถอ่านได้เท่านั้น แต่ไม่สามารถเขียนได้ ยิ่งไปกว่านั้น โมดูลบาร์ที่อ่านใน index.js และโมดูลบาร์ที่อ่านใน foo.js นั้นเป็นอินสแตนซ์เดียวกันโดยพื้นฐานแล้ว
นี่คือเหตุผลว่าทำไมผลลัพธ์ของการแยกวิเคราะห์ที่สมบูรณ์จึงออกมาในผลลัพธ์ของตัวอย่างนี้
สิ่งนี้แตกต่างโดยพื้นฐานจากแนวทางที่ใช้โดยระบบ CommonJS หากโมดูลนำเข้าโมดูล CommonJS ระบบจะคัดลอกอ็อบเจ็กต์การส่งออกทั้งหมดของโมดูลหลังและคัดลอกเนื้อหาไปยังโมดูลปัจจุบัน ในกรณีนี้ หากโมดูลที่นำเข้าแก้ไขตัวแปรการคัดลอกของตัวเอง ผู้ใช้จะไม่สามารถเห็นค่าใหม่ได้ .
ขั้นตอนการดำเนินการ:
ในขั้นตอนนี้ เอ็นจิ้นจะดำเนินการโค้ดของโมดูล กราฟการขึ้นต่อกันยังคงเข้าถึงได้ตามลำดับจากล่างขึ้นบน และไฟล์ที่เข้าถึงจะถูกดำเนินการทีละไฟล์ การดำเนินการเริ่มต้นจากไฟล์ bar.js ไปจนถึง foo.js และสุดท้ายคือ index.js ในกระบวนการนี้ ค่าของตัวระบุในตารางการส่งออกจะค่อยๆ ปรับปรุง
กระบวนการนี้ดูเหมือนจะไม่แตกต่างจาก CommonJS มากนัก แต่จริงๆ แล้วมีความแตกต่างที่สำคัญอยู่ เนื่องจาก CommonJS เป็นแบบไดนามิก จึงแยกวิเคราะห์กราฟการพึ่งพาขณะเรียกใช้ไฟล์ที่เกี่ยวข้อง ตราบเท่าที่คุณเห็นคำสั่ง need คุณสามารถมั่นใจได้ว่าเมื่อโปรแกรมมาถึงคำสั่งนี้ โค้ดก่อนหน้านี้ทั้งหมดได้ถูกดำเนินการแล้ว ดังนั้นคำสั่ง need ไม่จำเป็นต้องปรากฏที่จุดเริ่มต้นของไฟล์ แต่สามารถปรากฏได้ทุกที่ และตัวระบุโมดูลก็สามารถสร้างจากตัวแปรได้เช่นกัน
แต่ ESM นั้นแตกต่างออกไป ใน ESM สามขั้นตอนข้างต้นจะถูกแยกออกจากกัน โดยจะต้องสร้างกราฟการพึ่งพาให้สมบูรณ์ก่อนจึงจะสามารถรันโค้ดได้ ดังนั้น การดำเนินการของการแนะนำโมดูลและการส่งออกโมดูลจะต้องคงที่ ไม่ต้องรอจนกว่าโค้ดจะถูกดำเนินการ
นอกจากความแตกต่างหลายประการที่กล่าวถึงข้างต้นแล้ว ยังมีความแตกต่างบางประการที่ควรสังเกตด้วย:
เมื่อใช้คีย์เวิร์ดนำเข้าใน ESM เพื่อแก้ไขตัวระบุแบบสัมพัทธ์หรือแบบสัมบูรณ์ จะต้องระบุนามสกุลไฟล์และต้องระบุดัชนีไดเร็กทอรี ('./path/index.js') ให้ครบถ้วน ฟังก์ชัน CommonJS need อนุญาตให้ละเว้นส่วนขยายนี้
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
จะถูกสร้างขึ้น:
นำเข้า { fileURLToPath } จาก 'url' นำเข้า { dirname } จาก 'เส้นทาง' const __filename = fileURLToPath (import.meta.url) const __dirname = dirname(__ชื่อไฟล์)
นอกจากนี้ยังสามารถจำลองฟังก์ชัน need() ใน CommonJS ได้อีกด้วย
นำเข้า { createRequire } จาก 'โมดูล' const ต้องการ = createRequire (import.meta.url)
ในขอบเขตโกลบอล ESM ไม่ได้กำหนดไว้ แต่ในระบบโมดูล CommonJS เป็นการอ้างอิงถึงการส่งออก:
//อีเอสเอ็ม console.log(นี่) // ไม่ได้กำหนด // CommonJS console.log (สิ่งนี้ === ส่งออก) // จริง
ดังที่ได้กล่าวไว้ข้างต้น สามารถจำลองฟังก์ชัน CommonJS need() ใน ESM เพื่อโหลดโมดูล CommonJS ได้ นอกจากนี้ คุณยังสามารถใช้ไวยากรณ์การนำเข้ามาตรฐานเพื่อแนะนำโมดูล CommonJS ได้ แต่วิธีการนำเข้านี้สามารถนำเข้าเฉพาะสิ่งที่ส่งออกตามค่าเริ่มต้นเท่านั้น:
นำเข้า packageMain จาก 'commonjs-package' // เป็นไปได้อย่างสมบูรณ์ที่จะนำเข้า { method } จาก 'commonjs-package' // ข้อผิดพลาด
ความต้องการของโมดูล CommonJS จะถือว่าไฟล์ที่อ้างอิงเป็น CommonJS เสมอ ไม่รองรับการโหลดโมดูล ES โดยใช้ need เนื่องจากโมดูล ES มีการดำเนินการแบบอะซิงโครนัส แต่คุณสามารถใช้ import()
เพื่อโหลดโมดูล ES จากโมดูล CommonJS
แม้ว่า ESM เปิดตัวมาเป็นเวลา 7 ปีแล้ว แต่ node.js ก็สนับสนุนมันอย่างเสถียรเช่นกัน เมื่อเราพัฒนาไลบรารีส่วนประกอบ เราสามารถรองรับได้เฉพาะ ESM เท่านั้น แต่เพื่อให้เข้ากันได้กับโปรเจ็กต์เก่า การสนับสนุน CommonJS ก็เป็นสิ่งจำเป็นเช่นกัน มีสองวิธีที่ใช้กันอย่างแพร่หลายในการสร้างไลบรารีส่วนประกอบที่สนับสนุนการส่งออกจากทั้งสองระบบโมดูล
เขียนแพ็คเกจใน CommonJS หรือแปลงซอร์สโค้ดของโมดูล ES เป็น CommonJS และสร้างไฟล์ wrapper ของโมดูล ES ที่กำหนดชื่อการส่งออก ใช้การส่งออกแบบมีเงื่อนไข การนำเข้าใช้ wrapper โมดูล ES และกำหนดให้ใช้จุดเข้า CommonJS ตัวอย่างเช่น ในโมดูลตัวอย่าง
// package.json - "type": "โมดูล", "ส่งออก": { "นำเข้า": "./wrapper.mjs", "ต้องการ": "./index.cjs" - -
ใช้ส่วนขยายการแสดงผล .cjs
และ .mjs
เนื่องจากการใช้เพียง .js
จะเป็นค่าเริ่มต้นเป็น CommonJS หรือ "type": "module"
จะทำให้ไฟล์เหล่านี้ถือเป็นโมดูล ES
// ./index.cjs ส่งออกชื่อ = 'ชื่อ'; // ./wrapper.mjs นำเข้า cjsModule จาก './index.cjs' ส่งออกชื่อ const = cjsModule.name;
ในตัวอย่างนี้:
// ใช้ ESM เพื่อแนะนำการนำเข้า { ชื่อ } จาก 'ตัวอย่าง' // ใช้ CommonJS เพื่อแนะนำ const { name } = need('example')
ชื่อที่แนะนำทั้งสองวิธีคือซิงเกิลตันเดียวกัน
ไฟล์ package.json สามารถกำหนดจุดเข้าใช้งานโมดูล CommonJS และ ES แยกกันได้โดยตรง:
// package.json - "type": "โมดูล", "ส่งออก": { "นำเข้า": "./index.mjs", "ต้องการ": "./index.cjs" - -
ซึ่งสามารถทำได้หากเวอร์ชัน CommonJS และ ESM เทียบเท่ากัน เช่น เนื่องจากเวอร์ชันหนึ่งเป็นเอาต์พุตแบบ transpiled ของเวอร์ชันอื่น และการจัดการสถานะของแพ็กเกจถูกแยกออกอย่างระมัดระวัง (หรือแพ็กเกจไม่มีสถานะ)
สถานะเหตุผลคือปัญหาเนื่องจากอาจใช้ทั้งเวอร์ชัน CommonJS และ ESM ในแอปพลิเคชัน ตัวอย่างเช่น รหัสผู้อ้างอิงของผู้ใช้สามารถนำเข้าเวอร์ชัน ESM ในขณะที่การขึ้นต่อกันต้องใช้เวอร์ชัน CommonJS หากสิ่งนี้เกิดขึ้น สำเนาสองชุดของแพ็คเกจจะถูกโหลดลงในหน่วยความจำ ดังนั้นสถานะที่แตกต่างกันสองสถานะจะเกิดขึ้น ซึ่งอาจนำไปสู่ข้อผิดพลาดที่ยากต่อการแก้ไข
นอกเหนือจากการเขียนแพ็คเกจไร้สัญชาติ (เช่น หาก Math ของ JavaScript เป็นแพ็คเกจ มันจะไร้สัญชาติเพราะวิธีการทั้งหมดเป็นแบบคงที่) ยังมีวิธีแยกสถานะเพื่อให้สามารถใช้ใน CommonJS และ ESM ที่อาจโหลดได้ แบ่งปันระหว่าง อินสแตนซ์แพ็คเกจ:
หากเป็นไปได้ ให้รวมสถานะทั้งหมดไว้ในออบเจ็กต์ที่สร้างอินสแตนซ์ ตัวอย่างเช่น จำเป็นต้องสร้างอินสแตนซ์ Date ของ JavaScript เพื่อให้มีสถานะ หากเป็นแพ็คเกจ ก็จะใช้ดังนี้:
วันที่นำเข้าจาก 'วันที่'; const someDate = วันที่ใหม่ (); // someDate มีสถานะ; วันที่ไม่มี
ไม่จำเป็นต้องใช้คีย์เวิร์ดใหม่ ฟังก์ชันแพ็คเกจสามารถส่งคืนออบเจ็กต์ใหม่หรือแก้ไขออบเจ็กต์ที่ส่งผ่านเพื่อรักษาสถานะภายนอกแพ็คเกจ
แยกสถานะในไฟล์ CommonJS อย่างน้อย 1 ไฟล์ที่แชร์ระหว่างแพ็คเกจเวอร์ชัน CommonJS และ ESM ตัวอย่างเช่น จุดเริ่มต้นสำหรับ CommonJS และ ESM คือ index.cjs และ index.mjs ตามลำดับ:
//index.cjs รัฐ const = ต้องการ ('./state.cjs') module.exports.state = รัฐ; //ดัชนี.mjs นำเข้าสถานะจาก './state.cjs' ส่งออก { สถานะ -
แม้ว่าตัวอย่างจะถูกใช้ในแอปพลิเคชันผ่านทาง need และ import ทุกการอ้างอิงถึงตัวอย่างจะมีสถานะเดียวกัน และการเปลี่ยนแปลงสถานะโดยระบบโมดูลใดระบบหนึ่งจะมีผลกับทั้งสองระบบ
หากบทความนี้มีประโยชน์สำหรับคุณ โปรดกดไลค์และสนับสนุนบทความนี้ด้วย "ไลค์" ของคุณคือแรงผลักดันให้ฉันสร้างสรรค์ผลงานต่อไป
บทความนี้อ้างอิงข้อมูลต่อไปนี้:
เอกสารอย่างเป็นทางการของ node.js
รูปแบบการออกแบบ Node.js