البرنامج التعليمي: إنشاء تطبيق ويب أحادي الصفحة

تحذير

في 30 أكتوبر 2020، انتقلت واجهات برمجة التطبيقات بحث Bing من خدمات Azure الذكاء الاصطناعي إلى خدمات بحث Bing. تتوفر هذه الوثائق للرجوع إليها فحسب. للحصول على وثائق محدثة، راجع الوثائق الخاصة ببحث Bing API . للحصول على إرشادات بشأن إنشاء موارد Azure جديدة لـ Bing Search، راجع إنشاء مورد Bing Search من خلال Azure Marketplace.

تتيح لك واجهة برمجة تطبيقات بحث أخبار Bing البحث في الويب والحصول على نتائج من نوع الأخبار ذات الصلة باستعلام البحث. في هذا البرنامج التعليمي، نقوم بإنشاء تطبيق ويب من صفحة واحدة يستخدم Bing News Search API لعرض نتائج البحث على الصفحة. يتضمن التطبيق مكونات HTML وCSS وJavaScript. التعليمة البرمجية المصدر لهذا النموذج متاحة على GitHub.

ملاحظة

تظهر عناوين JSON وHTTP في أسفل الصفحة عند النقر عليها استجابة JSON ومعلومات طلب HTTP. يمكن أن تكون هذه التفاصيل مفيدة عند استكشاف الخدمة.

يوضح تطبيق البرنامج التعليمي كيفية:

  • إجراء استدعاء Bing News Search API في JavaScript
  • تمرير خيارات البحث إلى Bing News Search API
  • عرض نتائج بحث الأخبار من أربع فئات: أي نوع، أو نشاط تجاري، أو صحة، أو نهج، من الأطر الزمنية 24 ساعة، أو الأسبوع الماضي، أو الشهر، أو كل الأوقات المتاحة
  • نتائج البحث من خلال الصفحة
  • معالجة معرف عميل Bing، ومفتاح الاشتراك في API
  • معالجة الأخطاء التي قد تحدث

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

المتطلبات الأساسية

لمتابعة البرنامج التعليمي، تحتاج إلى مفتاح اشتراك Bing Search API. إذا لم تكن لديك هذه، فستحتاج إلى إنشائها:

مكونات التطبيق

مثل أي تطبيق ويب من صفحة واحدة، يتضمن تطبيق البرنامج التعليمي هذا ثلاثة أجزاء:

  • HTML - تحديد بنية الصفحة ومحتواها
  • CSS - تحديد مظهر الصفحة
  • JavaScript -- تحديد سلوك الصفحة

معظم HTML وCSS تقليدية؛ لذلك لا يناقش البرنامج التعليمي ذلك. يحتوي HTML على نموذج البحث الذي يقوم المستخدم بإدخال الاستعلام، وتحديد خيارات البحث فيه. يتم توصيل النموذج بـ JavaScript الذي يقوم بالبحث باستخدام سمة onsubmit لعلامة <form>:

<form name="bing" onsubmit="return newBingNewsSearch(this)">

يعود المعالج onsubmit بالنتيجة false، الذي يمنع إرسال النموذج إلى أحد الخوادم. تقوم التعليمات البرمجية JavaScript بعمل جمع المعلومات الضرورية من النموذج، وإجراء البحث.

يحتوي HTML أيضا على الأقسام (علامات <div> لـ HTML) حيث تظهر نتائج البحث.

إدارة مفتاح الاشتراك

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

نحن نحدد دالة storeValue وretrieveValue التي تستخدم إما الكائن localStorage (لا تدعمها جميع المتصفحات) أو ملف تعريف الارتباط. تستخدم الدالة getSubscriptionKey()هذه الدالات لتخزين مفتاح المستخدم واسترداده. يمكنك استخدام نقطة النهاية العامة أدناه، أو نقطة نهاية المجال الفرعي المخصص المعروضة في مدخل Microsoft Azure لموردك.

// Cookie names for data we store
API_KEY_COOKIE   = "bing-search-api-key";
CLIENT_ID_COOKIE = "bing-search-client-id";

// Bing Search API endpoint
BING_ENDPOINT = "https://api.cognitive.microsoft.com/bing/v7.0/news";

// ... omitted definitions of storeValue() and retrieveValue()
// Browsers differ in their support for persistent storage by 
// local HTML files. See the source code for browser-specific
// options.

