الکس راسل، وبکار مشهور، در اثر مهم خود «بازار لیمو»، شکستهای بیشمار صنعت ما را با تمرکز بر عواقب فاجعهبار برای کاربران نهایی بیان میکند. این نارضایتی طبق آیین نامه رسانه ما کاملاً مناسب است.
چارچوبها در این معادله بسیار نقش دارند، با این حال میتواند دلایل خوبی برای توسعهدهندگان فرانتاند برای انتخاب یک چارچوب یا کتابخانه برای آن موضوع وجود داشته باشد: بهروزرسانی پویا رابطهای وب میتواند به روشهای غیر واضح دشوار باشد. بیایید با شروع از ابتدا و بازگشت به اصول اول بررسی کنیم.
دسته بندی های نشانه گذاری
همه چیز در وب با نشانه گذاری، یعنی HTML شروع می شود. ساختارهای نشانه گذاری را می توان به سه دسته تقسیم کرد:
- قطعات ثابتی که همیشه ثابت می مانند.
- قسمت های متغیری که یک بار در ابتدا تعریف می شوند.
- قسمت های متغیری که در زمان اجرا به صورت پویا به روز می شوند.
برای مثال، هدر یک مقاله ممکن است به شکل زیر باشد:
<header>
<h1>«Hello World»</h1>
<small>«123» backlinks</small>
</header>
در اینجا بخشهای متغیر در «guillemets» پیچیده شدهاند: «Hello World» عنوان مربوطه است که فقط بین مقالهها تغییر میکند. با این حال، شمارنده بک لینک ممکن است به طور مداوم از طریق برنامه نویسی سمت مشتری به روز شود. ما آماده هستیم تا در فضای وبلاگ به صورت ویروسی تبدیل شویم. همه چیزهای دیگر در تمام مقالات ما یکسان باقی می مانند.
مقاله ای که اکنون می خوانید متعاقباً بر دسته سوم تمرکز دارد: محتوایی که باید در زمان اجرا به روز شود.
مرورگر رنگی
تصور کنید ما در حال ساخت یک مرورگر رنگی ساده هستیم: یک ویجت کوچک برای بررسی مجموعه ای از رنگ های نامگذاری شده از پیش تعریف شده، ارائه شده به عنوان لیستی که یک نمونه رنگ را با مقدار رنگ مربوطه جفت می کند. کاربران باید بتوانند نام رنگ ها را جستجو کنند و بین کدهای رنگ هگزادسیمال و سه قلوهای قرمز، آبی و سبز (RGB) جابجا شوند. فقط با کمی HTML و CSS می توانیم یک اسکلت بی اثر ایجاد کنیم:
رندر سمت مشتری
ما با اکراه تصمیم گرفتیم از رندر سمت مشتری برای نسخه تعاملی استفاده کنیم. برای اهداف ما در اینجا، فرقی نمی کند که این ویجت یک برنامه کاربردی کامل باشد یا صرفاً یک جزیره مستقل که در یک سند HTML ایستا یا تولید شده توسط سرور تعبیه شده است.
با توجه به تمایل ما به جاوا اسکریپت وانیلی (به اصول اول و همه موارد مراجعه کنید)، ما با APIهای داخلی DOM مرورگر شروع می کنیم:
function renderPalette(colors) {
let items = ();
for(let color of colors) {
let item = document.createElement("li");
items.push(item);
let value = color.hex;
makeElement("input", {
parent: item,
type: "color",
value
});
makeElement("span", {
parent: item,
text: color.name
});
makeElement("code", {
parent: item,
text: value
});
}
let list = document.createElement("ul");
list.append(...items);
return list;
}
توجه داشته باشید:
موارد فوق به یک تابع کاربردی کوچک برای ایجاد عناصر مختصرتر متکی است:function makeElement(tag, { parent, children, text, ...attribs }) { let el = document.createElement(tag); if(text) { el.textContent = text; } for(let (name, value) of Object.entries(attribs)) { el.setAttribute(name, value); } if(children) { el.append(...children); } parent?.appendChild(el); return el; }
همچنین ممکن است متوجه یک تناقض سبک شده باشید: در داخل
items
حلقه، عناصر تازه ایجاد شده خود را به ظرف خود متصل می کنند. بعداً، ما مسئولیتها را برمیگردانیمlist
ظرف به جای آن عناصر کودک را می بلعد.
وویلا: renderPalette
لیست رنگ های ما را تولید می کند. بیایید یک فرم برای تعامل اضافه کنیم:
function renderControls() {
return makeElement("form", {
method: "dialog",
children: (
createField("search", "Search"),
createField("checkbox", "RGB")
)
});
}
را createField
تابع ابزار ساختارهای DOM مورد نیاز برای فیلدهای ورودی را محصور می کند. این یک جزء کوچک نشانه گذاری قابل استفاده مجدد است:
function createField(type, caption) {
let children = (
makeElement("span", { text: caption }),
makeElement("input", { type })
);
return makeElement("label", {
children: type === "checkbox" ? children.reverse() : children
});
}
اکنون فقط باید آن قطعات را با هم ترکیب کنیم. بیایید آنها را در یک عنصر سفارشی بپیچیم:
import { COLORS } from "./colors.js"; // an array of `{ name, hex, rgb }` objects
customElements.define("color-browser", class ColorBrowser extends HTMLElement {
colors = (...COLORS); // local copy
connectedCallback() {
this.append(
renderControls(),
renderPalette(this.colors)
);
}
});
از این پس، الف <color-browser>
عنصر در هر نقطه از HTML ما کل رابط کاربری را همانجا تولید می کند. (من دوست دارم به آن به عنوان یک کلان در حال گسترش فکر کنم.) این پیاده سازی تا حدودی بیانگر است1، با ساختارهای DOM که با ترکیب انواع مولدهای نشانه گذاری ساده ایجاد می شوند، اگر بخواهید مؤلفه هایی به وضوح مشخص شده اند.
1 مفیدترین توضیحی که در مورد تفاوت بین برنامه نویسی اعلانی و امری که من با آن برخورد کردم، بر روی خوانندگان تمرکز دارد. متأسفانه، آن منبع خاص از من فراری است، بنابراین من در اینجا تعبیر می کنم: کد اعلامی چی در حالی که کد امری توصیف می کند چگونه. یک پیامد این است که کد امری نیاز به تلاش شناختی دارد تا به طور متوالی دستورالعمل های کد را طی کند و یک مدل ذهنی از نتیجه مربوطه ایجاد کند.
تعامل
در این مرحله، ما فقط در حال بازسازی اسکلت بی اثر خود هستیم. هنوز هیچ تعامل واقعی وجود ندارد. گردانندگان رویداد برای نجات:
class ColorBrowser extends HTMLElement {
colors = (...COLORS);
query = null;
rgb = false;
connectedCallback() {
this.append(renderControls(), renderPalette(this.colors));
this.addEventListener("input", this);
this.addEventListener("change", this);
}
handleEvent(ev) {
let el = ev.target;
switch(ev.type) {
case "change":
if(el.type === "checkbox") {
this.rgb = el.checked;
}
break;
case "input":
if(el.type === "search") {
this.query = el.value.toLowerCase();
}
break;
}
}
}
توجه داشته باشید:
handleEvent
به این معنی است که ما نباید نگران اتصال تابع باشیم. همچنین با مزایای مختلفی همراه است. الگوهای دیگر موجود است.
هر زمان که یک فیلد تغییر می کند، متغیر نمونه مربوطه را به روز می کنیم (گاهی اوقات به آن اتصال داده یک طرفه می گویند). افسوس، تغییر این وضعیت داخلی2 تاکنون در هیچ کجای UI منعکس نشده است.
2 در کنسول توسعه دهنده مرورگر خود، بررسی کنید document.querySelector("color-browser").query
پس از وارد کردن عبارت جستجو
توجه داشته باشید که این کنترل کننده رویداد به شدت به هم متصل شده است renderControls
داخلی، زیرا به ترتیب انتظار یک چک باکس و فیلد جستجو را دارد. بنابراین، هر گونه تغییر مربوطه به renderControls
– شاید جابهجایی به دکمههای رادیویی برای نمایش رنگ – اکنون باید این قطعه کد دیگر را در نظر بگیرید: اقدام در فاصله! گسترش قرارداد این مؤلفه برای گنجاندن نام فیلدها می تواند این نگرانی ها را کاهش دهد.
ما اکنون با انتخاب بین زیر روبرو هستیم:
- دسترسی به DOM قبلا ایجاد شده برای اصلاح آن، یا
- بازآفرینی آن در حالی که حالت جدیدی را در خود جای داده است.
بازپردازی
از آنجایی که قبلاً ترکیب نشانه گذاری خود را در یک مکان تعریف کرده ایم، اجازه دهید با گزینه دوم شروع کنیم. ما به سادگی مولدهای نشانه گذاری خود را مجدداً اجرا می کنیم و وضعیت فعلی را به آنها می دهیم.
class ColorBrowser extends HTMLElement {
// (previous details omitted)
connectedCallback() {
this.#render();
this.addEventListener("input", this);
this.addEventListener("change", this);
}
handleEvent(ev) {
// (previous details omitted)
this.#render();
}
#render() {
this.replaceChildren();
this.append(renderControls(), renderPalette(this.colors));
}
}
ما تمام منطق رندر را به یک متد اختصاصی منتقل کرده ایم3، که نه تنها یک بار در هنگام راه اندازی، بلکه هر زمان که وضعیت تغییر می کند، از آن فراخوانی می کنیم.
3 ممکن است بخواهید از ویژگی های خصوصی اجتناب کنید، به خصوص اگر دیگران ممکن است بر اساس پیاده سازی شما ساخته شوند.
بعد، می توانیم بچرخیم colors
به یک گیرنده برای برگرداندن فقط ورودی های مطابق با وضعیت مربوطه، یعنی عبارت جستجوی کاربر:
class ColorBrowser extends HTMLElement {
query = null;
rgb = false;
// (previous details omitted)
get colors() {
let { query } = this;
if(!query) {
return (...COLORS);
}
return COLORS.filter(color => color.name.toLowerCase().includes(query));
}
}
توجه داشته باشید:
من نسبت به الگوی جست و خیز طرفدار هستم.
تغییر نمایش رنگ به عنوان تمرینی برای خواننده باقی مانده است. شاید بگذریthis.rgb
بهrenderPalette
و سپس پر کنید<code>
با هر کدامcolor.hex
یاcolor.rgb
، شاید با استفاده از این ابزار:function formatRGB(value) { return value.split(","). map(num => num.toString().padStart(3, " ")). join(", "); }
اکنون این رفتار جالب (واقعاً آزاردهنده) ایجاد می کند:
وارد کردن یک پرس و جو غیرممکن به نظر می رسد زیرا فیلد ورودی پس از انجام تغییر تمرکز خود را از دست می دهد و فیلد ورودی خالی می ماند. با این حال، وارد کردن یک کاراکتر غیر معمول (مثلاً “v”) این را روشن می کند چیزی در حال وقوع است: لیست رنگ ها واقعاً تغییر می کند.
دلیل آن این است که رویکرد فعلی ما خودت انجام بده (DIY) کاملاً خام است: #render
با هر تغییر DOM عمده فروشی را پاک و دوباره ایجاد می کند. دور انداختن گرههای DOM موجود نیز وضعیت مربوطه را بازنشانی میکند، از جمله مقدار فیلدهای فرم، فوکوس و موقعیت اسکرول. این خوب نیست!
رندر افزایشی
رابط کاربری داده محور بخش قبلی ایده خوبی به نظر می رسید: ساختارهای نشانه گذاری یک بار تعریف می شوند و به دلخواه بر اساس مدل داده ای که به طور واضح وضعیت فعلی را نشان می دهد، دوباره ارائه می شوند. با این حال، وضعیت صریح جزء ما به وضوح کافی نیست. ما باید آن را با حالت ضمنی مرورگر در حین رندر کردن مجدد تطبیق دهیم.
مطمئنا، ما ممکن است تلاش کنیم تا آن را انجام دهیم ضمنی حالت صریح و آن را در مدل داده ما، مانند گنجاندن یک فیلد، بگنجانیم value
یا checked
خواص اما این هنوز بسیاری از موارد را نادیده میگیرد، از جمله مدیریت فوکوس، موقعیت اسکرول و جزئیات بیشماری که احتمالاً حتی به آن فکر نکردهایم (اغلب، این به معنای ویژگیهای دسترسی است). خیلی زود، ما به طور موثر مرورگر را دوباره ایجاد می کنیم!
در عوض ممکن است سعی کنیم تشخیص دهیم کدام بخش از UI نیاز به به روز رسانی دارد و بقیه DOM را دست نخورده بگذاریم. متأسفانه، این به دور از اهمیت است، جایی که کتابخانههایی مانند React بیش از یک دهه پیش وارد عمل شدند: در ظاهر، آنها روشی شفافتر برای تعریف ساختارهای DOM ارائه کردند.4 (در حالی که ترکیب مولفه ای را نیز تشویق می کند، یک منبع حقیقت واحد را برای هر الگوی UI فردی ایجاد می کند). چنین کتابخانه هایی در زیر سرپوش، مکانیسم هایی را معرفی کردند5 بهجای بازآفرینی درختهای DOM از ابتدا، بهروزرسانیهای تدریجی و دانهای DOM ارائه دهید – هم برای جلوگیری از تضاد حالت و هم برای بهبود عملکرد6.
4 در این زمینه، این اساساً به معنای نوشتن چیزی است که شبیه HTML است، که بسته به سیستم اعتقادی شما، یا ضروری است یا طغیانکننده. وضعیت قالب HTML در آن زمان تا حدودی وخیم بود و در برخی از محیطها پایینتر باقی میماند.
5 نولان لاوسون «بیایید با ساختن فریمورکهای جاوا اسکریپت مدرن یاد بگیریم که چگونه کار میکنند» بینشهای ارزشمند زیادی در مورد آن موضوع ارائه میکند. برای جزئیات بیشتر، مستندات توسعه دهنده lit-html ارزش مطالعه دارد.
6 ما از آن زمان یاد گرفتیم که مقداری این مکانیسم ها در واقع بسیار گران هستند.
نتیجه نهایی: اگر میخواهیم تعاریف نشانهگذاری را کپسوله کنیم و سپس رابط کاربری خود را از یک مدل داده متغیر استخراج کنیم، باید به یک کتابخانه شخص ثالث برای تطبیق تکیه کنیم.
Actus Imperatus
در انتهای دیگر طیف، ما ممکن است اصلاحات جراحی را انتخاب کنیم. اگر بدانیم چه چیزی را هدف قرار دهیم، کد برنامه ما میتواند به DOM برسد و تنها قسمتهایی را که نیاز به بهروزرسانی دارند اصلاح کند.
با این حال، متأسفانه، این رویکرد معمولاً منجر به اتصال شدید فاجعهآمیز میشود، با منطق مرتبط به هم که در سراسر برنامه پخش میشود، در حالی که روالهای هدفمند ناگزیر کپسولهسازی اجزا را نقض میکنند. وقتی جایگشتهای UI پیچیدهتر را در نظر میگیریم (به موارد لبه، گزارش خطا و غیره فکر کنید) اوضاع پیچیدهتر میشود. این همان مسائلی است که کتابخانههای مذکور امیدوار بودند آنها را ریشه کن کنند.
در مورد مرورگر رنگی ما، این به معنای یافتن و پنهان کردن ورودیهای رنگی است که با پرس و جو مطابقت ندارند، البته اگر هیچ ورودی منطبقی باقی نماند، لیست را با یک پیام جایگزین جایگزین کنید. ما همچنین باید نمایش های رنگی را در جای خود عوض کنیم. احتمالاً میتوانید تصور کنید که چگونه کد حاصل میتواند هر گونه جدایی از نگرانیها را از بین ببرد و عناصری را که در اصل منحصراً متعلق به renderPalette
.
class ColorBrowser extends HTMLElement {
// (previous details omitted)
handleEvent(ev) {
// (previous details omitted)
for(let item of this.#list.children) {
item.hidden = !item.textContent.toLowerCase().includes(this.query);
}
if(this.#list.children.filter(el => !el.hidden).length === 0) {
// inject substitute message
}
}
#render() {
// (previous details omitted)
this.#list = renderPalette(this.colors);
}
}
همانطور که روزی یک مرد خردمند گفت: این دانش بسیار است!
همه چیز با فیلدهای فرم حتی خطرناک تر می شود: نه تنها ممکن است مجبور باشیم وضعیت خاص یک فیلد را به روز کنیم، بلکه باید بدانیم کجا پیام های خطا را تزریق کنیم. در حین رسیدن به renderPalette
به اندازه کافی بد بود، در اینجا باید چندین لایه را سوراخ کنیم: createField
یک ابزار عمومی است که توسط renderControls
، که به نوبه خود توسط سطح بالای ما فراخوانی می شود ColorBrowser
.
اگر حتی در این مثال حداقلی همه چیز پرمو می شود، تصور کنید که کاربرد پیچیده تری با لایه ها و غیرجهت های بیشتر داشته باشید. حفظ در بالای همه آن اتصالات غیرممکن می شود. چنین سیستم هایی معمولاً به یک توپ بزرگ از گل تبدیل می شوند که در آن هیچ کس جرات تغییر چیزی را ندارد از ترس شکستن ناخواسته اشیا.
نتیجه
به نظر می رسد که در API های استاندارد مرورگر یک حذف آشکار وجود دارد. ترجیح ما برای راه حل های جاوا اسکریپت وانیلی بدون وابستگی به دلیل نیاز به به روز رسانی غیر مخرب ساختارهای DOM موجود خنثی شده است. با این فرض که ما برای یک رویکرد اعلامی با کپسولهسازی غیرقابل تعرض ارزش قائل هستیم، که بهعنوان «مهندسی نرمافزار مدرن: قطعات خوب» شناخته میشود.
همانطور که در حال حاضر وجود دارد، نظر شخصی من این است که یک کتابخانه کوچک مانند lit-html یا Preact اغلب ضمانت دارد، به ویژه هنگامی که با در نظر گرفتن قابلیت جایگزینی استفاده می شود: یک API استاندارد شده ممکن است هنوز اتفاق بیفتد! در هر صورت، کتابخانههای کافی دارای ردپایی سبک هستند و معمولاً محدودیت زیادی برای کاربران نهایی ایجاد نمیکنند، بهویژه هنگامی که با بهبود تدریجی ترکیب شوند.
با این حال، نمیخواهم شما را معلق بگذارم، بنابراین پیادهسازی جاوا اسکریپت وانیلی خود را فریب دادم تا اغلب کاری را که ما انتظار داریم انجام دهد:
(Yk)
]
منبع: https://smashingmagazine.com/2024/02/vanilla-javascript-libraries-quest-stateful-dom-rendering/