دليل Deno الشامل: بيئة تشغيل TypeScript الحديثة مع أمثلة برمجية عملية

دقائق القراءة: 21
في عالم تطوير الويب سريع التطور، تظهر تقنيات جديدة باستمرار لتلبية احتياجات المطورين المتغيرة. من بين هذه التقنيات، برزت Deno كبديل واعد لبيئة تشغيل Node.js الشهيرة. أطلقها ريان دال (Ryan Dahl)، المبتكر الأصلي لـ Node.js، بهدف معالجة بعض القرارات المبكرة التي ندم عليها في تصميم Node.js، وتقديم بيئة تشغيل حديثة تتوافق مع أحدث ميزات لغة JavaScript و TypeScript.

يهدف هذا الدليل الشامل إلى تعريفك بـ Deno بسرعة وفعالية. سنستكشف ميزاته الفريدة، نقارنه بـ Node.js، ونرشدك خطوة بخطوة لبناء أول واجهة برمجة تطبيقات (REST API) خاصة بك باستخدام Deno. استعد لاكتشاف بيئة تشغيل قد تغير طريقة تفكيرك في تطوير تطبيقات الخادم.

ما هو Deno؟

إذا كنت على دراية بـ Node.js، النظام البيئي الشهير لـ JavaScript من جانب الخادم، فإن Deno يشبه Node، ولكنه محسّن بشكل عميق في جوانب متعددة. دعنا نبدأ بقائمة سريعة لأبرز الميزات التي تميز Deno:

  • يعتمد على الميزات الحديثة للغة JavaScript.
  • يمتلك مكتبة قياسية (Standard Library) واسعة النطاق.
  • تعتبر TypeScript جوهر عمله، مما يوفر ميزة هائلة بطرق مختلفة، بما في ذلك دعم TypeScript من الدرجة الأولى (لا تحتاج إلى تجميع TypeScript بشكل منفصل، فـ Deno يقوم بذلك تلقائيًا).
  • يتبنى الوحدات النمطية لـ ES Modules بشكل أصيل.
  • لا يعتمد على مدير حزم (Package Manager) تقليدي.
  • يقدم دعمًا متكاملًا لكلمة await المفتاحية على المستوى الأعلى (top-level await).
  • يتضمن مرفقًا مدمجًا للاختبار (Built-in testing facility).
  • يهدف إلى أن يكون متوافقًا مع المتصفحات قدر الإمكان، على سبيل المثال من خلال توفير دالة fetch مدمجة وكائن window العام.

سنستكشف جميع هذه الميزات في هذا الدليل. بعد استخدام Deno وتقدير ميزاته، قد تبدو Node.js وكأنها تقنية قديمة، خاصة وأن واجهة برمجة تطبيقات Node.js تعتمد على دوال الاستدعاء (callbacks)، حيث كُتبت قبل ظهور الوعود (promises) و async/await بوقت طويل. لا يوجد تغيير متاح لذلك في Node.js لأن مثل هذا التغيير سيكون هائلاً. لذا، نحن عالقون مع callbacks أو مع تحويل استدعاءات API إلى وعود (promisifying).

Node.js تقنية رائعة وستظل المعيار الفعلي في عالم JavaScript. لكننا نعتقد أننا سنرى Deno يُعتمد تدريجيًا أكثر فأكثر بسبب دعمه المتكامل لـ TypeScript ومكتبته القياسية الحديثة. يستطيع Deno تحمل كتابة كل شيء باستخدام التقنيات الحديثة، حيث لا توجد توافقية عكسية للحفاظ عليها. بالطبع، لا يوجد ضمان بأن نفس الشيء لن يحدث لـ Deno في عقد من الزمان وتظهر تقنية جديدة، لكن هذا هو الواقع في الوقت الحالي.

لماذا Deno؟ ولماذا الآن بالذات؟

أُعلن عن Deno قبل ما يقرب من عامين من قبل المبتكر الأصلي لـ Node.js، ريان دال، في مؤتمر JSConf EU. يمكنك مشاهدة فيديو المحاضرة على YouTube، إنه مثير للاهتمام للغاية ومشاهدته إلزامية إذا كنت منخرطًا في Node.js و JavaScript بشكل عام.

يجب على كل مدير مشروع اتخاذ قرارات. ندم ريان على بعض القرارات المبكرة في Node. بالإضافة إلى ذلك، تتطور التكنولوجيا، واليوم JavaScript لغة مختلفة تمامًا عما كانت عليه في عام 2009 عندما بدأ Node. فكر في ميزات ES6/2016/2017 الحديثة وما إلى ذلك. لذلك بدأ مشروعًا جديدًا لإنشاء نوع من الموجة الثانية من تطبيقات الخادم التي تعمل بـ JavaScript.

السبب في كتابتي لهذا الدليل الآن وليس في ذلك الوقت هو أن التقنيات تحتاج إلى الكثير من الوقت لتنضج. وقد وصلنا أخيرًا إلى Deno 1.0 (كان من المقرر إطلاق 1.0 في 13 مايو 2020)، وهو الإصدار الأول من Deno الذي أُعلن عنه رسميًا بأنه مستقر. قد يبدو هذا مجرد رقم، لكن 1.0 يعني أنه لن تكون هناك تغييرات جذرية كبيرة حتى Deno 2.0. وهذا أمر مهم عندما تتعمق في تقنية جديدة – فأنت لا تريد تعلم شيء ثم يتغير بسرعة كبيرة.

