Skip to content

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

7.1. دور AJAX في تطبيق الويب

حتى الآن، كانت الأمثلة التعليمية التي درسناها تتمتع بالبنية التالية:

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

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

هذا هو النمط الكلاسيكي:

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

منذ عدة سنوات، ظهر نمط آخر للتفاعل بين المتصفح وخادم الويب: AJAX (جافا سكريبت غير متزامن و XML). يتضمن هذا تفاعلات بين العرض الذي يعرضه المتصفح وخادم الويب. يواصل المتصفح القيام بما يجيده — عرض HTML — ولكنه الآن يخضع لسيطرة جافا سكريبت المضمنة في عرض HTML المعروض. الرسم التخطيطي كما يلي:

  • في [1]، يحدث حدث على الصفحة المعروضة في المتصفح (نقرة على زر، تغيير في النص، إلخ). يتم اعتراض هذا الحدث بواسطة JavaScript (JS) المضمن في الصفحة؛
  • في [2]، يقوم كود JavaScript بإرسال طلب HTTP تمامًا كما كان سيفعل المتصفح. الطلب غير متزامن: يمكن للمستخدم الاستمرار في التفاعل مع الصفحة دون أن يتم حظره أثناء انتظار استجابة HTTP. يتبع الطلب مسار المعالجة القياسي. لا شيء (أو القليل جدًا) يميزه عن الطلب القياسي؛
  • في [3]، يتم إرسال استجابة إلى عميل JS. وبدلاً من عرض HTML كامل، عادةً ما يتم إرسال عرض HTML جزئي أو موجز XML أو JSON (ترميز كائنات JavaScript
  • في [4]، يسترد JavaScript هذا الرد ويستخدمه لتحديث منطقة من صفحة 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) {
        // valid 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));
            }
        }
        // prepare the view model [view-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) {
        // valid tempo?
...
        // prepare the view model [view-01]
        modèle.addAttribute("actionModel01", new ActionModel01());
...
        // view
        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 {
 
    // posted data
    @NotNull
    @DecimalMin(value = "0.0")
    private Double a;
 
    @NotNull
    @DecimalMin(value = "0.0")
    private Double b;
 
    // getters and setters
    ...
}
  • السطران 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) {
...
        // prepare the view model [view-01]
        modèle.addAttribute("actionModel01", new ActionModel01());
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
...
        // view
        return "vue-01";
}
  • السطران 6-7: نضيف مثيلًا من النوع [Results] إلى النموذج؛

نوع [Results] الموضوع في النموذج هو كما يلي:

  

package istia.st.springmvc.models;
 
public class Resultats {
 
    // data
    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;
 
    // getters and setters
    ...
}
  • الأسطر 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) {
        ...
        // local
        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) {
        // we only manage fr-FR, en-US locales
        String language = locale.getLanguage();
        String country = null;
        switch (language) {
        case "fr":
            country = "FR";
            break;
        default:
            language = "en";
            country = "US";
            break;
        }
        // culture
        résultats.setCulture(String.format("%s-%s", language, country));
}

