ข้ามไปยังเนื้อหา

โมดูล & npm

Node.js มาพร้อมกับระบบโมดูลสองแบบ CommonJS (CJS) คือระบบดั้งเดิมที่ใช้ require() สำหรับโหลดโมดูลแบบ synchronous ECMAScript Modules (ESM) คือมาตรฐานสมัยใหม่ที่ใช้ร่วมกับ browser และใช้คำสั่ง import / export

ทั้งสองระบบได้รับการรองรับอย่างสมบูรณ์ใน Node.js เวอร์ชันปัจจุบัน ไฟล์จะใช้ระบบใดขึ้นอยู่กับนามสกุลไฟล์และฟิลด์ "type" ใน package.json ที่ใกล้ที่สุด

CommonJSESM
ไวยากรณ์require() / module.exportsimport / export
นามสกุลไฟล์.js (ค่าเริ่มต้น) หรือ .cjs.mjs หรือ .js เมื่อ "type": "module"
"type" ใน package.json"commonjs" (หรือไม่มี)"module"
การโหลดSynchronousAsynchronous (static analysis)
Top-level awaitไม่รองรับรองรับ
บทเรียนหัวข้อ
ภาพรวม (หน้านี้)การเปรียบเทียบ CJS กับ ESM และพื้นฐานระบบโมดูล
เจาะลึก package.jsonฟิลด์สำคัญ, semver ranges, และ npm init
การค้นหาโมดูลวิธีที่ Node ค้นหาไฟล์, exports map, และ bare specifiers
การจัดการ Dependenciesการติดตั้ง, อัปเดต, และตรวจสอบแพ็คเกจด้วย npm
การเผยแพร่แพ็คเกจการเตรียมและเผยแพร่แพ็คเกจไปยัง npm registry

โค้ดด้านล่างใช้ ESM import เพื่อโหลดโมดูลในตัวอย่าง path และแสดงตัวคั่นเส้นทางของแพลตฟอร์ม — ตัวอักษรที่ Node ใช้ระหว่างส่วนประกอบไดเรกทอรี (/ บน Unix, \ บน Windows):

Node.js
Output (verified on Node 24)
path.sep: /
Joined path: src/content/index.mdx

ฟิลด์ "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 ที่ควบคุมมันล้วนๆ ไม่ใช่จากตัวโค้ดเอง

Node.js
Output (verified on Node 24)
probe.js under { "type": "commonjs" }:
CJS (module global present)
identical probe.js under { "type": "module" }:
ESM (no module global)

ระบบโมดูลของไฟล์หนึ่งไม่ใช่ property ของตัวไฟล์เอง — มันเป็น property ของโฟลเดอร์ที่มันอยู่ Node ไม่เคยถาม “ไฟล์นี้หน้าตาเป็นยังไง” มันถาม “package.json ที่ใกล้ที่สุดเหนือไฟล์นี้บอกว่าอะไร” monorepo ที่มี 10 package สามารถมีคำตอบต่างกัน 10 แบบ อยู่เคียงข้างกัน โดยไม่ต้อง rename ไฟล์เลย ตราบใดที่แต่ละ package ประกาศ "type" ของตัวเอง

ตัวเลือกประโยชน์ต้นทุน
ระบุ "type" ชัดเจนในทุก packageresolve เร็ว (ไม่ต้อง 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
  • “"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 เป็นระบบโมดูลอื่นได้หลังการย้าย

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'))

CommonJS ใช้ไวยากรณ์อะไรในการโหลดโมดูลอื่น?
นามสกุลไฟล์อะไรที่บังคับให้ไฟล์ถูกถือว่าเป็น ES module เสมอ โดยไม่คำนึงถึง package.json?
ฟิลด์ใดใน package.json ที่สลับทั้งแพ็คเกจไปใช้ ESM เพื่อให้ไฟล์ .js ถูกถือว่าเป็น ES modules?
จะเกิดอะไรขึ้นเมื่อไฟล์นี้รัน?
// package.json (nearest to this file): { "type": "module" }
const data = require('./data.json');
console.log(data);
โฟลเดอร์ A มี package.json ที่มี "type": "commonjs"; โฟลเดอร์ B มี "type": "module" ทั้งคู่มีไฟล์ probe.js ที่มี source เดียวกันเป๊ะคือ console.log(typeof module); ไฟล์ทั้งสองจะพิมพ์อะไร?