โมดูล & npm
ระบบโมดูลสองแบบ หนึ่งระบบนิเวศ
หัวข้อที่มีชื่อว่า “ระบบโมดูลสองแบบ หนึ่งระบบนิเวศ”Node.js มาพร้อมกับระบบโมดูลสองแบบ CommonJS (CJS) คือระบบดั้งเดิมที่ใช้ require() สำหรับโหลดโมดูลแบบ synchronous ECMAScript Modules (ESM) คือมาตรฐานสมัยใหม่ที่ใช้ร่วมกับ browser และใช้คำสั่ง import / export
ทั้งสองระบบได้รับการรองรับอย่างสมบูรณ์ใน Node.js เวอร์ชันปัจจุบัน ไฟล์จะใช้ระบบใดขึ้นอยู่กับนามสกุลไฟล์และฟิลด์ "type" ใน package.json ที่ใกล้ที่สุด
CJS เทียบกับ ESM โดยสรุป
หัวข้อที่มีชื่อว่า “CJS เทียบกับ ESM โดยสรุป”| CommonJS | ESM | |
|---|---|---|
| ไวยากรณ์ | require() / module.exports | import / export |
| นามสกุลไฟล์ | .js (ค่าเริ่มต้น) หรือ .cjs | .mjs หรือ .js เมื่อ "type": "module" |
"type" ใน package.json | "commonjs" (หรือไม่มี) | "module" |
| การโหลด | Synchronous | Asynchronous (static analysis) |
Top-level await | ไม่รองรับ | รองรับ |
สิ่งที่โมดูลนี้ครอบคลุม
หัวข้อที่มีชื่อว่า “สิ่งที่โมดูลนี้ครอบคลุม”| บทเรียน | หัวข้อ |
|---|---|
| ภาพรวม (หน้านี้) | การเปรียบเทียบ CJS กับ ESM และพื้นฐานระบบโมดูล |
| เจาะลึก package.json | ฟิลด์สำคัญ, semver ranges, และ npm init |
| การค้นหาโมดูล | วิธีที่ Node ค้นหาไฟล์, exports map, และ bare specifiers |
| การจัดการ Dependencies | การติดตั้ง, อัปเดต, และตรวจสอบแพ็คเกจด้วย npm |
| การเผยแพร่แพ็คเกจ | การเตรียมและเผยแพร่แพ็คเกจไปยัง npm registry |
ESM ในการทำงานจริง
หัวข้อที่มีชื่อว่า “ESM ในการทำงานจริง”โค้ดด้านล่างใช้ ESM import เพื่อโหลดโมดูลในตัวอย่าง path และแสดงตัวคั่นเส้นทางของแพลตฟอร์ม — ตัวอักษรที่ Node ใช้ระหว่างส่วนประกอบไดเรกทอรี (/ บน Unix, \ บน Windows):
Output (verified on Node 24)
path.sep: /
Joined path: src/content/index.mdxPackage scope: package.json ตัวไหนที่ควบคุมไฟล์นี้
หัวข้อที่มีชื่อว่า “Package scope: package.json ตัวไหนที่ควบคุมไฟล์นี้”ฟิลด์ "type" ไม่ใช่ switch ระดับโปรเจกต์ทั้งหมด — มัน scope อยู่ที่ package.json ที่ใกล้ที่สุด เหนือ ไฟล์ที่กำลังโหลด Node เดินขึ้นไปจาก directory ของไฟล์นั้นจนกว่าจะเจอไฟล์หนึ่ง โฟลเดอร์ย่อยที่มี package.json ของตัวเองจะสร้าง boundary ใหม่: ทุกอย่างข้างล่างมันจะทำตาม "type" ของไฟล์ใหม่ ไม่ใช่ไฟล์ที่อยู่สูงกว่าในโครงสร้าง dependency ใน node_modules ก็เป็น boundary แยกเช่นกัน — "type" ของคุณเองไม่มีทางไปถึง package ที่คุณติดตั้งมา
Demo ด้านล่างเขียน source text เดียวกันเป๊ะลงในสองโฟลเดอร์ที่ต่างกันแค่ "type" ใน package.json แล้วโหลดทั้งคู่ source นั้นไม่มีไวยากรณ์ import/export/require เลย — มีแค่ typeof module ซึ่งถูก define เฉพาะภายใน CommonJS module wrapper — ดังนั้นความแตกต่างของผลลัพธ์มาจาก package.json ที่ควบคุมมันล้วนๆ ไม่ใช่จากตัวโค้ดเอง
Output (verified on Node 24)
probe.js under { "type": "commonjs" }:
CJS (module global present)
identical probe.js under { "type": "module" }:
ESM (no module global)Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ระบบโมดูลของไฟล์หนึ่งไม่ใช่ property ของตัวไฟล์เอง — มันเป็น property ของโฟลเดอร์ที่มันอยู่ Node ไม่เคยถาม “ไฟล์นี้หน้าตาเป็นยังไง” มันถาม “package.json ที่ใกล้ที่สุดเหนือไฟล์นี้บอกว่าอะไร” monorepo ที่มี 10 package สามารถมีคำตอบต่างกัน 10 แบบ อยู่เคียงข้างกัน โดยไม่ต้อง rename ไฟล์เลย ตราบใดที่แต่ละ package ประกาศ "type" ของตัวเอง
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ตัวเลือก | ประโยชน์ | ต้นทุน |
|---|---|---|
ระบุ "type" ชัดเจนในทุก package | resolve เร็ว (ไม่ต้อง sniff ไฟล์), อ่านแล้วไม่คลุมเครือ | ต้องคอยดูแลให้ฟิลด์นี้ถูกต้องเมื่อ package เปลี่ยนแปลง |
พึ่ง detect-module (ไม่มีฟิลด์ "type") | Zero-config; Node 24 parse syntax ของ .js ที่กำกวมแล้วเดาได้ถูก | resolve ช้ากว่า; คนที่กวาดตาดู tree บอกระบบโมดูลไม่ได้ทันที |
ซ้อน package.json ตัวใหม่เพื่อสลับ "type" เฉพาะ subfolder | ทำให้โฟลเดอร์ CommonJS แบบเก่าอยู่ในโปรเจกต์ ESM ได้โดยไม่ต้อง rename ทุกไฟล์ | พลาดดูง่าย — “ทำไมโฟลเดอร์นี้ถึงต่างจากที่อื่น” เป็นจุดสับสนที่พบบ่อยตอน review |
ใช้นามสกุล .mjs/.cjs แทนการซ้อน package.json | ชัดเจนต่อไฟล์ ไม่ต้องคิดเรื่อง scope boundary | มี churn จากการ rename ไฟล์เดิม; tooling เก่าบางตัวรู้จักแค่ .js |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “
"type": "module"ที่ root package.json ของโปรเจกต์ครอบคลุมทุกอย่างที่รัน รวมถึง dependency ด้วย” ไม่จริง — ทุก package ที่ติดตั้งมีมีpackage.jsonและ boundary"type"ของตัวเอง"type"ของคุณควบคุมแค่ไฟล์ภายใน package ของคุณเอง - “ถ้าไม่มีฟิลด์
"type"ไฟล์.jsจะเป็น CommonJS ตลอดไป” ไม่ใช่อีกต่อไปตั้งแต่detect-moduleกลายเป็นค่าเริ่มต้น: Node 24 parse syntax ของไฟล์.jsที่กำกวมได้ และถือว่าเป็น ESM อัตโนมัติถ้ามันใช้import/exportการระบุ"type"ชัดเจนยังคงเป็นตัวเลือกที่เร็วกว่าและชัดเจนกว่า — แต่การไม่มีมันไม่ได้การันตี CommonJS แบบเดิมอีกแล้ว - “ย้ายไฟล์ไปโฟลเดอร์อื่นไม่เปลี่ยนระบบโมดูลของมัน ตราบใดที่นามสกุลยังเป็น
.js” ไม่จริง — ถ้าโฟลเดอร์ปลายทางอยู่ใต้ scope ของpackage.jsonคนละตัว เนื้อหาไฟล์เดียวกันเป๊ะสามารถถูก parse เป็นระบบโมดูลอื่นได้หลังการย้าย
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”Training data เต็มไปด้วยตัวอย่าง Node ก่อนปี 2020 ที่ไฟล์ .js ทุกไฟล์เป็น CommonJS โดยค่าเริ่มต้น โมเดลที่ไม่ได้ internalize "type": "module" ของโปรเจกต์จะเผลอใช้ require() ตามความเคยชิน แม้อยู่ใน codebase ที่ประกาศใช้ ESM ทุกที่อย่างชัดเจน
🐛 หา bug ในโค้ดนี้
// package.json (nearest to this file): { "type": "module" }const fs = require('node:fs');const data = require('./data.json');
console.log(data);เฉลย
package.json ที่ใกล้ที่สุดบอกว่า "type": "module" ดังนั้นไฟล์ .js นี้เป็น ES module — และ require ไม่ใช่ global ภายใน ES module มันจะ throw ReferenceError: require is not defined in ES module scope, you can use import instead ตั้งแต่บรรทัดแรก ก่อนที่ data.json จะถูกแตะด้วยซ้ำ วิธีแก้: ใช้ import fs from 'node:fs'; และสำหรับไฟล์ JSON ใช้ import data from './data.json' with { type: 'json' }; (Node 22.10+ import attributes) หรือ JSON.parse(fs.readFileSync('./data.json', 'utf8'))