Skip to content

7. ایجاکسی‌سازی یک برنامه Spring MVC

7.1. نقش AJAX در یک برنامه وب

تا به اینجا، مثال‌های آموزشی مورد مطالعه معماری زیر را داشته‌اند:

برای ناوبری از یک نمای [Vue1] به یک نمای [Vue2]، مرورگر:

  • یک درخواست به برنامه وب ارسال می‌کند؛
  • نما [Vue2] را دریافت کرده و به جای نما [Vue1] نمایش می‌دهد.

این الگوی استاندارد است:

  • درخواست از مرورگر؛
  • سرور وب در پاسخ به کلاینت یک نما تولید می‌کند؛
  • مرورگر این نمای جدید را نمایش می‌دهد.

طی چند سال گذشته، راه دیگری برای تعامل مرورگر و وب‌سرور وجود داشته است: AJAX (جاوااسکریپت و XML غیرهمزمان). این در واقع شامل تعاملاتی بین نمای نمایش‌داده‌شده توسط مرورگر و وب‌سرور است. مرورگر به انجام کاری که در آن بهترین است ادامه می‌دهد – نمایش یک نمای HTML – اما اکنون توسط جاوااسکریپتی که در نمای نمایش‌داده‌شده HTML جاسازی شده، کنترل می‌شود. این فرآیند به شرح زیر است:

  • در [1]، یک رویداد در صفحه‌ای که در مرورگر نمایش داده می‌شود رخ می‌دهد (مانند کلیک روی دکمه، تغییر متن و غیره). این رویداد توسط جاوااسکریپت (jS) جاسازی‌شده در صفحه رهگیری می‌شود؛
  • در [2]، کد جاوااسکریپت درخواستی HTTP ارسال می‌کند، درست همان‌طور که مرورگر انجام می‌داد. این درخواست ناهمزمان است: کاربر می‌تواند بدون معطل ماندن در حین انتظار برای پاسخ به درخواست HTTP، به تعامل با صفحه ادامه دهد. این درخواست از جریان پردازش استاندارد پیروی می‌کند. تفاوت کمی (یا هیچ تفاوتی) با یک درخواست استاندارد دارد؛
  • در [3]، پاسخی به کلاینت jS ارسال می‌شود. به جای یک نمای کامل HTML، این یک نمای جزئی HTML، یک XML یا jSON است (JavaScript نشانهٔ شیء) که ارسال می‌شود؛
  • در [4]، جاوااسکریپت این پاسخ را بازیابی می‌کند و از آن برای به‌روزرسانی بخشی از صفحهٔ نمایش‌داده‌شدهٔ HTML استفاده می‌کند.

از دیدگاه کاربر، یک تغییر در نما وجود دارد زیرا آنچه می‌بیند تغییر کرده است. با این حال، هیچ بارگذاری مجدد کامل صفحه‌ای انجام نمی‌شود؛ بلکه صرفاً یک اصلاح جزئی از صفحه نمایش داده شده است. این امر به روان‌تر و تعاملی‌تر شدن صفحه کمک می‌کند: از آنجا که صفحه به‌طور کامل دوباره بارگذاری نمی‌شود، می‌توان رویدادهایی را که قبلاً قابل مدیریت نبودند، مدیریت کرد. برای مثال، ارائه فهرستی از گزینه‌ها به کاربر همزمان با تایپ حروف در یک کادر ورودی. با تایپ هر کاراکتر جدید، یک درخواست AJAX به سرور ارسال می‌شود که سپس پیشنهادات بیشتری را بازمی‌گرداند. بدون Ajax، این نوع کمک به ورودی قبلاً غیرممکن بود. بارگذاری مجدد یک صفحه جدید با تایپ هر کاراکتر امکان‌پذیر نبود.

7.2. به‌روزرسانی یک صفحه با فید HTML

7.2.1. نماها

اکنون اپلیکیشن زیر را بررسی خواهیم کرد:

  • در [1]، زمان بارگذاری صفحه؛
  • در [2]، چهار عمل اصلی روی دو عدد حقیقی A و B انجام می‌شود؛
  • در [3]، پاسخ سرور در بخشی از صفحه نمایش داده می‌شود؛
  • در [4]، زمان محاسبه. این با زمان بارگذاری صفحه [5] متفاوت است. مورد دوم برابر با [1] است که نشان می‌دهد ناحیه [6] دوباره بارگذاری نشده است. علاوه بر این، نواحی URL و [7] صفحه تغییر نکرده‌اند.

7.2.2. عمل [/ajax-01]

  

کنترل‌کننده [Ajax.java] اقدام زیر [/ajax-01] را تعریف می‌کند:


    @RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax01(Locale locale, Model modèle, HttpSession session, String tempo) {
        //آیا زمان معتبر است؟
        if (tempo != null) {
            boolean valide = false;
            int valueTempo = 0;
            try {
                valueTempo = Integer.parseInt(tempo);
                valide = valueTempo >= 0;
            } catch (NumberFormatException e) {

            }
            if (valide) {
                session.setAttribute("tempo", new Integer(valueTempo));
            }
        }
        // آماده‌سازی قالب نما [vue-01]
        ...
}
  • خط ۲: عمل [/ajax-01] تنها یک پارامتر، [tempo] را می‌پذیرد. این مدت زمان، به میلی‌ثانیه، است که سرور باید قبل از ارسال نتایج عملیات‌های محاسباتی صبر کند؛
  • خط ۴: پارامتر [tempo] اختیاری است؛
  • خطوط ۵–۱۲: مقدار پارامتر [tempo] بررسی می‌شود تا از معتبر بودن آن اطمینان حاصل شود؛
  • خطوط ۱۳–۱۵: اگر چنین باشد، مقدار تأخیر در جلسه ذخیره می‌شود. این بدان معناست که تا زمانی که تغییر داده نشود، به قوت خود باقی خواهد ماند؛

کد مربوط به اقدام [/ajax-01] به شرح زیر ادامه می‌یابد:


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

کلاس [ActionModel01] عمدتاً برای محصور کردن مقادیری که توسط اکشن [/ajax-01] ارسال می‌شوند، استفاده می‌شود. در این مورد، هیچ چیزی ارسال نمی‌شود. ما یک کلاس خالی ایجاد می‌کنیم و آن را در مدل قرار می‌دهیم زیرا ویو [vue-01.xml] از آن استفاده می‌کند. کلاس [ActionModel01] به شرح زیر است:


package istia.st.springmvc.models;

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

public class ActionModel01 {

    // داده‌ها ارسال شدند
    @NotNull
    @DecimalMin(value = "0.0")
    private Double a;

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

    // گیرنده و تنظیم‌کننده
    ...
}
  • خطوط ۱۱ و ۱۵: دو عدد حقیقی [a,b] که از طریق یک فرم ارسال خواهند شد؛

بیایید به کد اکشن بازگردیم:


    @RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax01(Locale locale, Model modèle, HttpSession session, String tempo) {
...
        // آماده‌سازی قالب نما [vue-01]
        modèle.addAttribute("actionModel01", new ActionModel01());
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
...
        //نما
        return "vue-01";
}
  • خطوط ۶–۷: ما یک نمونه از نوع [Resultats] را در قالب وارد می‌کنیم؛

نوع [Resultats] که به مدل اضافه شده به شرح زیر است:

  

package istia.st.springmvc.models;

public class Resultats {

    // داده‌ها
    private String aplusb;
    private String amoinsb;
    private String amultiplieparb;
    private String adiviseparb;
    private String heureGet;
    private String heurePost;
    private String erreur;
    private String vue;
    private String culture;

    // گیرنده و تنظیم‌کننده
    ...
}
  • خطوط ۶–۹: نتیجهٔ عملیات چهارگانهٔ حساب بر روی اعداد [a,b];
  • خط ۱۰: زمان بارگذاری اولیهٔ صفحه؛
  • خط ۱۱: زمانی که چهار عمل اصلی روی اعداد [a,b] انجام شد؛
  • خط ۱۲: هرگونه پیام خطا؛
  • خط ۱۳: نمای قابل نمایش، در صورت وجود؛
  • خط ۱۴: فرهنگ نما، [fr-FR] یا [en-US];

کد مربوط به عمل [/ajax-01] به شرح زیر ادامه می‌یابد:


    @RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax01(ActionModel01 formulaire, Locale locale, Model modèle, HttpSession session) {
        ...
        // محلی
        setLocale(locale, modèle, résultats);
...
}
  • خط ۵: متد [setLocale] برای تعیین فرهنگ مورد استفاده در قالب نما، [fr-FR] یا [en-US]، استفاده می‌شود. این فرهنگ برای جاوااسکریپت جاسازی‌شده در نما در نظر گرفته شده است؛

روش [setLocale] به شرح زیر است:


    private void setLocale(Locale locale, Model modèle, Resultats résultats) {
        // فقط زبان‌های فرانسوی (fr-FR) و انگلیسی (en-US) پشتیبانی می‌شوند
        String language = locale.getLanguage();
        String country = null;
        switch (language) {
        case "fr":
            country = "FR";
            break;
        default:
            language = "en";
            country = "US";
            break;
        }
        // فرهنگ
        résultats.setCulture(String.format("%s-%s", language, country));
}

در قالب، رشته [${resultats.culture}] روی 'fr-FR' یا 'en-US' تنظیم خواهد شد.

بیایید به اکشن [/ajax-01] بازگردیم:


@RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax01(ActionModel01 formulaire, Locale locale, Model modèle, HttpSession session) {
...
        // محلی
        setLocale(locale, modèle, résultats);
        //زمان
        résultats.setHeureGet(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        // نما
        return "vue-01";
    }
  • خط ۷: زمان را از GET در قالب وارد می‌کنیم؛
  • خط ۹: نمای [vue-01.xml] نمایش داده می‌شود:

7.2.3. نما [vue-01.xml]

نما [vue-01.xml] به شرح زیر است:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>Ajax-01</title>
        <link rel="stylesheet" href="/css/ajax01.css" />
        <script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="/js/jquery/jquery.validate.min.js"></script>
        <script type="text/javascript" src="/js/jquery/jquery.validate.unobtrusive.min.js"></script>
        <script type="text/javascript" src="/js/jquery/globalize/globalize.js"></script>
        <script type="text/javascript" src="/js/jquery/globalize/cultures/globalize.culture.fr-FR.js"></script>
        <script type="text/javascript" src="/js/jquery/globalize/cultures/globalize.culture.en-US.js"></script>
        <script type="text/javascript" src="/js/jquery/jquery.unobtrusive-ajax.js"></script>
        <script type="text/javascript" src="/js/json3.js"></script>
        <script type="text/javascript" src="/js/client-validation.js"></script>
        <script type="text/javascript" src="/js/local1.js"></script>
        <script th:inline="javascript">
            /*<![CDATA[*/
                    var culture = [[${resultats.culture}]];
                    Globalize.culture(culture);
                    /*]]>*/
        </script>
    </head>
    <body>
        <h2>Ajax - 01</h2>
        <p>
            <strong th:text="#{labelHeureGetCulture(${resultats.heureGet},${resultats.culture})}">
                Heure de chargement :
            </strong>
        </p>
        <h4>
            <p th:text="#{titre.part1}">
                Opérations arithmétiques sur deux nombres réels A et B positifs ou nuls
            </p>
        </h4>
        <form id="formulaire" name="formulaire" ... ">
...
        </form>
        <hr />
        <div id="resultats" />
    </body>
</html>
  • خطوط ۷–۱۲: کتابخانه‌های اعتبارسنجی و بین‌المللی‌سازی (فرهنگ‌ها) jQuery؛
  • خط ۱۵: کتابخانه [client-validation] که در بند ۶.۳ ساخته شده است؛
  • خط ۱۴: کتابخانه jSON که توسط کتابخانه [client-validation] استفاده می‌شود. این کتابخانه در صورتی که گزارش‌های اعتبارسنجی غیرفعال شده باشند، اختیاری است؛
  • خط ۱۳: کتابخانه مایکروسافت [Unobtrusive Ajax]. این کتابخانه گاهی اوقات امکان اجتناب از نوشتن جاوااسکریپت را فراهم می‌کند؛
  • خط ۱۶: یک فایل jS برای مقاصد خودمان؛
  • خطوط ۱۷–۲۲: برای مدیریت فرهنگ‌های [fr-FR] و [en-US] در سمت کلاینت. ما قبلاً با این کد مواجه شده‌ایم؛
  • خط ۲۷: یک پیام سفارشی. ما این موارد را در بخش ۵.۱۸ بررسی کردیم؛
  • خطوط ۳۶–۳۸: فرم، که بعداً به آن باز خواهیم گشت؛
  • خط ۴۰: ناحیه‌ای از سند که جاوااسکریپت پاسخ سرور را در آن قرار خواهد داد؛

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>
  • خط ۱۶: فیلد [a] با اعتبارسنج‌های [required]، [number] و [min] مرتبط است؛
  • خط ۱۹: همین موضوع در مورد فیلد [b] نیز صدق می‌کند؛

پیام‌های مختلف را می‌توان در فایل‌های [messages.properties] درون پروژه یافت:

  

[messages_fr.properties]


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

[messages_en.properties]


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

اکنون، بیایید ویژگی‌های تگ [form] را بررسی کنیم:


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

ما می‌توانیم ویژگی‌های استاندارد تگ [form] را تشخیص دهیم:


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

بلافاصله مشخص است که اگر جاوااسکریپت در مرورگری که صفحه را نمایش می‌دهد غیرفعال باشد، فرم به URL [/ajax-02.html] ارسال خواهد شد. اکنون بیایید سایر ویژگی‌ها را تحلیل کنیم:


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

ویژگی‌های [data-ajax-xxx] توسط کتابخانه jS [unobtrusive-ajax] مدیریت می‌شوند که توسط نما [vue-01.xml] وارد شده است:


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

وقتی ویژگی‌های [data-ajax-xxx] وجود دارند، [submit] فرم از طریق یک فراخوانی Ajax به کتابخانه [unobtrusive-ajax] اجرا خواهد شد. پارامترها معانی زیر را دارند:

  • [data-ajax="true"]: وجود این ویژگی باعث می‌شود که [submit] فرم از طریق Ajax اجرا شود؛
  • [data-ajax-method="post"]: متد [submit]. متد URL برای درخواست 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"]: تابع jS که باید قبل از انجام [submit] اجرا شود؛
  • [data-ajax-complete="afterComplete"]: تابع jS که باید هنگام دریافت پاسخ اجرا شود؛
  • [data-ajax-update="#resultats"]: شناسه فیلدی که نتیجه ارسال‌شده توسط سرور در آن قرار می‌گیرد. نما [vue-01.xml] شامل فیلد زیر است:

<div id="resultats" />
  • [data-ajax-mode="replace"]: حالت درج نتیجه در فیلد قبلی. حالت [replace] باعث می‌شود که نتیجه، هر چیزی را که قبلاً در فیلد با شناسه [resultats] وجود داشته است، 'به‌روزرسانی' (overwrite) کند؛

توجه داشته باشید که جاوااسکریپت [submit] تنها در صورتی اجرا می‌شود که اعتبارسنج‌ها مقادیر آزمایش‌شده را معتبر اعلام کرده باشند.

کتابخانه jS [unobtrusive-ajax] دو هدف دارد:

  • تا اطمینان حاصل شود که فرم به درستی با هر دو سناریو سازگار می‌شود: چه جاوا اسکریپت در مرورگر فعال باشد یا غیرفعال؛
  • اجتناب از نوشتن جاوااسکریپت. خواهیم دید که در این مورد، این امر غیرممکن بود.

7.2.5. اقدام [/ajax-02]

ما دیده‌ایم که مقادیر ارسال‌شده به اکشن [/ajax-02] فرستاده شده‌اند. این به شرح زیر است:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(ActionModel01 formulaire, Locale locale, Model modèle, HttpSession session) throws InterruptedException {
        // تِمپو؟
        Integer tempo = (Integer) session.getAttribute("tempo");
        if (tempo != null && tempo > 0) {
            Thread.sleep(tempo);
        }
        // آماده‌سازی قالب برای نمای بعدی
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
        // تنظیم زمان محلی
        setLocale(locale, modèle, résultats);
        // زمان
        résultats.setHeurePost(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        ...
}
  • فعلاً کار را ساده‌تر می‌کنیم: فرض می‌کنیم که فراخوانی POST واقعاً توسط جاوااسکریپت در نمای [vue-01.xml] انجام شده است. کمی بعد به این فرض باز خواهیم گشت؛
  • خط ۲: مقادیر ارسال‌شده در [a,b] در قالب [ActionModel01] قرار می‌گیرند؛
  • خطوط ۴–۷: اگر کاربر در یک GET قبلی تأخیری را تنظیم کرده باشد، این تأخیر از جلسه بازیابی شده و اعمال می‌شود (خط ۶). هدف از این کار این است که کاربر بتواند اثر ویژگی [data-ajax-loading="#loading"] را در فرم مشاهده کند؛
  • خطوط ۹–۱۰: یک ویژگی [resultats] به قالب اضافه می‌شود؛
  • خط ۱۲: فرهنگ [fr-FR] یا [en-US] به قالب اضافه می‌شود؛
  • خط ۱۴: زمان را برای POST در قالب وارد کنید؛

به‌عنوان یادآوری، نوع [Resultats] واردشده در قالب عبارت است از:


public class Resultats {

    // داده‌ها
    private String aplusb;
    private String amoinsb;
    private String amultiplieparb;
    private String adiviseparb;
    private String heureGet;
    private String heurePost;
    private String erreur;
    private String vue;
    private String culture;

    // گیرنده و تنظیم‌کننده
...
}

کد عملیاتی برای [/ajax-02] به شرح زیر ادامه می‌یابد:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle,    HttpSession session) throws InterruptedException {
...
        résultats.setHeurePost(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        // هر بار دیگر یک خطا تولید می‌شود
        int val = new Random().nextInt(2);
        if (val == 0) {
            // پیام خطا را بازمی‌گرداند
            résultats.setErreur("erreur.aleatoire");
            return "vue-03";
        }
...
    }
  • خطوط ۶–۱۱: به‌عنوان مثال، نشان می‌دهیم چگونه یک صفحهٔ خطا را به مشتری jS بازگردانیم. نیمی از مواقع، نمای زیر [vue-03.xml] بازگردانده می‌شود:

توجه کنید که در خط ۹ این یک پیام قرار داده شده در قالب نیست، بلکه یک کلید پیام است:

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

  • در خط ۱۲، به پیامی توجه کنید که با یک کلید پیام تعریف شده و خودِ آن محاسبه می‌شود. ما این مفهوم را در بخش ۵.۱۸، صفحه ۱۷۰ معرفی کردیم.

کد مربوط به اقدام [/ajax-02] به شرح زیر ادامه می‌یابد:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle,    HttpSession session) throws InterruptedException {
...
        //مقادیر ارسال‌شده را بازیابی می‌کند
        double a = formulaire.getA();
        double b = formulaire.getB();
        //قالب ساخته می‌شود
        résultats.setAplusb(String.valueOf(a + b));
        résultats.setAmoinsb(String.valueOf(a - b));
        résultats.setAmultiplieparb(String.valueOf(a * b));
        try {
            résultats.setAdiviseparb(String.valueOf(a / b));
        } catch (RuntimeException e) {
            résultats.setAdiviseparb("NaN");
        }
        //نما نمایش داده می‌شود
        return "vue-02";
    }
  • خطوط ۵–۱۵: چهار عمل اصلی بر روی اعداد [a,b] انجام شده و در نمونه مدل [Resultats] جایگذاری شده‌اند؛
  • خط ۱۷: نمای زیر، [vue-02.xml]، بازگردانده می‌شود:

نما [vue-02.xml] به شرح زیر است:


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

خواه نتیجه نما [vue-02.xml] باشد یا نما [vue-03.xml]، این نتیجه، HTML، به دلیل ویژگی [data-ajax-update="#resultats"] فرم، در فیلدی که با [resultats] مشخص شده است در نمای [vue-01.xml] قرار می‌گیرد.

7.2.6. POST مقادیر واردشده

ما در اینجا با مقادیر ارسال‌شده (posted values) با مشکلی مواجه می‌شویم. ما با دو فرهنگ، [fr-FR] و [en-US]، کار می‌کنیم که اعداد حقیقی را به شیوه‌های متفاوتی نمایش می‌دهند. ما قبلاً این مشکل را در بخش 6.3، صفحه 190، زمانی که نیاز به ارسال اعداد حقیقی در دو فرهنگ مختلف داشتیم، برطرف کرده بودیم. اکنون ابزارهای مورد استفاده در آن زمان را مجدداً بررسی خواهیم کرد. با این حال، با چالش دیگری مواجه هستیم: ما به روشی که تبدیل POST مقادیر واردشده را انجام می‌دهد، دسترسی نداریم. به همین دلیل، ویژگی‌های زیر را به تگ فرم اضافه کرده‌ایم:

  • [data-ajax-begin="beforeSend"]: تابع jS که باید قبل از انجام [submit] اجرا شود؛
  • [data-ajax-complete="afterComplete"]: تابع jS که باید پس از دریافت پاسخ اجرا شود؛

ما به تابع jS که مقادیر واردشده را ارسال می‌کند دسترسی نداریم، اما می‌توانیم دو تابع jS بنویسیم:

  • [beforeSend]: تابعی jS که قبل از POST اجرا می‌شود؛
  • [afterComplete]: تابعی jS که هنگام دریافت پاسخ به POST اجرا می‌شود؛

این دو تابع در فایلی به نام [local1.js] قرار دارند:

  

فایل [local1.js] محیط jS نمای [vue-01.xml] را به شرح زیر inicialize می‌کند:


// داده‌های جهانی
var loading;
var formulaire;
var résultats;
var a, b;

// هنگامی که سند بارگذاری می‌شود
$(document).ready(function() {
    // بازیابی ارجاعات برای اجزای مختلف صفحه
    loading = $("#loading");
    formulaire = $("#formulaire");
    resultats = $('#نتایج');
    a = $("#a");
    b = $("#b");
    // برخی عناصر را در حافظه پنهان ذخیره می‌کند
    loading.hide();
    // پارس کردن اعتبارسنج‌های فرم
    $.validator.unobtrusive.parse(formulaire);
    // ما دو لوکال را مدیریت می‌کنیم [fr_FR, en_US]
    //داده‌های واقعی [a,b] توسط سرور در قالب آنگلو-ساکسون ارسال می‌شوند
    // در صورت لزوم به فرمت فرانسوی تبدیل می‌شوند
    checkCulture(2);
});
  • خط ۲۲: تابع [checkCulture] کمی جلوتر توضیح داده شده است؛

تابع jS [beforeSend] به شرح زیر خواهد بود:


function beforeSend(jqXHR, settings) {
    //قبل از POST
    //اعداد باید به فرمت انگلیسی-ساکسون ارسال شوند
    var culture = Globalize.culture().name;
    if (culture === 'fr-FR') {
        checkCulture(1);
        settings.data = formulaire.serialize();
    }
}

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

function checkCulture(mode) {
    if (mode == 1) {
        //اعداد [a,b] به فرمت انگلو-ساکسون تبدیل می‌شوند
        var value1 = a.val().replace(",", ".");
        a.val(value1);
        var value2 = b.val().replace(",", ".");
        b.val(value2);
    }
    if (mode == 2) {
...
    }
}
  • خطوط ۴–۶: سیستم بررسی می‌کند که آیا فرهنگ نما [fr-FR] است. در این صورت، مقادیر ارسال‌شده باید تغییر داده شوند. در واقع، اگر کاربر [1,6] را وارد کرده باشد، مقدار [1.6] باید ارسال شود؛ در غیر این صورت، مقدار [1,6] در سمت سرور رد خواهد شد. برای این کار، کافی است ویرگول در مقادیر ارسال‌شده را به نقطه اعشاری تغییر دهید (خطوط ۱۸–۲۱)؛
  • اما نمی‌توانیم به همین‌جا بسنده کنیم. این به آن دلیل است که وقتی تابع [beforeSend] فراخوانی می‌شود، رشته مقادیر ارسال‌شده [a=val1&b=valB] قبلاً ساخته شده است. بنابراین باید آن را اصلاح کنیم. این کار با استفاده از پارامتر دوم تابع، [settings] انجام می‌شود؛
  • خط ۷: [settings.data] (settings یک پارامتر تابع است) نشان‌دهنده رشته ارسال‌شده است. ما این رشته را با استفاده از عبارت [formulaire.serialize()] بازسازی می‌کنیم. این عبارت فرم را برای مقادیری که باید ارسال شوند اسکن می‌کند و رشته POST را می‌سازد. سپس مقادیر جدید را از [a,b]، شامل ممیزهای اعشاری، دریافت می‌کند؛

اگر هیچ اقدام دیگری انجام نشود، سرور پاسخ خود را ارسال می‌کند که به درستی نمایش داده خواهد شد. با این حال، مقادیر در [a,b] اکنون از ممیز اعشاری استفاده می‌کنند، در حالی که فرهنگ هنوز روی [fr-FR] تنظیم شده است. بنابراین اگر کاربر متوجه این موضوع نشود و دوباره روی [Calculer] کلیک کند، اعتبارسنج‌ها به او خواهند گفت که مقادیر [a,b] نامعتبر هستند. که این امر صحیح است. اینجاست که تابع [afterComplete] وارد عمل می‌شود، که پس از دریافت نتیجه اجرا می‌گردد:


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

function afterComplete(jqXHR, settings) {
    // پس از POST
    // اعداد در صورت لزوم باید به فرمت فرانسوی تبدیل شوند
    var culture = Globalize.culture().name;
    if (culture === 'fr-FR') {
        checkCulture(2);
    }
}

function checkCulture(mode) {
    if (mode == 1) {
...
    }
    if (mode == 2) {
        // اعداد را به فرمت فرانسوی تبدیل کنید
        var value1 = a.val().replace(".", ",");
        a.val(value1);
        var value2 = b.val().replace(".", ",");
        b.val(value2);
    }
}
  • خطوط ۹–۱۲: اگر زبان نما [fr-FR] باشد، اعداد [a,b] به فرمت فرانسوی بازفرمت می‌شوند.

7.2.7. آزمایش‌ها

در اینجا چند اسکرین‌شات از تست‌ها آورده شده است:

  • در [1]، پاسخ سرور؛
  • در [2]، پاسخ سرور با یک پیام خطا؛
  • در [3]، یک تایم‌اوت ۵ ثانیه‌ای تنظیم شده است. این بدان معناست که سرور قبل از ارسال پاسخ خود ۵ ثانیه صبر خواهد کرد. در تگ [form]، از ویژگی [data-ajax-loading='#loading'] استفاده کرده‌ایم. پارامتر [loading] شناسهٔ منطقه‌ای است که:
    • در تمام مدت زمان انتظار نمایش داده می‌شود؛
    • پس از دریافت پاسخ سرور پنهان می‌شود؛

در اینجا، [loading] شناسه یک تصویر متحرک است که در [4] قابل مشاهده است.

7.2.8. غیرفعال کردن جاوااسکریپت با تم [en-US]

اگر جاوااسکریپت در مرورگر غیرفعال باشد چه اتفاقی می‌افتد؟

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؟
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        ...
    }
  • خط ۴: اکنون می‌توان اکشن [/ajax-02] را از طریق Ajax POST یا از طریق استاندارد POST فراخوانی کرد. ما باید بتوانیم بین این دو حالت تمایز قائل شویم. ما این کار را با بررسی هدرهای HTTP ارسال‌شده توسط مرورگر کلاینت انجام می‌دهیم؛

وقتی در کنسول توسعه‌دهنده کروم (Ctrl+Shift+I) در حالی که جاوااسکریپت فعال است به ترافیک شبکه نگاه می‌کنیم، می‌بینیم که کلاینت هنگام ارسال درخواست POST سربرگ‌های زیر را ارسال می‌کند:

همان‌طور که در بالا مشاهده می‌شود:

  • یک هدر [X-Requested-With] به همراه [1] ارسال شده است؛
  • یک پارامتر [X-Requested-With] به مقادیر ارسال‌شده [2] اضافه شده است؛

این امر در مورد یک POST استاندارد صدق نمی‌کند. بنابراین دو راه برای بازیابی اطلاعات وجود دارد: از سربرگ‌های HTTP یا از مقادیر ارسال‌شده. خط ۴ از اقدام [/ajax-02] راه‌حل اول را انتخاب کرده است.

بیایید با کد این عمل ادامه دهیم:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle, HttpSession session, HttpServletRequest request) throws InterruptedException {
        //درخواست Ajax؟
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        //زمان‌بندی؟
        Integer tempo = (Integer) session.getAttribute("tempo");
        if (tempo != null && tempo > 0) {
            Thread.sleep(tempo);
        }
        // آماده‌سازی قالب برای نمای بعدی
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
        // محلی را تنظیم کنید
        setLocale(locale, modèle, résultats);
        //زمان
        String heure = new SimpleDateFormat("hh:mm:ss").format(new Date());
        résultats.setHeurePost(heure);
        résultats.setHeureGet(heure);
        // درخواست معتبر؟
        if (!isAjax && result.hasErrors()) {
            return "vue-01";
        }
...
  • خط ۲: پارامتر [@Valid ActionModel01 formulaire] اعتبارسنج‌های سمت سرور را فعال می‌کند؛
  • خطوط ۲۰–۲۲: اگر فراخوانی یک فراخوانی Ajax نباشد و اعتبارسنجی ناموفق باشد، آنگاه نمای [vue-01.xml] همراه با پیام‌های خطا بازگردانده می‌شود.

در اینجا یک مثال آورده شده است:

بیایید بررسی خود را از اقدام [/ajax-02] ادامه دهیم:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle,    HttpSession session, HttpServletRequest request) throws InterruptedException {
        // درخواست Ajax؟
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
        // درخواست معتبر؟
        if (!isAjax && result.hasErrors()) {
            return "vue-01";
        }
        // هر بار دیگر یک خطا تولید می‌شود
        int val = new Random().nextInt(2);
        if (val == 0) {
            // یک پیام خطا بازگردانده می‌شود
            résultats.setErreur("erreur.aleatoire");
            if (isAjax) {
                return "vue-03";
            } else {
                résultats.setVue("vue-03");
                return "vue-01";
            }
        }
...
  • خط ۱۴: یک خطای تصادفی تولید می‌شود؛
  • خط ۱۶: در صورت فراخوانی Ajax، نمای [vue-03.xml] بازگردانده می‌شود که در ناحیه‌ای که توسط [resultats] مشخص شده است، قرار می‌گیرد؛
  • خط ۱۸: در صورت عدم استفاده از Ajax، نمای نمایش داده شده در مدل [Resultats] قرار می‌گیرد؛
  • خط ۱۹: نما [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" />
  • خط ۳: نمای [vue-03.xml] زیر ناحیه [resultats] درج می‌شود؛

در اینجا یک مثال آورده شده است:

توجه داشته باشید که زمان‌های [1] و [2] اکنون یکسان هستند.

بیایید بررسی خود را از عمل [/ajax-02] ادامه دهیم:


    @RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle, HttpSession session, HttpServletRequest request) throws InterruptedException {
        //درخواست ایجکس؟
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
        // ارزش‌های ارسال‌شده را بازیابی کنید
        double a = formulaire.getA();
        double b = formulaire.getB();
        //مدل ساخته می‌شود
        résultats.setAplusb(String.valueOf(a + b));
        résultats.setAmoinsb(String.valueOf(a - b));
        résultats.setAmultiplieparb(String.valueOf(a * b));
        try {
            résultats.setAdiviseparb(String.valueOf(a / b));
        } catch (RuntimeException e) {
            résultats.setAdiviseparb("NaN");
        }
        //نمایش ویو
        if (isAjax) {
            return "vue-02";
        } else {
            résultats.setVue("vue-02");
            return "vue-01";
        }
}
  • سطور ۷–۱۷: نتایج چهار عمل اصلی در قالب قرار می‌گیرند؛
  • خطوط ۲۲–۲۳: نما [vue-01.xml] (خط ۲۲) با درج نما [vue-02.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" />
  • خط ۲: نما [vue-02.xml] در زیر ناحیه [resultats] درج خواهد شد؛

در اینجا مثالی از خروجی آورده شده است:

 

7.2.9. غیرفعال کردن جاوااسکریپت با فرهنگ [fr-FR]

با فرهنگ [fr-FR]، مشکل زیر پیش می‌آید:

مقادیر وارد شده با فرمت فرانسوی نامعتبر اعلام شده‌اند. این به این دلیل است که سرور اعداد حقیقی را در فرمت آنگلو-ساکسون انتظار دارد. راه‌حل نسبتاً پیچیده است. ما قصد داریم فیلتری بسازیم که:

  • درخواست را رهگیری کند؛
  • ویرگول‌های موجود در مقادیر ارسال‌شده [a] و [b] را با ممیزهای اعشاری جایگزین کند؛
  • سپس درخواست جدید را به اکشنی که باید آن را پردازش کند، ارسال کند؛

ابتدا، ما یک فیلد مخفی را در نما [vue-01.xml] معرفی می‌کنیم:


<form ...>
...
</p>
    <!-- fields مخفی -->
    <input type="hidden" id="culture" name="culture" th:value="${resultats.culture}"></input>
</form>
  • خط ۵: فیلد [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();
    }
}
  • خط ۷: این واقعیت که بین [cultureFilter] نوع [Filter] را برمی‌گرداند، آن را به یک فیلتر تبدیل می‌کند. خود بین می‌تواند هر نامی داشته باشد؛

گام بعدی ایجاد خود فیلتر است:

  

package istia.st.springmvc.config;

import java.io.IOException;

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

import org.springframework.web.filter.OncePerRequestFilter;

public class CultureFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        // پردازشگر بعدی
        filterChain.doFilter(new CultureRequestWrapper(request), response);
    }
}
  • خط ۱۲: ما کلاس [OncePerRequestFilter] را که یک کلاس Spring است، گسترش می‌دهیم و کاری که باید انجام دهیم این است که متد [doFilterInternal] این کلاس را بازنویسی کنیم؛
  • خط ۱۵: متد [doFilterInternal] سه پارامتر می‌گیرد:
    • [HttpServletRequest request]: درخواستی که باید فیلتر شود. این مورد قابل تغییر نیست،
    • [HttpServletResponse response]: پاسخی که باید به سرور ارسال شود. فیلتر ممکن است انتخاب کند که خود این پاسخ را تولید کند،
    • [FilterChain filterChain]: زنجیره فیلتر. هنگامی که متد [doFilterInternal] کار خود را به پایان رساند، باید درخواست را به فیلتر بعدی در زنجیره فیلتر ارسال کند؛
  • خط ۱۸: یک درخواست جدید بر اساس درخواست دریافت‌شده ([new CultureRequestWrapper(request)]) ایجاد شده و به فیلتر بعدی ارسال می‌شود. از آنجایی که درخواست اصلی ([HttpServletRequest request]) قابل تغییر نیست، یک درخواست جدید ایجاد می‌شود؛