هل يجب أن تتعلم Deno؟

هذا سؤال كبير. تعلم شيء جديد مثل Deno يتطلب جهدًا كبيرًا. اقتراحي هو أنه إذا كنت تبدأ الآن في تطوير JavaScript من جانب الخادم ولم تكن تعرف Node بعد، ولم تكتب أي TypeScript مطلقًا، فابدأ بـ Node. لم يُطرد أحد أبدًا لاختياره Node.js (إعادة صياغة لمقولة شائعة). ولكن إذا كنت تحب TypeScript، ولا تعتمد على عدد لا يحصى من حزم npm في مشاريعك، وترغب في استخدام await في أي مكان، فربما يكون Deno هو ما تبحث عنه.

هل سيحل Deno محل Node.js؟

لا. Node.js عملاق، راسخ، مدعوم بشكل لا يصدق، وسيبقى موجودًا لعقود. لا توجد تقنية واحدة تحل محل أخرى تمامًا في عالم البرمجيات، بل تتطور البيئات وتتكامل.

دعم TypeScript من الدرجة الأولى

كُتب Deno بلغة Rust و TypeScript، وهما من اللغات التي تنمو بسرعة كبيرة اليوم. على وجه الخصوص، كون Deno مكتوبًا بـ TypeScript يعني أننا نحصل على الكثير من فوائد TypeScript حتى لو اخترنا كتابة شيفرتنا بـ JavaScript العادية. وتشغيل شيفرة TypeScript باستخدام Deno لا يتطلب خطوة تجميع – Deno يقوم بذلك تلقائيًا لك.

لست مجبرًا على الكتابة بـ TypeScript، لكن حقيقة أن جوهر Deno مكتوب بـ TypeScript أمر ضخم. أولاً، نسبة متزايدة من مبرمجي JavaScript يحبون TypeScript. ثانيًا، يمكن للأدوات التي تستخدمها استنتاج الكثير من المعلومات حول البرامج المكتوبة بـ TypeScript، مثل Deno. هذا يعني أنه عندما نقوم بالبرمجة في VS Code، على سبيل المثال (الذي يتمتع بالطبع بتكامل وثيق مع TypeScript نظرًا لأن كلاهما تم تطويرهما في Microsoft)، يمكننا الحصول على فوائد مثل التحقق من الأنواع (type checking) أثناء كتابة شيفرتنا، وميزات IntelliSense المتقدمة. بعبارة أخرى، يمكن للمحرر مساعدتنا بطريقة مفيدة للغاية.

أوجه التشابه والاختلاف مع Node.js

بما أن Deno هو في الأساس بديل لـ Node.js، فمن المفيد مقارنة الاثنين مباشرة.

أوجه التشابه:

  • كلاهما تم تطويرهما بناءً على محرك V8 Chromium Engine.
  • كلاهما رائع لتطوير تطبيقات جانب الخادم باستخدام JavaScript.

أوجه الاختلاف:

  • كُتب Node بلغة C++ و JavaScript. بينما كُتب Deno بلغة Rust و TypeScript.
  • يمتلك Node مدير حزم رسميًا يسمى npm. بينما لا يمتلك Deno مدير حزم، وبدلاً من ذلك يتيح لك استيراد أي وحدة ES Module مباشرة من عناوين URL.
  • يستخدم Node صيغة CommonJS لاستيراد الحزم. بينما يستخدم Deno وحدات ES Modules، وهي الطريقة الرسمية.
  • يستخدم Deno ميزات ECMAScript الحديثة في جميع واجهات برمجة التطبيقات (API) والمكتبة القياسية الخاصة به، بينما يستخدم Node.js مكتبة قياسية تعتمد على دوال الاستدعاء (callbacks) وليس لديه خطط لترقيتها.
  • يقدم Deno طبقة أمان (sandbox security layer) من خلال الأذونات (permissions). يمكن للبرنامج فقط الوصول إلى الأذونات المحددة للملف التنفيذي كعلامات (flags) من قبل المستخدم. بينما يمكن لبرنامج Node.js الوصول إلى أي شيء يمكن للمستخدم الوصول إليه.
  • لطالما تصور Deno إمكانية تجميع برنامج في ملف تنفيذي يمكن تشغيله بدون تبعيات خارجية، مثل Go، لكنه لم يتحقق بعد. سيكون ذلك تغييرًا جذريًا.

غياب مدير الحزم (Package Manager)

عدم وجود مدير حزم والاضطرار إلى الاعتماد على عناوين URL لاستضافة واستيراد الحزم له إيجابيات وسلبيات. من الإيجابيات التي نفضلها هي المرونة الكبيرة، حيث يمكننا إنشاء حزم دون نشرها في مستودع مثل npm. نعتقد أن نوعًا من مديري الحزم سيظهر في المستقبل، لكن لا يوجد شيء رسمي حتى الآن.

يوفر موقع Deno استضافة للشيفرة (وبالتالي التوزيع عبر عناوين URL) لحزم الطرف الثالث على الرابط: https://deno.land/x/.

تثبيت Deno

كفى حديثًا! لنقم بتثبيت Deno. أسهل طريقة هي استخدام Homebrew (مدير الحزم الشهير لأنظمة macOS و Linux):

