โหมดมืด
บทที่ 1 — Modules + npm: ระบบเลือดของโปรเจกต์ Node
← บทที่ 0: Node คืออะไร | สารบัญ | บทที่ 2: Core Modules + process →
ทุกโปรเจกต์ Node เริ่มต้นด้วย 2 เรื่องนี้เสมอ และเป็น 2 เรื่องที่มือใหม่งงมากที่สุด:
- Module — แตกโค้ดเป็นหลายไฟล์แล้วเอามาต่อกันยังไง (
requirevsimport— ทำไมมี 2 แบบ?) - npm —
npm installทำอะไรกันแน่,package.jsonคืออะไร, ตัวเลขเวอร์ชัน^1.2.3แปลว่าอะไร (จะอธิบายเต็มในข้อ 6)
เข้าใจ 2 เรื่องนี้ = เลิกกลัว error สีแดงตอนตั้งโปรเจกต์
1. ทำไมต้องมี module
โปรแกรมจริงมีโค้ดเป็นพัน ๆ บรรทัด — ยัดไว้ไฟล์เดียวคือหายนะ module คือการแบ่งโค้ดเป็นไฟล์ ๆ แล้วให้แต่ละไฟล์ "ส่งออก" (export) สิ่งที่อยากให้ไฟล์อื่นใช้ และ "นำเข้า" (import) สิ่งที่ต้องการจากไฟล์อื่น
ปัญหาคือ Node มี 2 ระบบ module ที่ต้องรู้จักทั้งคู่ เพราะคุณจะเจอทั้งสองในโลกจริง:
text
CommonJS (CJS) ← ของเดิมของ Node ตั้งแต่ 2009 → require() / module.exports
ES Modules (ESM) ← มาตรฐานทางการของภาษา (ES2015) → import / export2. CommonJS (CJS) — ของดั้งเดิมของ Node
ตอน Node เกิด (2009) ภาษา JavaScript ยังไม่มี ระบบ module ทางการ Node เลยสร้างของตัวเองชื่อ CommonJS ขึ้นมาใช้ก่อน — ใช้ 2 คำ: require() (นำเข้า) และ module.exports (ส่งออก)
javascript
// math.cjs — ไฟล์ที่ "ส่งออก" ฟังก์ชัน
function add(a, b) {
return a + b;
}
function sub(a, b) {
return a - b;
}
// module.exports = object ที่บอกว่า "ไฟล์นี้ส่งออกอะไรบ้าง"
module.exports = { add, sub };javascript
// app.cjs — ไฟล์ที่ "นำเข้า" มาใช้
// require() = อ่านไฟล์นั้น, รันมัน, แล้วคืน module.exports ของมันกลับมา
const math = require("./math.cjs");
console.log(math.add(2, 3)); // 5
// หรือ destructure เอาเฉพาะที่ใช้ (destructure = แกะเอาเฉพาะตัวที่ต้องการออกมาจาก object/array)
const { sub } = require("./math.cjs");
console.log(sub(5, 2)); // 3จุดเด่นของ CJS: require() ทำงาน แบบ synchronous (synchronous = ทำเสร็จทีละบรรทัดตามลำดับ อ่านไฟล์เสร็จแล้วค่อยทำบรรทัดถัดไป) และเรียกที่ไหนก็ได้กลางโค้ด — ยืดหยุ่นแต่ไม่ค่อยเหมาะกับการ optimize (optimize = ทำให้โค้ดรันเร็วขึ้น/เล็กลง เช่นตัดของที่ import แล้วไม่ได้ใช้ทิ้ง — เครื่องมือทำแบบนี้ได้ง่ายกว่าถ้ารู้ล่วงหน้าว่าไฟล์ import อะไรบ้างตั้งแต่ต้น ซึ่ง require() กลางโค้ดทำให้รู้ล่วงหน้าไม่ได้)
💡 อัปเดต 2026: เดิมที CJS กับ ESM แยกขาดกันชัดเจน (CJS โหลด ESM ไม่ได้ ต้อง dynamic import) — แต่ Node เวอร์ชันใหม่ ๆ เพิ่มความสามารถ
require(esm)ให้ CJS โหลดไฟล์ ESM แบบ synchronous ได้แล้ว (ยังมีข้อจำกัด เช่น ไฟล์ ESM นั้นต้องไม่มี top-level await) — ตรวจสอบเวอร์ชัน Node ขั้นต่ำที่แน่ชัดในเอกสารทางการก่อนใช้ เพราะฟีเจอร์นี้ยังค่อนข้างใหม่ เส้นแบ่งระหว่าง 2 ระบบเริ่มเบลอลง แต่ของใหม่ก็ยัง เลือก ESM ไว้ก่อน
3. ES Modules (ESM) — มาตรฐานทางการของภาษา (ใช้ตัวนี้ในของใหม่)
ปี 2015 ภาษา JavaScript มีระบบ module ทางการแล้ว: import / export — ตัวเดียวกับที่ใช้ใน React/frontend และเป็น มาตรฐานที่ควรใช้กับโปรเจกต์ใหม่
javascript
// math.mjs — ส่งออกด้วย export
export function add(a, b) {
return a + b;
}
export function sub(a, b) {
return a - b;
}
// จะมี "default export" 1 ตัวต่อไฟล์ก็ได้
export default function multiply(a, b) {
return a * b;
}javascript
// app.mjs — นำเข้าด้วย import
// ⚠️ ESM ต้องใส่นามสกุลไฟล์เต็ม ๆ ใน relative import เสมอ ("./math.mjs" ไม่ใช่ "./math")
import multiply, { add, sub } from "./math.mjs";
// ↑ default ↑ named exports (ต้องชื่อตรงกับที่ export)
console.log(add(2, 3)); // 5
console.log(multiply(2, 4)); // 8
// import ทั้งหมดเป็น namespace ก็ได้
import * as math from "./math.mjs";
console.log(math.sub(5, 2)); // 3ความต่างที่สำคัญ: import ต้องอยู่ บนสุดของไฟล์ (static = ตายตัว เขียนได้ที่เดียวบนสุด ย้ายไปกลางโค้ดไม่ได้) — Node อ่าน import ทั้งหมดก่อนเริ่มรัน ทำให้ optimize/วิเคราะห์ได้ดีกว่า ถ้าจะ import แบบมีเงื่อนไขกลางโค้ดต้องใช้ dynamic import (dynamic = ยืดหยุ่น เรียกตอนไหนก็ได้) ซึ่งคืน Promise (Promise = วัตถุที่แทนงานที่จะเสร็จในอนาคต — บทที่ 3 อธิบายเต็ม):
javascript
// dynamic import — ใช้ตอนต้องการโหลด module แบบมีเงื่อนไข / lazy
// process.env.DEBUG = ตัวแปรสภาพแวดล้อมชื่อ DEBUG (ตั้งค่าจากนอกโปรแกรม เช่น `DEBUG=1 node app.js`) — อธิบายเต็มบทที่ 2
if (process.env.DEBUG) {
const { inspect } = await import("node:util"); // คืน Promise → ต้อง await
console.log(inspect(someObject));
}💡
awaitกลางโค้ดได้ทำไม? ปกติawait(= "รอผลของ Promise") ต้องอยู่ในasync function(ฟังก์ชันที่รองรับงาน async — บทที่ 3 อธิบายเต็ม) — แต่ Node 14.8+ รองรับ top-level await ในไฟล์ ESM (มี"type": "module"หรือไฟล์.mjs) เลยใช้await import(...)ได้ตรง ๆ ที่ระดับบนสุดของไฟล์ไม่ต้องห่อฟังก์ชัน · ใน CommonJS ยังต้องห่อด้วยasync functionแล้วเรียกเสมอ
4. Node รู้ได้ยังไงว่าไฟล์นี้เป็น CJS หรือ ESM
นี่คือจุดที่มือใหม่พังบ่อยที่สุด Node ตัดสินจาก 2 อย่าง:
| เงื่อนไข | Node มองเป็น |
|---|---|
นามสกุล .mjs | ESM เสมอ |
นามสกุล .cjs | CommonJS เสมอ |
นามสกุล .js และ package.json มี "type": "module" | ESM |
นามสกุล .js และ ไม่มี "type" (หรือ "type": "commonjs") | CommonJS (ค่า default เก่า) |
แปลว่า: ในโปรเจกต์ใหม่ ใส่ "type": "module" ใน package.json แล้วไฟล์ .js ทุกไฟล์จะเป็น ESM อัตโนมัติ — เป็นวิธีที่แนะนำในปี 2026
⚠️ Pitfall ที่เจอบ่อยสุด: เขียน
importในไฟล์.jsแต่ลืมใส่"type": "module"→ Node เด้ง errorSyntaxError: Cannot use import statement outside a module(แปล: "ใช้คำสั่ง import นอกไฟล์ที่เป็น module ไม่ได้") — แก้ด้วยการเพิ่ม"type": "module"หรือเปลี่ยนนามสกุลเป็น.mjs(สำหรับคน TypeScript ใช้.mtsเป็นคู่)
ปัญหา ESM ที่ต้องรู้: ไม่มี __dirname / require
ใน CJS มีตัวแปรวิเศษ __dirname (path โฟลเดอร์ปัจจุบัน) และ require ให้ใช้ฟรี — ใน ESM ไม่มี ต้องสร้างเอง:
javascript
// ESM: หา path ของไฟล์/โฟลเดอร์ปัจจุบัน
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
// import.meta.url = URL ของไฟล์นี้ เช่น 'file:///home/app/index.js'
const __filename = fileURLToPath(import.meta.url); // → /home/app/index.js
const __dirname = dirname(__filename); // → /home/app
// Node 20.11+ มีทางลัด:
console.log(import.meta.dirname); // → /home/app (ไม่ต้องแปลงเอง)💡 จำง่าย ๆ: เห็น
require/module.exports/__dirname= CJS · เห็นimport/export/import.meta= ESM · โปรเจกต์ใหม่เลือก ESM
5. package.json — บัตรประชาชนของโปรเจกต์
ทุกโปรเจกต์ Node มีไฟล์ package.json ที่ราก — เป็น "บัตรประชาชน + รายการของที่ต้องใช้" สร้างด้วย:
bash
npm init -y # -y = ตอบ yes ทุกข้อ สร้างไฟล์ default ให้เลยได้ไฟล์ประมาณนี้ (ตัวอย่างนี้เพิ่ม // comment เพื่ออธิบายแต่ละ field):
⚠️ อย่าก๊อปตัวอย่างนี้ไปวางใน
package.jsonตรง ๆ — JSON มาตรฐาน ไม่รองรับ comment จะ parse ไม่ผ่าน · ตัวอย่างนี้ใช้ fencejsonc(JSON with Comments) เพื่ออ่านง่ายเท่านั้น เวลาใช้จริงให้ลบ//ทั้งหมดออก
jsonc
{
"name": "my-app", // ชื่อ package (ตัวเล็ก ไม่มีเว้นวรรค)
"version": "1.0.0", // เวอร์ชันของเรา (รูปแบบ semver)
"type": "module", // ⭐ บอกว่าไฟล์ .js เป็น ESM
"main": "index.js", // ไฟล์เริ่มต้นเมื่อคนอื่น require/import package นี้
"scripts": { // คำสั่งลัด (ดูข้อ 8)
"start": "node index.js",
"dev": "node --watch index.js",
"test": "node --test"
},
"dependencies": { // library ที่ต้องใช้ตอนรันจริง (production = ของจริงที่ผู้ใช้ใช้งาน)
"express": "^5.0.0"
},
"devDependencies": { // library ที่ใช้แค่ตอนพัฒนา/เทส (ไม่ขึ้น production)
"vitest": "^3.0.0"
}
}dependencies vs devDependencies — ต่างกันตรงไหนและทำไมต้องแยก:
- dependencies = ของที่โค้ด production ต้องใช้ (เช่น
express— ถ้าไม่มี server รันไม่ได้) - devDependencies = ของที่ใช้แค่ตอนพัฒนา (เช่น
vitestทดสอบ,eslintตรวจโค้ด,typescriptแปลงโค้ด) — ตอน build (= แปลงโค้ดให้พร้อมรันจริง) ขึ้น production (= เซิร์ฟเวอร์จริงที่ผู้ใช้เข้าใช้งาน) จะnpm ci --omit=devเพื่อไม่ติดตั้งของกลุ่มนี้ → image (= แพ็คเกจของ Docker ที่บรรจุโปรแกรม + ไฟล์ที่ต้องใช้รวมกัน — Docker คือเครื่องมือห่อโปรแกรมให้รันเหมือนกันทุกเครื่อง ยังไม่ต้องรู้รายละเอียดตอนนี้) เล็กลง ปลอดภัยขึ้น
6. semver — ตัวเลขเวอร์ชัน ^1.2.3 แปลว่าอะไร
npm ใช้ Semantic Versioning (semver) — เวอร์ชันมี 3 ตัวเลข MAJOR.MINOR.PATCH:
text
1 . 2 . 3
│ │ │
MAJOR MINOR PATCH
(พังของเก่า) (เพิ่มของ (แก้บั๊ก
breaking) ใหม่ ไม่พัง) ไม่เพิ่มของ)- PATCH (1.2.3 → 1.2.4) แก้บั๊ก ไม่เปลี่ยนพฤติกรรม → อัปได้สบาย
- MINOR (1.2.3 → 1.3.0) เพิ่มฟีเจอร์ใหม่ แต่ของเก่ายังใช้ได้ → อัปได้
- MAJOR (1.2.3 → 2.0.0) มี breaking change → อัปแล้วโค้ดอาจพัง ต้องอ่าน changelog
เครื่องหมายหน้าเวอร์ชันใน package.json บอกว่า "ยอมให้อัปขึ้นได้แค่ไหน":
| สัญลักษณ์ | ตัวอย่าง | ความหมาย |
|---|---|---|
^ (caret) | ^1.2.3 | อัปได้ถึงก่อน MAJOR ถัดไป → 1.x.x ใด ๆ ที่ ≥ 1.2.3 (default ของ npm) |
~ (tilde) | ~1.2.3 | อัปได้แค่ PATCH → 1.2.x ที่ ≥ 1.2.3 |
| ไม่มี | 1.2.3 | เป๊ะ ตัวนี้เท่านั้น |
* / latest | * | อะไรก็ได้ (อันตราย อย่าใช้) |
⚠️ เคสพิเศษของ
^กับเวอร์ชัน0.x:^0.2.3อัปได้แค่0.2.x(≥ 0.2.3) ไม่ขึ้น 0.3.0 เพราะ semver ถือว่าเวอร์ชัน 0.x ยังเป็นช่วงทดลอง แม้แต่การอัป minor (0.2 → 0.3) ก็อาจ break ได้
^ปลอดภัยตราบใดที่ library ทำตาม semver จริง — แต่บาง library ก็ปล่อย breaking change ใน minor (มนุษย์พลาดได้) นี่คือเหตุผลที่ต้องมี lockfile (ไฟล์ตรึงเวอร์ชัน — อธิบายเต็มในข้อ 7)
7. npm install ทำอะไร + ทำไม lockfile สำคัญที่สุด
bash
npm install express # ติดตั้ง express, เพิ่มลง dependencies
npm install -D vitest # -D = devDependencies
npm install -g @nestjs/cli # -g = global (ติดตั้งทั้งเครื่อง ใช้เป็น CLI — ตัวอย่างนี้แสดง syntax เฉย ๆ ในทางปฏิบัติปี 2026 แนะนำ npx แทน ดูย่อหน้าถัดไป)
npm install # ไม่ระบุชื่อ = ติดตั้งทุกอย่างใน package.json💡 2026 แนวคิด: การติดตั้งแบบ global (
-g) เป็นของยุคเก่า — ปัจจุบันแนะนำใช้npx <tool>(รันแบบไม่ติดตั้งถาวร — ดูข้อ 9) สำหรับงานครั้งคราว หรือใส่ในdevDependenciesของโปรเจกต์เพื่อตรึงเวอร์ชันให้ทุกคนในทีมตรงกัน · global install ทำให้เพื่อนร่วมทีมได้เวอร์ชันไม่ตรงกัน + ไม่ได้ถูกตรึงโดย lockfile
npm install ทำ 3 อย่าง:
- อ่าน
package.jsonดูว่าต้องการอะไรบ้าง - คำนวณว่าจะดึงเวอร์ชันไหนมา (ตาม
^/~) แล้วโหลดเก็บในnode_modules/(โฟลเดอร์ที่เก็บโค้ด library ทั้งหมด — ใหญ่มาก ห้าม commit ขึ้น git, ใส่.gitignore) - เขียน
package-lock.json— บันทึก "เวอร์ชันเป๊ะ ๆ ที่ติดตั้งจริง" ของทุก package รวมทั้ง dependency ของ dependency
ทำไม lockfile คือพระเอกตัวจริง: package.json บอกว่า "เอา express ^4.19.0" ซึ่งกว้าง — วันนี้ได้ 4.19.2 พรุ่งนี้อาจได้ 4.21.0 ทำให้เครื่องคุณกับเครื่องเพื่อน (และ server) ได้เวอร์ชันไม่ตรงกัน ผลคือเจอปัญหาแบบ "ในเครื่องผมรันได้นะ!" 🐛 (มุกยอดฮิตในวงการ dev ที่มักพูดตอนโค้ดรันได้ในเครื่องตัวเองแต่รันไม่ได้ที่อื่น เพราะเวอร์ชัน library ไม่ตรงกัน) package-lock.json ตรึงเวอร์ชันเป๊ะ ทุกคนได้เหมือนกันหมด — ต้อง commit ขึ้น git เสมอ
bash
# ใน CI / production ใช้คำสั่งนี้ ไม่ใช่ npm install:
# CI = Continuous Integration (ระบบ server ที่รัน test/build ของโปรเจกต์อัตโนมัติทุกครั้งที่มีคน push code)
npm ci # ติดตั้งจาก lockfile เป๊ะ ๆ (เร็วกว่า + ไม่แก้ lockfile) — ถ้า lockfile กับ package.json ไม่ตรงจะ error ทันที💡 กฎทอง:
node_modules/อยู่ใน.gitignore(สร้างใหม่ได้จาก lockfile) ·package-lock.jsoncommit เสมอ · บนเครื่องนักพัฒนาใช้npm install· ใน CI/production ใช้npm ci
8. npm scripts — คำสั่งลัดของโปรเจกต์
แทนที่จะจำคำสั่งยาว ๆ ตั้งชื่อสั้น ๆ ใน "scripts" แล้วเรียกด้วย npm run:
jsonc
"scripts": {
"start": "node index.js", // npm start (start/test เรียกได้โดยไม่ต้อง run)
"dev": "node --watch index.js", // npm run dev — --watch = restart อัตโนมัติเมื่อไฟล์เปลี่ยน
"test": "node --test", // npm test
"lint": "eslint .", // npm run lint
"build": "tsc" // npm run build
}bash
npm run dev # รัน "node --watch index.js"
npm start # ลัด: start กับ test ไม่ต้องพิมพ์ run
npm testข้อดี: คนเข้าทีมใหม่ไม่ต้องเดาว่ารันโปรเจกต์ยังไง — เปิด package.json ดู scripts ก็รู้
9. npx, pnpm, yarn — ญาติ ๆ ของ npm
npx— รัน CLI tool โดยไม่ต้องติดตั้งถาวร เหมาะกับ tool ที่ใช้ครั้งเดียว เช่นสร้างโปรเจกต์:bash# vite = เครื่องมือสร้าง/รันโปรเจกต์ frontend สมัยใหม่ (React/Vue/Svelte) ใช้บ่อยเป็นจุดเริ่ม npx create-vite@latest my-app # ดาวน์โหลด create-vite ชั่วคราว รันแล้วทิ้ง npx prettier --write . # รัน prettier ที่ติดตั้งในโปรเจกต์pnpm— package manager ทางเลือก เร็วกว่า npm มาก + ประหยัด disk (เก็บ package ไว้ที่เดียวแล้วใช้ลิงก์แทนการ copy ซ้ำทุกโปรเจกต์) — นิยมขึ้นเรื่อย ๆ คำสั่งคล้าย npm:pnpm install,pnpm add expressyarn— package manager รุ่นเก่ากว่า ยังเจอในโปรเจกต์เดิม ๆ
| npm | pnpm | yarn | |
|---|---|---|---|
| มากับ Node | ✅ | ❌ (ติดตั้งเอง) | ❌ |
| ความเร็ว | กลาง | เร็วสุด ⭐ | เร็ว |
| ประหยัด disk | ❌ | ✅ ⭐ | กลาง |
| lockfile | package-lock.json | pnpm-lock.yaml | yarn.lock |
💡 มือใหม่ใช้ npm ไปก่อน (มากับ Node ไม่ต้องลงอะไร) — เมื่อโปรเจกต์ใหญ่ขึ้นหรือทำ monorepo (= หลายโปรเจกต์/แพ็คเกจที่เก็บรวมในไฟล์ repo เดียวกัน) ค่อยขยับไป pnpm · อย่าใช้หลายตัวปนกันในโปรเจกต์เดียว (lockfile จะตีกัน)
🛠️ Checkpoint 1 — ลงมือก่อนไปบทถัดไป
- สร้างโฟลเดอร์ใหม่ →
npm init -y→ เพิ่ม"type": "module"ในpackage.json - สร้าง
math.js(exportadd,sub) และapp.js(import มาใช้) แล้วรันnode app.jsให้สำเร็จ - ลองลบ
"type": "module"ออก แล้วรันอีกครั้ง — อ่าน error ที่ได้ แล้วแก้กลับ (เข้าใจว่าทำไมพัง) npm install dayjsแล้ว import มา format วันที่ — เปิดดูpackage.jsonว่าdependenciesเปลี่ยนไหม, มีpackage-lock.jsonกับnode_modules/โผล่มาไหม- เพิ่ม script
"dev": "node --watch app.js"แล้วรันnpm run dev— แก้ไฟล์แล้วบันทึก ดูว่ามัน restart เองไหม - ใน ESM ลองพิมพ์
console.log(import.meta.dirname)เทียบกับการพยายามใช้__dirnameตรง ๆ (จะ error)
เฉลยข้อ 3: ได้
SyntaxError: Cannot use import statement outside a module— เพราะไม่มี"type": "module"Node เลยมองไฟล์.jsเป็น CommonJS ซึ่งไม่รู้จักimport
สรุปบทที่ 1
- Node มี 2 ระบบ module: CJS (
require/module.exports— ของเดิม) และ ESM (import/export— มาตรฐานทางการ ใช้กับของใหม่) - Node แยก CJS/ESM จาก นามสกุลไฟล์ (
.mjs/.cjs) หรือ"type"ใน package.json - ESM ไม่มี
__dirname/requireต้องใช้import.meta.dirname/import.meta.url package.json= บัตรประชาชน + รายการ dependency · แยกdependencies(รันจริง) กับdevDependencies(พัฒนา)- semver
^= อัปได้ถึงก่อน major ถัดไป,~= อัปแค่ patch package-lock.jsonตรึงเวอร์ชันเป๊ะ → commit เสมอ · CI/production ใช้npm cinpxรัน tool ชั่วคราว ·pnpmเร็ว+ประหยัด disk
บทต่อไปลงลึก "ของแถม" ที่ Node ให้มาฟรี — process, path, os และ global objects ที่เบราว์เซอร์ไม่มี