Skip to content

7. تحويل تطبيق Spring MVC إلى Ajax

7.1. مكانة AJAX في تطبيق ويب

في الوقت الحالي، كانت أمثلة التعلم التي تمت دراستها تتمتع بالبنية التالية:

للانتقال من عرض [Vue1] إلى عرض [Vue2]، يقوم المتصفح بما يلي:

  • يرسل طلبًا إلى تطبيق الويب؛
  • يتلقى العرض [Vue2] ويعرضه بدلاً من العرض [Vue1].

هذا هو النمط التقليدي:

  • طلب من المتصفح؛
  • يقوم خادم الويب بإنشاء عرض كاستجابة للعميل؛
  • عرض هذه الصفحة الجديدة بواسطة المتصفح.

هناك منذ بضع سنوات طريقة أخرى للتفاعل بين المتصفح وخادم الويب: AJAX (Asynchronous Javascript And Xml). وهي في الواقع عبارة عن تفاعلات بين العرض الذي يعرضه المتصفح وخادم الويب. يستمر المتصفح في القيام بما يجيده، وهو عرض عرض HTML، لكنه أصبح الآن يخضع للتحكم بواسطة جافا سكريبت مدمج في العرض HTML المعروض. والمخطط هو كما يلي:

  • في [1]، يحدث حدث ما في الصفحة المعروضة في المتصفح (النقر على زر، تغيير نص، ...). يتم اعتراض هذا الحدث بواسطة جافا سكريبت (jS) المضمن في الصفحة؛
  • في [2]، يقوم كود جافا سكريبت بإرسال طلب HTTP كما كان سيفعل المتصفح. الطلب غير متزامن: يمكن للمستخدم الاستمرار في التفاعل مع الصفحة دون أن يتعطل بسبب انتظار الرد على الطلب HTTP. يتبع الطلب عملية المعالجة التقليدية. ولا يوجد ما يميزه (أو يكاد لا يوجد) عن الطلب التقليدي؛
  • في [3]، يتم إرسال استجابة إلى العميل jS. وبدلاً من عرض HTML الكامل، يتم إرسال عرض HTML الجزئي، أو تدفق XML أو jSON (ترميز الكائنات JavaScript
  • في [4]، يسترد جافا سكريبت هذا الرد ويستخدمه لتحديث منطقة من الصفحة المعروضة HTML.

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

7.2. تحديث صفحة باستخدام تدفق HTML

7.2.1. طرق العرض

نقترح دراسة التطبيق التالي:

  • في [1]، وقت تحميل الصفحة؛
  • في [2]، يتم إجراء العمليات الحسابية الأربع على عددين حقيقيين A و B؛
  • في [3]، يتم تسجيل استجابة الخادم في منطقة من الصفحة؛
  • في [4]، وقت إجراء الحساب. ويختلف هذا الوقت عن وقت تحميل الصفحة [5]. وهذا الأخير يساوي [1]، مما يدل على أن المنطقة [6] لم يتم إعادة تحميلها. علاوة على ذلك، لم يتغير URL و[7] في الصفحة.

7.2.2. الإجراء [/ajax-01]

  

يحدد وحدة التحكم [Ajax.java] الإجراء التالي [/ajax-01]:


    @RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax01(Locale locale, Model modèle, HttpSession session, String tempo) {
        // هل التوقيت صالح؟
        if (tempo != null) {
            boolean valide = false;
            int valueTempo = 0;
            try {
                valueTempo = Integer.parseInt(tempo);
                valide = valueTempo >= 0;
            } catch (NumberFormatException e) {

            }
            if (valide) {
                session.setAttribute("tempo", new Integer(valueTempo));
            }
        }
        // يتم إعداد نموذج العرض [vue-01]
        ...
}
  • السطر 2: لا تقبل الإجراء [/ajax-01] سوى معلمة واحدة هي [tempo]. وهي المدة بالمللي ثانية التي يجب على الخادم انتظارها قبل إرسال نتائج العمليات الحسابية؛
  • السطر 4: المعلمة [tempo] اختيارية؛
  • الأسطر 5-12: يتم التحقق من أن قيمة المعلمة [tempo] مقبولة؛
  • الأسطر 13-15: إذا كان الأمر كذلك، يتم تخزين قيمة فترة الانتظار في الجلسة. وهذا يعني أنها ستظل سارية المفعول ما لم يتم تغييرها؛

ويستمر كود الإجراء [/ajax-01] على النحو التالي:


    @RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax01(Locale locale, Model modèle, HttpSession session, String tempo) {
        // هل الإيقاع صالح؟
...
        // جاري إعداد نموذج العرض [vue-01]
        modèle.addAttribute("actionModel01", new ActionModel01());
...
        // عرض
        return "vue-01";
}

تُستخدم الفئة [ActionModel01] بشكل أساسي لتغليف القيم التي يتم إرسالها بواسطة الإجراء [/ajax-01]. هنا، لا يوجد أي شيء يتم إرساله. نقوم بإنشاء فئة فارغة ونضعها في النموذج لأن العرض [vue-01.xml] يستخدمها. الفئة [ActionModel01] هي كما يلي:


package istia.st.springmvc.models;

import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotNull;

public class ActionModel01 {

    // البيانات المرسلة
    @NotNull
    @DecimalMin(value = "0.0")
    private Double a;

    @NotNull
    @DecimalMin(value = "0.0")
    private Double b;

    // وظائف الحصول والتعيين
    ...
}
  • السطران 11 و15: قيمتان عدديتان حقيقيتان [a,b] سيتم إرسالهما عبر نموذج؛

لنعد إلى كود الإجراء:


    @RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax01(Locale locale, Model modèle, HttpSession session, String tempo) {
...
        // يتم إعداد نموذج العرض [vue-01]
        modèle.addAttribute("actionModel01", new ActionModel01());
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
...
        // طريقة العرض
        return "vue-01";
}
  • السطران 6-7: نضع مثيلًا من النوع [Resultats] في القالب؛

النوع [Resultats] الذي تم إدراجه في النموذج هو التالي:

  

package istia.st.springmvc.models;

public class Resultats {

    // البيانات
    private String aplusb;
    private String amoinsb;
    private String amultiplieparb;
    private String adiviseparb;
    private String heureGet;
    private String heurePost;
    private String erreur;
    private String vue;
    private String culture;

    // دالات الاسترجاع والتعيين
    ...
}
  • الأسطر 6-9: نتيجة العمليات الحسابية الأربع على الأرقام [a,b
  • السطر 10: وقت التحميل الأولي للصفحة؛
  • السطر 11: وقت تنفيذ العمليات الحسابية الأربع؛
  • السطر 12: رسالة خطأ محتملة؛
  • السطر 13: العرض الذي يجب عرضه (إن وجد)؛
  • السطر 14: نوع العرض، [fr-FR] أو [en-US

ويستمر كود الإجراء [/ajax-01] على النحو التالي:


    @RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax01(ActionModel01 formulaire, Locale locale, Model modèle, HttpSession session) {
        ...
        // الإعدادات المحلية
        setLocale(locale, modèle, résultats);
...
}
  • السطر 5: تُستخدم الطريقة [setLocale] لإدراج الثقافة المطلوب استخدامها في قالب العرض، [fr-FR] أو [en-US]. هذه الثقافة مخصصة لـ Javascript المضمن في العرض؛

الطريقة [setLocale] هي كما يلي:


    private void setLocale(Locale locale, Model modèle, Resultats résultats) {
        // لا يتم دعم سوى الإعدادات المحلية fr-FR و en-US
        String language = locale.getLanguage();
        String country = null;
        switch (language) {
        case "fr":
            country = "FR";
            break;
        default:
            language = "en";
            country = "US";
            break;
        }
        // الثقافة
        résultats.setCulture(String.format("%s-%s", language, country));
}

في النموذج، ستكون السلسلة [${resultats.culture}] مساوية لـ 'fr-FR' أو 'en-US'.

لنعد إلى الإجراء [/ajax-01]:


@RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax01(ActionModel01 formulaire, Locale locale, Model modèle, HttpSession session) {
...
        // الإعدادات المحلية
        setLocale(locale, modèle, résultats);
        // الوقت
        résultats.setHeureGet(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        // طريقة العرض
        return "vue-01";
    }
  • السطر 7: نضع وقت GET في النموذج؛
  • السطر 9: يتم عرض العرض [vue-01.xml]:

7.2.3. عرض [vue-01.xml]

الطريقة [vue-01.xml] هي كما يلي:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>Ajax-01</title>
        <link rel="stylesheet" href="/css/ajax01.css" />
        <script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="/js/jquery/jquery.validate.min.js"></script>
        <script type="text/javascript" src="/js/jquery/jquery.validate.unobtrusive.min.js"></script>
        <script type="text/javascript" src="/js/jquery/globalize/globalize.js"></script>
        <script type="text/javascript" src="/js/jquery/globalize/cultures/globalize.culture.fr-FR.js"></script>
        <script type="text/javascript" src="/js/jquery/globalize/cultures/globalize.culture.en-US.js"></script>
        <script type="text/javascript" src="/js/jquery/jquery.unobtrusive-ajax.js"></script>
        <script type="text/javascript" src="/js/json3.js"></script>
        <script type="text/javascript" src="/js/client-validation.js"></script>
        <script type="text/javascript" src="/js/local1.js"></script>
        <script th:inline="javascript">
            /*<![CDATA[*/
                    var culture = [[${resultats.culture}]];
                    Globalize.culture(culture);
                    /*]]>*/
        </script>
    </head>
    <body>
        <h2>Ajax - 01</h2>
        <p>
            <strong th:text="#{labelHeureGetCulture(${resultats.heureGet},${resultats.culture})}">
                Heure de chargement :
            </strong>
        </p>
        <h4>
            <p th:text="#{titre.part1}">
                Opérations arithmétiques sur deux nombres réels A et B positifs ou nuls
            </p>
        </h4>
        <form id="formulaire" name="formulaire" ... ">
...
        </form>
        <hr />
        <div id="resultats" />
    </body>
</html>
  • الأسطر 7-12: مكتبات jQuery الخاصة بالتحقق من الصحة والتدويل (الثقافات)؛
  • السطر 15: مكتبة [client-validation] التي تم إنشاؤها في الفقرة 6.3؛
  • السطر 14: المكتبة jSON التي تستخدمها المكتبة [client-validation]. وهي اختيارية إذا تم تعطيل سجلات التحقق من الصحة؛
  • السطر 13: مكتبة [Unobtrusive Ajax] من Microsoft. تسمح هذه المكتبة أحيانًا بتجنب كتابة جافا سكريبت؛
  • السطر 16: ملف jS لتلبية احتياجاتنا الخاصة؛
  • الأسطر 17-22: لإدارة الإعدادات المحلية [fr-FR] و [en-US] من جانب العميل. وقد سبق أن تناولنا هذا الكود؛
  • السطر 27: رسالة معدة مسبقًا. وقد درسناها في الفقرة 5.18؛
  • الأسطر 36-38: النموذج الذي سنعود إليه لاحقًا؛
  • السطر 40: منطقة المستند التي سيضع فيها جافا سكريبت استجابة الخادم؛

7.2.4. النموذج

 

في العرض [vue-01.xml]، يكون النموذج كما يلي:


<form id="formulaire" name="formulaire" th:action="@{/ajax-02.html}" method="post" th:object="${actionModel01}" th:attr="data-ajax='true',data-ajax-loading='#loading',data-ajax-loading-duration='0',data-ajax-method='post',data-ajax-mode='replace',data-ajax-update='#resultats', data-ajax-begin='beforeSend',data-ajax-complete='afterComplete' ">
    <table>
        <thead>
            <tr>
                <th>
                    <span th:text="#{valeur.a}"></span>
                </th>
                <th>
                    <span th:text="#{valeur.b}"></span>
                </th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>
                    <input type="text" th:field="*{a}" th:value="*{a}" data-val="true"
                        th:attr="data-val-required=#{NotNull},data-val-number=#{typeMismatch},data-val-min=#{actionModel01.a.min},data-val-min-value=#{actionModel01.a.min.value}" />
                </td>
                <td>
                    <input type="text" th:field="*{b}" th:value="*{b}" data-val="true"
                        th:attr="data-val-required=#{NotNull},data-val-number=#{typeMismatch},data-val-min=#{actionModel01.b.min},data-val-min-value=#{actionModel01.b.min.value}" />
                </td>
            </tr>
            <tr>
                <td>
                    <span class="field-validation-valid" data-valmsg-for="a" data-valmsg-replace="true"></span>
                    <span th:if="${#fields.hasErrors('a')}" th:errors="*{a}" class="error">Donnée
                        erronée
                    </span>
                </td>
                <td>
                    <span class="field-validation-valid" data-valmsg-for="b" data-valmsg-replace="true"></span>
                    <span th:if="${#fields.hasErrors('b')}" th:errors="*{b}" class="error">Donnée
                        erronée
                    </span>
                </td>
            </tr>
        </tbody>
    </table>
    <p>
        <input type="submit" th:value="#{action.calculer}" value="Calculer"></input>
        <img id="loading" style="display: none" src="/images/loading.gif" />
        <a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>
    </p>
</form>

الذي ينتج HTML التالي:


<form id="formulaire" name="formulaire" method="post" data-ajax-update="#resultats" data-ajax-complete="afterComplete"     data-ajax-begin="beforeSend" data-ajax-loading-duration="0" data-ajax-mode="replace" data-ajax="true" data-ajax-method="post" data-ajax-loading="#loading" action="/ajax-02.html">
    <table>
        <thead>
            <tr>
                <th>
                    <span>valeur de A</span>
                </th>
                <th>
                    <span>valeur de B</span>
                </th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>
                    <input type="text" data-val="true" data-val-min="Le nombre doit être supérieur ou égal à 0" data-val-number="Format invalide" data-val-min-value="0" data-val-required="Le champ est obligatoire" value="" id="a" name="a" />
                </td>
                <td>
                    <input type="text" data-val="true" data-val-min="Le nombre doit être supérieur ou égal à 0" data-val-number="Format invalide" data-val-min-value="0" data-val-required="Le champ est obligatoire" value="" id="b" name="b" />
                </td>
            </tr>
            <tr>
                <td>
                    <span class="field-validation-valid" data-valmsg-for="a" data-valmsg-replace="true"></span>

                </td>
                <td>
                    <span class="field-validation-valid" data-valmsg-for="b" data-valmsg-replace="true"></span>

                </td>
            </tr>
        </tbody>
    </table>
    <p>
        <input type="submit" value="Calculer" />
        <img id="loading" style="display: none" src="/images/loading.gif" />
        <a href="javascript:postForm()">Calculer</a>
    </p>
</form>
  • السطر 16: يرتبط الحقل [a] بأدوات التحقق من الصحة [required] و [number] و [min]؛
  • السطر 19: الأمر نفسه بالنسبة للحقل [b]؛

توجد الرسائل المختلفة في ملفات [messages.properties] الخاصة بالمشروع:

  

[messages_fr.properties]


NotNull=Le champ est obligatoire
typeMismatch=Format invalide
actionModel01.a.min=Le nombre doit être supérieur ou égal à 0
DecimalMin.actionModel01.a=Le nombre doit être supérieur ou égal à 0
DecimalMax.actionModel01.b=Le nombre doit être supérieur ou égal à 0
actionModel01.b.min=Le nombre doit être supérieur ou égal à 0
valeur.a=valeur de A
valeur.b=valeur de B
actionModel01.a.min.value=0
actionModel01.b.min.value=0
labelHeureCalcul=Heure de calcul : 
LabelErreur=Une erreur s''est produite : [{0}]
labelAplusB=A+B=
labelAmoinsB=A-B=
labelAfoisB=A*B=
labelAdivB=A/B=
titre.part1=Opérations arithmétiques sur deux nombres réels A et B positifs ou nuls
labelHeureGetCulture=Heure de chargement : [{0}], culture : [{1}]
action.calculer=Calculer
erreur.aleatoire=erreur aléatoire
resultats=Résultats
resultats.erreur=Une erreur s''est produite : [{0}]
resultats.titre=Résultats
message.zone=Nombre d'accès : 

[messages_en.properties]


NotNull=Required field
typeMismatch=Invalid format
actionModel01.a.min=The number must be greater or equal to 0
DecimalMin.actionModel01.a=The number must be greater or equal to 0
DecimalMax.actionModel01.b=The number must be greater or equal to 0
actionModel01.b.min=The number must be greater or equal to 0
valeur.a=A value
valeur.b=B value
actionModel01.a.min.value=0
actionModel01.b.min.value=0
labelHeureCalcul=Computing hour: 
LabelErreur=There was an error: [{0}]
labelAplusB=A+B=
labelAmoinsB=A-B=
labelAfoisB=A*B=
labelAdivB=A/B=
titre.part1=Arithmetic operations on two positive or equal to zero real numbers
labelHeureGetCulture=Loading hour: [{0}], culture: [{1}]
action.calculer=Calculate
erreur.aleatoire=randomly generated error
resultats=Results
resultats.erreur=Some error occurred : [{0}]
resultats.titre=Results
message.zone=Number of hits:

الآن، دعونا ندرس سمات العلامة [form]:


<form id="formulaire" name="formulaire" method="post" data-ajax-update="#resultats" data-ajax-complete="afterComplete" data-ajax-begin="beforeSend" data-ajax-loading-duration="0" data-ajax-mode="replace" data-ajax="true" data-ajax-method="post" data-ajax-loading="#loading" action="/ajax-02.html">

يمكننا التعرف على السمات التقليدية لعلامة [form]:


<form id="formulaire" name="formulaire" method="post" action="/ajax-02.html">

يمكن ملاحظة على الفور أنه إذا كان جافا سكريبت معطلاً في المتصفح الذي يعرض الصفحة، فسيتم إرسال النموذج إلى URL [/ajax-02.html]. والآن، دعونا نحلل السمات الأخرى:


<form ... data-ajax-update="#resultats" data-ajax-complete="afterComplete" data-ajax-begin="beforeSend" data-ajax-loading-duration="0" data-ajax-mode="replace" data-ajax="true" data-ajax-method="post" data-ajax-loading="#loading">

يتم إدارة السمات [data-ajax-xxx] بواسطة المكتبة jS [unobtrusive-ajax] التي تم استيرادها بواسطة العرض [vue-01.xml]:


<script type="text/javascript" src="/js/jquery/jquery.unobtrusive-ajax.js"></script>

عند وجود سمات [data-ajax-xxx]، سيتم تنفيذ [submit] الخاص بالنموذج عن طريق استدعاء Ajax من المكتبة [unobtrusive-ajax]. وفيما يلي معنى المعلمات:

  • [data-ajax="true"]: وجود هذه السمة هو ما يؤدي إلى تنفيذ وظيفة [submit] الخاصة بالنموذج عبر Ajax؛
  • [data-ajax-method="post"]: طريقة [submit]. وسيكون URL الخاص بعملية الإرسال هو نفس قيمة السمة [action="/ajax-02.html"]؛
  • [data-ajax-loading="#loading"]: معرّف الحقل المراد عرضه أثناء انتظار استجابة الخادم. الحقل المحدد بواسطة [loading] في العرض [vue-01.xml] هو التالي:

<img id="loading" style="display: none" src="/images/loading.gif" />

وهي صورة متحركة للانتظار سيتم عرضها إلى أن يتم استلام استجابة الخادم؛

  • [data-ajax-loading-duration="0"]: مدة الانتظار بالمللي ثانية قبل عرض المنطقة [data-ajax-loading="#loading"]. هنا، سيتم عرضها فور بدء الانتظار؛
  • [data-ajax-begin="beforeSend"]: الدالة jS التي يجب تنفيذها قبل تنفيذ [submit
  • [data-ajax-complete="afterComplete"]: الدالة jS التي يجب تنفيذها عند استلام الرد؛
  • [data-ajax-update="#resultats"]: معرّف المنطقة التي سيتم وضع النتيجة المرسلة من الخادم فيها. تحتوي طريقة العرض [vue-01.xml] على المنطقة التالية:

<div id="resultats" />
  • [data-ajax-mode="replace"]: طريقة إدراج النتيجة في الحقل السابق. أما في الوضع [replace]، فستؤدي النتيجة إلى «الكتابة فوق» ما كان موجودًا سابقًا في الحقل ذي المعرّف [resultats

تجدر الإشارة إلى أن جافا سكريبت [submit] لن يتم تنفيذه إلا إذا أعلنت أدوات التحقق من الصحة أن القيم التي تم اختبارها صالحة.

تهدف مكتبة jS [unobtrusive-ajax] إلى تحقيق هدفين:

  • ضمان أن يتكيف النموذج بشكل صحيح مع الحالتين: تفعيل جافا سكريبت أو عدم تفعيله في المتصفح؛
  • تجنب كتابة جافا سكريبت. سنرى هنا أنه لم يكن بالإمكان تجنب ذلك.

7.2.5. الإجراء [/ajax-02]

لقد رأينا أن القيم المرسلة كانت تُرسل إلى الإجراء [/ajax-02]. وهذا الإجراء هو كما يلي:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(ActionModel01 formulaire, Locale locale, Model modèle, HttpSession session) throws InterruptedException {
        // الإيقاع؟
        Integer tempo = (Integer) session.getAttribute("tempo");
        if (tempo != null && tempo > 0) {
            Thread.sleep(tempo);
        }
        // يتم إعداد قالب المنظر التالي
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
        // يتم تحديد الإعدادات المحلية
        setLocale(locale, modèle, résultats);
        // الساعة
        résultats.setHeurePost(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        ...
}
  • سنقوم أولاً بالتبسيط: نفترض أن الإجراء POST الذي يتم تنفيذه قد تم بالفعل بواسطة جافا سكريبت الخاص بالعرض [vue-01.xml]. سنعود إلى هذه الفرضية لاحقًا؛
  • السطر 2: يتم وضع القيم المرسلة لـ [a,b] في النموذج [ActionModel01
  • الأسطر 4-7: إذا كان المستخدم قد حدد فترة انتظار في عملية GET سابقة، يتم استردادها من الجلسة ويتم تنفيذ فترة الانتظار (السطر 6). والغرض من ذلك هو السماح للمستخدم برؤية تأثير السمة [data-ajax-loading="#loading"] في النموذج؛
  • السطران 9-10: يتم وضع السمة [resultats] في القالب؛
  • السطر 12: يتم إدراج الإعداد الثقافي [fr-FR] أو [en-US] في النموذج؛
  • السطر 14: يتم إدخال وقت POST في النموذج؛

لنتذكر النوع [Resultats] الذي تم إدراجه في النموذج:


public class Resultats {

    // البيانات
    private String aplusb;
    private String amoinsb;
    private String amultiplieparb;
    private String adiviseparb;
    private String heureGet;
    private String heurePost;
    private String erreur;
    private String vue;
    private String culture;

    // وظائف الاسترجاع والتعيين
...
}

ويستمر رمز الإجراء [/ajax-02] على النحو التالي:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle,    HttpSession session) throws InterruptedException {
...
        résultats.setHeurePost(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        // يتم إنشاء خطأ مرة كل مرتين
        int val = new Random().nextInt(2);
        if (val == 0) {
            // إرجاع رسالة خطأ
            résultats.setErreur("erreur.aleatoire");
            return "vue-03";
        }
...
    }
  • الأسطر 6-11: في هذا المثال، نوضح كيفية إرجاع صفحة خطأ إلى العميل jS. في نصف الحالات، يتم إرجاع العرض التالي [vue-03.xml]:

يُلاحظ في السطر 9 أنه ليس رسالة يتم إدراجها في النموذج، بل مفتاح رسالة:

[messages_fr.properties]


erreur.aleatoire=erreur aléatoire

[messages_fr.properties]


erreur.aleatoire=randomly generated error

رمز العرض [vue-03.xml] هو كما يلي:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h4>Résultats</h4>
        <p>
            <strong>
                <span th:text="#{labelHeureCalcul}">Heure de calcul :</span>
                <span id="heureCalcul" th:text="${resultats.heurePost}"></span>
            </strong>
        </p>
        <p style="color: red;">
            <span th:text="#{LabelErreur(#{${resultats.erreur}})}">Une erreur s'est produite :</span>
            <!-- <span id="erreur" th:text="${resultats.erreur}"></span> -->
        </p>
    </body>
</html>

  • في السطر 12، نلاحظ رسالة تم تعيين معلماتها بواسطة مفتاح رسالة يتم حسابه بدوره. وقد تناولنا هذا المفهوم في الفقرة 5.18، الصفحة 171.

ويستمر رمز الإجراء [/ajax-02] على النحو التالي:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle,    HttpSession session) throws InterruptedException {
...
        // يتم استرداد القيم المرسلة
        double a = formulaire.getA();
        double b = formulaire.getB();
        // يتم إنشاء النموذج
        résultats.setAplusb(String.valueOf(a + b));
        résultats.setAmoinsb(String.valueOf(a - b));
        résultats.setAmultiplieparb(String.valueOf(a * b));
        try {
            résultats.setAdiviseparb(String.valueOf(a / b));
        } catch (RuntimeException e) {
            résultats.setAdiviseparb("NaN");
        }
        // يتم عرض العرض
        return "vue-02";
    }
  • الأسطر 5-15: تُجرى العمليات الحسابية الأربع على الأرقام [a,b] وتُغلف في مثيل [Resultats] للنموذج؛
  • السطر 17: يتم إرجاع العرض التالي [vue-02.xml]:

الطريقة [vue-02.xml] هي كما يلي:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h4>Résultats</h4>
        <p>
            <strong>
                <span th:text="#{labelHeureCalcul}">Heure de calcul :</span>
                <span id="heureCalcul" th:text="${resultats.heurePost}"></span>
            </strong>
        </p>
        <p>
            <span th:text="#{labelAplusB}">A+B=</span>
            <span id="aplusb" th:text="${resultats.aplusb}"></span>
        </p>
        <p>
            <span th:text="#{labelAmoinsB}">A-B=</span>
            <span id="amoinsb" th:text="${resultats.amoinsb}"></span>
        </p>
        <p>
            <span th:text="#{labelAfoisB}">A*B=</span>
            <span id="amultiplieparb" th:text="${resultats.amultiplieparb}"></span>
        </p>
        <p>
            <span th:text="#{labelAdivB}">A/B=</span>
            <span id="adiviseparb" th:text="${resultats.adiviseparb}"></span>
        </p>
    </body>
</html>

سواء كانت النتيجة هي العرض [vue-02.xml] أو العرض [vue-03.xml]، يتم وضع هذه النتيجة HTML في الحقل المحدد بـ [resultats] في العرض [vue-01.xml]، وذلك بسبب السمة [data-ajax-update="#resultats"] الخاصة بالنموذج.

7.2.6. القيمة POST للقيم التي تم إدخالها

نواجه هنا صعوبة تتعلق بالقيم التي تم إدخالها. نحن نعمل مع ثقافتين هما [fr-FR] و [en-US] اللتين تكتبان الأعداد الحقيقية بطرق مختلفة. وقد تناولنا هذه الصعوبة سابقًا عندما تطلب الأمر، في الفقرة 6.3، الصفحة 191، إدخال أعداد حقيقية في ثقافتين مختلفتين. سنستخدم هنا الأدوات التي استخدمناها آنذاك. لكننا نواجه صعوبة إضافية: لا يمكننا الوصول إلى الطريقة التي تقوم بتحويل القيم المدخلة إلى تنسيق POST. ولهذا السبب، أضفنا السمات التالية إلى علامة النموذج:

  • [data-ajax-begin="beforeSend"]: الدالة jS التي يجب تنفيذها قبل إجراء [submit
  • [data-ajax-complete="afterComplete"]: الدالة jS التي يجب تنفيذها عند استلام الرد؛

لا يمكننا الوصول إلى الدالة jS التي ستقوم بإرسال القيم المدخلة، ولكن يمكننا كتابة دالتين jS:

  • [beforeSend]: دالة jS تُنفَّذ قبل الدالة POST؛
  • [afterComplete]: دالة jS تُنفَّذ عند استلام الرد على الدالة POST؛

يتم وضع هاتين الوظيفتين في ملف [local1.js]:

  

يقوم الملف [local1.js] بتهيئة بيئة jS الخاصة بعرض [vue-01.xml] بالطريقة التالية:


// البيانات العامة
var loading;
var formulaire;
var résultats;
var a, b;

// عند تحميل المستند
$(document).ready(function() {
    // يتم استرداد مراجع المكونات المختلفة للصفحة
    loading = $("#loading");
    formulaire = $("#formulaire");
    resultats = $('#النتائج');
    a = $("#a");
    b = $("#b");
    // يتم إخفاء بعض العناصر
    loading.hide();
    // تحليل مدققات صحة النموذج
    $.validator.unobtrusive.parse(formulaire);
    // تتم إدارة لغتين محليتين [fr_FR, en_US]
    // يتم إرسال القيم الفعلية [a,b] من الخادم بالتنسيق الأنجلوساكسوني
    // يتم تحويلها إلى التنسيق الفرنسي إذا لزم الأمر
    checkCulture(2);
});
  • السطر 22: يتم عرض الدالة [checkCulture] لاحقًا؛

وستكون الدالة jS [beforeSend] كما يلي:


function beforeSend(jqXHR, settings) {
    // قبل POST
    // يجب إرسال الأرقام بالصيغة الأنجلوساكسونية
    var culture = Globalize.culture().name;
    if (culture === 'fr-FR') {
        checkCulture(1);
        settings.data = formulaire.serialize();
    }
}

function afterComplete(jqXHR, settings) {
    ...
}

function checkCulture(mode) {
    if (mode == 1) {
        // نقوم بتحويل الأرقام [a,b] إلى التنسيق الأنجلوساكسوني
        var value1 = a.val().replace(",", ".");
        a.val(value1);
        var value2 = b.val().replace(",", ".");
        b.val(value2);
    }
    if (mode == 2) {
...
    }
}
  • الأسطر 4-6: يتم التحقق مما إذا كانت ثقافة العرض هي [fr-FR]. في هذه الحالة، يجب تغيير القيم المرسلة. فعليًّا، إذا أدخل المستخدم [1,6]، فيجب إرسال القيمة [1.6]، وإلا فسيتم رفض القيمة [1,6] من جانب الخادم. ويكفي لذلك تغيير الفاصلة في القيم المرسلة إلى نقطة عشرية (الأسطر 18-21)؛
  • ولكن لا يمكن الاكتفاء بذلك. ففي الواقع، عند استدعاء الدالة [beforeSend]، تكون سلسلة القيم المرسلة [a=val1&b=valB] قد تم تكوينها بالفعل. لذا يتعين علينا تعديلها. ويتم ذلك باستخدام المعلمة الثانية للدالة [settings
  • السطر 7: [settings.data] (settings هو معلمة من معلمات الدالة) تمثل السلسلة التي تم إرسالها. نقوم بإعادة إنشاء هذه السلسلة باستخدام التعبير [formulaire.serialize()]. يقوم هذا التعبير بمسح النموذج بحثًا عن القيم المطلوب إرسالها ويقوم ببناء سلسلة POST. ثم سيأخذ القيم الجديدة لـ [a,b] مع النقاط العشرية؛

وإذا لم يتم اتخاذ أي إجراء إضافي، فسيرسل الخادم ردّه الذي سيُعرض بشكل صحيح. إلا أن قيم [a,b] أصبحت الآن تحتوي على الفاصلة العشرية، في حين أننا ما زلنا في الإعداد الثقافي لـ [fr-FR]. لذا، إذا لم يلاحظ المستخدم ذلك ونقر مرة أخرى على [Calculer]، فسترد عليه أدوات التحقق بأن قيم [a,b] غير صالحة. وهذا صحيح. وهنا تدخل الدالة [afterComplete] التي يتم تنفيذها عند استلام النتيجة:


function beforeSend(jqXHR, settings) {
    // قبل POST
...
}

function afterComplete(jqXHR, settings) {
    // بعد POST
    // يجب إعادة الأرقام إلى التنسيق الفرنسي إذا لزم الأمر
    var culture = Globalize.culture().name;
    if (culture === 'fr-FR') {
        checkCulture(2);
    }
}

function checkCulture(mode) {
    if (mode == 1) {
...
    }
    if (mode == 2) {
        // يتم تحويل الأرقام إلى التنسيق الفرنسي
        var value1 = a.val().replace(".", ",");
        a.val(value1);
        var value2 = b.val().replace(".", ",");
        b.val(value2);
    }
}
  • الأسطر 9-12: إذا كانت ثقافة العرض هي [fr-FR]، يتم تحويل الأرقام [a,b] إلى التنسيق الفرنسي.

7.2.7. الاختبارات

فيما يلي بعض لقطات الشاشة للاختبارات:

  • في [1]، رد الخادم؛
  • في [2]، رد الخادم مع رسالة خطأ؛
  • في [3]، يتم تعيين مهلة قدرها 5 ثوانٍ. وهذا يعني أن الخادم سينتظر 5 ثوانٍ قبل إرسال استجابته. في العلامة [form]، استخدمنا السمة [data-ajax-loading='#loading']. المعلمة [loading] هي معرّف منطقة يتم:
    • تُعرض طوال فترة الانتظار؛
    • يتم إخفاؤها بعد استلام الرد من الخادم؛

هنا، [loading] هو معرّف صورة متحركة تظهر في [4].

7.2.8. تعطيل جافا سكريبت مع القالب [en-US]

ماذا يحدث إذا تم تعطيل جافا سكريبت في المتصفح؟

سيتم معالجة القيم المدخلة وفقًا لعلامة POST، حيث لن يتم استخدام سمات [data-ajax-attr] الخاصة بها. وسيكون الأمر كما لو كانت لدينا العلامة التالية: [form]:


<form id="formulaire" name="formulaire" method="post" action="/ajax-02.html">

وبالتالي، سيتم إرسال القيم المدخلة إلى الإجراء [/ajax-02]. ولن يتم التحقق منها من جانب العميل. لذا، ستتدخل أدوات التحقق من صحة البيانات من جانب الخادم. كانت هذه الأدوات تتدخل من قبل، ولكن على قيم تم التحقق من صحتها مسبقًا من جانب العميل، وبالتالي كانت صحيحة. لم يعد هذا هو الحال الآن.

نقوم بتعديل الإجراء [/ajax-02] على النحو التالي:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle,    HttpSession session, HttpServletRequest request) throws InterruptedException {
        // طلب Ajax؟
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        ...
    }
  • السطر 4: يمكن الآن استدعاء الإجراء [/ajax-02] عبر POST Ajax أو عبر POST التقليدي. علينا التمييز بين هاتين الحالتين. ونقوم بذلك من خلال رؤوس HTTP التي يرسلها متصفح العميل؛

وعند مراقبة حركة مرور الشبكة في وحدة التحكم في التطوير في متصفح Chrome (Ctrl-Shift-I) مع تفعيل جافا سكريبت، نلاحظ أن العميل يرسل الرؤوس التالية عند تنفيذ POST:

ونلاحظ أعلاه ما يلي:

  • تم إرسال رأس [X-Requested-With] مع [1]؛
  • تمت إضافة المعلمة [X-Requested-With] إلى القيم المرسلة [2]؛

لا يحدث هذا في حالة POST التقليدي. لذلك، لدينا خياران لاسترداد المعلومات: استردادها من الرؤوس HTTP أو من القيم المنشورة. وقد اختار السطر 4 من الإجراء [/ajax-02] الحل الأول.

لنواصل مع كود هذه العملية:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle, HttpSession session, HttpServletRequest request) throws InterruptedException {
        // طلب Ajax؟
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        // الوقت؟
        Integer tempo = (Integer) session.getAttribute("tempo");
        if (tempo != null && tempo > 0) {
            Thread.sleep(tempo);
        }
        // يتم إعداد قالب العرض التالي
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
        // يتم تعيين الإعدادات المحلية
        setLocale(locale, modèle, résultats);
        // الساعة
        String heure = new SimpleDateFormat("hh:mm:ss").format(new Date());
        résultats.setHeurePost(heure);
        résultats.setHeureGet(heure);
        // هل الطلب صالح؟
        if (!isAjax && result.hasErrors()) {
            return "vue-01";
        }
...
  • السطر 2: المعلمة [@Valid ActionModel01 formulaire] تُشغّل أدوات التحقق من صحة البيانات على جانب الخادم؛
  • الأسطر 20-22: إذا لم يكن الاستدعاء استدعاءً من نوع Ajax وفشلت عملية التحقق من الصحة، يتم إرجاع العرض [vue-01.xml] مع رسائل الخطأ.

فيما يلي مثال:

لنواصل دراسة الإجراء [/ajax-02]:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle,    HttpSession session, HttpServletRequest request) throws InterruptedException {
        // هل الطلب من Ajax؟
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
        // هل الطلب صالح؟
        if (!isAjax && result.hasErrors()) {
            return "vue-01";
        }
        // يتم إنشاء خطأ في كل مرة من كل مرتين
        int val = new Random().nextInt(2);
        if (val == 0) {
            // يتم إرجاع رسالة خطأ
            résultats.setErreur("erreur.aleatoire");
            if (isAjax) {
                return "vue-03";
            } else {
                résultats.setVue("vue-03");
                return "vue-01";
            }
        }
...
  • السطر 14: يتم إنشاء خطأ عشوائي؛
  • السطر 16: في حالة استدعاء Ajax، يتم إرجاع العرض [vue-03.xml] الذي سيتم وضعه في المنطقة المحددة بواسطة [resultats
  • السطر 18: في حالة استدعاء غير Ajax، يتم وضع العرض المراد عرضه في النموذج من النوع [Resultats
  • السطر 19: يتم عرض العرض [vue-01.xml] مرة أخرى؛

يتم تعديل العرض [vue-01.xml] على النحو التالي:


<div id="resultats" />
<div th:if="${resultats.vue}=='vue-02'" th:include="vue-02" />
<div th:if="${resultats.vue}=='vue-03'" th:include="vue-03" />
  • السطر 3: سيتم إدراج العرض [vue-03.xml] أسفل المنطقة [resultats

فيما يلي مثال:

تجدر الإشارة إلى أن الساعتين [1] و [2] أصبحتا متطابقتين الآن.

لنواصل دراسة الإجراء [/ajax-02]:


    @RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle, HttpSession session, HttpServletRequest request) throws InterruptedException {
        // طلب Ajax؟
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
        // يتم استرداد القيم المرسلة
        double a = formulaire.getA();
        double b = formulaire.getB();
        // يتم إنشاء النموذج
        résultats.setAplusb(String.valueOf(a + b));
        résultats.setAmoinsb(String.valueOf(a - b));
        résultats.setAmultiplieparb(String.valueOf(a * b));
        try {
            résultats.setAdiviseparb(String.valueOf(a / b));
        } catch (RuntimeException e) {
            résultats.setAdiviseparb("NaN");
        }
        // عرض العرض
        if (isAjax) {
            return "vue-02";
        } else {
            résultats.setVue("vue-02");
            return "vue-01";
        }
}
  • الأسطر 7-17: يتم إدراج نتائج العمليات الحسابية الأربع في النموذج؛
  • الأسطر 22-23: يتم عرض القسم [vue-01.xml] (السطر 22) عن طريق إدراج القسم [vue-02.xml] (السطر 22) فيه؛

ويتم هذا الإدراج في [vue-01.xml] على النحو التالي:


<div id="resultats" />
<div th:if="${resultats.vue}=='vue-02'" th:include="vue-02" />
<div th:if="${resultats.vue}=='vue-03'" th:include="vue-03" />
  • السطر 2: سيتم إدراج العرض [vue-02.xml] أسفل المنطقة [resultats

فيما يلي مثال على التنفيذ:

 

7.2.9. تعطيل جافا سكريبت باستخدام الإعداد الثقافي [fr-FR]

مع الإعداد [fr-FR]، نواجه المشكلة التالية:

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

  • اعتراض الطلب؛
  • تغيير الفواصل في القيم المرسلة [a] و [b] إلى نقطة عشرية؛
  • ثم تمرير الطلب الجديد إلى الإجراء الذي يجب أن يعالجه؛

أولاً، نضيف حقلًا مخفيًّا في العرض [vue-01.xml]:


<form ...>
...
</p>
    <!-- الحقول المخفية -->
    <input type="hidden" id="culture" name="culture" th:value="${resultats.culture}"></input>
</form>
  • السطر 5: يتم وضع القيمة [fr-FR] أو [en-US] في حقل السمة [name=culture]. ونظرًا لوجود العلامة [input] في النموذج، فسيتم إرسال قيمتها مع قيمتي [a] و [b]. وبذلك نحصل على سلسلة مرسلة بالشكل التالي:
culture=fr-FR&a=12,7&b=20,78

من المهم فهم هذه النقطة.

بعد ذلك، نقوم بتضمين مرشح في تكوين التطبيق:

  

تم تعديل الملف [Config] على النحو التالي:


@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
...
    @Bean
    public Filter cultureFilter() {
        return new CultureFilter();
    }
}
  • السطر 7: إن كون الكائن [cultureFilter] يُرجع نوعًا [Filter] يجعله مرشحًا. أما الكائن نفسه، فيمكن أن يحمل أي اسم؛

الخطوة التالية هي إنشاء المرشح نفسه:

  

package istia.st.springmvc.config;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.filter.OncePerRequestFilter;

public class CultureFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        // المعالج التالي
        filterChain.doFilter(new CultureRequestWrapper(request), response);
    }
}
  • السطر 12: نقوم بتوسيع فئة [OncePerRequestFilter]، وهي فئة Spring، وما يتعين علينا فعله هو إعادة تعريف الطريقة [doFilterInternal] لهذه الفئة؛
  • السطر 15: تتلقى الطريقة [doFilterInternal] ثلاث معطيات:
    • [HttpServletRequest request]: الاستعلام المراد تصفيته. لا يمكن تعديل هذا الاستعلام،
    • [HttpServletResponse response]: الرد الذي سيتم إرساله إلى الخادم. يمكن للمرشح أن يقرر إرساله بنفسه،
    • [FilterChain filterChain]: سلسلة المرشحات. بمجرد أن تنتهي الطريقة [doFilterInternal] من عملها، يجب عليها تمرير الطلب إلى المرشح التالي في سلسلة المرشحات؛
  • السطر 18: يتم إنشاء طلب جديد استنادًا إلى الطلب الذي تم استلامه [new CultureRequestWrapper(request)] وتمريره إلى المرشح التالي. ونظرًا لأنه لا يمكن تعديل الطلب الأصلي [HttpServletRequest request]، يتم إنشاء طلب جديد؛

الفئة [CultureRequestWrapper] هي كما يلي:

  

package istia.st.springmvc.config;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class CultureRequestWrapper extends HttpServletRequestWrapper {

    public CultureRequestWrapper(HttpServletRequest request) {
        super(request);
    }

    @Override
    public String[] getParameterValues(String name) {
        // القيم المرسلة a و b
        if (name != null && (name.equals("a") || name.equals("b"))) {
            String[] values = super.getParameterValues(name);
            String[] newValues = values.clone();
            newValues[0] = newValues[0].replace(",", ".");
            return newValues;
        }
        // حالات أخرى
        return super.getParameterValues(name);
    }

}
  • السطر 6: الفئة [CultureRequestWrapper] تمتد من الفئة [HttpServletRequestWrapper] وستعيد تعريف بعض هذه الطرق؛
  • الأسطر 8-10: المنشئ الذي يستقبل الطلب المراد تصفيته ويمرره إلى الفئة الأم؛
  • يجب أن نفهم هنا أن الطلب المُصفى سيصبح في النهاية معلمة إدخال لفئة تُسمى «سيرفلت». مع Spring MVC، تكون هذه السيرفلت من النوع [DispatcherServlet]. تحتوي هذه الفئة على طرق متنوعة لاسترداد معلمات الطلب: [getParameter, getParameterMap, getParameterNames, getParameterValues, ...]. يلزم إعادة تعريف الطريقة التي تستخدمها السيرفلت. ولهذا الغرض، ينبغي قراءة كود الفئة [DispatcherServlet]. لم أقم بذلك وقمت بإعادة تعريف طرق متنوعة. وفي النهاية، تمت إعادة تعريف الطريقة [getParameterValues
  • السطر 13: تتلقى الطريقة [getParameterValues] كمعلمة اسم أحد المعلمات التي ترجعها الطريقة [getParameterNames]، ويجب أن ترجع مصفوفة قيمها. ففي الواقع، من المعروف أن المعلمة الواحدة قد تظهر بعدة نسخ في الطلب الواحد؛
  • السطر 18: يتم استبدال الفاصلة بعلامة عشرية؛

فيما يلي مثال على التنفيذ:

  • في [1]، يتم إدخال قيم [a,b] بالتنسيق الفرنسي؛
  • في [2]، النتائج؛
  • في [3]، أرسل الخادم صفحة تحتوي على أرقام بالتنسيق الأنجلوساكسوني.

يمكن حل هذه المشكلة الأخيرة باستخدام Thymeleaf بالطريقة التالية في العرض [vue-01.xml]


<tr>
    <td>
        <input type="text" id="a" name="a"    th:value="${resultats.culture}=='fr-FR' and ${actionModel01.a}!=null? ${#strings.replace(actionModel01.a,'.',',')} : ${actionModel01.a}" data-val="true" th:attr="data-val-required=#{NotNull},data-val-number=#{typeMismatch},data-val-min=#{actionModel01.a.min},data-val-min-value=#{actionModel01.a.min.value}" />
    </td>
    <td>
        <input type="text" id="b" name="b" th:value="${resultats.culture}=='fr-FR' and ${actionModel01.b}!=null? ${#strings.replace(actionModel01.b,'.',',')} : ${actionModel01.b}" data-val="true" th:attr="data-val-required=#{NotNull},data-val-number=#{typeMismatch},data-val-min=#{actionModel01.b.min},data-val-min-value=#{actionModel01.b.min.value}" />
    </td>
</tr>

هناك عدة تعديلات يجب إجراؤها في السطرين 3 و6. سنركز على السطر 3:

  • كنا قد كتبنا [th:field="*{a}"]. المعلمة [th:field] تحدد سمات [id, name, value] لعلامة HTML [input] التي تم إنشاؤها. هنا، نريد إدارة السمة [value] بأنفسنا. لذا، نقوم أيضًا بتعيين السمات [id, name] بأنفسنا؛
  • السمة [th:value] تُقيّم تعبيرًا يستخدم العامل الثلاثي «؟». نقوم باختبار التعبير [${resultats.culture}=='fr-FR' and ${actionModel01.b}!=null]. إذا كان صحيحًا، فإننا نعطي السمة [value] قيمة a [actionModel01.a] حيث يتم استبدال النقطة العشرية بالفاصلة. وإذا كانت كاذبة، يُعطى السمة [value] قيمة a [actionModel01.a] دون أي تعديلات؛
  • السطر 6: نكرر نفس الإجراء للحقل [b]؛

فيما يلي مثال على التنفيذ:

  • في [1]، احتفظت الأرقام في [a,b] بالكتابة الفرنسية. وهذا ليس هو الحال في [2]؛

يتم التعامل مع هذه المشكلة الجديدة بنفس الطريقة التي تم التعامل بها مع المشكلة السابقة. يتم تعديل العرض [vue-03.xml] بالطريقة التالية:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h4 th:text="#{resultats}">Résultats</h4>
        <p>
            <strong>
                <span th:text="#{labelHeureCalcul}">Heure de calcul :</span>
                <span id="heureCalcul" th:text="${resultats.heurePost}"></span>
            </strong>
        </p>
        <p>
            <span th:text="#{labelAplusB}">A+B=</span>
            <span id="aplusb" th:text="${resultats.culture}=='fr-FR' and ${resultats.aplusb}!=null? ${#strings.replace(resultats.aplusb,'.',',')} : ${resultats.aplusb}"></span>
        </p>
        <p>
            <span th:text="#{labelAmoinsB}">A-B=</span>
            <span id="amoinsb"     th:text="${resultats.culture}=='fr-FR' and ${resultats.amoinsb}!=null? ${#strings.replace(resultats.amoinsb,'.',',')} : ${resultats.amoinsb}"></span>
        </p>
        <p>
            <span th:text="#{labelAfoisB}">A*B=</span>
            <span id="amultiplieparb" th:text="${resultats.culture}=='fr-FR' and ${resultats.amultiplieparb}!=null? ${#strings.replace(resultats.amultiplieparb,'.',',')} : ${resultats.amultiplieparb}"></span>
        </p>
        <p>
            <span th:text="#{labelAdivB}">A/B=</span>
            <span id="adiviseparb" th:text="${resultats.culture}=='fr-FR' and ${resultats.adiviseparb}!=null? ${#strings.replace(resultats.adiviseparb,'.',',')} : ${resultats.adiviseparb}"></span>
        </p>
    </body>
</html>

فيما يلي مثال:

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

7.2.10. إدارة الرابط [Calculer]

دعونا نلقي نظرة على الرابط [Calculer] الموجود في الصفحة الرئيسية [vue-01.xml]:

رمز الرابط [Calculer] في العرض [vue-01.xml] هو كما يلي:


<a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>

تم تعريف الدالة jS [postForm] في الملف [local1.js] على النحو التالي:


// بيانات عامة
var loading;
var formulaire;
var résultats;
var a, b;

function postForm() {
    // هل النموذج صالح؟
    if (!formulaire.validate().form()) {
        // نموذج غير صالح - تم إنهاء العملية
        return;
    }
    // يتم إدارة لغتين محليتين [fr_FR, en_US]
    // يجب إرسال القيم الفعلية [a,b] بالصيغة الأنجلوساكسونية في جميع الأحوال
    // وسيتم ذلك بواسطة المرشح [CultureFilter]

    // يتم إجراء استدعاء Ajax يدويًّا
    $.ajax({
        url : '/ajax-02',
        headers : {
            'X-Requested-With: 'XMLHttpRequest'
        },
        type : 'POST',
        data : formulaire.serialize(),
        dataType : 'html',
        beforeSend : function() {
            loading.show();
        },
        success : function(data) {
            resultats.html(data);
        },
        complete : function() {
            loading.hide();
        },
        error : function(jqXHR) {
            résultats.html(jqXHR.responseText);
        }
    })
}
  • الأسطر 2-5: تجدر الإشارة إلى أن هذه العناصر قد تم تهيئتها بواسطة الدالة [$(document).ready
  • الأسطر 9-12: يتم تنفيذ أدوات التحقق من صحة النموذج jS. إذا كانت إحدى القيم غير صالحة، فإن التعبير [formulaire.validate().form()] يُرجع القيمة false. في هذه الحالة، يتم إلغاء [submit] الخاص بالنموذج؛
  • الأسطر 18-38: يتم إجراء استدعاء Ajax يدويًّا؛
  • السطر 19: القيمة URL المستهدفة لطلب Ajax؛
  • الأسطر 20-22: مصفوفة من الرؤوس HTTP تُضاف إلى تلك الموجودة افتراضيًا في الطلب HTTP. هنا، نضيف الرأس HTTP الذي سيُشير إلى الخادم بأننا نقوم باستدعاء Ajax؛
  • السطر 23: الطريقة المستخدمة HTTP؛
  • السطر 24: البيانات المرسلة. تقوم [formulaire.serialize] بإنشاء السلسلة المراد إرسالها [culture=fr-FR&a=12,7&b=20,89] من النموذج ذي المعرف [formulaire]. سنجد هنا المشكلة التي تمت دراستها سابقًا: يجب إرسال قيم [a,b] بالتنسيق الأنجلوساكسوني. نعلم أن هذه المشكلة قد تم حلها الآن بإنشاء المرشح [cultureFilter
  • السطر 25: نوع البيانات المتوقع استلامه. نعلم أن الخادم سيرسل تدفقًا HTML؛
  • السطر 26: الطريقة التي يجب تنفيذها عند بدء الطلب. هنا، يُشار إلى ضرورة عرض المكون ذي المعرف [loading]. وهي الصورة المتحركة التي تظهر أثناء الانتظار؛
  • السطر 29: الطريقة التي يجب تنفيذها في حالة نجاح طلب Ajax. المعلمة [data] هي الاستجابة الكاملة من الخادم. نعلم أن هذا تدفق HTML؛
  • السطر 30: يتم تحديث المكون ذي المعرف [résultats] بقيمة HTML من المعلمة [data].
  • السطر 33: يتم إخفاء إشارة الانتظار؛
  • السطر 35: وظيفة يتم تنفيذها عند استلام استجابة الخادم، بغض النظر عن طبيعتها، سواء كانت ناجحة أو خطأ؛
  • الأسطر 35-37: في حالة حدوث خطأ (أرسل الخادم استجابة HTTP مع حالة تشير إلى حدوث خطأ من جانب الخادم)، يتم عرض استجابة الخادم HTML في المنطقة [resultats

فيما يلي مثال على التنفيذ:

7.3. تحديث صفحة HTML باستخدام تدفق jSON

في المثال السابق، كان خادم الويب يرد على طلب Ajax HTTP بتدفق HTML. وفي هذا التدفق، كانت هناك بيانات مصحوبة بتنسيق HTML. نقترح استئناف المثال السابق، ولكن هذه المرة باستخدام استجابات jSON (JavaScript Object Notation) التي لا تحتوي إلا على البيانات. وتكمن الفائدة في أنه يتم بذلك إرسال عدد أقل من البايتات. نفترض أن جافا سكريبت (JavaScript) مفعل على المتصفح.

7.3.1. الإجراء [/ajax-04]

الإجراء [/ajax-04] مطابق للإجراء [/ajax-01]، باستثناء أنه يتم عرض العرض [vue-04.xml] بدلاً من العرض [vue-01.xml]:


@RequestMapping(value = "/ajax-04", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax04(Locale locale, Model modèle, HttpSession session, String tempo) {
        ...
        // عرض
        return "vue-04";
    }

7.3.2. طريقة العرض [vue-04.xml]

 

تستند طريقة العرض [vue-04.xml] إلى نص طريقة العرض [vue-01.xml] مع الاختلافات التالية:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        ...
        <script type="text/javascript" src="/js/local4.js"></script>
        <script th:inline="javascript">
            /*<![CDATA[*/
                    var culture = [[${resultats.culture}]];
                    Globalize.culture(culture);
                    /*]]>*/
        </script>
    </head>
    <body>
        <h2>Ajax - 04</h2>
    ...
        <form id="formulaire" name="formulaire" th:object="${actionModel01}">
...
            <p>
                <img id="loading" style="display: none" src="/images/loading.gif" />
                <a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>
            </p>
            <!-- الحقول المخفية -->
            <input type="hidden" id="culture" name="culture" th:value="${resultats.culture}"></input>
</form>
        <hr />
        <div id="entete">
            <h4 id="titre">Résultats</h4>
            <p>
                <strong>
                    <span id="labelHeureCalcul">Heure de calcul :</span>
                    <span id="heureCalcul">12:10:87</span>
                </strong>
            </p>
        </div>
        <div id="résultats">
            <p>
                A+B=
                <span id="aplusb">16,7</span>
            </p>
            <p>
                A-B=
                <span id="amoinsb">16,7</span>
            </p>
            <p>
                A*B=
                <span id="afoisb">16,7</span>
            </p>
            <p>
                A/B=
                <span id="adivb">16,7</span>
            </p>
        </div>
        <div id="erreur">
            <p style="color: red;">
                <span id="msgErreur">xx</span>
            </p>
        </div>
    </body>
</html>
  • السطر 5: أصبح جافا سكريبت للعرض موجودًا الآن في الملف [local4.js
  • السطر 16: لم تعد العلامة [form] تحتوي على المعلمات [data-ajax-attr] الخاصة بمكتبة [Unobtrusive Ajax]. ولن نستخدمها هنا. كما أن العلامة [form] لا تحتوي على السمات [method] و [action] التي تحدد كيفية ومكان إرسال القيم التي تم إدخالها في النموذج. وذلك لأن هذه القيمة سيتم إرسالها بواسطة الدالة jS (السطر 20)؛
  • الأسطر 26-57: حقل المعرف [resultats] الذي كان في السابق حقلًا فارغًا يحتوي الآن على الرمز HTML لعرض النتائج؛
  • الأسطر 26-34: رأس النتائج حيث يتم عرض وقت الحساب؛
  • الأسطر 35-52: نتائج العمليات الحسابية الأربع؛
  • الأسطر 53-57: أي رسالة خطأ قد يرسلها الخادم؛

يوجد الرمز jS الذي يتم تنفيذه عند تحميل العرض [vue-04.xm] في الملف [local4.js]. وهو كما يلي:


// البيانات العامة
    var loading;
    var formulaire;
    var résultats;
    var titre;
    var labelHeureCalcul;
    var heureCalcul;
    var aplusb;
    var amoinsb;
    var afoisb;
    var adivb;
    var msgErreur;

// عند تحميل المستند
$(document).ready(function() {
    // يتم استرداد مراجع المكونات المختلفة للصفحة
    loading = $("#loading");
    formulaire = $("#formulaire");
    résultats = $('#النتائج');
    titre=$("#titre");
    labelHeureCalcul=$("#labelHeureCalcul");
    heureCalcul=$("#heureCalcul");
    aplusb=$("#aplusb");
    amoinsb=$("#amoinsb");
    afoisb=$("#afoisb");
    adivb=$("#adivb");
    msgErreur=$("#msgErreur");
    // يتم إخفاء بعض العناصر
    résultats.hide();
    erreur.hide();
    loading.hide();
});
  • الأسطر 17-27: يتم استرداد المراجع jQuery لجميع عناصر الصفحة؛
  • السطر 29: يتم إخفاء منطقة النتائج؛
  • السطر 30: وكذلك منطقة الخطأ؛
  • السطر 31: وكذلك الصورة المتحركة الخاصة بوقت الانتظار؛
  • الأسطر 2-12: يتم تعيين المراجع المسترجعة كمتغيرات عامة حتى يتسنى للوظائف الأخرى استخدامها؛

7.3.3. الدالة jS [postForm]

الرابط [Calculer] هو التالي:


<p>
    <img id="loading" style="display: none" src="/images/loading.gif" />
    <a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>
</p>

الدالة jS [postForm] مُعرَّفة في الملف [local.js] على النحو التالي:


function postForm() {
    // هل النموذج صالح؟
    if (!formulaire.validate().form()) {
        // النموذج غير صالح - انتهى
        return;
    }
    // يتم إجراء استدعاء Ajax يدويًا
    $.ajax({
        url : '/ajax-05',
        headers : {
            'Accept: 'application/json'
        },
        type : 'POST',
        data : formulaire.serialize(),
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}

// قبل استدعاء Ajax
function onBegin() {
...
}

// عند استلام الرد من الخادم
// في حالة النجاح
function onSuccess(data) {
...
}

// عند استلام الرد من الخادم
// في حالة الفشل
function onError(jqXHR) {
...
}

// بعد [onSuccess, onError]
function onComplete() {
...
}
  • الأسطر 3-6: قبل إرسال القيم المدخلة، يتم التحقق منها. إذا كانت غير صحيحة، لا يتم تنفيذ POST للنموذج؛
  • السطر 9: تُرسَل القيم المدخلة إلى الإجراء [/ajax-05] الذي سنفصله لاحقًا؛
  • الأسطر 10-12: رأس HTTP لإعلام الخادم بأننا ننتظر ردًا بتنسيق jSON؛
  • السطر 13: سيتم إرسال القيم التي تم إدخالها؛
  • السطر 14: تسلسل القيم المدخلة في سلسلة جاهزة للإرسال بتنسيق [a=1,6&b=2,4&culture=fr-FR
  • السطر 15: نوع الرد المرسل من الخادم. سيكون jSON؛
  • السطر 16: الدالة المطلوب تنفيذها قبل POST؛
  • السطر 17: الدالة التي يجب تنفيذها عند استلام استجابة الخادم في حالة نجاحها. ويُقاس «نجاح» الطلب HTTP بناءً على حالة استجابة الخادم HTTP. الاستجابة [HTTP/1.1 200 OK ] هي استجابة ناجحة. أما الاستجابة [HTTP/1.1 500 Internal Server Error] فهي استجابة فاشلة. ما يُسمى بحالة الاستجابة HTTP هو الرمز [200] أو [500]. يرتبط عدد من هذه الرموز بـ«النجاح» بينما ترتبط رموز أخرى بـ«الفشل»؛
  • السطر 18: الدالة التي يجب تنفيذها عند استلام استجابة الخادم عندما تكون حالة هذه الاستجابة HTTP هي حالة فشل؛
  • السطر 18: الوظيفة التي يجب تنفيذها أخيرًا، بعد الوظائف [onSuccess, onError] السابقة؛

الدالة [onBegin] هي كما يلي:


// قبل استدعاء Ajax
function onBegin() {
    console.log("onBegin");
    // يتم عرض الصورة المتحركة
    loading.show();
    // يتم إخفاء بعض عناصر العرض
    entete.hide();
    résultats.hide();
    erreur.hide();
}

قبل دراسة الوظائف الأخرى jS الخاصة باستدعاء Ajax، نحتاج إلى معرفة الرد الذي أرسلته الإجراء [/ajax-05].

7.3.4. الإجراء [/ajax-05]

الإجراء [/ajax-05] هو كما يلي:


    @RequestMapping(value = "/ajax-05", method = RequestMethod.POST)
    @ResponseBody()
    // معالجة POST من العرض [vue-04]
    public JsonResults ajax05(@Valid ActionModel01 formulaire, BindingResult result, Locale locale,    HttpServletRequest request, HttpSession session) throws InterruptedException {
        if(result.hasErrors()){
            // حالة غير عادية - لا يتم عرض أي شيء
            return null;
        }
        ...
}
  • السطر 2: يشير السمة [ResponseBody] إلى أن الإجراء [/ajax-05] يقوم بنفسه بإرجاع الاستجابة إلى العميل. ونظرًا لوجود مكتبة jSON ضمن تبعيات المشروع، يقوم Spring Boot تلقائيًا بتكوين هذا النوع من الإجراءات بحيث تعرض jSON. وبالتالي، فإن السلسلة jSON من النوع [JsonResults] (السطر 4) هي التي سيتم إرسالها إلى العميل؛
  • السطر 2: سيتم تغليف القيم المرسلة [a, b, culture] في نوع [ActionModel01] الذي يُطلب التحقق من صحته [@Valid ActionModel01]. وهذا إجراء شكلي. لقد انطلقنا من افتراض أن جافا سكريبت كان مفعلًا على متصفح العميل، وبالتالي عند وصولها، تكون القيم المرسلة قد تم التحقق منها بالفعل من جانب العميل. ومع ذلك، يمكننا توقع حالة وجود POST غير خاضع للرقابة لا يستخدم عميلنا jS. في هذه الحالة، قد تفشل عملية التحقق؛
  • الأسطر 5-7: في حالة حدوث خطأ، يتم إرجاع تدفق jSON فارغًا؛

لنواصل دراسة الإجراء [/ajax-05]:


    @RequestMapping(value = "/ajax-05", method = RequestMethod.POST)
    @ResponseBody()
    // يعالج POST من العرض [vue-04]
    public JsonResults ajax05(@Valid ActionModel01 formulaire, BindingResult result, Locale locale,
            HttpServletRequest request, HttpSession session) throws InterruptedException {
...
        // سياق تطبيق Spring
        WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
        // الوقت؟
        Integer tempo = (Integer) session.getAttribute("tempo");
        if (tempo != null && tempo > 0) {
            Thread.sleep(tempo);
        }
    ...
        // يتم عرض النتيجة
        return résultats;
}
  • السطر 8: يتم استرداد السياق [ctx] من تطبيق Spring. نحتاج إليه لاسترداد الرسائل من الملفات [messages.properties] باستخدام مفتاح الرسالة والإعدادات المحلية. ويتم ذلك باستخدام الصيغة التالية:

ctx.getMessage(clé_message, tableau_de_paramètres, locale)
    • [clé_message]: مفتاح الرسالة المطلوبة؛
    • [locale]: الإعدادات اللغوية المستخدمة. وبالتالي، إذا كانت هذه الإعدادات اللغوية هي [en_US]، فسيتم استخدام الملف [messages_en.properties
    • [tableau_de_paramètres]: يمكن تهيئة الرسالة التي تم الحصول عليها كما هو موضح في [clé=message {0} {1}]. تحتوي هذه الرسالة على معلمتين [{0} {1}]. يجب توفير مصفوفة من قيمتين كمعلمة ثانية لـ [ctx.getMessage
  • الأسطر 10-13: في حالة وجود فترة تأخير في الجلسة، يتم إيقاف مؤشر الترابط الحالي طوال مدة هذا التأخير؛

تستمر العملية [/ajax-05] على النحو التالي:


        // يتم إعداد نموذج العرض التالي
        JsonResults résultats = new JsonResults();
        ...
}
  • السطر 2: إنشاء قالب السلسلة jSON المرسلة إلى العميل؛

القالب [JsonResults] هو كما يلي:

 

package istia.st.springmvc.models;

public class JsonResults {

    // البيانات
    private String titre;
    private String labelHeureCalcul;
    private String heureCalcul;
    private String aplusb;
    private String amoinsb;
    private String afoisb;
    private String adivb;
    private String msgErreur;

    // وظائف الحصول على القيم وتعيينها
...

}
  • الأسطر 6-13: كل حقل من حقول الفئة [JsonResult] يتوافق مع حقل مماثل في [id] في العرض [vue-04.xml]:

تستمر العملية [/ajax-05] على النحو التالي:


        // يتم إعداد نموذج العرض التالي
        JsonResults résultats = new JsonResults();
        // رأس الصفحة
        résultats.setTitre(ctx.getMessage("resultats.titre", null, locale));
        résultats.setLabelHeureCalcul(ctx.getMessage("labelHeureCalcul", null, locale));
        résultats.setHeureCalcul(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        // يتم إنشاء خطأ مرة كل مرتين
        int val = new Random().nextInt(2);
        if (val == 0) {
            // إرجاع رسالة خطأ
            résultats.setMsgErreur(ctx.getMessage("resultats.erreur",
                    new Object[] { ctx.getMessage("erreur.aleatoire", null, locale) }, locale));
            return résultats;
}
  • السطر 2: إنشاء نموذج السلسلة jSON المرسلة إلى العميل؛
  • الأسطر 4-6: يتم إنشاء رسائل رأس النتائج؛
  • الأسطر 8-14: يتم إنشاء رسالة خطأ في المتوسط مرة من كل مرتين. في هذه الحالة، لا يتم المضي قدمًا ويتم إرجاع السلسلة jSON إلى العميل (السطر 13)؛
  • السطر 11: لدينا هنا مثال على رسالة معدة مسبقًا:

erreur.aleatoire=erreur aléatoire
resultats.erreur=Une erreur s''est produite : [{0}]

تستمر العملية [/ajax-05] على النحو التالي:


        // يتم استرداد القيم المرسلة
        double a = formulaire.getA();
        double b = formulaire.getB();
        // يتم إنشاء النموذج
        résultats.setAplusb(String.valueOf(a + b));
        résultats.setAmoinsb(String.valueOf(a - b));
        résultats.setAfoisb(String.valueOf(a * b));
        try {
            résultats.setAdivb(String.valueOf(a / b));
        } catch (RuntimeException e) {
            résultats.setAdivb("NaN");
        }
        // يتم عرض النتيجة
return résultats;
  • السطران 2-3: يتم استرداد قيم [a] و [b]؛
  • الأسطر 5-12: يتم إنشاء النتائج الأربع؛
  • السطر 14: يتم إرسال السلسلة jSON [JsonResults] إلى العميل؛

لنرى النتيجة مع العميل [Advanced Rest Client]:

  • في [1-2]، يتم إجراء استعلام POST إلى الإجراء [/ajax-05
  • في [3]، يتم إرسال قيم غير صحيحة؛
  • في [4]، أرسل الخادم دفقًا فارغًا؛
  • في [1]، يتم إرسال قيم صحيحة؛
  • في [2]، الكائن jSON الذي أعاده الخادم، مع ظهور رسالة خطأ هنا؛
  • في [1]، يتم إرسال قيم صحيحة؛
  • في [2]، الكائن jSON الذي أعاده الخادم، مع عرض النتائج الأربع هنا؛
  • في [1]، يتم إرسال قيم صحيحة؛
  • في [2]، تم التلاعب لإحداث استثناء من جانب الخادم. نلاحظ أن الخادم يرسل كائنًا آخر هو jSON. في هذه الرسالة، نلاحظ أن حالة الرد HTTP هي [500]، مما يشير إلى حدوث خطأ من جانب الخادم؛

7.3.5. الدالة jS [postForm] - 2

الآن بعد أن عرفنا الكائن jSON الذي أرسله الخادم، يمكننا استخدامه في جافا سكريبت. الطريقة [onSuccess] التي يتم تنفيذها عندما يرسل الخادم استجابة بالحالة HTTP [200] هي كما يلي:


// عند استلام الرد من الخادم
// في حالة النجاح
function onSuccess(data) {
    console.log("onSuccess");
    // يتم ملء منطقة النتائج
    titre.text(data.titre);
    labelHeureCalcul.text(data.labelHeureCalcul);
    heureCalcul.text(data.heureCalcul);
    entete.show();
    // النتائج بدون أخطاء
    if (!data.msgErreur) {
        aplusb.text(data.aplusb);
        amoinsb.text(data.amoinsb);
        afoisb.text(data.afoisb);
        adivb.text(data.adivb);
        résultats.show();
        return;
    }
    // نتائج بها أخطاء
    msgErreur.text(data.msgErreur);
    erreur.show();
}
  • السطر 3: المعلمة [data] هي الكائن jSON الذي أعاده الخادم:
 

الطريقة [onError] التي يتم تنفيذها عندما تكون حالة الاستجابة HTTP هي [500] هي كما يلي:


// عند استلام الرد من الخادم
// في حالة الفشل
function onError(jqXHR) {
    console.log("onError");
    // خطأ في النظام
    msgErreur.text(jqXHR.responseText);
    erreur.show();
}
  • السطر 3: يحتوي الكائن JQuery [jqXHR] على الخصائص التالية:
    • responseText: نص استجابة الخادم،
    • status: رمز الخطأ الذي أعاده الخادم،
    • statusText: النص المرتبط برمز الخطأ هذا؛
  • السطر 6: الكائن [jqXHR.responseText] هو الكائن jSON التالي:
 

7.3.6. الاختبارات

دعونا نلقي نظرة على بعض لقطات الشاشة لتشغيل تطبيق الويب:

 
 
 

7.4. تطبيق ويب ذو صفحة واحدة

7.4.1. مقدمة

تتيح تقنية Ajax إنشاء تطبيقات ذات صفحة واحدة:

  • يتم عرض الصفحة الأولى من خلال طلب تقليدي من المتصفح؛
  • أما الصفحات التالية فيتم الحصول عليها من خلال استدعاءات Ajax. وبالتالي، لا يغير المتصفح في النهاية عنوان URL أبدًا ولا يقوم بتحميل أي صفحة جديدة. يُطلق على هذا النوع من التطبيقات اسم «تطبيق ذو صفحة واحدة» (APU) أو باللغة الإنجليزية «Single Page Application» (SPA).

فيما يلي مثال بسيط على مثل هذا التطبيق. سيتضمن التطبيق الجديد عرضين:

  • في [1]، تتيح لنا الإجراء [/ajax-06] الحصول على الصفحة الأولى، أي الصفحة 1؛
  • في [2]، يتيح لنا رابط الانتقال إلى الصفحة 2 بفضل استدعاء Ajax؛
  • في [3]، لم يتغير URL. الصفحة المعروضة هي الصفحة
  • في [4]، يتيح لنا رابط العودة إلى الصفحة 1 باستخدام استدعاء Ajax؛
  • في [5]، لم يتغير URL. الصفحة المعروضة هي الصفحة 1.

7.4.2. الإجراء [/ajax-06]

رمز الإجراء [/ajax-06] هو كما يلي:


    @RequestMapping(value = "/ajax-06", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax06() {
        return "vue-06";
}
  • الأسطر 1-4: يقتصر الإجراء [/ajax-06] على عرض القالب [vue-06.xml

7.4.3. طريقة العرض [vue-06.xml]

الطريقة [vue-06.xml] هي كما يلي:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>Ajax-06</title>
        <link rel="stylesheet" href="/css/ajax01.css" />
        <script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="/js/local6.js"></script>
    </head>
    <body>
        <h3>Ajax - 06 - Navigation dans une Application à Page Unique</h3>
        <div id="content" th:include="vue-07" />
    </body>
</html>
  • السطر 8: تستخدم طريقة العرض برنامجًا نصيًّا [local6.js
  • السطر 12: يتم تضمين العرض [vue-07.xml] في منطقة المعرف [content] الخاصة بالعرض [vue-06.xml

7.4.4. الطريقة [vue-07.xml]

الطريقة [vue-07.xml] هي كما يلي:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h4>Page 1</h4>
        <p>
            <a href="javascript:gotoPage(2)">Page 2</a>
        </p>
    </body>
</html>

7.4.5. الوظيفة jS [gotoPage]

يستخدم الرابط [Page 2] الخاص بالعرض [vue-07.xml] الدالة jS [gotoPage] المُعرَّفة في الملف [local6.js] التالي:


// بيانات عامة
var content;

function gotoPage(num) {
    // يتم إجراء استدعاء Ajax يدويًا
    $.ajax({
        url : '/ajax-07',
        type : 'POST',
        data : 'num=' + num,
        dataType : 'html',
        beforeSend : function() {
        },
        success : function(data) {
            content.html(data)
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // خطأ في النظام
            content.html(jqXHR.responseText);
        }
    })
}

// أثناء تحميل المستند
$(document).ready(function() {
    // يتم استرداد مراجع المكونات المختلفة للصفحة
    content = $("#content");
});
  • السطر 28: عند تحميل الصفحة، يتم تخزين منطقة المعرف [content] وتحويلها إلى متغير عام (السطر 2)؛
  • السطر 4: تتلقى الدالة [gotoPage] كمعلمة رقم الصفحة (1 أو 2) المراد عرضها في العرض الحالي؛
  • السطر 7: الدالة URL هي الدالة المستهدفة للدالة POST؛
  • السطر 8: يتم استدعاء الدالة URL الواردة في السطر 7 عبر الدالة POST؛
  • السطر 9: السلسلة المرسلة. يتم إرسال معلمة باسم [num]. قيمتها هي رقم الصفحة (السطر 4) المطلوب عرضها في العرض الحالي؛
  • السطر 10: سيقوم الخادم بإرجاع HTML، وهو معلمة الصفحة المراد عرضها؛
  • الأسطر 13-15: في حالة النجاح (الحالة HTTP تساوي 200)، يتم وضع القيمة HTML المرسلة من الخادم في حقل المعرف [content
  • الأسطر 18-20: في حالة الفشل (الحالة HTTP تساوي 500)، يتم وضع HTML المرسلة من الخادم في حقل المعرف [content

7.4.6. الإجراء [/ajax-07]

رمز الإجراء [/ajax-07] هو كما يلي:


@RequestMapping(value = "/ajax-07", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax07(int num) {
        // رقم: رقم الصفحة
        switch (num) {
        case 1:
            return "vue-07";
        case 2:
            return "vue-08";
        default:
            return "vue-07";
        }
    }
  • السطر 2: يتم استرداد المعلمة المرسلة التي تسمى [num]. تجدر الإشارة إلى أن المعلمة في السطر 2 يجب أن تحمل اسم المعلمة المرسلة، وهي هنا [num]. [num] هو رقم الصفحة أو العرض؛
  • السطران 5-6: في حالة [num==1]، يتم إرجاع العرض [vue-07.xml
  • السطران 7-8: في حالة وجود [num==2]، يتم إرجاع العرض [vue-08.xml
  • السطران 9-10: في الحالات الأخرى (وهو أمر مستحيل عادةً)، يتم إرجاع العرض [vue-07.xml

7.4.7. الطريقة [vue-08.xml]

تشكل طريقة العرض [vue-08.xml] الصفحة رقم 2 من التطبيق:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h4>Page 2</h4>
        <p>
            <a href="javascript:gotoPage(1)">Page 1</a>
        </p>
    </body>
</html>

7.5. تضمين عدة تدفقات HTML في استجابة jSON

7.5.1. مقدمة

ننظر إلى التطبيق التالي:

تحتوي الصفحة [1] على أربع مناطق:

  • [Zone 1, Zone 3] هي مناطق تظهر/تختفي عند النقر على الزر [Rafraîchir]. نحسب عدد مرات ظهور كل من هاتين المنطقتين [2]. المنطقة [Zone 1] تستخدم اللغة الفرنسية بينما المنطقة [Zone 3] تستخدم اللغة الإنجليزية؛
  • المنطقة [Zone 2] موجودة بشكل دائم؛
  • المنطقة [Saisies] موجودة بشكل دائم؛

يعرض الرابط [Valider] الصفحة التالية [3]:

  • الرابط [Retour à la page 1] يعيد الصفحة رقم 1 إلى الحالة التي كانت عليها [4]؛

التطبيق مكون من صفحة واحدة. يطلب المتصفح الصفحة الأولى من الخادم. أما الصفحات التالية فيتم الحصول عليها من الخادم عن طريق استدعاءات Ajax.

7.5.2. السهم [/ajax-09]

  

الإجراء [/ajax-09] هو كما يلي:


    @RequestMapping(value = "/ajax-09", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax09() {
        return "vue-09";
}

وهي تقتصر على عرض العرض [vue-09.xml].

7.5.3. طرق العرض XML

  

الطريقة [vue-09.xml] هي الصفحة الرئيسية للتطبيق:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>Ajax-09</title>
        <link rel="stylesheet" href="/css/ajax01.css" />
        <script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="/js/json3.js"></script>
        <script type="text/javascript" src="/js/local9.js"></script>
    </head>
    <body>
        <h3>Ajax - 09 - Navigation dans une Application à Page Unique</h3>
        <h3>avec des flux HTML embarqués dans des chaînes jSON</h3>
        <hr />
        <div id="content" th:include="vue-09-page1" />
        <img id="loading" src="/images/loading.gif" />
        <div id="erreur" style="background-color:lightgrey"></div>
    </body>
</html>
  • السطر 9: الملف JS المستخدم في التطبيق؛
  • السطر 15: محتوى الصفحة الرئيسية؛
  • السطر 16: صورة متحركة للانتظار:
  • السطر 17: منطقة لعرض أي خطأ محتمل؛

الطريقة [vue-09-page1.xml] هي الصفحة 1 من التطبيق:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h2>Page 1</h2>
        <!-- المنطقة 1 -->
        <fieldset id="zone1" style="background-color:pink">
            <legend>Zone 1</legend>
            <span id="zone1-content" th:text="xx">xx</span>
        </fieldset>
        <!-- المنطقة 2 -->
        <fieldset id="zone2" style="background-color:lightgreen">
            <legend>Zone 2</legend>
            <span>Ce texte reste toujours présent</span>
        </fieldset>
        <!-- المنطقة 3 -->
        <fieldset id="zone3" style="background-color:yellow">
            <legend>Zone 3</legend>
            <span id="zone3-content" th:text="zz">zz</span>
        </fieldset>
        <br />
        <p>
            <button onclick="javascript:postForm()">Rafraîchir</button>
        </p>
        <hr />
        <div id="saisies" th:include="vue-09-saisies">
        </div>
    </body>
</html>
  • الأسطر 6-9: المنطقة [Zone 1]. يتم وضع محتواها في المكون [id="zone1-content"]؛
  • الأسطر 11-14: المنطقة [Zone 2] التي لا تتغير؛
  • الأسطر 16-19: الحقل [Zone 3]. يتم وضع محتواه في المكون [id="zone3-content"]؛
  • السطر 22: الدالة JS التي ترسل النموذج؛
  • السطر 25: تضمين منطقة الإدخال؛

تجدر الإشارة إلى أن الصفحة 1 لا تحتوي على علامة [form]. سيتم معالجة كل شيء باستخدام جافا سكريبت.

تبدو طريقة العرض [vue-09-saisies.xml] كما يلي:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <div id="saisies">
        <h4>Saisies :</h4>
        <p>
            Chaîne de caractères :
            <input type="text" id="text1" size="30" th:value="${value1}" />
        </p>
        <p>
            Nombre entier :
            <input type="text" id="text2" size="10" th:value="${value2}" />
        </p>
        <p>
            <a href="javascript:valider()">Valider</a>
        </p>
    </div>
</html>
  • الأسطر 5-8: إدخال سلسلة أحرف؛
  • الأسطر 13-16: إدخال عدد صحيح؛
  • السطر 14: الدالة JS التي ترسل القيم التي تم إدخالها؛

مرة أخرى، تجدر الإشارة إلى أن منطقة الإدخال لا تحتوي على علامة [form].

إجمالًا، تقدم الصفحة رقم 1 وظيفتين:

  • [Rafraîchir]: التي تقوم بتحديث الحقول 1 و3. تتم معالجة هذا الإجراء بواسطة الخادم الذي يعيد عرض ما يلي بشكل عشوائي:
    • المنطقة 1 مع عداد الزيارات الخاص بها ولا شيء للمنطقة 3،
    • المنطقة 3 مع عداد الزيارات الخاص بها ولا شيء للمنطقة 1،
    • المنطقتين مع عدادي الوصول الخاصين بهما؛
  • [Valider]: يعرض الصفحة 2 بالقيم التي تم إدخالها أو رسالة خطأ إذا كانت البيانات المدخلة غير صالحة؛

سنركز أولاً على الزر [Rafraîchir].

7.5.4. الرمز JS الخاص بإدارة الزر [Rafraîchir]

  

رمز الملف [local9.js] هو كما يلي:


// المتغيرات العامة
var content;
var loading;
var erreur;

// عند تحميل المستند
$(document).ready(function() {
    // يتم استرداد مراجع المكونات المختلفة للصفحة
    loading = $("#loading");
    loading.hide();
    erreur = $("#erreur");
    erreur.hide();
    content = $("#content");
});
  • الأسطر 9-13: عند تحميل الصفحة الرئيسية، يتم تخزين المراجع الخاصة بالمكونات الثلاثة المحددة بواسطة [loading, erreur, content
  • الأسطر 2-4: يتم تخزين مراجع هذه المكونات الثلاثة في متغيرات عامة. وتظل هذه المراجع ثابتة لأن المناطق الثلاث المعنية موجودة دائمًا في الصفحة المعروضة، بغض النظر عن الوقت. ونظرًا لأنها تظل ثابتة، يمكن حسابها في [$(document).ready] ومشاركتها مع الوظائف الأخرى في الملف JS؛

تتولى الدالة [postForm] معالجة النقر على الزر [Rafraîchir]:


function postForm() {
    console.log("postForm");
    // يتم إجراء استدعاء Ajax يدويًا
    $.ajax({
        url : '/ajax-10',
        headers : {
            'Accept' : 'application/json'
        },
        type : 'POST',
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}
  • الأسطر 4-15: استدعاء Ajax إلى الخادم؛
  • السطر 5: الإجراء [ajax-10] هو الذي سيتولى معالجة POST؛
  • الأسطر 6-8: سيكون الرد هو jSON. يشير العميل JS إلى أنه يقبل المستندات jSON؛
  • السطر 9: يتم استدعاء الإجراء [ajax-10] مع عملية POST؛
  • السطر 10: سيتم استلام jSON؛
  • السطر 11: الدالة التي يتم تنفيذها قبل استدعاء Ajax؛
  • السطر 12: الدالة التي يتم تنفيذها عند استلام استجابة الخادم، عندما تكون الاستجابة ناجحة [200 OK
  • السطر 13: الدالة التي يتم تنفيذها عند استلام استجابة الخادم، في حالة فشل العملية [500 Internal server error, ...]؛
  • السطر 14: الدالة التي يتم تنفيذها بعد استلام الرد؛

الدالة [onBegin] هي كما يلي:


// قبل استدعاء Ajax
function onBegin() {
    console.log("onBegin");
    // صورة الانتظار
    loading.show();
}

وهي تقتصر على تشغيل الصورة المتحركة التي تُظهر انتظار نتيجة الخادم.

7.5.5. الإجراء [/ajax-10]

  

الإجراء [/ajax-10] هو كما يلي:


// الجلسة
    @Autowired
    private SessionModel1 session;
    // محرك Thymeleaf / Spring
    @Autowired
    private SpringTemplateEngine engine;

    @RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
    @ResponseBody()
    public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
    ...
    }
  • السطر 3: يتم إدخال الجلسة. وهي من النوع [SessionModel1] التالي:
  

package istia.st.springmvc.models;

import java.io.Serializable;

import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;

@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessionModel1 implements Serializable {

    private static final long serialVersionUID = 1L;
    // عدادان
    private int cpt1 = 0;
    private int cpt3 = 0;
    // المناطق الثلاث
    private String zone1 = "xx";
    private String zone3 = "zz";
    private String saisies;
    private boolean zone1Active = true;
    private boolean zone3Active = true;

    // وظائف الحصول والتعيين
    ...
}

تخزن الجلسة [SessionModel1] العناصر التالية:

  • السطر 15: عدد المرات التي يتم فيها عرض الحقل [Zone 1]؛
  • السطر 16: عدد المرات [cpt3] التي يتم فيها عرض المنطقة [Zone 3]؛
  • الأسطر 18-20: تدفقات HTML للمناطق [Zone 1] و [Zone 3] و [Saisies]. وهذا ضروري في التسلسل [Page 1] --> [Page 2] --> [Page 1]. عند الانتقال من [Page 2] إلى [Page 1]، يجب استعادة [Page 1] وبالتالي المناطق الثلاث التابعة له؛
  • السطران 21-22: متغيران منطقيان يشيران إلى ما إذا كانت الحقول [Zone 1] و [Zone 3] معروضة (مرئية)؛

العنصر الآخر الذي تم إدراجه في وحدة التحكم [AjaxController] هو التالي:


    // محرك Thymeleaf / Spring
    @Autowired
private SpringTemplateEngine engine;

يتم تعريف الكائن من النوع [SpringTemplateEngine] في ملف التكوين [Config]:

  

وهو مُعرَّف على النحو التالي:


    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
        templateResolver.setPrefix("classpath:/templates/");
        templateResolver.setSuffix(".xml");
        templateResolver.setTemplateMode("HTML5");
        templateResolver.setCacheable(true);
        templateResolver.setCharacterEncoding("UTF-8");
        return templateResolver;
    }

    @Bean
    SpringTemplateEngine templateEngine(SpringResourceTemplateResolver templateResolver) {
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);
        return templateEngine;
}
  • الأسطر 2-10: نحن نعرف البين من النوع [SpringResourceTemplateResolver] الذي يسمح لنا بتحديد بعض خصائص العروض؛
  • الأسطر 13-17: تتيح لنا البين من النوع [SpringTemplateEngine] تحديد «محرك» العروض، أي الفئة المسؤولة عن توليد الاستجابات [Thymeleaf] للعملاء. يحتوي [Thymeleaf] على «محرك» افتراضي وآخر عند استخدامه في بيئة [Spring]. وهذا الأخير هو الذي نستخدمه هنا؛

توقيع الإجراء [/ajax-10] هو كما يلي:


@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
    @ResponseBody()
    public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
    ...
}
  • السطر 1: لا تقبل الإجراء [/ajax-10] سوى POST؛
  • السطر 2: الإجراء [/ajax-10] يقوم بنفسه بإرجاع الرد إلى العميل. وسيتم تحويل هذا الرد تلقائيًا إلى jSON؛
  • السطر 3: الرد من النوع [JsonResult10] كما يلي:
  

package istia.st.springmvc.models;

public class JsonResult10 {

    // البيانات
    private String content;
    private String zone1;
    private String zone3;
    private String erreur;
    private String saisies;
    private boolean zone1Active;
    private boolean zone3Active;

    public JsonResult10() {
    }

    // وظائف الاسترجاع والتعيين
...
}
  • السطر 6: المحتوى HTML الخاص بالمنطقة المحددة بـ [content
  • السطر 7: محتوى HTML للمنطقة [Zone 1]؛
  • السطر 8: محتوى HTML للمنطقة [Zone 3]؛
  • السطر 9: محتوى HTML من المنطقة [Erreur
  • السطر 10: محتوى HTML من المنطقة [Saisies
  • السطر 11: قيمة منطقية تشير إلى ما إذا كان يجب عرض المنطقة [Zone 1]؛
  • السطر 12: قيمة منطقية تشير إلى ما إذا كان يجب عرض المنطقة [Zone 3]؛

رمز الإجراء [/ajax-10] هو كما يلي:


@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
    @ResponseBody()
    public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
        // سياق Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // الاستجابة
        JsonResult10 result = new JsonResult10();
        // الجلسة
        session.setZone1(null);
        session.setZone3(null);
        session.setZone1Active(false);
        session.setZone3Active(false);
        // إرجاع استجابة عشوائية
        int cas = new Random().nextInt(3);
        switch (cas) {
        case 0:
            // المنطقة 1 نشطة
            setZone1(thymeleafContext, result);
            return result;
        case 1:
            // المنطقة 3 نشطة
            setZone3(thymeleafContext, result);
            return result;
        case 2:
            // المنطقتان 1 و3 نشطتان
            setZone1(thymeleafContext, result);
            setZone3(thymeleafContext, result);
            return result;
        }
        return null;
    }
  • السطر 5: نسترد السياق [Thymeleaf]. سنرى لاحقًا ما الذي سنستخدمه من أجله؛
  • السطر 7: نقوم بإنشاء استجابة فارغة في الوقت الحالي؛
  • الأسطر 9-12: نضع في [null] الحقلين الموجودين في الجلسة ونشير إلى أنه لا يجب عرضهما. سيتم إنشاء هذين الحقلين قريبًا، ولكن من الممكن ألا يتم إنشاء سوى أحدهما؛
  • الأسطر 14-29: يتم إنشاء الحقلين؛
  • الأسطر 17-19: يتم إنشاء المنطقة [Zone 1] وحدها؛
  • الأسطر 21-23: يتم إنشاء المنطقة [Zone 3] وحدها؛
  • الأسطر 25-28: يتم إنشاء المنطقتين [Zone 1] و [Zone 3]؛

يتم إنشاء التدفق HTML من المنطقة [Zone 1] بالطريقة التالية:


    private void setZone1(WebContext thymeleafContext, JsonResult10 result) {
        // المنطقة 1 نشطة
        // تدفق HTML
        int cpt1 = session.getCpt1() + 1;
        thymeleafContext.setVariable("cpt1", cpt1);
        thymeleafContext.setLocale(new Locale("fr", "FR"));
        String zone1 = engine.process("vue-09-zone1", thymeleafContext);
        result.setZone1(zone1);
        result.setZone1Active(true);
        // جلسة
        session.setCpt1(cpt1);
        session.setZone1(zone1);
        session.setZone1Active(true);
}
  • السطر 1: المعلمات هي:
    • السياق [Thymeleaf] من النوع [WebContext
    • الاستجابة للعميل قيد الإنشاء من النوع [JsonResult10
  • السطر 3: يتم زيادة عداد [cpt1] الخاص بالجلسة، والذي يحسب عدد المرات التي يتم فيها عرض الحقل [Zone 1]؛
  • السطر 4: يعمل السياق [Thymeleaf] من النوع [WebContext] بشكل مشابه إلى حد ما للنموذج [Model] في Spring MVC. لإضافة عنصر إلى النموذج، نستخدم [WebContext.setVariable]. وهنا، نضع العداد [cpt1] في النموذج [Thymeleaf]. سيسمح ذلك بتقييم تعبير Thymeleaf [${cpt1}]
  • السطر 5: السياق [Thymeleaf] له إعدادات لغة محلية. وهذا يسمح له بتقييم التعبيرات من النوع [#{clé_msg}]. هنا، نربط سياق Thymeleaf بإعدادات لغة محلية فرنسية؛
  • السطر 6: هذه هي التعليمات الأكثر إثارة للاهتمام. سيقوم محرك Thymeleaf بمعالجة العرض [vue-09-zone1.xml] باستخدام النموذج والإعدادات المحلية التي تم حسابها للتو، وبدلاً من إرسال التدفق الناتج HTML إلى العميل، فإنه يعرضه كسلسلة أحرف؛
  • الأسطر 7-9: يتم تخزين التدفق HTML الخاص بالمنطقة [Zone 1] الذي تم حسابه للتو في الجلسة وفي النتيجة التي سيتم إرسالها إلى العميل. بالإضافة إلى ذلك، يُشار إلى أنه يجب عرض المنطقة [Zone 1]؛
  • الأسطر 11-13: يتم تخزين المعلومات المتعلقة بالمنطقة [Zone 1] في الجلسة حتى يتسنى إعادة إنشاؤها؛

السطر 7 يعالج العرض التالي [vue-09-zone1.xml]:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <span th:text="#{message.zone}"></span>
    <span th:text="${cpt1}"></span>
</html>
  • السطر 3: سيتم تقييم التعبير [#{message.zone}] باستخدام النموذج المحلي؛
  • السطر 4: سيتم تقييم التعبير [${cpt1}] باستخدام قالب Thymeleaf؛

يتم تعريف رسالة المفتاح [message.zone] في ملفات الرسائل [messages_fr.properties] و [messages_en.properties]:

  

[messages_fr.properties]


message.zone=Nombre d'accès : 

[messages_en.properties]


message.zone=Number of hits: 

يتم إنشاء التدفق HTML من المنطقة [Zone 3] بطريقة مماثلة:


    private void setZone3(WebContext thymeleafContext, JsonResult10 result) {
        // المنطقة 3 نشطة
        // التدفق HTML
        int cpt3 = session.getCpt3() + 1;
        thymeleafContext.setVariable("cpt3", cpt3);
        thymeleafContext.setLocale(new Locale("en", "US"));
        String zone3 = engine.process("vue-09-zone3", thymeleafContext);
        result.setZone3(zone3);
        result.setZone3Active(true);
        // جلسة
        session.setCpt3(cpt3);
        session.setZone3(zone3);
        session.setZone3Active(true);
}
  • السطر 6: الإعدادات المحلية للمنطقة [Zone 3] هي الإعدادات المحلية للغة الإنجليزية؛

7.5.6. معالجة استجابة الإجراء [/ajax-10]

لنعد إلى الرمز JS التابع لـ [local9.js] الذي سيقوم بمعالجة استجابة الخادم:


// عند استلام الرد من الخادم
// في حالة النجاح
function onSuccess(data) {
    console.log("onSuccess");
    // المحتوى
    if (data.content) {
        content.html(data.content);
    }
    // المنطقة 1
    if (data.zone1Active) {
        $("#zone1").show();
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
        }
    } else {
        $("#zone1").hide();
    }
    // المنطقة 3 نشطة؟
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // البيانات المدخلة؟
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // خطأ؟
    if (data.erreur) {
        erreur.text(data.erreur);
        erreur.show();
    } else {
        erreur.hide();
    }
}

لنتذكر بنية Java للاستجابة المستلمة في السطر 3 في المتغير [data]:


public class JsonResult10 {

    // البيانات
    private String content;
    private String zone1;
    private String zone3;
    private String erreur;
    private String saisies;
    private boolean zone1Active;
    private boolean zone3Active;

}
  • الأسطر 6-8: إذا كان [data.content!=null]، فسيتم تهيئة المنطقة [id=content] بهذه القيمة. تمثل هذه المنطقة [Page 1] أو [Page 2] بالكامل. في هذا المثال، لدينا [data.content==null]، وبالتالي لن يتم تعديل المنطقة [id=content] وستستمر في عرض [Page 1]؛
  • الأسطر 10-17: يتم عرض [Zone 1] إذا كان [data.zone1Active==true]. وإذا كان [data.zone1!=null] أيضًا، فسيتم تعديل محتوى [Zone 1]، وإلا فسيبقى كما هو؛
  • الأسطر 19-26: الأمر نفسه بالنسبة لـ [Zone 3]؛
  • الأسطر 28-30: إذا كان لدينا [data.saisies!=null]، فإن المنطقة [Saisies] يتم إعادة إنشاؤها. في هذا المثال التوضيحي، لدينا [data.saisies==null]، وبالتالي تظل المنطقة [Saisies] كما هي؛
  • الأسطر 32-37: نفس المنطق ينطبق على المنطقة [Erreur] مع الفروق التالية:
    • السطر 33: ستكون [data.erreur] رسالة خطأ بتنسيق نصي؛
    • السطر 36: إذا كان [data.erreur==null]، فإن الحقل [Erreur] يُخفى. ذلك لأنه ربما تم عرضه خلال الاستعلام السابق؛

في حالة حدوث خطأ من جانب الخادم (حالة HTTP من نوع 500 Internal server error)، يتم تنفيذ الوظيفة التالية:


// عند استلام الرد من الخادم
// في حالة الفشل
function onError(jqXHR) {
    console.log("onError");
    // خطأ في النظام
    erreur.text(jqXHR.responseText);
    erreur.show();
}

لرؤية مثل هذا الخطأ، لنقوم بتعديل الدالة [postForm] على النحو التالي:


function postForm() {
    console.log("postForm");
    // يتم استرداد المراجع من الصفحة الحالية
    ...
    // يتم إجراء استدعاء Ajax يدويًا
    $.ajax({
        url : '/ajax-10x',
        ...
    })
}
  • السطر 7: نضع URL التي لا وجود لها؛

فيما يلي النتائج عند النقر على الزر [Rafraîchir]:

من المثير للاهتمام ملاحظة أن الخطأ قد أُرسل أيضًا على شكل سلسلة jSON.

الطريقة التي يتم تنفيذها بعد استلام الرد من الخادم هي التالية:


// بعد [onSuccess, onError]
function onComplete() {
    console.log("onComplete");
    // صورة انتظار
    loading.hide();
}

يكتفى بإخفاء الصورة المتحركة التي تشير إلى حالة الانتظار.

7.5.7. عرض الصفحة [Page 2]

الرمز HTML للرابط [Valider] هو التالي:


<a href="javascript:valider()">Valider</a>

الوظيفة JS [valider] هي كما يلي:


// التحقق من صحة القيم المدخلة
function valider() {
    // القيمة التي تم إرسالها
    var post = JSON3.stringify({
        "value1" : $("#text1").val().trim(),
        "value2" : $("#text2").val().trim()
    });
    // يتم إجراء استدعاء Ajax يدويًا
    $.ajax({
        url : '/ajax-11A',
        headers : {
            'Accept: 'application/json',
            'Content-Type' : 'application/json'
        },
        type : 'POST',
        data : post,
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}
  • الأسطر 4-7: لدينا قيمتان v1 و v2 لنقوم بنشرهما: وهما قيمتي مكونات الإدخال المحددة بواسطة [#text1] و [#text2]. سنقوم بشيء جديد. سنرسل هاتين القيمتين في شكل سلسلة jSON {"value1":v1,"value2":v2}؛
  • السطر 10: سيتم إرسال القيم المرسلة إلى الإجراء [ajax-11A
  • السطر 12: نظرًا لأننا نعلم أننا سنستلم استجابة jSON، فإننا نشير إلى أنه يمكننا استلامها من jSON؛
  • السطر 13: نُبلغ الخادم بأننا سنرسل إليه القيمة المُرسلة في شكل سلسلة jSON؛
  • السطران 15-16: يتم تحويل القيمة المراد إرسالها إلى POST؛
  • السطر 17: سنستقبل jSON؛

7.5.8. الإجراء [ajax-11A]

الإجراء [ajax-11A] الذي يعالج السلسلة jSON التي تم نشرها هو كما يلي:


@RequestMapping(value = "/ajax-11A", method = RequestMethod.POST, consumes = "application/json")
    @ResponseBody
    public JsonResult10 ajax11A(@RequestBody @Valid PostAjax11A post, BindingResult bindingResult, Locale locale,     HttpServletRequest request, HttpServletResponse response) {
        ...
    }
  • السطر 1: يُشار بواسطة ["application/json"] إلى أن الإجراء ينتظر مستندًا بتنسيق jSON. هذا المستند هو القيمة التي أرسلها العميل؛
  • السطر 3: سيتم استرداد القيمة المرسلة في الكائن [PostAjax11A post] التالي:
  

package istia.st.springmvc.models;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.Range;

public class PostAjax11A {

    // البيانات
    @Size(min = 4, max = 6)
    @NotNull
    private String value1;
    @Range(min = 10, max = 14)
    @NotNull
    private Integer value2;

    // وظائف الاسترجاع والتعيين
    ...
}
  • يجب أن تعكس بنية الكائن [PostAjax11A] بنية الكائن الذي تم إرساله {"value1":v1,"value2":v2}. لذلك، يجب وجود حقل [value1] (السطر 13) و [value2] (السطر 16)؛
  • وقد تم وضع قيود التكامل على الحقلين؛

لنعد إلى كود الإجراء [ajax-11A]:


@RequestMapping(value = "/ajax-11A", method = RequestMethod.POST, consumes = "application/json")
    @ResponseBody
    public JsonResult10 ajax11A(@RequestBody @Valid PostAjax11A post, BindingResult bindingResult, Locale locale,    HttpServletRequest request, HttpServletResponse response) {
        // سياق Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // الاستجابة
        JsonResult10 result = new JsonResult10();
        // هل الإرسال صالح؟
        if (bindingResult.hasErrors()) {
            // يتم إعادة الصفحة 1 مع ظهور خطأ
            result.setZone1Active(session.isZone1Active());
            result.setZone3Active(session.isZone3Active());
            result.setErreur(getErreursForModel(bindingResult));
            return result;
        }
        ...
}
  • السطر 3: يشير التعليق التوضيحي [@RequestBody] إلى المستند المرسل من قبل العميل. وهي القيمة التي أرسلها العميل في jSON. وبالتالي، سيتم استخدام هذه القيمة لإنشاء الكائن [PostAjax11A
  • السطر 3: التعليق التوضيحي [@Valid] يفرض التحقق من صحة القيمة المرسلة؛
  • السطر 9: في حالة فشل التحقق من الصحة:
    • السطر 13: يتم إرجاع رسالة خطأ،
    • السطران 11-12: يتم إعادة الحقول 1 و3 إلى الحالة التي كانتا عليها (سواء كانتا معروضتين أم لا)؛

يتم حساب رسالة الخطأ بالطريقة التالية:


    private String getErreursForModel(BindingResult result) {
        StringBuffer buffer = new StringBuffer();
        for (FieldError error : result.getFieldErrors()) {
            StringBuffer bufferCodes = new StringBuffer("(");
            for (String code : error.getCodes()) {
                bufferCodes.append(String.format("%s ", code));
            }
            bufferCodes.append(")");
            buffer.append(String.format("[%s:%s:%s:%s]", error.getField(), error.getRejectedValue(), bufferCodes,
                    error.getDefaultMessage()));
        }
        return buffer.toString();
}

هذه دالة سبق أن تناولناها.

تستمر العملية [ajax-11A] على النحو التالي:


@RequestMapping(value = "/ajax-11A", method = RequestMethod.POST, consumes = "application/json")
    @ResponseBody
    public JsonResult10 ajax11A(@RequestBody @Valid PostAjax11A post, BindingResult bindingResult, Locale locale,    HttpServletRequest request, HttpServletResponse response) {
        // سياق Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // الرد
        JsonResult10 result = new JsonResult10();
        // هل الإرسال صالح؟
        if (bindingResult.hasErrors()) {
    ...
        }
        // يتم حفظ حقل الإدخال
        thymeleafContext.setVariable("value1", post.getValue1());
        thymeleafContext.setVariable("value2", post.getValue2());
        session.setSaisies(engine.process("vue-09-saisies", thymeleafContext));
        // إرسال الصفحة 2
        result.setContent(engine.process("vue-09-page2", thymeleafContext));
        return result;
}
  • السطران 13-14: يتم وضع القيم المرسلة في سياق Thymeleaf؛
  • السطر 15: باستخدام هذا السياق، يتم حساب العرض [vue-09-saisies] وإدراجه في الجلسة حتى يمكن إعادة إنشاؤه لاحقًا؛
  • السطر 17: يتم وضع الصفحة 2 في النتيجة التي سيتم إرسالها إلى العميل؛

الطريقة [vue-09-page2.xml] هي كما يلي:

  

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h2>Page 2</h2>
        <p>
            <h4>Valeurs saisies :</h4>
            <p>
                Chaîne de caractères :
                <span th:text="${value1}"></span>
            </p>
            <p>
                Nombre entier :
                <span th:text="${value2}"></span>
            </p>
            <a href="javascript:retourPage1()">Retour à la page 1</a>
        </p>
    </body>
</html>
  • في السطرين 9 و13، يتم عرض قيم [value1, value2] التي وضعتها الإجراء [/ajax-11A] في سياق Thymeleaf؛

7.5.9. معالجة استجابة الإجراء [/ajax-11A]

على جانب العميل، تتم معالجة استجابة الإجراء [/ajax-10] بواسطة الدالة [onSuccess]:


function onSuccess(data) {
    console.log("onSuccess");
    // المحتوى
    if (data.content) {
        content.html(data.content);
    }
    // الحقل 1
    if (data.zone1Active) {
        $("#zone1").show();
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
        }
    } else {
        $("#zone1").hide();
    }
    // هل الحقل 3 نشط؟
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // البيانات المدخلة؟
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // خطأ؟
    if (data.erreur) {
        erreur.text(data.erreur);
        erreur.show();
    } else {
        erreur.hide();
    }
}

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

مع وجود خطأ

في هذه الحالة، أرسلت العملية [/ajax-11A] استجابة jSON بالصيغة {"zone1":null, "zone3":null,"saisies":null,"erreur":erreur,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":null}. إذا اتبعنا الكود أعلاه، نلاحظ أن:

  • الحقل [content] لا يتغير. كان يحتوي على الصفحة رقم 1؛
  • يتم عرض المنطقة [Erreur
  • تُترك المناطق [Zone 1] و [Zone 3] و [Saisies] على حالها؛

بدون أخطاء

في هذه الحالة، أرسلت العملية [/ajax-11A] استجابة jSON بالصيغة {"zone1":null, "zone3":null,"saisies":null,"erreur":null,"zone1Active":false,"zone3Active":false,"content":content}. إذا اتبعنا الكود أعلاه، نلاحظ أن:

  • يتم عرض المنطقة [content]. وهي تحتوي على الصفحة رقم 2؛

فيما يلي ثلاثة أمثلة على التنفيذ:

حالة بها خطأ في التحقق من الصحة:

حالة تحتوي على خطأ POST:

هذا النوع من الأخطاء مختلف. نظرًا لأن Spring لم يتمكن من تحويل السلسلة jSON إلى النوع [PostAjax11A]، فقد أرجع استجابة HTTP مع [status=400]. لم يتم تنفيذ الإجراء [ajax-11A

حالة خالية من الأخطاء:

7.5.10. العودة إلى الصفحة رقم 1

الرابط [Retour vers la page 1] في الصفحة رقم 2 هو التالي:


<a href="javascript:retourPage1()">Retour à la page 1</a>

الطريقة JS [retourPage1] هي كما يلي:


// العودة إلى الصفحة 1
function retourPage1() {
    // نقوم بإجراء استدعاء Ajax يدويًا
    $.ajax({
        url : '/ajax-11B',
        headers : {
            'Accept' : 'application/json',
        },
        type : 'POST',
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}

وهي تقوم بإجراء POST، بدون قيمة مرسلة، إلى الإجراء [/ajax-11B].

7.5.11. الإجراء [/ajax-11B]

الإجراء [/ajax-11B] هو كما يلي:


    @RequestMapping(value = "/ajax-11B", method = RequestMethod.POST)
    @ResponseBody
    public JsonResult10 ajax11B(HttpServletRequest request, HttpServletResponse response) {
        // سياق Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // الاستجابة
        JsonResult10 result = new JsonResult10();
        // نعيد الصفحة 1 إلى حالتها الأصلية
        result.setContent(engine.process("vue-09-page1", thymeleafContext));
        result.setSaisies(session.getSaisies());
        result.setZone1(session.getZone1());
        result.setZone3(session.getZone3());
        result.setZone1Active(session.isZone1Active());
        result.setZone3Active(session.isZone3Active());
        return result;
}

يجب أن تعيد العملية إنشاء الصفحة رقم 1 مع مناطقها الثلاث [Zone1, Zone3, Erreur]:

  • السطر 9: يتم إدراج الصفحة رقم 1 في النتيجة؛
  • السطر 10: يتم إدراج منطقة الإدخالات في النتيجة؛
  • السطر 11: يتم إدراج المنطقة [Zone 1] في النتيجة؛
  • السطر 12: يتم إدراج المنطقة [Zone 3] في النتيجة؛
  • السطران 13-14: يتم إدراج حالة الحقول [Zone 1] و [Zone 3] في النتيجة؛

7.5.12. معالجة استجابة الإجراء [/ajax-11B]

تتم معالجة استجابة الإجراء [/ajax-11B] بواسطة الدالة [onSuccess]:


function onSuccess(data) {
    console.log("onSuccess");
    // المحتوى
    if (data.content) {
        content.html(data.content);
    }
    // المنطقة 1
    if (data.zone1Active) {
        $("#zone1").show();
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
        }
    } else {
        $("#zone1").hide();
    }
    // المنطقة 3 نشطة؟
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // البيانات المدخلة؟
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // خطأ؟
    if (data.erreur) {
        erreur.text(data.erreur);
        erreur.show();
    } else {
        erreur.hide();
    }
}

أرسلت العملية [/ajax-11B] استجابة jSON بالصيغة {"zone1":zone1, "zone3":zone3,"saisies":saisies,"erreur":null,"zone1Active":zone1Active، "zone3Active":zone3Active، "content":content}. إذا تتبعنا الكود أعلاه، نلاحظ أن:

  • تم تعديل المنطقة [content]. كانت تحتوي على الصفحة رقم 2. وستحتوي الآن على الصفحة رقم 1؛
  • تم إخفاء المنطقة [Erreur
  • يتم عرض المناطق [Zone 1] و [Zone 3] و [Saisies] كما كانت؛

7.6. إدارة الجلسة من جانب العميل

7.6.1. مقدمة

في الفقرة السابقة، قمنا بإدارة جلسة عمل كانت بنيتها كما يلي:


public class SessionModel1 implements Serializable {

    // عدادان
    private int cpt1 = 0;
    private int cpt3 = 0;
    // المناطق الثلاث
    private String zone1 = "xx";
    private String zone3 = "zz";
    private String saisies;
    private boolean zone1Active = true;
    private boolean zone3Active = true;
...
}

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

7.6.2. الإجراء [/ajax-12]

  

الإجراء [/ajax-12] هو كما يلي:


    @RequestMapping(value = "/ajax-12", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax12() {
        return "vue-12";
}

العرض [vue-12.xml] هو كما يلي:

  

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>Ajax-12</title>
        <link rel="stylesheet" href="/css/ajax01.css" />
        <script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="/js/json3.js"></script>        
        <script type="text/javascript" src="/js/local12.js"></script>
    </head>
    <body>
        <h3>Ajax - 12 - Navigation dans une Application à Page Unique</h3>
        <h3>avec des flux HTML embarqués dans une chaîne jSON</h3>
        <h3>et une session gérée par le client JS</h3>
        <hr />
        <div id="content" th:include="vue-09-page1" />
        <img id="loading" src="/images/loading.gif" />
        <div id="erreur" style="background-color:lightgrey"></div>
    </body>
</html>
  • هذه العرضة مطابقة للعرضة [vue-09] باستثناء البرنامج النصي JS المستخدم في السطر 9؛

العرض المعروض هو كما يلي:

 

7.6.3. الرمز JS الخاص بإدارة الزر [Rafraîchir]

  

رمز الملف [local12.js] هو التالي:


// المتغيرات العامة
var content;
var loading;
var erreur;
var page1;
var page2;
var value1;
var value2;
var session = {
        "cpt1" : 0,
        "cpt3" : 0
    };

// عند تحميل المستند
$(document).ready(function() {
    // يتم استرداد مراجع المكونات المختلفة للصفحة
    loading = $("#loading");
    loading.hide();
    erreur = $("#erreur");
    erreur.hide();
    content = $("#content");
});
  • الأسطر 17-21: عند تحميل الصفحة الرئيسية، يتم تخزين مراجع المكونات الثلاثة المحددة بواسطة [loading, erreur, content] في المتغيرات العامة في الأسطر 2-4؛
  • الأسطر 5-6: لتخزين الصفحتين؛
  • الأسطر 7-8: لتخزين القيمتين المرسلتين عبر الرابط [Valider
  • السطر 9: الجلسة. تقوم بتخزين قيم العدادات [cpt1, cpt3] على جانب العميل؛

تتولى الدالة [postForm] معالجة النقر على الزر [Rafraîchir]:


function postForm() {
    console.log("postForm");
    // يتم تعيين الجلسة
    var post = JSON3.stringify(session);
    // يتم إجراء استدعاء Ajax يدويًا
    $.ajax({
        url : '/ajax-13',
        headers : {
            'Accept: 'application/json',
            'Content-Type' : 'application/json'
        },
        type : 'POST',
        data : post,
        dataType : 'json',
        beforeSend : onBegin,
        success : function(data) {
            ...
        },
        error : onError,
        complete : onComplete
    })
}

الاختلافات عن الإصدار السابق هي كما يلي:

  • تختلف الدالة URL في السطر 7؛
  • السطر 4: يتم إرسال قيمة بينما لم يكن يتم إرسال أي قيمة في السابق. هذه القيمة هي السلسلة jSON الخاصة بالجلسة. المبدأ هو كما يلي:
    • يرسل العميل الجلسة إلى الخادم،
    • ويقوم الخادم بتعديلها وإعادتها إليه،
    • ويحفظ العميل الجلسة الجديدة؛
  • السطر 10: يتم إرسال مستند بتنسيق jSON (القيمة المرسلة)؛
  • السطر 13: لدينا شيء ما لنرسله؛
  • الأسطر 15-20: وظائف [beforeSend, error, complete] هي نفسها الموجودة في الإصدار السابق. فقط الوظيفة [success] هي التي تتغير (الأسطر 16-18)؛

7.6.4. الإجراء [/ajax-13]

  

الإجراء [/ajax-13] هو كما يلي:


    @RequestMapping(value = "/ajax-13", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody()
    public JsonResult13 ajax13(@RequestBody SessionModel2 session2, HttpServletRequest request,    HttpServletResponse response) {
    ...
}
  • السطر 3: المعلمة [@RequestBody SessionModel2 session2] تسترد الجلسة التي أرسلها العميل. وهذه الجلسة من النوع [SessionModel2] التالي:
  

package istia.st.springmvc.models;

import java.io.Serializable;

public class SessionModel2 implements Serializable {

    private static final long serialVersionUID = 1L;
    // عدادان
    private int cpt1 = 0;
    private int cpt3 = 0;

    // وظائف الاسترجاع والتعيين
    ...
}

تخزن الجلسة [SessionModel2] العناصر التالية:

  • السطر 9: عدد المرات التي يتم فيها عرض الحقل [Zone 1]؛
  • السطر 10: عدد المرات [cpt3] التي يتم فيها عرض المنطقة [Zone 3]؛

لنواصل دراسة كود الإجراء [/ajax-13]:


    @RequestMapping(value = "/ajax-13", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody()
    public JsonResult13 ajax13(@RequestBody SessionModel2 session2, HttpServletRequest request,    HttpServletResponse response) {
    ...
}
  • السطر 3، نوع الاستجابة [JsonResult13] هو كما يلي:
  

package istia.st.springmvc.models;

public class JsonResult13 {

    // البيانات
    private String page2;
    private String zone1;
    private String zone3;
    private String erreur;
    private String value1;
    private Integer value2;

    // الجلسة
    private SessionModel2 session;

    // وظائف الاسترجاع والتعيين
    ...
}
  • السطر 14: الجلسة. يقوم الخادم بإعادة إرسالها إلى العميل لتخزينها؛
  • السطر 6: محتوى الصفحة رقم 2 HTML؛
  • السطر 7: محتوى HTML للمنطقة [Zone 1]؛
  • السطر 8: محتوى HTML للمنطقة [Zone 3]؛
  • السطر 9: رسالة الخطأ (إن وجدت)؛
  • السطران 10-11: معلومتان يحسبهما الخادم وتعرضهما الصفحة رقم 2؛

لنواصل دراسة كود الإجراء [/ajax-13]:


@RequestMapping(value = "/ajax-13", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody()
    public JsonResult13 ajax13(@RequestBody SessionModel2 session2, HttpServletRequest request,
            HttpServletResponse response) {
        // سياق Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // الاستجابة
        JsonResult13 result = new JsonResult13();
        result.setSession(session2);
        // نقوم بإرجاع استجابة عشوائية
        int cas = new Random().nextInt(3);
        switch (cas) {
        case 0:
            // المنطقة 1 نشطة
            setZone1B(thymeleafContext, result);
            return result;
        case 1:
            // المنطقة 3 نشطة
            setZone3B(thymeleafContext, result);
            return result;
        case 2:
            // المنطقتان 1 و3 نشطتان
            setZone1B(thymeleafContext, result);
            setZone3B(thymeleafContext, result);
            return result;
        }
        return null;
    }
  • السطر 9: يتم إدراج الجلسة في نتيجة الإجراء؛

الطريقة [setZone1B] التي تنشط المنطقة [Zone 1] هي كما يلي:


    private void setZone1B(WebContext thymeleafContext, JsonResult13 result) {
        // يتم استرداد الجلسة
        SessionModel2 session = result.getSession();
        // المنطقة 1 نشطة
        // التدفق HTML
        int cpt1 = session.getCpt1() + 1;
        thymeleafContext.setVariable("cpt1", cpt1);
        thymeleafContext.setLocale(new Locale("fr", "FR"));
        String zone1 = engine.process("vue-09-zone1", thymeleafContext);
        result.setZone1(zone1);
        // الجلسة
        session.setCpt1(cpt1);
}
  • السطر 3: يتم استرداد الجلسة. سيتم تعديلها في السطر 12 باستخدام العداد الجديد [cpt1]. تجدر الإشارة إلى أن هذه الجلسة سيتم إرسالها إلى العميل؛
  • السطر 10: المنطقة الجديدة [Zone 1]؛

الطريقة [setZone3B] التي تنشط الحقل [Zone 3] مماثلة:


private void setZone3B(WebContext thymeleafContext, JsonResult13 result) {
        // يتم استرداد الجلسة
        SessionModel2 session = result.getSession();
        // المنطقة 3 نشطة
        // التدفق HTML
        int cpt3 = session.getCpt3() + 1;
        thymeleafContext.setVariable("cpt3", cpt3);
        thymeleafContext.setLocale(new Locale("en", "US"));
        String zone3 = engine.process("vue-09-zone3", thymeleafContext);
        result.setZone3(zone3);
        // جلسة
        session.setCpt3(cpt3);
    }

7.6.5. معالجة استجابة الإجراء [/ajax-13]

على جانب العميل، تتم معالجة الاستجابة jSON للإجراء [/ajax-13] بواسطة الدالة [onSuccess] التالية:


function postForm() {
    console.log("postForm");
    // نقوم بنشر الجلسة
    var post = JSON3.stringify(session);
    // يتم إجراء استدعاء Ajax يدويًا
    $.ajax({
    ...
        success : function(data) {
            // يتم حفظ الجلسة
            session = data.session;
            // يتم تحديث الحقلين
            if (data.zone1) {
                $("#zone1-content").html(data.zone1);
                $("#zone1").show();
            } else {
                $("#zone1").hide();
            }
            if (data.zone3) {
                $("#zone3").show();
                $("#zone3-content").html(data.zone3);
            } else {
                $("#zone3").hide();
            }
        },
...
    })
}
  • الأسطر 12-17: إذا كان الخادم قد أدخل شيئًا في الحقل [zone1] في الرد، فيجب إعادة إنشاء المنطقة [Zone 1] وعرضها، وإلا فيجب إخفاؤها؛
  • الأسطر 18-23: ينطبق نفس المنطق على المنطقة [Zone 3]؛

7.6.6. عرض الصفحة [Page 2]

الرمز HTML للرابط [Valider] هو كما يلي:


<a href="javascript:valider()">Valider</a>

الوظيفة JS [valider] هي كما يلي:


// التحقق من صحة القيم المدخلة
function valider() {
    // يتم حفظ الصفحة 1
    page1 = content.html();
    // يتم حفظ القيم التي تم إدخالها
    value1 = $("#text1").val().trim();
    value2 = $("#text2").val().trim();
    // القيمة تم تسجيلها
    var post = JSON3.stringify({
        "value1" : value1,
        "value2" : value2,
        "pageRequired" : page2 ? false : true
    });
    // يتم إجراء استدعاء Ajax يدويًّا
    $.ajax({
        url : '/ajax-14',
        headers : {
            'Accept: 'application/json',
            'Content-Type: 'application/json'
        },
        type : 'POST',
        data : post,
        dataType : 'json',
        beforeSend : onBegin,
        success : function(data) {
        ...
        },
        error : onError,
        complete : onComplete
    })
}
  • سنقوم بإنشاء POST الذي سيقوم عادةً بنقلنا إلى الصفحة رقم 2؛
  • السطر 4: نقوم بحفظ الصفحة رقم 1 حتى نتمكن من العودة إليها لاحقًا؛
  • السطران 6-7: العملية السابقة لا تحفظ القيم التي تم إدخالها، بل تحفظ فقط رمز الصفحة HTML. لذا، نقوم الآن بحفظ القيمتين اللتين تم إدخالهما في النموذج؛
  • السطران 9-13: يتم وضع القيمتين المدخلتين في سلسلة jSON. وهذه هي السلسلة التي سيتم إرسالها؛
  • السطر 12: معلمة لإعلام الخادم بما إذا كنا بحاجة إلى الصفحة رقم 2. سنقوم بذلك على النحو التالي: سنطلب الصفحة رقم 2 مرة واحدة، ثم نخزنها في المتغير JS [page2]. بعد ذلك، لن نطلبها مرة أخرى. سنستخدم الصفحة المخزنة في ذاكرة التخزين المؤقت. في السطر 2، تساوي قيمة [pageRequired] قيمة [true] إذا كانت المتغير [page2] فارغة، وإلا فإنها تساوي قيمة [false
  • تجدر الإشارة إلى أن الجلسة لم يتم إرسالها. ففي الواقع، تقوم هذه الجلسة بتخزين عدادات لا تقوم الإجراء [/ajax-14] في السطر 20 بتعديلها؛

7.6.7. الإجراء [/ajax-14]

الإجراء [/ajax-14] هو كما يلي:


@RequestMapping(value = "/ajax-14", method = RequestMethod.POST)
    @ResponseBody
    public JsonResult13 ajax14(@RequestBody @Valid PostAjax14 post, BindingResult bindingResult, Locale locale,    HttpServletRequest request, HttpServletResponse response) {
        ...
    }
  • السطر 3: لا تزال الاستجابة من النوع [JsonResult13
  • السطر 3: القيمة المرسلة مغلفة في النوع [PostAjax14] التالي:

package istia.st.springmvc.models;

public class PostAjax14 extends PostAjax11A {

    // الصفحة 2
    private boolean pageRequired;

    // دالات الاسترجاع والتعيين
    ...
}
  • السطر 3: الفئة [PostAjax14] تمتد من الفئة [PostAjax11A] من الإصدار السابق. وبالتالي، فإنها تتمتع ببنية [value1, value2, pageRequired

تستمر العملية [/ajax-14] على النحو التالي:


    @RequestMapping(value = "/ajax-14", method = RequestMethod.POST)
    @ResponseBody
public JsonResult13 ajax14(@RequestBody @Valid PostAjax14 post, BindingResult bindingResult, Locale locale,    HttpServletRequest request, HttpServletResponse response) {
        // سياق Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // الاستجابة
        JsonResult13 result = new JsonResult13();
        // هل الإرسال صالح؟
        if (bindingResult.hasErrors()) {
            // إرجاع خطأ
            result.setErreur(getErreursForModel(bindingResult));
            return result;
        }
        // إرسال الصفحة 2
        result.setValue1(post.getValue1());
        result.setValue2(post.getValue2());
        // هل الصفحة مطلوبة؟
        if (post.isPageRequired()) {
            result.setPage2(engine.process("vue-12-page2", thymeleafContext));
        }
        return result;
}
  • الأسطر 9-13: إذا كانت القيم المرسلة [value1, value2] غير صالحة، يتم إرجاع رسالة خطأ؛
  • الأسطر 15-16: عادةً، ينبغي أن يقوم الخادم بإجراء عملية حسابية باستخدام القيم المرسلة. هنا، يكتفي بإعادة إرسالها لإثبات أنه قد استلمها بالفعل؛
  • الأسطر 18-20: لا يتم إرجاع الصفحة رقم 2 إلا إذا طلبها العميل. في السطر 19، العرض [vue-12-page2] جديد:
 

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h2>Page 2</h2>
        <p>
            <h4>Valeurs saisies :</h4>
            <p>
                Chaîne de caractères :
                <span id="value1"></span>
            </p>
            <p>
                Nombre entier :
                <span id="value2"></span>
            </p>
            <a href="javascript:retourPage1()">Retour à la page 1</a>
        </p>
    </body>
</html>
  • لم يعد الرمز XML يحتوي على قيم يتم تقييمها بواسطة Thymeleaf كما كان الحال سابقًا؛
  • وقد تم تحديد المواقع التي يجب وضع القيم التي يعيدها الخادم [value1, value2] فيها. السطر 9، يشير [id='value1'] إلى المكان الذي يجب وضع [value1] فيه. السطر 13، الأمر نفسه بالنسبة لـ [value2

7.6.8. معالجة استجابة الإجراء [/ajax-14]

تتم معالجة استجابة الإجراء [/ajax-14] بواسطة الدالة التالية [success]:


// التحقق من صحة القيم المدخلة
function valider() {
    ...
    // يتم إجراء استدعاء Ajax يدويًا
    $.ajax({
        ...
        success : function(data) {
            // خطأ؟
            if (data.erreur) {
                // عرض الخطأ
                erreur.html(data.erreur);
                erreur.show();
            } else {
                // لا توجد أخطاء
                erreur.hide();
                // الصفحة 2
                if (page2) {
                    // يتم استخدام الصفحة المخزنة في ذاكرة التخزين المؤقت
                    content.html(page2);
                } else {
                    // يتم حفظ الصفحة 2
                    page2 = data.page2;
                    // يتم عرضها
                    content.html(data.page2);
                }
                // يتم تحديثها بالمعلومات الواردة من الخادم
                $("#value1").text(data.value1);
                $("#value2").text(data.value2);
            }
        },
...
    })
}
  • الأسطر 9-13: إذا أرسل الخادم خطأً، يتم عرضه؛
  • الأسطر 14-29: في حالة عدم وجود خطأ. يجب عندئذ عرض الصفحة رقم 2؛
  • السطر 17: نتحقق مما إذا كانت الصفحة رقم 2 مسجلة بالفعل في المتغير [page2
  • السطر 19: في هذه الحالة، نستخدم المتغير [page2] لعرض الصفحة رقم 2؛
  • السطر 24: وإلا، نستخدم الحقل [data.page2] المقدم من الخادم؛
  • السطر 22: نحرص على حفظ الصفحة رقم 2 حتى لا نضطر إلى طلبها مرة أخرى لاحقًا؛
  • السطران 27-28: في الصفحة رقم 2، يتم عرض المعلوماتتين [value1, value2] المرسلتين من الخادم؛

7.6.9. العودة إلى الصفحة رقم 1

الرابط [Retour vers la page 1] في الصفحة رقم 2 هو التالي:


<a href="javascript:retourPage1()">Retour à la page 1</a>

الطريقة JS [retourPage1] هي كما يلي:


// العودة إلى الصفحة 1
function retourPage1() {
    // إعادة إنشاء الصفحة 1
    content.html(page1);
    // إعادة إنشاء البيانات المدخلة
    $("#text1").val(value1);
    $("#text2").val(value2);
}
  • وهي عملية JS لا تتطلب تفاعلًا مع الخادم لأن الصفحة رقم 1 تم تخزينها محليًا في المتغير [page1
  • السطر 4: يتم إعادة إنشاء الصفحة رقم 1؛
  • السطران 6 و7: لم يتم تخزين سوى الجزء HTML من الصفحة رقم 1. ولم يتم تخزين البيانات المدخلة. لذا يجب إعادة إنشاء هذه البيانات؛

7.6.10. الخلاصة

من خلال الاستفادة من إمكانيات النموذج APU، نجحنا في تبسيط خادم الويب الذي أصبح الآن عديم الحالة (بدون جلسة عمل) وأقل إجهادًا:

  • لقد أزلنا التفاعل مع الخادم في الدالة (JS [retourPage1])؛
  • لا يقوم الخادم بإنشاء الصفحة رقم 2 إلا مرة واحدة؛

7.7. تنظيم كود جافا سكريبت في طبقات

7.7.1. مقدمة

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

وستكون البنية الجديدة كما يلي:

7.7.2. صفحة البداية

الإجراء الذي يقوم بتشغيل التطبيق هو الإجراء [/ajax-16] التالي:


    @RequestMapping(value = "/ajax-16", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax16() {
        return "vue-16";
}

وهي تعرض العرض التالي: [vue-16.xml]:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>Ajax-12</title>
        <link rel="stylesheet" href="/css/ajax01.css" />
        <script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="/js/json3.js"></script>
        <script type="text/javascript" src="/js/local16-dao.js"></script>
        <script type="text/javascript" src="/js/local16-ui.js"></script>
    </head>
    <body>
        <h3>Ajax - 16 - Navigation dans une Application à Page Unique</h3>
        <h3>Structuration du code JS</h3>
        <hr />
        <div id="content" th:include="vue-09-page1" />
        <img id="loading" src="/images/loading.gif" />
        <div id="erreur" style="background-color:lightgrey"></div>
    </body>
</html>
  • السطران 9-10: تم وضع الكود JS في ملفين مختلفين:
    • يُنفذ الرمز [local-ui] الطبقة [présentation
    • [local-dao] ينفذ الطبقة [DAO
  

7.7.3. تنفيذ الطبقة [DAO]

7.7.4. الواجهة

ستقدم الطبقة [DAO] في [local-dao.js] الواجهة التالية للطبقة [présentation]:


function updatePage1(deferred, sendMeBack)
لتحديث الصفحة 1 باستخدام الزر [Rafraîchir]

function getPage2(deferred, sendMeBack, value1, value2, pageRequired)
لعرض الصفحة 2 باستخدام الزر [Valider]

لا يوجد مفهوم "الواجهة" في جافا سكريبت. لقد استخدمت هذا المصطلح ببساطة للإشارة إلى أن الطبقة [présentation] تلتزم بالتواصل مع الطبقة [DAO] فقط عبر الدالتين السابقتين.

7.7.5. تنفيذ الواجهة

الهيكل الأساسي للتنفيذ هو كما يلي:


var session = {
    "cpt1" : 0,
    "cpt3" : 0
};

// تحديث الصفحة 1
function updatePage1(deferred, sendMeBack) {
...
}

// الصفحة 2
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
...
}

الغرض من الطبقة [DAO] هو إخفاء تفاصيل الطلبات HTTP الموجهة إلى خادم الويب عن الطبقة [présentation]. وتعد الجلسة جزءًا من هذه التفاصيل. ولذلك، أصبحت تُدار الآن بواسطة الطبقة [DAO].

7.7.5.1. الدالة [updatePage1]

الدالة [updatePage1] هي الدالة التي تستدعيها الطبقة [présentation] لتحديث الصفحة 1. وفيما يلي شفرة هذه الدالة:


// تحديث الصفحة 1
function updatePage1(deferred, sendMeBack) {
    // الاستعلام HTTP
    executePost(deferred, sendMeBack, '/ajax-13', session);
}
  • السطر 1: تتلقى الدالة [updatePage1] معلمتين:
    1. كائن من النوع [jQuery.Deferred]. هذا النوع من الكائنات يخزن حالة يمكن أن تتخذ ثلاث قيم ['pending', 'resolved', 'rejected']. عند وصوله إلى الدالة [updatePage1]، يكون في الحالة [pending
    2. كائن JS ليتم إرجاعه إلى الطبقة [présentation

يتم تنفيذ جميع الاستعلامات HTTP بواسطة الوظيفة التالية [executePost]:


// طلب HTTP
function executePost(deferred, sendMeBack, url, post) {
    // يتم إجراء استدعاء Ajax يدويًا
    $.ajax({
        headers : {
            'Accept: 'application/json',
            'Content-Type' : 'application/json'
        },
        url : url,
        type : 'POST',
        data : JSON3.stringify(post),
        dataType : 'json',
        success : function(data) {
            // يتم حفظ الجلسة
            if (data.session) {
                session = data.session;
            }
            // نقوم بعرض النتيجة
            deferred.resolve({
                "status" : 1,
                "data" : data,
                "sendMeBack" : sendMeBack
            });
        },
        error : function(jqXHR) {
            // عرض الخطأ
            deferred.resolve({
                "status" : 2,
                "data" : jqXHR.responseText,
                "sendMeBack" : sendMeBack
            });
        }
    });
}
  • السطر 1: تقوم الدالة [executePost] بتنفيذ استدعاء Ajax من النوع POST. وهي تتوقع أربعة معلمات:
    1. كائن من النوع [jQuery.Deferred] في الحالة [pending
    2. كائن من نوع JS ليتم إرجاعه في الطبقة [présentation
    3. URL من POST؛
    4. القيمة المراد نشرها ككائن JS؛
  • الأسطر 5-8: وظيفة الإرسال لـ jSON (السطر 7) وتستقبل من jSON (السطر 6)؛
  • السطر 11: يتم تحويل القيمة المراد إرسالها إلى jSON؛
  • الأسطر 13-24: الدالة التي يتم تنفيذها في حالة نجاح استدعاء Ajax؛
  • الأسطر 19-23: إذا أرسل الخادم جلسة عمل، يتم تخزينها؛
  • الأسطر 13-18: يتم تمرير الكائن [deferred] إلى الحالة [resolved] مع تمرير نتيجة تحتوي على الحقول التالية:
    • [status]: تساوي 1 في حالة النجاح، و2 في حالة الفشل،
    • [data]: استجابة الخادم jSON،
    • [sendMeBack]: المعلمة الثانية للدالة، وهي كائن يرغب المستدعي في استرداده؛
  • الأسطر 17-31: الدالة التي يتم تنفيذها في حالة فشل استدعاء Ajax. يتم تنفيذ نفس الإجراء كما في السابق مع اختلافين:
    • يتم تعيين قيمة [status] إلى 2 للإشارة إلى وجود خطأ؛
    • [data] هي مرة أخرى استجابة jSON من الخادم، ولكن تم الحصول عليها بطريقة مختلفة؛

7.7.5.2. الوظيفة [getPage2]

الوظيفة [getPage2] هي كما يلي:


// الصفحة 2
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
    // الطلب HTTP
    executePost(deferred, sendMeBack, '/ajax-14', {
        "value1" : value1,
        "value2" : value2,
        "pageRequired" : pageRequired,
    });
}
  • تستقبل الدالة المعلمات التالية:
    1. [deferred]: كائن من النوع [jQuery.Deferred] في الحالة [pending
    2. [sendMeBack]: كائن من نوع JS ليتم إرجاعه في الطبقة [présentation
    3. [value1]: الإدخال الأول في الصفحة 1،
    4. [value2]: الإدخال الثاني في الصفحة 2،
    5. [pageRequired]: قيمة منطقية تُشير إلى الخادم بما إذا كان عليه إرسال تدفق HTML من الصفحة رقم 2 أم لا؛
  • يتم استدعاء الدالة [executePost] لتنفيذ الاستعلام HTTP المطلوب؛

7.7.6. الطبقة [présentation]

يتم تنفيذ الطبقة [présentation] بواسطة الملف [local-ui.js]. ويستمد هذا الملف الكود من الملف [local12.js] بعد إعادة صياغته لاستخدام الطبقة السابقة [DAO]. لم تتغير سوى وظيفتين: [postForm] و [valider].

7.7.6.1. الدالة [postForm]

الدالة [postForm] هي كما يلي:


// تحديث الصفحة 1
function postForm() {
    // يتم تحديث الصفحة 1
    var deferred = $.Deferred();
    loading.show();
    updatePage1(deferred, {
        'المرسل: "postForm"،
        '«المعلومات»: 10
    });
    // عرض النتائج
    deferred.done(postFormDone);
}
  • السطر 4: يتم إنشاء كائن [jQuery.Deferred]. بشكل افتراضي، يكون في الحالة [pending
  • السطر 5: يتم عرض صورة الانتظار
  • الأسطر 6-9: يتم تنفيذ الدالة [updatePage1]. يتم تمرير كائن وهمي [sendMeBack]، فقط لإظهار الغرض من ذلك؛
  • السطر 11: المعلمة الخاصة بالدالة [deferred.done] هي دالة بحد ذاتها. وهي الدالة التي يجب تنفيذها عندما ينتقل حالة الكائن [deferred] إلى الحالة [resolved]. لقد رأينا للتو أن الدالة DAO [executePost] كانت تنقل حالة هذا الكائن إلى [resolved] عند استلام الرد من الخادم. وهذا يعني أنه عند تنفيذ الدالة [postFormDone]، تكون استجابة الخادم قد تم استلامها؛

الدالة [postFormDone] هي كما يلي:


function postFormDone(result) {
    // نهاية الانتظار
    loading.hide();
    // جريان استرداد البيانات
    var data = result.data
    // للعرض التوضيحي
    console.log(JSON3.stringify(result.sendMeBack));
    // تحليل الحالة
    switch (result.status) {
    case 1:
        // تحديث الحقلين
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
            $("#zone1").show();
        } else {
            $("#zone1").hide();
        }
        if (data.zone3) {
            $("#zone3").show();
            $("#zone3-content").html(data.zone3);
        } else {
            $("#zone3").hide();
        }
        break;
    case 2:
        // عرض الخطأ
        erreur.html(data);
        break;
    }
}
  • السطر 1: المعلمة [result] المستلمة هي المعلمة التي تم تمريرها إلى الأسلوب [deferred.resolve] في الدالة [executePost]، على سبيل المثال:

            // عرض النتيجة
            deferred.resolve({
                "status" : 1,
                "data" : data,
                "sendMeBack" : sendMeBack
});
  • السطر 5: يتم استرداد الاستجابة من الخادم؛
  • الأسطر 10-24: يوجد هنا الكود الذي كان موجودًا في الإصدار السابق ضمن الدالة [onSuccess] التابعة للدالة [postForm
  • الأسطر 25-28: نجد الكود الذي كان موجودًا في الإصدار السابق ضمن الدالة [onError] التابعة للدالة [postForm

7.7.6.2. دور المعلمة [sendMeBack]

ما الغرض من المعلمة [sendMeBack]؟ لنلقِ نظرة على كود استدعاء الدالة [updatePage1]:


// تحديث الصفحة 1
function postForm() {
    // يتم تحديث الصفحة 1
    var deferred = $.Deferred();
    loading.show();
    updatePage1(deferred, {
        'المرسل: "postForm"،
        '«المعلومات»: 10
    });
    // عرض النتائج
    deferred.done(postFormDone);
}

وتوقيع الدالة [validerDone]:


function postFormDone(result) {
}

كيف يمكن للدالة [postForm] أن تنقل المعلومات إلى الدالة [postFormDone]؟ هذه الأخيرة لا تحتوي إلا على معلمة واحدة هي [result]. ويتم إنشاء هذا المعامل بواسطة الدالة [executePost] من الطبقة [DAO]. لنقل المعلومات إلى الدالة [postFormDone]، يجب أولاً أن تقوم الدالة [postForm] بنقلها إلى الدالة [updatePage1]. وهذا هو دور المعلمة [sendMeBack]. وتُستخدم على النحو التالي:


function postFormDone(result) {
    // نهاية الانتظار
    loading.hide();
    // يتم استرداد البيانات
    var data = result.data
    // للعرض التوضيحي
    console.log(JSON3.stringify(result.sendMeBack));
    // تحليل الحالة
    switch (result.status) {
...
  • السطر 7، استرجعت الدالة [postFormDone] المعلمة [sendMeBack] التي تم إرسالها في البداية إلى الدالة DAO [updatePage1] بواسطة الدالة [postForm

7.7.7. الدالة [valider]

الدالة [valider] هي كما يلي:


// التحقق من صحة القيم المدخلة
function valider() {
    // تخزين الصفحة 1
    page1 = content.html();
    // تخزين القيم المدخلة
    value1 = $("#text1").val().trim();
    value2 = $("#text2").val().trim();
    // لا توجد أخطاء
    erreur.hide();
    // يتم طلب الصفحة 2
    var deferred = $.Deferred();
    loading.show();
    getPage2(deferred, {
        'المرسل: «تأكيد»،
        'info' : 20
    }, value1, value2, page2 ? false : true);
    // عرض النتائج
    deferred.done(validerDone);
}

والوظيفة [validerDone] (السطر 18) هي كما يلي:


function validerDone(result) {
    // نهاية الانتظار
    loading.hide();
    // يتم استرداد البيانات
    var data = result.data
    // للعرض التوضيحي
    console.log(JSON3.stringify(result.sendMeBack));
    // تحليل الحالة
    switch (result.status) {
    case 1:
        // خطأ؟
        if (data.erreur) {
            // عرض الخطأ
            erreur.html(data.erreur);
            erreur.show();
        } else {
            // لا توجد أخطاء
            erreur.hide();
            // الصفحة 2
            if (page2) {
                // يتم استخدام الصفحة المخزنة في ذاكرة التخزين المؤقت
                content.html(page2);
            } else {
                // يتم حفظ الصفحة 2
                page2 = data.page2;
                // يتم عرضها
                content.html(data.page2);
            }
            // يتم تحديثها بالمعلومات الواردة من الخادم
            $("#value1").text(data.value1);
            $("#value2").text(data.value2);
        }
        break;
    case 2:
        // عرض خطأ
        erreur.html(data);
        erreur.show();
        break;
    }
}
  • السطر 5: يتم استرداد استجابة الخادم؛
  • الأسطر 10-32: نجد الكود الذي كان موجودًا في الإصدار السابق في الدالة [onSuccess] التابعة للدالة [valider
  • الأسطر 34-38: نجد الكود الذي كان موجودًا في الإصدار السابق ضمن الدالة [onError] التابعة للدالة [valider

7.7.8. الاختبارات

يستمر التطبيق في العمل كما كان من قبل، وفي وحدة التحكم في Chrome، يمكن رؤية المعلمات [sendMeBack] للدالتين [postForm] و [valider]:

 

7.8. Conclusion

لنعد إلى المخطط العام لتطبيق Spring MVC:

بفضل جافا سكريبت المضمنة في الصفحات HTML والتي يتم تنفيذها في المتصفح، وبفضل النموذج APU، يمكن نقل الكود إلى المتصفح والحصول على البنية التالية:

  • لدينا بنية عميل [2] / خادم [1] حيث يتواصل العميل والخادم عبر jSON؛
  • في [1]، تقوم طبقة الويب Spring MVC بتقديم العروض وأجزاء العروض والبيانات في jSON؛
  • في [2]: يمكن تنظيم كود جافا سكريبت المضمن في العرض الذي يتم تحميله عند بدء تشغيل التطبيق إلى طبقات:
    • تتولى الطبقة [présentation] التعامل مع تفاعلات المستخدم،
    • الطبقة [DAO] تتولى الوصول إلى البيانات عبر خادم الويب [1]،
    • قد لا توجد الطبقة [métier] أو قد تتولى بعض الوظائف غير السرية للطبقة [métier] الخاصة بالخادم من أجل تخفيف العبء عنه؛
  • يمكن للعميل [2] تخزين بعض العروض مؤقتًا في ذاكرة التخزين المؤقت (الكاش) لتخفيف العبء عن الخادم أيضًا. وهو يدير الجلسة؛