brew install deno

لقطة شاشة لعملية تثبيت Deno باستخدام Homebrew بنجاح
بمجرد الانتهاء من ذلك، ستتمكن من الوصول إلى الأمر deno. إليك المساعدة التي يمكنك الحصول عليها باستخدام deno --help:

flavio@mbp~> deno --help
deno 0.42.0
A secure JavaScript and TypeScript runtime

Docs: https://deno.land/std/manual.md
Modules: https://deno.land/std/
         https://deno.land/x/
Bugs: https://github.com/denoland/deno/issues

To start the REPL, supply no arguments: deno
To execute a script: deno run https://deno.land/std/examples/welcome.ts
                     deno https://deno.land/std/examples/welcome.ts
To evaluate code in the shell: deno eval "console.log(30933 + 404)"

Run 'deno help run' for 'run'-specific flags.

USAGE:
    deno [OPTIONS] [SUBCOMMAND]

OPTIONS:
    -h, --help                     Prints help information
    -L, --log-level     Set log level [possible values: debug, info]
    -q, --quiet                    Suppress diagnostic output
                                   By default, subcommands print human-readable diagnostic messages to stderr. If the
                                   flag is set, restrict these messages to errors.
    -V, --version                  Prints version information

SUBCOMMANDS:
    bundle      Bundle module and dependencies into single file
    cache       Cache the dependencies
    completions Generate shell completions
    doc         Show documentation for a module
    eval        Eval script
    fmt         Format source files
    help        Prints this message or the help of the given subcommand(s)
    info        Show info about cache or info related to source file
    install     Install script as an executable
    repl        Read Eval Print Loop
    run         Run a program given a filename or url to the module
    test        Run tests
    types       Print runtime TypeScript declarations
    upgrade     Upgrade deno executable to newest version

ENVIRONMENT VARIABLES:
    DENO_DIR         Set deno's base directory (defaults to $HOME/.deno)
    DENO_INSTALL_ROOT Set deno install's output directory (defaults to $HOME/.deno/bin)
    NO_COLOR         Set to disable color
    HTTP_PROXY       Proxy address for HTTP requests (module downloads, fetch)
    HTTPS_PROXY      Same but for HTTPS

أوامر Deno الأساسية

لاحظ قسم SUBCOMMANDS في المساعدة، والذي يسرد جميع الأوامر التي يمكننا تشغيلها. ما هي الأوامر الفرعية المتاحة؟

  • bundle: لتجميع الوحدة النمطية والتبعيات في ملف واحد.
  • cache: لتخزين التبعيات مؤقتًا.
  • completions: لتوليد إكمال الأوامر للصدفة (shell).
  • doc: لعرض وثائق وحدة نمطية.
  • eval: لتقييم جزء من الشيفرة، على سبيل المثال: deno eval "console.log(1 + 2)".
  • fmt: أداة تنسيق شيفرة مدمجة (مشابهة لـ gofmt في Go).
  • help: لعرض هذه الرسالة أو مساعدة الأوامر الفرعية المحددة.
  • info: لعرض معلومات حول التخزين المؤقت أو معلومات متعلقة بملف المصدر.
  • install: لتثبيت السكربت كملف تنفيذي.
  • repl: حلقة قراءة-تقييم-طباعة (Read-Eval-Print-Loop) (الوضع الافتراضي).
  • run: لتشغيل برنامج معين بملف أو عنوان URL للوحدة النمطية.
  • test: لتشغيل الاختبارات.
  • types: لطباعة تعريفات TypeScript الخاصة ببيئة التشغيل.
  • upgrade: لترقية ملف deno التنفيذي إلى أحدث إصدار.

يمكنك تشغيل deno <subcommand> help للحصول على وثائق إضافية محددة للأمر، على سبيل المثال deno run --help. كما تشير المساعدة، يمكننا استخدام هذا الأمر لبدء REPL (حلقة قراءة-تنفيذ-طباعة) باستخدام deno بدون أي خيار آخر.

لقطة شاشة لبيئة Deno REPL تعمل في الطرفية
هذا هو نفس تشغيل deno repl. الطريقة الأكثر شيوعًا التي ستستخدم بها هذا الأمر هي لتنفيذ تطبيق Deno موجود في ملف TypeScript. يمكنك تشغيل ملفات TypeScript (.ts) أو ملفات JavaScript (.js). إذا لم تكن على دراية بـ TypeScript، فلا تقلق: Deno مكتوب بـ TypeScript، ولكن يمكنك كتابة تطبيقاتك بـ JavaScript.

تطبيق Deno الأول لك

لنقم بتشغيل تطبيق Deno لأول مرة. ما أجده مدهشًا حقًا هو أنك لا تحتاج حتى إلى كتابة سطر واحد من الشيفرة – يمكنك تشغيل أمر من أي عنوان URL. يقوم Deno بتنزيل البرنامج وتجميعه ثم تشغيله:

لقطة شاشة لتشغيل تطبيق Deno من URL
بالطبع، تشغيل شيفرة عشوائية من الإنترنت ليس ممارسة أوصي بها عمومًا. في هذه الحالة، نقوم بتشغيلها من موقع Deno الرسمي، بالإضافة إلى أن Deno يحتوي على نموذج أمان (sandbox) يمنع البرامج من القيام بأي شيء لا ترغب في السماح به. المزيد عن هذا لاحقًا.