کلاس [CultureRequestWrapper] به شرح زیر است:

  

package istia.st.springmvc.config;

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

public class CultureRequestWrapper extends HttpServletRequestWrapper {

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

    @Override
    public String[] getParameterValues(String name) {
        //مقادیر ارسال‌شده a و b
        if (name != null && (name.equals("a") || name.equals("b"))) {
            String[] values = super.getParameterValues(name);
            String[] newValues = values.clone();
            newValues[0] = newValues[0].replace(",", ".");
            return newValues;
        }
        // موارد دیگر
        return super.getParameterValues(name);
    }

}
  • خط ۶: کلاس [CultureRequestWrapper] از کلاس [HttpServletRequestWrapper] ارث می‌برد و برخی از متدهای آن را مجدداً تعریف خواهد کرد؛
  • خطوط ۸–۱۰: سازنده درخواست را برای فیلتر شدن دریافت کرده و آن را به کلاس والد پاس می‌دهد؛
  • در اینجا باید توجه داشت که درخواست فیلترشده در نهایت به‌عنوان یک پارامتر ورودی به کلاسی به نام servlet ارسال خواهد شد. با Spring MVC، این servlet از نوع [DispatcherServlet] است. این کلاس دارای متدهای مختلفی برای بازیابی پارامترهای درخواست است: [getParameter, getParameterMap, getParameterNames, getParameterValues, ...]. روش مورد استفاده توسط سرولت نیاز به بازنویسی دارد. برای انجام این کار، باید کد کلاس [DispatcherServlet] را خواند. من این کار را انجام ندادم و روش‌های مختلفی را بازنویسی کردم. در نهایت، این روش [getParameterValues] بود که بازنویسی شد؛
  • خط ۱۳: متد [getParameterValues] به عنوان پارامتر، نام یکی از پارامترهای بازگشتی متد [getParameterNames] را می‌گیرد و باید یک آرایه از مقادیر آن را بازگرداند. این به این دلیل است که می‌دانیم یک پارامتر ممکن است چندین بار در یک درخواست ظاهر شود؛
  • خط ۱۸: ویرگول با نقطهٔ اعشاری جایگزین می‌شود؛

