إضافات المتصفح بالطريقة الحديثة. إنّ WXT بالنسبة لإضافات المتصفح هو ما يمثّله Nuxt لـVue وNext.js لـReact — إطار عمل متكامل يتولّى ملف الـmanifest، والتجميع، وإعادة التحميل الفوري، والنشر عبر المتصفحات، حتى تركّز على الميزات بدلًا من إعدادات البناء.
ما الذي ستتعلّمه
في هذا الدرس، سوف:
- تنشئ مشروع WXT باستخدام TypeScript وReact
- تفهم نقاط الدخول القائمة على الملفات (النافذة المنبثقة، الخلفية، سكربت المحتوى)
- تبني واجهة نافذة منبثقة وسكربت محتوى يحقن React في أي صفحة
- تحفظ البيانات عبر واجهة التخزين الآمنة الأنواع في WXT
- ترسل الرسائل بين النافذة المنبثقة والخلفية وسكربت المحتوى
- تبني وتحزّم الإضافة لمتصفحي Chrome وFirefox
في النهاية ستحصل على إضافة عاملة باسم "مسطرة القراءة" تعرض شريط تركيز فوق أي صفحة ويب، مع زر تبديل ومنتقي ألوان في النافذة المنبثقة — جميعها تتشارك الحالة عبر التخزين.
المتطلبات المسبقة
قبل البدء، تأكّد من توفّر:
- Node.js 20+ وnpm (أو pnpm/bun) مثبّتة
- معرفة أساسية بـTypeScript وReact
- متصفح Google Chrome أو Firefox للاختبار
- محرّر أكواد (يُفضّل VS Code)
لست بحاجة لمعرفة واجهات Manifest V3 الخام — يولّد WXT ملف الـmanifest نيابةً عنك ويوفّر كائن browser موحّدًا يعمل عبر جميع المتصفحات.
لماذا WXT بدلًا من Manifest V3 الخام؟
كتابة إضافة متصفح يدويًا تعني تحرير manifest.json بنفسك، وإعداد أداة تجميع، ونسخ الأصول، وإعادة تحميل الإضافة عند كل تغيير، وصيانة بناءات منفصلة لـChrome (MV3) وFirefox (MV2/MV3). يزيل WXT كل هذا العناء.
| المسألة | MV3 الخام | WXT |
|---|---|---|
| الـManifest | JSON مكتوب يدويًا | مُولّد من نقاط الدخول + الإعدادات |
| أداة التجميع | يدوية (webpack/rollup) | Vite مُهيّأ مسبقًا |
| إعادة التحميل الفوري | يدوية للإضافة | HMR للواجهة وإعادة تحميل تلقائية للسكربتات |
| تعدّد المتصفحات | إعدادات منفصلة | راية wxt -b firefox |
| واجهة المتصفح | chrome.* مقابل browser.* | كائن browser موحّد |
| النشر | تحزيم ورفع يدوي | wxt zip وwxt submit |
إنّ WXT محايد تجاه أطر العمل — يعمل مع TypeScript الخام وReact وVue وSvelte وSolid عبر وحدات رسمية.
الخطوة 1: إنشاء المشروع
أنشئ مشروع WXT جديدًا باستخدام أداة البدء التفاعلية. عند السؤال، اختر قالب React وnpm كمدير حزم.
npx wxt@latest init reading-ruler
cd reading-ruler
npm installيولّد هذا بنية مشروع نظيفة:
reading-ruler/
├── entrypoints/
│ ├── background.ts
│ ├── content.ts
│ └── popup/
│ ├── App.tsx
│ ├── index.html
│ └── main.tsx
├── public/
│ └── icon/
├── wxt.config.ts
├── package.json
└── tsconfig.json
الفكرة الأساسية: كل ملف في entrypoints/ يصبح جزءًا من إضافتك. يفحص WXT هذه الملفات وقت البناء ويولّد الـmanifest الصحيح تلقائيًا. لا حاجة للتسجيل اليدوي.
يحتوي ملف package.json لديك مسبقًا على السكربتات التي تحتاجها:
{
"scripts": {
"dev": "wxt",
"dev:firefox": "wxt -b firefox",
"build": "wxt build",
"build:firefox": "wxt build -b firefox",
"zip": "wxt zip",
"zip:firefox": "wxt zip -b firefox",
"postinstall": "wxt prepare"
}
}شغّل خادم التطوير الآن — سيُطلق WXT نسخة متصفح جديدة مع إضافتك مثبّتة مسبقًا:
npm run devاترك هذا قيد التشغيل. من الآن فصاعدًا، حفظ أي ملف يعيد تحميل الجزء المعني من إضافتك فوريًا.
الخطوة 2: ضبط الـManifest
افتح wxt.config.ts. هنا تعلن الأذونات وأي حقول manifest لا يستطيع WXT استنتاجها من كودك. تحتاج إضافتنا إذن storage لتذكّر إعدادات المستخدم.
// wxt.config.ts
import { defineConfig } from 'wxt';
export default defineConfig({
modules: ['@wxt-dev/module-react'],
manifest: {
name: 'Reading Ruler',
description: 'Overlay a focus bar on any web page to aid reading.',
permissions: ['storage'],
},
});لا تضبط أبدًا manifest_version أو background أو content_scripts أو action هنا — يشتقّ WXT هذه من ملفات نقاط الدخول. أنت تعلن فقط الأمور الشاملة مثل permissions وname وhost_permissions.
نصيحة: يستورد WXT تلقائيًا مساعديه الأساسيين — defineBackground وdefineContentScript وstorage والكائن browser متاحة دون استيراد. يشغّل سكربت postinstall الأمر wxt prepare الذي يولّد أنواع TypeScript التي تجعل هذا ممكنًا. إذا اشتكى محرّرك من أنواع مفقودة، شغّل npm run postinstall.
الخطوة 3: تعريف حالة مشتركة آمنة الأنواع
تحتاج كل من النافذة المنبثقة وسكربت المحتوى إلى قراءة وكتابة الإعدادات نفسها. أنشئ عنصر تخزين محدّد النوع بحيث يُعرّف المفتاح ونوع القيمة في مكان واحد بالضبط.
أنشئ utils/settings.ts:
// utils/settings.ts
export interface RulerSettings {
enabled: boolean;
color: string;
height: number;
}
export const rulerSettings = storage.defineItem<RulerSettings>(
'sync:rulerSettings',
{
fallback: {
enabled: false,
color: '#7c3aed',
height: 28,
},
},
);نقاط مهمة:
- المفتاح مسبوق بـ
sync:— يدعم WXT المناطقlocal:وsync:وsession:وmanaged:. استخدامsync:يعني أنّ الإعدادات تتبع المستخدم عبر المتصفحات المسجّل دخوله إليها. - تُعيد
getValue()قيمةfallbackعندما لا يكون هناك شيء مخزّن بعد، فلا يتعامل المستدعون أبدًا معundefined. - تُعيد
defineItemالدوالgetValueوsetValueوremoveValueوwatch— اشتراك تفاعلي يُطلَق كلما تغيّرت القيمة في أي مكان.
هذا المصدر الوحيد للحقيقة هو العمود الفقري للإضافة بأكملها.
الخطوة 4: بناء سكربت المحتوى
يعمل سكربت المحتوى داخل كل صفحة ويب. سيعرض شريط تركيز قابلًا للتحريك باستخدام React، معزولًا داخل shadow root حتى لا تتداخل أنماط CSS الخاصة بالصفحة المضيفة مع أنماطنا.
استبدل entrypoints/content.ts بـentrypoints/content/index.tsx:
// entrypoints/content/index.tsx
import ReactDOM from 'react-dom/client';
import { rulerSettings } from '@/utils/settings';
import Ruler from './Ruler';
export default defineContentScript({
matches: ['<all_urls>'],
cssInjectionMode: 'ui',
async main(ctx) {
const ui = await createShadowRootUi(ctx, {
name: 'reading-ruler-ui',
position: 'overlay',
anchor: 'body',
onMount: (container) => {
const root = ReactDOM.createRoot(container);
root.render(<Ruler />);
return root;
},
onRemove: (root) => root?.unmount(),
});
ui.mount();
// إزالة الواجهة تلقائيًا إذا عطّل المستخدم المسطرة.
rulerSettings.watch((settings) => {
if (!settings.enabled) ui.remove();
});
},
});النقاط الأساسية:
- يحقن
matches: ['<all_urls>']السكربت في كل صفحة. ضيّق هذا إلى نطاقات محدّدة في الإنتاج. - يخبر
cssInjectionMode: 'ui'الـWXT بحقن CSS داخل الـshadow root بدلًا من الصفحة، مما يضمن العزل. - يركّب
createShadowRootUiشجرة React داخل shadow DOM. لا تستطيع الصفحة المضيفة رؤية عناصرك ولا تنسيقها. - إنّ
ctxهوContentScriptContext. يستخدمه WXT لتنظيف واجهتك تلقائيًا عند انتقال المستخدم في مواقع تطبيقات الصفحة الواحدة.
الآن أنشئ مكوّن Ruler الذي يقرأ الإعدادات ويتبع الفأرة:
// entrypoints/content/Ruler.tsx
import { useEffect, useState } from 'react';
import { rulerSettings, type RulerSettings } from '@/utils/settings';
export default function Ruler() {
const [settings, setSettings] = useState<RulerSettings | null>(null);
const [top, setTop] = useState(0);
useEffect(() => {
rulerSettings.getValue().then(setSettings);
const unwatch = rulerSettings.watch(setSettings);
return unwatch;
}, []);
useEffect(() => {
const onMove = (e: MouseEvent) => setTop(e.clientY);
window.addEventListener('mousemove', onMove);
return () => window.removeEventListener('mousemove', onMove);
}, []);
if (!settings?.enabled) return null;
return (
<div
style={{
position: 'fixed',
left: 0,
right: 0,
top: top - settings.height / 2,
height: settings.height,
background: settings.color,
opacity: 0.25,
pointerEvents: 'none',
zIndex: 2147483647,
transition: 'top 60ms linear',
}}
/>
);
}ولأنّ المكوّن يشترك في rulerSettings.watch، فإنّ تغيير اللون أو الارتفاع في النافذة المنبثقة يحدّث الشريط مباشرةً على الصفحة — دون إعادة تحميل الصفحة ودون تمرير رسائل يدوي للحالة.
الخطوة 5: بناء واجهة النافذة المنبثقة
النافذة المنبثقة هي ما يظهر عند نقر المستخدم على أيقونة شريط الأدوات. إنها تطبيق HTML + React عادي يقع تحت entrypoints/popup/. استبدل entrypoints/popup/App.tsx:
// entrypoints/popup/App.tsx
import { useEffect, useState } from 'react';
import { rulerSettings, type RulerSettings } from '@/utils/settings';
export default function App() {
const [settings, setSettings] = useState<RulerSettings | null>(null);
useEffect(() => {
rulerSettings.getValue().then(setSettings);
}, []);
async function update(patch: Partial<RulerSettings>) {
if (!settings) return;
const next = { ...settings, ...patch };
setSettings(next);
await rulerSettings.setValue(next);
}
if (!settings) return <p>Loading…</p>;
return (
<main style={{ width: 240, padding: 16, fontFamily: 'system-ui' }}>
<h1 style={{ fontSize: 16, marginBottom: 12 }}>Reading Ruler</h1>
<label style={{ display: 'flex', justifyContent: 'space-between' }}>
Enabled
<input
type="checkbox"
checked={settings.enabled}
onChange={(e) => update({ enabled: e.target.checked })}
/>
</label>
<label style={{ display: 'flex', justifyContent: 'space-between', marginTop: 10 }}>
Color
<input
type="color"
value={settings.color}
onChange={(e) => update({ color: e.target.value })}
/>
</label>
<label style={{ display: 'block', marginTop: 10 }}>
Height: {settings.height}px
<input
type="range"
min={8}
max={80}
value={settings.height}
onChange={(e) => update({ height: Number(e.target.value) })}
style={{ width: '100%' }}
/>
</label>
</main>
);
}لاحظ مدى قلّة الأسلاك المطلوبة. تكتب النافذة المنبثقة إلى rulerSettings، فيُطلَق رد نداء watch في سكربت المحتوى، ويُحدَّث الشريط فوريًا. التخزين هو طبقة مزامنة الحالة لديك — نادرًا ما تحتاج تمرير رسائل صريحًا للبيانات المشتركة.
الخطوة 6: إضافة سكربت خلفية
سكربت الخلفية (service worker في MV3) هو منسّق الإضافة المتاح دائمًا. سنستخدمه لتبديل المسطرة عبر أمر لوحة مفاتيح ولضبط قيم افتراضية معقولة عند التثبيت.
استبدل entrypoints/background.ts:
// entrypoints/background.ts
import { rulerSettings } from '@/utils/settings';
export default defineBackground(() => {
// تبديل المسطرة عند ضغط المستخدم على الاختصار المُعدّ.
browser.commands.onCommand.addListener(async (command) => {
if (command !== 'toggle-ruler') return;
const current = await rulerSettings.getValue();
await rulerSettings.setValue({ ...current, enabled: !current.enabled });
});
browser.runtime.onInstalled.addListener((details) => {
if (details.reason === 'install') {
console.log('Reading Ruler installed.');
}
});
});إنّ دالة main في defineBackground لا يمكن أن تكون async — سجّل مستمعيك بشكل متزامن، ثم نفّذ العمل غير المتزامن داخلهم. يضمن هذا أن يلتقط الـservice worker الأحداث التي تُطلَق فور استيقاظه.
لتسجيل اختصار لوحة المفاتيح، أضف كتلة commands إلى الـmanifest:
// wxt.config.ts
import { defineConfig } from 'wxt';
export default defineConfig({
modules: ['@wxt-dev/module-react'],
manifest: {
name: 'Reading Ruler',
description: 'Overlay a focus bar on any web page to aid reading.',
permissions: ['storage'],
commands: {
'toggle-ruler': {
suggested_key: { default: 'Alt+R' },
description: 'Toggle the reading ruler',
},
},
},
});لماذا استخدام الكائن browser بدلًا من chrome؟ يأتي WXT مع كائن browser موحّد مبنيّ على WebExtension polyfill. يعمل الكود نفسه دون تغيير على Chrome وFirefox وEdge وSafari — دون فروع تكشّف الميزات ودون تباعد بين chrome وbrowser.
الخطوة 7: المراسلة بين السياقات
يغطّي التخزين الحالة المشتركة، لكنك أحيانًا تحتاج إلى طلب/استجابة صريح — مثلًا، سؤال التبويب النشط "ما زمن قراءتك؟". يوصي WXT بـ@webext-core/messaging لطبقة آمنة الأنواع فوق browser.runtime.sendMessage.
npm install @webext-core/messagingعرّف بروتوكولك مرة واحدة:
// utils/messaging.ts
import { defineExtensionMessaging } from '@webext-core/messaging';
interface ProtocolMap {
getWordCount(): number;
}
export const { sendMessage, onMessage } =
defineExtensionMessaging<ProtocolMap>();عالجه في دالة main لسكربت المحتوى:
import { onMessage } from '@/utils/messaging';
onMessage('getWordCount', () => {
return document.body.innerText.trim().split(/\s+/).length;
});واستدعِه من النافذة المنبثقة أو الخلفية مع استنتاج كامل للأنواع:
import { sendMessage } from '@/utils/messaging';
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
const count = await sendMessage('getWordCount', undefined, tab.id);
console.log(`This page has ${count} words.`);يُستنتَج نوع إرجاع sendMessage من ProtocolMap، لذا فإنّ خطأ مطبعيًا في اسم الرسالة أو نوع وسيطة خاطئًا يصبح خطأ تصريف، لا مفاجأة وقت التشغيل.
الخطوة 8: الاختبار في المتصفح
مع تشغيل npm run dev، يكون متصفح WXT التطويري قد حمّل إضافتك مسبقًا. جرّب المسار الكامل:
- انقر أيقونة Reading Ruler في شريط الأدوات لفتح النافذة المنبثقة.
- بدّل Enabled — يظهر شريط ملوّن ويتبع الفأرة على الصفحة.
- غيّر اللون والارتفاع — يُحدَّث الشريط مباشرةً دون إعادة تحميل.
- اضغط Alt+R — يبدّل أمر الخلفية المسطرة بين التشغيل والإيقاف.
- افتح تبويبًا ثانيًا — ولأنّ التخزين
sync:، تنتقل إعداداتك معك.
إذا غيّرت أي ملف مصدري، يعيد WXT التحميل فوريًا: تحديثات النافذة المنبثقة تُطبَّق آنيًا، وتعديلات سكربت المحتوى/الخلفية تُطلِق إعادة تحميل تلقائية للإضافة.
لاختبار Firefox بالتوازي:
npm run dev:firefoxيُطلِق WXT نسخة Firefox مع manifest بنكهة Firefox — دون أي تغييرات في الإعدادات.
الخطوة 9: البناء والتحزيم للنشر
عندما تكون جاهزًا للإطلاق، أنتج بناءات إنتاج محسّنة وملفات ZIP جاهزة للمتجر:
# بناء الإنتاج (الناتج في .output/chrome-mv3/)
npm run build
# تحزيم لمتجر Chrome
npm run zip
# بناء وتحزيم Firefox
npm run zip:firefoxينشئ wxt zip أرشيفًا جاهزًا للرفع في .output/. بالنسبة لـFirefox، يولّد WXT أيضًا ملف sources.zip يحتوي على شيفرتك المصدرية، وهو ما تتطلّبه Mozilla للمراجعة.
يستطيع WXT حتى أتمتة خطوة الرفع. اضبط بيانات اعتماد المتجر وشغّل wxt submit للنشر إلى متجر Chrome وإضافات Firefox وإضافات Edge بأمر واحد — مثالي لخطوط أنابيب CI.
اختبار التنفيذ
تحقّق من الإضافة من البداية إلى النهاية:
- صحة الـManifest: افتح
.output/chrome-mv3/manifest.jsonوتأكّد من أنّpermissionsوcommandsومدخلاتcontent_scripts/backgroundالمُولّدة تطابق كودك. - العزل: حمّل المسطرة على موقع كثيف الـCSS (مثل صفحة أخبار) وتأكّد من أنّ أنماط المضيف لا تتسرّب إلى واجهة shadow root لديك والعكس.
- الثبات: بدّل الإعدادات، أغلق المتصفح وأعد فتحه، وتأكّد من بقائها.
- تعدّد المتصفحات: شغّل
npm run dev:firefoxوكرّر اختبار التدخين.
استكشاف الأخطاء وإصلاحها
browser is not defined أو أنواع استيراد تلقائي مفقودة. شغّل npm run postinstall (الذي يستدعي wxt prepare) لإعادة توليد أنواع .wxt/، ثم أعد تشغيل خادم TypeScript في محرّرك.
أنماط سكربت المحتوى تتسرّب إلى الصفحة. تأكّد من ضبط cssInjectionMode: 'ui' والتركيب عبر createShadowRootUi، لا عبر document.body.append العادي.
يبدو الـservice worker "ميتًا". ينام عمّال خدمة MV3 عند الخمول. سجّل جميع مستمعي الأحداث بشكل متزامن في أعلى defineBackground — المستمعون المُضافون داخل await قد يفوّتون الأحداث المبكرة.
قيمة التخزين undefined. قدّم دائمًا قيمة fallback في defineItem، وتأكّد من أنّ بادئة المنطقة (local: وsync: وsession:) تطابق إذن storage الذي أعلنته.
فشل بناء Firefox عند الإرسال. يتطلّب Firefox أرشيف مصدر؛ يولّد wxt zip -b firefox ذلك تلقائيًا — ارفع كلًا من ZIP البناء وsources.zip.
الخطوات التالية
- أضف صفحة خيارات: أنشئ
entrypoints/options/لشاشة إعدادات كاملة، منفصلة عن النافذة المنبثقة المدمجة. - جرّب أطر عمل أخرى: استبدل
@wxt-dev/module-reactبـ@wxt-dev/module-vueأو@wxt-dev/module-svelte— تبقى نقاط الدخول متطابقة. - استكشف وحدات WXT: تزيل وحدتا auto-icons وi18n مزيدًا من الكود المكرّر.
- اربط CI: ادمج
wxt zipوwxt submitفي سير عمل GitHub Actions لإصدارات بنقرة واحدة.
للقراءة ذات الصلة، اطّلع على دروسنا حول بناء إضافة Chrome باستخدام Manifest V3 وVite 6 مع React وTypeScript.
الخاتمة
يحوّل WXT تطوير إضافات المتصفح من مهمّة يدوية معرّضة للأخطاء إلى سير عمل حديث مدعوم بـVite. أنشأت مشروعًا، وعرّفت نقاط دخول قائمة على الملفات، وعزلت واجهة React داخل shadow root، وزامنت الحالة عبر تخزين آمن الأنواع، ونسّقت المنطق في service worker خلفي، وحزّمت النتيجة لمتصفحات متعدّدة — كل ذلك دون كتابة سطر واحد من manifest.json يدويًا.
تتوسّع الأنماط نفسها من تجربة في عطلة نهاية الأسبوع إلى إضافة منشورة متعدّدة المتاجر. بفضل واجهات المتصفح الموحّدة وإعادة التحميل الفوري والنشر بأمر واحد، يتيح لك WXT قضاء وقتك على ما يراه المستخدمون فعليًا بدلًا من محاربة المنصّة.