توسعهی فولاستک با یک زبان یعنی سرور، لایهی داده، صفحهها و کدی که در مرورگر اجرا میشود را بنویسید، بی آنکه سر مرز میان آنها زبان عوض کنید. زبان برنامهنویسی سلام از بیشتر زبانهای کامپایلی به این هدف نزدیکتر است: یک برنامهی سلام میتواند به درخواستهای HTTP پاسخ دهد، از پایگاه داده پرسمان بگیرد، صفحههای HTML گونهدار رندر کند و رویدادگیرهایی برای مرورگر بفرستد که همان کامپایلر آنها را به جاوااسکریپت ترجمه میکند. سلام هنوز در سمت کاربر جای یک پشتهی پختهی جاوااسکریپت را نمیگیرد، و این مقاله دقیق میگوید مرز کجاست.
این پرسش برای تیمهای کوچک و برای کسانی که تازه برنامهنویسی وب یاد میگیرند اهمیت دارد. هر مرز زبانی در یک پشتهی وب هزینه دارد: دو مجموعه گونه که از هم فاصله میگیرند، اعتبارسنجیای که دو بار نوشته میشود، دو زنجیرهی ابزار، دو مدیر بسته و یک جابهجایی ذهنی هر بار که خطایی از سرور به صفحه میرسد. Node.js شعار «جاوااسکریپت همهجا» را دقیقاً به همین دلیل محبوب کرد. سلام از سوی دیگر میآید: زبانی کامپایلی با گونههای ایستا که تا مرورگر بالا میرود.
«یک زبان» در سلام چه چیزهایی را پوشش میدهد
یک برنامهی وب معمولی پنج لایه دارد. این جدول نشان میدهد سلام برای هر لایه چه چیزی دارد، و همهاش همراه کامپایلر میآید:
| لایه | در سلام | برای مطالعهی بیشتر |
|---|---|---|
| سرور HTTP، مسیریابی، میانافزار | net.http، net.router، net.http.middleware | net.http، سرور وب سریع |
| داده و SQL | db با درایورهای SQLite، MySQL و PostgreSQL، و بررسیهای db.sql | SQL گونهدار |
| HTML و CSS | زبان صفحه، صفحه: و صفحه نام(پارامترها): | درس زبان صفحه |
| رفتار مرورگر | رویدادگیرهای سلام که به جاوااسکریپت کامپایل میشوند | بخشها و حالت در رابط کاربری |
| ساخت و استقرار | salam build، salam page build، salam web، salam serve | salam serve |
چارچوب وبی برای انتخاب وجود ندارد، چون همین تکهها خودشان چارچوباند، و ساخت جداگانهای برای فرانتاند هم در کار نیست: نه بستهساز (bundler)، نه پیکربندی ترنسپایلر و نه node_modules. کامپایلر یک فایل اجرایی است.
بکاند: سرور HTTP که صفحهی گونهدار رندر میکند
مرکز یک برنامهی وب سلام یک برنامهی معمولی است. رویدادگیرها شناسهی زمینهی درخواست را میگیرند، درخواست را با تابعهایی مثل اچ تی تی پی.پارامتر، اچ تی تی پی.پرسمان، اچ تی تی پی.فرم و اچ تی تی پی.بدنه میخوانند و با اچ تی تی پی.متن، اچ تی تی پی.جیسون یا اچ تی تی پی.اچ تی ام ال پاسخ میدهند.
بخش جالب، خود صفحه است. صفحهای که نام و پارامترهای گونهدار دارد یک چیدمان زمان اجراست: کامپایلر ساختار سند و CSS آن را هنگام کامپایل میسازد و چیدمان را به تابعی تبدیل میکند که HTML را با مقدارهای پرشده برمیگرداند. نه مفسر قالبی در زمان اجرا هست و نه پروندهی قالبی که از کد فاصله بگیرد. هر مقداری که از پارامتر بیاید خودکار گریز HTML داده میشود.
برنامهی زیر یک برنامهی کامل یادداشت در یک پرونده است. صفحه را ارائه میکند، ارسال فرم را میپذیرد، آن را اعتبارسنجی میکند، یادداشت را نگه میدارد و همان داده را برای کلاینتهای دیگر به شکل JSON هم عرضه میکند. برای اینکه اینجا قابل اجرا باشد، ریشه سرور را در پسزمینه روی یک درگاه آزاد روشن میکند و خودش نقش کلاینت را بازی میکند.
واردسازی شبکه.اچ تی تی پی
واردسازی شبکه.مسیریاب
واردسازی کدگذاری.جیسون
واردسازی رشته
ناپایا یادداشتها := وکتور {} برگردان وکتور<یادداشت>
ساختار یادداشت:
@جیسون "id"
همگانی شماره: صحیح = ۰
@جیسون "text"
همگانی متن: رشته = ""
پایان
صفحه برگه یادداشتها(موارد: وکتور<یادداشت>، مشکل: رشته):
عنوان = "یادداشتها"
سرتیتر: اندازه = ۱ محتوا = "یادداشتها" پایان
اگر مشکل != "":
پاراگراف: کلاس = "error" محتوا = مشکل پایان
پایان
فهرست:
هر ی در موارد:
مورد: محتوا = ی.متن پایان
پایان
پایان
فرم:
روش = "post"
مقصد ارسال = "/notes"
ورودی: نام = "text" پایان
دکمه: نوع = ارسال محتوا = "افزودن" پایان
پایان
پایان
تابع ایراد متن(متن: رشته): رشته:
اگر متن.طول() == ۰:
برگشت "اول چیزی بنویسید"
پایان
اگر متن.طول() > ۱۴۰:
برگشت "یادداشت را کوتاهتر کنید (حداکثر ۱۴۰ بایت)"
پایان
برگشت ""
پایان
تابع نمایش برگه(زمینه: صحیح۶۴):
اچ تی تی پی.اچ تی ام ال(زمینه، برگه یادداشتها(یادداشتها، ""))
پایان
تابع افزودن(زمینه: صحیح۶۴):
متن := اچ تی تی پی.فرم(زمینه، "text")
مشکل := ایراد متن(متن)
اگر مشکل != "":
اچ تی تی پی.تنظیم وضعیت(زمینه، ۴۲۲)
اچ تی تی پی.اچ تی ام ال(زمینه، برگه یادداشتها(یادداشتها، مشکل))
برگشت
پایان
یادداشتها.بیفزا(یادداشت { شماره = یادداشتها.طول() + ۱، متن = رشته.رونوشت(متن) })
اچ تی تی پی.تغییرمسیر(زمینه، "/")
پایان
تابع رابط برنامه(زمینه: صحیح۶۴):
اچ تی تی پی.جیسون(زمینه، جیسون.سریال سازی(یادداشتها))
پایان
تابع ریشه:
ناپایا م := مسیریاب.مسیریاب جدید()
مسیریاب.دریافت(م، "/"، نمایش برگه)
مسیریاب.ارسال(م، "/notes"، افزودن)
مسیریاب.دریافت(م، "/api/notes"، رابط برنامه)
ناپایا س := اچ تی تی پی.سرور جدید(۰)
اچ تی تی پی.استفاده(س، م)
درگاه := اچ تی تی پی.اجرا در پس زمینه با درگاه(س)
پایه := "http://127.0.0.1:" + درگاه
سرچاپ اچ تی تی پی.وضعیت(اچ تی تی پی.ارسال(پایه + "/notes"، "text=<b>خرید شیر</b>"))
سرچاپ اچ تی تی پی.وضعیت(اچ تی تی پی.ارسال(پایه + "/notes"، "text="))
سرچاپ اچ تی تی پی.بدنه(اچ تی تی پی.دریافت(پایه + "/api/notes"))
سند := اچ تی تی پی.بدنه(اچ تی تی پی.دریافت(پایه + "/"))
سرچاپ سند.دربردارد("<li><b>خرید شیر</b></li>")
پایانخروجی را از بالا بخوانید. نخستین ارسال موفق است و به صفحه برمیگرداند. ارسال خالی در اعتبارسنجی رد میشود و پاسخ ۴۲۲ میگیرد، همراه با صفحهای که دور یک پیام خطا دوباره رندر شده است. مسیر JSON یادداشت ذخیرهشده را دقیقاً همانطور که تایپ شده برمیگرداند و مسیر HTML آن را گریزداده نشان میدهد: تگ <b> که بازدیدکننده تایپ کرده روی صفحه متن است و هرگز نشانهگذاری نمیشود. لازم نبود کسی گریز دادن را به خاطر بسپارد.
دو نکته ارزش دیدن دارد. برگه یادداشتها یک وکتور<یادداشت> میگیرد، همان ساختاری که API آن را به JSON تبدیل میکند، پس صفحه و API نمیتوانند دربارهی اینکه یادداشت چیست اختلاف داشته باشند؛ نام یک فیلد را عوض کنید و هر دو کامپایل نمیشوند. و یادداشت با رشته.رونوشت(متن) ذخیره میشود: مقداری که از درخواست خوانده شده مال همان درخواست است و وقتی رویدادگیر برمیگردد آزاد میشود، پس هر چیزی که بیشتر نگه داشته شود باید رونوشت شود. این بهای سروری بدون زبالهروب (Garbage Collector) است، و مرجع net.http این قاعده را بیان میکند.
قاعدههایی که صفحهی سمت سرور را امن نگه میدارند
چیدمان زمان اجرا عمداً محدود شده است، و همین محدودیتها هستند که دادن دادهی کاربر به آن را امن میکنند:
- یک تابع ناب است. چیدمان نمیتواند پرونده بخواند، به شبکه وصل شود، عدد تصادفی بسازد یا متغیر سراسری بنویسد؛ کامپایلر آن را رد میکند (E012). رویدادگیر داده را میآورد و به صفحه میدهد؛ خواندن یک پارامتر
وکتور، مثل طول یا عنصرهایش، مجاز است ولی تغییر دادنش نه. - متن همیشه گریز داده میشود. گونهای برای «HTML خام» وجود ندارد که تصادفی سراغش بروید.
- نشانیها و مقدارهای CSS گونه دارند. یک
رشتهی ساده نمیتواند جایی بنشیند که نشانی، رنگ یا طول لازم است (E135). اول آن را باوب.خواندن نشانی وب،وب.خواندن رنگ وبیاوب.خواندن اندازه وبمیخوانید که مقدارهایی مثل پیوندهایjavascript:را رد میکنند. - ساختار در زمان کامپایل ثابت است. فقط مقدارها عوض میشوند، پس بازدیدکننده نمیتواند عنصری تزریق کند.
اینها همان درسهاییاند که موتورهای قالبی مثل html/template در Go یا گریز دادن JSX در React طی سالها آموختند، با این تفاوت که اینجا بررسیکنندهی گونه آنها را اجبار میکند، نه قرارداد.
کد مرورگر به زبان سلام
صفحهای که سمت سرور رندر میشود و فرم دارد خودش یک برنامهی کامل است، و برای بسیاری از ابزارهای داخلی و سایتهای محتوایی طراحی درست همین است. وقتی صفحه باید پیش از رفتوبرگشت به سرور واکنش نشان دهد، رویدادگیرها هم به سلام نوشته میشوند. این صفحه یادداشت را پیش از فرستادن فرم بررسی میکند و از یک تابع کمکی در همین پرونده استفاده میکند:
واردسازی دام
تابع ایراد عنوان(عنوان یادداشت: رشته): رشته:
اگر عنوان یادداشت.طول() == ۰:
برگشت "اول چیزی بنویسید"
پایان
اگر عنوان یادداشت.طول() > ۱۴۰:
برگشت "یادداشت را کوتاهتر کنید (حداکثر ۱۴۰ بایت)"
پایان
برگشت ""
پایان
تابع بررسی(رویداد: صحیح۶۴):
مشکل := ایراد عنوان(دام.دریافت مقدار(دام.با شناسه("text")))
دام.تنظیم متن(دام.با شناسه("hint")، مشکل)
اگر مشکل != "":
دام.جلوگیری پیش فرض(رویداد)
پایان
پایان
صفحه:
عنوان = "یادداشتها"
فرم:
روش = "post"
مقصد ارسال = "/notes"
ارسال = بررسی
ورودی: شناسه = "text" نام = "text" پایان
دکمه: نوع = ارسال محتوا = "افزودن" پایان
پایان
پاراگراف: شناسه = "hint" رنگ = "#b91c1c" پایان
پایانفرمان salam page build notes.salam تابعهای بررسی و ایراد عنوان را با پشتوانهی جاوااسکریپت سلام در اسکریپت صفحه کامپایل میکند. بررسی در مرورگر فقط برای راحتی بازدیدکننده است؛ سرور همچنان اعتبارسنجی میکند، چون هر چه در مرورگر اجرا شود به دست کسی که از آن استفاده میکند قابل تغییر است. درس حالت، DOM و زمانسنجها هم با همین هشدار تمام میشود.
فراتر از رویدادگیرهای تکی، سه ابزار برای سمت کاربر هست:
- بخشهای حالتدار، که در آنها رویدادگیرها فقط متغیرها را تغییر میدهند و صفحه خودش دوباره رسم میشود. مقالهی رابط کاربری تعاملی در سلام آنها را پوشش میدهد.
salam js app.salamکل یک برنامهی سلام را به جاوااسکریپت کامپایل میکند، یا اگر نام خروجی به.htmlختم شود، به یک صفحهی HTML خودبسنده.salam webیک صفحه را همراه هر برنامهیscript: src = "app.salam"که به آن اشاره میکند در یک پروندهی HTML میسازد، یا با--splitدر پروندههای جداگانهی.html،.cssو.js.
اشتراک کد میان سرور و مرورگر
وعدهی واقعی یک زبان واحد این است که هر قاعده یک بار نوشته شود. در سلام یک پروندهی کمکی میتواند هم در سرور کامپایل شود و هم در اسکریپت مرورگر. یک پروژهی کوچک میتواند این شکل را داشته باشد:
notes/
rules/rules.salam # بستهی قواعد: ایراد عنوان و تابعهای همخانواده
main.salam # سرور: فراخوانی قواعد "rules/rules.salam"
client.salam # کد مرورگر: فراخوانی قواعد "rules/rules.salam"
index.salam # صفحه: ... اسکریپتی که به client.salam اشاره میکندپروندهی مشترک سلام ساده است:
بسته قواعد
همگانی تابع ایراد عنوان(عنوان: رشته): رشته:
اگر عنوان.طول() == ۰:
برگشت "عنوان لازم است"
پایان
اگر عنوان.طول() > ۸۰:
برگشت "عنوان را کوتاهتر از ۸۰ بایت نگه دارید"
پایان
برگشت ""
پایانسرور آن را با فراخوانی قواعد "rules/rules.salam" وارد میکند و در رویدادگیرش قواعد.ایراد عنوان را صدا میزند؛ client.salam هم همین کار را میکند و تابع را با دام.هنگام رویداد جزئی (dom.OnEventE) به فرم وصل میکند. سپس:
salam build main.salam --output=notes-server
salam web index.salam --output=public/index.htmlوقتی این چیدمان را آزمودیم، هر دو ساخت همان تابع را کامپایل کردند: پروندهی اجرایی سرور به شکل بومی و نسخهی مرورگر در اسکریپت صفحه. یک نکته از همان آزمون: کمکیای که با فراخوانی در یک پروندهی سادهی صفحه: وارد شد به اسکریپت ساختهشدهی آن منتقل نشد؛ پس کد مرورگری را که از بستههای مشترک استفاده میکند در یک برنامهی اسکریپت جداگانه بگذارید که salam web آن را میسازد، مثل بالا.
اشتراک کد در سلام یک فایدهی ظریف هم دارد. هم ساخت بومی و هم ساخت جاوااسکریپت طول رشته را بر حسب بایتهای UTF-8 میشمارند، پس یک قاعدهی طول در هر دو سو همان پاسخ را میدهد. این در هر چیدمان «ایزومورفیک» صادق نیست؛ خود جاوااسکریپت واحدهای کد UTF-16 را میشمارد. برای متن فارسی این نکته اهمیت بیشتری دارد: هر حرف فارسی در UTF-8 دو بایت است، پس سقف ۱۴۰ بایتی برنامهی بالا حدود ۷۰ حرف فارسی است. اگر در سرور شمار حرفها را لازم دارید، .شمارنویسه() حرفها را میشمارد و .طول() بایتها را.
استقرار یک برنامهی فولاستک سلام
برنامهی وب سلام به یک فایل اجرایی بومی تبدیل میشود و به نصب محیط اجرایی نیاز ندارد. شکلهای رایج در محیط عملیاتی اینهایند:
- برنامه پشت یک پراکسی TLS. سرور را روی یک درگاه محلی اجرا کنید و Caddy یا nginx را برای HTTPS جلویش بگذارید، یا
اچ تی تی پی.اجرای امن(http.RunTLS) را با گواهی و کلید صدا بزنید. پروندههای ایستا را میتوان بامسیریاب.ایستااز همان برنامه ارائه کرد. salam serve --config=web.salamبرای سمت ایستا. سرور وب بستهیnet.httpdمیزبانهای مجازی، مسیرها، تغییر مسیرها، حافظهی نهان و PHP از راه FastCGI را مدیریت میکند. در نسخهی کنونی خودش TLS و پراکسی معکوس ندارد، برای همین درس salam serve آن را با Caddy جفت میکند.- Cloudflare Workers. برنامهای که با
net/httpوnet/routerنوشته شده با--target=wasm32-wasiبه WebAssembly ساخته میشود و بدون تغییر در لبهی شبکه اجرا میشود، با این محدودیتها: یک فرایند برای هر درخواست، بدون سیستم پرونده و بدون سوکت خروجی از درون ماژول.
و چون کل پشته یک زبان است، همهی برنامهها را میتوان در CI با یک salam check بررسی گونه کرد و رشتههای SQL آنها را با salam sql check.
هنوز چه چیزهایی کم است
مقایسهی منصفانه با یک پشتهی فولاستک جاوااسکریپت یا TypeScript باید فهرست کند سلام هنوز چه ندارد:
- کمکتابعی برای گرفتن داده در سمت کاربر ندارد. بستهی
دامDOM، زمانسنجها و حافظهی مرورگر را میپیچد ولیfetchرا نه. برنامههای تکصفحهای غنی که JSON را در پسزمینه بار میکنند امروز شکل طبیعی یک برنامهی سلام نیستند؛ صفحههای رندرشده در سرور با فرمها هستند. - اکوسیستم بخشها مثل npm وجود ندارد. بخشهایتان را خودتان مینویسید یا از راه تابعهای بیگونهی
دام.فراخوانبه جاوااسکریپت موجود پل میزنید. - زنجیرهی ابزار جوان است. زبان صفحه، پشتوانهی جاوااسکریپت و اجرای بومی رابط کاربری میان نسخهها تغییر میکنند. بعضی قابلیتهای زبان روی پشتوانهی جاوااسکریپت رفتار متفاوتی دارند؛ شمردن بایت در رشتهها یکی از آنهاست.
- جامعهی کوچک و استقرارهای عملیاتی اندک. احتمال اینکه پاسخ مشکلات غیرمعمول از پیش وجود داشته باشد کمتر است.
مقایسه با محبوبترین پشتهی «یک زبان» در مقالهی سلام در برابر TypeScript آمده است.
فولاستک با زبان سلام کجا مناسب است
داستان فولاستک سلام برای برنامههایی که سمت سرور رندر میشوند و تعامل متمرکزی دارند قویتر است: پنلهای مدیریت، داشبوردهای داخلی، ابزارهای کسبوکار پر از فرم، سایتهای مستندات و محتوا، پشتصحنهی سرویسهای کوچک نرمافزاری و پروژههای آموزشی که دانشجو باید کل چرخهی درخواست را در یک زبان ببیند، آن هم برای دانشجوی فارسیزبان، به زبان خودش. عملکرد کامپایلی در سرور، صفحههای گونهداری که نمیشود در آنها نشانهگذاری تزریق کرد، یک زنجیرهی ابزار و یک مجموعه گونه به دست میآورید. برای برنامهای که ارزشش بیشتر در یک رابط بزرگ سمت کاربر است، یک چارچوب جاوااسکریپت امروز هنوز فرانتاند عملیتری است، و سرور سلام پشت آن همچنان بکاند معقولی است.
پرسشهای پرتکرار
آیا زبان سلام میتواند جای جاوااسکریپت را در مرورگر بگیرد؟
برای رویدادگیرها، صفحههای تعاملی کوچک و بخشهای حالتدار، بله: سلام مینویسید و کامپایلر جاوااسکریپت را میسازد. برای برنامههایی که به کتابخانههای فراوان شخص ثالث سمت کاربر یا گرفتن داده در پسزمینه نیاز دارند، هنوز نه.
آیا سلام چارچوب وب (Web Framework) دارد؟
کتابخانهی استاندارد خودش چارچوب است: net.http و net.router برای سرور، زبان صفحه برای HTML و CSS، net.http.session، net.http.validate و net.http.middleware برای نیازهای رایج، و db برای داده. چیز اضافهای برای نصب وجود ندارد.
آیا رندر سمت سرور در سلام در برابر XSS امن است؟
مقدارهایی که به چیدمان زمان اجرا داده میشوند همیشه گریز HTML میگیرند، نشانیها و مقدارهای CSS باید اول به گونههای امن خوانده شوند و ساختار صفحه در زمان کامپایل ثابت است. این کار راههای رایج تزریق را میبندد، ولی سرآیندهای امنیتی، توکنهای CSRF و نشستها همچنان بر عهدهی شماست؛ net.http.middleware و net.http.session در این کارها کمک میکنند.
یک پروژهی فولاستک با زبان برنامهنویسی سلام را چطور شروع کنم؟
کامپایلر را از صفحهی نصب نصب کنید، درس زبان صفحه و درس پروژههای وب را پیش ببرید و سپس با نمونههای صفحهی net.http یک سرور به آن اضافه کنید.