كتابة Plugin
تفتح plugins كامل قدرات محرك webpack أمام مطوري الطرف الثالث. باستخدام callbacks الخاصة بمراحل build، يستطيع المطورون إدخال سلوكياتهم الخاصة داخل عملية build في webpack. كتابة plugins أكثر تقدمًا قليلًا من كتابة loaders؛ لأنك ستحتاج إلى فهم بعض التفاصيل الداخلية منخفضة المستوى في webpack حتى تعرف أين تدخل. كن مستعدًا لقراءة جزء من الكود المصدري.
إنشاء Plugin
يتكون plugin في webpack من:
- دالة JavaScript لها اسم، أو صنف JavaScript.
- يعرّف دالة
applyعلى prototype الخاص به. - يحدد event hook للدخول إليه.
- يتعامل مع بيانات داخلية خاصة بـ instance في webpack.
- يستدعي callback التي يوفرها webpack بعد اكتمال عمله.
// صنف JavaScript.
class MyExampleWebpackPlugin {
// عرّف `apply` كدالة على prototype، وتستقبل compiler كـ argument
apply(compiler) {
// الدخول إلى compilation
compiler.hooks.thisCompilation.tap(
"MyExampleWebpackPlugin",
(compilation) => {
// تحديد hook الخاص بمعالجة assets
compilation.hooks.processAssets.tap(
{
name: "MyExampleWebpackPlugin",
stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
},
(assets) => {
console.log("This is an example plugin!");
console.log(
"Here’s the `compilation` object which represents a single build of assets:",
compilation,
);
},
);
},
);
}
}البنية الأساسية للـ plugin
الـ plugins هي كائنات يتم إنشاء instance منها، وتحتوي على دالة apply في prototype. يستدعي webpack compiler هذه الدالة مرة واحدة أثناء تثبيت plugin. تحصل دالة apply على reference إلى compiler الأساسي في webpack، وهذا يمنحها وصولًا إلى compiler callbacks. تكون بنية plugin عادةً بهذا الشكل:
class HelloWorldPlugin {
apply(compiler) {
// يعمل هذا hook عندما تكتمل عملية build بالكامل
compiler.hooks.done.tap(
"Hello World Plugin",
(stats /* يتم تمرير stats كـ argument عند استخدام done hook. */) => {
console.log("Hello World!");
},
);
}
}
export default HelloWorldPlugin;لاستخدام plugin، أضف instance منه داخل array المسماة plugins في إعدادات webpack:
// webpack.config.js
import HelloWorldPlugin from "hello-world";
export default {
// ... إعداداتك هنا ...
plugins: [new HelloWorldPlugin({ options: true })],
};الطريقة المعتادة للتحقق من خيارات plugin في إصدارات webpack قبل 5.106 هي استخدام schema-utils داخل constructor:
import { validate } from "schema-utils";
// schema لكائن options
const schema = {
type: "object",
properties: {
test: {
type: "string",
},
},
};
export default class HelloWorldPlugin {
constructor(options = {}) {
validate(schema, options, {
name: "Hello World Plugin",
baseDataPath: "options",
});
}
apply(compiler) {}
}ابتداءً من webpack 5.106، يمكنك نقل التحقق إلى apply() عبر الدخول إلى compiler.hooks.validate واستدعاء compiler.validate(...) بدلًا من ذلك:
export default class HelloWorldPlugin {
constructor(options = {}) {
this.options = options;
}
apply(compiler) {
compiler.hooks.validate.tap("HelloWorldPlugin", () => {
compiler.validate(
() => require("./schema/hello-world-plugin.json"),
this.options,
);
});
}
}Compiler وCompilation
من أهم الموارد أثناء تطوير plugins كائنان: compiler وcompilation. فهم دور كل واحد منهما خطوة أولى مهمة لتوسيع محرك webpack.
class HelloCompilationPlugin {
apply(compiler) {
// الدخول إلى compilation hook الذي يمرر compilation كـ argument إلى callback
compiler.hooks.compilation.tap("HelloCompilationPlugin", (compilation) => {
// الآن يمكننا الدخول إلى hooks المتاحة عبر compilation
compilation.hooks.optimize.tap("HelloCompilationPlugin", () => {
console.log("Assets are being optimized.");
});
});
}
}
export default HelloCompilationPlugin;للحصول على قائمة hooks المتاحة على compiler وcompilation والكائنات المهمة الأخرى، راجع توثيق plugins API.
Event hooks غير متزامنة
بعض plugin hooks غير متزامنة. للدخول إليها يمكن استخدام tap، والتي تتصرف بشكل متزامن، أو استخدام tapAsync أو tapPromise، وهما طريقتان غير متزامنتين.
tapAsync
عند استخدام tapAsync للدخول إلى plugins، يجب استدعاء دالة callback التي تُمرر كآخر argument إلى دالتنا.
class HelloAsyncPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync(
"HelloAsyncPlugin",
(compilation, callback) => {
// نفّذ عملًا غير متزامن...
setTimeout(() => {
console.log("Done with async work...");
callback();
}, 1000);
},
);
}
}
export default HelloAsyncPlugin;tapPromise
عند استخدام tapPromise للدخول إلى plugins، يجب إرجاع Promise يتم حلّها عند اكتمال المهمة غير المتزامنة.
class HelloAsyncPlugin {
apply(compiler) {
compiler.hooks.emit.tapPromise(
"HelloAsyncPlugin",
(compilation) =>
// أرجع Promise تُحل عندما ننتهي...
new Promise((resolve, reject) => {
setTimeout(() => {
console.log("Done with async work...");
resolve();
}, 1000);
}),
);
}
}
export default HelloAsyncPlugin;مثال
بعد أن نستطيع الإمساك بـ webpack compiler وبكل compilation منفردة، تصبح الاحتمالات كثيرة جدًا. يمكننا إعادة تنسيق ملفات موجودة، أو إنشاء ملفات مشتقة، أو توليد assets جديدة بالكامل.
سنكتب مثال plugin ينشئ ملف build جديدًا باسم assets.md، ويحتوي هذا الملف على قائمة بكل ملفات assets الموجودة في build. قد يبدو plugin بهذا الشكل:
class FileListPlugin {
static defaultOptions = {
outputFile: "assets.md",
};
// يجب تمرير أي options عبر constructor الخاص بالـ plugin،
// فهذا جزء من public API للـ plugin.
constructor(options = {}) {
// تطبيق options التي حددها المستخدم فوق options الافتراضية،
// وإتاحة options المدمجة لاحقًا لدوال plugin.
// غالبًا يجب أن تتحقق من كل options هنا أيضًا.
this.options = { ...FileListPlugin.defaultOptions, ...options };
}
apply(compiler) {
const pluginName = FileListPlugin.name;
// يمكن الوصول إلى instance الخاصة بـ webpack module من كائن compiler،
// وهذا يضمن استخدام الإصدار الصحيح من module
// لا تستخدم require/import لـ webpack أو أي رموز منه مباشرة.
const { webpack } = compiler;
// يعطينا كائن Compilation reference إلى ثوابت مفيدة.
const { Compilation } = webpack;
// RawSource أحد أصناف "sources" التي يجب استخدامها
// لتمثيل مصادر assets داخل compilation.
const { RawSource } = webpack.sources;
// الدخول إلى hook "thisCompilation" حتى ندخل لاحقًا
// إلى عملية compilation في مرحلة مبكرة.
compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
// الدخول إلى مسار معالجة assets في مرحلة محددة.
compilation.hooks.processAssets.tap(
{
name: pluginName,
// استخدام مرحلة متأخرة من مراحل معالجة assets للتأكد
// من أن كل assets أُضيفت بالفعل إلى compilation بواسطة plugins أخرى.
stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
},
(assets) => {
// "assets" هو object يحتوي على كل assets
// داخل compilation. مفاتيح object هي مسارات assets
// والقيم هي مصادر الملفات.
// المرور على كل assets
// وتوليد محتوى ملف Markdown.
const content = `# In this build:\n\n${Object.keys(assets)
.map((filename) => `- ${filename}`)
.join("\n")}`;
// إضافة asset جديد إلى compilation، حتى يولده webpack
// تلقائيًا داخل مجلد output.
compilation.emitAsset(
this.options.outputFile,
new RawSource(content),
);
},
);
});
}
}
export default FileListPlugin;webpack.config.js
import FileListPlugin from "./file-list-plugin.js";
// استخدم plugin داخل إعدادات webpack:
export default {
// …
plugins: [
// إضافة plugin بالخيارات الافتراضية
new FileListPlugin(),
// أو:
// يمكنك تمرير أي options مدعومة:
new FileListPlugin({
outputFile: "my-assets.md",
}),
],
};سيولّد هذا ملف Markdown بالاسم الذي اخترته، وسيبدو مثل:
# In this build:
- main.css
- main.js
- index.htmlمراقبة تغييرات الملفات
عندما يعمل webpack في watch mode، مثل webpack --watch أو webpack serve، فإنه ينشئ compilation جديدًا لكل rebuild يحدث بسبب تغييرات الملفات.
تسمح مجموعة compiler.modifiedFiles للـ plugin بمعرفة الملفات المحددة التي سببت rebuild، وبالتالي يمكنك تجاوز الأعمال المكلفة عندما تكون الملفات المتغيرة غير مهمة للـ plugin.
هذا مفيد للـ plugins التي تحتاج إلى التفاعل مع تغييرات ملفات معينة فقط، مثل إعادة توليد assets أو إعادة معالجة templates أو إبطال caches.
class WatchNotifierPlugin {
apply(compiler) {
compiler.hooks.watchRun.tap("WatchNotifierPlugin", (compiler) => {
if (compiler.modifiedFiles) {
const changedFiles = [...compiler.modifiedFiles]
.map((file) => ` • ${file}`)
.join("\n");
console.log(`\nFiles changed:\n${changedFiles}`);
}
});
}
}
export default WatchNotifierPlugin;ملاحظة:
compiler.modifiedFilesهيSetوليست array.- تكون
undefinedفي أول build بارد.- لا تُملأ إلا أثناء watch rebuilds.
إضافة file dependencies مخصصة
إذا كان plugin يقرأ ملفات خارجية، مثل ملفات إعدادات أو templates، وwebpack لا يتتبعها افتراضيًا، فيجب أن تخبر webpack بمراقبتها.
يمكنك إخبار webpack بمراقبة أنواع مختلفة من dependencies:
-
تُستخدم
compilation.fileDependenciesلتتبع ملفات فردية يعتمد عليها plugin، حتى يستطيع webpack تشغيل rebuild عندما تتغير هذه الملفات. -
تُستخدم
compilation.contextDependenciesلمراقبة مجلدات، بحيث يؤدي أي تغيير داخلها إلى rebuild. -
تُستخدم
compilation.missingDependenciesلتتبع ملفات غير موجودة حاليًا، حتى يستطيع webpack تشغيل rebuild عند إنشائها.
import path from "node:path";
class TemplateWatchPlugin {
apply(compiler) {
compiler.hooks.compilation.tap("TemplateWatchPlugin", (compilation) => {
const templatePath = path.resolve(__dirname, "my-template.html");
// تأكد من أن webpack يراقب هذا الملف
compilation.fileDependencies.add(templatePath);
// راقب مجلدًا كاملًا context dependency
const templatesDir = path.resolve(__dirname, "templates");
compilation.contextDependencies.add(templatesDir);
// مثال: علّم dependency مفقودًا
const missingFile = path.resolve(__dirname, "missing-file.txt");
compilation.missingDependencies.add(missingFile);
});
}
}
export default TemplateWatchPlugin;بدون استدعاء fileDependencies.add()، لن يشغّل webpack rebuild عند تغير الملف، حتى لو كان plugin يعتمد عليه.
أشكال Plugins المختلفة
يمكن تصنيف plugin إلى أنواع بناءً على event hooks التي يدخل إليها. كل event hook يكون معرّفًا مسبقًا كـ hook متزامن أو غير متزامن أو waterfall أو parallel، ويتم استدعاؤه داخليًا باستخدام call أو callAsync. عادةً تكون قائمة hooks المدعومة أو القابلة للدخول موجودة في الخاصية this.hooks.
مثال:
this.hooks = {
shouldEmit: new SyncBailHook(["compilation"]),
};هذا يعني أن hook الوحيد المدعوم هو shouldEmit، وهو من نوع SyncBailHook، وأن parameter الوحيد الذي سيُمرر لأي plugin يدخل إلى shouldEmit هو compilation.
أنواع hooks المدعومة متعددة:
Hooks متزامنة
-
SyncHook
- تُعرّف بالشكل
new SyncHook([params]). - يتم الدخول إليها باستخدام
tap. - تُستدعى باستخدام
call(...params).
- تُعرّف بالشكل
-
Bail Hooks
- تُعرّف باستخدام
SyncBailHook[params]. - يتم الدخول إليها باستخدام
tap. - تُستدعى باستخدام
call(...params).
في هذا النوع من hooks، تُستدعى callbacks الخاصة بكل plugin واحدًا تلو الآخر مع
argsالمحددة. إذا أرجع أي plugin أي قيمة غيرundefined، فترجع hook هذه القيمة ولا يتم استدعاء callbacks اللاحقة. أحداث مفيدة كثيرة مثلoptimizeChunksوoptimizeChunkModulesهي SyncBailHooks. - تُعرّف باستخدام
-
Waterfall Hooks
- تُعرّف باستخدام
SyncWaterfallHook[params]. - يتم الدخول إليها باستخدام
tap. - تُستدعى باستخدام
call(...params).
هنا يُستدعى كل plugin واحدًا تلو الآخر، وتأتي arguments من القيمة التي أرجعها plugin السابق. يجب أن يأخذ plugin ترتيب تنفيذه بالحسبان، وأن يقبل arguments القادمة من plugin السابق. قيمة أول plugin هي
init. لذلك يجب تمرير parameter واحد على الأقل لـ waterfall hooks. يُستخدم هذا النمط في instances الخاصة بـ Tapable المرتبطة بقوالب webpack مثلModuleTemplateوChunkTemplateوغيرها. - تُعرّف باستخدام
Hooks غير متزامنة
-
Async Series Hook
- تُعرّف باستخدام
AsyncSeriesHook[params]. - يتم الدخول إليها باستخدام
tapأوtapAsyncأوtapPromise. - تُستدعى باستخدام
callAsync(...params).
تُستدعى دوال handlers الخاصة بـ plugin مع كل arguments ومع callback بالشكل
(err?: Error) -> void. تُستدعى دوال handlers حسب ترتيب تسجيلها. ويتم استدعاءcallbackبعد استدعاء كل handlers. هذا نمط شائع أيضًا لأحداث مثلemitوrun. - تُعرّف باستخدام
-
Async waterfall يتم تطبيق plugins بشكل غير متزامن بطريقة waterfall.
- تُعرّف باستخدام
AsyncWaterfallHook[params]. - يتم الدخول إليها باستخدام
tapأوtapAsyncأوtapPromise. - تُستدعى باستخدام
callAsync(...params).
تُستدعى دوال handlers الخاصة بـ plugin مع القيمة الحالية ومع callback بالشكل
(err: Error, nextValue: any) -> void.عند الاستدعاء، تكونnextValueهي القيمة الحالية للـ handler التالي. والقيمة الحالية لأول handler هيinit. بعد تطبيق كل handlers، تُستدعى callback بالقيمة الأخيرة. إذا مرر أي handler قيمة لـerr، تُستدعى callback بهذا الخطأ ولا يتم استدعاء بقية handlers. يُتوقع هذا النمط لأحداث مثلbefore-resolveوafter-resolve. - تُعرّف باستخدام
-
Async Series Bail
- تُعرّف باستخدام
AsyncSeriesBailHook[params]. - يتم الدخول إليها باستخدام
tapأوtapAsyncأوtapPromise. - تُستدعى باستخدام
callAsync(...params).
- تُعرّف باستخدام
-
Async Parallel
- تُعرّف باستخدام
AsyncParallelHook[params]. - يتم الدخول إليها باستخدام
tapأوtapAsyncأوtapPromise. - تُستدعى باستخدام
callAsync(...params).
- تُعرّف باستخدام
القيم الافتراضية للإعدادات
يطبق webpack القيم الافتراضية للإعدادات بعد تطبيق القيم الافتراضية الخاصة بـ plugins. هذا يسمح للـ plugins بتوفير defaults خاصة بها، ويوفر طريقة لإنشاء configuration preset plugins.
مثال: AssetLoggerPlugin
يوضح المثال التالي plugin بسيطًا في webpack يسجل كل assets المولدة باستخدام واجهة logger في webpack.
class AssetLoggerPlugin {
apply(compiler) {
compiler.hooks.thisCompilation.tap("AssetLoggerPlugin", (compilation) => {
const logger = compilation.getLogger("AssetLoggerPlugin");
compilation.hooks.processAssets.tap(
{
name: "AssetLoggerPlugin",
stage: compilation.constructor.PROCESS_ASSETS_STAGE_SUMMARIZE,
},
(assets) => {
logger.info("Generated assets:");
for (const assetName of Object.keys(assets)) {
logger.info(assetName);
}
},
);
});
}
}
export default AssetLoggerPlugin;يدخل هذا plugin إلى hook المسمى emit في webpack compiler ويطبع أسماء كل assets المولدة في console. يوضح كيف يمكن لـ plugins التفاعل مع عملية compilation باستخدام webpack hooks.
مثلًا، عند تشغيل webpack build، قد يبدو الناتج بهذا الشكل:
Generated assets:
main.js
vendor.js
styles.css


