كيفية أتمتة إعادة التوجيه الديناميكي من الويب إلى التطبيق عبر المتصفحات

opoinstall
2026-10-02
5 min read

ما هي إعادة التوجيه من الويب إلى التطبيق وكيف يتم تنفيذها؟ تقوم إعادة التوجيه من الويب إلى التطبيق (Web to App) بتوجيه زوار موقع الويب عبر الهاتف إلى التطبيقات الأصلية باستخدام Universal Links أو App Links أو مسارات بديلة، مع تنفيذ جافا سكريبت مدفوع بمستخدم لتشغيل التطبيقات المثبتة أو توجيه المستخدمين الذين لا يملكون التطبيق إلى متاجر التطبيقات.

إعادة التوجيه من الويب إلى التطبيق هي آلية توجيه آلية تنقل زوار الويب عبر الهاتف من صفحات الهبوط إلى واجهات محددة داخل التطبيق الأصلي. ومن خلال تقييم منصات الأجهزة، وقدرات المتصفح، ومقاييس نجاح الإطلاق، تقوم أطر عمل إعادة التوجيه الديناميكي بتنفيذ فتح عميق (Deep-link) للمستخدمين الحاليين، مع توجيه المستخدمين غير المثبتين للتطبيق إلى متاجر التطبيقات مع تمرير معلمات سياقية إذا كان نظام الإحالة المطبق يدعم ذلك.

المصطلح التعريف الكيان ذو الصلة دور نية البحث
من الويب إلى التطبيق العملية الهندسية لتوجيه زوار متصفح الويب إلى تطبيقات الهاتف الأصلية. الفتح العميق للجوال (Mobile Deep Linking) معلوماتي / تجاري
Apple Smart App Banner لافتة ترويجية أصلية في Safari يتم تكوينها عبر وسم apple-itunes-app meta tag. تصفح الويب في Safari معلوماتي
Custom Web-to-App Banner مكون HTML وجافا سكريبت عبر المتصفحات يعرض دعوات لاتخاذ إجراء (CTAs) ديناميكية لتشغيل التطبيق أو تحميله. إعادة التوجيه من الويب إلى التطبيق معلوماتي
Custom URL Scheme بروتوكول URI خاص يسمح لصفحات الويب الخارجية بتشغيل التطبيقات الأصلية. توجيه الفتح العميق تقني / معلوماتي

يحدد التوجيه المدفوع بالمستخدم التطبيق الأصلي أو بديل المتصفح حسب القدرة.

لماذا تعد إعادة التوجيه من الويب إلى التطبيق حاسمة لتحويل قمع الجوال؟

فجوة التحويل: مقارنة معدلات ارتداد ويب الجوال مقابل القيمة الدائمة (LTV) للتطبيق الأصلي

تمثل مواقع الويب عبر الجوال قناة استحواذ واسعة، حيث تجذب حركة المرور من البحث المدفوع، واكتشافات وسائل التواصل الاجتماعي، وإعلانات العرض، والإحالات العضوية. ومع ذلك، يظهر زوار الويب عبر الجوال عادةً معدلات تحويل أقل، وجلسات أقصر، ومعدلات ارتداد أعلى مقارنة بمستخدمي التطبيقات الأصلية. في المقابل، توفر تطبيقات الهاتف الأصلية واجهات مستخدم محسنة، واستمرارية للجلسة محلياً، وعرضاً مسرعاً بالأجهزة، ودعماً للإشعارات الفورية، مما يدعم مشاركة أعلى على المدى الطويل وقيمة دائمة (LTV) أكبر.

الهدف التشغيلي الأساسي لإعادة التوجيه من الويب إلى التطبيق هو سد هذه الفجوة التجارية. فمن خلال نقل زوار ويب الجوال مباشرة إلى تجارب التطبيق الأصلي، تستفيد فرق النمو من نية شراء المستخدم قبل أن يتسبب الاحتكاك في التخلي عن الجلسة. عندما يستكشف زائر للجوال عرضاً ترويجياً على متصفح الهاتف، فإن توجيه ذلك المستخدم مباشرة إلى صفحة المنتج المقابلة داخل التطبيق يحافظ على زخم النية العالية ويقضي على خطوات المصادقة أو التنقل غير الضرورية.

المعضلة التقنية: لماذا تفشل الروابط الثابتة في تقديم رحلات مستخدم موحدة

يؤدي الاعتماد على الروابط التشعبية الثابتة الأساسية إلى احتكاك منهجي عبر أنظمة تشغيل الهواتف الحديثة. الرابط الثابت الذي يشير حصرياً إلى متجر تطبيقات أو متجر Google Play يقدم احتكاكاً فورياً للمستخدمين الموجودين بالفعل لديهم التطبيق؛ حيث يؤدي النقر على الرابط إلى إطلاق قائمة المتجر بدلاً من فتح التطبيق المثبت. وعلى العكس من ذلك، فإن تكوين مخطط URI مخصص ثابت (مثل myapp://promo) يعمل فقط إذا كان التطبيق مثبتاً حالياً؛ فإذا كان المستخدم يفتقر إلى التطبيق، ستعرض متصفحات سطح المكتب والموبايل رسائل خطأ مزعجة، مثل تنبيه Safari "لا يمكن فتح الصفحة لأن العنوان غير صالح" أو أخطاء بروتوكول غير معالجة في الحاويات المدمجة.

علاوة على ذلك، لا تحافظ روابط المتجر فقط الثابتة عموماً على سياق توجيه التطبيق بعد التثبيت ما لم يكن هناك نظام توجيه مؤجل. إذا تم توجيه مستخدم لم يثبت التطبيق مباشرة إلى قائمة متجر تطبيقات دون ربط معلمات سياقية، فإن إطلاق التطبيق الذي تم تثبيته حديثاً يسقط المستخدم على شاشة رئيسية عامة. يتعين على المستخدم بعد ذلك البحث يدوياً عن الحملة الترويجية أو العنصر الذي أدى إلى التحميل في البداية. هذا الانفصال السياقي يزيد من تراجع المستخدمين عند التثبيت ويقلل من كفاءة التسويق المدفوع.

دور تمرير المعلمات السياقية في الحفاظ على النية الترويجية

تتغلب أطر عمل إعادة التوجيه الآلي من الويب إلى التطبيق على قيود الروابط الثابتة من خلال تنفيذ نقل المعلمات الديناميكي. عندما يتفاعل المستخدم مع عنصر دعوة لاتخاذ إجراء (CTA) على صفحة هبوط للجوال، تلتقط نصوص التوجيه من جانب العميل معلمات سياقية—بما في ذلك وسوم الحملة، ورموز الإحالة، ومعرفات القسائم، ومسارات المشهد المستهدف.

تُمكّن OpoInstall، وهي منصة إسناد وفتح عميق للجوال، مطوري الواجهة الأمامية من ربط معلمات استعلام ديناميكية بوظائف إعادة التوجيه من جانب العميل. إذا كان التطبيق مثبتاً بالفعل، يقوم النظام بتوجيه المستخدم إلى التطبيق الأصلي وتسليم الحمولة عبر مندوبي نظام التشغيل. إذا كان التطبيق غائباً، تلتقط طبقة التوجيه المعلمات السياقية على خادم الإسناد، وتوجه المستخدم إلى متجر التطبيقات المناسب، وتستعيد المعلمات الأصلية عند إطلاق التطبيق لأول مرة بعد التثبيت، حيثما كان ذلك مدعوماً من نظام الفتح العميق المؤجل المطبق ومسموحاً به من قبل قيود خصوصية المنصة.

كيف يؤثر تصفح المتصفح الحديث على التوجيه من الويب إلى التطبيق

حتمية إيماءة المستخدم: لماذا تحظر المتصفحات الحديثة عمليات إعادة التوجيه التلقائية برمجياً

تاريخياً، حاولت صفحات ويب الجوال إيقاظ التطبيقات الأصلية تلقائياً عن طريق تنفيذ عمليات إعادة توجيه برمجية عند تحميل الصفحة، مثل حقن عناصر iframe ديناميكية تشير إلى مخططات URI مخصصة أو تنفيذ window.location.href = "myapp://" داخل حلقات توقيت آلية. تقيد متصفحات الهواتف الحديثة—بما في ذلك Apple Safari و Google Chrome و Mozilla Firefox—بشكل نشط التنقل البرمجي غير المطلب به.

تفرض سياسات أمان المتصفح متطلبات تنشيط مستخدم متميزة اعتماداً على آلية التوجيه: Chrome Intent URIs: يتطلب Google Chrome إيماءة مستخدم مباشرة (مثل حدث نقر أو ضغط صريح) لتشغيل معالجة بروتوكول intent://؛ يتم تجاهل النصوص البرمجية الآلية أو التنفيذ القائم على المؤقت دون تنشيط مستخدم مسبق.

  • نوافذ منبثقة وبروتوكولات خارجية: تمنع Safari و Firefox تأكيدات النوافذ أو عمليات تسليم البروتوكول التي تفتقر إلى تفاعل مباشر للمستخدم لمنع إعادة التوجيه الخلفي المسيئة والنقر الخادع.
  • روابط HTTPS الموثقة: بينما تتبع Universal Links و App Links استدلالات حل عناوين URL على مستوى النظام، فإن استدعاءها عبر حلقات نصية آلية دون مشغل تفاعلي غالباً ما يتسبب في افتراض المتصفحات للتنقل الافتراضي في الويب.

تعرض متصفحات الجوال بروتوكولات وبدائل مختلفة لتوجيه الويب إلى التطبيق.

قيود النطاق الواحد في Safari: التنقل عبر حدود النطاقات لروابط Universal Links

توفر روابط Universal Links من Apple تحققاً مشفراً للنطاق لتطبيقات iOS. ومع ذلك، يفرض Apple Safari قيوداً صارمة على التنقل ضمن النطاق نفسه، كما هو موثق في توثيق مطور Apple حول السماح للتطبيقات ومواقع الويب بالارتباط بمحتواك. إذا تصفح المستخدم صفحة ويب مستضافة على https://example.com/promo ونقر على رابط Universal Link يشير إلى نفس النطاق تماماً (https://example.com/app/open)، يفترض Safari أن المستخدم ينوي مواصلة تصفح موقع الويب وينفذ التنقل القياسي في الويب بدلاً من إطلاق التطبيق الأصلي.

عندما يكون السلوك المطلوب في Safari هو مغادرة الموقع الذي يتم تصفحه حالياً وفتح التطبيق، فإن استخدام نطاق فرعي مرتبط مختلف هو نمط موثق من Apple. يجب أن تقيم صفحة هبوط الويب على نطاق متميز أو نطاق فرعي مخصص (مثل https://www.example.com أو https://link.example.com) بينما يتم توجيه رابط Universal Link Associated Domain من خلال مضيف وجهة منفصل (مثل https://app.example.com). النقر على رابط يشير عبر حدود نطاق فرعي متميزة يرضي استدلالات التنقل في Safari ويستدعي تنفيذ التطبيق الأصلي.

عناوين Intent URIs في Google Chrome: هيكلة حزم البدائل على Android

على أجهزة Android التي تشغل Google Chrome، يمكن لمطوري الويب الاستفادة من Android Intent URIs لبدء إيقاظ التطبيق الأصلي وتحديد بدائل المتصفح الآلية ضمن سلسلة URI واحدة. يستخدم Android Intent URI صيغة intent://، مع ترميز مسار الوجهة، واسم حزمة التطبيق المستهدفة، ورابط بديل للمتصفح:

intent://product/detail/1024#Intent;scheme=myapp;package=com.example.app;S.browser_fallback_url=https%3A%2F%2Fplay.google.com%2Fstore%2Fapps%2Fdetails%3Fid%3Dcom.example.app;end

عندما ينقر مستخدم على Intent URI في Chrome:

  1. حل النشاط: يحاول Chrome حل الـ Intent إلى نشاط مؤهل وقابل للتصفح يطابق الحزمة والمخطط المعلن عنهما.
  2. إطلاق أصلي: إذا كان هناك نشاط مؤهل موجود، يقوم نظام التشغيل بإطلاق التطبيق المستهدف وتسليم حمولة الـ Intent.
  3. تنفيذ البديل: إذا لم يكن هناك تطبيق مطابق مثبتاً، يلتقط Chrome فشل الحل وينتقل إلى الرابط المحدد في S.browser_fallback_url، مما يوجه المستخدم إلى Google Play أو صفحة هبوط ويب صريحة دون عرض حوارات خطأ بروتوكول غير معالجة.

اكتشاف إيقاظ التطبيق عبر واجهة W3C Page Visibility API (document.hidden)

عند استخدام مخططات URL المخصصة القياسية أو Universal Links، تقوم صفحات الويب بتقييم ما إذا كان إطلاق التطبيق قد حدث من أجل إيقاف مؤقتات البديل الثانوية. نظراً لأن جافا سكريبت المتصفح لا يمكنها الاستعلام مباشرة عن جداول عمليات نظام التشغيل الأصلي لتأكيد حالات التطبيق، تعتمد بنية الواجهة الأمامية على مواصفة W3C Page Visibility Level 2.

عندما يسلم المتصفح التنفيذ بنجاح إلى تطبيق خارجي، تنتقل علامة تبويب المتصفح إلى الخلفية، مما يغير حالة رؤيتها. تستمع النصوص البرمجية من جانب العميل لحدث visibilitychange وتفحص خاصية document.hidden:

document.addEventListener("visibilitychange", function() {
    if (document.hidden) {
        // فقد المتصفح رؤية الواجهة الأمامية؛ قم بمسح مؤقت البديل المعلق للمتجر
        clearTimeout(fallbackTimer);
    }
});

يوفر استخدام visibilitychange إشارة إلى أن المتصفح فقد تركيز الواجهة الأمامية بعد محاولة الإطلاق. بينما لا يثبت في حد ذاته أن التطبيق المستهدف المحدد قد أطلق بنجاح، فإنه يعمل كإشارة قمع مفيدة لمنع عمليات إعادة توجيه المتجر غير المرغوب فيها بينما يكون المستخدم خارج المتصفح.

الآليات التقنية للإيقاظ المباشر وبدائل المتجر المتأخرة

استدلالات الرؤية والتوقيت تقمع البديل بعد تسليم محتمل للتطبيق الأصلي.

شلال إعادة التوجيه متعدد المستويات: Universal Links، المخططات المخصصة، وبدائل المتجر

تنشر بنى Web to App في الإنتاج شلال إعادة توجيه متعدد المستويات يتدهور بسلاسة عبر أنظمة تشغيل متنوعة، وبائعي متصفحات، وحالات تثبيت:

  • المستوى 1: روابط الفتح العميق الموثقة من نظام التشغيل (Universal Links و App Links): يستخدم التوجيه الأساسي روابط HTTPS الموثقة بالنطاق. على iOS 9+ و Android 6.0+، توفر هذه الروابط إطلاقاً مباشراً للتطبيق دون حوار عند تثبيت التطبيق، بينما تعود إلى تنقل الويب عند إلغاء التثبيت.
  • المستوى 2: مخططات URI المخصصة وبروتوكولات الـ Intent: عندما لا تتوفر الروابط الموثقة أو عند العمل ضمن إصدارات متصفح قديمة، يعود محرك التوجيه إلى مخططات URI مخصصة (myapp://) أو Android Intent URIs التي يتم تشغيلها بواسطة نقرات المستخدم المباشرة.
  • المستوى 3: إعادة التوجيه السياقي للمتجر: إذا لم يطلق أي بروتوكول أصلي جلسة تطبيق، توجه طبقة البديل المتصفح إلى Apple App Store أو Google Play Store أو نقطة تنزيل صريحة مع تمرير معلمات الإسناد إلى الخادم الخلفي حيثما كان ذلك مدعوماً.

تجنب مسبارات إطار iframe المخفية القديمة في المتصفحات الحديثة

تضمن حل بديل تاريخي للمخططات المخصصة حقن عناصر iframe مخفية في DOM لاستكشاف ما إذا كان التطبيق مثبتاً دون الابتعاد عن الصفحة. في متصفحات الجوال الحديثة، هذا النهج مهمل وغير منتج:

  • تعمل الإصدارات الحديثة من Apple Safari و Google Chrome بنشاط على حظر استدعاءات المخطط المخصصة الناشئة عن iframes المحقونة ديناميكياً.
  • في Safari، قد تظل محاولة تحميل مخطط غير مثبت عبر iframe تؤدي إلى تنبيهات "عنوان غير صالح" مزعجة.
  • تتجاوز مسبارات iframe المخفية تتبع إيماءات المستخدم، مما يؤدي إلى تحذيرات أمان المتصفح.

بدلاً من الاعتماد على مسبارات iframe الصامتة، تستخدم البنى الحديثة Universal Links و App Links الموثقة. عندما يجب دعم المخططات المخصصة كبدائل، يجب استدعاؤها مباشرة من معالجات النقر التي بدأها المستخدم والمصحوبة بدعوات اتخاذ إجراء (CTAs) بديلة صريحة يتحكم فيها المستخدم.

استراتيجيات توقيت البديل القائمة على المؤقت والقائمة على الرؤية

لنقل المستخدمين غير المثبتين للتطبيق إلى قوائم متجر التطبيقات دون التدخل في عمليات إطلاق التطبيق الأصلي، تنشر نصوص العميل استدلالاً تجريبياً يجمع بين فحوصات الرؤية ومؤقتات التنفيذ. عندما ينقر المستخدم على زر CTA الخاص بـ Web to App:

  1. محاولة الإطلاق: يحاول النص البرمجي تشغيل التطبيق الأصلي عبر Universal Link أو مخطط مخصص.
  2. تسليح المؤقت: يتم تعيين مهلة JavaScript (عادةً نافذة استدلالية خاصة بالتنفيذ بين 1500ms و 2500ms).
  3. حساب نبضات القلب: يسجل النص البرمجي الطابع الزمني قبل تشغيل التنقل ويقيم الوقت المنقضي عند تنفيذ المؤقت. إذا حدث إطلاق للتطبيق، فإن تعليق نظام التشغيل يؤخر تنفيذ المؤقت؛ عندما يعود المستخدم في النهاية إلى المتصفح، يتجاوز الوقت المنقضي بشكل كبير الفاصل الزمني المحدد (Δt≫2000ms\Delta t \gg 2000\text{ms})، مما يسمح للنص البرمجي بإلغاء إعادة التوجيه إلى المتجر.
  4. إعادة التوجيه البديلة: إذا ظلت document.hidden خاطئة وكان الوقت المنقضي يتماشى بشكل وثيق مع الفاصل الزمني المحدد (Δt≈2000ms\Delta t \approx 2000\text{ms})، فإن محاولة الإطلاق لم تنتج إشارة فقدان واجهة أمامية ضمن النافذة المكونة، ويستمر النص البرمجي في إعادة التوجيه الصريح للمتجر.
[المستخدم ينقر على زر CTA ويب]
             │
             ▼
[طبقة اكتشاف المتصفح ونظام التشغيل]
   ┌─────────┴─────────┐
   ▼                                      ▼
[iOS Safari]                    [Android Chrome / Firefox]
   │                                      │
   ▼                                      ▼
[Universal Link]               [Verified App Link / Intent]
   │                                      │
┌──┴─────────┐         ┌──┴─────────┐
▼                        ▼         ▼                        ▼
مؤهل             مسار ويب     مؤهل                  مسار ويب
│                        │         │                        │
▼                        ▼         ▼                        ▼
[فتح التطبيق]        [ويب]    [فتح التطبيق]                 [ويب]
             │                                  │
             ▼                                  ▼
     [فحص الرؤية]                  [Intent Fallback]
             │                                  │
             ▼                                  ▼
     [إعادة التوجيه لمتجر التطبيقات]              [إعادة التوجيه لمتجر Play]

كيفية تنفيذ إعادة التوجيه من الويب إلى التطبيق باستخدام SDKs جافا سكريبت

تهيئة إعادة التوجيه من جانب العميل عبر OpoInstall Web JS SDK

توفر OpoInstall Web JS SDK توجيهاً عبر المتصفحات، وتلخص تعقيدات تحليل وكيل المستخدم (user-agent)، وتوجيه Universal Link، وتوقيت البديل. يقوم المطورون بتهيئة الـ SDK على صفحات هبوط الويب الخاصة بهم، مع تكوين معرف التطبيق الفريد ومعلمات التوجيه الخاصة بهم.

يجب تحميل النص البرمجي بشكل غير متزامن وتهيئته أثناء إنشاء DOM الأولي. راجع توثيق تكامل Web SDK للحصول على معلمات الواجهة الكاملة ومواصفات API.

ربط النقرات التي بدأها المستخدم بأساليب m.wakeupOrInstall()

للامتثال لقيود إيماءات المستخدم في المتصفح الحديث، يجب ربط إعادة التوجيه مباشرة بأحداث اللمس أو النقر الفعلية. تكشف OpoInstall SDK عن أسلوب wakeupOrInstall()، الذي ينسق شلال إعادة التوجيه:

  • يقوم بتقييم نوع جهاز العميل (iOS، Android، سطح المكتب).
  • ينشئ Universal Links أو Chrome Intent URIs مناسبة على الطاير.
  • يرفق معلمات سياقية بطلب الإطلاق.
  • يدير مستمعي الرؤية ومؤقتات البديل تلقائياً.

تمرير معلمات الاستعلام الديناميكية عبر انتقالات الويب إلى التطبيق

يمكن أن تتضمن المعلمات الديناميكية التي يتم تمريرها أثناء إعادة توجيه الويب رموزاً ترويجية، ورموزاً للمدعو، ومعرفات قنوات التسويق، أو وجهات توجيه داخل التطبيق. يلتقط نص الواجهة الأمامية هذه المعلمات من سلسلة استعلام URL، ويقوم بتنقية المدخلات مقابل قيود صارمة للطول والأحرف، ويربطها بحمولة إعادة التوجيه.

يوضح التنفيذ التقني أدناه كيفية تكوين OpoInstall Web JS SDK على صفحة هبوط H5، وإرفاق معلمات حملة منظفة، وربط التنفيذ بنقرة زر تفاعلية:

// ويب: تنفيذ صفحة هبوط H5 باستخدام OpoInstall Web JS SDK
// مثال تكامل مرجعي؛ تحقق من URL النص البرمجي، والباني، ودورة حياة رد الاتصال،
// ومخطط المعلمات، وتوقيع wakeupOrInstall() مقابل إصدار OpoInstall Web SDK المنشور حالياً في الإنتاج.
(function() {
    // حقن نص برمجي ديناميكي لـ OpoInstall Web SDK
    var script = document.createElement("script");
    script.type = "text/javascript";
    script.src = "https://web.cdn.opoinstallcloud.com/openinstall.js";
    script.onload = function() {
        // تهيئة مثيل OpenInstall Web SDK
        var openInstall = new OpenInstall({
            appKey: "YOUR_OPOINSTALL_APPKEY",
            onready: function() {
                var m = this;
                var openAppButton = document.getElementById("openAppBtn");

                // ربط إعادة التوجيه مباشرة بحدث نقر المستخدم (يلبي متطلبات إيماءات المستخدم للمتصفح)
                openAppButton.addEventListener("click", function() {
                    var urlParams = new URLSearchParams(window.location.search);
                    
                    // تنقية المدخلات: التحقق من صحة المعلمات مقابل قوائم السماح الصارمة وحدود الطول
                    var rawTarget = urlParams.get("target") || "product_detail";
                    var rawId = urlParams.get("id") || "";
                    var rawSource = urlParams.get("utm_source") || "web_campaign";
                    var rawChannel = urlParams.get("channelCode") || "web_landing";

                    var allowedScenes = ["product_detail", "promo_hub", "category_view"];
                    var targetScene = allowedScenes.indexOf(rawTarget) !== -1 ? rawTarget : "product_detail";
                    
                    // فرض قيود أبجدية رقمية وطولية على المعرفات
                    var idRegex = /^[A-Za-z0-9_-]{1,64}$/;
                    var targetId = idRegex.test(rawId) ? rawId : "DEFAULT_ID";
                    
                    var channelRegex = /^[A-Za-z0-9_-]{1,32}$/;
                    var channelCode = channelRegex.test(rawChannel) ? rawChannel : "web_landing";
                    
                    var campaignSource = rawSource.replace(/[^A-Za-z0-9_-]/g, "").substring(0, 32);

                    var dynamicPayload = {
                        targetScene: targetScene,
                        targetId: targetId,
                        campaignSource: campaignSource
                    };

                    // تنفيذ شلال إعادة التوجيه متعدد المستويات:
                    // 1. يحاول إيقاظ مباشر عبر Universal Link / App Link
                    // 2. يعود إلى تحميل المتجر إذا لم يتم اكتشاف إشارات إطلاق
                    m.wakeupOrInstall({
                        data: dynamicPayload,
                        channelCode: channelCode
                    });
                });
            }
        }, document.getElementById("openAppBtn"));
    };
    document.head.appendChild(script);
})();
// Android: تجاوز WebViewClient لـ shouldOverrideUrlLoading مع حدود الإطار وأمان المسار
// يعترض المخططات المخصصة، وبروتوكولات الـ intent، والروابط الداخلية للتطبيق مع التحقق الصارم من الهدف
package com.example.app.webview

import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient

data class ValidatedAppRoute(val path: String, val queryParams: Map<String, String>)

object AppLinkRouteValidator {
    private val allowedPrefixes = setOf("/open/", "/product/", "/promo/")
    private val allowedQueryKeys = setOf("target", "id", "promo", "utm_source")

    fun validate(uri: Uri): ValidatedAppRoute? {
        val path = uri.path ?: ""
        if (allowedPrefixes.none { path.startsWith(it) }) {
            return null
        }

        val sanitizedParams = mutableMapOf<String, String>()
        for (name in uri.queryParameterNames) {
            // موقف أمني صارم: رفض الطلب إذا كانت مفاتيح الاستعلام غير المعروفة موجودة
            if (name !in allowedQueryKeys) {
                return null
            }
            val value = uri.getQueryParameter(name) ?: ""
            if (value.length <= 64 && value.matches(Regex("^[A-Za-z0-9_-]+$"))) {
                sanitizedParams[name] = value
            } else {
                return null
            }
        }

        return ValidatedAppRoute(path, sanitizedParams)
    }
}

object CustomSchemeValidator {
    private val allowedSchemes = setOf("myapp")
    private val allowedHosts = setOf("open", "product", "event")
    private val allowedPathPrefixes = setOf("/detail/", "/view/", "/main/")
    private val allowedQueryKeys = setOf("target", "id", "promo", "utm_source")

    fun validate(uri: Uri): Uri? {
        val scheme = uri.scheme?.lowercase() ?: return null
        val host = uri.host?.lowercase() ?: return null
        val path = uri.path ?: ""

        if (!allowedSchemes.contains(scheme) || !allowedHosts.contains(host)) {
            return null
        }

        if (allowedPathPrefixes.none { path.startsWith(it) }) {
            return null
        }

        for (name in uri.queryParameterNames) {
            if (name !in allowedQueryKeys) return null
            val value = uri.getQueryParameter(name) ?: ""
            if (value.length > 128 || !value.matches(Regex("^[A-Za-z0-9_-]+$"))) {
                return null
            }
        }

        return uri
    }
}

class CustomWebViewClient(
    private val context: Context,
    private val internalRouter: (ValidatedAppRoute) -> Unit
) : WebViewClient() {

    override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
        request ?: return false

        // متطلب أمني: حصر توجيه التطبيق الخارجي على تنقلات الإطار الرئيسي فقط
        if (!request.isForMainFrame) {
            return false // الأطر الفرعية و iframes لا يمكنها تشغيل intents خارجية أو مخططات مخصصة
        }

        val url = request.url.toString()
        return handleUrlNavigation(url)
    }

    @Deprecated("Deprecated in Java")
    override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
        if (url == null) return false

        val uri = Uri.parse(url)
        val scheme = uri.scheme?.lowercase()

        // رد الاتصال القديم يفتقر إلى بيانات isForMainFrame الوصفية الموثوقة؛ لا توجه المخططات الخارجية
        return if (scheme == "http" || scheme == "https") {
            false // السماح بتحميل محتوى الويب القياسي
        } else {
            Log.w("WebViewClient", "تم حظر المخطط الخارجي على رد اتصال قديم: $scheme")
            true // قمع أخطاء المخطط غير المعالج دون تشغيل intents خارجية
        }
    }

    private fun handleUrlNavigation(url: String): Boolean {
        val uri = Uri.parse(url)
        val scheme = uri.scheme?.lowercase() ?: return false

        // 1. التعامل مع HTTP / HTTPS والروابط الداخلية للتطبيق
        if (scheme == "http" || scheme == "https") {
            // تحقق مما إذا كان URL يطابق نطاق App Link الخاص بالتطبيق المضيف
            if (scheme == "https" && uri.host?.lowercase() == "app.example.com") {
                val validatedRoute = AppLinkRouteValidator.validate(uri)
                if (validatedRoute != null) {
                    Log.i("WebViewClient", "تم التحقق من رابط التطبيق الداخلي. يتم الإرسال إلى الموجه الداخلي.")
                    internalRouter(validatedRoute)
                } else {
                    Log.w("WebViewClient", "رابط تطبيق داخلي مشوه أو غير مصرح به مرفوض: $uri")
                }
                return true // تم التعامل معه داخلياً؛ إلغاء تحميل webview
            }
            return false // السماح لـ WebView بتحميل محتوى الويب القياسي أصلياً
        }

        // 2. التحقق من المخططات المخصصة مقابل قوائم السماح الصارمة للمخطط والمضيف والمسار والاستعلام
        val validatedUri = CustomSchemeValidator.validate(uri)
        if (validatedUri != null) {
            return launchSafeIntent(Intent(Intent.ACTION_VIEW, validatedUri))
        }

        // 3. التعامل مع Chrome Intent URIs مع إعادة بناء آمنة
        if (scheme == "intent") {
            return try {
                val parsed = Intent.parseUri(url, Intent.URI_INTENT_SCHEME)
                val targetPackage = parsed.`package`
                val allowedPackages = setOf("com.example.app", "com.example.partner")
                
                if (targetPackage != null && allowedPackages.contains(targetPackage)) {
                    val rawDataUri = parsed.data
                    if (rawDataUri != null) {
                        val validatedDataUri = CustomSchemeValidator.validate(rawDataUri)
                        if (validatedDataUri != null) {
                            // إعادة بناء Intent ACTION_VIEW نظيف ومخصص بدلاً من تنفيذ كائن محلل خام
                            val safeIntent = Intent(Intent.ACTION_VIEW, validatedDataUri).apply {
                                `package` = targetPackage
                                addCategory(Intent.CATEGORY_BROWSABLE)
                                flags = Intent.FLAG_ACTIVITY_NEW_TASK
                            }
                            launchSafeIntent(safeIntent)
                        } else {
                            Log.w("WebViewClient", "تم رفض intent مع URI بيانات غير صالح: $rawDataUri")
                            true
                        }
                    } else {
                        Log.w("WebViewClient", "تم رفض intent يفتقر إلى حمولة URI بيانات")
                        true
                    }
                } else {
                    Log.w("WebViewClient", "تم رفض intent يستهدف حزمة غير موثقة: $targetPackage")
                    true // إلغاء intent الحزمة غير الموثقة
                }
            } catch (e: Exception) {
                Log.e("WebViewClient", "فشل تحليل intent URI: $url", e)
                true // قمع أخطاء المخطط غير المعالج
            }
        }

        // رفض المخططات غير المعروفة لتجنب أخطاء net::ERR_UNKNOWN_URL_SCHEME
        Log.w("WebViewClient", "تم حظر تنقل مخطط غير موثق: $scheme")
        return true
    }

    private fun launchSafeIntent(intent: Intent): Boolean {
        return try {
            intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
            context.startActivity(intent)
            true
        } catch (e: Exception) {
            Log.w("WebViewClient", "لم يتم العثور على نشاط لمعالجة intent: ${intent.data}", e)
            true // منع تعطل webview
        }
    }
}
// iOS: تنفيذ WKNavigationDelegate لـ Universal Links واعتراض المخططات المخصصة
// يعترض إجراءات التنقل داخل WKWebView ويفرض حدود أمان النطاق والمسار والاستعلام
import UIKit
import WebKit

struct ValidatedAppRoute {
    let path: String
    let queryParams: [String: String]
}

class AppRouteValidator {
    static let allowedPrefixes = ["/open/", "/product/", "/promo/"]
    static let allowedQueryKeys = ["target", "id", "promo", "utm_source"]

    static func validate(url: URL) -> ValidatedAppRoute? {
        let path = url.path
        guard allowedPrefixes.contains(where: { path.hasPrefix($0) }) else {
            return nil
        }

        var sanitizedParams: [String: String] = [:]
        if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
           let queryItems = components.queryItems {
            let validChars = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-")
            for item in queryItems {
                let name = item.name
                // التحقق من صحة الفشل المغلق: رفض URL إذا كانت مفاتيح الاستعلام غير المعروفة موجودة
                guard allowedQueryKeys.contains(name) else { return nil }
                let value = item.value ?? ""
                if value.count <= 64 && value.rangeOfCharacter(from: validChars.inverted) == nil {
                    sanitizedParams[name] = value
                } else {
                    return nil
                }
            }
        }

        return ValidatedAppRoute(path: path, queryParams: sanitizedParams)
    }
}

class CustomSchemeValidator {
    static let allowedSchemes = ["myapp"]
    static let allowedHosts = ["open", "product", "event"]
    static let allowedPathPrefixes = ["/detail/", "/view/", "/main/"]
    static let allowedQueryKeys = ["target", "id", "promo", "utm_source"]

    static func validate(url: URL) -> URL? {
        guard let scheme = url.scheme?.lowercased(), allowedSchemes.contains(scheme) else {
            return nil
        }
        guard let host = url.host?.lowercased(), allowedHosts.contains(host) else {
            return nil
        }

        let path = url.path
        if !path.isEmpty && !allowedPathPrefixes.contains(where: { path.hasPrefix($0) }) {
            return nil
        }

        // التحقق من صحة مفاتيح وقيم معلمات الاستعلام مع موقف الفشل المغلق
        if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
           let queryItems = components.queryItems {
            let validChars = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-")
            for item in queryItems {
                guard allowedQueryKeys.contains(item.name) else { return nil }
                let value = item.value ?? ""
                if value.count > 128 || value.rangeOfCharacter(from: validChars.inverted) != nil {
                    return nil
                }
            }
        }

        return url
    }
}

class CustomWebViewController: UIViewController, WKNavigationDelegate {

    var webView: WKWebView!

    override fun viewDidLoad() {
        super.viewDidLoad()

        let configuration = WKWebViewConfiguration()
        webView = WKWebView(frame: view.bounds, configuration: configuration)
        webView.navigationDelegate = self
        view.addSubview(webView)
    }

    func webView(
        _ webView: WKWebView,
        decidePolicyFor navigationAction: WKNavigationAction,
        decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
    ) {
        guard let url = navigationAction.request.url else {
            decisionHandler(.allow)
            return
        }

        let scheme = url.scheme?.lowercased() ?? ""

        // 1. التعامل مع النطاق المرتبط بالتطبيق المضيف (Universal Link الخاص)
        // ملاحظة توثيق Apple: فتح رابط Universal Link الخاص بالتطبيق عبر UIApplication.open()
        // لن يوجه العودة إلى التطبيق. يجب التحقق من صحته وإرساله إلى الموجه الداخلي مباشرة.
        if let host = url.host?.lowercased(), host == "app.example.com" {
            NSLog("[WKWebView] تم الكشف عن نطاق مرتبط بالمضيف: %@", url.path)
            if let validatedRoute = AppRouteValidator.validate(url: url) {
                AppInternalRouter.shared.navigate(to: validatedRoute)
            } else {
                NSLog("[WKWebView] تم قمع مسار داخلي غير مصرح به أو مشوه: %@", url.absoluteString)
            }
            decisionHandler(.cancel)
            return
        }

        // 2. التعامل مع المخططات المخصصة مع قائمة سماح صارمة والتحقق من صحة المسار
        if scheme != "http" && scheme != "https" && scheme != "about" {
            if let validatedURL = CustomSchemeValidator.validate(url: url) {
                UIApplication.shared.open(validatedURL, options: [:]) { success in
                    if !success {
                        NSLog("[WKWebView] فشل إطلاق مخطط مخصص مسموح به: %@", validatedURL.absoluteString)
                    }
                }
            } else {
                NSLog("[WKWebView] تم قمع مخطط أو مسار غير مصرح به: %@", url.absoluteString)
            }
            decisionHandler(.cancel)
            return
        }

        // 3. السماح بالتنقل القياسي في الويب
        decisionHandler(.allow)
    }
}

التغلب على بيئات WebView والمتصفحات داخل التطبيق

تحدي المتصفح داخل التطبيق: التنقل في WebViews مقيدة في تطبيقات التواصل الاجتماعي

تنشأ نسبة كبيرة من حركة مرور ويب الجوال من روابط تمت مشاركتها داخل منصات التواصل الاجتماعي وتطبيقات المراسلة (مثل WeChat و Line و Instagram و Facebook Messenger). عندما ينقر المستخدم على رابط داخل هذه التطبيقات، تعرض المنصة صفحة الهبوط داخل متصفح داخل التطبيق (WebView) مدمج بدلاً من فتح متصفح النظام الافتراضي (Safari أو Chrome).

نظراً لأن متصفحات التواصل الاجتماعي داخل التطبيق تفرض بيئات معزولة خاصة بالمورد، فإن قدراتها تختلف بشكل كبير:

  • تعتترض بعض البيئات مخططات URI المخصصة (myapp://) وتقمعها تماماً.
  • قد يتم تقييد تسليمات Universal Link، مما يبقي المستخدمين محاصرين داخل WebView الخاص بتطبيق التواصل الاجتماعي.
  • قد يتم حظر تحميلات ملفات APK المباشرة أو إعادة توجيهات المتجر الخارجية اعتماداً على سياسات المنصة وإصدارات التطبيق.

بدلاً من افتراض آلية كسر آلية عالمية، تكتشف بنى Web to App بيئات التشغيل هذه وتوفر توجيهاً مرئياً واضحاً يوجه المستخدمين لفتح الرابط في متصفح النظام الافتراضي (مثلاً، عبر قائمة "فتح في Safari" أو "فتح في Chrome").

WebViews المدمجة تتحقق من المسارات وتعيد بناءها قبل التنقل الأصلي الخارجي.

التعامل مع Android WebView: تجاوز shouldOverrideUrlLoading مع قائمة سماح صارمة

عندما تستضيف تطبيقات الهاتف WebViews مدمجة ضمن واجهاتها الأصلية، يجب على المطورين تكوين اعتراض التنقل المناسب. افتراضياً، يتعرف WebView في Android فقط على بروتوكولات الويب القياسية (http:// و https://). عندما تحاول صفحة ويب داخل WebView الانتقال إلى مخطط URI مخصص (myapp://) أو Android intent URI (intent://)، يطرح WebView خطأ بروتوكول غير معالج: net::ERR_UNKNOWN_URL_SCHEME.

كما هو موضح في مرجع مطوري Android حول WebViewClient shouldOverrideUrlLoading، يجب على المطورين توسيع WebViewClient وتجاوز shouldOverrideUrlLoading. ووفقاً لـ توجيه OWASP Mobile Application Security Testing Guide حول روابط الفتح العميق غير الآمنة، يجب على المطورين فرض قائمة سماح صارمة للمخطط، والتحقق من أن أحداث التنقل تنشأ من الإطار الرئيسي (request.isForMainFrame) لمنع إساءة استخدام iframe، وإعادة بناء intents ACTION_VIEW آمنة بدلاً من إرسال كائنات intent خام غير مقيدة إلى نظام التشغيل.

التعامل مع iOS WKWebView: تنفيذ decidePolicyForNavigationAction وحدود النطاق الذاتي

على iOS، يجب على التطبيقات التي تدمج مكونات WKWebView إدارة سياسات التنقل عبر توجيه مطور Apple حول WKNavigationDelegate.

يوجد تمييز معماري حاسم فيما يتعلق بـ Universal Links: كما هو موثق بواسطة Apple، إذا قام تطبيق باستدعاء UIApplication.shared.open() على رابط Universal Link يشير إلى النطاق المرتبط الخاص به، فلن يفتح iOS الرابط في التطبيق المضيف كرابط Universal Link. لذلك:

  • النطاقات المرتبطة بالتطبيق المضيف: إذا كان URL يطابق النطاق المرتبط الخاص بالتطبيق المضيف (app.example.com)، يجب على التطبيق إلغاء تحميل webview (decisionHandler(.cancel))، والتحقق من صحة مسار المسار والمعلمات مقابل قوائم سماح صارمة، واستدعاء موجه التنقل الداخلي للتطبيق مباشرة.
  • المخططات الخارجية المسموح بها: إذا كان URL يحتوي على مخطط مخصص مسموح به يستهدف تطبيقاً خارجياً، تحقق من صحة المخطط، والمضيف، والمسار، وتنسيق الاستعلام مقابل قائمة سماح صريحة قبل التفويض إلى UIApplication.shared.open.

التمييز بين لافتات Apple Smart App Banners ولافتات الويب إلى التطبيق المخصصة

في السيناريوهات التي قد تؤدي فيها إعادة التوجيه الكامل للصفحة إلى تعطيل تجربة المستخدم (مثل مدونات المحتوى، بوابات الأخبار، أو التوثيق التقني)، تنشر فرق النمو لافتات ترويجية. يجب على فرق الهندسة التمييز بين بنيتي لافتة متميزتين:

  • Apple Smart App Banners: ميزة Safari أصلية يتم الإعلان عنها عبر <meta name="apple-itunes-app" content="app-id=myAppID">. يتم عرض هذه اللافتة محلياً بواسطة Safari على أجهزة iOS دون تنفيذ JavaScript، ولكنها لا تظهر على Android أو متصفحات الطرف الثالث.
  • لافتات CTA المخصصة للويب إلى التطبيق: مكونات HTML يتم عرضها بواسطة JavaScript ويمكنها العرض عبر متصفحات الهاتف الرئيسية، بينما يظل سلوك فتح التطبيق معتمداً على المتصفح والمنصة. تعرض هذه اللافتات أيقونات تطبيق ديناميكية، وعناوين، وتقييمات، ودعوات لاتخاذ إجراء سياقية، مما يؤدي إلى تشغيل m.wakeupOrInstall() لتمرير معلمات وقت التشغيل مباشرة إلى التطبيق الأصلي. يمكن للمطورين الحصول على مكتبات العميل مباشرة من مركز تحميل OpoInstall SDK.

مصفوفة توافق إعادة التوجيه من الويب إلى التطبيق عبر المتصفحات

تقييم بروتوكولات إعادة التوجيه عبر بيئات متصفح الهاتف

نظراً لأن بائعي المتصفحات يقومون باستمرار بتحديث سياسات أمان التنقل، يجب على فرق الهندسة تقييم بروتوكولات إعادة التوجيه مقابل بيئات تشغيل متصفح محددة للحفاظ على قمع مستخدم موثوق.

القدرات التقنية والقيود السلوكية لبيئة المتصفح

يلخص الجدول أدناه التوافق التقني عبر المتصفحات، وقيود إيماءات المستخدم، وآليات البديل:

المتصفح / البيئة البروتوكول الأساسي المدعوم قيود تنشيط المستخدم سلوك إيقاظ التطبيق آلية البديل غير المثبت
iOS Safari Universal Links إيماءة المستخدم مطلوبة للمخططات المخصصة؛ نمط عبر النطاق الفرعي موصى به لـ Universal Links يطلق التطبيق مباشرة إذا كان مؤهلاً وموثقاً يحمل URL الويب في المتصفح →\to بديل صريح لمتجر التطبيقات
Android Chrome Android App Links / Intent URIs إيماءة مستخدم مباشرة مطلوبة لـ intent:// والمخططات المخصصة يتجاوز حوار الاختيار إذا كان المضيف موثقاً ينتقل إلى S.browser_fallback_url أو متجر Play
Android Firefox روابط HTTPS / توجيه التطبيق المدعوم من نظام التشغيل حيثما كان مؤهلاً يعتمد على إصدار المتصفح؛ تحقق من صحته مقابل سلوك Firefox Android الحالي قد يختلف حسب إصدار Firefox وحالة معالج الروابط في Android يتبع رابط الويب القياسي →\to يوصى ببديل ويب/متجر صريح
In-App Social WebViews يختلف حسب التطبيق والإصدار خاضع لبيئات الحاوية الاجتماعية واعتراض النقر غالباً ما يتم اعتراضه أو تقييده وجه المستخدم إلى متصفح النظام الافتراضي (Safari / Chrome)
Embedded Android WebView معترض عبر shouldOverrideUrlLoading تنفيذ التطبيق المضيف يتحكم في قواعد التنشيط يتطلب تجاوز صريح لـ WebViewClient يطرح net::ERR_UNKNOWN_URL_SCHEME ما لم يتم تجاوزه

الأسئلة الشائعة (FAQ)

كيف تتعامل إعادة التوجيه من الويب إلى التطبيق مع المستخدمين الذين ليس لديهم التطبيق مثبتاً؟
عندما لا يكون لدى المستخدم التطبيق مثبتاً، تقوم Universal Links و App Links بتحميل URL الويب الوجهة في المتصفح. تكتشف جافا سكريبت صفحة الويب أن التطبيق لم يفتح—استنتاجاً استدلالياً عندما لا تؤدي واجهة W3C Page Visibility API إلى إشارة فقدان واجهة أمامية ضمن نافذة بديلة مكونة—وتوجه المستخدم إلى متجر التطبيقات المناسب مع الحفاظ على سياق الإسناد حيثما كان ذلك مدعوماً.
لماذا تفشل عمليات إعادة التوجيه البرمجية بدون حدث نقر من المستخدم؟
تفرض متصفحات الهاتف الحديثة سياسات أمان صارمة تحظر عمليات تنفيذ مخطط URL الآلية والنوافذ المنبثقة التي لا يتم تشغيلها بواسطة إيماءة مستخدم مباشرة. يؤدي استدعاء وظائف إعادة التوجيه داخل مؤقتات غير متزامنة أو نصوص برمجية ذات تحميل تلقائي إلى قيام متصفحات مثل Chrome و Safari بقمع محاولة التنقل لمنع عمليات إعادة التوجيه المتطفلة.
كيف يمكنني منع أخطاء net::ERR_UNKNOWN_URL_SCHEME في Android WebViews؟
يحدث هذا الخطأ عندما يواجه Android WebView مخططاً مخصصاً (<code>myapp://</code>) أو intent URI لا يمكنه التعرف عليه أصلياً. يجب على المطورين تجاوز أسلوب <code>shouldOverrideUrlLoading</code> في <code>WebViewClient</code>، والتحقق من صحة URI الوارد مقابل قائمة سماح صريحة للمخططات المصرح بها، وإرسال المخططات المسموح بها عبر <code>Intent</code> خارجي في Android بدلاً من محاولة تحميلها كموارد ويب.

الملخص وإطار عمل القرار

تتطلب أتمتة إعادة التوجيه من الويب إلى التطبيق سد الفجوة بين بيئات متصفح الويب وأنظمة تشغيل الهاتف الأصلية. الاعتماد على روابط ثابتة أساسية أو مخططات مخصصة غير موثقة يقدم احتكاكاً، ويطلق تحذيرات المتصفح، ويزيد من التراجع عبر رحلة المستخدم.

يساعد نشر خط أنابيب إعادة توجيه متعدد المستويات—يجمع بين روابط Universal Links الموثقة، و App Links، وجافا سكريبت التي تعمل بإيماءة المستخدم، وبدائل المتجر السلسة—في تقليل فشل التوجيه على مستوى المتصفح ويدعم انتقالات أكثر سلاسة إلى التطبيقات الأصلية. من خلال إقران SDKs الويب من جانب العميل بمندوبي WebView مرنين ومطبقين لقائمة السماح، تقلل فرق الهندسة من احتكاك التحويل وتدعم أهداف مشاركة دورة حياة المستخدم.

لمعرفة كيفية تنفيذ تدفقات إعادة التوجيه من الويب إلى التطبيق القوية، استشر توثيق تكامل Web SDK، وقم بتحميل مكتبات العميل من مركز تحميل OpoInstall SDK، واستكشف مرجع تنفيذ إسناد الجوال، أو سجل تطبيقك على وحدة تحكم مطوري OpoInstall.

مواد ذات صلة

Share this article