Menu

CommonJS מול ES Modules: require מול import ב-JavaScript

שתי מערכות המודולים שמגיעות עם JavaScript, למה שתיהן קיימות, ואיך לבחור בין require ל-import בפרויקטים של Node.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

שתי מערכות מודולים, שפה אחת

במקור לא הייתה ל-JavaScript שום מערכת מודולים. Node מילאה את החור ב-2009 עם CommonJS (require, module.exports), ובמשך שנים כך נראה קוד Node. ואז ב-2015 השפה עצמה קיבלה מערכת מודולים סטנדרטית, ES Modules (import, export), שהיום גם דפדפנים וגם Node תומכים בה.

לכן תראו את שתיהן בשטח. הנה אותו מודול קטנטן כתוב בכל אחת מהדרכים:

אותה פונקציה, שתי מעטפות שונות. שאר העמוד עוסק במתי כל מעטפה משנה ובאיזו לבחור.

ההבדלים בתחביר

ההבדלים היומיומיים נכנסים על גלויה:

// CommonJS
const fs = require("fs");
const { readFile } = require("fs/promises");

module.exports = something;
module.exports.name = value;
exports.name = value;
// ES Modules
import fs from "fs";
import { readFile } from "fs/promises";

export default something;
export const name = value;
export { name };

require היא קריאה רגילה לפונקציה. import היא פקודה: היא יכולה להופיע רק ברמה העליונה של מודול, והנתיב חייב להיות מחרוזת ליטרלית. ההגבלה הזו לא שרירותית; היא מה שמאפשר ל-ESM לעשות דברים ש-CommonJS לא יכול.

ההבדל האמיתי: סטטי מול דינמי

CommonJS מעריך את require() כשהשורה רצה. אפשר לשים אותו בתוך if, לחשב את הנתיב בזמן ריצה, לטעון מודול לפי תנאי:

ES Modules הם סטטיים. המנוע מנתח את כל פקודות ה-import לפני שהוא מריץ קוד כלשהו, בונה גרף תלויות ופותר הכול מראש. לכן הנתיב חייב להיות מחרוזת ליטרלית, ולכן import מופיע רק ברמה העליונה.

התמורה: כלים יכולים לראות את כל גרף המודולים בלי להריץ כלום. כך bundlers עושים tree-shaking (השמטת exports שלא בשימוש), כך עורכים נותנים השלמה אוטומטית מדויקת, וכך הדפדפן יכול להוריד מודולים במקביל.

כשבאמת צריך טעינה דינמית ב-ESM, השתמשו ב-import(): ביטוי שנראה כמו פונקציה ומחזיר Promise:

איך Node מחליטה באיזו מערכת קובץ משתמש

פרויקט Node אחד יכול להכיל את שני סוגי הקבצים. Node קובעת באיזו מערכת כל קובץ משתמש לפי שני דברים:

  • סיומת הקובץ: .mjs הוא תמיד ESM, .cjs הוא תמיד CommonJS.
  • השדה "type" ב-package.json הקרוב ביותר: "module" אומר שקבצי .js הם ESM, ו-"commonjs" (ברירת המחדל אם השדה חסר) אומר שהם CJS.
// package.json
{
    "name": "my-app",
    "type": "module"
}

עם "type": "module", קובץ hello.js רגיל באותה חבילה משתמש ב-import/export. שימו לידו hello.cjs, והקובץ הזה בלבד ישתמש ב-require. כך פרויקט יכול לעבור בהדרגה, וכך ספרייה יכולה להפיץ את שתי הגרסאות זו לצד זו.

מלכודת למתחילים: בקובץ ESM, require ו-module.exports פשוט לא קיימים. אם תשתמשו בהם מתוך הרגל, תקבלו ReferenceError.

שילוב: לערבב בין השניים

לא פעם תצטרכו שקובץ ESM ישתמש בחבילת CommonJS, או להפך. הכללים לא סימטריים.

ESM שמייבא CommonJS עובד ישירות. האובייקט module.exports של CJS הופך ל-default export:

// app.mjs
import greet from "./greet.cjs";
console.log(greet("Rosa"));

ייבוא בשם מ-CommonJS עובד לפעמים, Node מנסה לזהות exports בשם באופן סטטי, אבל לאמינות, קחו את ה-default ופרקו אותו עם destructuring:

import pkg from "./utils.cjs";
const { parse, stringify } = pkg;

CommonJS שמייבא ESM הוא הכיוון הכואב. אי אפשר לעשות require() ל-ES module: זה זורק ERR_REQUIRE_ESM. פתח המילוט הוא import() דינמי, שעובד גם ב-CJS ומחזיר Promise:

Node מודרנית (22 ומעלה) הוסיפה require() סינכרוני ל-ESM בתנאים מסוימים, אבל import() דינמי היא התשובה הניידת.

הבדלי התנהגות נוספים שכדאי להכיר

מעבר לתחביר, שתי המערכות חלוקות בכמה פרטים שנושכים מדי פעם:

עוד כמה:

  • this ברמה העליונה. ב-CJS, this הוא module.exports. ב-ESM הוא undefined. ESM תמיד ב-strict mode.
  • __dirname ו-__filename. CJS נותן אותם בחינם. ב-ESM גוזרים אותם מ-import.meta.url:
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
  • סיומות קבצים בייבוא. ESM דורש את הסיומת ("./utils.js", לא "./utils") בנתיבים יחסיים. CJS סלחני.
  • Live bindings מול תמונות מצב. ייבוא ב-ESM הוא הפניה חיה למשתנים של המודול המייצא. CJS נותן לכם עותק של מה שהוצב ב-module.exports בזמן הטעינה. רוב הקוד אף פעם לא מרגיש בזה, אבל זה משנה בתלויות מעגליות.

במה כדאי להשתמש?

לפרויקט חדש: ES Modules. הגדירו "type": "module" ב-package.json ואל תסתכלו אחורה. ESM הוא התקן של השפה, עובד אותו דבר בדפדפנים וב-Node, תומך ב-await ברמה העליונה, והכלים נבנים סביבו.

הישארו עם CommonJS כש:

  • אתם מתחזקים בסיס קוד קיים ב-CJS והמעבר עוד לא שווה את זה.
  • אתם מפרסמים ספרייה שצריכה לתמוך בגרסאות Node ישנות מאוד או בצרכנים שלא יכולים להשתמש ב-ESM.
  • תלות קריטית מופצת רק כ-CJS והשילוב שלה מסובך. (נדיר היום, אבל עדיין קורה.)

וגם אז, תקראו קוד ESM כל הזמן: כל מה שפורסם ב-npm בשנים האחרונות נוטה לכיוון הזה. שליטה בשתיהן היא לא אופציונלית; שליטה בניבים של זו שאתם באמת כותבים היא העיקר.

צ'קליסט מחשבתי מהיר

כשאתם פותחים קובץ חדש, שאלו:

  • האם הקובץ הזה משתמש ב-import/export או ב-require/module.exports? אל תערבבו.
  • מה אומר ה-package.json הקרוב ביותר על "type"?
  • אם אתם מייבאים חבילה, בדקו את ה-package.json שלה: היא מפיצה ESM, CJS או את שתיהן?
  • אם קיבלתם ERR_REQUIRE_ESM, אתם ב-CJS ומנסים לטעון ESM. עברו ל-import() דינמי או העבירו את הקוד הקורא ל-ESM.

תשעים אחוז מהבלבול סביב מודולים ב-Node הוא אחד מארבעת אלה.

הבא בתור: יסודות npm

מודולים הם הדרך לפצל את הקוד שלכם בין קבצים. הצעד הבא הוא להכניס קוד שאחרים כתבו, ובשביל זה יש npm. נעבור על התקנת חבילות, טווחי semver, והחלקים בתהליך העבודה של npm שבאמת משתמשים בהם ביום יום.

שאלות נפוצות

מה ההבדל בין require ל-import ב-JavaScript?

require היא הדרך של CommonJS לטעון מודולים: סינכרונית, רצה בנקודה שבה קוראים לה, ומחזירה את מה שהמודול הציב ב-module.exports. import הוא התחביר של ES Modules: סטטי, מורם לראש הקובץ ומנותח לפני שהקוד רץ. הם גם חלוקים לגבי מה זה this, איך תלויות מעגליות נפתרות, והאם מותר await ברמה העליונה.

כדאי להשתמש ב-CommonJS או ב-ES Modules בפרויקט Node חדש?

השתמשו ב-ES Modules. הגדירו "type": "module" ב-package.json וכתבו import/export. ESM הוא התקן הרשמי, עובד בדפדפנים וב-Node, ותומך ב-await ברמה העליונה. CommonJS עדיין מופיע בחבילות ובכלים ישנים יותר, כך שתקראו אותו גם אם לא תכתבו אותו.

אפשר לשלב require ו-import באותו פרויקט?

כן, לפי כללים. קובץ .mjs או חבילה עם "type": "module" משתמשים ב-ESM; .cjs או "type": "commonjs" משתמשים ב-CJS. ESM יכול לעשות import למודול CommonJS (ה-module.exports מופיע כ-default export). CommonJS לא יכול לעשות require() למודול ESM ישירות: צריך להשתמש ב-import() דינמי, שמחזיר Promise.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל