منصة الويب الحديثة
يصف هذا الدليل أنماطًا عملية في webpack لكل من مكونات الويب، وخرائط الاستيراد، وتطبيقات الويب التقدمية (PWA) مع Service Workers. يوضح كل قسم المشكلة، ويعرض إعدادًا بسيطًا يمكنك نسخه، ثم يذكر الحدود الحالية مقارنة بالتحسينات المستقبلية في webpack.
مكونات الويب مع webpack
المشكلة
إذا نفذت أكثر من حزمة JavaScript واحدة customElements.define() لاسم الوسم نفسه، فسيطرح المتصفح DOMException: Failed to execute 'define' on 'CustomElementRegistry'. يحدث ذلك غالبًا عندما تتكرر الوحدة التي تسجل العنصر: فقد تحتوي نقاط إدخال منفصلة أو أجزاء غير متزامنة على نسخة من كود التسجيل، فتشغّل حزمتان define للوسم نفسه.
النهج
استخدم optimization.splitChunks بحيث تعيش الوحدة التي تعرّف العنصر داخل جزء مشترك واحد يُحمّل مرة واحدة. عدّل cacheGroups بحيث تُفرض تعريفات العناصر لديك، أو مجلد مخصص مثل src/elements/، داخل جزء واحد. راجع منع التكرار للفكرة العامة.
webpack.config.js
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default {
entry: {
main: "./src/main.js",
admin: "./src/admin.js",
},
output: {
filename: "[name].js",
path: path.resolve(__dirname, "dist"),
clean: true,
},
optimization: {
splitChunks: {
chunks: "all",
cacheGroups: {
// ضع وحدات العناصر المخصصة المشتركة في جزء غير متزامن واحد.
customElements: {
test: /[\\/]src[\\/]elements[\\/]/,
name: "custom-elements",
chunks: "all",
enforce: true,
},
},
},
},
};تأكد من أن نقطتي الإدخال تستوردان وحدة التسجيل نفسها، مثل ./elements/my-element.js، حتى يتمكن webpack من إصدار جزء واحد باسم custom-elements.js بدلًا من تضمين تسجيل مكرر داخل main وadmin.
القيود والعمل المستقبلي
لا يغيّر التقسيم وحده قواعد المتصفح: يجب أن يُسجل اسم الوسم مرة واحدة بالضبط لكل مستند. لا يوفر webpack حتى الآن بدائية مدمجة من الدرجة الأولى بمعنى «سجّل هذا العنصر المخصص مرة واحدة» خارج التحكم في مخطط الأجزاء. دعم إزالة تكرار تسجيل العناصر المخصصة عبر البناء مخطط له؛ وحتى ذلك الحين، اعتمد على الأجزاء المشتركة ووحدة تسجيل واحدة.
خرائط الاستيراد مع webpack
المشكلة
تتيح خرائط الاستيراد للمتصفح حل المحددات العارية (import "lodash-es" من importmap.json أو من <script type="importmap"> مضمن). إذا كان webpack يجمع تلك التبعيات، فلن تحتاج إلى خريطة استيراد لها. أما إذا أردت أن يحمّل المتصفح تبعية من عنوان URL، مثل CDN أو /vendor/، مع إبقاء كود التطبيق على الاستيرادات العارية، فميّز تلك الوحدات كـ externals حتى يصدر webpack تعليمات import تطابق خريطتك.
النهج
فعّل إخراج وحدات ES عبر experiments.outputModule وoutput.module، واضبط externalsType: "module" للاستيرادات الساكنة، ثم أدرج كل محدد عارٍ في externals بالسلسلة نفسها التي سيحلها المتصفح عبر خريطة الاستيراد.
webpack.config.js
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default {
mode: "production",
experiments: {
outputModule: true,
},
entry: "./src/index.js",
externalsType: "module",
externals: {
"lodash-es": "lodash-es",
},
output: {
module: true,
filename: "[name].mjs",
path: path.resolve(__dirname, "dist"),
clean: true,
},
};importmap.json (يُقدّم بجانب ملف HTML؛ يجب أن تطابق عناوين URL طريقة النشر لديك)
ملف vendor محلي:
{
"imports": {
"lodash-es": "/vendor/lodash-es.js"
}
}CDN (لا حاجة إلى الاستضافة الذاتية):
{
"imports": {
"lodash-es": "https://cdn.jsdelivr.net/npm/lodash-es@4/+esm"
}
}يجب أن يطابق المفتاح "lodash-es" كلًا من مفتاح externals والمحدد في المصدر لديك (import … from "lodash-es"). أما القيمة فهي عنوان URL الذي يحمّله المتصفح، إما مسارًا محليًا أو عنوان CDN؛ ولا يتحقق webpack من ذلك الملف.
index.html (الترتيب مهم: خريطة الاستيراد قبل الحزمة)
<script type="importmap" src="/importmap.json"></script>
<script type="module" src="/dist/main.mjs"></script>القيود والعمل المستقبلي
لا يصدر webpack ملف importmap.json ولا يحدّثه نيابة عنك. يجب أن تحافظ على الخريطة بحيث تبقى المحددات وعناوين URL متوافقة مع externals وبنية الخادم لديك. إنشاء خرائط الاستيراد تلقائيًا غير متاح في webpack 5 اليوم؛ وقد تقلل الأدوات المستقبلية هذه الخطوة اليدوية.
تطبيقات الويب التقدمية (PWA) وService Workers
المشكلة
يتطلب التخزين المؤقت طويل الأمد عناوين URL ثابتة لملفات HTML، لكن عناوين URL ذات إصدارات للسكربتات والأنماط. يؤدي استخدام [contenthash] في output.filename إلى تغيير تلك العناوين في كل بناء. يجب أن تسرد قائمة التخزين المسبق في service worker عناوين URL الدقيقة بعد كل بناء، وإلا ستشير واجهات العمل دون اتصال إلى ملفات مفقودة.
ينشئ ملحق workbox-webpack-plugin GenerateSW ملف service worker كاملًا نيابة عنك. هذا ملائم، لكن عندما تحتاج إلى تحكم كامل في كود service worker، مثل التوجيه المخصص، أو سلوك skipWaiting، أو التنسيق مع [contenthash] وملحقات أخرى، يكون InjectManifest مناسبًا: تكتب العامل بنفسك، ويحقن Workbox بيان التخزين المسبق وقت البناء من قائمة أصول webpack.
النهج
استخدم [contenthash] للأصول الصادرة، وأضف InjectManifest من workbox-webpack-plugin. يستورد قالب المصدر workbox-precaching ويستدعي precacheAndRoute(self.__WB_MANIFEST)؛ يستبدل الملحق self.__WB_MANIFEST بقائمة أصول webpack، بما في ذلك أسماء الملفات ذات الهاش.
التثبيت:
npm install workbox-webpack-plugin workbox-precaching --save-devwebpack.config.js
import path from "node:path";
import { fileURLToPath } from "node:url";
import HtmlWebpackPlugin from "html-webpack-plugin";
import { InjectManifest } from "workbox-webpack-plugin";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default {
entry: "./src/index.js",
output: {
filename: "[name].[contenthash].js",
path: path.resolve(__dirname, "dist"),
clean: true,
},
plugins: [
new HtmlWebpackPlugin({ title: "PWA + content hashes" }),
new InjectManifest({
swSrc: path.resolve(__dirname, "src/service-worker.js"),
swDest: "service-worker.js",
}),
],
};src/service-worker.js (قالب التخزين المسبق)
import { precacheAndRoute } from "workbox-precaching";
// يُستبدل وقت البناء ببيان التخزين المسبق الخاص بـ webpack (عناوين أصول ذات هاش).
precacheAndRoute(globalThis.__WB_MANIFEST);سجّل ملف service-worker.js الصادر من تطبيقك، مثلًا في src/index.js، باستخدام navigator.serviceWorker.register("/service-worker.js")، مع تقديمه من dist/ بالنطاق الصحيح.
القيود والعمل المستقبلي
يجب أن تبقي InjectManifest متزامنًا مع أسماء ملفات الإخراج والملحقات لديك؛ ويظل GenerateSW الطريق الأبسط عندما لا تحتاج إلى عامل مخصص. لا يشحن webpack مولّد تخزين مسبق مدمجًا لـ service worker؛ وقد يصل تكامل أوثق مع الأصول ذات الهاش في إصدارات مستقبلية. وحتى ذلك الحين، يُعد InjectManifest من Workbox طريقة مدعومة جيدًا لمواءمة خرج [contenthash] مع التخزين المسبق.



