מבנה פרויקט Node.JS

ב Node.JS יש שלושה סוגים מרכזיים של פרויקטים שאנחנו עשויים לפגוש: פרויקטי CommonJS, פרויקטי ESM ופרויקטי TypeScript.

1. למה שלושה סוגי פרויקטים

כש Node.JS נוצר JavaScript היתה במצב הרבה פחות מתקדם ממה שהיא היום, ו TypeScript עדיין לא נוצרה. לכן ריאן דל היה צריך להיות יצירתי ולהמציא הרבה מבנים שהשפה צריכה ופשוט לא היו קיימים. לימים במיוחד עם יצירת התקן של ES Modules ויצירת TypeScript מתכנתים רצו להשתמש בכלים אלה כדי ליצור פרויקטי Node.JS וכך גם Node עצמה היתה צריכה להתפתח. היום אנחנו מדברים על שלושה סוגים מרכזיים של פרויקטי Node.JS שאנחנו עשויים למצוא, כשההבדל המרכזי ביניהם הוא שיטת ההרצה והקשר בין קבצים בתוך הפרויקט:

  1. פרויקט Node קלאסי עליו נלמד בשיעור זה, ישתמש בכתיב מודולים שנקרא CommonJS ובמילה require כדי לטעון קבצים אחרים בפרויקט.

  2. פרויקט ES Modules עליו נלמד בשיעור הבא, ישתמש בכתיב מודולים שנקרא ESM ובמילה import כדי לטעון קבצים אחרים בפרויקט.

  3. פרויקט TypeScript. אלה הפרויקטים המורכבים ביותר מבחינת מנגנון הבנייה שכן הם משתמשים בכלי בשם TypeScript Compiler כדי לבנות את קבצי ה CommonJS של Node מתוך קבצי ה TypeScript שאנחנו כותבים. אפשר למצוא הסבר על פיתוח פרויקט TypeScript ב node.js בקורס טייפסקריפט בקישור: https://www.tocode.co.il/bundles/typescript/lessons/typescript-node-js?tab=text

בנוסף לאלה יש גם תבניות פרויקט ספציפיות לפריימוורקים מסוימים, למשל ל Next.JS יש תבנית פרויקט משלהם המבוססת על טייפסקריפט. ברוב הקורס אני משתמש בכתיב ה CommonJS הקלאסי בשביל לכתוב את תוכניות הדוגמה כי זה הכתיב שיש לו את התאימות הטובה ביותר למודולים חיצוניים ולא דורש קומפילציה בשביל להריץ. בפרויקטים שלכם אני ממליץ לבחור את סוג הפרויקט שהכי מתאים לצרכים שלכם, ואם אתם יכולים TypeScript הוא בעיניי מבנה הפרויקט הטוב ביותר היום לפרויקט Node.JS.

2. יצירת הפרויקט

צרו תיקיה חדשה על המחשב בשם myapp. פיתחו את מסך ה cmd וכנסו עם cd לתיקיה. כשאתם בתוך התיקיה כתבו:

$ npm init -y

והכלי אוטומטית ייצור לכם קובץ בשם package.json בתיקייה עם התוכן הבא:

{
  "name": "myapp",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

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

בינתיים פיתחו ב VS Code לעריכה קובץ חדש בשם index.js ובו כתבו את התוכן:

console.log('Hello World');

המשיכו לערוך את הקובץ package.json ועדכנו אותו כך שיכיל את התוכן הבא:

{
  "name": "myapp",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

בחזרה ל cmd ושם נכתוב:

PS C:\Users\ynonp\demo1> npm start

> demo1@1.0.0 start
> node index.js

Hello World
PS C:\Users\ynonp\demo1>
PS C:\Users\ynonp\demo1>

בתגובה המחשב באופן אוטומטי הריץ עבורנו את הפקודה שרשמנו בשדה scripts.start שהיא הפקודה שהפעילה את הפרויקט.

3. טעינת פונקציה מקובץ אחר בפרויקט

ניצור בפרויקט תיקייה חדשה בשם src ובתוכה קובץ חדש בשם utils.js ובתוכו נכתוב את התוכן הבא:

exports.twice = function(x) {
    return x * 2;
}

אל תהיו מודאגים אם אתם לא מבינים מאיפה הגיע המשתנה exports, יש לנו שיעור עליו בהמשך הקורס. בגדול משתנה זה מאפשר לנו לשתף פונקציות ומשתנים בין קבצים שונים בפרויקט. נחזור לקובץ index.js ושם נכתוב:

const {twice} = require('./src/utils');

console.log(`6 * 2 = ${twice(6)}`);

עכשיו כשאני מריץ את התוכנית אני מקבל את התוצאה 12 כי הקובץ index טען את הפונקציה שהוגדרה בקובץ utils.

4. טעינת פונקציה מספריה חיצונית

פרויקט node יכול להשתמש במאגר עצום של חבילות חופשיות מהרשת שנקרא npm. חזרה ל cmd ושם כתבו:

> npm add express

נ.ב. לפעמים אתם תראו בהוראות התקנה פקודות כמו npm install, npm install --save או npm add --save. בגירסאות עדכניות של node.js כל הפקודות האלה זהות. אני משתמש ב add בדוגמה כאן כי הוא הכי קצר, אבל במקומות אחרים בקורס משתמש גם בפקודות האחרות, חלק מתוך הרגל וחלק כי אלה שיעורים שהוקלטו כשעוד היה הבדל בין האפשרויות.

אחרי קצת עבודת חשיבה המחשב יחזיר אתכם לשורת הפקודה והפעם עם קצת דברים חדשים. הקובץ package.json השתנה והוא כעת נראה כך:

{
  "name": "myapp",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.16.4"
  }
}

בנוסף נוצרה לכם תיקיה חדשה בשם node_modules שמכילה די הרבה תתי ספריות, אחת מהן נקראת express.

מנגנון שילוב מודולים חיצוניים בפרויקט משתמש בקובץ בשם package.json כדי לתאר את התלויות בין הפרויקט למודולים החיצוניים. השדה dependencies בקובץ מתאר את כל המודולים החיצוניים והגירסאות שלהם שהפרויקט שלנו צריך. אבל הקובץ package.json לא כולל את קוד המודולים עצמם - בשביל זה יש את הספריה node_modules. בספריה זו תמצאו את הספריה express החדשה שביקשנו להתקין, וגם את כל הספריות ש express צריכה כדי לעבוד כמו שצריך.

בנוסף נוצר לכם קובץ בשם package-lock.json. קובץ זה הרבה יותר ארוך וכולל את הגירסאות הספציפיות של כל התלויות שהותקנות בספריית node_modules. כשתגיעו להפיץ את הפרויקט אתם לא צריכים להפיץ איתו גם את כל תיקיית node_modules שהיא ענקית. אם יש למישהו את הקבצים package.json ו package-lock.json הוא תמיד יכול להפעיל משורת הפקודה:

> npm install

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

בואו ננסה את זה! מחקו את התיקיה node_modules לחלוטין ואז הריצו:

> npm install

ותוכלו לראות ש npm הלך לרשת והביא מחדש את כל התוכן שהיה קודם ב node_modules.

בהמשך הקורס נדבר יותר לעומק על package.json, על משמעות המספרים ליד החבילות ועל שידרוג חבילות. בינתיים בואו נלך לעדכן את התוכנית שלנו כך שתשתמש במודול החדש שהתקנו ותריץ שרת ווב.

5. עדכון התוכנית והפעלת שרת ווב

החליפו את תוכן הקובץ index.js בקוד הבא:

const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => {
  res.send('Hello World!')
});

app.listen(port, () => console.log(`Example app listening on port ${port}!`))

חזרו ל cmd והפעילו את הפרויקט עם:

> npm start

עכשיו אפשר להפעיל דפדפן ולהיכנס לכתובת localhost:3000 כדי לראות את הודעת ה Hello World שלנו.

שרת Express שומר על הרוח של node.js:

  1. הקוד בנוי בתור יצירת רצף של Event Handlers: כל פעם שמשתמש מגיע לנתיב מסוים יש לקרוא לפוקנצית טיפול מתאימה.

  2. כל פונקציית Event Handler מקבלת אוביקט בקשה ואוביקט תשובה. היא יכולה להשתמש באוביקט הבקשה כדי לבחון את המידע שהלקוח שלח ועליה לכתוב תשובה לאוביקט התשובה.

  3. השרת ממשיך לרוץ עד שנעצור אותו כי Express דואג להשאיר תהליך אסינכרוני פתוח: תהליך ההמתנה לגולש חדש.