هذا البرنامج بسيط للغاية، مجرد استدعاء لـ console.log():

console.log('Welcome to Deno ?')

إذا فتحت عنوان URL https://deno.land/std/examples/welcome.ts باستخدام المتصفح، فسترى هذه الصفحة:

لقطة شاشة لصفحة الويب التي تعرض ملف welcome.ts
غريب، أليس كذلك؟ ربما تتوقع ملف TypeScript، لكن بدلاً من ذلك لدينا صفحة ويب. السبب هو أن خادم الويب الخاص بموقع Deno يعرف أنك تستخدم متصفحًا ويقدم لك صفحة أكثر سهولة في الاستخدام. قم بتنزيل نفس URL باستخدام wget على سبيل المثال، والذي يطلب إصدار text/plain بدلاً من text/html:

لقطة شاشة لتنزيل ملف welcome.ts باستخدام wget
إذا كنت ترغب في تشغيل البرنامج مرة أخرى، فإنه الآن مخزن مؤقتًا بواسطة Deno ولا يحتاج إلى تنزيله مرة أخرى:

لقطة شاشة لتشغيل تطبيق Deno المخزن مؤقتًا
يمكنك فرض إعادة تحميل المصدر الأصلي باستخدام العلامة --reload:

لقطة شاشة لإعادة تحميل تطبيق Deno باستخدام --reload
يحتوي deno run على الكثير من الخيارات المختلفة التي لم تُدرج في deno --help. بدلاً من ذلك، تحتاج إلى تشغيل deno run --help للكشف عنها:

flavio@mbp~> deno run --help
deno-run
Run a program given a filename or url to the module. By default all programs are run in sandbox without access to disk, network or ability to spawn subprocesses.

    deno run https://deno.land/std/examples/welcome.ts

Grant all permissions:
    deno run -A https://deno.land/std/http/file_server.ts

Grant permission to read from disk and listen to network:
    deno run --allow-read --allow-net https://deno.land/std/http/file_server.ts

Grant permission to read whitelisted files from disk:
    deno run --allow-read=/etc https://deno.land/std/http/file_server.ts

USAGE:
    deno run [OPTIONS] <SCRIPT_ARG>...

OPTIONS:
    -A, --allow-all                        Allow all permissions
        --allow-env                        Allow environment access
        --allow-hrtime                     Allow high resolution time measurement
        --allow-net=<allow-net>            Allow network access
        --allow-plugin                     Allow loading plugins
        --allow-read=<allow-read>          Allow file system read access
        --allow-run                        Allow running subprocesses
        --allow-write=<allow-write>         Allow file system write access
        --cached-only                      Require that remote dependencies are already cached
        --cert <FILE>                      Load certificate authority from PEM encoded file
    -c, --config <FILE>                    Load tsconfig.json configuration file
    -h, --help                             Prints help information
        --importmap <FILE>                 UNSTABLE: Load import map file
                                           Docs: https://deno.land/std/manual.md#import-maps
                                           Specification: https://wicg.github.io/import-maps/
                                           Examples: https://github.com/WICG/import-maps#the-import-map
        --inspect=<HOST:PORT>              activate inspector on host:port (default: 127.0.0.1:9229)
        --inspect-brk=<HOST:PORT>          activate inspector on host:port and break at start of user script
        --lock <FILE>                      Check the specified lock file
        --lock-write                       Write lock file. Use with --lock.
    -L, --log-level <log-level>            Set log level [possible values: debug, info]
        --no-remote                        Do not resolve remote modules
    -q, --quiet                            Suppress diagnostic output
                                           By default, subcommands print human-readable diagnostic messages to stderr. If the
                                           flag is set, restrict these messages to errors.
    -r, --reload=<CACHE_BLACKLIST>         Reload source code cache (recompile TypeScript)
        --reload                           Reload everything
        --reload=https://deno.land/std     Reload only standard modules
        --reload=https://deno.land/std/fs/utils.ts,https://deno.land/std/fmt/colors.ts
                                           Reloads specific modules
        --seed <NUMBER>                    Seed Math.random()
        --unstable                         Enable unstable APIs
        --v8-flags=<v8-flags>              Set V8 command line options. For help: --v8-flags=--help

ARGS:
    <SCRIPT_ARG>...    script args

أمثلة برمجية إضافية في Deno

بالإضافة إلى المثال الذي قمنا بتشغيله أعلاه، يوفر موقع Deno بعض الأمثلة الأخرى التي يمكنك التحقق منها على الرابط: https://deno.land/std/examples/. وقت كتابة هذا الدليل، يمكننا العثور على:

  • cat.ts: يطبع محتوى قائمة الملفات المقدمة كوسائط.
  • catj.ts: يطبع محتوى قائمة الملفات المقدمة كوسائط (نسخة مختلفة).
  • chat/: تطبيق لتنفيذ محادثة.
  • colors.ts: مثال على استخدام الألوان في الطرفية.
  • curl.ts: تطبيق بسيط لـ curl يطبع محتوى عنوان URL المحدد كوسيطة.
  • echo_server.ts: خادم TCP echo.
  • gist.ts: برنامج لنشر الملفات على gist.github.com.
  • test.ts: مجموعة اختبارات نموذجية.
  • welcome.ts: جملة console.log بسيطة (البرنامج الأول الذي قمنا بتشغيله أعلاه).
  • xeval.ts: يتيح لك تشغيل أي شيفرة TypeScript لأي سطر من الإدخال القياسي المستلم. كان يُعرف سابقًا باسم deno xeval ولكنه أُزيل من الأمر الرسمي.