// Get stored API subscription key, or 
// prompt if it's not found.
function getSubscriptionKey() {
    var key = retrieveValue(API_KEY_COOKIE);
    while (key.length !== 32) {
        key = prompt("Enter Bing Search API subscription key:", "").trim();
    }
    // always set the cookie in order to update the expiration date
    storeValue(API_KEY_COOKIE, key);
    return key;
}

تقوم العلامة <form>HTMLonsubmit باستدعاء الدالة bingWebSearch للعودة بنتائج البحث. تقوم bingWebSearch باستخدام getSubscriptionKey() لمصادقة كل استعلام. كما هو موضح في التعريف السابق، يطالب getSubscriptionKey المستخدم بالمفتاح إذا لم يتم إدخال المفتاح. ثم يتم تخزين المفتاح لاستمرار استخدامه بواسطة التطبيق.

<form name="bing" onsubmit="this.offset.value = 0; return bingWebSearch(this.query.value, 
    bingSearchOptions(this), getSubscriptionKey())">

تحديد خيارات البحث

يظهر الشكل التالي مربع نص الاستعلام والخيارات التي تحدد البحث عن أخبار حول تمويل المدرسة.

خيارات Bing News Search

يتضمن نموذج HTML عناصر بالأسماء التالية:

العنصر الوصف
where قائمة منسدلة لاختيار السوق (الموقع واللغة) المستخدمة للبحث.
query حقل النص لإدخال عبارات البحث.
category مربعات الاختيار لتعزيز أنواع معينة من النتائج. الترويج للصحة، على سبيل المثال، يزيد من ترتيب الأخبار الصحية.
when القائمة المنسدلة لتقيد البحث اختياريًا على آخر يوم أو أسبوع أو شهر.
safe مربع اختيار يشير إلى ما إذا كان سيتم استخدام ميزة Bing SafeSearch لتصفية نتائج "البالغين".
count حقل مخفي. عدد نتائج البحث التي سيتم إرجاعها عند كل طلب. التغيير لعرض نتائج أقل أو أكثر في كل صفحة.
offset حقل مخفي. إزاحة نتيجة البحث الأولى في الطلب، تستخدم لترحيل الصفحات. يتم إعادة تعيينها إلى 0 عند وجود طلب جديد.

ملاحظة

يقدم Bing Web Search معلمات استعلام أخرى. نحن نستخدم القليل منهم فقط.

// build query options from the HTML form
function bingSearchOptions(form) {

    var options = [];
    options.push("mkt=" + form.where.value);
    options.push("SafeSearch=" + (form.safe.checked ? "strict" : "off"));
    if (form.when.value.length) options.push("freshness=" + form.when.value);

    for (var i = 0; i < form.category.length; i++) {
        if (form.category[i].checked) {
            category = form.category[i].value;
            break;
        }
    }
    if (category.valueOf() != "all".valueOf()) { 
        options.push("category=" + category); 
        }
    options.push("count=" + form.count.value);
    options.push("offset=" + form.offset.value);
    return options.join("&");
}

على سبيل المثال، قد تكون المعلمة SafeSearch في استدعاء API الفعلي strict أو moderate أو off مع كون moderate الإعداد الافتراضي. نموذجنا، ومع ذلك، يستخدم مربع الاختيار، الذي يحتوي على حالتين فقط. تقوم تعليمة JavaScript البرمجية بتحويل هذا الإعداد إلى إما strict أو off (moderate غير مستخدم).

تنفيذ الطلب

ونظراً للاستعلام وسلسلة الخيارات ومفتاح API، تستخدم الدالة BingNewsSearch كائن XMLHttpRequest لتقديم الطلب إلى نقطة نهاية Bing News Search.