في القالب، ستكون السلسلة [${results.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) {
...
        // local
        setLocale(locale, modèle, résultats);
        // hour
        résultats.setHeureGet(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        // view
        return "vue-01";
    }
  • السطر 7: تعيين الوقت من طلب GET في القالب؛
  • السطر 9: نعرض العرض [vue-01.xml]:

7.2.3. الطريقة [view-01.xml]

الطريقة [view-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. تسمح لك هذه المكتبة أحيانًا بتجنب كتابة JavaScript؛
  • السطر 16: ملف JavaScript لاحتياجاتنا الخاصة؛
  • الأسطر 17–22: للتعامل مع الإعدادات المحلية [fr-FR] و [en-US] على جانب العميل. لقد صادفنا هذا الكود من قبل؛
  • السطر 27: رسالة مكونة. لقد درسنا هذه الرسائل في القسم 5.18؛
  • الأسطر 36–38: النموذج الذي سنعود إليه لاحقًا؛
  • السطر 40: منطقة المستند التي سيضع فيها JavaScript استجابة الخادم؛

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">

من الواضح على الفور أنه إذا تم تعطيل JavaScript في المتصفح الذي يعرض الصفحة، فسيتم إرسال النموذج إلى عنوان 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] بواسطة مكتبة JavaScript [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 لـ POST هو عنوان السمة [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"]: دالة JavaScript التي يجب تنفيذها قبل الإرسال؛
  • [data-ajax-complete="afterComplete"] : وظيفة JavaScript التي يجب تنفيذها عند استلام الرد؛
  • [data-ajax-update="#resultats"]: معرف المنطقة التي سيتم وضع النتيجة المرسلة من الخادم فيها. تحتوي طريقة العرض [vue-01.xml] على المنطقة التالية:

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

لاحظ أن JavaScript [submit] لن يحدث إلا إذا أعلنت أدوات التحقق من الصحة أن القيم التي تم اختبارها صالحة.

مكتبة JavaScript [unobtrusive-ajax] لها هدفان:

  • ضمان أن يتكيف النموذج بشكل صحيح مع كلتا الحالتين: سواء كان JavaScript ممكّنًا أو معطّلًا في المتصفح؛
  • تجنب كتابة JavaScript. سنرى أنه في هذه الحالة، لم يكن من الممكن تجنب ذلك.

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 {
        // tempo?
        Integer tempo = (Integer) session.getAttribute("tempo");
        if (tempo != null && tempo > 0) {
            Thread.sleep(tempo);
        }
        // prepare the model for the next view
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
        // we set the locale
        setLocale(locale, modèle, résultats);
        // hour
        résultats.setHeurePost(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        ...
}
  • سنبسط الأمور في الوقت الحالي: سنفترض أن طلب POST قد تم إرساله بالفعل بواسطة JavaScript في العرض [vue-01.xml]. سنعود إلى هذا الافتراض لاحقًا؛
  • السطر 2: يتم وضع القيم المرسلة [a,b] في النموذج [ActionModel01
  • الأسطر 4-7: إذا قام المستخدم بتعيين مهلة خلال طلب GET سابق، يتم استردادها من الجلسة ويتم تطبيق المهلة (السطر 6). والغرض من ذلك هو السماح للمستخدم برؤية تأثير السمة [data-ajax-loading="#loading"] في النموذج؛
  • السطران 9-10: تتم إضافة سمة [results] إلى النموذج؛
  • السطر 12: تتم إضافة الإعدادات المحلية [fr-FR] أو [en-US] إلى النموذج؛
  • السطر 14: نحدد وقت POST في النموذج؛

تذكر نوع [Resultats] المضاف إلى النموذج:


public class Resultats {
 
    // data
    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;
 
    // getters and setters
...
}

يستمر كود الإجراء [/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()));
        // we generate an error every other time
        int val = new Random().nextInt(2);
        if (val == 0) {
            // an error message is returned
            résultats.setErreur("erreur.aleatoire");
            return "vue-03";
        }
...
    }
  • الأسطر 6–11: في هذا المثال، نوضح كيفية إرجاع صفحة خطأ إلى عميل JavaScript. في نصف الحالات، نرجع العرض التالي [view-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="error" th:text="${resultats.erreur}"></span> -->
        </p>
    </body>
</html>
 
  • السطر 12، لاحظ رسالة تم تكوينها بواسطة مفتاح رسالة يتم حسابه بدوره. وقد قدمنا هذا المفهوم في القسم 5.18، الصفحة 170.

يستمر كود الإجراء [/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 {
...
        // retrieve posted values
        double a = formulaire.getA();
        double b = formulaire.getB();
        // we build the model
        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");
        }
        // the view is displayed
        return "vue-02";
    }
  • الأسطر 5–15: يتم تنفيذ العمليات الحسابية الأربع على الأرقام [a, b] وتغليفها في مثيل [Resultats] للنموذج؛
  • السطر 17: يتم إرجاع العرض التالي [view-02.xml]:

الطريقة [view-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. إرسال القيم المدخلة

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

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

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

  • [beforeSend]: دالة JavaScript يتم تنفيذها قبل إرسال POST؛
  • [afterComplete]: دالة JavaScript يتم تنفيذها عند استلام استجابة الإرسال (POST

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

  

يقوم ملف [local1.js] بتهيئة بيئة JavaScript لعرض [vue-01.xml] على النحو التالي:


// global data
var loading;
var formulaire;
var résultats;
var a, b;
 
// document loading
$(document).ready(function() {
    // retrieve the references of the page's various components
    loading = $("#loading");
    formulaire = $("#formulaire");
    resultats = $('#resultats');
    a = $("#a");
    b = $("#b");
    // we hide certain elements
    loading.hide();
    // parse the form validators
    $.validator.unobtrusive.parse(formulaire);
    // we manage two locales [fr_FR, en_US]
    // the reals [a,b] are sent by the server in Anglo-Saxon format
    // we put them in French format if necessary
    checkCulture(2);
});
  • السطر 22: يتم وصف الدالة [checkCulture] لاحقًا؛

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


function beforeSend(jqXHR, settings) {
    // before POST
    // numbers must be posted in Anglo-Saxon format
    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) {
        // we put the numbers [a,b] in Anglo-Saxon format
        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 هي معلمة دالة) تمثل السلسلة المنشورة. نعيد إنشاء هذه السلسلة باستخدام التعبير [form.serialize()]. يتجول هذا التعبير في النموذج للعثور على القيم المراد نشرها ويبني سلسلة POST. ثم سيأخذ القيم الجديدة لـ [a,b] مع النقاط العشرية؛

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


function beforeSend(jqXHR, settings) {
    // before POST
...
}
 
function afterComplete(jqXHR, settings) {
    // after POST
    // numbers must be supplied in French format if necessary
    var culture = Globalize.culture().name;
    if (culture === 'fr-FR') {
        checkCulture(2);
    }
}
 
function checkCulture(mode) {
    if (mode == 1) {
...
    }
    if (mode == 2) {
        // put the numbers in French format
        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. تعطيل JavaScript باستخدام الإعدادات المحلية [en-US]

ماذا يحدث إذا قمنا بتعطيل JavaScript في المتصفح؟

سيتم إرسال القيم المدخلة عبر POST وفقًا لعلامة [form]، حيث لن يتم استخدام سمات [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 request?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        ...
    }
  • السطر 4: يمكن الآن استدعاء الإجراء [/ajax-02] عبر طلب Ajax POST أو طلب POST قياسي. نحتاج إلى التمييز بين هاتين الحالتين. ونقوم بذلك باستخدام رؤوس HTTP المرسلة من متصفح العميل؛

عندما ننظر إلى حركة مرور الشبكة في Chrome DevTools (Ctrl-Shift-I) مع تمكين JavaScript، نرى أن العميل يرسل الرؤوس التالية أثناء طلب 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 request?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        // tempo?
        Integer tempo = (Integer) session.getAttribute("tempo");
        if (tempo != null && tempo > 0) {
            Thread.sleep(tempo);
        }
        // prepare the model for the next view
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
        // we set the locale
        setLocale(locale, modèle, résultats);
        // hour
        String heure = new SimpleDateFormat("hh:mm:ss").format(new Date());
        résultats.setHeurePost(heure);
        résultats.setHeureGet(heure);
        // valid request?
        if (!isAjax && result.hasErrors()) {
            return "vue-01";
        }
...
  • السطر 2: المعلمة [@Valid ActionModel01 form] تُشغّل أدوات التحقق من الصحة من جانب الخادم؛
  • الأسطر 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 request?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
        // valid request?
        if (!isAjax && result.hasErrors()) {
            return "vue-01";
        }
        // we generate an error every other time
        int val = new Random().nextInt(2);
        if (val == 0) {
            // an error message is returned
            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: سيتم إدراج العرض [view-03.xml] أسفل منطقة [results

إليك مثال على ذلك:

لاحظ أن الوقتين [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 request?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
        // retrieve posted values
        double a = formulaire.getA();
        double b = formulaire.getB();
        // we build the model
        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");
        }
        // the view is displayed
        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. تعطيل JavaScript مع الإعدادات المحلية [fr-FR]

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

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

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

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


<form ...>
...
</p>
    <!-- hidden fields -->
    <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 {
        // next handler
        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) {
        // posted values a and 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;
        }
        // other cases
        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] لعلامة [input] HTML التي تم إنشاؤها. هنا، نريد إدارة السمة [value] بأنفسنا. لذلك، نقوم أيضًا بتعيين سمات [id، name] بأنفسنا؛
  • تقوم السمة [th:value] بتقييم تعبير باستخدام المشغل الثلاثي ?. نختبر التعبير [${resultats.culture}=='fr-FR' and ${actionModel01.b}!=null]. إذا كان صحيحًا، فإننا نعيّن السمة [value] بقيمة [actionModel01.a]، حيث يتم استبدال النقطة العشرية بفاصلة. إذا كانت كاذبة، فإننا نعيّن السمة [value] بقيمة [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>

إليك مثال على ذلك:

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

7.2.10. التعامل مع رابط [Calculate]

دعونا نفحص رابط [Calculate] في الصفحة الرئيسية [vue-01.xml]:

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


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

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


// global data
var loading;
var formulaire;
var résultats;
var a, b;
 
function postForm() {
    // valid form?
    if (!formulaire.validate().form()) {
        // invalid form - terminated
        return;
    }
    // we manage two locales [fr_FR, en_US]
    // the real [a,b] must be posted in Anglo-Saxon format in all cases
    // they will be filtered by [CultureFilter]
 
    // make a manual Ajax call
    $.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: نقوم بتشغيل أدوات التحقق من صحة JavaScript الخاصة بالنموذج. إذا كانت أي من القيم غير صالحة، فإن التعبير [form.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: نقوم بتحديث المكون الذي يحمل المعرف [results] باستخدام HTML من المعلمة [data].
  • السطر 33: نخفي مؤشر التحميل؛
  • السطر 35: الدالة التي يتم تنفيذها عند استلام استجابة الخادم، بغض النظر عما إذا كانت ناجحة أم خطأ؛
  • الأسطر 35-37: في حالة حدوث خطأ (أعاد الخادم استجابة HTTP برمز حالة يشير إلى خطأ من جانب الخادم)، يتم عرض استجابة HTML للخادم في منطقة [results

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

7.3. تحديث صفحة HTML باستخدام موجز JSON

في المثال السابق، استجاب خادم الويب لطلب HTTP Ajax بتدفق HTML. احتوى هذا التدفق على بيانات مصحوبة بتنسيق HTML. سنعيد النظر في المثال السابق، ولكن هذه المرة باستخدام استجابات JSON (ترميز كائنات JavaScript) التي تحتوي على البيانات فقط. وتتمثل الميزة في نقل عدد أقل من البايتات. نفترض أن 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) {
        ...
        // view
        return "vue-04";
    }

7.3.2. الطريقة [view-04.xml]

 

يستخدم العرض [view-04.xml] نص العرض [view-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>
            <!-- hidden fields -->
            <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]، التي تحدد كيفية ومكان إرسال القيم المدخلة في النموذج. وذلك لأن النموذج سيتم إرساله بواسطة دالة JavaScript (السطر 20)؛
  • الأسطر 26–57: المنطقة التي تحمل المعرّف [resultats]، والتي كانت فارغة سابقًا، تحتوي الآن على كود HTML لعرض النتائج؛
  • الأسطر 26–34: عنوان النتائج حيث يتم عرض وقت الحساب؛
  • الأسطر 35–52: نتائج العمليات الحسابية الأربع؛
  • الأسطر 53–57: أي رسائل خطأ مرسلة من الخادم؛

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


// global data
    var loading;
    var formulaire;
    var résultats;
    var titre;
    var labelHeureCalcul;
    var heureCalcul;
    var aplusb;
    var amoinsb;
    var afoisb;
    var adivb;
    var msgErreur;
 
// document loading
$(document).ready(function() {
    // retrieve the references of the page's various components
    loading = $("#loading");
    formulaire = $("#formulaire");
    résultats = $('#résultats');
    titre=$("#titre");
    labelHeureCalcul=$("#labelHeureCalcul");
    heureCalcul=$("#heureCalcul");
    aplusb=$("#aplusb");
    amoinsb=$("#amoinsb");
    afoisb=$("#afoisb");
    adivb=$("#adivb");
    msgErreur=$("#msgErreur");
    // we hide certain elements
    résultats.hide();
    erreur.hide();
    loading.hide();
});
  • الأسطر 17–27: استرداد مراجع jQuery لجميع العناصر الموجودة على الصفحة؛
  • السطر 29: إخفاء منطقة النتائج؛
  • السطر 30: وكذلك منطقة الأخطاء؛
  • السطر 31: وكذلك صورة التحميل المتحركة؛
  • الأسطر 2–12: يتم جعل المراجع المسترجعة عالمية حتى تتمكن الوظائف الأخرى من الوصول إليها؛

7.3.3. وظيفة jS [postForm]

الرابط [Calculate] هو كما يلي:


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

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


function postForm() {
    // valid form?
    if (!formulaire.validate().form()) {
        // invalid form - terminated
        return;
    }
    // make a manual Ajax call
    $.ajax({
        url : '/ajax-05',
        headers : {
            'Accept' : 'application/json'
        },
        type : 'POST',
        data : formulaire.serialize(),
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}
 
// before the Ajax call
function onBegin() {
...
}
 
// on receipt of the server response
// in case of success
function onSuccess(data) {
...
}
 
// on receipt of the server response
// in case of failure
function onError(jqXHR) {
...
}
 
// after [onSuccess, onError]
function onComplete() {
...
}
  • الأسطر 3–6: قبل إرسال القيم المدخلة، نقوم بالتحقق من صحتها. إذا كانت غير صحيحة، لا نرسل النموذج؛
  • السطر 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] هي كما يلي:


// before the Ajax call
function onBegin() {
    console.log("onBegin");
    // we show the moving image
    loading.show();
    // hide certain elements of the view
    entete.hide();
    résultats.hide();
    erreur.hide();
}

قبل استكشاف وظائف JavaScript الأخرى لاستدعاء Ajax، نحتاج إلى معرفة الاستجابة المرسلة بواسطة الإجراء [/ajax-05].

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

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


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

دعونا نواصل فحص الإجراء [/ajax-05]:


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

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

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


        // on prépare le modèle de la prochaine vue
        JsonResults résultats = new JsonResults();
        ...
}
  • السطر 2: إنشاء قالب سلسلة JSON المرسلة إلى العميل؛

نموذج [JsonResults] هو كما يلي:

 

package istia.st.springmvc.models;
 
public class JsonResults {
 
    // data
    private String titre;
    private String labelHeureCalcul;
    private String heureCalcul;
    private String aplusb;
    private String amoinsb;
    private String afoisb;
    private String adivb;
    private String msgErreur;
 
    // getters and setters
...
 
}
  • الأسطر 6–13: كل حقل في فئة [JsonResult] يتوافق مع حقل يحمل نفس [id] في عرض [vue-04.xml]:

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


        // on prépare le modèle de la prochaine vue
        JsonResults résultats = new JsonResults();
        // entête
        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()));
        // on génère une erreur une fois sur deux
        int val = new Random().nextInt(2);
        if (val == 0) {
            // on renvoie un message d'erreur
            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] على النحو التالي:


        // on récupère les valeurs postées
        double a = formulaire.getA();
        double b = formulaire.getB();
        // on construit le modèle
        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");
        }
        // on rend le résultat
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 الذي أعاده الخادم، يمكننا استخدامه في JavaScript. طريقة [onSuccess]، التي يتم تنفيذها عندما يرسل الخادم استجابة بحالة HTTP [200]، هي كما يلي:


// on receipt of the server response
// in case of success
function onSuccess(data) {
    console.log("onSuccess");
    // fill in the results area
    titre.text(data.titre);
    labelHeureCalcul.text(data.labelHeureCalcul);
    heureCalcul.text(data.heureCalcul);
    entete.show();
    // error-free results
    if (!data.msgErreur) {
        aplusb.text(data.aplusb);
        amoinsb.text(data.amoinsb);
        afoisb.text(data.afoisb);
        adivb.text(data.adivb);
        résultats.show();
        return;
    }
    // results with error
    msgErreur.text(data.msgErreur);
    erreur.show();
}
  • السطر 3: المعلمة [data] هي كائن JSON الذي يعيده الخادم:
 

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


// on receipt of the server response
// in case of failure
function onError(jqXHR) {
    console.log("onError");
    // system error
    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 أبدًا ولا يقوم بتحميل صفحة جديدة أبدًا. يُطلق على هذا النوع من التطبيقات اسم "تطبيق صفحة واحدة" (SPA).

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

  • في [1]، تعرض الإجراء [/ajax-06] الصفحة الأولى، الصفحة 1؛
  • في [2]، يتيح لنا رابط الانتقال إلى الصفحة 2 عبر استدعاء Ajax؛
  • في [3]، لم يتغير عنوان URL. الصفحة المعروضة هي الصفحة 2؛
  • في [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] التالي:


// global data
var content;
 
function gotoPage(num) {
    // make a manual Ajax call
    $.ajax({
        url : '/ajax-07',
        type : 'POST',
        data : 'num=' + num,
        dataType : 'html',
        beforeSend : function() {
        },
        success : function(data) {
            content.html(data)
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // system error
            content.html(jqXHR.responseText);
        }
    })
}
 
// document loading
$(document).ready(function() {
    // retrieve the references of the page's various components
    content = $("#content");
});
  • السطر 28: عند تحميل الصفحة، نقوم بتخزين العنصر الذي يحمل المعرف [content] ونجعله متغيرًا عامًا (السطر 2)؛
  • السطر 4: تتلقى الدالة [gotoPage] كمعلمة رقم الصفحة (1 أو 2) المراد عرضها في العرض الحالي؛
  • السطر 7: عنوان URL الهدف لطلب POST؛
  • السطر 8: يتم طلب عنوان URL من السطر 7 عبر POST؛
  • السطر 9: السلسلة المرسلة. يتم إرسال معلمة باسم [num]. قيمتها هي رقم الصفحة (السطر 4) المراد عرضها في العرض الحالي؛
  • السطر 10: سيعيد الخادم HTML، وتحديدًا 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) {
        // num : page number
        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. طريقة العرض [view-08.xml]

تشكل طريقة العرض [view-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] على أربع مناطق:

  • [المنطقة 1] و[المنطقة 3] هما منطقتان تظهران أو تختفيان عند النقر على زر [تحديث]. نحسب عدد المرات التي تظهر فيها كل من هاتين المنطقتين [2]. تستخدم منطقة [المنطقة 1] اللغة الفرنسية، بينما تستخدم منطقة [المنطقة 3] اللغة الإنجليزية؛
  • [المنطقة 2] موجودة دائمًا؛
  • قسم [Entries] مرئي دائمًا؛

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

  • يعيد الرابط [العودة إلى الصفحة 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>
        <!-- zone 1 -->
        <fieldset id="zone1" style="background-color:pink">
            <legend>Zone 1</legend>
            <span id="zone1-content" th:text="xx">xx</span>
        </fieldset>
        <!-- zone 2 -->
        <fieldset id="zone2" style="background-color:lightgreen">
            <legend>Zone 2</legend>
            <span>Ce texte reste toujours présent</span>
        </fieldset>
        <!-- zone 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]. سيتم التعامل مع كل شيء في JavaScript.

الطريقة [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 على ميزتين:

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

سنركز أولاً على زر [تحديث].

7.5.4. كود JS الخاص بزر [تحديث]

  

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


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

تتعامل الدالة [postForm] مع النقر على زر [Refresh]:


function postForm() {
    console.log("postForm");
    // make a manual Ajax call
    $.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] هي كما يلي:


// before the Ajax call
function onBegin() {
    console.log("onBegin");
    // waiting image
    loading.show();
}

يعرض ببساطة صورة التحميل المتحركة أثناء انتظار استجابة الخادم.

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

  

تتمثل عملية [/ajax-10] فيما يلي:


// the session
    @Autowired
    private SessionModel1 session;
    // the Thymeleaf / Spring engine
    @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;
    // two meters
    private int cpt1 = 0;
    private int cpt3 = 0;
    // the three zones
    private String zone1 = "xx";
    private String zone3 = "zz";
    private String saisies;
    private boolean zone1Active = true;
    private boolean zone3Active = true;
 
    // getters and setters
    ...
}

تخزن الجلسة [SessionModel1] ما يلي:

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

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


    // the Thymeleaf / Spring engine
    @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: نحن على دراية بـ bean [SpringResourceTemplateResolver]، الذي يسمح لنا بتعريف خصائص معينة للعروض؛
  • الأسطر 13–17: يسمح لنا bean [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 {
 
    // data
    private String content;
    private String zone1;
    private String zone3;
    private String erreur;
    private String saisies;
    private boolean zone1Active;
    private boolean zone3Active;
 
    public JsonResult10() {
    }
 
    // getters and setters
...
}
  • السطر 6: محتوى HTML للمنطقة المحددة بـ [content
  • السطر 7: محتوى HTML لمنطقة [المنطقة 1]؛
  • السطر 8: محتوى HTML لمنطقة [المنطقة 3]؛
  • السطر 9: محتوى HTML لمنطقة [Error
  • السطر 10: محتوى HTML لمنطقة [Inputs
  • السطر 11: قيمة منطقية تشير إلى ما إذا كان يجب عرض منطقة [Zone 1]؛
  • السطر 12: قيمة منطقية تشير إلى ما إذا كان يجب عرض منطقة [المنطقة 3]؛

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


@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
    @ResponseBody()
    public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
        // thymeleaf context
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // answer
        JsonResult10 result = new JsonResult10();
        // session
        session.setZone1(null);
        session.setZone3(null);
        session.setZone1Active(false);
        session.setZone3Active(false);
        // randomize an answer
        int cas = new Random().nextInt(3);
        switch (cas) {
        case 0:
            // zone 1 active
            setZone1(thymeleafContext, result);
            return result;
        case 1:
            // zone 3 active
            setZone3(thymeleafContext, result);
            return result;
        case 2:
            // active zones 1 and 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) {
        // zone 1 active
        // flow 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
        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] على إعدادات لغة. وهذا يسمح له بتقييم تعبيرات من النوع [#{key_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 لمنطقة [المنطقة 3] بطريقة مماثلة:


    private void setZone3(WebContext thymeleafContext, JsonResult10 result) {
        // zone 3 active
        // flow 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
        session.setCpt3(cpt3);
        session.setZone3(zone3);
        session.setZone3Active(true);
}
  • السطر 6: الإعدادات المحلية للمنطقة [Zone 3] هي اللغة الإنجليزية؛

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

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


// on receipt of the server response
// in case of success
function onSuccess(data) {
    console.log("onSuccess");
    // content
    if (data.content) {
        content.html(data.content);
    }
    // zone 1
    if (data.zone1Active) {
        $("#zone1").show();
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
        }
    } else {
        $("#zone1").hide();
    }
    // zone 3 active?
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // seized?
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // mistake?
    if (data.erreur) {
        erreur.text(data.erreur);
        erreur.show();
    } else {
        erreur.hide();
    }
}

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


public class JsonResult10 {
 
    // data
    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] غير فارغة، يتم تهيئة الحقل [id=content] بها. يمثل هذا الحقل [الصفحة 1] أو [الصفحة 2] بالكامل. في هذا المثال، [data.content == null]، لذا لن يتم تعديل منطقة [id=content] وستستمر في عرض [الصفحة 1]؛
  • الأسطر 10-17: عرض [Zone 1] إذا كان [data.zone1Active==true]. إذا كان [data.zone1!=null] أيضًا، فسيتم تعديل محتوى [Zone 1]؛ وإلا، فسيبقى كما هو؛
  • الأسطر 19-26: ينطبق الأمر نفسه على [المنطقة 3]؛
  • الأسطر 28-30: إذا كان [data.saisies!=null]، فسيتم تحديث منطقة [Saisies]. في هذا العرض التوضيحي، [data.saisies==null]، لذا تظل منطقة [Saisies] دون تغيير؛
  • الأسطر 32-37: ينطبق نفس المنطق على حقل [Error]، مع الفروق الدقيقة التالية:
    • السطر 33: سيكون [data.error] عبارة عن رسالة خطأ بتنسيق نصي؛
    • السطر 36: إذا كان [data.error] فارغًا، فسيتم إخفاء حقل [Error]. وذلك لأنه ربما تم عرضه خلال الطلب السابق؛

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


// on receipt of the server response
// in case of failure
function onError(jqXHR) {
    console.log("onError");
    // system error
    erreur.text(jqXHR.responseText);
    erreur.show();
}

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


function postForm() {
    console.log("postForm");
    // retrieve references to the current page
    ...
    // make a manual Ajax call
    $.ajax({
        url : '/ajax-10x',
        ...
    })
}
  • السطر 7: ندخل عنوان URL غير موجود؛

فيما يلي النتائج عند النقر على زر [تحديث]:

من المثير للاهتمام ملاحظة أن الخطأ تم إرساله أيضًا في شكل سلسلة JSON.

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


// after [onSuccess, onError]
function onComplete() {
    console.log("onComplete");
    // waiting image
    loading.hide();
}

نقوم ببساطة بإخفاء صورة التحميل المتحركة.

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

فيما يلي كود HTML للرابط [إرسال]:


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

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


// validation of entered values
function valider() {
    // posted value
    var post = JSON3.stringify({
        "value1" : $("#text1").val().trim(),
        "value2" : $("#text2").val().trim()
    });
    // make a manual Ajax call
    $.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 {
 
    // data
    @Size(min = 4, max = 6)
    @NotNull
    private String value1;
    @Range(min = 10, max = 14)
    @NotNull
    private Integer value2;
 
    // getters and setters
    ...
}
  • يجب أن تتطابق بنية الكائن [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 context
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // answer
        JsonResult10 result = new JsonResult10();
        // valid post?
        if (bindingResult.hasErrors()) {
            // page 1 is returned with an error
            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 context
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // answer
        JsonResult10 result = new JsonResult10();
        // valid post?
        if (bindingResult.hasErrors()) {
    ...
        }
        // the input field is saved
        thymeleafContext.setVariable("value1", post.getValue1());
        thymeleafContext.setVariable("value2", post.getValue2());
        session.setSaisies(engine.process("vue-09-saisies", thymeleafContext));
        // send page 2
        result.setContent(engine.process("vue-09-page2", thymeleafContext));
        return result;
}
  • السطران 13-14: يتم وضع القيم المرسلة في سياق Thymeleaf؛
  • السطر 15: باستخدام هذا السياق، نحسب العرض [vue-09-saisies] ونخزنه في الجلسة حتى نتمكن من إعادة إنشائه لاحقًا؛
  • السطر 17: يتم وضع الصفحة 2 في النتيجة التي سيتم إرسالها إلى العميل؛

العرض [view-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");
    // content
    if (data.content) {
        content.html(data.content);
    }
    // zone 1
    if (data.zone1Active) {
        $("#zone1").show();
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
        }
    } else {
        $("#zone1").hide();
    }
    // zone 3 active?
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // seized?
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // mistake?
    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؛
  • يتم عرض الحقل [Error
  • تظل الحقول [Zone 1] و[Zone 3] و[Entries] دون تغيير؛

لا توجد أخطاء

في هذه الحالة، أرسلت الإجراء [/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

رابط [العودة إلى الصفحة 1] الموجود في الصفحة 2 هو كما يلي:


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

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


// back to page 1
function retourPage1() {
    // make a manual Ajax call
    $.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 context
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // answer
        JsonResult10 result = new JsonResult10();
        // we return page 1 in its original state
        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 مع مناطقها الثلاث [المنطقة 1، المنطقة 3، خطأ]:

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

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

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


function onSuccess(data) {
    console.log("onSuccess");
    // content
    if (data.content) {
        content.html(data.content);
    }
    // zone 1
    if (data.zone1Active) {
        $("#zone1").show();
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
        }
    } else {
        $("#zone1").hide();
    }
    // zone 3 active?
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // seized?
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // mistake?
    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؛
  • يتم إخفاء حقل [الخطأ]؛
  • يتم عرض مناطق [المنطقة 1] و[المنطقة 3] و[المدخلات] كما هي؛

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

7.6.1. مقدمة

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


public class SessionModel1 implements Serializable {
 
    // two meters
    private int cpt1 = 0;
    private int cpt3 = 0;
    // the three zones
    private String zone1 = "xx";
    private String zone3 = "zz";
    private String saisies;
    private boolean zone1Active = true;
    private boolean zone3Active = true;
...
}

عندما يكون هناك عدد كبير من المستخدمين، قد تصبح الذاكرة التي تشغلها جلسات عمل جميع هؤلاء المستخدمين مشكلة. ولذلك، فإن القاعدة هي تقليل حجم هذه الذاكرة إلى أدنى حد ممكن. يتيح لك نموذج SPV (تطبيق الصفحة الواحدة) إدارة الجلسة على جانب العميل والحصول على خادم ويب بدون جلسات. في الواقع، يتم تحميل الصفحة الواحدة مبدئيًا بواسطة المتصفح. ويأتي معها ملف JavaScript المصاحب. نظرًا لعدم وجود إعادة تحميل للصفحة، سيبقى ملف 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 الخاص بزر [تحديث]

  

فيما يلي الكود الموجود في ملف [local12.js]:


// global variables
var content;
var loading;
var erreur;
var page1;
var page2;
var value1;
var value2;
var session = {
        "cpt1" : 0,
        "cpt3" : 0
    };
 
// document loading
$(document).ready(function() {
    // retrieve the references of the page's various components
    loading = $("#loading");
    loading.hide();
    erreur = $("#erreur");
    erreur.hide();
    content = $("#content");
});
  • الأسطر 17–21: عند تحميل الصفحة الرئيسية، يتم تخزين الإشارات إلى المكونات الثلاثة المحددة بـ [loading، error، content] في المتغيرات العالمية في الأسطر 2–4؛
  • السطور 5-6: لتخزين الصفحتين؛
  • السطور 7-8: لتخزين القيمتين المرسلتين عبر رابط [Validate
  • السطر 9: الجلسة. وهي تُخزّن قيم العدادات [cpt1، cpt3] على جانب العميل؛

تتعامل الدالة [postForm] مع النقر على زر [Refresh]:


function postForm() {
    console.log("postForm");
    // we post the session
    var post = JSON3.stringify(session);
    // make a manual Ajax call
    $.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;
    // two meters
    private int cpt1 = 0;
    private int cpt3 = 0;
 
    // getters and setters
    ...
}

تخزن جلسة [SessionModel2] ما يلي:

  • السطر 9: عدد المرات [cpt1] التي يتم فيها عرض منطقة [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 {
 
    // data
    private String page2;
    private String zone1;
    private String zone3;
    private String erreur;
    private String value1;
    private Integer value2;
 
    // session
    private SessionModel2 session;
 
    // getters and setters
    ...
}
  • السطر 14: الجلسة. يقوم الخادم بإرسالها مرة أخرى إلى العميل لتخزينها؛
  • السطر 6: محتوى HTML للصفحة 2؛
  • السطر 7: محتوى HTML لمنطقة [المنطقة 1]؛
  • السطر 8: محتوى HTML لمنطقة [المنطقة 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 context
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // answer
        JsonResult13 result = new JsonResult13();
        result.setSession(session2);
        // randomize an answer
        int cas = new Random().nextInt(3);
        switch (cas) {
        case 0:
            // zone 1 active
            setZone1B(thymeleafContext, result);
            return result;
        case 1:
            // zone 3 active
            setZone3B(thymeleafContext, result);
            return result;
        case 2:
            // active zones 1 and 3
            setZone1B(thymeleafContext, result);
            setZone3B(thymeleafContext, result);
            return result;
        }
        return null;
    }
  • السطر 9: يتم وضع الجلسة في نتيجة الإجراء؛

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


    private void setZone1B(WebContext thymeleafContext, JsonResult13 result) {
        // retrieve the session
        SessionModel2 session = result.getSession();
        // zone 1 active
        // flow 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
        session.setCpt1(cpt1);
}
  • السطر 3: نسترد الجلسة. سيتم تعديلها في السطر 12 بالعداد الجديد [cpt1]. لاحظ أن هذه الجلسة سيتم إرسالها مرة أخرى إلى العميل؛
  • السطر 10: المنطقة الجديدة [Zone 1]؛

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


private void setZone3B(WebContext thymeleafContext, JsonResult13 result) {
        // retrieve the session
        SessionModel2 session = result.getSession();
        // zone 3 active
        // flow 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
        session.setCpt3(cpt3);
    }

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

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


function postForm() {
    console.log("postForm");
    // we post the session
    var post = JSON3.stringify(session);
    // make a manual Ajax call
    $.ajax({
    ...
        success : function(data) {
            // save the session
            session = data.session;
            // update both zones
            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 للرابط [Submit]:


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

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


// validation of entered values
function valider() {
    // memorize page 1
    page1 = content.html();
    // store the values entered
    value1 = $("#text1").val().trim();
    value2 = $("#text2").val().trim();
    // posted value
    var post = JSON3.stringify({
        "value1" : value1,
        "value2" : value2,
        "pageRequired" : page2 ? false : true
    });
    // make a manual Ajax call
    $.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 {
 
    // page 2
    private boolean pageRequired;
 
    // getters and setters
    ...
}
  • السطر 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 context
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // answer
        JsonResult13 result = new JsonResult13();
        // valid post?
        if (bindingResult.hasErrors()) {
            // an error is returned
            result.setErreur(getErreursForModel(bindingResult));
            return result;
        }
        // send page 2
        result.setValue1(post.getValue1());
        result.setValue2(post.getValue2());
        // page required?
        if (post.isPageRequired()) {
            result.setPage2(engine.process("vue-12-page2", thymeleafContext));
        }
        return result;
}
  • السطور 9–13: إذا كانت القيم المرسلة [value1, value2] غير صالحة، يتم إرجاع رسالة خطأ؛
  • السطور 15-16: عادةً، يجب أن يقوم الخادم بإجراء عملية حسابية باستخدام القيم المرسلة. هنا، يقوم ببساطة بإرجاعها لإظهار أنه قد استلمها؛
  • الأسطر 18-20: لا يتم إرجاع الصفحة رقم 2 إلا إذا طلبها العميل. السطر 19، العرض [view-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] التالية:


// validation des valeurs saisies
function valider() {
    ...
    // on fait un appel Ajax à la main
    $.ajax({
        ...
        success : function(data) {
            // erreur ?
            if (data.erreur) {
                // affichage erreur
                erreur.html(data.erreur);
                erreur.show();
            } else {
                // pas d'erreur
                erreur.hide();
                // page 2
                if (page2) {
                    // on utilise la page en cache
                    content.html(page2);
                } else {
                    // on mémorise la page 2
                    page2 = data.page2;
                    // on l'affiche
                    content.html(data.page2);
                }
                // on la met à jour avec les infos du serveur
                $("#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

الرابط [العودة إلى الصفحة 1] في الصفحة 2 هو كما يلي:


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

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


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

7.6.10. الخلاصة

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

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

7.7. تنظيم كود JavaScript في طبقات

7.7.1. مقدمة

بدأ كود JavaScript من التطبيق السابق في التعقيد. حان الوقت لتنظيمه في طبقات. سيبقى التطبيق كما هو. لن نقوم بأي تغييرات على الخادم باستثناء تعريف صفحة هبوط جديدة. سنقوم بإعادة هيكلة كود 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] ينفذ طبقة [presentation
    • [local-dao] ينفذ طبقة [DAO
  

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

7.7.4. الواجهة

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


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

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

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

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

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


var session = {
    "cpt1" : 0,
    "cpt3" : 0
};
 
// update Page 1
function updatePage1(deferred, sendMeBack) {
...
}
 
// page 2
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
...
}

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

7.7.5.1. وظيفة [updatePage1]

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


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

يتم إجراء جميع طلبات HTTP بواسطة الدالة [executePost] التالية:


// requête HTTP
function executePost(deferred, sendMeBack, url, post) {
    // on fait un appel Ajax à la main
    $.ajax({
        headers : {
            'Accept' : 'application/json',
            'Content-Type' : 'application/json'
        },
        url : url,
        type : 'POST',
        data : JSON3.stringify(post),
        dataType : 'json',
        success : function(data) {
            // on mémorise la session
            if (data.session) {
                session = data.session;
            }
            // on rend le résultat
            deferred.resolve({
                "status" : 1,
                "data" : data,
                "sendMeBack" : sendMeBack
            });
        },
        error : function(jqXHR) {
            // on rend l'erreur
            deferred.resolve({
                "status" : 2,
                "data" : jqXHR.responseText,
                "sendMeBack" : sendMeBack
            });
        }
    });
}
  • السطر 1: تقوم الدالة [executePost] بتنفيذ استدعاء Ajax من نوع POST. وتتوقع أربعة معلمات:
    1. كائن [jQuery.Deferred] في الحالة [pending
    2. كائن JS ليتم إرجاعه إلى طبقة [presentation
    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] هي كما يلي:


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

7.7.6. طبقة [presentation]

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

7.7.6.1. وظيفة [postForm]

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


// update Page 1
function postForm() {
    // on met à jour la page 1
    var deferred = $.Deferred();
    loading.show();
    updatePage1(deferred, {
        'sender' : "postForm",
        'info' : 10
    });
    // affichage résultats
    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) {
    // end waiting
    loading.hide();
    // data recovery
    var data = result.data
    // for demo
    console.log(JSON3.stringify(result.sendMeBack));
    // status analysis
    switch (result.status) {
    case 1:
        // update both zones
        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:
        // error display
        erreur.html(data);
        break;
    }
}
  • السطر 1: المعلمة [result] المستلمة هي المعلمة التي تم تمريرها إلى الأسلوب [deferred.resolve] في الدالة [executePost]، على سبيل المثال:

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

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

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


// update Page 1
function postForm() {
    // on met à jour la page 1
    var deferred = $.Deferred();
    loading.show();
    updatePage1(deferred, {
        'sender' : "postForm",
        'info' : 10
    });
    // affichage résultats
    deferred.done(postFormDone);
}

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


function postFormDone(result) {
}

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


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

7.7.7. وظيفة [valider]

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


// validation valeurs saisies
function valider() {
    // on mémorise la page 1
    page1 = content.html();
    // on mémorise les valeurs saisies
    value1 = $("#text1").val().trim();
    value2 = $("#text2").val().trim();
    // pas d'erreur
    erreur.hide();
    // on demande la page 2
    var deferred = $.Deferred();
    loading.show();
    getPage2(deferred, {
        'sender' : 'valider',
        'info' : 20
    }, value1, value2, page2 ? false : true);
    // affichage résultats
    deferred.done(validerDone);
}

والوظيفة [validerDone] (السطر 18) على النحو التالي:


function validerDone(result) {
    // end waiting
    loading.hide();
    // data recovery
    var data = result.data
    // for demo
    console.log(JSON3.stringify(result.sendMeBack));
    // status analysis
    switch (result.status) {
    case 1:
        // mistake?
        if (data.erreur) {
            // error display
            erreur.html(data.erreur);
            erreur.show();
        } else {
            // no error
            erreur.hide();
            // page 2
            if (page2) {
                // use the cached page
                content.html(page2);
            } else {
                // memorize page 2
                page2 = data.page2;
                // we display it
                content.html(data.page2);
            }
            // we update it with server info
            $("#value1").text(data.value1);
            $("#value2").text(data.value2);
        }
        break;
    case 2:
        // error display
        erreur.html(data);
        erreur.show();
        break;
    }
}
  • السطر 5: نسترد الاستجابة من الخادم؛
  • الأسطر 10–32: هذا هو الكود الذي كان موجودًا، في الإصدار السابق، في دالة [onSuccess] التابعة لدالة [validate
  • الأسطر 34–38: هذا هو الكود الذي كان موجودًا سابقًا في دالة [onError] التابعة لدالة [validate

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

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

 

7.8. الخلاصة

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

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

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