قابلیتهای جدید NodeJS 14
۸ مرداد ۱۳۹۹
جدیدترین و آخرین قابلیتهای NodeJS، معمولا نکات مهمی در عرضه این پلتفرم نیستند. NodeJS به خاطر سرعت و سادگی مشهور شده است. به همین دلیل شرکتهای بسیاری از آن استفاده میکنند. به هر حال، با توجه به انتشار نسخه 14، با پشتیبانی بلند مدت یا LTS، قابلیتهای هیجانانگیز زیادی در اختیار توسعهدهندگان NodeJS قرار میگیرد. اما چرا؟ به این دلیل که ویژگیهای ارائه شده از نسخه 12 تا 14، به همراه امکاناتی که آنها ارائه میکنند، بسیار جذاباند!
در این مقاله قابلیتهای جدید و آخرین تغییرها در نسخه 14 را بررسی میکنیم. به عبارتی در اینجا ویژگیهای اضافه شده از نسخه 12 تا 14 را به صورت کلی مرور خواهیم کرد.
Node + Web Assembly = <3
Web Assembly به آرامی در حال رشد و محبوبتر شدن است. Node و ماژولهای آن بیشتر و بیشتر با این زبان تجره کسب میکنند. با انتشار نسخه 14، به Web Assembly System Interface و یا WASI، به صورت آزمایشی، دسترسی خواهیم داشت. ایده اصلی این موضوع، راحتتر کردن دسترسی به لایههای پایینتر در سیستمعامل است. بی صبرانه منتظر مشاهده Web Assembly در Node هستیم.
V8 8.1
این که Node به همراه V8 گوگل ارائه میشود را همگی میدانیم. این موضوع نه تنها اجازه دسترسی به فیلدهای خصوصی را میدهد، بلکه میتوان کارایی و عملکرد را بهبود داد. از آنجایی که JS بهتر و سریعتر میشود، awaitها نیز سریعتر کار میکنند.
برنامههایمان باید سریعتر لود شوند، همچنین دیباگ توابع async نیز راحتتر میشود، زیرا بالاخره میتوانیم stack را در آنها، به صورت دقیق، دنبال کنیم. علاوه بر این، اندازه heap تغییر میکند. تا امروز اندازه آن برای سیستمهای ۳۲ بیتی، ۷۰۰ مگابایت و برای سیستمهای ۶۴ بیتی، ۱۴۰۰ مگابایت بوده است. با توجه به تغییرهای جدید، این اندازه بر اساس memory در دسترس خواهد بود.
با توجه به نسخه 14، به جدیدترین نسخه از V8 دسترسی خواهیم داشت. این بدان معنی است که از قابلیتهای جدید این موتور جاوااسکریپت، بهره خواهیم برد. اگر از TypeScript استفاده میکنید، به احتمال زیاد nullish coalescing و optional chaining امتحان کردهاید. هر دو اینها را میتوانید در Node 14 استفاده کنید.
همچنین آپدیتهای جدیدی در رابطه با Intl دریافت خواهیم کرد. اول پشتیبانی از Intl.DisplayNames و دومی پشتیبانی از تقویم و سیستم اعداد (numberingSystem) برای Intl.DateTimeFormat است.
گزارشهای تشخیصی پایدار و دقیق
در Node 12، یک قابلیت آزمایشی تحت عنوان گزارشهای تشخیصی یا Diagnostic Reports در اختیارمان است. با استفاده از آن، به راحتی میتوانیم گزارشی از وضعیت سیستم، ارائه کنیم. علاوه بر این، میتوانیم این گزارشها را نه تنها به هنگام ایجاد تقاضا، بلکه بعد از یک event مشخص تولید کنیم. اگر یک برنامه Node در حال استفاده دارید، این مورد چیزی است که باید آن را بررسی کنید.
Threadهای تقریبا پایدار
با انتشار آخرین نسخه LTS به Threadها دسترسی پیدا کردیم. البته این یک قابلیت آزمایشی بود و به گزینه experimental-worker
برای کار کردن نیاز داشت.
با این نسخه LTS (یعنی Node 12) این قابلیت همچنان آزمایشی است اما دیگر به این گزینه یا flag نیاز نخواهد داشت. با این اوصاف در حال نزدیک شدن به یک نسخه پایدار هستیم.
پشتیبانی ماژولهای ES
بیایید این موضوع را بررسی کنیم، ماژولهای ES، در حال حاضر راهی برای ورود به دنیای توسعه جاوااسکریپت است. از آنها برای توسعه برنامههای فرانتاند، دسکتاپ و حتی موبایل، استفاده میکنیم. و در Node، همچنان در میان ماژولهای common.js گیر افتادهایم.
البته میتوانیم از Babel و یا TypeScript استفاده کنیم، اما از آنجایی که NodeJS یک تکنولوژی بکاند است، تنها چیزی که اهمیت دارد، نسخه Node استفاده شده بر روی سرور است. نیازی نیست که در رابطه با پشتیبانی از برنامه در مرورگرهای مختلف، نگرانی داشته باشیم، پس هدف از نصب ابزاری که با توجه به این موضوع ساخته شده است، چیست ( مثل Babel و یا Webpack)؟
در Node 10، تا حدودی میتوانستیم از ماژولهای ES استفاده کنیم (نسخه LTS کنونی به صورت آزمایشی ماژولها را پیاده سازی میکند)، اما برای این کار نیاز بود که برای فایلهایمان از یک پسوند خاص، یعنی .mjs
(که مخفف module javascript است)، استفاده کنیم.
در Node 12، کار کردن با این موضوع تا حدودی راحتتر شد. به صورتی که در برنامههای تحت وب، یک ویژگی type
داریم که با استفاده از آن مشخص میکنیم که باید با کد همانند common.js یا es module، رفتار شود.
برای اینکه کد شما یک ماژول باشد، تمام کاری که باید انجام دهید این است که در فایل package.json
، ویژگی type
را برابر با module
قرار دهید:
{
"type": "module"
}
از حالا به بعد، اگر این فایل package.json
، نزدیکترین فایل به فایل .js
ما باشد، همانند یک ماژول با آن برخورد خواهد شد. دیگر نیاز به استفاده از .mjs
نیست، اما اگر بخواهید میتوانید از آن استفاده کنید.
اما اگر بخواهیم از کدهای common.js استفاده کنیم چه؟ تا زمانی که نزدیکترین فایل package.json
، شامل فیلد type
و مقدار module
نباشد، با این کد همانند common.js رفتار خواهد شد. علاوه بر این، یک پسوند جدید، یعنی cjs
برای فایلهای common.js نیز در اختیار ما قرار خواهد گرفت. با هر فایل mjs
مانند ماژول و با هر فایل cjs
همانند common.js، رفتار خواهد شد. اگر تا به حال از اینها استفاده نکردید، اکنون زمان استفاده از آنها فرارسیده است.
جاوااسکریپت و متغیرهای خصوصی
وقتی صحبت از جاوااسکریپت میشود، همیشه به دنبال این بودهایم که از دادههای موجود در کلاسها و یا توابع، در خارج از آنها، محافظت کنیم. جاوااسکریپت به خاطر monkey patchin مشهور است، زیرا همیشه به طریقی، میتوانیم به همه چیز دسترسی داشته باشیم.
با استفاده از محدود کردن متغیرها به یک بلوک و یا closures و symbolها و … تلاش کردیم که متغیرهای خصوصی را شبیهسازی کنیم. Node 12 نسخه جدیدی از V8 را ارائه میکند که توسط آن، به یک قابلیت جذاب دسترسی داشتیم، آن هم تعریف متغیرهای خصوصی یا private در کلاسها است. مطمئن هستیم که همگی راه قدیمی برای ایجاد متغیرهای خصوصی را در Node، به یاد دارید:
class MyClass {
constructor() {
this._x = 10
}
get x() {
return this._x
}
}
همگی میدانیم که این روش، واقعا خصوصی یا private نیست، به هر حال میتوانیم به آن دسترسی پیدا کنیم، اما اکثر IDEها با این مورد، همانند یک فیلد خصوصی رفتار میکنند و توسعهدهندگان Node این موضوع را میدانند. سرانجام میتوانیم این را فراموش کنیم.
class MyClass {
#x = 10
get x() {
return this.#x
}
}
متوجه تفاوت میشوید؟ بله! از کاراکتر #
استفاده کردیم تا به Node بگوییم که این متغیر، خصوصی است و میخواهیم که تنها بتوانیم از درون این کلاس، به آن دسترسی داشته باشیم.
تلاش کنید که مستقیما به آن دسترسی پیدا کنید، با خطای عدم وجود این متغیر روبرو خواهید شد. متاسفانه برخی از IDEها، این نوع متغیرها را به عنوان یک متغیر صحیح و مناسب تشخیص نمیدهند.
Flat and flatMap
در Node 12، به قابلیتهای جدیدی از جاوااسکریپت دسترسی داریم. اول از همه، به متدهای جدیدی برای آرایهها دسترسی داریم، flat
و flatMap
. متد flat
مشابه متد flattenDepth
در loadash
است.
اگر یک آرایه تو در تو را به این متد بدهیم، یک آرایه flat شدهتر (به عبارتی لایههای یک آرایه تو در تو و پیچیده را کمتر میکند) را تحویل میدهد. برای فهم بهتر به مثال زیر توجه کنید:
[10, [20, 30], [40, 50, [60, 70]]].flat() // => [10, 20, 30, 40, 50, [60, 70]]
[10, [20, 30], [40, 50, [60, 70]]].flat(2) // => [10, 20, 30, 40, 50, 60, 70]
از آنجایی که میتوانید مشاهده کنید، یک پارامتر اختصاصی، یعنی depth
، دارد که با استفاده از آن تعیین میکنید تا چه سطح و لایه این عملیات انجام شود. متد دوم، flatMap
است که همانند map
، به همراه flat
، کار میکند.
استفاده اختیاری از catch
قابلیت جدید بعدی، استفاده از بلوک catch
، به صورت اختیاری است. تا قبل از این قابلیت، باید همیشه متغیر error
را در بلوک catch
، تعریف میکردیم:
try {
someMethod()
} catch(err) {
// err is required
}
در Node 12، نمیتوانیم کلا از بلوک catch
استفاده نکنیم، اما میتوانیم از تعریف متغیر error
، بگذریم:
try {
someMethod()
} catch {
// err is optional
}
Object.fromEntries
قابلیت جدید دیگر در جاوااسکریپت، اضافه شدن متد Object.fromEntries
است. کارایی اصلی این متد، ایجاد آبجکت از یک map
و یا یک آرایه از کلید و مقدار است.
Object.fromEntries(new Map([['key', 'value'], ['otherKey', 'otherValue']]));
// { key: 'value', otherKey: 'otherValue' }
Object.fromEntries([['key', 'value'], ['otherKey', 'otherValue']]);
// { key: 'value', otherKey: 'otherValue' }
NodeJS جدید و threadها
یک موضوع است که همگی بر روی آن توافق داریم، و آن هم این است که هر زبان، مزایا و معایب خود را دارد. بیشتر تکنولوژیهای مشهور، جایگاه خودشان را در دنیا تکنولوژیها، پیدا کردهاند. NodeJS هم از این قضیه مستثنی نیست.
سالها بر این که NodeJS برای ایجاد API و یا داشبوردهای real-time مناسب است، تاکید کردهایم (به طور مثال توسط websockets). در حقیقت، طراحی آن، به خودی خود باعث میشود تا به معماری میکروسرویسها، برای حل مسائل و مشکلها، وابسته شویم.
در پایان، میدانیم که به دلیل طراحی تک رشتهای NodeJS، به معنی اجرای محاسبات و عملیات زمانبر، سنگین برای پردازشگر و یا آنهایی که باعث مسدود شدن اجرای مابقی عملیات میشوند، نیست. این موضوع ماهیت event loop است.
اگر این حلقه را با یک عملیات sync مسدود کنیم، قادر به انجام هیچ کار نخواهد بود تا اینکه اجرای این عملیات به پایان برسد. به همین دلیل به شدت از عملیات async استفاده میکنیم و یا اجرای کارهای زمانبر را به سایر میکروسرویسها منتقل میکنم.
این موضوع و راه حل، بنا به قابلیتهای NodeJS 10، دیگر ضروری نیست. ابزاری که باعث ایجاد تفاوت میشود، worker threadها هستند. در نهایت، NodeJS در زمینههایی که از زبانهای دیگر استفاده میکنیم، بهتر و برتر است.
برای مثال میتوانیم به هوش مصنوعی، یادگیری ماشین و یا پردازش بیگ دیتا، اشاره کنیم. قبلتر، این موضوعها، به محاسبات سنگین در CPU نیاز داشتند و چارهای برای ایجاد سرویسی دیگر یا انتخاب یک زبان مناسبتر، در اختیارمان نمیگذاشتند، نه بیشتر.
threadها
این قابلیت جدید NodeJS نیز همچنان در وضعیت آزمایشی است، پس نمیتوان از آن در محیط پروداکشن استفاده کرد. اما میتوانیم از آن استفاده کنیم. خب، از کجا باید شروع کنیم؟ از Node 12 شروع میکنیم که نیاز به استفاده از گزینه experimental-worker
نیست، زیرا workerها به صورت پیشفرض فعال هستند.
node index.js
حالا میتوانیم از تمام مزایای ماژول worker_threads
استفاده کنیم. بیایید با یک سرور ساده HTTP، به همراه دو متد، شروع کنیم:
- یک متد
GET
برای آدرس/hello
که یک آبجکت JSON به همراه پیام"Hello World"
برمیگرداند. - یک متد
GET
دیگر، برای آدرس/compute
که یک فایل JSON بزرگ را توسط یک متد sync، چندین بار لود میکند.
const express = require('express');
const fs = require('fs');
const app = express();
app.get('/hello', (req, res) => {
res.json({
message: 'Hello world!'
})
});
app.get('/compute', (req, res) => {
let json = {};
for (let i=0;i<100;i++) {
json = JSON.parse(fs.readFileSync('./big-file.json', 'utf8'));
}
json.data.sort((a, b) => a.index - b.index);
res.json({
message: 'done'
})
});
app.listen(3000);
پیشبینی خروجی بسیار آسان است. وقتی متد GET
بر روی /hello
و /compute
، به دنبال هم اجرا میشود، باید صبر کنیم تا عملیات درخواست به آدرس compute
، به پایان برسد تا بتوانیم به درخواستی برای آدرس hello
، پاسخ دهیم. به عبارتی event loop تا پایان لود فایل، مسدود میشود.
بیایید این قضیه را توسط threadها، حل کنیم:
const express = require('express');
const fs = require('fs');
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
if (isMainThread) {
console.log("Spawn http server");
const app = express();
app.get('/hello', (req, res) => {
res.json({
message: 'Hello world!'
})
});
app.get('/compute', (req, res) => {
const worker = new Worker(__filename, {workerData: null});
worker.on('message', (msg) => {
res.json({
message: 'done'
});
})
worker.on('error', console.error);
worker.on('exit', (code) => {
if(code != 0)
console.error(new Error(`Worker stopped with exit code ${code}`))
});
});
app.listen(3000);
} else {
let json = {};
for (let i=0;i<100;i++) {
json = JSON.parse(fs.readFileSync('./big-file.json', 'utf8'));
}
json.data.sort((a, b) => a.index - b.index);
parentPort.postMessage({});
}
همانطور که میبینید، سینتکس به چیزی که در رابطه با تغییر مقیاس توسط Cluster در NodeJS میدانیم، بسیار شبیه است. اما موضوع جذاب از اینجا شروع میشود. تلاش کنید که به هر دو آدرس، در یک زمان واحد، درخواستی را ارسال کنید. متوجه چیزی شدید؟ در واقع، event loop دیگر مسدود نشده است و ما میتوانیم در حین لود فایل، به آدرس hello
، درخواستی را ارسال کنیم و بلافاصله پاسخ را دریافت کنیم. حالا این تمام چیزی است که همگی منتظر آن هستیم! تمام چیزی که باقی مانده، صبر کردن برای یک API پایدار است.
N-API برای ساخت ماژولهای C/C++
سرعت NodeJS، یکی از دلایلی است که این تکنولوژی را انتخاب میکنیم. worker threads، مبحث بعدی برای اثبات این قضیه است. اما آیا واقعا کافی است؟
NodeJS یک تکنولوژی براساس C است. طبیعتا از جاوااسکریپت به عنوان یک زبان برنامه نویسی اصلی استفاده میکنیم. اما اگر بتوانیم از C، برای محاسبات پیچیدهتر استفاده کنیم، چه اتفاقی میافتد؟
NodeJS 10 یک N-API پایدار در اختیار ما میگذارد. این یک API استاندارد برای ماژولهای پایهای است، که این امکان را به ما میدهد تا ماژولها را به زبان C/C++
یا حتی Rust، ایجاد کنیم. به نظر جالب است، نه؟
یک ماژول پایهای میتواند مشابه مثال زیر باشد:
#include <napi.h>
#include <math.h>
namespace helloworld {
Napi::Value Method(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
return Napi::String::New(env, "hello world");
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set(Napi::String::New(env, "hello"),
Napi::Function::New(env, Method));
return exports;
}
NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init)
}
اگر یک دانش پایهای در رابطه با C++
دارید، نوشتن یک ماژول سفارشی، نباید کار سختی باشد. تنها چیزی که نیاز است به یاد داشته باشید، تبدیل تایپهای C++
به NodeJS، در انتهای ماژول است. مورد بعدی که نیاز داریم، binding است:
{
"targets": [
{
"target_name": "helloworld",
"sources": [ "hello-world.cpp"],
"include_dirs": ["<!@(node -p \"require('node-addon-api').include\")"],
"dependencies": ["<!(node -p \"require('node-addon-api').gyp\")"],
"defines": [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ]
}
]
}
این تنظیمات ساده، این امکان را به ما میدهد تا فایلهای *.cpp
را ایجاد کنیم، بنابراین میتوانیم بعدها در برنامههای NodeJS از آنها استفاده کنیم. قبل از استفاده از آن در کد جاوااسکریپتمان، باید فایل package.json
را بیلد و کانفیگ کنیم تا مانند فایل gypfile
شود (فایل binding).
{
"name": "n-api-example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"install": "node-gyp rebuild"
},
"gypfile": true,
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"node-addon-api": "^1.5.0",
"node-gyp": "^3.8.0"
}
}
هنگامی که ماژول آماده شود، میتوانیم از دستور node-gyp build
، برای بیلد و استفاده در کد، بهره ببریم. همانند سایر ماژولهایی که استفاده میکنیم.
const addon = require('./build/Release/helloworld.node');
console.log(addon.hello());
در کنار worker threads، ابزارهای زیادی توسط N-API، برای ساخت برنامههایی با عملکرد بالا، در اختیارمان قرار میدهد. APIها یا داشبوردها، حتی پردازش پیچیده دادهها یا سیستمهای یادگیری ماشین، غیر ممکن نیست. بسیار عالی!
پشتیبانی از HTTP/2
میتوانیم محاسبات را سریعتر انجام دهیم، حتی میتوانیم آنها را به صورت موازی اجرا کنیم. پس در رابطه با assetها و ارائه صفحات چی؟
سالها است که از ماژول قدیمی و عالی http و HTTP/1.1 استفاده میکنیم. هرچه assetهای بیشتری توسط سرورهایمان ارائه شود، مدت زمان بارگذاری به صورت زیادی افزایش مییابد. هر مرورگر حداکثر تعدادی برای اتصالات پیاپی و همزمان، به ازای هر سرور یا پروکسی، دارد، به ویژه برای HTTP/1.1. با پشتیبانی از HTTP/2، به راحتی میتوانیم از این مشکل عبور کنیم.
بااینحال، از کجا شروع کنیم؟ آیا این مثال پایهای از یک سرور NodeJS، که در هر آموزشی وجود دارد را به یاد دارید؟
const http = require('http');
http.createServer(function (req, res) {
res.write('Hello World!');
res.end();
}).listen(3000);
در NodeJS 10، به ماژول جدید http2 دسترسی داریم که اجازه استفاده از HTTP/2.0 را به ما میدهد:
const http = require('http2');
const fs = require('fs');
const options = {
key: fs.readFileSync('example.key'),
cert: fs.readFileSync('example.crt')
};
http.createSecureServer(options, function (req, res) {
res.write('Hello World!');
res.end();
}).listen(3000);
آینده درخشان NodeJS، با توجه به این قابلیتها
قابلیتهای جدید NodeJS، حال و هوای تازهای را به اکوسیستم فناوری ما وارد و کاملا امکانات جدیدی را به NodeJS اضافه میکنند. آیا تا به حال تصور این را کرده بودید که روزی بتوان از این تکنولوژی برای پردازش تصور یا علم داده استفاده کرد؟ من هم فکرش را نمیکردم.
آخرین نسخه Node، قابلیتهایی که خیلی انتظار میرفتند را ارائه میکند، نظیر: پشتیبانی از ماژولهای es (هنوز در حالت آزمایشی قرار دارد) یا تغییرها در متدهای فایل سیستم، که اجازه استفاده از promiseها را به جای callbackها، میدهد.
همانطور که در نمودار زیر میبینید، به نظر میرسد که محبوبیت NodeJS در اوایل ۲۰۱۷، پس از سالها و سالها رشد، به اوج خود رسیده است. این به معنی آروم و کند بودن آن نیست، بلکه به معنی بلوغ این فناوری است.
بااینحال، قطعا میتوانم ببینم که چگونه همه این پیشرفت و بهبودها، باعث افزایش محبوبیت برنامههای بلاکچین NodeJS (بر اساس truffle.js) میشود، ممکن است باعث سرعت بخشیدن به NodeJS، به جهت بهبود و یا شکوفا شدن آن، در مدلهای جدید از پروژهها، نقشها و شرایط شود.
با انتشار NodeJS 14، آینده NodeJS، روشنتر و درخشانتر خواهد شد.