// perform a search given query, options string, and API key
function bingNewsSearch(query, options, key) {

    // scroll to top of window
    window.scrollTo(0, 0);
    if (!query.trim().length) return false;     // empty query, do nothing

    showDiv("noresults", "Working. Please wait.");
    hideDivs("results", "related", "_json", "_http", "paging1", "paging2", "error");

    var request = new XMLHttpRequest();
     if (category.valueOf() != "all".valueOf()) {
        var queryurl = BING_ENDPOINT + "/search?" + "?q=" + encodeURIComponent(query) + "&" + options;
    }
    else
    {
        if (query){
        var queryurl = BING_ENDPOINT + "?q=" + encodeURIComponent(query) + "&" + options;
        }
        else {
            var queryurl = BING_ENDPOINT + "?" + options;
        }
    }

    // open the request
    try {
        request.open("GET", queryurl);
    } 
    catch (e) {
        renderErrorMessage("Bad request (invalid URL)\n" + queryurl);
        return false;
    }

    // add request headers
    request.setRequestHeader("Ocp-Apim-Subscription-Key", key);
    request.setRequestHeader("Accept", "application/json");
    var clientid = retrieveValue(CLIENT_ID_COOKIE);
    if (clientid) request.setRequestHeader("X-MSEdge-ClientID", clientid);

    // event handler for successful response
    request.addEventListener("load", handleBingResponse);

    // event handler for erorrs
    request.addEventListener("error", function() {
        renderErrorMessage("Error completing request");
    });

    // event handler for aborted request
    request.addEventListener("abort", function() {
        renderErrorMessage("Request aborted");
    });

    // send the request
    request.send();
    return false;
}

عند إكمال طلب HTTP بنجاح، يستدعي JavaScript معالج الأحداث load، والدالة handleBingResponse() لمعالجة طلب HTTP GET ناجح إلى API.

// handle Bing search request results
function handleBingResponse() {
    hideDivs("noresults");

    var json = this.responseText.trim();
    var jsobj = {};

    // try to parse JSON results
    try {
        if (json.length) jsobj = JSON.parse(json);
    } catch(e) {
        renderErrorMessage("Invalid JSON response");
    }

    // show raw JSON and HTTP request
    showDiv("json", preFormat(JSON.stringify(jsobj, null, 2)));
    showDiv("http", preFormat("GET " + this.responseURL + "\n\nStatus: " + this.status + " " + 
        this.statusText + "\n" + this.getAllResponseHeaders()));

    // if HTTP response is 200 OK, try to render search results
    if (this.status === 200) {
        var clientid = this.getResponseHeader("X-MSEdge-ClientID");
        if (clientid) retrieveValue(CLIENT_ID_COOKIE, clientid);
        if (json.length) {
            if (jsobj._type === "News") {
                renderSearchResults(jsobj);
            } else {
                renderErrorMessage("No search results in JSON response");
            }
        } else {
            renderErrorMessage("Empty response (are you sending too many requests too quickly?)");
        }
    }

    // Any other HTTP response is an error
    else {
        // 401 is unauthorized; force re-prompt for API key for next request
        if (this.status === 401) invalidateSubscriptionKey();

        // some error responses don't have a top-level errors object, so gin one up
        var errors = jsobj.errors || [jsobj];
        var errmsg = [];

        // display HTTP status code
        errmsg.push("HTTP Status " + this.status + " " + this.statusText + "\n");

        // add all fields from all error responses
        for (var i = 0; i < errors.length; i++) {
            if (i) errmsg.push("\n");
            for (var k in errors[i]) errmsg.push(k + ": " + errors[i][k]);
        }

        // also display Bing Trace ID if it isn't blocked by CORS
        var traceid = this.getResponseHeader("BingAPIs-TraceId");
        if (traceid) errmsg.push("\nTrace ID " + traceid);

        // and display the error message
        renderErrorMessage(errmsg.join("\n"));
    }
}

هام

طلب HTTP الناجح لا يعني بالضرورة نجاح البحث نفسه. إذا حدث خطأ في عملية البحث، تعود Bing News Search API بالتعليمات البرمجية لحالة HTTP غير 200، وتدرج معلومات الخطأ في استجابة JSON. بالإضافة إلى ذلك، إذا كان الطلب محدود السعر، فإن API تعود باستجابة فارغة.

تُخصص الكثير من التعليمات البرمجية في كل من الدالات السابقة لمعالجة الأخطاء. قد تحدث أخطاء في المراحل التالية:

المرحلة الخطأ (الأخطاء) المحتمل (المحتملة) معالجة بواسطة
إنشاء كائن طلب JavaScript عنوان URL غير صالح حظر try/catch
تقديم الطلب أخطاء الشبكة، الاتصالات التي توقفت قبل الإكمال error وabort معالجات الأحداث
تنفيذ البحث طلب غير صالح، JSON غير صالح، حدود الأسعار اختبارات في معالج الأحداث load