بناء تطبيق Deno الأول عملياً

لنكتب بعض الشيفرة. تطبيق Deno الأول الذي قمت بتشغيله باستخدام deno run https://deno.land/std/examples/welcome.ts كان تطبيقًا كتبه شخص آخر، لذلك لم ترَ أي شيء يتعلق بكيفية ظهور شيفرة Deno. سنبدأ من مثال التطبيق الافتراضي المدرج على موقع Deno الرسمي:

import { serve } from 'https://deno.land/std/http/server.ts'

const s = serve({ port: 8000 })
console.log('http://localhost:8000/')

for await (const req of s) {
  req.respond({ body: 'Hello World\n' })
}

تستورد هذه الشيفرة الدالة serve من الوحدة http/server. لاحظ؟ لا نحتاج إلى تثبيتها أولاً، وهي أيضًا لا تُخزن على جهازك المحلي كما يحدث مع وحدات Node. هذا أحد الأسباب التي جعلت تثبيت Deno سريعًا جدًا.

استيراد من https://deno.land/std/http/server.ts يستورد أحدث إصدار من الوحدة. يمكنك استيراد إصدار محدد باستخدام @VERSION، مثل هذا:

import { serve } from 'https://deno.land/std@v0.42.0/http/server.ts'

تُعرّف الدالة serve في هذا الملف على النحو التالي:

/**
 * Create a HTTP server
 *
 * import { serve } from "https://deno.land/std/http/server.ts";
 * const body = "Hello World\n";
 * const s = serve({ port: 8000 });
 * for await (const req of s) {
 * req.respond({ body });
 * }
 */
export function serve(addr: string | HTTPOptions): Server {
  if (typeof addr === 'string') {
    const [hostname, port] = addr.split(':')
    addr = { hostname, port: Number(port) }
  }
  const listener = listen(addr)
  return new Server(listener)
}

ننتقل إلى إنشاء خادم عن طريق استدعاء الدالة serve() وتمرير كائن يحتوي على خاصية port. ثم نقوم بتشغيل هذه الحلقة للاستجابة لكل طلب قادم من الخادم.

for await (const req of s) {
  req.respond({ body: 'Hello World\n' })
}

لاحظ أننا نستخدم الكلمة المفتاحية await دون الحاجة إلى تضمينها في دالة async لأن Deno يطبق top-level await (الانتظار على المستوى الأعلى).

لنقم بتشغيل هذا البرنامج محليًا. أفترض أنك تستخدم VS Code، ولكن يمكنك استخدام أي محرر تريده. أوصي بتثبيت إضافة Deno من justjavac (كانت هناك إضافة أخرى بنفس الاسم عندما جربت، ولكنها مهملة – قد تختفي في المستقبل).

لقطة شاشة لإضافة Deno في VS Code
ستوفر الإضافة العديد من الأدوات والميزات الرائعة لـ VS Code لمساعدتك في كتابة تطبيقاتك. الآن قم بإنشاء ملف app.ts في مجلد والصق الشيفرة أعلاه:

لقطة شاشة لملف app.ts في VS Code
الآن قم بتشغيله باستخدام deno run app.ts:

لقطة شاشة لتشغيل تطبيق Deno محليًا
يقوم Deno بتنزيل جميع التبعيات التي يحتاجها، عن طريق تنزيل الوحدة التي استوردناها أولاً. يحتوي ملف https://deno.land/std/http/server.ts على عدة تبعيات خاصة به:

import { encode } from '../encoding/utf8.ts'
import { BufReader, BufWriter } from '../io/bufio.ts'
import { assert } from '../testing/asserts.ts'
import { deferred, Deferred, MuxAsyncIterator } from '../async/mod.ts'
import {
  bodyReader,
  chunkedBodyReader,
  emptyReader,
  writeResponse,
  readRequest,
} from './_io.ts'
import Listener = Deno.Listener
import Conn = Deno.Conn
import Reader = Deno.Reader

ويتم استيراد هذه التبعيات تلقائيًا. في النهاية، لدينا مشكلة:

لقطة شاشة لخطأ رفض الوصول في Deno
ماذا يحدث؟ لدينا مشكلة رفض الإذن (permission denied). دعنا نتحدث عن نموذج الأمان (sandbox).

نموذج الأمان “Sandbox” في Deno

ذكرت سابقًا أن Deno يحتوي على نموذج أمان يمنع البرامج من القيام بأي شيء لا ترغب في السماح به. ماذا يعني هذا؟

أحد الأشياء التي يذكرها ريان في محاضرته التقديمية لـ Deno هو أنك في بعض الأحيان ترغب في تشغيل برنامج JavaScript خارج متصفح الويب، ومع ذلك لا ترغب في السماح له بالوصول إلى أي شيء يريده على نظامك. أو التواصل مع العالم الخارجي باستخدام الشبكة. لا يوجد ما يمنع تطبيق Node.js من الحصول على مفاتيح SSH الخاصة بك أو أي شيء آخر على نظامك وإرساله إلى خادم. لهذا السبب عادةً ما نقوم بتثبيت حزم Node فقط من مصادر موثوقة. ولكن كيف يمكننا معرفة ما إذا تم اختراق أحد المشاريع التي نستخدمها، وبالتالي اختراق كل شخص آخر؟

