إنشاء تطبيق ويب

مقدمة

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

المكتبات الضرورية

علينا تضمين بعض المكتبات الضرورية للعمل في هذا الدرس كما يلي:

اشمل "مـتم/نـظام"؛
اشمل "بـناء"؛
اشمل "مـحا"؛
مـحا.اشمل_ملف("Alusus/WebPlatform"، "مـنصة_ويب.أسس")؛
مـحا.اشمل_ملف("Alusus/Http"، "بـننف.أسس")؛
مـحا.اشمل_ملف("Alusus/Json"، "جـيسون.أسس")؛
اشمل "مـتم/سندات"؛
اشمل "مـتم/مـصفوفة"؛
اشمل "مـتم/نـص"؛
اشمل "مـتم/طـرفية"؛
اشمل "مغلفة"؛

استخدم مـتم؛
استخدم مـنصة_ويب؛
import "Apm";
Apm.importFile("Alusus/WebPlatform");
use Srl;
use WebPlatform;

إنشاء منفذ مرئي

كل ما علينا فعله لتعريف منفذ مرئي هو كتابة دالة عادية واستعمال المبدل @منفذ_مرئي (@uiEndpoint) معه. ويمكننا إعطاء عنوان لهذا المنفذ عن طريق المبدل @عنوان (@title). و بالتالي يمكننا كتابة ما يلي لتعريف منفذ مرئي:

@منفذ_مرئي["/"]
@عنوان["مثال منصة ويب - نص بسيط"]
دالة رئيسي {

}
@uiEndpoint["/"]
@title["WebPlatform Example - Simple Text"]
func main {

}

لاحظ هنا كيف كتبنا المسار الذي نريده لهذا المنفذ المرئي باستعمال المبدل @منفذ_مرئي (@uiEndpoint). الآن يجب علينا تعريف ما نريد عرضه عندما يدخل المستخدم إلى هذه الصفحة. يمكننا القيام بذلك عن طريق تحديد المشهد الخاص بالنافذة على أنه نص. أي كما يلي:

نـافذة.النموذج.حدد_المشهد(
    كـتابة(نـص("السلام عليكم"))
)؛
Window.instance.setView(
    Text(String("Hello world!"))
);

الآن كل ما ينقصنا لتشغيل هذا التطبيق هو تشغيل الخادم كما يلي:

طـرفية.اطبع("تشغيل الخادم على المنفذ 8020...\nURL: http://localhost:8020/\n")؛
شغل_الخادم ({ "listening_ports"، "8020"})؛
Console.print("Starting server on port 8020...\nURL: http://localhost:8020/\n");
runServer({ "listening_ports", "8020"});

لاحظ أننا نطبع عبارة توضيحية للمستخدم لندله على عنوان الخادم، ومن ثم نقوم بتشغيل الخادم باستعمال الدالة runServer مع تمرير المنفذ الذي نريد لهذا الخادم أن يعمل عليه. بالتالي يصبح لدينا التطبيق الحالي كما يلي:

اشمل "مـتم/نـظام"؛
اشمل "بـناء"؛
اشمل "مـحا"؛
مـحا.اشمل_ملف("Alusus/WebPlatform"، "مـنصة_ويب.أسس")؛
مـحا.اشمل_ملف("Alusus/Http"، "بـننف.أسس")؛
مـحا.اشمل_ملف("Alusus/Json"، "جـيسون.أسس")؛
اشمل "مـتم/سندات"؛
اشمل "مـتم/مـصفوفة"؛
اشمل "مـتم/نـص"؛
اشمل "مـتم/طـرفية"؛
اشمل "مغلفة"؛

استخدم مـتم؛
استخدم مـنصة_ويب؛


@منفذ_مرئي["/"]
@عنوان["مثال منصة ويب - نص بسيط"]
دالة رئيسي {
	نـافذة.النموذج.حدد_المشهد(
        كـتابة(نـص("السلام عليكم"))
    )؛
}

طـرفية.اطبع("تشغيل الخادم على المنفذ 8020...\nURL: http://localhost:8020/\n")؛
شغل_الخادم ({ "listening_ports"، "8020"})؛
import "Apm";
Apm.importFile("Alusus/WebPlatform");
use Srl;
use WebPlatform;

@uiEndpoint["/"]
@title["WebPlatform Example - Simple Text"]
func main {
    Window.instance.setView(
        Text(String("Hello world!"))
    );
}


Console.print("Starting server on port 8020...\nURL: http://localhost:8020/\n");
runServer({ "listening_ports", "8020"});

استقبال الدخل من المستخدم ضمن حقل إدخال وعرضه

بما أننا نريد حقل إدخال فيمكننا استعمال ودجة مـدخل_نص (TextInput). كما يمكننا استعمال ودجة الـزر (Button) لعرض زر يمكن للمستخدم الضغط عليه عند الانتهاء من الكتابة. وسنعرض ما أدخله المستخدم كما عرضنا سابقاً النص الثابت. بالتالي سنحذف ما كتبناه سابقاً في الدالة ونبدأ بكتابة تعريف لسندين يشيران إلى المكون الخاص بالكتابة والمكون الخاص بالعرض، وذلك حتى يمكننا التحكم فيهما لاحقاً.

عرف مدخل_النص: سـندنا[مـدخل_نص]؛
عرف عرض_النص: سـندنا[كـتابة]؛
def textInput: SrdRef[TextInput];
def textShow: SrdRef[Text];

الآن يمكن إضافة المكونات التي نريدها، سنبدأ بإضافة حقل الإدخال كما فعلنا سابقاً لنضيف نص:

نـافذة.النموذج.حدد_المشهد(
    صـندوق({}).{
        أضف_فروع({
            مـدخل_نص().{
                مدخل_النص = هذا؛
            }،
        })؛
    }

)؛
Window.instance.setView(
    Box({}).{
        addChildren({
            TextInput().{
                textInput = this;
            },
        });
    }
);

هنا قمنا بإسناد قيمة للسند الذي عرفناه سابقاً ليشير إلى مكون الإدخال عن طريق إسناد هذا (this) له، وهي تشير إلى المكون الذي يحتويها والذي هو هنا حقل الإدخال. يمكن تعريف المكون النصي الذي سيعرض مدخلات المستخدم كما يلي:

صـندوق({}).{
    أضف_فروع({
        كـتابة(نـص("هنا سوف ترى النص الذي أدخلته")).{
            عرض_النص = هذا؛
        }
    })؛
}،
Box({}).{
    addChildren({
        Text(String("Here you will see the text you entered")).{
            textShow = this;
        }
    });
},

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

الـزر(نـص("أرسل")).{
    عند_الضغط.اربط(مغلفة (ودجة: سند[ودجـة]، الحمولة: سند[صحيح]) {
        عرف كتابة: نـص = مدخل_النص.هات_النص()؛
        مدخل_النص.حدد_النص(نـص())؛
        عرض_النص.حدد_النص(كتابة)؛
    })؛
}
Button(String("Send")).{
    onClick.connect(closure (widget: ref[Widget], payload: ref[Int]) {
        def text: String = textInput.getText();
        textInput.setText(String());
        textShow.setText(text);
    });
}

كما نلاحظ يمكن الاستماع إلى حدث الضغط على الزر عن طريق ربط مغلفة بالحدث عند_الضغط (onClick) ليتم استدعاؤها عند حدوثه. و هنا نقوم في البداية بجلب النص من مكون الإدخال عن طريق الوظيفة هات_النص (getText)، ثم نفرغ مكون الإدخال حتى نعيده إلى حالته الأولية، وفي النهاية نحدد النص ضمن مكون العرض بواسطة الوظيفة حدد_النص (setText). تبقى شيء واحد فقط، وهو الحاجة لتشغيل حلقة معالجة الأحداث (event loop) كي تصل الأحداث إلى أهدافها. بدون الدخول في حلقة معالجة الأحداث لن تُوجه الأحداث إلى أهدافها وستبقى في طابور الانتظار تنتظر المعالجة. لمعالجة الأحداث بشكل مستمر يمكننا استدعاء الدالة التالية بعد الانتهاء من إنشاء الواجهة:

نفذ_حلقة_معالجة_الأحداث()؛
runEventLoop();

و بالتالي يصبح البرنامج كما يلي:

اشمل "مـتم/نـظام"؛
اشمل "بـناء"؛
اشمل "مـحا"؛
مـحا.اشمل_ملف("Alusus/WebPlatform"، "مـنصة_ويب.أسس")؛
مـحا.اشمل_ملف("Alusus/Http"، "بـننف.أسس")؛
مـحا.اشمل_ملف("Alusus/Json"، "جـيسون.أسس")؛
اشمل "مـتم/سندات"؛
اشمل "مـتم/مـصفوفة"؛
اشمل "مـتم/نـص"؛
اشمل "مـتم/طـرفية"؛
اشمل "مغلفة"؛

استخدم مـتم؛
استخدم مـنصة_ويب؛

@منفذ_مرئي["/"]
@عنوان["مثال منصة ويب - مربع إدخال نصي و زر"]
دالة رئيسي {
	عرف مدخل_النص: سـندنا[مـدخل_نص]؛
	عرف عرض_النص: سـندنا[كـتابة]؛

	نـافذة.النموذج.حدد_المشهد(
		صـندوق({}).{
			أضف_فروع({
				صـندوق({}).{
					أضف_فروع({
						كـتابة(نـص("هنا سوف ترى النص الذي أدخلته")).{
							عرض_النص = هذا؛
						}
					})؛
				}،
				مـدخل_نص().{
					مدخل_النص = هذا؛
				}،
				الـزر(نـص("أرسل")).{
					عند_الضغط.اربط(مغلفة (ودجة: سند[ودجـة]، الحمولة: سند[صحيح]) {
                        عرف كتابة: نـص = مدخل_النص.هات_النص()؛
                        مدخل_النص.حدد_النص(نـص())؛
                        عرض_النص.حدد_النص(كتابة)؛
                    })؛
				}
			})؛
		}

    )؛

    نفذ_حلقة_معالجة_الأحداث()؛
}

