~10 دقیقه مطالعه • بروزرسانی ۸ دی ۱۴۰۴
1. معرفی
ماژول node:module از نسخهٔ v0.3.7 معرفی شد و ابزارهای عمومی برای مدیریت ماژولها ارائه میدهد. این ماژول با require('node:module') یا import 'node:module' در دسترس است.
2. module.builtinModules
لیستی از نام تمام ماژولهای داخلی Node.js را برمیگرداند. این ویژگی برای تشخیص ماژولهای داخلی از ماژولهای شخص ثالث مفید است.
3. module.createRequire(filename)
امکان ایجاد تابع require در محیط ESM را فراهم میکند:
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const siblingModule = require('./sibling-module');
4. module.findPackageJSON(specifier[, base])
فایل package.json مرتبط با یک ماژول را پیدا میکند. این قابلیت برای یافتن ریشهٔ پکیج یا نزدیکترین package.json مفید است.
5. module.isBuiltin(moduleName)
بررسی میکند که آیا یک ماژول داخلی است یا خیر:
import { isBuiltin } from 'node:module';
isBuiltin('fs'); // true
isBuiltin('wss'); // false
6. module.register و module.registerHooks
module.register: ثبت hooks سفارشی برای تغییر رفتار بارگذاری ماژولها.module.registerHooks: ثبت hooks همگام برای تغییر رفتار بارگذاری.
7. module.stripTypeScriptTypes
این متد annotationهای TypeScript را از کد حذف میکند و در حالت transform ویژگیهای TS را به جاوااسکریپت تبدیل میکند.
const { stripTypeScriptTypes } = require('node:module');
const code = 'const a: number = 1;';
console.log(stripTypeScriptTypes(code));
// خروجی: const a = 1;
8. module.syncBuiltinESMExports()
این متد bindingهای زندهٔ ماژولهای داخلی ESM را با خروجیهای CommonJS همگام میکند.
9. Compile Cache
Node.js میتواند cache کامپایل V8 را روی دیسک ذخیره کند تا بارگذاریهای بعدی سریعتر انجام شوند. این قابلیت با module.enableCompileCache() فعال میشود و مسیر cache با module.getCompileCacheDir() قابل دسترسی است.
- گزینهٔ portable امکان استفاده از cache حتی پس از جابهجایی پروژه را فراهم میکند.
- با
module.flushCompileCache()میتوان cache را بهطور دستی روی دیسک نوشت.
10. وضعیت Compile Cache
نتیجهٔ فعالسازی cache با مقادیر ثابت مانند ENABLED، ALREADY_ENABLED، FAILED یا DISABLED مشخص میشود.
نتیجهگیری
ماژول node:module در Node.js ابزارهای قدرتمندی برای مدیریت ماژولها، سفارشیسازی رفتار بارگذاری، و بهینهسازی عملکرد فراهم میکند. این قابلیتها توسعهدهندگان را قادر میسازد کنترل دقیقتری بر چرخهٔ بارگذاری و اجرای ماژولها داشته باشند.
1. فعالسازی hooks
برای سفارشیسازی بارگذاری ماژولها میتوان از دو روش استفاده کرد:
module.register(): ثبت hooks ناهمگام در یک thread جداگانه.module.registerHooks(): ثبت hooks همگام در thread اصلی.
این hooks میتوانند قبل از اجرای برنامه با فلگهای زیر فعال شوند:
node --import ./register-hooks.js ./my-app.js node --require ./register-hooks.js ./my-app.js
2. مثال ساده
// register-hooks.js
const { register } = require('node:module');
const { pathToFileURL } = require('node:url');
// ثبت hooks ناهمگام
register('./hooks.mjs', pathToFileURL(__filename));
// ثبت hooks همگام
const { registerHooks } = require('node:module');
registerHooks({
resolve(specifier, context, nextResolve) { /* implementation */ },
load(url, context, nextLoad) { /* implementation */ },
});
3. استفاده از dependency برای hooks
میتوان فایل hooks را از یک پکیج خارجی نیز بارگذاری کرد:
node --import some-package/register ./my-app.js node --require some-package/register ./my-app.js
4. تفاوت import پویا و ایستا
- اگر از
import()پویا استفاده شود، hooks قبل از بارگذاری ماژول اعمال میشوند. - اگر از
importایستا استفاده شود، ماژول قبل از ثبت hooks بارگذاری میشود.
5. پشتیبانی از require و createRequire
hooks همگام علاوه بر import، روی require() و createRequire() نیز اعمال میشوند.
6. زنجیرهسازی hooks
میتوان چندین hook را پشت سر هم ثبت کرد. این زنجیرهها بهصورت LIFO اجرا میشوند:
// entrypoint.cjs
const { register } = require('node:module');
const { pathToFileURL } = require('node:url');
const parentURL = pathToFileURL(__filename);
register('./foo.mjs', parentURL);
register('./bar.mjs', parentURL);
import('./my-app.mjs');
در این مثال، ابتدا bar.mjs اجرا میشود، سپس foo.mjs و در نهایت رفتار پیشفرض Node.js.
7. ارتباط با hooks ناهمگام
از آنجا که hooks ناهمگام در thread جداگانه اجرا میشوند، برای ارتباط باید از MessageChannel استفاده کرد:
const { register } = require('node:module');
const { pathToFileURL } = require('node:url');
const { MessageChannel } = require('node:worker_threads');
const { port1, port2 } = new MessageChannel();
port1.on('message', (msg) => console.log(msg));
port1.unref();
register('./my-hooks.mjs', {
parentURL: pathToFileURL(__filename),
data: { number: 1, port: port2 },
transferList: [port2],
});
نتیجهگیری
سیستم hooks در Node.js ابزاری قدرتمند برای سفارشیسازی فرآیند بارگذاری ماژولهاست. با استفاده از register و registerHooks میتوان رفتار بارگذاری را تغییر داد، چندین hook را زنجیره کرد، و حتی ارتباط بین threadها را مدیریت نمود. این قابلیتها انعطافپذیری بالایی برای توسعهدهندگان فراهم میکنند.
1. Hooks ناهمگام (Asynchronous)
با استفاده از module.register() میتوان مجموعهای از hooks ناهمگام را ثبت کرد. این hooks در یک thread جداگانه اجرا میشوند:
export async function initialize({ number, port }) {
// دریافت داده از register
}
export async function resolve(specifier, context, nextResolve) {
// تبدیل specifier به URL
}
export async function load(url, context, nextLoad) {
// بارگذاری کد منبع از URL
}
این hooks بهصورت پیشفرض به workerهای فرزند نیز منتقل میشوند.
2. Hooks همگام (Synchronous)
با استفاده از module.registerHooks() میتوان hooks همگام را ثبت کرد. این hooks در همان thread اصلی اجرا میشوند:
function resolve(specifier, context, nextResolve) {
// تبدیل specifier به URL
}
function load(url, context, nextLoad) {
// بارگذاری کد منبع از URL
}
برخلاف hooks ناهمگام، این hooks بهطور پیشفرض به workerهای فرزند منتقل نمیشوند مگر اینکه با --import یا --require ثبت شوند.
3. Conventionهای Hooks
- هر hook باید یک شیء ساده برگرداند.
- زنجیرهٔ hooks بهصورت LIFO اجرا میشود.
- برای پایان دادن به زنجیره باید
shortCircuit: trueبرگردانده شود.
4. initialize()
این hook فقط توسط register() پشتیبانی میشود و برای اجرای کد اولیه در thread hooks استفاده میشود. میتواند دادهها و اشیاء قابل انتقال مانند MessagePort دریافت کند.
// path-to-my-hooks.js
export async function initialize({ number, port }) {
port.postMessage(`increment: ${number + 1}`);
}
5. ارتباط با MessageChannel
برای ارتباط بین thread اصلی و hooks ناهمگام میتوان از MessageChannel استفاده کرد:
const { register } = require('node:module');
const { pathToFileURL } = require('node:url');
const { MessageChannel } = require('node:worker_threads');
const { port1, port2 } = new MessageChannel();
port1.on('message', (msg) => console.log(msg));
port1.unref();
register('./path-to-my-hooks.js', {
parentURL: pathToFileURL(__filename),
data: { number: 1, port: port2 },
transferList: [port2],
});
6. resolve()
این hook مسئول تعیین مسیر نهایی ماژول است. میتواند فرمت ماژول را نیز بهعنوان hint به load hook ارسال کند:
// Asynchronous
export async function resolve(specifier, context, nextResolve) {
return nextResolve(specifier, {
...context,
conditions: [...context.conditions, 'another-condition'],
});
}
// Synchronous
function resolve(specifier, context, nextResolve) {
return nextResolve(specifier);
}
نتیجهگیری
Hooks در Node.js ابزار قدرتمندی برای کنترل فرآیند بارگذاری و resolution ماژولها هستند. با استفاده از initialize، resolve و load میتوان رفتار پیشفرض Node.js را تغییر داد، زنجیرههای سفارشی ایجاد کرد و حتی ارتباط بین threadها را مدیریت نمود.
1. تعریف load hook
تابع load(url, context, nextLoad) سه ورودی اصلی دارد:
- url: مسیر ماژول که توسط resolve chain تعیین شده است.
- context: شامل شرایط، فرمت و ویژگیهای import.
- nextLoad: hook بعدی در زنجیره یا بارگذار پیشفرض Node.js.
خروجی باید یک شیء شامل format و source باشد.
2. فرمتهای پشتیبانیشده
| Format | Description | Source Types |
|---|---|---|
| addon | بارگذاری افزونهٔ Node.js | null |
| builtin | ماژول داخلی Node.js | null |
| commonjs | ماژول CommonJS | string / ArrayBuffer / TypedArray |
| json | فایل JSON | string / ArrayBuffer / TypedArray |
| module | ماژول ECMAScript | string / ArrayBuffer / TypedArray |
| wasm | ماژول WebAssembly | ArrayBuffer / TypedArray |
3. تفاوت نسخهٔ ناهمگام و همگام
- ناهمگام: میتواند Promise برگرداند و در thread جداگانه اجرا میشود.
- همگام: در همان thread اصلی اجرا میشود و همیشه خروجی همگام دارد.
در نسخهٔ ناهمگام، اگر source برای CommonJS مشخص نشود، بارگذاری توسط loader پیشفرض انجام میشود. در نسخهٔ همگام، همهٔ require و resolve از طریق hooks سفارشی عبور میکنند.
4. مثال ساده
// Asynchronous
export async function load(url, context, nextLoad) {
const { format } = context;
if (Math.random() > 0.5) {
return { format, shortCircuit: true, source: '...' };
}
return nextLoad(url);
}
// Synchronous
function load(url, context, nextLoad) {
return nextLoad(url);
}
5. بارگذاری از HTTPS
با استفاده از load hook میتوان کد را از شبکه بارگذاری کرد:
// https-hooks.mjs
import { get } from 'node:https';
export function load(url, context, nextLoad) {
if (url.startsWith('https://')) {
return new Promise((resolve, reject) => {
get(url, (res) => {
let data = '';
res.setEncoding('utf8');
res.on('data', (chunk) => data += chunk);
res.on('end', () => resolve({
format: 'module',
shortCircuit: true,
source: data,
}));
}).on('error', reject);
});
}
return nextLoad(url);
}
این روش امکان بارگذاری مستقیم کد از اینترنت را فراهم میکند، اما مشکلاتی مانند کندی، نبود cache و مسائل امنیتی دارد.
6. نکات مهم
- در نسخهٔ ناهمگام، استفاده همزمان با named exports در CommonJS ناسازگار است.
- source در فرمتهای متنی اگر buffer باشد، با
TextDecoderبه string تبدیل میشود. - برای پایان دادن به زنجیره باید
shortCircuit: trueبرگردانده شود.
نتیجهگیری
load hook در Node.js ابزاری قدرتمند برای کنترل نحوهٔ بارگذاری ماژولهاست. با استفاده از آن میتوان منابع را از دیسک، شبکه یا حتی فرمتهای سفارشی بارگذاری کرد. این قابلیت انعطافپذیری بالایی برای توسعهدهندگان فراهم میکند تا رفتار پیشفرض بارگذاری را تغییر دهند و نیازهای خاص پروژه را برآورده سازند.
1. ترنسپایل با load hooks
فرمتهایی که Node.js بهطور پیشفرض نمیشناسد میتوانند با استفاده از load hook به جاوااسکریپت تبدیل شوند. این قابلیت اجازه میدهد فایلهای CoffeeScript یا فرمتهای سفارشی دیگر اجرا شوند.
مثال ناهمگام
// coffeescript-hooks.mjs
import { readFile } from 'node:fs/promises';
import { findPackageJSON } from 'node:module';
import coffeescript from 'coffeescript';
export async function load(url, context, nextLoad) {
if (/\.(coffee|litcoffee|coffee\.md)$/.test(url)) {
const { source: rawSource } = await nextLoad(url, { ...context, format: 'coffee' });
const transformedSource = coffeescript.compile(rawSource.toString(), url);
return { format: await getPackageType(url), shortCircuit: true, source: transformedSource };
}
return nextLoad(url, context);
}
مثال همگام
// coffeescript-sync-hooks.mjs
import { readFileSync } from 'node:fs';
import { registerHooks, findPackageJSON } from 'node:module';
import coffeescript from 'coffeescript';
function load(url, context, nextLoad) {
if (/\.(coffee|litcoffee|coffee\.md)$/.test(url)) {
const { source: rawSource } = nextLoad(url, { ...context, format: 'coffee' });
const transformedSource = coffeescript.compile(rawSource.toString(), url);
return { format: getPackageType(url), shortCircuit: true, source: transformedSource };
}
return nextLoad(url, context);
}
registerHooks({ load });
2. اجرای CoffeeScript Hooks
با داشتن فایل package.json با مقدار "type": "module"، فایلهای CoffeeScript میتوانند در لحظه ترنسپایل و اجرا شوند.
3. Import Maps با resolve hooks
Import maps به توسعهدهندگان اجازه میدهند specifierها را به مسیرهای دلخواه تغییر دهند. این قابلیت با resolve hooks پیادهسازی میشود.
مثال ناهمگام
// import-map-hooks.js
import fs from 'node:fs/promises';
const { imports } = JSON.parse(await fs.readFile('import-map.json'));
export async function resolve(specifier, context, nextResolve) {
if (Object.hasOwn(imports, specifier)) {
return nextResolve(imports[specifier], context);
}
return nextResolve(specifier, context);
}
مثال همگام
// import-map-sync-hooks.js
import fs from 'node:fs';
import module from 'node:module';
const { imports } = JSON.parse(fs.readFileSync('import-map.json', 'utf-8'));
function resolve(specifier, context, nextResolve) {
if (Object.hasOwn(imports, specifier)) {
return nextResolve(imports[specifier], context);
}
return nextResolve(specifier, context);
}
module.registerHooks({ resolve });
4. پشتیبانی Source Map
Node.js از فرمت TC39 ECMA-426 Source Map برای دیباگ بهتر پشتیبانی میکند. Source map کد کامپایلشده را به فایلهای اصلی نگاشت میکند.
module.getSourceMapsSupport(): بررسی فعال بودن پشتیبانی source map.module.findSourceMap(path): دریافت source map برای یک فایل مشخص.module.setSourceMapsSupport(enabled, options): فعال یا غیرفعال کردن پشتیبانی source map بهصورت برنامهنویسی.
کلاس SourceMap
کلاس SourceMap امکان تعامل با source mapها را فراهم میکند:
new SourceMap(payload): ایجاد یک نمونهٔ جدید source map.findEntry(lineOffset, columnOffset): یافتن نگاشتها.findOrigin(lineNumber, columnNumber): نگاشت به فایل اصلی.
نتیجهگیری
ترنسپایل hooks، import maps و پشتیبانی source map قابلیتهای Node.js را فراتر از حالت پیشفرض گسترش میدهند. این امکانات اجازه میدهند فرمتهای غیر استاندارد اجرا شوند، مسیرهای ماژول سفارشی شوند و دیباگ مؤثرتر انجام شود. هرچند قدرتمند، ترنسپایل با hooks بیشتر برای محیط توسعه و تست مناسب است.
نوشته و پژوهش شده توسط دکتر شاهین صیامی