در اینجا مثالی از اجرا آورده شده است:

  • در [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>

چند تغییر باید در خطوط ۳ و ۶ اعمال شود. بیایید خط ۳ را بررسی کنیم:

  • ما [th:field="*{a}"] را نوشته بودیم. پارامتر [th:field] ویژگی‌های [id, name, value] تگ تولیدشده HTML [input] را تنظیم می‌کند. در اینجا، می‌خواهیم خودمان ویژگی [value] را مدیریت کنیم. بنابراین، ویژگی‌های [id, name] را نیز خودمان تنظیم می‌کنیم؛
  • ویژگی [th:value] یک عبارت را با استفاده از عملگر سه‌تایی ؟ ارزیابی می‌کند. ما عبارت [${resultats.culture}=='fr-FR' and ${actionModel01.b}!=null] را بررسی می‌کنیم. اگر درست باشد، ویژگی [value] را روی مقدار [actionModel01.a] تنظیم می‌کنیم، که در آن نقطه اعشاری با یک ویرگول جایگزین شده است. اگر مقدار آن false باشد، ویژگی [value] بدون هیچ تغییری روی مقدار [actionModel01.a] تنظیم می‌شود؛
  • خط ۶: همین فرآیند برای فیلد [b] تکرار می‌شود؛

در اینجا مثالی از خروجی آورده شده است:

  • در [1]، اعداد در [a,b] نگارش فرانسوی خود را حفظ کرده‌اند. این امر در [2] صادق نیست؛

این مشکل جدید به همان شیوهٔ مورد قبلی حل می‌شود. نما [vue-03.xml] به شرح زیر اصلاح شده است:


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

در اینجا یک مثال آورده شده است:

اکنون ما یک برنامه کاربردی داریم که به درستی با دو فرهنگ در محیطی که ممکن است از جاوا اسکریپت استفاده کند یا نکند، کار می‌کند. برای دستیابی به این هدف، ما مجبور شدیم پیچیدگی کد سمت سرور را به طور قابل توجهی افزایش دهیم. از این پس، ما همیشه فرض می‌کنیم که جاوا اسکریپت در مرورگر فعال است. این امر امکان ویژگی‌هایی را فراهم می‌کند که در حالت فقط-سرور امکان‌پذیر نیستند.

7.2.10. پردازش لینک [Calculer]

بیایید لینک [Calculer] را در صفحه اصلی [vue-01.xml] بررسی کنیم:

کد لینک [Calculer] در نما [vue-01.xml] به شرح زیر است:


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

تابع jS [postForm] در فایل [local1.js] به صورت زیر تعریف شده است:


// داده‌های جهانی
var loading;
var formulaire;
var résultats;
var a, b;

function postForm() {
    //آیا فرم معتبر است؟
    if (!formulaire.validate().form()) {
        // فرم نامعتبر – تکمیل شد
        return;
    }
    //دو لوکال مدیریت می‌شوند [fr_FR, en_US]
    //مقادیر واقعی [a,b] باید در همه موارد به فرمت آنگلو-ساکسون ارسال شوند
    // آنها توسط فیلتر [CultureFilter] پردازش خواهند شد

    //یک فراخوانی Ajax به‌صورت دستی انجام می‌شود
    $.ajax({
        url : '/ajax-02',
        headers : {
            ''X-Requested-With': 'XMLHttpRequest'
        },
        type : 'POST',
        data : formulaire.serialize(),
        dataType : 'html',
        beforeSend : function() {
            loading.show();
        },
        success : function(data) {
            resultats.html(data);
        },
        complete : function() {
            loading.hide();
        },
        error : function(jqXHR) {
            résultats.html(jqXHR.responseText);
        }
    })
}
  • خطوط ۲–۵: توجه داشته باشید که این عناصر توسط تابع [$(document).ready] مقداردهی اولیه شده‌اند؛
  • خطوط ۹–۱۲: اعتبارسنج‌های فرم jS اجرا می‌شوند. اگر هر یک از مقادیر نامعتبر باشد، عبارت [formulaire.validate().form()] مقدار false را برمی‌گرداند. در این حالت، اعتبارسنج [submit] فرم لغو می‌شود؛
  • خطوط ۱۸–۳۸: یک فراخوانی دستی Ajax انجام می‌شود؛
  • خط ۱۹: URL هدف فراخوانی Ajax؛
  • خطوط ۲۰–۲۲: آرایه‌ای از سربرگ‌ها HTTP که به سربرگ‌های پیش‌فرض موجود در درخواست HTTP افزوده می‌شود. در اینجا، هدر HTTP را اضافه می‌کنیم که به سرور نشان می‌دهد ما در حال انجام یک فراخوانی ایجکس هستیم؛
  • خط ۲۳: متد HTTP مورد استفاده؛
  • خط ۲۴: داده‌های در حال ارسال. [formulaire.serialize] رشته را برای ارسال ([culture=fr-FR&a=12,7&b=20,89]) از فرم با شناسه [formulaire] ایجاد می‌کند. در اینجا با مشکل مورد بحث در بالا مواجه می‌شویم: مقادیر [a,b] باید در قالب Anglo-Saxon ارسال شوند. می‌دانیم که این مشکل اکنون با ایجاد فیلتر [cultureFilter] حل شده است؛
  • خط 25: نوع داده‌ای که در پاسخ انتظار می‌رود. می‌دانیم که سرور یک جریان HTML را بازخواهد گرداند؛
  • خط ۲۶: متدی که هنگام شروع درخواست باید اجرا شود. در اینجا، مشخص می‌کنیم که کامپوننت با شناسه [loading] باید نمایش داده شود. این انیمیشن بارگذاری است؛
  • خط ۲۹: متدی که در صورت موفقیت درخواست Ajax باید اجرا شود. پارامتر [data] پاسخ کامل از سرور است. ما می‌دانیم که این یک استریم HTML است؛
  • خط ۳۰: کامپوننت با شناسه [résultats] با مقدار HTML از پارامتر [data] به‌روزرسانی می‌شود.
  • خط ۳۳: سیگنال انتظار پاک می‌شود؛
  • خط ۳۵: تابع زمانی اجرا می‌شود که پاسخ سرور دریافت شده باشد، صرف‌نظر از اینکه موفقیت‌آمیز باشد یا خطا؛
  • خطوط ۳۵–۳۷: در صورت بروز خطا (سرور پاسخی با شناسه HTTP و وضعیت نشان‌دهنده خطای سمت سرور بازگردانده است)، پاسخ سرور HTML در فیلد [resultats] نمایش داده می‌شود؛

در اینجا مثالی از اجرای برنامه آورده شده است:

7.3. به‌روزرسانی یک صفحه HTML با یک فید jSON

در مثال قبلی، سرور وب به درخواست Ajax HTTP با یک جریان HTML پاسخ داد. این جریان حاوی داده‌هایی همراه با قالب‌بندی HTML بود. ما پیشنهاد می‌کنیم مثال قبلی را مجدداً بررسی کنیم، این بار با استفاده از پاسخ‌های jSON (نشانه‌گذاری شیء 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. نما [vue-04.xml]

 

نما [vue-04.xml] بدنهٔ نما [vue-01.xml] را با تفاوت‌های زیر می‌گیرد:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        ...
        <script type="text/javascript" src="/js/local4.js"></script>
        <script th:inline="javascript">
            /*<![CDATA[*/
                    var culture = [[${resultats.culture}]];
                    Globalize.culture(culture);
                    /*]]>*/
        </script>
    </head>
    <body>
        <h2>Ajax - 04</h2>
    ...
        <form id="formulaire" name="formulaire" th:object="${actionModel01}">
...
            <p>
                <img id="loading" style="display: none" src="/images/loading.gif" />
                <a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>
            </p>
            <!-- 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>
  • خط ۵: جاوااسکریپت نما اکنون در فایل [local4.js] قرار دارد؛
  • خط 16: تگ [form] دیگر پارامترهای [data-ajax-attr] از کتابخانه [Unobtrusive Ajax] را ندارد. ما در اینجا از آن استفاده نخواهیم کرد. برچسب [form] نیز دارای ویژگی‌های [method] و [action] نیست که مشخص می‌کنند چگونه و کجا مقادیر وارد شده در فرم را ارسال کنیم. این به این دلیل است که قرار است توسط تابع jS (خط ۲۰) ارسال شود؛
  • خطوط 26–57: فیلد ID [resultats] که قبلاً یک فیلد خالی بود، اکنون حاوی کد HTML برای نمایش نتایج است؛
  • خطوط ۲۶–۳۴: سربرگ نتایج، که زمان محاسبه در آن نمایش داده می‌شود؛
  • خطوط ۳۵–۵۲: نتایج چهار عمل اصلی؛
  • خطوط ۵۳–۵۷: هرگونه پیام خطا ارسال‌شده توسط سرور؛

کد jS که هنگام بارگذاری نما [vue-04.xm] اجرا می‌شود، در فایل [local4.js] قرار دارد. این کد به شرح زیر است:


// داده‌های جهانی
    var loading;
    var formulaire;
    var résultats;
    var titre;
    var labelHeureCalcul;
    var heureCalcul;
    var aplusb;
    var amoinsb;
    var afoisb;
    var adivb;
    var msgErreur;

// هنگام بارگذاری سند
$(document).ready(function() {
    // بازیابی ارجاعات برای اجزای مختلف صفحه
    loading = $("#loading");
    formulaire = $("#formulaire");
    résultats = $('#نتایج');
    titre=$("#titre");
    labelHeureCalcul=$("#labelHeureCalcul");
    heureCalcul=$("#heureCalcul");
    aplusb=$("#aplusb");
    amoinsb=$("#amoinsb");
    afoisb=$("#afoisb");
    adivb=$("#adivb");
    msgErreur=$("#msgErreur");
    // کش کردن برخی عناصر
    résultats.hide();
    erreur.hide();
    loading.hide();
});
  • خطوط ۱۷–۲۷: مراجع jQuery برای تمام عناصر صفحه بازیابی می‌شوند؛
  • خط ۲۹: ناحیه نتایج پنهان است؛
  • خط ۳۰: همین‌طور ناحیه خطا؛
  • خط ۳۱: و همچنین تصویر متحرک بارگذاری؛
  • خطوط ۲–۱۲: مراجع بازیابی‌شده به‌عنوان سراسری (global) اعلام می‌شوند تا سایر توابع بتوانند به آن‌ها دسترسی داشته باشند؛

7.3.3. تابع jS [postForm]

لینک [Calculer] به شرح زیر است:


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

تابع jS [postForm] در فایل [local.js] به صورت زیر تعریف شده است:


function postForm() {
    //آیا فرم معتبر است؟
    if (!formulaire.validate().form()) {
        // فرم نامعتبر – پایان
        return;
    }
    // یک فراخوانی دستی Ajax انجام دهید
    $.ajax({
        url : '/ajax-05',
        headers : {
            'Accept: 'application/json'
        },
        type : 'POST',
        data : formulaire.serialize(),
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}

// قبل از فراخوانی Ajax
function onBegin() {
...
}

// پس از دریافت پاسخ سرور
// در صورت موفقیت
function onSuccess(data) {
...
}

// پس از دریافت پاسخ سرور
// در صورت خطا
function onError(jqXHR) {
...
}

// پس از [onSuccess, onError]
function onComplete() {
...
}
  • خطوط ۳–۶: قبل از ارسال مقادیر وارد شده، آنها بررسی می‌شوند. اگر نادرست باشند، اقدام POST برای فرم انجام نمی‌شود؛
  • خط ۹: مقادیر وارد شده به اقدام [/ajax-05] ارسال می‌شوند که در ادامه با جزئیات بیشتری به آن خواهیم پرداخت؛
  • خطوط ۱۰–۱۲: یک هدر HTTP برای اطلاع‌رسانی به سرور که ما انتظار پاسخ در قالب jSON را داریم؛
  • خط ۱۳: مقادیر وارد شده در آستانه ارسال هستند؛
  • خط ۱۴: سریال‌سازی مقادیر واردشده به یک رشته آماده برای ارسال ([a=1,6&b=2,4&culture=fr-FR]);
  • خط ۱۵: نوع پاسخ ارسال‌شده توسط سرور. این مقدار jSON خواهد بود؛
  • خط ۱۶: تابع قابل اجرا قبل از POST;
  • خط ۱۷: تابعی که در صورت موفقیت‌آمیز بودن پاسخ سرور باید اجرا شود. «موفقیت» یک درخواست HTTP توسط وضعیت پاسخ HTTP سرور تعیین می‌شود. پاسخ [HTTP/1.1 200 OK ] یک پاسخ موفق است. پاسخ [HTTP/1.1 500 Internal Server Error] یک پاسخ ناموفق است. وضعیت یک پاسخ HTTP با کد [200] یا [500] مشخص می‌شود. برخی از این کدها با «موفقیت» و برخی دیگر با «شکست» مرتبط هستند؛
  • خط ۱۸: تابع قابل اجرا پس از دریافت پاسخ سرور، زمانی که وضعیت HTTP آن پاسخ، خرابی را نشان می‌دهد؛
  • خط ۱۸: تابع نهایی که پس از توابع قبلی [onSuccess, onError] اجرا می‌شود؛

تابع [onBegin] به شرح زیر است:


// قبل از فراخوانی Ajax
function onBegin() {
    console.log("onBegin");
    // تصویر متحرک نمایش داده می‌شود
    loading.show();
    // عناصر خاصی از نما پنهان شده‌اند
    entete.hide();
    résultats.hide();
    erreur.hide();
}

قبل از بررسی سایر توابع jS در فراخوانی Ajax، باید پاسخ ارسال‌شده توسط اقدام [/ajax-05] را بدانیم.

7.3.4. عمل [/ajax-05]

عمل [/ajax-05] به شرح زیر است:


    @RequestMapping(value = "/ajax-05", method = RequestMethod.POST)
    @ResponseBody()
    // پردازش POST از نما [vue-04]
    public JsonResults ajax05(@Valid ActionModel01 formulaire, BindingResult result, Locale locale,    HttpServletRequest request, HttpSession session) throws InterruptedException {
        if(result.hasErrors()){
            //حالت غیرعادی – هیچ چیزی رندر نمی‌شود
            return null;
        }
        ...
}
  • خط ۲: ویژگی [ResponseBody] نشان می‌دهد که خود اکشن [/ajax-05] پاسخ را به کلاینت بازمی‌گرداند. از آنجا که کتابخانه jSON در وابستگی‌های پروژه گنجانده شده است، Spring Boot به‌طور خودکار این نوع اکشن را برای بازگرداندن یک jSON پیکربندی می‌کند. بنابراین، رشته jSON از نوع [JsonResults] (خط ۴) به کلاینت ارسال خواهد شد؛
  • خط ۲: مقادیر ارسال‌شده [a, b, culture] در یک نوع [ActionModel01] قرار داده می‌شوند، که اعتبارسنجی آن به صورت [@Valid ActionModel01] درخواست شده است. این فقط برای تشریفات است. ما فرض کرده‌ایم که جاوااسکریپت در مرورگر کلاینت فعال است و بنابراین، تا زمانی که داده‌ها ارسال شوند، مقادیر ارسال‌شده قبلاً در سمت کلاینت بررسی شده‌اند. با این حال، می‌توانیم سناریوی یک POST غیرمجاز را پیش‌بینی کنیم که از کلاینت ما jS استفاده نمی‌کند. در این حالت، اعتبارسنجی ممکن است با شکست مواجه شود؛
  • خطوط ۵–۷: در صورت بروز خطا، یک جریان خالی jSON بازگردانده می‌شود؛

بیایید بررسی خود را از عمل [/ajax-05] ادامه دهیم:


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

ctx.getMessage(clé_message, tableau_de_paramètres, locale)
    • [clé_message]: کلید پیامی که در حال جستجو است؛
    • [locale]: لوکِیلی که استفاده می‌شود. بنابراین، اگر این لوکیل [en_US] باشد، از فایل [messages_en.properties] استفاده خواهد شد؛
    • [tableau_de_paramètres]: پیام حاصل را می‌توان همانند [clé=message {0} {1}] پیکربندی کرد. دو پارامتر در این پیام وجود دارد: [{0} {1}]. به عنوان پارامتر دوم برای [ctx.getMessage]، باید یک آرایه با دو مقدار ارائه شود؛
  • خطوط ۱۰–۱۳: اگر در جلسه وقفه زمانی (timeout) رخ دهد، نخ (thread) جاری برای مدت زمان وقفه متوقف می‌شود؛

عمل [/ajax-05] به شرح زیر ادامه می‌یابد:


        //آماده‌سازی مدل برای نمای بعدی
        JsonResults résultats = new JsonResults();
        ...
}
  • خط ۲: ایجاد قالب برای رشته jSON که به کلاینت ارسال می‌شود؛

قالب [JsonResults] به شرح زیر است:

 

package istia.st.springmvc.models;

public class JsonResults {

    // داده‌ها
    private String titre;
    private String labelHeureCalcul;
    private String heureCalcul;
    private String aplusb;
    private String amoinsb;
    private String afoisb;
    private String adivb;
    private String msgErreur;

    // گیرنده‌ها و تنظیم‌کننده‌ها
...

}
  • خطوط ۶–۱۳: هر فیلد در کلاس [JsonResult] با فیلدی با همین نام در [id] در نما [vue-04.xml] مطابقت دارد:

عمل [/ajax-05] به شرح زیر انجام می‌شود:


        //آماده‌سازی مدل برای نمای بعدی
        JsonResults résultats = new JsonResults();
        // سربرگ
        résultats.setTitre(ctx.getMessage("resultats.titre", null, locale));
        résultats.setLabelHeureCalcul(ctx.getMessage("labelHeureCalcul", null, locale));
        résultats.setHeureCalcul(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        // هر بار دیگر یک خطا تولید می‌شود
        int val = new Random().nextInt(2);
        if (val == 0) {
            // بازگرداندن یک پیام خطا
            résultats.setMsgErreur(ctx.getMessage("resultats.erreur",
                    new Object[] { ctx.getMessage("erreur.aleatoire", null, locale) }, locale));
            return résultats;
}
  • خط ۲: ایجاد قالب برای رشته jSON که به کلاینت ارسال می‌شود؛
  • خطوط ۴–۶: پیام‌های سربرگ نتیجه ایجاد می‌شوند؛
  • خطوط ۸–۱۴: به طور متوسط، یک پیام خطا در هر بار دوم تولید می‌شود. در این حالت، فرآیند ادامه نمی‌یابد و رشته jSON به کلاینت بازگردانده می‌شود (خط ۱۳);
  • خط ۱۱: در اینجا مثالی از یک پیام سفارشی آمده است:

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

عمل [/ajax-05] به شرح زیر ادامه می‌یابد:


        // بازیابی مقادیر ارسال‌شده
        double a = formulaire.getA();
        double b = formulaire.getB();
        //مدل ساخته می‌شود
        résultats.setAplusb(String.valueOf(a + b));
        résultats.setAmoinsb(String.valueOf(a - b));
        résultats.setAfoisb(String.valueOf(a * b));
        try {
            résultats.setAdivb(String.valueOf(a / b));
        } catch (RuntimeException e) {
            résultats.setAdivb("NaN");
        }
        // نتیجه بازگردانده می‌شود
return résultats;
  • خطوط ۲–۳: مقادیر از [a] و [b] بازیابی می‌شوند؛
  • خطوط ۵–۱۲: چهار نتیجه ساخته می‌شوند؛
  • خط ۱۴: رشته jSON [JsonResults] به کلاینت ارسال می‌شود؛

بیایید ببینیم با کلاینت [Advanced Rest Client] چه اتفاقی می‌افتد:

  • در [1-2]، درخواستی برای POST به اکشن [/ajax-05] ارسال می‌شود؛
  • در [3]، مقادیر نادرست ارسال شده‌اند؛
  • در [4]، سرور پاسخ خالی برگردانده است؛
  • در [1]، مقادیر صحیح ارسال شده‌اند؛
  • در [2]، شیء jSON که توسط سرور بازگردانده شده، این بار با یک پیام خطا؛
  • در [1]، مقادیر صحیح ارسال می‌شوند؛
  • در [2]، ابجکت jSON که توسط سرور بازگردانده شده، این بار چهار نتیجه را نشان می‌دهد؛
  • در [1]، مقادیر صحیح ارسال می‌شوند؛
  • در [2]، ما موفق به ایجاد یک استثنای سمت سرور شده‌ایم. می‌توانیم ببینیم که سرور همچنان در حال ارسال یک شیء jSON است. در این پیام، می‌توانیم ببینیم که وضعیت پاسخ، HTTP، [500] است، که نشان می‌دهد یک خطای سمت سرور رخ داده است؛

7.3.5. تابع jS [postForm] – 2

اکنون که با شی jSON که توسط سرور بازگردانده شده آشنا شدیم، می‌توانیم از آن در جاوااسکریپت استفاده کنیم. متد [onSuccess] که زمانی اجرا می‌شود که سرور پاسخی با وضعیت HTTP [200] ارسال می‌کند، به شرح زیر است:


// پس از دریافت پاسخ سرور
// در صورت موفقیت
function onSuccess(data) {
    console.log("onSuccess");
    // میدان نتایج پر می‌شود
    titre.text(data.titre);
    labelHeureCalcul.text(data.labelHeureCalcul);
    heureCalcul.text(data.heureCalcul);
    entete.show();
    // نتایج بدون خطا
    if (!data.msgErreur) {
        aplusb.text(data.aplusb);
        amoinsb.text(data.amoinsb);
        afoisb.text(data.afoisb);
        adivb.text(data.adivb);
        résultats.show();
        return;
    }
    // نتایج با خطا
    msgErreur.text(data.msgErreur);
    erreur.show();
}
  • خط ۳: پارامتر [data] شیء jSON است که توسط سرور بازگردانده می‌شود:
 

متد [onError] که زمانی اجرا می‌شود که وضعیت پاسخ HTTP برابر [500] باشد، به شرح زیر است:


// پس از دریافت پاسخ سرور
//در صورت بروز خطا
function onError(jqXHR) {
    console.log("onError");
    // خطای سیستم
    msgErreur.text(jqXHR.responseText);
    erreur.show();
}
  • خط ۳: شیء JQuery [jqXHR] دارای ویژگی‌های زیر است:
    • responseText: متن پاسخ سرور،
    • وضعیت: کد خطای بازگردانده‌شده توسط سرور،
    • statusText: متنی که با این کد خطا مرتبط است؛
  • خط ۶: شیء [jqXHR.responseText] همان شیء jSON است:
 

7.3.6. آزمایش‌ها

بیایید نگاهی بیندازیم به چند اسکرین‌شات از اپلیکیشن وب در حال اجرا:

 
 
 

7.4. برنامه وب تک‌صفحه‌ای

7.4.1. مقدمه

فناوری Ajax امکان ایجاد برنامه‌های تک‌صفحه‌ای را فراهم می‌کند:

  • صفحهٔ اول از طریق یک درخواست استاندارد مرورگر بارگذاری می‌شود؛
  • صفحات بعدی از طریق فراخوانی‌های Ajax دریافت می‌شوند. در نتیجه، مرورگر هرگز URL را تغییر نمی‌دهد و هرگز صفحه جدیدی را بارگذاری نمی‌کند. این نوع برنامه به عنوان برنامه تک‌صفحه‌ای (APU) یا به انگلیسی، Single Page Application (SPA) شناخته می‌شود.

در اینجا یک مثال ساده از چنین برنامه‌ای آورده شده است. برنامه جدید دو نما خواهد داشت:

  • در [1]، اکشن [/ajax-06] به ما امکان می‌دهد صفحه اول، صفحه ۱ را نمایش دهیم؛
  • در [2]، یک لینک به ما امکان می‌دهد تا از طریق یک فراخوانی Ajax به صفحه ۲ برویم؛
  • در [3]، URL تغییر نکرده است. صفحه نمایش داده شده صفحه ۲ است؛
  • در [4]، یک لینک به ما امکان بازگشت به صفحهٔ ۱ را از طریق یک فراخوانی Ajax می‌دهد؛
  • در [5]، URL تغییر نکرده است. صفحه نمایش داده شده صفحه ۱ است.

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";
}
  • خطوط ۱–۴: عمل [/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>
  • خط ۸: نما از اسکریپت [local6.js] استفاده می‌کند؛
  • خط ۱۲: دیدگاه [vue-07.xml] در فیلد ID دیدگاه [content] از دیدگاه [vue-06.xml] گنجانده شده است؛

7.4.4. نما [vue-07.xml]

نما [vue-07.xml] به شرح زیر است:


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

7.4.5. توابع jS [gotoPage]

لینک [Page 2] در نما [vue-07.xml] از تابع jS [gotoPage] که در فایل زیر [local6.js] تعریف شده است، استفاده می‌کند:


// داده‌های جهانی
var content;

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

// هنگام بارگذاری سند
$(document).ready(function() {
    //بازیابی ارجاعات به اجزای مختلف صفحه
    content = $("#content");
});
  • خط ۲۸: هنگامی که صفحه بارگیری می‌شود، فیلدی با شناسه [content] ذخیره شده و به یک متغیر سراسری (خط ۲) اختصاص داده می‌شود؛
  • خط ۴: تابع [gotoPage] به عنوان پارامتر، شماره صفحه‌ای (۱ یا ۲) را که باید در نمای فعلی نمایش داده شود، دریافت می‌کند؛
  • خط ۷: URL هدف POST است؛
  • خط ۸: تابع URL از خط ۷ از طریق یک POST فراخوانی می‌شود؛
  • خط ۹: رشته ارسال‌شده. یک پارامتر با نام [num] ارسال شده است. مقدار آن شماره صفحه (خط ۴) است که باید در نمای فعلی نمایش داده شود؛
  • خط ۱۰: سرور HTML را بازمی‌گرداند که مربوط به صفحه‌ای است که باید نمایش داده شود؛
  • خطوط ۱۳–۱۵: در صورت موفقیت (وضعیت HTTP برابر با ۲۰۰)، مقدار HTML ارسال‌شده توسط سرور در فیلد ID یعنی [content] قرار می‌گیرد؛
  • خطوط ۱۸–۲۰: اگر عملیات ناموفق باشد (وضعیت HTTP برابر با ۵۰۰)، HTML ارسال‌شده توسط سرور در فیلد ID [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: شماره صفحه
        switch (num) {
        case 1:
            return "vue-07";
        case 2:
            return "vue-08";
        default:
            return "vue-07";
        }
    }
  • خط ۲: پارامتر ارسال‌شده‌ای به نام [num] بازیابی می‌شود. توجه داشته باشید که پارامتر در خط ۲ باید نام پارامتر ارسال‌شده را داشته باشد، در این مورد [num]. [num] شماره صفحه یا نما است؛
  • خطوط ۵–۶: اگر [num==1] باشد، نما [vue-07.xml] بازگردانده می‌شود؛
  • خطوط ۷–۸: اگر مقدار [num==2] باشد، نما [vue-08.xml] بازگردانده می‌شود؛
  • خطوط ۹–۱۰: در سایر موارد (که معمولاً نباید رخ دهد)، نما [vue-07.xml] بازگردانده می‌شود؛

7.4.7. نما [vue-08.xml]

نما [vue-08.xml] صفحهٔ ۲ از برنامه را تشکیل می‌دهد:


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

7.5. ادغام چندین خوراک HTML در یک پاسخ واحد jSON

7.5.1. مقدمه

برنامه زیر را در نظر بگیرید:

صفحه [1] دارای چهار فیلد است:

  • [Zone 1, Zone 3] مناطقی هستند که با کلیک روی دکمه [Rafraîchir] ظاهر یا ناپدید می‌شوند. ما تعداد دفعات ظاهر شدن هر یک از این دو ناحیه، [2]، را می‌شماریم. ناحیه [Zone 1] از زبان فرانسوی استفاده می‌کند، در حالی که ناحیه [Zone 3] از زبان انگلیسی استفاده می‌کند؛
  • میدان [Zone 2] همیشه موجود است؛
  • منطقه [Saisies] همیشه موجود است؛

لینک [Valider] صفحه زیر را نمایش می‌دهد، [3]:

  • لینک [Retour à la page 1] صفحه ۱ را به حالت قبلی خود بازمی‌گرداند ([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>
  • خط ۹: فایلی که در برنامه استفاده می‌شود JS;
  • خط ۱۵: محتوای صفحهٔ اصلی؛
  • خط ۱۶: یک تصویر متحرک بارگذاری:
  • خط ۱۷: ناحیه نمایش خطاها؛

نما [vue-09-page1.xml] صفحهٔ ۱ برنامه است:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h2>Page 1</h2>
        <!-- منطقه ۱ -->
        <fieldset id="zone1" style="background-color:pink">
            <legend>Zone 1</legend>
            <span id="zone1-content" th:text="xx">xx</span>
        </fieldset>
        <!-- منطقه ۲ -->
        <fieldset id="zone2" style="background-color:lightgreen">
            <legend>Zone 2</legend>
            <span>Ce texte reste toujours présent</span>
        </fieldset>
        <!-- منطقه ۳ -->
        <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>
  • خطوط ۶–۹: ناحیه [Zone 1]. محتوای آن در کامپوننت [id="zone1-content"] قرار می‌گیرد؛
  • خطوط ۱۱–۱۴: فیلد [Zone 2]، که بدون تغییر باقی می‌ماند؛
  • خطوط ۱۶–۱۹: فیلد [Zone 3]. محتویات آن در کامپوننت [id="zone3-content"] قرار می‌گیرد؛
  • خط ۲۲: تابع JS که فرم را ارسال می‌کند؛
  • خط ۲۵: درج فیلد ورودی؛

توجه داشته باشید که صفحهٔ ۱ تگ [form] ندارد. همه چیز با جاوااسکریپت مدیریت خواهد شد.

ویوی [vue-09-saisies.xml] به شرح زیر است:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <div id="saisies">
        <h4>Saisies :</h4>
        <p>
            Chaîne de caractères :
            <input type="text" id="text1" size="30" th:value="${value1}" />
        </p>
        <p>
            Nombre entier :
            <input type="text" id="text2" size="10" th:value="${value2}" />
        </p>
        <p>
            <a href="javascript:valider()">Valider</a>
        </p>
    </div>
</html>
  • خطوط ۵–۸: وارد کردن یک رشته کاراکتری؛
  • خطوط ۱۳–۱۶: وارد کردن یک عدد صحیح؛
  • خط ۱۴: تابع JS که مقادیر واردشده را ارسال می‌کند؛

بار دیگر باید توجه داشت که فیلد ورودی تگ [form] را ندارد.

در مجموع، صفحهٔ ۱ شامل دو تابع است:

  • [Rafraîchir]: که فیلدهای ۱ و ۳ را تازه‌سازی می‌کند. این عمل توسط سرور انجام می‌شود که به‌طور تصادفی یکی از موارد زیر را بازمی‌گرداند:
    • منطقه ۱ با شمارنده دسترسی خود و هیچ چیز برای منطقه ۳،
    • منطقه ۳ با شمارنده دسترسی خود و هیچ چیز برای منطقه ۱،
    • هر دو ناحیه با شمارشگر دسترسی خود؛
  • [Valider]: که صفحهٔ ۲ را با مقادیر واردشده نمایش می‌دهد یا در صورت نادرست بودن داده‌های واردشده، یک پیام خطا را نشان می‌دهد؛

ابتدا به دکمه [Rafraîchir] می‌پردازیم.

7.5.4. کد JS برای مدیریت دکمه [Rafraîchir]

  

کد مربوط به فایل [local9.js] به شرح زیر است:


// متغیرهای سراسری
var content;
var loading;
var erreur;

// هنگام بارگذاری سند
$(document).ready(function() {
    // بازیابی ارجاعات برای اجزای مختلف صفحه
    loading = $("#loading");
    loading.hide();
    erreur = $("#erreur");
    erreur.hide();
    content = $("#content");
});
  • خطوط ۹–۱۳: هنگامی که صفحه اصلی بارگذاری می‌شود، ارجاع به سه مؤلفه‌ای که توسط [loading, erreur, content] شناسایی شده‌اند، ذخیره می‌شود؛
  • خطوط ۲–۴: ارجاعات این سه مؤلفه در متغیرهای سراسری ذخیره می‌شوند. آنها ثابت باقی می‌مانند زیرا این سه ناحیه همیشه در صفحه نمایش‌داده‌شده حضور دارند. از آنجایی که این ارجاعات ثابت باقی می‌مانند، می‌توان آنها را در [$(document).ready] محاسبه کرد و با سایر توابع در فایل JS به اشتراک گذاشت؛

تابع [postForm] کلیک روی دکمه [Rafraîchir] را مدیریت می‌کند:


function postForm() {
    console.log("postForm");
    // یک فراخوانی Ajax به‌صورت دستی انجام می‌شود
    $.ajax({
        url : '/ajax-10',
        headers : {
            'Accept: 'application/json'
        },
        type : 'POST',
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}
  • خطوط ۴–۱۵: فراخوانی Ajax به سرور؛
  • خط ۵: اکشن [ajax-10] اقدام POST را پردازش خواهد کرد؛
  • خطوط ۶–۸: پاسخ jSON خواهد بود. کلاینت JS نشان می‌دهد که اسناد jSON را می‌پذیرد؛
  • خط ۹: عمل [ajax-10] با عملیاتی به نام POST فراخوانی می‌شود؛
  • خط ۱۰: ما jSON را دریافت خواهیم کرد؛
  • خط ۱۱: تابع اجرا شده قبل از فراخوانی Ajax؛
  • خط ۱۲: تابع اجرا شده هنگام دریافت پاسخ سرور، زمانی که فراخوانی موفقیت‌آمیز باشد: [200 OK];
  • خط ۱۳: تابع اجرا شده هنگام دریافت پاسخ سرور، زمانی که درخواست ناموفق باشد [500 Internal server error, ...];
  • خط ۱۴: تابع اجرا شده پس از دریافت پاسخ؛

تابع [onBegin] به شرح زیر است:


// قبل از فراخوانی Ajax
function onBegin() {
    console.log("onBegin");
    // بارگذاری تصویر
    loading.show();
}

این تابع صرفاً تصویر متحرکی را که انتظار پاسخ سرور را نشان می‌دهد، اجرا می‌کند.

7.5.5. عمل [/ajax-10]

  

عمل [/ajax-10] به شرح زیر است:


// سشن
    @Autowired
    private SessionModel1 session;
    // موتور Thymeleaf / Spring
    @Autowired
    private SpringTemplateEngine engine;

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

package istia.st.springmvc.models;

import java.io.Serializable;

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

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

    private static final long serialVersionUID = 1L;
    // دو شمارنده
    private int cpt1 = 0;
    private int cpt3 = 0;
    // سه فیلد
    private String zone1 = "xx";
    private String zone3 = "zz";
    private String saisies;
    private boolean zone1Active = true;
    private boolean zone3Active = true;

    // گیرنده و تنظیم‌کننده
    ...
}

سشن [SessionModel1] عناصر زیر را ذخیره می‌کند:

  • خط ۱۵: تعداد دفعات نمایش [cpt1] که در آن فیلد [Zone 1] نمایش داده می‌شود؛
  • خط ۱۶: تعداد دفعات [cpt3] که در آن فیلد [Zone 3] نمایش داده می‌شود؛
  • رده‌های ۱۸–۲۰: جریان HTML از نواحی [Zone 1]، [Zone 3] و [Saisies] سرچشمه می‌گیرد. این در توالی [Page 1] --> [Page 2] --> [Page 1] ضروری است. هنگام حرکت از [Page 2] به [Page 1]، باید [Page 1] بازیابی شود، و بنابراین سه فیلد آن؛
  • رده‌های ۲۱–۲۲: دو بولین که نشان می‌دهند آیا فیلدهای [Zone 1] و [Zone 3] نمایش داده می‌شوند (مشهود)؛

عنصر دیگری که به کنترل‌کننده [AjaxController] تزریق شده است به شرح زیر است:


    //موتور Thymeleaf / Spring
    @Autowired
private SpringTemplateEngine engine;

بیان از نوع [SpringTemplateEngine] در فایل پیکربندی [Config] تعریف شده است:

  

به صورت زیر تعریف شده است:


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

    @Bean
    SpringTemplateEngine templateEngine(SpringResourceTemplateResolver templateResolver) {
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);
        return templateEngine;
}
  • خطوط ۲–۱۰: ما با بین از نوع [SpringResourceTemplateResolver] آشنا هستیم، که به ما امکان می‌دهد ویژگی‌های خاصی از ویوها را تعریف کنیم؛
  • خطوط ۱۳–۱۷: بین با نوع [SpringTemplateEngine] به ما امکان می‌دهد موتور نما را تعریف کنیم، کلاسی که مسئول تولید پاسخ‌های [Thymeleaf] برای کلاینت‌ها است. [Thymeleaf] دارای یک «engine» پیش‌فرض است و دیگری زمانی که در محیط [Spring] استفاده می‌شود. این مورد دوم است که ما در اینجا استفاده می‌کنیم؛

امضای اکشن [/ajax-10] به شرح زیر است:


@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
    @ResponseBody()
    public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
    ...
}
  • خط ۱: اکشن [/ajax-10] فقط یک POST را می‌پذیرد؛
  • خط ۲: خود اکشن [/ajax-10] پاسخ را به کلاینت بازمی‌گرداند. این پاسخ به طور خودکار به jSON تبدیل خواهد شد؛
  • خط ۳: پاسخ از نوع [JsonResult10] به شرح زیر است:
  

package istia.st.springmvc.models;

public class JsonResult10 {

    // داده‌ها
    private String content;
    private String zone1;
    private String zone3;
    private String erreur;
    private String saisies;
    private boolean zone1Active;
    private boolean zone3Active;

    public JsonResult10() {
    }

    // گیرنده‌ها و تنظیم‌کننده‌ها
...
}
  • خط ۶: محتوای HTML از فیلدی که با [content] مشخص شده است؛
  • خط ۷: محتوای HTML از فیلد [Zone 1];
  • خط ۸: محتوای HTML از ناحیه [Zone 3];
  • خط ۹: محتویات HTML از فیلد [Erreur];
  • خط ۱۰: محتویات HTML از فیلد [Saisies];
  • خط ۱۱: یک مقدار بولی که نشان می‌دهد آیا فیلد [Zone 1] باید نمایش داده شود؛
  • خط ۱۲: یک مقدار بولی که نشان می‌دهد آیا فیلد [Zone 3] باید نمایش داده شود؛

کد اقدام برای [/ajax-10] به شرح زیر است:


@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
    @ResponseBody()
    public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
        // زمینه Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        //پاسخ
        JsonResult10 result = new JsonResult10();
        // جلسه
        session.setZone1(null);
        session.setZone3(null);
        session.setZone1Active(false);
        session.setZone3Active(false);
        // یک پاسخ تصادفی بازگردانده می‌شود
        int cas = new Random().nextInt(3);
        switch (cas) {
        case 0:
            // منطقه ۱ فعال
            setZone1(thymeleafContext, result);
            return result;
        case 1:
            // منطقه ۳ فعال
            setZone3(thymeleafContext, result);
            return result;
        case 2:
            // زون‌های ۱ و ۳ فعال
            setZone1(thymeleafContext, result);
            setZone3(thymeleafContext, result);
            return result;
        }
        return null;
    }
  • خط ۵: ما زمینه [Thymeleaf] را بازیابی می‌کنیم. بعداً خواهیم دید که این برای چه منظوری استفاده می‌شود؛
  • خط ۷: در حال حاضر یک پاسخ خالی ایجاد می‌کنیم؛
  • خطوط ۹–۱۲: ما دو فیلد موجود در جلسه را به [null] اختصاص می‌دهیم و مشخص می‌کنیم که نباید نمایش داده شوند. این دو فیلد به زودی ایجاد خواهند شد، اما ممکن است تنها یکی از آنها ایجاد شود؛
  • خطوط ۱۴–۲۹: هر دو فیلد تولید می‌شوند؛
  • خطوط ۱۷–۱۹: تنها فیلد [Zone 1] تولید می‌شود؛
  • خطوط ۲۱–۲۳: تنها فیلد [Zone 3] تولید می‌شود؛
  • خطوط ۲۵–۲۸: هر دو ناحیه [Zone 1] و [Zone 3] ایجاد می‌شوند؛

جریان HTML از ناحیه [Zone 1] با استفاده از روش زیر تولید می‌شود:


    private void setZone1(WebContext thymeleafContext, JsonResult10 result) {
        // منطقه ۱ فعال
        // HTML جریان
        int cpt1 = session.getCpt1() + 1;
        thymeleafContext.setVariable("cpt1", cpt1);
        thymeleafContext.setLocale(new Locale("fr", "FR"));
        String zone1 = engine.process("vue-09-zone1", thymeleafContext);
        result.setZone1(zone1);
        result.setZone1Active(true);
        // جلسه
        session.setCpt1(cpt1);
        session.setZone1(zone1);
        session.setZone1Active(true);
}
  • خط ۱: پارامترها عبارتند از:
    • زمینه [Thymeleaf] از نوع [WebContext
    • پاسخ در حال ساخت برای کلاینت، از نوع [JsonResult10
  • خط ۳: شمارنده جلسه [cpt1]، که تعداد دفعات نمایش فیلد [Zone 1] را می‌شمارد، افزایش می‌یابد؛
  • خط ۴: زمینه [Thymeleaf]، از نوع [WebContext]، تا حدودی مانند قالب Spring [Model] MVC رفتار می‌کند. برای افزودن یک عنصر به قالب، از [WebContext.setVariable] استفاده می‌کنیم. در اینجا، بنابراین شمارنده [cpt1] را در قالب [Thymeleaf] قرار می‌دهیم. این کار امکان ارزیابی عبارت Thymeleaf [${cpt1}] را فراهم می‌کند
  • خط ۵: زمینه [Thymeleaf] دارای یک لوکال است. این امکان را فراهم می‌کند تا عبارت‌هایی از نوع [#{clé_msg}] ارزیابی شوند. در اینجا، زمینه Thymeleaf با یک لوکال فرانسوی مرتبط است؛
  • خط ۶: این جالب‌ترین دستور است. موتور Thymeleaf نمای [vue-09-zone1.xml] را با استفاده از قالب و لوکال به‌تازگی محاسبه شده پردازش می‌کند و به جای ارسال جریان خروجی HTML به کلاینت، آن را به صورت یک رشته بازمی‌گرداند؛
  • خطوط ۷–۹: جریان HTML از فیلد [Zone 1] که به‌تازگی محاسبه شده است، در جلسه و در نتیجه ارسال‌شده به کلاینت ذخیره می‌شود. علاوه بر این، مشخص شده است که فیلد [Zone 1] باید نمایش داده شود؛
  • خطوط ۱۱–۱۳: اطلاعات مربوط به ناحیه [Zone 1] در جلسه ذخیره می‌شود تا بتوان آن را دوباره تولید کرد؛

خط ۷ نمای زیر را پردازش می‌کند [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>
  • خط ۳: عبارت [#{message.zone}] با استفاده از لوکال ارزیابی خواهد شد؛
  • خط ۴: عبارت [${cpt1}] با استفاده از قالب Thymeleaf ارزیابی خواهد شد؛

پیام با کلید [message.zone] در فایل‌های پیام [messages_fr.properties] و [messages_en.properties] تعریف شده است:

  

[messages_fr.properties]


message.zone=Nombre d'accès : 

[messages_en.properties]


message.zone=Number of hits: 

جریان HTML از ناحیه [Zone 3] با استفاده از روشی مشابه تولید می‌شود:


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

7.5.6. پردازش پاسخ از اقدام [/ajax-10]

بیایید به کد JS از [local9.js] بازگردیم که پاسخ سرور را پردازش خواهد کرد:


// پس از دریافت پاسخ سرور
// در صورت موفقیت
function onSuccess(data) {
    console.log("onSuccess");
    // محتوا
    if (data.content) {
        content.html(data.content);
    }
    // منطقه ۱
    if (data.zone1Active) {
        $("#zone1").show();
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
        }
    } else {
        $("#zone1").hide();
    }
    // منطقه ۳ فعال؟
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    //ورودی‌ها؟
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // خطا؟
    if (data.erreur) {
        erreur.text(data.erreur);
        erreur.show();
    } else {
        erreur.hide();
    }
}

بیایید ساختار جاوا پاسخ دریافت‌شده در خط ۳ در متغیر [data] را به یاد بیاوریم:


public class JsonResult10 {

    // داده‌ها
    private String content;
    private String zone1;
    private String zone3;
    private String erreur;
    private String saisies;
    private boolean zone1Active;
    private boolean zone3Active;

}
  • رده‌های ۶–۸: اگر [data.content!=null] باشد، آنگاه فیلد [id=content] با آن مقداردهی اولیه می‌شود. این فیلد نمایانگر [Page 1] یا [Page 2] به‌طور کامل است. در این نمایش، ما [data.content==null] را داریم، بنابراین فیلد [id=content] تغییر نخواهد کرد و به نمایش [Page 1] ادامه خواهد داد؛
  • خطوط ۱۰–۱۷: اگر [data.zone1Active==true] باشد، [Zone 1] را نمایش می‌دهد. اگر علاوه بر این، [data.zone1!=null] باشد، آنگاه محتوای [Zone 1] تغییر می‌کند؛ در غیر این صورت، همان‌طور که بود باقی می‌ماند؛
  • خطوط ۱۹–۲۶: همین امر در مورد [Zone 3] نیز صدق می‌کند؛
  • خطوط ۲۸–۳۰: اگر [data.saisies!=null] موجود باشد، آنگاه ناحیه [Saisies] بازتولید می‌شود. در این نمایش، ما [data.saisies==null] را داریم و بنابراین ناحیه [Saisies] همان‌طور که بود باقی می‌ماند؛
  • خطوط ۳۲–۳۷: استدلال برای فیلد [Erreur] مشابه است، با ظرایف زیر:
    • خط ۳۳: [data.erreur] یک پیام خطای متنی خواهد بود؛
    • خط ۳۶: اگر [data.erreur==null] باشد، آنگاه فیلد [Erreur] پنهان می‌شود. این به این دلیل است که ممکن است در طول درخواست قبلی نمایش داده شده باشد؛

در صورت بروز خطای سمت سرور (وضعیت HTTP مانند خطای داخلی سرور 500)، تابع زیر اجرا می‌شود:


// پس از دریافت پاسخ سرور
//در صورت بروز خطا
function onError(jqXHR) {
    console.log("onError");
    // خطای سیستم
    erreur.text(jqXHR.responseText);
    erreur.show();
}

برای مشاهده چنین خطایی، تابع [postForm] را به صورت زیر تغییر می‌دهیم:


function postForm() {
    console.log("postForm");
    // بازیابی ارجاعات از صفحهٔ جاری
    ...
    // یک فراخوانی دستی Ajax انجام می‌شود
    $.ajax({
        url : '/ajax-10x',
        ...
    })
}
  • خط ۷: یک URL که وجود ندارد را وارد می‌کنیم؛

در اینجا نتایج هنگام کلیک بر روی دکمه [Rafraîchir] آورده شده است:

جالب است بدانید که خطا به صورت یک رشته نیز ارسال شده است: jSON.

متدی که پس از دریافت پاسخ سرور اجرا می‌شود به شرح زیر است:


// پس از [onSuccess, onError]
function onComplete() {
    console.log("onComplete");
    // بارگذاری تصویر
    loading.hide();
}

ما به سادگی تصویر متحرک بارگذاری را مخفی می‌کنیم.

7.5.7. نمایش صفحه [Page 2]

کد HTML برای لینک [Valider] به شرح زیر است:


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

تابع JS [valider] به شرح زیر است:


//اعتبارسنجی مقادیر واردشده
function valider() {
    // مقدار ارسال‌شده
    var post = JSON3.stringify({
        "value1" : $("#text1").val().trim(),
        "value2" : $("#text2").val().trim()
    });
    // انجام دستی یک فراخوانی Ajax
    $.ajax({
        url : '/ajax-11A',
        headers : {
            'Accept: 'application/json',
            'Content-Type: 'application/json'
        },
        type : 'POST',
        data : post,
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}
  • خطوط ۴–۷: ما دو مقدار v1 و v2 داریم که باید ارسال شوند: این مقادیر از کامپوننت‌های ورودی شناسایی‌شده توسط [#text1] و [#text2] هستند. ما قصد داریم کاری جدید انجام دهیم. ما این دو مقدار را به صورت یک رشته ارسال خواهیم کرد: jSON {"value1":v1,"value2":v2};
  • خط ۱۰: مقادیر ارسال‌شده به اکشن [ajax-11A] ارسال خواهند شد؛
  • خط ۱۲: از آنجا که می‌دانیم قرار است پاسخی از jSON دریافت کنیم، مشخص می‌کنیم که می‌توانیم آن را از jSON دریافت کنیم؛
  • خط ۱۳: به سرور می‌گوییم که قصد داریم مقدار ارسال‌شده را به صورت یک رشته jSON برای آن ارسال کنیم؛
  • خطوط ۱۵–۱۶: ما مقدار ارسال‌شده را به POST تبدیل می‌کنیم؛
  • خط ۱۷: قرار است یک 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) {
        ...
    }
  • خط ۱: ["application/json"] نشان می‌دهد که این عمل انتظار یک سند در قالب jSON را دارد. این سند، مقداری است که توسط کلاینت ارسال شده است؛
  • خط ۳: مقدار ارسال‌شده از شیء زیر، [PostAjax11A post]، بازیابی خواهد شد:
  

package istia.st.springmvc.models;

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

import org.hibernate.validator.constraints.Range;

public class PostAjax11A {

    // داده‌ها
    @Size(min = 4, max = 6)
    @NotNull
    private String value1;
    @Range(min = 10, max = 14)
    @NotNull
    private Integer value2;

    // گیرنده و تنظیم‌کننده
    ...
}
  • ساختار شی [PostAjax11A] باید با ساختار شی ارسال‌شده {"value1":v1,"value2":v2} مطابقت داشته باشد. بنابراین فیلدهای [value1] (خط ۱۳) و [value2] (خط ۱۶) الزامی هستند؛
  • ما محدودیت‌های یکپارچگی را برای هر دو فیلد اعمال کرده‌ایم؛

بیایید به کد مربوط به اقدام [ajax-11A] بازگردیم:


@RequestMapping(value = "/ajax-11A", method = RequestMethod.POST, consumes = "application/json")
    @ResponseBody
    public JsonResult10 ajax11A(@RequestBody @Valid PostAjax11A post, BindingResult bindingResult, Locale locale,    HttpServletRequest request, HttpServletResponse response) {
        // زمینه Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // پاسخ
        JsonResult10 result = new JsonResult10();
        //آیا POST معتبر است؟
        if (bindingResult.hasErrors()) {
            // صفحهٔ ۱ با خطا بازگردانده می‌شود
            result.setZone1Active(session.isZone1Active());
            result.setZone3Active(session.isZone3Active());
            result.setErreur(getErreursForModel(bindingResult));
            return result;
        }
        ...
}
  • خط ۳: حاشیه‌نویسی [@RequestBody] به سندی اشاره دارد که توسط کلاینت ارسال شده است. این همان مقداری است که کلاینت در jSON ارسال کرده است. بنابراین، این مقدار برای ساختن شیء [PostAjax11A] استفاده خواهد شد؛
  • خط ۳: حاشیه‌نویسی [@Valid] اعتبارسنجی مقدار ارسال‌شده را الزامی می‌کند؛
  • خط ۹: اگر اعتبارسنجی ناموفق باشد:
    • خط ۱۳: یک پیام خطا بازگردانده می‌شود؛
    • خطوط ۱۱–۱۲: فیلدهای ۱ و ۳ به وضعیت قبلی خود بازگردانده می‌شوند (چه نمایش داده شده باشند یا نه)؛

پیام خطا به شرح زیر محاسبه می‌شود:


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

این تابعی است که قبلاً با آن مواجه شده‌ایم.

عمل [ajax-11A] به شرح زیر ادامه می‌یابد:


@RequestMapping(value = "/ajax-11A", method = RequestMethod.POST, consumes = "application/json")
    @ResponseBody
    public JsonResult10 ajax11A(@RequestBody @Valid PostAjax11A post, BindingResult bindingResult, Locale locale,    HttpServletRequest request, HttpServletResponse response) {
        // زمینه Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // پاسخ
        JsonResult10 result = new JsonResult10();
        // پست معتبر است؟
        if (bindingResult.hasErrors()) {
    ...
        }
        // میدان ورودی ذخیره شد
        thymeleafContext.setVariable("value1", post.getValue1());
        thymeleafContext.setVariable("value2", post.getValue2());
        session.setSaisies(engine.process("vue-09-saisies", thymeleafContext));
        // ارسال صفحه ۲
        result.setContent(engine.process("vue-09-page2", thymeleafContext));
        return result;
}
  • خطوط ۱۳–۱۴: مقادیر ارسال‌شده در زمینه Thymeleaf قرار می‌گیرند؛
  • خط ۱۵: با استفاده از این زمینه، نما [vue-09-saisies] رندر شده و در جلسه ذخیره می‌شود تا بعداً دوباره تولید شود؛
  • خط ۱۷: صفحه ۲ در پاسخ ارسال‌شده به کلاینت گنجانده می‌شود؛

نما [vue-09-page2.xml] به شرح زیر است:

  

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h2>Page 2</h2>
        <p>
            <h4>Valeurs saisies :</h4>
            <p>
                Chaîne de caractères :
                <span th:text="${value1}"></span>
            </p>
            <p>
                Nombre entier :
                <span th:text="${value2}"></span>
            </p>
            <a href="javascript:retourPage1()">Retour à la page 1</a>
        </p>
    </body>
</html>
  • خطوط ۹ و ۱۳ مقادیر [value1, value2] را که اقدام [/ajax-11A] در زمینه Thymeleaf قرار داده است، نمایش می‌دهند؛

7.5.9. پردازش پاسخ از اکشن [/ajax-11A]

در سمت کلاینت، پاسخ ارسالی از اکشن [/ajax-10] توسط تابع [onSuccess] پردازش می‌شود:


function onSuccess(data) {
    console.log("onSuccess");
    // محتوا
    if (data.content) {
        content.html(data.content);
    }
    // field 1
    if (data.zone1Active) {
        $("#zone1").show();
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
        }
    } else {
        $("#zone1").hide();
    }
    //آیا فیلد ۳ فعال است؟
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // ورودی‌ها؟
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // خطا؟
    if (data.erreur) {
        erreur.text(data.erreur);
        erreur.show();
    } else {
        erreur.hide();
    }
}

ما قبلاً در مورد این کد نظر داده‌ایم. بیایید دو حالت را در نظر بگیریم: پاسخی با خطا یا بدون خطا:

با خطا

در این حالت، اقدام [/ajax-11A] یک پاسخ jSON را به شکل {"zone1":null, "zone3":null,"inputs":null,"error":error,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":null}. اگر کد بالا را دنبال کنیم، می‌توانیم ببینیم که:

  • میدان [content] تغییر نمی‌کند. این میدان حاوی شماره صفحه ۱ بود؛
  • میدان [Erreur] نمایش داده می‌شود؛
  • منطقه‌های [Zone 1]، [Zone 3] و [Saisies] بدون تغییر باقی می‌مانند؛

بدون خطا

در این مورد، اقدام [/ajax-11A] پاسخی را در قالب jSON بازگرداند: {"zone1":null, "zone3":null,"entries":null,"error":null,"zone1Active":false,"zone3Active":false,"content":content}. اگر کد بالا را دنبال کنیم، می‌بینیم که:

  • منطقه [content] نمایش داده می‌شود. این منطقه شامل صفحه شماره ۲ است؛

در اینجا سه مثال از اجرا آورده شده است:

یک مورد با خطای اعتبارسنجی:

یک مثال با خطا در POST:

این نوع خطا متفاوت است. از آنجا که Spring نتوانست رشته jSON را به نوع [PostAjax11A] تبدیل کند، پاسخی از نوع HTTP با [status=400] بازگرداند. اقدام [ajax-11A] اجرا نشد؛

یک مورد بدون خطا:

7.5.10. بازگشت به صفحه ۱

لینک [Retour vers la page 1] در صفحهٔ ۲ به شرح زیر است:


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

روش JS [retourPage1] به شرح زیر است:


//بازگشت به صفحهٔ ۱
function retourPage1() {
    // انجام تماس Ajax به‌صورت دستی
    $.ajax({
        url : '/ajax-11B',
        headers : {
            'Accept: 'application/json',
        },
        type : 'POST',
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}

این یک POST را بدون هیچ مقدار ارسال‌شده‌ای به اقدام [/ajax-11B] انجام می‌دهد.

7.5.11. اقدام [/ajax-11B]

عمل [/ajax-11B] به شرح زیر است:


    @RequestMapping(value = "/ajax-11B", method = RequestMethod.POST)
    @ResponseBody
    public JsonResult10 ajax11B(HttpServletRequest request, HttpServletResponse response) {
        // زمینه Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // پاسخ
        JsonResult10 result = new JsonResult10();
        // ما صفحهٔ ۱ را به وضعیت اصلی‌اش بازمی‌گردانیم
        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;
}

اقدام باید صفحه شماره ۱ را با سه فیلد [Zone1, Zone3, Erreur] آن مجدداً تولید کند:

  • خط ۹: صفحه شماره ۱ در نتیجه گنجانده شده است؛
  • خط ۱۰: فیلد ورودی در نتیجه گنجانده شده است؛
  • خط ۱۱: فیلد [Zone 1] در نتیجه گنجانده شده است؛
  • خط ۱۲: فیلد [Zone 3] در نتیجه گنجانده شده است؛
  • خطوط ۱۳–۱۴: وضعیت فیلدهای [Zone 1] و [Zone 3] در نتیجه گنجانده شده است؛

7.5.12. پردازش پاسخ از اقدام [/ajax-11B]

پاسخ حاصل از اقدام [/ajax-11B] توسط تابع [onSuccess] پردازش می‌شود:


function onSuccess(data) {
    console.log("onSuccess");
    // محتوا
    if (data.content) {
        content.html(data.content);
    }
    // منطقه ۱
    if (data.zone1Active) {
        $("#zone1").show();
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
        }
    } else {
        $("#zone1").hide();
    }
    //آیا ناحیه ۳ فعال است؟
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // ورودی‌ها؟
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // خطا؟
    if (data.erreur) {
        erreur.text(data.erreur);
        erreur.show();
    } else {
        erreur.hide();
    }
}

اقدام [/ajax-11B] یک پاسخ jSON را در قالب {"zone1":zone1, "zone3":zone3,"entries":entries,"error":null,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":content}. اگر کد بالا را بررسی کنیم، می‌توانیم ببینیم که:

  • میدان [content] تغییر یافته است. پیش از این حاوی صفحه شماره ۲ بود. اکنون حاوی صفحه شماره ۱ خواهد بود؛
  • منطقه [Erreur] مخفی شده است؛
  • میدان‌های [Zone 1]، [Zone 3] و [Saisies] دقیقاً همان‌طور که بودند نمایش داده می‌شوند؛

7.6. مدیریت جلسه در سمت کلاینت

7.6.1. مقدمه

در بخش قبلی، ما یک جلسه را با ساختار زیر مدیریت کردیم:


public class SessionModel1 implements Serializable {

    // دو شمارنده
    private int cpt1 = 0;
    private int cpt3 = 0;
    // هر سه ناحیه
    private String zone1 = "xx";
    private String zone3 = "zz";
    private String saisies;
    private boolean zone1Active = true;
    private boolean zone3Active = true;
...
}

وقتی تعداد کاربران زیاد باشد، حافظه‌ای که جلسات تمام این کاربران اشغال می‌کنند می‌تواند مشکل‌ساز شود. بنابراین، قاعده این است که اندازه جلسه را به حداقل برسانیم. مدل APU (برنامه تک‌صفحه‌ای) امکان مدیریت جلسه را در سمت کلاینت فراهم می‌کند و یک سرور وب بدون جلسه را ممکن می‌سازد. در واقع، صفحه واحد در ابتدا توسط مرورگر بارگذاری می‌شود. فایل جاوااسکریپت همراه آن نیز وجود دارد. از آنجایی که صفحه دوباره بارگیری نمی‌شود، این فایل JS به همان شکلی که در ابتدا بارگیری شده بود، به طور دائم در مرورگر باقی می‌ماند. سپس می‌توانیم از متغیرهای سراسری آن برای ذخیره اطلاعات مربوط به اقدامات مختلف کاربر استفاده کنیم. این چیزی است که اکنون به آن خواهیم پرداخت. ما نه تنها جلسه را در سمت کلاینت مدیریت خواهیم کرد، بلکه اپلیکیشن JS را نیز بازطراحی خواهیم کرد تا درخواست‌های سرور را تا حد امکان به حداقل برسانیم.

7.6.2. اقدام [/ajax-12]

  

عمل [/ajax-12] به شرح زیر است:


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

نما [vue-12.xml] به شرح زیر است:

  

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

نما نمایش داده شده به شرح زیر است:

 

7.6.3. کد JS برای مدیریت دکمه [Rafraîchir]

  

کد فایل [local12.js] به شرح زیر است:


// متغیرهای سراسری
var content;
var loading;
var erreur;
var page1;
var page2;
var value1;
var value2;
var session = {
        "cpt1" : 0,
        "cpt3" : 0
    };

// هنگامی که سند بارگذاری می‌شود
$(document).ready(function() {
    // ما ارجاعات اجزای مختلف صفحه را بازیابی می‌کنیم
    loading = $("#loading");
    loading.hide();
    erreur = $("#erreur");
    erreur.hide();
    content = $("#content");
});
  • خطوط ۱۷–۲۱: هنگامی که صفحه اصلی بارگذاری می‌شود، ارجاع‌های سه کامپوننت شناسایی‌شده توسط [loading, erreur, content] در متغیرهای سراسری در خطوط ۲–۴ ذخیره می‌شوند؛
  • خطوط ۵–۶: برای ذخیره دو صفحه؛
  • خطوط ۷–۸: برای ذخیره دو مقداری که از طریق لینک [Valider] ارسال شده‌اند؛
  • خط ۹: session. این متغیر مقادیر شمارنده‌های [cpt1, cpt3] را در سمت کلاینت ذخیره می‌کند؛

تابع [postForm] کلیک روی دکمه [Rafraîchir] را مدیریت می‌کند:


function postForm() {
    console.log("postForm");
    //جلسه تنظیم می‌شود
    var post = JSON3.stringify(session);
    // یک فراخوانی Ajax به‌صورت دستی انجام می‌شود
    $.ajax({
        url : '/ajax-13',
        headers : {
            'Accept: 'application/json',
            'Content-Type: 'application/json'
        },
        type : 'POST',
        data : post,
        dataType : 'json',
        beforeSend : onBegin,
        success : function(data) {
            ...
        },
        error : onError,
        complete : onComplete
    })
}

تفاوت‌ها نسبت به نسخه قبلی به شرح زیر است:

  • تابع URL در خط ۷ متفاوت است؛
  • خط ۴: اکنون یک مقدار ارسال می‌شود، در حالی که قبلاً هیچ مقداری ارسال نمی‌شد. این مقدار رشته jSON از جلسه است. اصل به شرح زیر است:
    • کلاینت جلسه را به سرور ارسال می‌کند،
    • سرور آن را تغییر داده و بازمی‌فرستد،
    • کلاینت جلسه جدید را ذخیره می‌کند؛
  • خط ۱۰: یک سند در قالب jSON ارسال می‌شود (ارزش ارسال‌شده)؛
  • خط ۱۳: چیزی برای ارسال وجود دارد؛
  • خطوط ۱۵–۲۰: توابع [beforeSend, error, complete] همانند نسخهٔ قبلی هستند. تنها تابع [success] تغییر می‌کند (خطوط ۱۶–۱۸)؛

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) {
    ...
}
  • خط ۳: پارامتر [@RequestBody SessionModel2 session2] جلسه ارسال‌شده توسط کلاینت را بازیابی می‌کند. این پارامتر دارای نوع زیر است، [SessionModel2]:
  

package istia.st.springmvc.models;

import java.io.Serializable;

public class SessionModel2 implements Serializable {

    private static final long serialVersionUID = 1L;
    // دو شمارنده
    private int cpt1 = 0;
    private int cpt3 = 0;

    // گیرنده و تنظیم‌کننده
    ...
}

جلسه [SessionModel2] عناصر زیر را ذخیره می‌کند:

  • خط ۹: تعداد دفعات نمایش [cpt1] که در آن فیلد [Zone 1] نمایش داده می‌شود؛
  • خط ۱۰: تعداد دفعات [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) {
    ...
}
  • خط ۳، نوع پاسخ [JsonResult13] به شرح زیر است:
  

package istia.st.springmvc.models;

public class JsonResult13 {

    // داده‌ها
    private String page2;
    private String zone1;
    private String zone3;
    private String erreur;
    private String value1;
    private Integer value2;

    // session
    private SessionModel2 session;

    // گیرنده‌ها و تنظیم‌کننده‌ها
    ...
}
  • خط ۱۴: جلسه. سرور این را برای ذخیره‌سازی به کلاینت بازمی‌فرستد؛
  • خط ۶: محتوای HTML صفحه شماره ۲؛
  • خط ۷: محتوای HTML از ناحیه [Zone 1];
  • خط ۸: محتوای HTML از ناحیه [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) {
        // زمینه Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // پاسخ
        JsonResult13 result = new JsonResult13();
        result.setSession(session2);
        // یک پاسخ تصادفی بازگردانده می‌شود
        int cas = new Random().nextInt(3);
        switch (cas) {
        case 0:
            // منطقه ۱ فعال
            setZone1B(thymeleafContext, result);
            return result;
        case 1:
            // منطقه ۳ فعال
            setZone3B(thymeleafContext, result);
            return result;
        case 2:
            // زون‌های ۱ و ۳ فعال
            setZone1B(thymeleafContext, result);
            setZone3B(thymeleafContext, result);
            return result;
        }
        return null;
    }
  • خط ۹: جلسه در نتیجه اقدام گنجانده شده است؛

متد [setZone1B] که فیلد [Zone 1] را فعال می‌کند، به شرح زیر است:


    private void setZone1B(WebContext thymeleafContext, JsonResult13 result) {
        // در حال بازیابی جلسه
        SessionModel2 session = result.getSession();
        // منطقه ۱ فعال
        // HTML جریان
        int cpt1 = session.getCpt1() + 1;
        thymeleafContext.setVariable("cpt1", cpt1);
        thymeleafContext.setLocale(new Locale("fr", "FR"));
        String zone1 = engine.process("vue-09-zone1", thymeleafContext);
        result.setZone1(zone1);
        // جلسه
        session.setCpt1(cpt1);
}
  • خط ۳: جلسه بازیابی می‌شود. این جلسه در خط ۱۲ با شمارنده جدید [cpt1] اصلاح خواهد شد. توجه داشته باشید که این جلسه به کلاینت بازگردانده خواهد شد؛
  • خط ۱۰: فیلد جدید [Zone 1];

متد [setZone3B]، که فیلد [Zone 3] را فعال می‌کند، مشابه است:


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

7.6.5. پردازش پاسخ از اقدام [/ajax-13]

در سمت کلاینت، پاسخ jSON از اقدام [/ajax-13] توسط تابع زیر [onSuccess] پردازش می‌شود:


function postForm() {
    console.log("postForm");
    // ارسال جلسه
    var post = JSON3.stringify(session);
    //یک فراخوانی Ajax به‌صورت دستی انجام می‌شود
    $.ajax({
    ...
        success : function(data) {
            //جلسه ذخیره می‌شود
            session = data.session;
            // هر دو فیلد را به‌روزرسانی کنید
            if (data.zone1) {
                $("#zone1-content").html(data.zone1);
                $("#zone1").show();
            } else {
                $("#zone1").hide();
            }
            if (data.zone3) {
                $("#zone3").show();
                $("#zone3-content").html(data.zone3);
            } else {
                $("#zone3").hide();
            }
        },
...
    })
}
  • خطوط ۱۲–۱۷: اگر سرور در فیلد [zone1] پاسخ چیزی وارد کرده باشد، آنگاه فیلد [Zone 1] باید مجدداً تولید و نمایش داده شود؛ در غیر این صورت، باید پنهان شود؛
  • خطوط ۱۸–۲۳: همین امر در مورد ناحیه [Zone 3] نیز صدق می‌کند؛

7.6.6. نمایش صفحه [Page 2]

کد HTML برای لینک [Valider] به شرح زیر است:


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

تابع JS [valider] به شرح زیر است:


//اعتبارسنجی مقادیر واردشده
function valider() {
    //صفحه ۱ در حافظه پنهان ذخیره می‌شود
    page1 = content.html();
    //ذخیرهٔ مقادیر واردشده
    value1 = $("#text1").val().trim();
    value2 = $("#text2").val().trim();
    // ارزش ارسال شد
    var post = JSON3.stringify({
        "value1" : value1,
        "value2" : value2,
        "pageRequired" : page2 ? false : true
    });
    // به صورت دستی یک فراخوانی Ajax انجام دهید
    $.ajax({
        url : '/ajax-14',
        headers : {
            'Accept: 'application/json',
            'Content-Type: 'application/json'
        },
        type : 'POST',
        data : post,
        dataType : 'json',
        beforeSend : onBegin,
        success : function(data) {
        ...
        },
        error : onError,
        complete : onComplete
    })
}
  • ما POST را اجرا خواهیم کرد که معمولاً باید ما را به صفحه ۲ ببرد؛
  • خط ۴: ما صفحه شماره ۱ را ذخیره می‌کنیم تا بعداً بتوانیم به آن بازگردیم؛
  • خطوط ۶–۷: عملیات قبلی مقادیر وارد شده را ذخیره نمی‌کند، بلکه فقط کد صفحه HTML را ذخیره می‌کند. بنابراین اکنون ما دو مقدار وارد شده را در فرم ذخیره می‌کنیم؛
  • خطوط ۹–۱۳: دو مقدار وارد شده در یک رشته به نام jSON قرار می‌گیرند. این همان رشته‌ای است که ارسال خواهد شد؛
  • خط ۱۲: یک پارامتر برای اطلاع دادن به سرور در مورد اینکه آیا به صفحه شماره ۲ نیاز داریم. ما به شرح زیر عمل خواهیم کرد. ما یک بار صفحه شماره ۲ را درخواست می‌کنیم، سپس آن را در متغیر JS [page2] ذخیره می‌کنیم. پس از آن، دیگر آن را درخواست نخواهیم کرد. ما از صفحهٔ کش‌شده استفاده خواهیم کرد. در خط ۲، [pageRequired] در صورتی که متغیر [page2] خالی باشد، روی [true] و در غیر این صورت روی [false] تنظیم می‌شود؛
  • توجه کنید که session ارسال نشده است. این به این دلیل است که آن شمارنده‌هایی را ذخیره می‌کند که عمل [/ajax-14] در خط ۲۰ آن‌ها را تغییر نمی‌دهد؛

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) {
        ...
    }
  • خط ۳: پاسخ همیشه از نوع [JsonResult13] است؛
  • خط ۳: مقدار ارسال‌شده در نوع زیر از [PostAjax14] محصور شده است:

package istia.st.springmvc.models;

public class PostAjax14 extends PostAjax11A {

    // صفحه ۲
    private boolean pageRequired;

    // گیرنده و تنظیم‌کننده
    ...
}
  • خط ۳: کلاس [PostAjax14]، کلاس [PostAjax11A] از نسخه قبلی را گسترش می‌دهد. بنابراین ساختار آن مشابه [value1, value2, pageRequired] است؛

عمل [/ajax-14] به شرح زیر ادامه می‌یابد:


    @RequestMapping(value = "/ajax-14", method = RequestMethod.POST)
    @ResponseBody
public JsonResult13 ajax14(@RequestBody @Valid PostAjax14 post, BindingResult bindingResult, Locale locale,    HttpServletRequest request, HttpServletResponse response) {
        // زمینه Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // پاسخ
        JsonResult13 result = new JsonResult13();
        //آیا POST معتبر است؟
        if (bindingResult.hasErrors()) {
            // یک خطا بازگردانده می‌شود
            result.setErreur(getErreursForModel(bindingResult));
            return result;
        }
        // صفحه ۲ ارسال شد
        result.setValue1(post.getValue1());
        result.setValue2(post.getValue2());
        // صفحه درخواست شده؟
        if (post.isPageRequired()) {
            result.setPage2(engine.process("vue-12-page2", thymeleafContext));
        }
        return result;
}
  • خطوط ۹–۱۳: اگر مقادیر ارسال‌شده برای [value1, value2] نامعتبر باشند، یک پیام خطا بازگردانده می‌شود؛
  • خطوط ۱۵–۱۶: معمولاً، سرور محاسبه‌ای را با استفاده از مقادیر ارسال‌شده انجام می‌دهد. در اینجا، صرفاً آن‌ها را بازمی‌گرداند تا نشان دهد که به درستی دریافت شده‌اند؛
  • خطوط ۱۸–۲۰: صفحه شماره ۲ تنها در صورتی بازگردانده می‌شود که توسط کلاینت درخواست شده باشد. خط ۱۹: نما [vue-12-page2] جدید است:
 

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h2>Page 2</h2>
        <p>
            <h4>Valeurs saisies :</h4>
            <p>
                Chaîne de caractères :
                <span id="value1"></span>
            </p>
            <p>
                Nombre entier :
                <span id="value2"></span>
            </p>
            <a href="javascript:retourPage1()">Retour à la page 1</a>
        </p>
    </body>
</html>
  • کد XML دیگر حاوی مقادیری که توسط Thymeleaf ارزیابی شده‌اند، نیست، برخلاف گذشته؛
  • ما نواحی‌ای را که مقادیر بازگردانده‌شده توسط سرور ([value1, value2]) باید در آن‌ها قرار گیرند، شناسایی کرده‌ایم. خط ۹: [id='value1'] نشان می‌دهد که [value1] کجا باید قرار گیرد. خط ۱۳: همین امر در مورد [value2] نیز صدق می‌کند؛

7.6.8. پردازش پاسخ برای اقدام [/ajax-14]

پاسخ حاصل از اقدام [/ajax-14] توسط تابع زیر، [success]، پردازش می‌شود:


// اعتبارسنجی مقادیر واردشده
function valider() {
    ...
    // فراخوانی دستی Ajax
    $.ajax({
        ...
        success : function(data) {
            // خطا؟
            if (data.erreur) {
                // نمایش خطا
                erreur.html(data.erreur);
                erreur.show();
            } else {
                // بدون خطا
                erreur.hide();
                // صفحه ۲
                if (page2) {
                    // صفحهٔ کش‌شده استفاده می‌شود
                    content.html(page2);
                } else {
                    //صفحه ۲ در حال کش شدن است
                    page2 = data.page2;
                    // نمایش آن
                    content.html(data.page2);
                }
                // به‌روزرسانی آن با اطلاعات از سرور
                $("#value1").text(data.value1);
                $("#value2").text(data.value2);
            }
        },
...
    })
}
  • خطوط ۹–۱۳: اگر سرور خطایی برگردانده باشد، این خطا نمایش داده می‌شود؛
  • خطوط ۱۴–۲۹: حالتی که خطایی رخ نداده است. در این صورت صفحهٔ ۲ باید نمایش داده شود؛
  • خط ۱۷: بررسی می‌کنیم که آیا صفحه شماره ۲ قبلاً در متغیر [page2] ذخیره شده است یا خیر؛
  • خط ۱۹: در این صورت، متغیر [page2] برای نمایش صفحه شماره ۲ استفاده می‌شود؛
  • خط ۲۴: در غیر این صورت، از فیلد [data.page2] ارائه‌شده توسط سرور استفاده می‌کنیم؛
  • خط ۲۲: دقت می‌شود که صفحه شماره ۲ ذخیره شود تا نیازی به درخواست مجدد آن در آینده نباشد؛
  • خطوط ۲۷–۲۸: در صفحه ۲، دو مورد اطلاعات [value1, value2] که توسط سرور ارسال شده‌اند، نمایش داده می‌شوند؛

7.6.9. بازگشت به صفحهٔ ۱

لینک [Retour vers la page 1] در صفحهٔ ۲ به شرح زیر است:


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

روش JS [retourPage1] به شرح زیر است:


//بازگشت به صفحهٔ ۱
function retourPage1() {
    //صفحهٔ ۱ دوباره بارگذاری می‌شود
    content.html(page1);
    // بازتولید ورودی‌ها
    $("#text1").val(value1);
    $("#text2").val(value2);
}
  • این یک اقدام JS است که هیچ تعامل با سرور ندارد، زیرا صفحه شماره 1 به صورت محلی در متغیر [page1] ذخیره شده است؛
  • خط ۴: صفحه شماره ۱ مجدداً تولید می‌شود؛
  • خطوط ۶–۷: تنها بخش HTML از صفحه شماره ۱ ذخیره شده بود. نه ورودی‌های کاربر. بنابراین این موارد باید مجدداً تولید شوند؛

7.6.10. نتیجه‌گیری

با بهره‌گیری از قابلیت‌های مدل APU، ما در ساده‌سازی سرور وب موفق شده‌ایم، که اکنون بدون حالت (بدون جلسه) و با بار کمتری است:

  • ما تعامل با سرور را در تابع JS ([retourPage1]) حذف کرده‌ایم؛
  • سرور صفحه شماره ۲ را تنها یک بار تولید می‌کند؛

7.7. ساختاردهی کد جاوااسکریپت به صورت لایه‌لایه

7.7.1. مقدمه

کد جاوا اسکریپت برای اپلیکیشن قبلی در حال پیچیده شدن است. زمان آن رسیده که آن را به صورت لایه‌لایه ساختاردهی کنیم. اپلیکیشن همانند قبل باقی خواهد ماند. ما به جز تعریف یک صفحه شروع جدید، هیچ تغییری در سرور ایجاد نخواهیم کرد. ما کد JS را بازسازی خواهیم کرد.

معماری جدید به شرح زیر خواهد بود:

7.7.2. صفحهٔ شروع

اقدامی که برنامه را راه‌اندازی می‌کند، اقدام زیر [/ajax-16] است:


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

این نما را نمایش می‌دهد: [vue-16.xml]:


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

7.7.3. پیاده‌سازی لایه [DAO]

7.7.4. رابط

لایه [DAO] در داخل [local-dao.js] رابط زیر را به لایه [présentation] ارائه خواهد داد:


function updatePage1(deferred, sendMeBack)
برای به‌روزرسانی صفحه ۱ با استفاده از دکمه [Rafraîchir]

function getPage2(deferred, sendMeBack, value1, value2, pageRequired)
برای نمایش صفحه ۲ با استفاده از دکمه [Valider]

جاوااسکریپت مفهوم رابط (interface) را ندارد. من این اصطلاح را صرفاً برای اشاره به این موضوع به کار برده‌ام که لایه [présentation] به گونه‌ای طراحی شده است که تنها از طریق دو تابع ذکر شده در بالا با لایه [DAO] ارتباط برقرار کند.

7.7.5. پیاده‌سازی رابط

ساختار اصلی پیاده‌سازی به شرح زیر است:


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

// به‌روزرسانی صفحهٔ ۱
function updatePage1(deferred, sendMeBack) {
...
}

// صفحه ۲
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
...
}

هدف لایه [DAO] این است که جزئیات درخواست‌های HTTP که از لایه [présentation] به وب سرور ارسال می‌شوند را پنهان کند. سشن یکی از این جزئیات است. بنابراین اکنون توسط لایه [DAO] مدیریت می‌شود.

7.7.5.1. تابع [updatePage1]

تابع [updatePage1] تابع فراخوانی‌شده توسط لایه [présentation] برای تازه‌سازی صفحه ۱ است. کد آن به شرح زیر است:


// به‌روزرسانی صفحه ۱
function updatePage1(deferred, sendMeBack) {
    // پرس‌وجو HTTP
    executePost(deferred, sendMeBack, '/ajax-13', session);
}
  • خط ۱: تابع [updatePage1] دو پارامتر می‌گیرد:
    1. یک شیء از نوع [jQuery.Deferred]. این نوع شیء حالتی را ذخیره می‌کند که می‌تواند سه مقدار داشته باشد: ['pending', 'resolved', 'rejected']. وقتی به تابع [updatePage1] می‌رسد، در وضعیت [pending] است؛
    2. یک شیء JS که باید به لایه [présentation] بازگردانده شود؛

تمام درخواست‌های HTTP توسط تابع زیر، [executePost]، پردازش می‌شوند:


//درخواست HTTP
function executePost(deferred, sendMeBack, url, post) {
    //ما یک فراخوانی Ajax را به‌صورت دستی انجام می‌دهیم
    $.ajax({
        headers : {
            'Accept: 'application/json',
            'Content-Type: 'application/json'
        },
        url : url,
        type : 'POST',
        data : JSON3.stringify(post),
        dataType : 'json',
        success : function(data) {
            // سشن ذخیره می‌شود
            if (data.session) {
                session = data.session;
            }
            // ما نتیجه را برمی‌گردانیم
            deferred.resolve({
                "status" : 1,
                "data" : data,
                "sendMeBack" : sendMeBack
            });
        },
        error : function(jqXHR) {
            // خطا را بازگردانید
            deferred.resolve({
                "status" : 2,
                "data" : jqXHR.responseText,
                "sendMeBack" : sendMeBack
            });
        }
    });
}
  • خط ۱: تابع [executePost] یک فراخوانی Ajax از نوع POST را اجرا می‌کند. این تابع چهار پارامتر را انتظار دارد:
    1. یک شیء از نوع [jQuery.Deferred] در وضعیت [pending
    2. یک شیء JS که باید در لایه [présentation] بازگردانده شود؛
    3. URL از POST;
    4. مقداری که باید به‌عنوان شیء JS ارسال شود؛
  • خطوط ۵–۸: تابع post از jSON (خط ۷) از jSON (خط ۶) دریافت می‌کند؛
  • خط ۱۱: مقداری که باید ارسال شود به jSON تبدیل می‌شود؛
  • خطوط ۱۳–۲۴: تابع اجرا شده در صورت موفقیت تماس آژاکس؛
  • خطوط ۱۹–۲۳: اگر سرور یک جلسه را بازگردانده باشد، آن ذخیره می‌شود؛
  • خطوط ۱۳–۱۸: شیء [deferred] را به وضعیت [resolved] منتقل کنید، در حالی که نتیجه شامل فیلدهای زیر است:
    • [status]: در صورت موفقیت برابر با 1 و در صورت خطا برابر با 2 تنظیم شود،
    • [data]: پاسخ سرور jSON،
    • [sendMeBack]: پارامتر دوم تابع، که یک شیء است که فراخواننده مایل به بازیابی آن است؛
  • خطوط 17–31: تابع اجرا می‌شود اگر فراخوانی Ajax ناموفق باشد. ما همانند قبل عمل می‌کنیم، با دو تفاوت:
    • [status] روی مقدار 2 تنظیم می‌شود تا خطا را نشان دهد؛
    • [data] بار دیگر پاسخ سرور jSON است، اما به روشی متفاوت به دست آمده است؛

7.7.5.2. تابع [getPage2]

تابع [getPage2] به شرح زیر است:


// صفحه ۲
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
    // درخواست HTTP
    executePost(deferred, sendMeBack, '/ajax-14', {
        "value1" : value1,
        "value2" : value2,
        "pageRequired" : pageRequired,
    });
}
  • این تابع پارامترهای زیر را می‌پذیرد:
    1. [deferred]: یک شی از نوع [jQuery.Deferred] در وضعیت [pending
    2. [sendMeBack]: یک شی از نوع JS که باید در لایه [présentation] بازگردانده شود،
    3. [value1]: اولین ورودی در صفحه ۱،
    4. [value2]: دومین ورودی در صفحه ۲،
    5. [pageRequired]: یک مقدار بولی که به سرور نشان می‌دهد آیا باید فید HTML را از صفحه ۲ ارسال کند یا خیر؛
  • تابع [executePost] برای اجرای پرس‌وجوی لازم HTTP فراخوانی می‌شود؛

7.7.6. لایه [présentation]

لایه [présentation] توسط فایل [local-ui.js] پیاده‌سازی شده است. این فایل حاوی کدهای فایل [local12.js] است که برای استفاده از لایه قبلی [DAO] بازنگری شده است. فقط دو تابع تغییر کرده‌اند: [postForm] و [valider].

7.7.6.1. تابع [postForm]

تابع [postForm] به شرح زیر است:


// به‌روزرسانی صفحه ۱
function postForm() {
    //صفحه ۱ به‌روزرسانی شد
    var deferred = $.Deferred();
    loading.show();
    updatePage1(deferred, {
        'فرستنده: "postForm",
        'info: 10
    });
    // نمایش نتایج
    deferred.done(postFormDone);
}
  • خط ۴: یک شیء [jQuery.Deferred] ایجاد می‌شود. به طور پیش‌فرض، در حالت [pending] قرار دارد؛
  • خط ۵: تصویر بارگذاری نمایش داده می‌شود
  • خطوط ۶–۹: تابع [updatePage1] اجرا می‌شود. یک شیء نمونه‌ای [sendMeBack] به آن پاس داده می‌شود، صرفاً برای نشان دادن اینکه چه کاربردی می‌تواند داشته باشد؛
  • خط ۱۱: پارامتر تابع [deferred.done] خود یک تابع است. این تابع زمانی اجرا می‌شود که وضعیت شیء [deferred] به وضعیت [resolved] تغییر کند. ما همین حالا دیدیم که تابع DAO [executePost] پس از دریافت پاسخ سرور، وضعیت این شیء را به [resolved] ارسال کرد. این بدان معناست که وقتی تابع [postFormDone] اجرا می‌شود، پاسخ سرور قبلاً دریافت شده است؛

تابع [postFormDone] به شرح زیر است:


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

            // بازگرداندن نتیجه
            deferred.resolve({
                "status" : 1,
                "data" : data,
                "sendMeBack" : sendMeBack
});
  • خط ۵: پاسخ از سرور دریافت می‌شود؛
  • خطوط ۱۰–۲۴: این شامل کدی است که در نسخه قبلی، در تابع [onSuccess] درون تابع [postForm] قرار داشت؛
  • خطوط ۲۵–۲۸: این شامل کدی است که در نسخه قبلی، در تابع [onError] درون تابع [postForm] قرار داشت؛

7.7.6.2. نقش پارامتر [sendMeBack]

پارامتر [sendMeBack] برای چه کاری استفاده می‌شود؟ بیایید کد فراخوانی تابع [updatePage1] را بررسی کنیم:


// به‌روزرسانی صفحه ۱
function postForm() {
    // به‌روزرسانی صفحه ۱
    var deferred = $.Deferred();
    loading.show();
    updatePage1(deferred, {
        'فرستنده: "postForm",
        'info: 10
    });
    // نمایش نتایج
    deferred.done(postFormDone);
}

و امضای تابع [validerDone]:


function postFormDone(result) {
}

تابع [postForm] چگونه می‌تواند اطلاعات را به تابع [postFormDone] منتقل کند؟ تابع دوم تنها یک پارامتر دارد، [result]. این پارامتر توسط تابع [executePost] در لایه [DAO] ایجاد می‌شود. برای ارسال اطلاعات به تابع [postFormDone]، تابع [postForm] ابتدا باید آن را به تابع [updatePage1] ارسال کند. این نقش پارامتر [sendMeBack] است. نحوه استفاده از آن به شرح زیر است:


function postFormDone(result) {
    // پایان انتظار
    loading.hide();
    //در حال بازیابی داده‌ها
    var data = result.data
    // برای دمو
    console.log(JSON3.stringify(result.sendMeBack));
    // تحلیل وضعیت
    switch (result.status) {
...
  • خط ۷، توابع [postFormDone] و [valider] پارامتر [sendMeBack] را که در ابتدا توسط تابع [postForm] به تابع DAO [updatePage1] ارسال شده بود، بازیابی کرده‌اند؛

7.7.7. تابع [valider]

تابع [valider] به شرح زیر است:


// اعتبارسنجی مقادیر واردشده
function valider() {
    // ذخیره صفحه ۱
    page1 = content.html();
    // ذخیره مقادیر واردشده
    value1 = $("#text1").val().trim();
    value2 = $("#text2").val().trim();
    // بدون خطا
    erreur.hide();
    // درخواست صفحه ۲
    var deferred = $.Deferred();
    loading.show();
    getPage2(deferred, {
        'فرستنده: 'submit',
        ''info': 20
    }, value1, value2, page2 ? false : true);
    // نمایش نتایج
    deferred.done(validerDone);
}

و تابع [validerDone] (خط ۱۸) به شرح زیر است:


function validerDone(result) {
    //پایان انتظار
    loading.hide();
    //در حال بازیابی داده‌ها
    var data = result.data
    // برای دمو
    console.log(JSON3.stringify(result.sendMeBack));
    // در حال تجزیه و تحلیل وضعیت
    switch (result.status) {
    case 1:
        // خطا؟
        if (data.erreur) {
            //خطا نمایش داده شد
            erreur.html(data.erreur);
            erreur.show();
        } else {
            // بدون خطا
            erreur.hide();
            // صفحه ۲
            if (page2) {
                // استفاده از صفحهٔ کش‌شده
                content.html(page2);
            } else {
                //صفحه ۲ در حال کش شدن است
                page2 = data.page2;
                // نمایش آن
                content.html(data.page2);
            }
            // به‌روزرسانی آن با اطلاعات از سرور
            $("#value1").text(data.value1);
            $("#value2").text(data.value2);
        }
        break;
    case 2:
        // خطا نمایش داده شد
        erreur.html(data);
        erreur.show();
        break;
    }
}
  • خط ۵: پاسخ سرور بازیابی می‌شود؛
  • خطوط ۱۰–۳۲: این شامل کدی است که در نسخه قبلی، در تابع [onSuccess] درون تابع [valider] قرار داشت؛
  • خطوط ۳۴–۳۸: این شامل کدی است که در نسخه قبلی، در تابع [onError] درون تابع [valider] قرار داشت؛

7.7.8. آزمایش‌ها

برنامه همچنان مانند قبل کار می‌کند و در کنسول کروم می‌توانید پارامترهای [sendMeBack] توابع [postForm] و [valider] را مشاهده کنید:

 

7.8. Conclusion

بیایید به معماری کلی یک برنامه Spring MVC بازگردیم:

به لطف جاوااسکریپتی که در صفحات HTML تعبیه شده و در مرورگر اجرا می‌شود، و به لطف مدل APU، می‌توانیم بخش کدنویسی را به مرورگر واگذار کرده و معماری زیر را محقق کنیم:

  • ما یک معماری کلاینت-سرور (کلاینت: [2] / سرور: [1]) داریم که در آن کلاینت و سرور از طریق jSON با یکدیگر ارتباط برقرار می‌کنند؛
  • در [1]، لایه وب Spring MVC، ویوها، قطعات ویو و داده‌ها را در jSON ارائه می‌دهد؛
  • در [2]: کد جاوااسکریپتی که در نمای بارگذاری‌شده هنگام راه‌اندازی برنامه تعبیه شده است، می‌تواند به لایه‌ها ساختاردهی شود:
    • لایه [présentation] تعاملات کاربر را مدیریت می‌کند،
    • لایه [DAO] دسترسی به داده‌ها را از طریق وب‌سرور [1] مدیریت می‌کند؛
    • لایه [métier] ممکن است وجود نداشته باشد یا برخی از وظایف غیرمحرمانه لایه [métier] سرور را برای کاهش بار سرور به عهده بگیرد؛
  • کلاینت [2] ممکن است برخی نماها را در حافظه پنهان ذخیره کند، مجدداً برای کاهش بار روی سرور. این لایه جلسه را مدیریت می‌کند؛