طـرفية.اطبع("تشغيل الخادم على المنفذ 8020...\nURL: http://localhost:8020/\n")؛
شغل_الخادم ({ "listening_ports"، "8020"})؛
import "Apm";
Apm.importFile("Alusus/WebPlatform");
use Srl;
use WebPlatform;

@uiEndpoint["/"]
@title["WebPlatform Example - Text Input and Button"]
func main {
    def textInput: SrdRef[TextInput];
    def textShow: SrdRef[Text];

    Window.instance.setView(
        Box({}).{
            addChildren({
                Box({}).{
                    addChildren({
                        Text(String("Here you will see the text you entered")).{
                            textShow = this;
                        }
                    });
                },
                TextInput().{
                    textInput = this;
                },
                Button(String("Send")).{
                    onClick.connect(closure (widget: ref[Widget], payload: ref[Int]) {
                        def text: String = textInput.getText();
                        textInput.setText(String());
                        textShow.setText(text);
                    });
                }
            });
        }
    );

    runEventLoop();
}

Console.print("Starting server on port 8020...\nURL: http://localhost:8020/\n");
runServer({ "listening_ports", "8020"});

إضافة منفذين بيانيين لحفظ واسترجاع مدخلات المستخدم

لحد الآن كل البيانات التي يدخلها المستخدم تبقى على متصفحه، وبالتالي لا يمكن استخدامه للدردشة بين عدة أشخاص كما أن البيانات تختفي بعد إعادة تحميل الصفحة، لذلك سنحتاج لتخزين البيانات على الخادم كي تكون متوفرة لكل المستخدمين ما يسمح لهم بالتواصل فيما بينهم. لفعل ذلك نحتاج لتعريف منفذي بيانات على الخادم، واحد لإرسال البيانات للخادم، والآخر لجلب البيانات من الخادم. لتعريف منفذ بياني نقوم بتعريف دالة عادية لها معطى من الصنف مؤشر[بـننف.اتـصال (ptr[Http.Connection]). يمثل هذا المعطى مؤشرا إلى كائن يتحكم بالاتصال القادم من المستخدم، ويسمح لنا بإعادة رد فيه حالة HTTP وربما بيانات. نحتاج ايضًا لتطبيق المبدل @منفذ_بياني (@beEndpoint) على الدالة مع إعطائه الطريقة التي يقبلها هذا المنفذ (مثلاً "POST") والمسار الخاص به. لتعريف منفذ لإرسال الرسالة التي أدخلها المستخدم إلى الخادم، سنحتاج إلى متغير لتخزين الرسالة بالإضافة إلى دالة المنفذ البياني.

عرف رسالة : نـص؛

// منفذ بياني يقبل الطريقة POST وله المسار المعطى
// يستعمل هذا المنفذ لإرسال الرسائل
@منفذ_بياني["POST"، "/message"]
دالة أضف_رسالة (اتصال:مؤشر[بـننف.اتـصال]){

}
def message: String;

@beEndpoint["POST", "/message"]
func postMessages (conn: ptr[Http.Connection]) {

}

كما نلاحظ حددنا الطريقة على أنها POST وذلك لأنه سيتم إرسال بيانات إلى هذا المنفذ. الآن علينا ضمن هذه الدالة أن نقرأ البيانات المرسلة من قبل المستخدم ونخزنها في المتغير رسالة (message). يمكن قراءة البيانات المرسلة عبر اتصال (conn) باستخدام الدالة بـننف.اقرأ (Http.read)، حيث نحتاج لتعريف صوان لتخزين البيانات واستدعاء هذه الطريقة. كما يلي:

عرف بيانات: مصفوفة[مـحرف،1024]؛
// نقوم بجلب المعلومات التي تم إرسالها و تخزينها في الصوان
عرف حجم_البيانات: صحيح = بـننف.اقرأ(اتصال، بيانات~مؤشر، بيانات~حجم)؛

// نخزن الرسالة الجديدة
رسالة = نـص(بيانات~مؤشر، حجم_البيانات)؛
def postData: array[Char, 1024];

def postDataSize: Int = Http.read(conn, postData~ptr, postData~size);
message = String(postData~ptr, postDataSize);

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

بـننف.اطبع(اتصال، "HTTP/1.1 200 Ok\r\n\r\n")؛
Http.print(conn, "HTTP/1.1 200 Ok\r\n\r\n");

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

// منفذ بياني يقبل الطريقة GET وله المسار المعطى
// يستعمل هذا المنفذ لجلب الرسائل
@منفذ_بياني["GET"، "/message"]
دالة هات_الرسائل (اتصال: مؤشر[بـننف.اتـصال]) {

}
@beEndpoint["GET", "/message"]
func getMessages (conn: ptr[Http.Connection]) {

}

كل ما علينا القيام به هنا هو إعادة الرسالة ضمن الرد وذلك بواسطة الدالة بـننف.اطبع (Http.print). بالطبع علينا أيضاً وضع الترويسات المناسبة، ويمكن كتابة ذلك كما يلي:

// منفذ بياني يقبل الطريقة GET وله المسار المعطى
// يستعمل هذا المنفذ لجلب الرسائل
@منفذ_بياني["GET"، "/message"]
دالة هات_الرسائل (اتصال: مؤشر[بـننف.اتـصال]) {
    عرف الرد: نـص = رسالة؛

    // نضع الترويسات اللازم
    بـننف.اطبع(اتصال، "HTTP/1.1 200 Ok\r\n")؛
    بـننف.اطبع(اتصال، "Content-Type: text/plain\r\n")؛
    بـننف.اطبع(اتصال، "Cache-Control: no-cache\r\n")؛
    بـننف.اطبع(اتصال، "Content-Length: %d\r\n\r\n"، الرد.هات_الطول())؛
    // نضع محتوى الصوان الذي يحمل الرسالة
    بـننف.اطبع(اتصال، الرد.صوان)؛
}
@beEndpoint["GET", "/message"]
func getMessages (conn: ptr[Http.Connection]) {
    // نضع الترويسات اللازمة
    Http.print(conn, "HTTP/1.1 200 Ok\r\n");
    Http.print(conn, "Content-Type: text/plain\r\n");
    Http.print(conn, "Cache-Control: no-cache\r\n");
    Http.print(conn, "Content-Length: %d\r\n\r\n", message.getLength());
    // نضع محتوى الصوان الذي يحمل الرسائل
    Http.print(conn, message.buf);
}

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

عرف _جلب_: "GET"؛
عرف _إرسال_: "POST"؛
عرف _المسار_: "/message"؛
عرف _ترويسة_صنف_بيانات_نصي_: "Content-Type: application/text"؛

في البداية لنقم بتعريف مغلف لحدث جلب البيانات.

عرف عند_استلام_بيانات: مغلفة (جـيسون)؛
def onFetch: closure (json: Json);

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

كـتابة(نـص("هنا سوف ترى النص الذي أدخلته")).{
    عرض_النص = هذا؛

    عند_استلام_بيانات = مغلفة (جيسون: جـيسون) {
        عرف الحالة: صحيح = جيسون.هات_كائن("eventData").هات_صحيح("status")؛
        إذا الحالة >= 200 و الحالة < 300 {
            عرف البيانات: نـص = جيسون.هات_كائن("eventData").هات_نص("body")؛
            إذا هذا.هات_النص() != البيانات {
                هذا.حدد_النص(البيانات)؛
            }
        } وإلا {
            هذا.حدد_النص(نـص("الاتصال مقطوع. رمز حالة ب.ن.ن.ف: ") + الحالة)؛
        }
    }؛
}
Text(String("Here you will see the text you entered")).{
    textShow = this;
    onFetch = closure (json: Json) {
        // في البداية نتحقق من أنه قد تم جلب البيانات بدون حدوث أخطاء
        def status: Int = json.getObject("eventData").getInt("status");
        if status >= 200 and status < 300 {
            // نستخرج البيانات
            def data: String = json.getObject("eventData").getString("body");
            // نقوم بتحديث النص
            if this.getText() != data {
                this.setText(data);
            }
        } else { // حالة حدوث خطأ
            this.setText(String("Connection error. HTTP status: ") + status);
        }
    };
}

مكون الإدخال سيبقى كما هو.

مـدخل_نص().{
    مدخل_النص = هذا؛
}،
TextInput().{
    textInput = this;
}

أما بالنسبة لمكون الزر فيجب أن يثحدث ليقوم بإرسال النص إلى الخادم عن طريق الدالة أرسل_نداء (sendRequest) كما سيقوم بجلب البيانات من الخادم وتمريرها إلى المغلف onFetch.

الـزر(نـص("أرسل")).{
    عند_الضغط.اربط(مغلفة (ودجة: سند[ودجـة]، الحمولة: سند[صحيح]) {
        عرف كتابة: نـص = مدخل_النص.هات_النص()؛
        مدخل_النص.حدد_النص(نـص())؛
        // نقوم بإرسال النص إلى الخادم
        // نحدد الطريقة، المسار، نوع البيانات، و من ثم نضع البيانات
        أرسل_نداء(
            _إرسال_، _المسار_، _ترويسة_صنف_بيانات_نصي_، كتابة، 10000، مغلفة(جـيسون) {}
        )؛
        // نقوم بجلب البيانات من الخادم و استدعاء المغلف عند_استلام_بيانات
        أرسل_نداء(_جلب_، _المسار_، 0، 0، 500، عند_استلام_بيانات)؛
    })؛
}
Button(String("Send")).{
    onClick.connect(closure (widget: ref[Widget], payload: ref[Int]) {
        def text: String = textInput.getText();
        textInput.setText(String());
        // نقوم بإرسال النص إلى الخادم
        // نحدد الطريقة، المسار، نوع البيانات، و من ثم نضع البيانات
        sendRequest(
            "POST", "/message", "Content-Type: application/text", text, 10000,
            closure (Json) {}
        );
        // نقوم بجلب البيانات من الخادم و استدعاء المغلف
        // onFetch
        sendRequest("GET", "/message", null, null, 500, onFetch);

    });
}

و بالتالي يصبح الكود كما يلي:

اشمل "مـتم/نـظام"؛
اشمل "بـناء"؛
اشمل "مـحا"؛
مـحا.اشمل_ملف("Alusus/WebPlatform"، "مـنصة_ويب.أسس")؛
مـحا.اشمل_ملف("Alusus/Http"، "بـننف.أسس")؛
مـحا.اشمل_ملف("Alusus/Json"، "جـيسون.أسس")؛
اشمل "مـتم/سندات"؛
اشمل "مـتم/مـصفوفة"؛
اشمل "مـتم/نـص"؛
اشمل "مـتم/طـرفية"؛
اشمل "مغلفة"؛

استخدم مـتم؛
استخدم مـنصة_ويب؛


//==============================================================================
// الخادم

عرف رسالة : نـص؛

// منفذ بياني يقبل الطريقة POST وله المسار المعطى
// يستعمل هذا المنفذ لإرسال الرسائل
@منفذ_بياني["POST"، "/message"]
دالة أضف_رسالة (اتصال:مؤشر[بـننف.اتـصال]){
    عرف بيانات: مصفوفة[مـحرف،1024]؛
	// نقوم بجلب المعلومات التي تم إرسالها و تخزينها في الصوان
    عرف حجم_البيانات: صحيح = بـننف.اقرأ(اتصال، بيانات~مؤشر، بيانات~حجم)؛

    // نخزن الرسالة الجديدة
    رسالة = نـص(بيانات~مؤشر، حجم_البيانات)؛
    بـننف.اطبع(اتصال، "HTTP/1.1 200 Ok\r\n\r\n")؛
}

// منفذ بياني يقبل الطريقة GET وله المسار المعطى
// يستعمل هذا المنفذ لجلب الرسائل
@منفذ_بياني["GET"، "/message"]
دالة هات_الرسائل (اتصال: مؤشر[بـننف.اتـصال]) {
    عرف الرد: نـص = رسالة؛

    // نضع الترويسات اللازم
    بـننف.اطبع(اتصال، "HTTP/1.1 200 Ok\r\n")؛
    بـننف.اطبع(اتصال، "Content-Type: text/plain\r\n")؛
    بـننف.اطبع(اتصال، "Cache-Control: no-cache\r\n")؛
    بـننف.اطبع(اتصال، "Content-Length: %d\r\n\r\n"، الرد.هات_الطول())؛
    // نضع محتوى الصوان الذي يحمل الرسالة
    بـننف.اطبع(اتصال، الرد.صوان)؛
}


//==============================================================================
// مـركبات واجهة المستخدم

عرف _جلب_: "GET"؛
عرف _إرسال_: "POST"؛
عرف _المسار_: "/message"؛
عرف _ترويسة_صنف_بيانات_نصي_: "Content-Type: application/text"؛

@منفذ_مرئي["/"]
@عنوان["مثال منصة ويب - مربع إدخال نصي و زر"]
دالة رئيسي {
	عرف مدخل_النص: سـندنا[مـدخل_نص]؛
	عرف عرض_النص: سـندنا[كـتابة]؛

	عرف عند_استلام_بيانات: مغلفة (جـيسون)؛

	نـافذة.النموذج.حدد_المشهد(
		صـندوق({}).{
			أضف_فروع({
				صـندوق({}).{
					أضف_فروع({
						كـتابة(نـص("هنا سوف ترى النص الذي أدخلته")).{
							عرض_النص = هذا؛

							عند_استلام_بيانات = مغلفة (جيسون: جـيسون) {
		                        عرف الحالة: صحيح = جيسون.هات_كائن("eventData").هات_صحيح("status")؛
		                        إذا الحالة >= 200 و الحالة < 300 {
		                            عرف البيانات: نـص = جيسون.هات_كائن("eventData").هات_نص("body")؛
		                            إذا هذا.هات_النص() != البيانات {
		                                هذا.حدد_النص(البيانات)؛
		                            }
		                        } وإلا {
		                            هذا.حدد_النص(نـص("الاتصال مقطوع. رمز حالة ب.ن.ن.ف: ") + الحالة)؛
		                        }
                  		  	}؛
						}
					})؛
				}،
				مـدخل_نص().{
					مدخل_النص = هذا؛
				}،
				الـزر(نـص("أرسل")).{
					عند_الضغط.اربط(مغلفة (ودجة: سند[ودجـة]، الحمولة: سند[صحيح]) {
                        عرف كتابة: نـص = مدخل_النص.هات_النص()؛
                        مدخل_النص.حدد_النص(نـص())؛
                        // نقوم بإرسال النص إلى الخادم
                        // نحدد الطريقة، المسار، نوع البيانات، و من ثم نضع البيانات
                        أرسل_نداء(
               		 		_إرسال_، _المسار_، _ترويسة_صنف_بيانات_نصي_، كتابة، 10000، مغلفة(جـيسون) {}
               		 	)؛
               		 	// نقوم بجلب البيانات من الخادم و استدعاء المغلف عند_استلام_بيانات
                    	أرسل_نداء(_جلب_، _المسار_، 0، 0، 500، عند_استلام_بيانات)؛
                    })؛
				}
			})؛
		}

    )؛

    أرسل_نداء(_جلب_، _المسار_، 0، 0، 500، عند_استلام_بيانات)؛

    نفذ_حلقة_معالجة_الأحداث()؛
}

طـرفية.اطبع("تشغيل الخادم على المنفذ 8020...\nURL: http://localhost:8020/\n")؛
شغل_الخادم ({ "listening_ports"، "8020"})؛
import "Apm";
Apm.importFile("Alusus/WebPlatform");
use Srl;
use WebPlatform;

//==============================================================================
// Server

def message: String;

// وله المسار المعطى POST منفذ بياني يقبل الطريقة
// يستعمل هذا المنفذ لإرسال الرسائل
@beEndpoint["POST", "/message"]
func postMessages (conn: ptr[Http.Connection]) {
    def postData: array[Char, 1024];
    // نقوم بجلب المعلومات التي تم إرسالها وتخزينها في الصوان
    def postDataSize: Int = Http.read(conn, postData~ptr, postData~size);
    // نضيف الرسالة الجديدة
    message = String(postData~ptr, postDataSize);
    Http.print(conn, "HTTP/1.1 200 Ok\r\n\r\n");
}

// وله المسار المعطى GET منفذ بياني يقبل الطريقة
// يستعمل هذا المنفذ لجلب الرسائل
@beEndpoint["GET", "/message"]
func getMessages (conn: ptr[Http.Connection]) {
    // ادمج الرسائل سويةً مع سطر فارغ بين كل رسالة والأخرى
    def response: String = message;
    // نضع الترويسات اللازمة
    Http.print(conn, "HTTP/1.1 200 Ok\r\n");
    Http.print(conn, "Content-Type: text/plain\r\n");
    Http.print(conn, "Cache-Control: no-cache\r\n");
    Http.print(conn, "Content-Length: %d\r\n\r\n", response.getLength());
    // نضع محتوى الصوان الذي يحمل الرسائل
    Http.print(conn, response.buf);
}


//==============================================================================
// واجهة المستخدم

@uiEndpoint["/"]
@title["WebPlatform Example - Text Input and Button"]
func main {
    def textInput: SrdRef[TextInput];
    def textShow: SrdRef[Text];

    def onFetch: closure (json: Json);
    def onNewEntry: closure (String);

    Window.instance.setView(
        Box({}).{
            addChildren({
                Box({}).{
                    addChildren({
                        Text(String("Here you will see the text you entered")).{
                            textShow = this;
                            onFetch = closure (json: Json) {
                                // في البداية نتحقق من أنه قد تم جلب البيانات بدون حدوث أخطاء
                                def status: Int = json.getObject("eventData").getInt("status");
                                if status >= 200 and status < 300 {
                                    // نستخرج البيانات
                                    def data: String = json.getObject("eventData").getString("body");
                                    // نقوم بتحديث النص
                                    if this.getText() != data {
                                        this.setText(data);
                                    }
                                } else { // حالة حدوث خطأ
                                    this.setText(String("Connection error. HTTP status: ") + status);
                                }
                            };
                        }
                    });
                },
                TextInput().{
                    textInput = this;
                    onNewEntry = closure (newData: String) {
                        // نقوم بإرسال النص إلى الخادم
                        // نحدد الطريقة، المسار، نوع البيانات، ومن ثم نضع البيانات
                        sendRequest(
                            "POST", "/message", "Content-Type: application/text", newData, 10000,
                            closure (Json) {}
                        );
                        // نقوم بجلب البيانات من الخادم واستدعاء المغلف
                        // onFetch
                        sendRequest("GET", "/message", null, null, 500, onFetch);
                    };
                },
                Button(String("Send")).{
                    onClick.connect(closure (widget: ref[Widget], payload: ref[Int]) {
                        def text: String = textInput.getText();
                        textInput.setText(String());
                        onNewEntry(text);
                    });
                }
            });
        }
    );

    sendRequest("GET", "/message", null, null, 500, onFetch);

    runEventLoop();
}

Console.print("Starting server on port 8020...\nURL: http://localhost:8020/\n");
runServer({ "listening_ports", "8020"});

تخزين سلسلة من الرسائل بدلاً من رسالة واحدة

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

عرف _حد_الرسائل_: 12؛  // عدد الرسائل الأعظمي
عرف رسائل : مـصفوفة[نـص]؛  // مصفوفة لتخزين الرسائل
def MAX_MESSAGES: 12;  // عدد الرسائل الأعظمي
def messages: Array[String];  // مصفوفة لتخزين الرسائل

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

// منفذ بياني يقبل الطريقة POST وله المسار المعطى
// يستعمل هذا المنفذ لإرسال الرسائل
@منفذ_بياني["POST"، "/messages"]
دالة أضف_رسالة (اتصال:مؤشر[بـننف.اتـصال]){

    عرف بيانات: مصفوفة[مـحرف،1024]؛
	// نقوم بجلب المعلومات التي تم إرسالها و تخزينها في الصوان
    عرف حجم_البيانات: صحيح = بـننف.اقرأ(اتصال، بيانات~مؤشر، بيانات~حجم)؛
    إذا رسائل.هات_الطول() >= _حد_الرسائل_ رسائل.أزل(0)؛
    // نخزن الرسالة الجديدة
    رسائل.أضف(نـص(بيانات~مؤشر، حجم_البيانات))؛
    بـننف.اطبع(اتصال، "HTTP/1.1 200 Ok\r\n\r\n")؛
}
@beEndpoint["POST", "/messages"]
func postMessages (conn: ptr[Http.Connection]) {
    def postData: array[Char, 1024];
    // نقوم بجلب المعلومات التي تم إرسالها وتخزينها في الصوان
    def postDataSize: Int = Http.read(conn, postData~ptr, postData~size);
    // في حال تجاوز الحد الأعظم للرسائل نحذف أقدم رسالة
    if messages.getLength() >= MAX_MESSAGES messages.remove(0);
    // نضيف الرسالة الجديدة
    messages.add(String(postData~ptr, postDataSize));
    Http.print(conn, "HTTP/1.1 200 Ok\r\n\r\n");
}

وسنعدل منفذ جلب الرسائل ليعيد مجموعة الرسائل كلها. للقيام بذلك كل ما علينا القيام به هو دمج كافة الرسائل في رسالة واحدة يفصل الرسائل فيها عن بعض محرف السطر الجديد (\n)، وبذلك تعرض كل رسالة في سطر في الواجهة. يمكن القيام بذلك كما يلي:

// منفذ بياني يقبل الطريقة GET وله المسار المعطى
// يستعمل هذا المنفذ لجلب الرسائل
@منفذ_بياني["GET"، "/messages"]
دالة هات_الرسائل (اتصال: مؤشر[بـننف.اتـصال]) {
    عرف الرد: نـص = نـص.ادمج(رسائل، "
")؛ // نضع الترويسات اللازم بـننف.اطبع(اتصال، "HTTP/1.1 200 Ok\r\n")؛ بـننف.اطبع(اتصال، "Content-Type: text/plain\r\n")؛ بـننف.اطبع(اتصال، "Cache-Control: no-cache\r\n")؛ بـننف.اطبع(اتصال، "Content-Length: %d\r\n\r\n"، الرد.هات_الطول())؛ // نضع محتوى الصوان الذي يحمل الرسالة بـننف.اطبع(اتصال، الرد.صوان)؛ }
@beEndpoint["GET", "/messages"]
func getMessages (conn: ptr[Http.Connection]) {
    // ادمج الرسائل سويةً مع سطر فارغ بين كل رسالة والأخرى
    def response: String = String.merge(messages, "
"); // نضع الترويسات اللازمة Http.print(conn, "HTTP/1.1 200 Ok\r\n"); Http.print(conn, "Content-Type: text/plain\r\n"); Http.print(conn, "Cache-Control: no-cache\r\n"); Http.print(conn, "Content-Length: %d\r\n\r\n", response.getLength()); // نضع محتوى الصوان الذي يحمل الرسائل Http.print(conn, response.buf); }

إعادة هيكلة بعض أجزاء الشفرة المصدرية

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

صنف مـدخل_نصي {
    @حقنة عرف مركب: مـركب؛
}
class TextEntry {
    @injection def component: Component;
}

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

صنف مـدخل_نصي {
    @حقنة عرف مركب: مـركب؛

    عرف قد_تم_الإدخال: مغلفة (نـص)؛
    عرف مدخل_النص: سـندنا[مـدخل_نص]؛
}
class TextEntry {
    @injection def component: Component;

    def onNewEntry: closure (String);

    def textInput: SrdRef[TextInput];
}

الآن نعرف المشهد الذي سيعرضه هذا المركب في واجهة المستخدم، ونفعل ذلك في الباني. أي أننا سننسخ ما كتبناه سابقاً ونعيد ترتيبه قليلًا داخل هذا الصنف. يمكن كتابة الباني كما يلي:

عملية هذا~هيئ() {
    عرف الكائن: سند[هذا_الصنف](هذا)؛

    هذا.المشهد = صـندوق({}).{
        أضف_فروع({
            مـدخل_نص().{
                الكائن.مدخل_النص = هذا؛
            }،
            الـزر(نـص("أرسل")).{
                عند_الضغط.اربط(مغلفة (ودجة: سند[ودجـة]، الحمولة: سند[صحيح]) {
                    عرف كتابة: نـص = الكائن.مدخل_النص.هات_النص()؛
                    الكائن.مدخل_النص.حدد_النص(نـص())؛
                    إذا ليس الكائن.قد_تم_الإدخال.أهو_عدم() الكائن.قد_تم_الإدخال(كتابة)؛
                })؛
            }
        })؛
    }؛
}
handler this~init() {
    def self: ref[this_type](this);

    this.view = Box({}).{
        addChildren({
            TextInput().{
                self.textInput = this;
            },
            Button(String("Send")).{
                onClick.connect(closure (widget: ref[Widget], payload: ref[Int]) {
                    def text: String = self.textInput.getText();
                    self.textInput.setText(String());
                    if text.trim() != String() {
                        self.onNewEntry(text);
                    }
                });
            }
        });
    };
}

هناك بعض الفروق البسيطة بين هذا الكود وما كتبناه سابقاً. الفرق الأول هو استعمال السند الكائن (self) والذي يشير إلى الكائن الذي استدعي الباني لتهيئته. يتم استعمال هذا السند للوصول إلى أعضاء هذا الكائن ويمكن ملاحظة استعماله للوصول إلى المتغير مدخل_النص (textInput). نحتاج لهذا التعريف لأن الكلمة المفتاحية هذا (this) داخل أي حزمة أوامر (أي داخل المؤثر .{}) ستشير إلى الكائن الذي طُبق عليه المؤثر، وليس إلى كائن المركب الذي يحتوي على المغير مدخل_النص (textInput). أيضاً قمنا هنا بتحسين بسيط فأضفنا التحقق فيما إذا كان النص المدخل فارغًا، فإن كان لا نقوم بإرساله. آخر أمر يجب إضافته إلى الصنف حتى يمكن استعماله في المنافذ المرئية هو العملية هذا_الصنف() (this_type()) والتي ترجع سندًا مشتركًا إلى هذا النمط. تُغير هذه العملية الطريقة التي يعمل فيها التركيب مـدخل_نصي (TextEntry()) من إنشاء متغير مؤقت في على المكدس (وهي الطريقة المبدئية التي يعمل فيها هذا التركيب) إلى إنشاء سند مشترك وهو ما نحتاجه لبناء شجرات واجهة المستخدم. تعريف هذا العملية يجعل إنشاء شجرات واجهة المستخدم أسهل وأوضح.

عملية هذا_الصنف(): سـندنا[مـدخل_نصي] {
    أرجع سـندنا[مـدخل_نصي].أنشئ()؛
}
handler this_type(): SrdRef[TextEntry] {
    return SrdRef[TextEntry].construct();
}

لاحظ أن تعريف العملية هذا_الصنف() اختياري فهو مجرد أمر تجميلي. بإمكاننا استخدام التركيب سـندنا[مـدخل_نصي].أنشئ() مباشرة ضمن شجرات واجهة المستخدم، لكن الأسهل والأوضح كتابة مـدخل_نصي(). الآن كل ما علينا القيام به هو استبدال مكون الإدخال السابق بهذا المكون. ويمكن القيام بذلك كما يلي:

مـدخل_نصي().{
    قد_تم_الإدخال = مغلفة (بيانات_جديده: نـص) {
        أرسل_نداء(
            _إرسال_، _المسار_، _ترويسة_صنف_بيانات_نصي_، بيانات_جديده، 10000، مغلفة(جـيسون) {}
        )؛
        أرسل_نداء(_جلب_، _المسار_، 0، 0، 500، عند_استلام_بيانات)؛
    }؛
}
TextEntry().{
    onNewEntry = closure (newData: String) {
        sendRequest(
            "POST", "/messages", "Content-Type: application/text", newData, 10000,
            closure (Json) {}
        );
        sendRequest("GET", "/messages", null, null, 500, onFetch);
    };
}

هنا قمنا بإسناد مغلفة إلى قد_تم_الإدخال (onNewEntry) وبهذه الطريقة نستطيع من خارج المركب تحديد ما سيفعله المركب عند كبس الزر، وهو في هذه الحالة إرسال البيانات إلى الخادم. بهذه الطريقة يبقى المركب عامًا غير مرتبط بعملية إرسال البيانات، فيمكننا لاحقًا استخدامه في أماكن أخرى لأغراض أخرى. الكود الخاص بمغلفةعند_استلام_بيانات (onNewEntry) مماثل للكود السابق لها؛ لا داعي لتغيير أي شيء. بالإضافة إلى ما سبق من كتابة مكون لتنظيم الكود، هناك مشكلة أخرى، وهي أنه في حال كان شخصان يتحادثان وقام الأول بإرسال رسالة إلى الثاني، فإن الثاني عليه إعادة تحميل الصفحة حتى تُحمل الرسالة بدل أن تظهر تلقائيًا. لحل هذه المشكلة يمكننا القيام بتحديث البيانات بشكل دوري عن طريق مؤقِت كما يلي:

ابدأ_المؤقت_المتكرر(500000، مغلفة (جـيسون) {
    أرسل_نداء(_جلب_، _المسار_، 0، 0، 500، عند_استلام_بيانات)؛
})؛
startTimer(500000, closure (json: Json) {
    sendRequest("GET", "/messages", null, null, 500, onFetch);
});

بالتالي يصبح الكود كما يلي:

اشمل "مـتم/نـظام"؛
اشمل "بـناء"؛
اشمل "مـحا"؛
مـحا.اشمل_ملف("Alusus/WebPlatform"، "مـنصة_ويب.أسس")؛
مـحا.اشمل_ملف("Alusus/Http"، "بـننف.أسس")؛
مـحا.اشمل_ملف("Alusus/Json"، "جـيسون.أسس")؛
اشمل "مـتم/سندات"؛
اشمل "مـتم/مـصفوفة"؛
اشمل "مـتم/نـص"؛
اشمل "مـتم/طـرفية"؛
اشمل "مغلفة"؛

استخدم مـتم؛
استخدم مـنصة_ويب؛

//==============================================================================
// الخادم

عرف _حد_الرسائل_: 12؛  // عدد الرسائل الأعظمي
عرف رسائل : مـصفوفة[نـص]؛  // مصفوفة لتخزين الرسائل

// منفذ بياني يقبل الطريقة POST وله المسار المعطى
// يستعمل هذا المنفذ لإرسال الرسائل
@منفذ_بياني["POST"، "/messages"]
دالة أضف_رسالة (اتصال:مؤشر[بـننف.اتـصال]){

    عرف بيانات: مصفوفة[مـحرف،1024]؛
	// نقوم بجلب المعلومات التي تم إرسالها و تخزينها في الصوان
    عرف حجم_البيانات: صحيح = بـننف.اقرأ(اتصال، بيانات~مؤشر، بيانات~حجم)؛
    إذا رسائل.هات_الطول() >= _حد_الرسائل_ رسائل.أزل(0)؛
    // نخزن الرسالة الجديدة
    رسائل.أضف(نـص(بيانات~مؤشر، حجم_البيانات))؛
    بـننف.اطبع(اتصال، "HTTP/1.1 200 Ok\r\n\r\n")؛
}

// منفذ بياني يقبل الطريقة GET وله المسار المعطى
// يستعمل هذا المنفذ لجلب الرسائل
@منفذ_بياني["GET"، "/messages"]
دالة هات_الرسائل (اتصال: مؤشر[بـننف.اتـصال]) {
    عرف الرد: نـص = نـص.ادمج(رسائل، "
")؛ // نضع الترويسات اللازم بـننف.اطبع(اتصال، "HTTP/1.1 200 Ok\r\n")؛ بـننف.اطبع(اتصال، "Content-Type: text/plain\r\n")؛ بـننف.اطبع(اتصال، "Cache-Control: no-cache\r\n")؛ بـننف.اطبع(اتصال، "Content-Length: %d\r\n\r\n"، الرد.هات_الطول())؛ // نضع محتوى الصوان الذي يحمل الرسالة بـننف.اطبع(اتصال، الرد.صوان)؛ } //============================================================================== // مـركبات واجهة المستخدم عرف _جلب_: "GET"؛ عرف _إرسال_: "POST"؛ عرف _المسار_: "/messages"؛ عرف _ترويسة_صنف_بيانات_نصي_: "Content-Type: application/text"؛ // مركب يمثل خانة إدخال نصية صنف مـدخل_نصي { @حقنة عرف مركب: مـركب؛ عرف قد_تم_الإدخال: مغلفة (نـص)؛ عرف مدخل_النص: سـندنا[مـدخل_نص]؛ عملية هذا~هيئ() { عرف الكائن: سند[هذا_الصنف](هذا)؛ هذا.المشهد = صـندوق({}).{ أضف_فروع({ مـدخل_نص().{ الكائن.مدخل_النص = هذا؛ }، الـزر(نـص("أرسل")).{ عند_الضغط.اربط(مغلفة (ودجة: سند[ودجـة]، الحمولة: سند[صحيح]) { عرف كتابة: نـص = الكائن.مدخل_النص.هات_النص()؛ الكائن.مدخل_النص.حدد_النص(نـص())؛ إذا ليس الكائن.قد_تم_الإدخال.أهو_عدم() الكائن.قد_تم_الإدخال(كتابة)؛ })؛ } })؛ }؛ } عملية هذا_الصنف(): سـندنا[مـدخل_نصي] { أرجع سـندنا[مـدخل_نصي].أنشئ()؛ } } @منفذ_مرئي["/"] @عنوان["مثال منصة ويب - مربع إدخال نصي و زر"] دالة رئيسي { عرف مدخل_النص: سـندنا[مـدخل_نص]؛ عرف عرض_النص: سـندنا[كـتابة]؛ عرف عند_استلام_بيانات: مغلفة (جـيسون)؛ نـافذة.النموذج.حدد_المشهد( صـندوق({}).{ أضف_فروع({ صـندوق({}).{ أضف_فروع({ كـتابة(نـص("هنا سوف ترى النص الذي أدخلته")).{ عرض_النص = هذا؛ عند_استلام_بيانات = مغلفة (جيسون: جـيسون) { عرف الحالة: صحيح = جيسون.هات_كائن("eventData").هات_صحيح("status")؛ إذا الحالة >= 200 و الحالة < 300 { عرف البيانات: نـص = جيسون.هات_كائن("eventData").هات_نص("body")؛ إذا هذا.هات_النص() != البيانات { هذا.حدد_النص(البيانات)؛ } } وإلا { هذا.حدد_النص(نـص("الاتصال مقطوع. رمز حالة ب.ن.ن.ف: ") + الحالة)؛ } }؛ } })؛ }، مـدخل_نصي().{ قد_تم_الإدخال = مغلفة (بيانات_جديده: نـص) { أرسل_نداء( _إرسال_، _المسار_، _ترويسة_صنف_بيانات_نصي_، بيانات_جديده، 10000، مغلفة(جـيسون) {} )؛ أرسل_نداء(_جلب_، _المسار_، 0، 0، 500، عند_استلام_بيانات)؛ }؛ } })؛ } )؛ // نقوم بتعريف مؤقت يقوم بتحديث البيانات بدون الحاجة إلى إعادة تحميل الصفحة بشكل دوري ابدأ_المؤقت_المتكرر(500000، مغلفة (جـيسون) { أرسل_نداء(_جلب_، _المسار_، 0، 0، 500، عند_استلام_بيانات)؛ })؛ أرسل_نداء(_جلب_، _المسار_، 0، 0، 500، عند_استلام_بيانات)؛ نفذ_حلقة_معالجة_الأحداث()؛ } طـرفية.اطبع("تشغيل الخادم على المنفذ 8020...\nURL: http://localhost:8020/\n")؛ شغل_الخادم ({ "listening_ports"، "8020"})؛
import "Apm";
Apm.importFile("Alusus/WebPlatform");
use Srl;
use WebPlatform;

//==============================================================================
// الخادم

def MAX_MESSAGES: 12;  // عدد الرسائل الأعظمي
def messages: Array[String];  // مصفوفة لتخزين الرسائل

// وله المسار المعطى POST منفذ بياني يقبل الطريقة
// يستعمل هذا المنفذ لإرسال الرسائل
@beEndpoint["POST", "/messages"]
func postMessages (conn: ptr[Http.Connection]) {
    def postData: array[Char, 1024];
    // نقوم بجلب المعلومات التي تم إرسالها وتخزينها في الصوان
    def postDataSize: Int = Http.read(conn, postData~ptr, postData~size);
    // في حال تجاوز الحد الأعظم للرسائل نحذف أقدم رسالة
    if messages.getLength() >= MAX_MESSAGES messages.remove(0);
    // نضيف الرسالة الجديدة
    messages.add(String(postData~ptr, postDataSize));
    Http.print(conn, "HTTP/1.1 200 Ok\r\n\r\n");
}

// وله المسار المعطى GET منفذ بياني يقبل الطريقة
// يستعمل هذا المنفذ لجلب الرسائل
@beEndpoint["GET", "/messages"]
func getMessages (conn: ptr[Http.Connection]) {
    // ادمج الرسائل سويةً مع سطر فارغ بين كل رسالة والأخرى
    def response: String = String.merge(messages, "
"); // نضع الترويسات اللازمة Http.print(conn, "HTTP/1.1 200 Ok\r\n"); Http.print(conn, "Content-Type: text/plain\r\n"); Http.print(conn, "Cache-Control: no-cache\r\n"); Http.print(conn, "Content-Length: %d\r\n\r\n", response.getLength()); // نضع محتوى الصوان الذي يحمل الرسائل Http.print(conn, response.buf); } //============================================================================== // مـركبات واجهة المستخدم // مركب يمثل خانة إدخال نصية class TextEntry { @injection def component: Component; def onNewEntry: closure (String); def textInput: SrdRef[TextInput]; handler this~init() { def self: ref[this_type](this); this.view = Box({}).{ addChildren({ TextInput().{ self.textInput = this; }, Button(String("Send")).{ onClick.connect(closure (widget: ref[Widget], payload: ref[Int]) { def text: String = self.textInput.getText(); self.textInput.setText(String()); if text.trim() != String() { self.onNewEntry(text); } }); } }); }; } handler this_type(): SrdRef[TextEntry] { return SrdRef[TextEntry].construct(); } } @uiEndpoint["/"] @title["WebPlatform Example - Text Input and Button"] func main { def textShow: SrdRef[Text]; def onFetch: closure (json: Json); Window.instance.setView( Box({}).{ addChildren({ Box({}).{ addChildren({ Text(String("Here you will see the text you entered")).{ textShow = this; onFetch = closure (json: Json) { // في البداية نتحقق من أنه قد تم جلب البيانات بدون حدوث أخطاء def status: Int = json.getObject("eventData").getInt("status"); if status >= 200 and status < 300 { // نستخرج البيانات def data: String = json.getObject("eventData").getString("body"); // نقوم بتحديث النص if this.getText() != data { this.setText(data); } } else { // حالة حدوث خطأ this.setText(String("Connection error. HTTP status: ") + status); } }; } }); }, TextEntry().{ onNewEntry = closure (newData: String) { sendRequest( "POST", "/messages", "Content-Type: application/text", newData, 10000, closure (Json) {} ); sendRequest("GET", "/messages", null, null, 500, onFetch); }; } }) } ); // نقوم بتعريف مؤقت يقوم بتحديث البيانات بدون الحاجة إلى إعادة تحميل الصفحة بشكل دوري startTimer(500000, closure (json: Json) { sendRequest("GET", "/messages", null, null, 500, onFetch); }); // نجلب البيانات بشكل مباشر في أول مرة // أما في المرات اللاحقة بالمغلف الذي حددناه للمؤقت أو إدخال بيانات // جديدة سيؤدي إلى إعادة تحديث المحتوى sendRequest("GET", "/messages", null, null, 500, onFetch); runEventLoop(); } Console.print("Starting server on port 8020...\nURL: http://localhost:8020/\n"); runServer({ "listening_ports", "8020"});

بعض التحسينات الجمالية

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

عرف _لون_اصلي_: لـون("8e50ef")؛
عرف _لون_فاتح_: لـون("c380ff")؛
def PRIMARY_COLOR: Color("8e50ef");
def LIGHT_COLOR: Color("c380ff");

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

عملية هذا.العرض = سـندنا[مـسافة] {
    هذا.المشهد.الطراز.العرض = قيمة؛
    أرجع قيمة؛
}

عملية هذا.الطول = سـندنا[مـسافة] {
    هذا.المشهد.الطراز.الطول = قيمة؛
    أرجع قيمة؛
}
handler this.width = SrdRef[Length] {
    this.view.style.width = value;
    return value;
}

handler this.height = SrdRef[Length] {
    this.view.style.height = value;
    return value;
}

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

الطراز.{
    الإظهار = إظـهار._مرن_؛
    النسق = نـسق._سطر_؛
    سمك_الإطار = مـسافة4.نقاط(1.5)؛
    طراز_الإطار = طـراز_إطار._مستمر_؛
    لون_الإطار = _لون_اصلي_؛
    الخلفية = خـلفية(_لون_اصلي_)؛
    ملء_السطر = مـلء_سطر._مسافة_بينية_؛
}؛
style.{
    display = Display.FLEX;
    layout = Layout.ROW;
    justify = Justify.SPACE_BETWEEN;
    borderWidth = Length4.pt(1.5);
    borderStyle = BorderStyle.SOLID;
    borderColor = PRIMARY_COLOR;
    background = Background(PRIMARY_COLOR);
};

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

مـدخل_نص().{
    الكائن.مدخل_النص = هذا؛
    الطراز.{
        العرض = مـسافة.مئوي(100)؛
        الطول = مـسافة.مئوي(100)؛
        الخلفية = خـلفية(لـون("fff"))؛
        حجم_الخط = مـسافة.نقاط(12.0)؛
    }؛
}،
TextInput().{
    self.textInput = this;
    style.{
        width = Length.percent(100);
        height = Length.percent(100);
        background = Background(Color("fff"));
        fontSize = Length.pt(12.0);
    };
}

أما فيما يخص الزر فسنقوم بتحديد نفس الخصائص السابقة بالإضافة إلى طريقة التموضع لمحتواه:

الطراز.{
    الطول = مـسافة.مئوي(100)؛
    العرض = مـسافة.نقاط(50)؛
    حدد_طرازا_خاما[الخلفية](نـص("lightblue"))؛
    حجم_الخط = مـسافة.نقاط(16.0)؛
    ملء_السطر = مـلء_سطر._وسط_؛
}؛
style.{
    height = Length.percent(100);
    width = Length.pt(50);
    background = Background(Color(200, 200, 200));
    fontSize = Length.pt(16.0);
    justify = Justify.CENTER;
};

بعد هذه التعديلات سيصبح هذا المكون كما يلي:

// مركب يمثل خانة إدخال نصية
صنف مـدخل_نصي {
    @حقنة عرف مركب: مـركب؛

    عرف قد_تم_الإدخال: مغلفة (نـص)؛
    عرف مدخل_النص: سـندنا[مـدخل_نص]؛

    عملية هذا.العرض = سـندنا[مـسافة] {
        هذا.المشهد.الطراز.العرض = قيمة؛
        أرجع قيمة؛
    }

    عملية هذا.الطول = سـندنا[مـسافة] {
        هذا.المشهد.الطراز.الطول = قيمة؛
        أرجع قيمة؛
    }

    عملية هذا~هيئ() {
        عرف الكائن: سند[هذا_الصنف](هذا)؛

        هذا.المشهد = صـندوق({}).{
   			الطراز.{
                الإظهار = إظـهار._مرن_؛
                النسق = نـسق._سطر_؛
                سمك_الإطار = مـسافة4.نقاط(1.5)؛
                طراز_الإطار = طـراز_إطار._مستمر_؛
                لون_الإطار = _لون_اصلي_؛
                الخلفية = خـلفية(_لون_اصلي_)؛
                ملء_السطر = مـلء_سطر._مسافة_بينية_؛
            }؛
        	أضف_فروع({
                مـدخل_نص().{
                    الكائن.مدخل_النص = هذا؛
                    الطراز.{
                        العرض = مـسافة.مئوي(100)؛
                        الطول = مـسافة.مئوي(100)؛
                        الخلفية = خـلفية(لـون("fff"))؛
                        حجم_الخط = مـسافة.نقاط(12.0)؛
                    }؛
                }،
                الـزر(نـص("أرسل")).{
                	الطراز.{
                        الطول = مـسافة.مئوي(100)؛
                        العرض = مـسافة.نقاط(50)؛
                        حدد_طرازا_خاما[الخلفية](نـص("lightblue"))؛
                        حجم_الخط = مـسافة.نقاط(16.0)؛
                        ملء_السطر = مـلء_سطر._وسط_؛
                    }؛
                    عند_الضغط.اربط(مغلفة (ودجة: سند[ودجـة]، الحمولة: سند[صحيح]) {
                        عرف كتابة: نـص = الكائن.مدخل_النص.هات_النص()؛
                        الكائن.مدخل_النص.حدد_النص(نـص())؛
                        إذا ليس الكائن.قد_تم_الإدخال.أهو_عدم() الكائن.قد_تم_الإدخال(كتابة)؛
                    })؛
                }
            })؛
        }؛
    }

    عملية هذا_الصنف(): سـندنا[مـدخل_نصي] {
        أرجع سـندنا[مـدخل_نصي].أنشئ()؛
    }
}
class TextEntry {
    @injection def component: Component;

    def onNewEntry: closure (String);

    def textInput: SrdRef[TextInput];

    handler this.width = SrdRef[Length] {
        this.view.style.width = value;
        return value;
    }

    handler this.height = SrdRef[Length] {
        this.view.style.height = value;
        return value;
    }

    handler this~init() {
        def self: ref[this_type](this);

        this.view = Box({}).{
            style.{
                display = Display.FLEX;
                layout = Layout.ROW;
                justify = Justify.SPACE_BETWEEN;
                borderWidth = Length4.pt(1.5);
                borderStyle = BorderStyle.SOLID;
                borderColor = PRIMARY_COLOR;
                background = Background(PRIMARY_COLOR);
            };
            addChildren({
                TextInput().{
                    self.textInput = this;
                    style.{
                        width = Length.percent(100);
                        height = Length.percent(100);
                        background = Background(Color("fff"));
                        fontSize = Length.pt(12.0);
                    };
                },
                Button(String("Send")).{
                    style.{
                        height = Length.percent(100);
                        width = Length.pt(50);
                        background = Background(Color(200, 200, 200));
                        fontSize = Length.pt(16.0);
                        justify = Justify.CENTER;
                    };
                    onClick.connect(closure (widget: ref[Widget], payload: ref[Int]) {
                        def text: String = self.textInput.getText();
                        self.textInput.setText(String());
                        if text.trim() != String() {
                            self.onNewEntry(text);
                        }
                    });
                }
            });
        };
    }
    handler this_type(): SrdRef[TextEntry] {
        return SrdRef[TextEntry].construct();
    }
}

و عند إنشاء هذا المكون علينا الآن تمرير الطول والعرض المناسبين له، مثلاً كما يلي:

مـدخل_نصي().{
    العرض = مـسافة.مئوي(100)؛
    الطول = مـسافة.نقاط(50)؛
    قد_تم_الإدخال = مغلفة (بيانات_جديده: نـص) {
        أرسل_نداء(
            _إرسال_، _المسار_، _ترويسة_صنف_بيانات_نصي_، بيانات_جديده، 10000، مغلفة(جـيسون) {}
        )؛
        أرسل_نداء(_جلب_، _المسار_، 0، 0، 500، عند_استلام_بيانات)؛
    }؛
}
TextEntry().{
    width = Length.percent(100) - Length.pt(3);
    height = Length.pt(50);
    onNewEntry = closure (newData: String) {
        sendRequest(
            "POST", "/messages", "Content-Type: application/text", newData, 10000,
            closure (Json) {}
        );
        sendRequest("GET", "/messages", null, null, 500, onFetch);
    };
}

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

عرف عنوان_إشعارات: سـندنا[كـتابة]؛
def notificationLabel: SrdRef[Text];

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

كـتابة(نـص()).{
    عنوان_إشعارات = هذا؛
    الطراز.{
        العرض = مـسافة.مئوي(100)؛
        الطول = مـسافة.نقاط(20)؛
        لون_الخط = لـون(200، 50، 50)؛
        حجم_الخط = مـسافة.نقاط(10.0)؛
    }؛
}،
Text(String()).{
    notificationLabel = this;
    style.{
        width = Length.percent(100);
        height = Length.pt(20);
        fontColor = Color(200, 50, 50);
        fontSize = Length.pt(10.0);
    };
}

عندما عرفنا المغلفة عند_استلام_بيانات (onFetch) ضمن المكون الخاص بعرض الرسائل، قمنا بالتحقق فيما إذا كان هناك خطأ أم لا وعرضه مباشرة في الرسائل. سنعدل ذلك لنضع نص رسالة الخطأ في المكون الخاص بالإشعارات بدل من مساحة عرض الرسائل.

إذا الحالة >= 200 و الحالة < 300 {
    عرف البيانات: نـص = جيسون.هات_كائن("eventData").هات_نص("body")؛
    إذا هذا.هات_النص() != البيانات {
        هذا.حدد_النص(البيانات)؛
    }
    إذا عنوان_إشعارات.هات_النص() != "" {
        عنوان_إشعارات.حدد_النص(نـص(""))؛
    }
} وإلا {
    عنوان_إشعارات.حدد_النص(نـص("الاتصال مقطوع. رمز حالة ب.ن.ن.ف: ") + الحالة)؛
}
if status >= 200 and status < 300 {
    // نستخرج البيانات
    def data: String = json.getObject("eventData").getString("body");
    // نقوم بتحديث النص
    if this.getText() != data {
        this.setText(data);
    }
    // نقوم بإزالة الإشعار بعد تحديث النص
    if notificationLabel.getText() != "" {
        notificationLabel.setText(String(""));
    }
} else { // حالة حدوث خطأ
    // نقوم بوضع إشعار حدوث خطأ
    notificationLabel.setText(String("Connection error. HTTP status: ") + status);
}

تبقى تطبيق أطرزة على الصفحة بشكل كامل، يمكن القيام بذلك عن طريق تطبيقها على المكونات صـندوق (Box) والتي نضع بقية المكونات ضمنها. تطبيق ذلك مماثل تماماً لتطبيق الأطرزة على أي مكون.

وهذه الشفرة بصورتها النهائية:

اشمل "مـتم/نـظام"؛
اشمل "بـناء"؛
اشمل "مـحا"؛
مـحا.اشمل_ملف("Alusus/WebPlatform"، "مـنصة_ويب.أسس")؛
مـحا.اشمل_ملف("Alusus/Http"، "بـننف.أسس")؛
مـحا.اشمل_ملف("Alusus/Json"، "جـيسون.أسس")؛
اشمل "مـتم/سندات"؛
اشمل "مـتم/مـصفوفة"؛
اشمل "مـتم/نـص"؛
اشمل "مـتم/طـرفية"؛
اشمل "مغلفة"؛

استخدم مـتم؛
استخدم مـنصة_ويب؛

//==============================================================================
// الخادم

عرف _حد_الرسائل_: 12؛  // عدد الرسائل الأعظمي
عرف رسائل : مـصفوفة[نـص]؛  // مصفوفة لتخزين الرسائل

// منفذ بياني يقبل الطريقة POST وله المسار المعطى
// يستعمل هذا المنفذ لإرسال الرسائل
@منفذ_بياني["POST"، "/messages"]
دالة أضف_رسالة (اتصال:مؤشر[بـننف.اتـصال]){

    عرف بيانات: مصفوفة[مـحرف،1024]؛
	// نقوم بجلب المعلومات التي تم إرسالها و تخزينها في الصوان
    عرف حجم_البيانات: صحيح = بـننف.اقرأ(اتصال، بيانات~مؤشر، بيانات~حجم)؛
    إذا رسائل.هات_الطول() >= _حد_الرسائل_ رسائل.أزل(0)؛
    // نخزن الرسالة الجديدة
    رسائل.أضف(نـص(بيانات~مؤشر، حجم_البيانات))؛
    بـننف.اطبع(اتصال، "HTTP/1.1 200 Ok\r\n\r\n")؛
}


// منفذ بياني يقبل الطريقة GET وله المسار المعطى
// يستعمل هذا المنفذ لجلب الرسائل
@منفذ_بياني["GET"، "/messages"]
دالة هات_الرسائل (اتصال: مؤشر[بـننف.اتـصال]) {
    عرف الرد: نـص = نـص.ادمج(رسائل، "
")؛ // نضع الترويسات اللازم بـننف.اطبع(اتصال، "HTTP/1.1 200 Ok\r\n")؛ بـننف.اطبع(اتصال، "Content-Type: text/plain\r\n")؛ بـننف.اطبع(اتصال، "Cache-Control: no-cache\r\n")؛ بـننف.اطبع(اتصال، "Content-Length: %d\r\n\r\n"، الرد.هات_الطول())؛ // نضع محتوى الصوان الذي يحمل الرسالة بـننف.اطبع(اتصال، الرد.صوان)؛ } //============================================================================== // مـركبات واجهة المستخدم عرف خط_الكتابة: "30px Arial"؛ عرف _لون_اصلي_: لـون("8e50ef")؛ عرف _لون_فاتح_: لـون("c380ff")؛ عرف _جلب_: "GET"؛ عرف _إرسال_: "POST"؛ عرف _المسار_: "/messages"؛ عرف _ترويسة_صنف_بيانات_نصي_: "Content-Type: application/text"؛ // مركب يمثل خانة إدخال نصية صنف مـدخل_نصي { @حقنة عرف مركب: مـركب؛ عرف قد_تم_الإدخال: مغلفة (نـص)؛ عرف مدخل_النص: سـندنا[مـدخل_نص]؛ عملية هذا.العرض = سـندنا[مـسافة] { هذا.المشهد.الطراز.العرض = قيمة؛ أرجع قيمة؛ } عملية هذا.الطول = سـندنا[مـسافة] { هذا.المشهد.الطراز.الطول = قيمة؛ أرجع قيمة؛ } عملية هذا~هيئ() { عرف الكائن: سند[هذا_الصنف](هذا)؛ هذا.المشهد = صـندوق({}).{ الطراز.{ الإظهار = إظـهار._مرن_؛ النسق = نـسق._سطر_؛ سمك_الإطار = مـسافة4.نقاط(1.5)؛ طراز_الإطار = طـراز_إطار._مستمر_؛ لون_الإطار = _لون_اصلي_؛ الخلفية = خـلفية(_لون_اصلي_)؛ ملء_السطر = مـلء_سطر._مسافة_بينية_؛ }؛ أضف_فروع({ مـدخل_نص().{ الكائن.مدخل_النص = هذا؛ الطراز.{ العرض = مـسافة.مئوي(100)؛ الطول = مـسافة.مئوي(100)؛ الخلفية = خـلفية(لـون("fff"))؛ حجم_الخط = مـسافة.نقاط(12.0)؛ }؛ }، الـزر(نـص("أرسل")).{ الطراز.{ الطول = مـسافة.مئوي(100)؛ العرض = مـسافة.نقاط(50)؛ حدد_طرازا_خاما[الخلفية](نـص("lightblue"))؛ حجم_الخط = مـسافة.نقاط(16.0)؛ ملء_السطر = مـلء_سطر._وسط_؛ }؛ عند_الضغط.اربط(مغلفة (ودجة: سند[ودجـة]، الحمولة: سند[صحيح]) { عرف كتابة: نـص = الكائن.مدخل_النص.هات_النص()؛ الكائن.مدخل_النص.حدد_النص(نـص())؛ إذا ليس الكائن.قد_تم_الإدخال.أهو_عدم() الكائن.قد_تم_الإدخال(كتابة)؛ })؛ } })؛ }؛ } عملية هذا_الصنف(): سـندنا[مـدخل_نصي] { أرجع سـندنا[مـدخل_نصي].أنشئ()؛ } } //============================================================================== // المنافذ المرئية @منفذ_مرئي["/"] @عنوان["مثال منصة ويب - مربع إدخال نصي و زر"] دالة رئيسي { عرف مدخل_النص: سـندنا[مـدخل_نص]؛ عرف عرض_النص: سـندنا[كـتابة]؛ عرف عند_استلام_بيانات: مغلفة (جـيسون)؛ نـافذة.النموذج.الطراز.{ الحشوة = مـسافة4.نقاط(0)؛ الهامش = مـسافة4.نقاط(0)؛ }؛ نـافذة.النموذج.حدد_المشهد( صـندوق({}).{ الطراز.{ الطول = مـسافة.مئوي(100)؛ ملء_السطر = مـلء_سطر._مسافة_بينية_؛ الاتجاه = اتـجاه._من_اليمين_؛ الإظهار = إظـهار._مرن_؛ النسق = نـسق._عمود_؛ }؛ أضف_فروع({ صـندوق({}).{ الطراز.{ العرض = مـسافة.مئوي(100)؛ الحشوة = مـسافة4.نقاط(5)؛ الإظهار = إظـهار._مرن_؛ النسق = نـسق._عمود_؛ المرونة = مـرونة(1، 1)؛ } عرف عنوان_إشعارات: سـندنا[كـتابة]؛ أضف_فروع({ كـتابة(نـص()).{ عنوان_إشعارات = هذا؛ الطراز.{ العرض = مـسافة.مئوي(100)؛ الطول = مـسافة.نقاط(20)؛ لون_الخط = لـون(200، 50، 50)؛ حجم_الخط = مـسافة.نقاط(10.0)؛ }؛ }، كـتابة(نـص()).{ الطراز.{ العرض = مـسافة.مئوي(100)؛ الطول = مـسافة.مئوي(100)؛ لون_الخط = لـون(50، 50، 50)؛ حجم_الخط = مـسافة.نقاط(20.0)؛ }؛ عرض_النص = هذا؛ عند_استلام_بيانات = مغلفة (جيسون: جـيسون) { عرف الحالة: صحيح = جيسون.هات_كائن("eventData").هات_صحيح("status")؛ إذا الحالة >= 200 و الحالة < 300 { عرف البيانات: نـص = جيسون.هات_كائن("eventData").هات_نص("body")؛ إذا هذا.هات_النص() != البيانات { هذا.حدد_النص(البيانات)؛ } إذا عنوان_إشعارات.هات_النص() != "" { عنوان_إشعارات.حدد_النص(نـص(""))؛ } } وإلا { عنوان_إشعارات.حدد_النص(نـص("الاتصال مقطوع. رمز حالة ب.ن.ن.ف: ") + الحالة)؛ } }؛ } })؛ }، مـدخل_نصي().{ العرض = مـسافة.مئوي(100)؛ الطول = مـسافة.نقاط(50)؛ قد_تم_الإدخال = مغلفة (بيانات_جديده: نـص) { أرسل_نداء( _إرسال_، _المسار_، _ترويسة_صنف_بيانات_نصي_، بيانات_جديده، 10000، مغلفة(جـيسون) {} )؛ أرسل_نداء(_جلب_، _المسار_، 0، 0، 500، عند_استلام_بيانات)؛ }؛ } })؛ } )؛ // نقوم بتعريف مؤقت يقوم بتحديث البيانات بدون الحاجة إلى إعادة تحميل الصفحة بشكل دوري ابدأ_المؤقت_المتكرر(500000، مغلفة (جـيسون) { أرسل_نداء(_جلب_، _المسار_، 0، 0، 500، عند_استلام_بيانات)؛ })؛ أرسل_نداء(_جلب_، _المسار_، 0، 0، 500، عند_استلام_بيانات)؛ نفذ_حلقة_معالجة_الأحداث()؛ } طـرفية.اطبع("تشغيل الخادم على المنفذ 8020...\nURL: http://localhost:8020/\n")؛ شغل_الخادم ({ "listening_ports"، "8020"})؛
import "Apm";
Apm.importFile("Alusus/WebPlatform");
use Srl;
use WebPlatform;

//==============================================================================
// Server

def MAX_MESSAGES: 12;  // عدد الرسائل الأعظمي
def messages: Array[String];  // مصفوفة لتخزين الرسائل

// وله المسار المعطى POST منفذ بياني يقبل الطريقة
// يستعمل هذا المنفذ لإرسال الرسائل
@beEndpoint["POST", "/messages"]
func postMessages (conn: ptr[Http.Connection]) {
    def postData: array[Char, 1024];
    // نقوم بجلب المعلومات التي تم إرسالها وتخزينها في الصوان
    def postDataSize: Int = Http.read(conn, postData~ptr, postData~size);
    // في حال تجاوز الحد الأعظم للرسائل نحذف أقدم رسالة
    if messages.getLength() >= MAX_MESSAGES messages.remove(0);
    // نضيف الرسالة الجديدة
    messages.add(String(postData~ptr, postDataSize));
    Http.print(conn, "HTTP/1.1 200 Ok\r\n\r\n");
}

// وله المسار المعطى GET منفذ بياني يقبل الطريقة
// يستعمل هذا المنفذ لجلب الرسائل
@beEndpoint["GET", "/messages"]
func getMessages (conn: ptr[Http.Connection]) {
    // ادمج الرسائل سويةً مع سطر فارغ بين كل رسالة والأخرى
    def response: String = String.merge(messages, "
"); // نضع الترويسات اللازمة Http.print(conn, "HTTP/1.1 200 Ok\r\n"); Http.print(conn, "Content-Type: text/plain\r\n"); Http.print(conn, "Cache-Control: no-cache\r\n"); Http.print(conn, "Content-Length: %d\r\n\r\n", response.getLength()); // نضع محتوى الصوان الذي يحمل الرسائل Http.print(conn, response.buf); } //============================================================================== // UI Components def PRIMARY_COLOR: Color("8e50ef"); def LIGHT_COLOR: Color("c380ff"); // مركب يمثل خانة إدخال نصية class TextEntry { @injection def component: Component; def onNewEntry: closure (String); def textInput: SrdRef[TextInput]; handler this.width = SrdRef[Length] { this.view.style.width = value; return value; } handler this.height = SrdRef[Length] { this.view.style.height = value; return value; } handler this~init() { def self: ref[this_type](this); this.view = Box({}).{ style.{ display = Display.FLEX; layout = Layout.ROW; justify = Justify.SPACE_BETWEEN; borderWidth = Length4.pt(1.5); borderStyle = BorderStyle.SOLID; borderColor = PRIMARY_COLOR; background = Background(PRIMARY_COLOR); }; addChildren({ TextInput().{ self.textInput = this; style.{ width = Length.percent(100); height = Length.percent(100); background = Background(Color("fff")); fontSize = Length.pt(12.0); }; }, Button(String("Send")).{ style.{ height = Length.percent(100); width = Length.pt(50); background = Background(Color(200, 200, 200)); fontSize = Length.pt(16.0); justify = Justify.CENTER; }; onClick.connect(closure (widget: ref[Widget], payload: ref[Int]) { def text: String = self.textInput.getText(); self.textInput.setText(String()); if text.trim() != String() { self.onNewEntry(text); } }); } }); }; } handler this_type(): SrdRef[TextEntry] { return SrdRef[TextEntry].construct(); } } //============================================================================== // UI Endpoints @uiEndpoint["/"] @title["WebPlatform Example - Text Input and Button"] func main { def textInput: SrdRef[TextInput]; def textShow: SrdRef[Text]; def onFetch: closure (json: Json); def onNewEntry: closure (String); Window.instance.style.{ padding = Length4.pt(0); margin = Length4.pt(0); }; Window.instance.setView( Box({}).{ style.{ height = Length.percent(100); justify = Justify.SPACE_BETWEEN; display = Display.FLEX; layout = Layout.COLUMN; }; addChildren({ Box({}).{ style.{ width = Length.percent(100) - Length.pt(10); padding = Length4.pt(5); display = Display.FLEX; layout = Layout.COLUMN; flex = Flex(1); }; def notificationLabel: SrdRef[Text]; addChildren({ Text(String()).{ notificationLabel = this; style.{ width = Length.percent(100); height = Length.pt(20); fontColor = Color(200, 50, 50); fontSize = Length.pt(10.0); }; }, Text(String()).{ style.{ width = Length.percent(100); height = Length.percent(100); fontColor = Color(50, 50, 50); fontSize = Length.pt(20.0); }; // نقوم بتغريف مغلف سيتم تتنفيذه عند جلب البيانات من الخادم onFetch = closure (json: Json) { // في البداية نتحقق من أنه قد تم جلب البيانات بدون حدوث أخطاء def status: Int = json.getObject("eventData").getInt("status"); if status >= 200 and status < 300 { // نستخرج البيانات def data: String = json.getObject("eventData").getString("body"); // نقوم بتحديث النص if this.getText() != data { this.setText(data); } // نقوم بإزالة الإشعار بعد تحديث النص if notificationLabel.getText() != "" { notificationLabel.setText(String("")); } } else { // حالة حدوث خطأ // نقوم بوضع إشعار حدوث خطأ notificationLabel.setText(String("Connection error. HTTP status: ") + status); } }; } }); }, TextEntry().{ width = Length.percent(100) - Length.pt(3); height = Length.pt(50); onNewEntry = closure (newData: String) { sendRequest( "POST", "/messages", "Content-Type: application/text", newData, 10000, closure (Json) {} ); sendRequest("GET", "/messages", null, null, 500, onFetch); }; } }); }); // نقوم بتعريف مؤقت يقوم بتحديث البيانات بدون الحاجة إلى إعادة تحميل الصفحة بشكل دوري startTimer(500000, closure (json: Json) { sendRequest("GET", "/messages", null, null, 500, onFetch); }); // نجلب البيانات بشكل مباشر في أول مرة // أما في المرات اللاحقة بالمغلف الذي حددناه للمؤقت أو إدخال بيانات // جديدة سيؤدي إلى إعادة تحديث المحتوى sendRequest("GET", "/messages", null, null, 500, onFetch); runEventLoop(); } Console.print("Starting server on port 8020...\nURL: http://localhost:8020/\n"); runServer({ "listening_ports", "8020"});

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