تتم معالجة الأخطاء عن طريق استدعاء renderErrorMessage() باستخدام أية تفاصيل معروفة حول الخطأ. إذا اجتازت الاستجابة تحدي اختبارات الخطأ بالكامل، فإننا نستدعي renderSearchResults() لعرض نتائج البحث في الصفحة.

عرض نتائج البحث

الدالة الرئيسية لعرض نتائج البحث هي renderSearchResults(). تأخذ هذه الدالة JSON التي تم إرجاعها بواسطة خدمة Bing News Search، وتقوم بتقديم نتائج الأخبار، وعمليات البحث ذات الصلة، إن وجدت.

// render the search results given the parsed JSON response
    function renderSearchResults(results) {

    // add Prev / Next links with result count
    var pagingLinks = renderPagingLinks(results);
    showDiv("paging1", pagingLinks);
    showDiv("paging2", pagingLinks);

    showDiv("results", renderResults(results.value));
    if (results.relatedSearches)
        showDiv("sidebar", renderRelatedItems(results.relatedSearches));
}

يتم إرجاع نتائج البحث ككائن المستوى الأعلى value في استجابة JSON. ونقوم بتمريرها إلى دالة renderResults() الخاصة بنا، والتي تتكرر من خلالهم وتستدعي إحدى الدوال لتقديم كل بند في HTML. يتم إرجاع HTML الناتج إلى renderSearchResults()، حيث يتم إدراجه في القسم results بالصفحة.

function renderResults(items) {
    var len = items.length;
    var html = [];
    if (!len) {
        showDiv("noresults", "No results.");
        hideDivs("paging1", "paging2");
        return "";
    }
    for (var i = 0; i < len; i++) {
        html.push(searchItemRenderers.news(items[i], i, len));
    }
    return html.join("\n\n");
}

تعود Bing News Search API بما يصل إلى أربعة أنواع مختلفة من النتائج ذات الصلة، كل منها في كائن المستوى الأعلى الخاص به. وهي كالتالي:

علاقة الوصف
pivotSuggestions الاستعلامات التي تستبدل كلمة محورية في البحث الأصلي بكلمة مختلفة. على سبيل المثال، إذا كنت تبحث عن "زهور حمراء"، فقد تكون الكلمة المحورية "حمراء"، وقد يكون الاقتراح المحوري هو "الزهور الصفراء."
queryExpansions الاستعلامات التي تضيق نطاق البحث الأصلي بإضافة المزيد من المصطلحات. على سبيل المثال، إذا كنت تبحث عن "Microsoft Surface"، فقد يكون توسيع الاستعلام "Microsoft Surface Pro."
relatedSearches الاستعلامات التي تم إدخالها أيضًا من قبل مستخدمين آخرين قاموا بإدخال البحث الأصلي. على سبيل المثال، إذا كنت تبحث عن "جبل رينييه"، فقد يكون البحث ذي الصلة "جبل سانت هيلين."
similarTerms الاستعلامات التي تشبه البحث الأصلي في المعنى. على سبيل المثال، إذا كنت تبحث عن "مدارس"، فقد يكون المصطلح المماثل هو "التعليم."

كما رأينا سابقاً في renderSearchResults()، نحن لا نقدم إلا اقتراحات relatedItems ونضع الارتباطات الناتجة في الشريط الجانبي للصفحة.

تقديم عناصر النتائج

في تعليمات JavaScript البرمجية، يحتوي الكائن searchItemRenderers على دوال renderers: التي تقوم بإنشاء HTML لكل نوع من نتائج البحث.

searchItemRenderers = {
    news: function(item) { ... },
    webPages: function (item) { ... }, 
    images: function(item, index, count) { ... },
    relatedSearches: function(item) { ... }
}

دالة جهاز العرض قد تقبل المعلمات التالية:

المعلمة الوصف
item كائن JavaScript الذي يحتوي على خصائص العنصر، مثل عنوان URL الخاص به ووصفه.
index فهرس عنصر النتيجة ضمن مجموعته.
count عدد العناصر الموجودة في مجموعة عناصر نتائج البحث.

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