يحاول Deno تكرار نفس نموذج الأذونات الذي يطبقه المتصفح. لا يمكن لأي JavaScript يعمل في المتصفح القيام بأشياء مشبوهة على نظامك ما لم تسمح بذلك صراحةً.

بالعودة إلى Deno، إذا أراد برنامج الوصول إلى الشبكة كما في الحالة السابقة، فنحن بحاجة إلى منحه الإذن. يمكننا القيام بذلك عن طريق تمرير علامة (flag) عند تشغيل الأمر، في هذه الحالة --allow-net:

deno run --allow-net app.ts

لقطة شاشة لتشغيل تطبيق Deno مع إذن الشبكة
يعمل التطبيق الآن كخادم HTTP على المنفذ 8000:

لقطة شاشة لتطبيق Deno يعمل كخادم HTTP
تسمح علامات أخرى لـ Deno بفتح وظائف أخرى:

  • --allow-env: للسماح بالوصول إلى متغيرات البيئة.
  • --allow-hrtime: للسماح بقياس الوقت بدقة عالية.
  • --allow-net=<allow-net>: للسماح بالوصول إلى الشبكة (يمكن تحديد نطاق الوصول).
  • --allow-plugin: للسماح بتحميل المكونات الإضافية.
  • --allow-read=<allow-read>: للسماح بالوصول للقراءة من نظام الملفات (يمكن تحديد مسارات معينة).
  • --allow-run: للسماح بتشغيل العمليات الفرعية (subprocesses).
  • --allow-write=<allow-write>: للسماح بالوصول للكتابة إلى نظام الملفات (يمكن تحديد مسارات معينة).
  • --allow-all: للسماح بجميع الأذونات (نفس -A).

يمكن أن تكون الأذونات لـ net و read و write دقيقة. على سبيل المثال، يمكنك السماح بالقراءة من مجلد معين باستخدام --allow-read=/dev.

تنسيق الشيفرة البرمجية في Deno

أحد الأشياء التي أعجبتني حقًا في Go هو الأمر gofmt الذي يأتي مع مترجم Go. تبدو جميع شيفرات Go متشابهة. الجميع يستخدم gofmt. اعتاد مبرمجو JavaScript على تشغيل Prettier، ويقوم deno fmt في الواقع بتشغيل ذلك في الخلفية.

لنفترض أن لديك ملفًا منسقًا بشكل سيء مثل هذا:

لقطة شاشة لشيفرة Deno غير منسقة
تقوم بتشغيل deno fmt app.ts ويتم تنسيقه تلقائيًا بشكل صحيح، مع إضافة الفواصل المنقوطة (semicolons) حيثما كانت مفقودة:

لقطة شاشة لشيفرة Deno منسقة باستخدام deno fmt

المكتبة القياسية في Deno

المكتبة القياسية لـ Deno واسعة النطاق على الرغم من أن المشروع لا يزال حديثًا جدًا. تتضمن:

  • archive: أدوات أرشيف tar.
  • async: أدوات مساعدة غير متزامنة.
  • bytes: أدوات مساعدة لمعالجة شرائح البايت.
  • datetime: تحليل التاريخ/الوقت.
  • encoding: ترميز/فك ترميز لمختلف التنسيقات.
  • flags: تحليل علامات سطر الأوامر.
  • fmt: التنسيق والطباعة.
  • fs: واجهة برمجة تطبيقات نظام الملفات.
  • hash: مكتبة التشفير.
  • http: خادم HTTP.
  • io: مكتبة الإدخال/الإخراج.
  • log: أدوات التسجيل.
  • mime: دعم البيانات متعددة الأجزاء (multipart data).
  • node: طبقة توافق مع المكتبة القياسية لـ Node.js (قيد التطوير).
  • path: معالجة المسارات.
  • ws: مقابس الويب (websockets).

مثال آخر على تطبيق Deno: أداة cat

دعنا نرى مثالًا آخر لتطبيق Deno، من أمثلة Deno: cat:

const filenames = Deno.args
for (const filename of filenames) {
  const file = await Deno.open(filename)
  await Deno.copy(file, Deno.stdout)
  file.close()
}

تقوم هذه الشيفرة بتعيين محتوى Deno.args للمتغير filenames، وهو متغير يحتوي على جميع الوسائط المرسلة إلى الأمر. نكرر عبرها، ولكل ملف نستخدم Deno.open() لفتح الملف ونستخدم Deno.copy() لطباعة محتوى الملف إلى Deno.stdout. أخيرًا نغلق الملف.

إذا قمت بتشغيل هذا باستخدام:

deno run https://deno.land/std/examples/cat.ts

يتم تنزيل البرنامج وتجميعه، ولا يحدث شيء لأننا لم نحدد أي وسيطة. جرب الآن:

deno run https://deno.land/std/examples/cat.ts app.ts

بافتراض أن لديك ملف app.ts من المشروع السابق في نفس المجلد. ستحصل على خطأ في الأذونات:

