در قطعه مهم خود “بازار لیمو“، وب میلنگ مشهور الکس راسل، شکست های بی شمار صنعت ما را با تمرکز بر عواقب فاجعه بار برای کاربران نهایی بیان می کند. این خشم کاملاً مناسب است با توجه به آیین نامه رسانه ما.
فریمورکها به شدت در آن معادله نقش دارند، با این حال میتواند دلایل خوبی برای توسعهدهندگان فرانتاند برای انتخاب یک چارچوب وجود داشته باشد. یا کتابخانه برای این موضوع: به روز رسانی پویا رابط های وب می تواند به روش های غیر واضح دشوار باشد. بیایید با شروع از ابتدا و بازگشت به اصول اول بررسی کنیم.
دسته بندی های نشانه گذاری
همه چیز در وب با نشانه گذاری، یعنی 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)