يظهر جهاز العرض news في المقتبس التالي من JavaScript.

    // render news story
    news: function (item) {
        var html = [];
        html.push("<p class='news'>");
        if (item.image) {
            width = 60;
            height = Math.round(width * item.image.thumbnail.height / item.image.thumbnail.width);
            html.push("<img src='" + item.image.thumbnail.contentUrl +
                "&h=" + height + "&w=" + width + "' width=" + width + " height=" + height + ">");
        }
        html.push("<a href='" + item.url + "'>" + item.name + "</a>");
        if (item.category) html.push(" - " + item.category);
        if (item.contractualRules) {    // MUST display source attributions
            html.push(" (");
            var rules = [];
            for (var i = 0; i < item.contractualRules.length; i++)
                rules.push(item.contractualRules[i].text);
                html.push(rules.join(", "));
                html.push(")");
            }
        html.push(" (" + getHost(item.url) + ")");
        html.push("<br>" + item.description);
        return html.join("");
    },

وظيفة تقديم الأخبار:

  • إنشاء علامة فقرة وتعيينها إلى الفئة news ودفعها إلى مصفوفة HTML.
  • حساب حجم الصورة المصغرة (يتم إصلاح العرض عند 60 بكسل، ويتم حساب الارتفاع بشكل متناسب).
  • إنشاء علامة HTML <img> لعرض الصورة المصغرة.
  • تُنشئ علامات HTML <a> التي ترتبط بالصورة والصفحة التي تحتوي عليها.
  • إنشاء الوصف الذي يعرض معلومات حول الصورة والموقع الذي تظهر عليه.

يتم استخدام حجم الصورة المصغرة في كل من العلامة <img> والحقول h وw في عنوان URL للصورة المصغرة. ثم تقدم خدمة Bing للصور المصغرة صورة مصغرة بهذا الحجم بالضبط.

معرف العميل الدائم

قد تتضمن الاستجابات من Bing search APIs رأس X-MSEdge-ClientID يجب إرسالها مرة أخرى إلى API مع طلبات متتالية. إذا تم استخدام عدة Bing Search APIs، يجب استخدام معرف العميل نفسه مع كل منهم، إذا كان ذلك ممكنًا.

يسمح توفير الرأس X-MSEdge-ClientID لـ Bing APIs بربط كافة عمليات البحث للمستخدم، الأمر الذي يعود بفائدتين مهمتين.

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

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

قد تمنع نهج أمان المستعرض (CORS) توفر الرأس X-MSEdge-ClientID لـJavaScript. يحدث هذا القيد عندما يكون لاستجابة البحث أصل مختلف عن الصفحة التي طلبت ذلك. في بيئة الإنتاج، تجب عنونة هذا النهج عن طريق استضافة برنامج نصي من جانب الخادم يقوم باستدعاء API على نفس المجال كصفحة ويب. وبما أن البرنامج النصي له نفس الأصل الخاص بصفحة الويب، فإن الرأس X-MSEdge-ClientID يكون متاحًا بعد ذلك لـJavaScript.

ملاحظة

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

لأغراض تتعلق بالتطوير، يمكنك تقديم طلب API Bing Web Search من خلال وكيل CORS. الاستجابة من وكيل لديه رأس Access-Control-Expose-Headers يسمح برؤوس الاستجابة ويجعلها متاحة لـJavaScript.

من السهل تثبيت وكيل CORS للسماح لتطبيق البرنامج التعليمي الخاص بنا بالوصول إلى رأس معرف العميل. أولاً، إذا لم يكن لديك بالفعل، فقم بتثبيت Node.js. ثم قم بإصدار الأمر التالي في نافذة الأمر:

npm install -g cors-proxy-server

بعد ذلك، قم بتغيير نقطة نهاية Bing Web Search في ملف HTML إلى:
http://localhost:9090/https://api.cognitive.microsoft.com/bing/v7.0/search

وأخيرًا، ابدأ تشغيل وكيل CORS باستخدام الأمر التالي:

cors-proxy-server

اترك نافذة الأمر مفتوحة أثناء استخدام تطبيق البرنامج التعليمي؛ إغلاق النافذة يوقف الوكيل. في قسم رؤوس HTTP القابلة للتوسيع أسفل نتائج البحث، يمكنك الآن مشاهدة الرأس X-MSEdge-ClientID (من بين رؤوس أخرى) والتحقق من مطابقتها لكل طلب.

الخطوات التالية