لقطة شاشة لخطأ رفض الوصول إلى نظام الملفات في Deno
لأن Deno يمنع الوصول إلى نظام الملفات افتراضيًا. امنح الوصول إلى المجلد الحالي باستخدام --allow-read=./:

deno run --allow-read=./ https://deno.land/std/examples/cat.ts app.ts

لقطة شاشة لتشغيل تطبيق Deno cat مع إذن القراءة

هل توجد أطر عمل مثل Express/Koa لـ Deno؟

نعم، بالتأكيد. تحقق من مشاريع مثل:

  • deno-drash
  • deno-express
  • oak
  • pogo
  • servest

مثال عملي: بناء واجهة برمجة تطبيقات (REST API) باستخدام إطار عمل Oak

أرغب في تقديم مثال بسيط لكيفية بناء واجهة برمجة تطبيقات REST API باستخدام Oak. يتميز Oak بأنه مستوحى من Koa، وهو وسيط Node.js الشهير، وبسبب هذا فهو مألوف جدًا إذا كنت قد استخدمته من قبل.

واجهة برمجة التطبيقات التي سنبنيها بسيطة جدًا. سيقوم خادمنا بتخزين قائمة من الكلاب بأسماء وأعمار في الذاكرة. نريد:

  • إضافة كلاب جديدة.
  • عرض قائمة الكلاب.
  • الحصول على تفاصيل حول كلب معين.
  • إزالة كلب من القائمة.
  • تحديث عمر كلب.

سنقوم بذلك في TypeScript، ولكن لا شيء يمنعك من كتابة API في JavaScript – ما عليك سوى إزالة الأنواع (types).

أنشئ ملف app.ts. لنبدأ باستيراد الكائنين Application و Router من Oak:

import { Application, Router } from 'https://deno.land/x/oak/mod.ts'

ثم نحصل على متغيرات البيئة PORT و HOST:

const env = Deno.env.toObject()
const PORT = env.PORT || 4000
const HOST = env.HOST || '127.0.0.1'

افتراضيًا، سيعمل تطبيقنا على localhost:4000. الآن نقوم بإنشاء تطبيق Oak ونبدأ تشغيله:

const router = new Router()
const app = new Application()
app.use(router.routes())
app.use(router.allowedMethods())

console.log(`Listening on port ${PORT} ...`)
await app.listen(`${HOST}:${PORT}`)

الآن يجب أن يتم تجميع التطبيق بشكل جيد. قم بتشغيل deno run --allow-env --allow-net app.ts وسيتم تنزيل التبعيات بواسطة Deno:

لقطة شاشة لتنزيل تبعيات Oak في Deno
ثم يستمع على المنفذ 4000. في المرات التالية التي تشغل فيها الأمر، سيتخطى Deno جزء التثبيت لأن هذه الحزم مخزنة مؤقتًا بالفعل:

لقطة شاشة لتطبيق Deno Oak يعمل على المنفذ 4000
في أعلى الملف، دعنا نحدد واجهة (interface) للكلب، ثم نعلن عن مصفوفة dogs أولية من كائنات Dog:

interface Dog {
  name: string
  age: number
}

let dogs: Array<Dog> = [
  {
    name: 'Roger',
    age: 8,
  },
  {
    name: 'Syd',
    age: 7,
  },
]

الآن دعنا ننفذ API بالفعل. لدينا كل شيء جاهز. بعد إنشاء الموجه (router)، دعنا نضيف بعض الدوال التي سيتم استدعاؤها في كل مرة يتم فيها الوصول إلى أحد هذه النقاط الطرفية (endpoints):

const router = new Router()
router
  .get('/dogs', getDogs)
  .get('/dogs/:name', getDog)
  .post('/dogs', addDog)
  .put('/dogs/:name', updateDog)
  .delete('/dogs/:name', removeDog)

لاحظ كيف نحدد:

  • GET /dogs
  • GET /dogs/:name
  • POST /dogs
  • PUT /dogs/:name
  • DELETE /dogs/:name

دعنا ننفذ هذه الدوال واحدة تلو الأخرى. بدءًا من GET /dogs، التي تُرجع قائمة بجميع الكلاب:

export const getDogs = ({ response }: { response: any }) => {
  response.body = dogs
}

لقطة شاشة لنتيجة GET /dogs في Deno API
بعد ذلك، إليك كيفية استرداد كلب واحد بالاسم:

export const getDog = (
  { params, response, }: { params: { name: string }; response: any }
) => {
  const dog = dogs.filter((dog) => dog.name === params.name)
  if (dog.length) {
    response.status = 200
    response.body = dog[0]
    return
  }
  response.status = 400
  response.body = { msg: `Cannot find dog ${params.name}` }
}

لقطة شاشة لنتيجة GET /dogs/:name في Deno API
إليك كيفية إضافة كلب جديد:

export const addDog = async ({ request, response, }: { request: any; response: any }) => {
  const body = await request.body()
  const dog: Dog = body.value
  dogs.push(dog)
  response.body = { msg: 'OK' }
  response.status = 200
}

لقطة شاشة لنتيجة POST /dogs في Deno API
لاحظ أنني استخدمت الآن const body = await request.body() للحصول على محتوى الجسم (body)، نظرًا لأن قيم name و age تُمرر كـ JSON. إليك كيفية تحديث عمر كلب:

export const updateDog = async (
  { params, request, response, }: { params: { name: string }; request: any; response: any }
) => {
  const temp = dogs.filter((existingDog) => existingDog.name === params.name)
  const body = await request.body()
  const { age }: { age: number } = body.value
  if (temp.length) {
    temp[0].age = age
    response.status = 200
    response.body = { msg: 'OK' }
    return
  }
  response.status = 400
  response.body = { msg: `Cannot find dog ${params.name}` }
}

لقطة شاشة لنتيجة PUT /dogs/:name في Deno API
وإليك كيفية إزالة كلب من قائمتنا:

export const removeDog = (
  { params, response, }: { params: { name: string }; response: any }
) => {
  const lengthBefore = dogs.length
  dogs = dogs.filter((dog) => dog.name !== params.name)
  if (dogs.length === lengthBefore) {
    response.status = 400
    response.body = { msg: `Cannot find dog ${params.name}` }
    return
  }
  response.body = { msg: 'OK' }
  response.status = 200
}

لقطة شاشة لنتيجة DELETE /dogs/:name في Deno API
إليك الشيفرة الكاملة للمثال:

import { Application, Router } from 'https://deno.land/x/oak/mod.ts'

const env = Deno.env.toObject()
const PORT = env.PORT || 4000
const HOST = env.HOST || '127.0.0.1'

interface Dog {
  name: string
  age: number
}

let dogs: Array<Dog> = [
  {
    name: 'Roger',
    age: 8,
  },
  {
    name: 'Syd',
    age: 7,
  },
]

export const getDogs = ({ response }: { response: any }) => {
  response.body = dogs
}

export const getDog = (
  { params, response, }: { params: { name: string }; response: any }
) => {
  const dog = dogs.filter((dog) => dog.name === params.name)
  if (dog.length) {
    response.status = 200
    response.body = dog[0]
    return
  }
  response.status = 400
  response.body = { msg: `Cannot find dog ${params.name}` }
}

export const addDog = async ({ request, response, }: { request: any; response: any }) => {
  const body = await request.body()
  const { name, age }: { name: string; age: number } = body.value
  dogs.push({ name: name, age: age, })
  response.body = { msg: 'OK' }
  response.status = 200
}

export const updateDog = async (
  { params, request, response, }: { params: { name: string }; request: any; response: any }
) => {
  const temp = dogs.filter((existingDog) => existingDog.name === params.name)
  const body = await request.body()
  const { age }: { age: number } = body.value
  if (temp.length) {
    temp[0].age = age
    response.status = 200
    response.body = { msg: 'OK' }
    return
  }
  response.status = 400
  response.body = { msg: `Cannot find dog ${params.name}` }
}

export const removeDog = (
  { params, response, }: { params: { name: string }; response: any }
) => {
  const lengthBefore = dogs.length
  dogs = dogs.filter((dog) => dog.name !== params.name)
  if (dogs.length === lengthBefore) {
    response.status = 400
    response.body = { msg: `Cannot find dog ${params.name}` }
    return
  }
  response.body = { msg: 'OK' }
  response.status = 200
}

const router = new Router()
router
  .get('/dogs', getDogs)
  .get('/dogs/:name', getDog)
  .post('/dogs', addDog)
  .put('/dogs/:name', updateDog)
  .delete('/dogs/:name', removeDog)

const app = new Application()
app.use(router.routes())
app.use(router.allowedMethods())

console.log(`Listening on port ${PORT} ...`)
await app.listen(`${HOST}:${PORT}`)

مصادر إضافية لاستكشاف Deno

  • الموقع الرسمي لـ Deno: https://deno.land
  • وثائق API: https://doc.deno.land و https://deno.land/typedoc/index.html
  • مجموعة مشاريع Deno الرائعة (awesome-deno): https://github.com/denolib/awesome-deno

ملاحظات متفرقة حول Deno

  • يوفر Deno تطبيقًا مدمجًا لـ fetch يتطابق مع المتوفر في المتصفح.
  • يحتوي Deno على طبقة توافق مع المكتبة القياسية لـ Node.js (stdlib) قيد التطوير.

الخلاصة التقنية

يمثل Deno خطوة جريئة نحو مستقبل تطوير تطبيقات الخادم، حيث يقدم بيئة تشغيل حديثة تستفيد من أحدث ميزات JavaScript و TypeScript. إن تركيزه على الأمان من خلال نموذج الأذونات (sandbox)، ودعم TypeScript الأصيل، ونظام استيراد الوحدات القائم على عناوين URL، يجعله خيارًا جذابًا للمطورين الذين يبحثون عن بديل أكثر حداثة وكفاءة لـ Node.js.

على الرغم من أن Node.js سيظل لاعبًا رئيسيًا في هذا المجال لسنوات قادمة نظرًا لرسوخه ودعمه الواسع، فإن Deno يقدم حلولًا لمشكلات تصميمية أساسية في Node، مما يجعله مثاليًا للمشاريع الجديدة التي تتطلب أداءً عاليًا وأمانًا معززًا وتجربة تطوير مبسطة. إن تبني Deno قد يتطلب بعض التكيف من قبل مطوري Node.js، خاصة فيما يتعلق بإدارة التبعيات والأذونات، ولكنه يوفر في المقابل بيئة عمل أكثر اتساقًا ومرونة، مما يمهد الطريق لجيل جديد من تطبيقات الويب.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *