• בלוג
  • פיתוח קוד שמתאים גם ל Node וגם ל Deno

פיתוח קוד שמתאים גם ל Node וגם ל Deno

23/04/2024

מצד אחד deno הולך להיות הדבר הגדול הבא אבל מצד שני הוא עדיין לא שם. האם אפשר לשלב את דינו בקטנה? האם אפשר לכתוב קוד node.js שיעבוד בעתיד גם ב deno כשנחליט לשדרג? ומה העלות? בואו ננסה כמה דוגמאות.

1. הבדלים בין Deno ל Node

נתחיל בדברים הפשוטים - בפרויקט JavaScript רגיל (ללא טייפסקריפט), אם נגדיר את התלויות שלנו בקובץ deno.json ובקובץ package.json במקביל נוכל לכתוב קוד שיעבוד בשתי הסביבות, וזו תהיה הדוגמה הראשונה לפוסט זה:

קובץ package.json:

{
  "type": "module",
  "dependencies": {
    "cowsay": "1.6.0"
  }
}

קובץ deno.json:

{
  "tasks": {
    "dev": "deno run --watch main.ts"
  },
  "imports": {
    "cowsay": "npm:cowsay@^1.6.0"
  }
}

קובץ main.js:

import cowsay from 'cowsay';
import { text } from './helper.js';

console.log(cowsay.say({
      text : text(),
      e : "oO",
      T : "U "
}));

קובץ helper.js:

export function text() {
  return "I'm a moooodule";
}

הבעיה מתחילה במעבר לטייפסקריפט, ובעיקר בגלל הקשר בין מודולים ביישום. נתבונן בשורה:

import { text } from './helper.js';

גם node וגם deno יודעים להתמודד עם כתיב ES Modules ולטעון סמלים מקבצי JavaScript אחרים. הבעיה היא ש node לא יודע לקרוא קבצי טייפסקריפט ישירות ולכן במעבר לטייפסקריפט ב Node צריך להפעיל כלי נוסף - ה TypeScript Compiler.

הקומפיילר של טייפסקריפט כרגע לא יודע להתמודד עם שורות import שמייבאות קבצי ts אחרים, כלומר זה לא עובר קומפילציה:

import { text } from './helper.ts;

בטייפסקריפט עלינו להשמיט את הסיומת והקומפיילר של טייפסקריפט יוסיף אותה בעצמו לפי הדבר שהוא בונה. בשביל ששורה כמו שהראיתי תתקמפל בטייפסקריפט אני צריך לכתוב:

import { text } from './helper;

אבל Deno לא מוכן לטעון מודול טייפסקריפט אם אין סיומת ts מפורשת לקובץ. הוא מצפה שמודול טייפסקריפט ייטען בדיוק כמו מודול JavaScript וכמו שמודולים נטענים בדפדפן - כלומר עם הסיומת. (סוגריים - יש אופציה לעקוף את המגבלה הזאת אבל הם רוצים להוריד אותה וממליצים לא להשתמש בה, אז אני מעדיף להשאיר אותה מחוץ לשיחה).

הבדל נוסף שהולך להפריע בהרבה תוכניות הוא שבשביל להתיחס לתיקיה או הקובץ הנוכחי ב node יש משתנים מיוחדים בשם __dirname ו __filename. לדינו אין אותם והוא משתמש במשתנה הסטנדרטי import.meta.url. נוד מכיר את import.meta.url רק במצב עבודה עם ES Modules, אבל במצב זה הרבה מודולים מ npm לא עובדים במיוחד מתוך טייפסקריפט.

2. פיתרון 1 - בוחרים סביבה

אני חייב להודות שהפיתרון הכי טוב לבעיית התאימות וההמלצה שלי היא לבחור את אחת הסביבות ולהישאר איתה. אם שוכנעתם שהעתיד הוא node.js וטייפסקריפט תבנו את היישום בזה ואם אתם חושבים שדינו הולך להשתלט על העולם לכו עליו. לפעמים גם יש אילוצים שלא תלויים בנו, למשל ספריית next.js לא תומכת בדינו ולכן אם אתם רוצים לכתוב יישום next.js תצטרכו לבחור ב Node. הבחירה בכלי אחד גם מאפשרת לכם להשתמש ביכולות הספציפיות של הכלי (בין אם זה node או Deno) וכך לכתוב קוד יעיל יותר.

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

3. פיתרון 2 - בניה עם esbuild, הרצה איך שרוצים

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

נתקין את esbuild עם:

npm install -g esbuild

ועכשיו הפקודה:

$ esbuild --bundle main.ts --packages=external --outdir=distNode --format=cjs

בונה את הפרויקט לתיקיה בשם distNode בצורה שמתאימה ל node, והפקודה:

$ esbuild --bundle main.ts --packages=external --outdir=distDeno --format=esm

בונה את הפרויקט לתיקיית distDeno בצורה שתואמת לדינו. הפרויקט ייבנה תמיד לקובץ js אחד ולכן לא מבצע import-ים יחסיים בין קבצי מקור והכל עובד.

נשים לב ש esbuild לא בודק הגדרות טיפוסים ולכן בנוסף אליו נצטרך להגדיר קובץ tsconfig.json לפרויקט כדי להריץ את ה TypeScript Compiler, תוך שאנחנו זוכרים ש tsc יבנה רק גירסה של הפרויקט שמותאמת ל node (וכן זה היה מושלם אם היתה דרך לשכנע את tsc לבנות גירסה שמתאימה ל deno. עד כמה שאני יודע אין).

בואו נראה את זה בקוד.

בתיקיית הדוגמה אני משנה את הסיומות של שני קבצי הקוד ל ts, מוסיף קובץ tsconfig.json עם התוכן הבא:

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "node",
    "strict": true,
    "skipLibCheck": true,
    "outDir": "./distNode"
  }
}

משנה את שורת ה import ל:

import { text } from './helper';

ומריץ tsc כדי לראות שהכל מתקמפל.

אחרי זה אני מריץ את שתי פקודות ה esbuild שהראיתי:

$ esbuild --bundle main.ts --packages=external --outdir=distNode --format=cjs
$ esbuild --bundle main.ts --packages=external --outdir=distDeno --format=esm

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

$ deno run -A distDeno/main.js
 _________________
< I'm a moooodule >
 -----------------
        \   ^__^
         \  (oO)\_______
            (__)\       )\/\
             U  ||----w |
                ||     ||
$ node distNode/main.js
 _________________
< I'm a moooodule >
 -----------------
        \   ^__^
         \  (oO)\_______
            (__)\       )\/\
             U  ||----w |
                ||     ||

בשביל לבדוק שהכל מתקמפל גם עם דינו נוכל להפעיל:

$ deno check --unstable-sloppy-imports *.ts

כאשר המתג --unstable-sloppy-imports גורם לדינו לעבוד גם עם import-ים ללא סיומת, כמו אלה בקבצי המקור של הטייפסקריפט שלנו. אבל זה מתג עבודה לא מומלץ ואמור לרדת בגירסה 2 של דינו.

בעיניי שיטה זו פחות טובה גם בגלל שהיא מסורבלת יותר וגם בגלל שאין באמת טעם לבנות את אותו פרויקט גם ל node וגם ל deno.

4. כיוון עבודה מומלץ

אנחנו נמצאים היום בצומת דרכים מבחינת העתיד של סביבות להרצת JavaScript מחוץ לדפדפן. עד לא מזמן node.js היתה האופציה היחידה שלנו, אבל היום יש כבר שלוש סביבות טובות להרצה - deno ,node, ו bun שהוא ניסיון לייצר סביבה אפילו יותר מהירה מ deno ויותר תואמת ל node.js. אני לא יודע איך התחרות הזאת תיגמר, ואם בסופו של דבר יהיה מנצח או שאנשים ישתמשו בשלושת הכלים לפי סוג הפרויקט.

כרגע יש חבילות שעובדות טוב יותר בדינו, וחבילות אחרות שעובדות טוב יותר ב node, וחבילות שעובדות די טוב בשתי הסביבות. אקספרס עובדת סביר בשתי הסביבות אבל עדיין פיצ'רים מסוימים שלה עובדים רק ב node. לדינו יש פריימוורק מתחרה וספציפי לדינו בשם Oak. נקסט עובדת רק ב node אבל לדינו יש פריימוורק מתחרה בשם fresh.

עוד דוגמה היא חבילות החיבור לבסיס נתונים. דרייברים של בסיס נתונים לא עובדים בצורה חלקה בין שתי הסביבות ויש הרבה ספריות לגישה לבסיס נתונים שמוגבלות רק ל node.js. אני אוהב לעבוד עם ספריה בשם kysely שכן עובדת על שתי הסביבות (יש גם את drizzle שעובדת בכל מקום), אבל הרבה ספריות מובילות במיוחד ספריות ORM לא תומכות בדינו.

אני מקווה לראות את האקוסיסטם של דינו ממשיך לגדול ויום אחד לקבל מערכת שמכילה מספיק מודולים ותיעוד כדי שאפשר יהיה לעבוד איתה בקלות בלי קשר ל node.js. כרגע דינו מתאים יותר להרפתקנים או לפיתוח יישומים ספציפיים. שירות Deno Deploy שלהם הוא מצוין ולכן כן הייתי בונה איתו היום מערכת ווב תוך שימוש ב Web Frameworks שלהם, אבל החיבור למודולים מ npm עדיין לא עובד בצורה מושלמת ובעיות תאימות שם יכולות לתסכל.