جاوااسکریپت | JavaScript
507 subscribers
654 photos
139 videos
3 files
512 links
کانال @IR_javascript حاوی اطلاعات مفید در حوزه برنامه نویس فرانت که بصورت روزانه بروز می‌شود.
در این کانال شما به:
[1] مطالب تازه
[2] تحلیل‌های عمیق
[3] نکات آموزشی
[4] چالش
[5] ابزار و راهنمایی‌های کاربردی
دسترسی خواهید داشت.

🆔@IR_javascript
Download Telegram
### آیا وب‌سایت شما واقعاً بین‌المللی است؟

اگر وب‌سایت شما از چندین زبان پشتیبانی می‌کند، به این معنا نیست که واقعاً برای کاربران سراسر جهان بهینه شده است.

🔹 بومی‌سازی (L10n) یعنی ترجمه محتوا و تطبیق آن با نیازهای یک مخاطب خاص.
🔹 بین‌المللی‌سازی (I18n) یعنی طراحی سیستم به‌گونه‌ای که از ابتدا امکان پشتیبانی از زبان‌ها، فرمت‌ها و ویژگی‌های فرهنگی مختلف را بدون تغییرات اساسی داشته باشد.

و اگر فکر می‌کنید که اضافه کردن فایل‌های ترجمه کافی است، باید بگوییم که این موضوع پیچیده‌تر از آن چیزی است که به نظر می‌رسد! بیایید نگاهی به مهم‌ترین چالش‌ها بیندازیم.

## پنج اشتباه رایج که مانع بین‌المللی شدن وب‌سایت شما می‌شود

### ۱. زبان فقط متن نیست!
اشتباه: استفاده از رشته‌های ثابت درون کد (Hardcoded Strings).
راه‌حل درست:
- استفاده از سیستم‌های مدیریت ترجمه، فایل‌های ترجمه، پایگاه‌های داده یا سرویس‌های تخصصی برای بومی‌سازی.
- اضافه کردن یک زبان جدید نباید نیازمند تغییر در کد باشد.
- یک عنصر ممکن است در زبان‌های مختلف طول و ساختار متفاوتی داشته باشد.

### ۲. متن‌ها می‌توانند دو برابر طولانی‌تر شوند
در زبان‌های مختلف، طول کلمات متفاوت است. مثلاً:
- "Sign in" (انگلیسی) – هفت کاراکتر
- "Anmelden" (آلمانی) – نه کاراکتر
- "Kirjaudu sisään" (فنلاندی) – شانزده کاراکتر

📌 اگر عرض دکمه‌ها یا عناصر به‌صورت ثابت تعیین شده باشد، متن ممکن است جا نشود یا به‌هم ریخته نمایش داده شود.

راه‌حل درست: استفاده از طراحی منعطف که امکان تغییر اندازه متن را فراهم کند.

### ۳. اعداد، تاریخ‌ها و ارزها در همه جا یکسان نیستند
📅 تاریخ‌ها: ۰۳/۰۷/۲۰۲۵ یعنی سوم ژوئیه یا هفتم مارس؟
- در ایالات متحده: هفتم مارس (MM/DD/YYYY)
- در اروپا: سوم ژوئیه (DD/MM/YYYY)

راه‌حل درست: استفاده از سیستم‌های فرمت‌دهی خودکار که اطلاعات را متناسب با منطقه کاربر تنظیم می‌کنند، مانند:
- Intl.DateTimeFormat برای تاریخ
- Intl.NumberFormat برای اعداد و ارزها

### ۴. پیچیدگی‌های گرامری: حالت‌های دستوری و جمع‌ها
در زبان انگلیسی جمع بستن ساده است:
- یک سیب → *1 apple*
- پنج سیب → *5 apples*

اما در زبان‌هایی مانند روسی و فارسی چنین نیست:
- یک سیب → *۱ سیب*
- دو سیب → *۲ سیب*
- پنج سیب → *۵ سیب*

📌 اشتباه: جایگذاری ساده اعداد در متن بدون در نظر گرفتن قوانین گرامری، مثلاً:
> *"شما {{count}} سیب دارید."*

راه‌حل درست: استفاده از مکانیزم‌های خاص برای تطبیق اعداد با کلمات، مثلاً:
- Intl.PluralRules در جاوا اسکریپت برای تعیین صحیح فرم کلمات بر اساس عدد.

### ۵. پشتیبانی از متن‌های راست به چپ (RTL)
برخی زبان‌ها مانند عربی، عبری و فارسی از راست به چپ (RTL) نوشته می‌شوند.

اگر وب‌سایت فقط برای چیدمان چپ به راست (LTR) طراحی شده باشد:
- متن‌ها به‌درستی نمایش داده نمی‌شوند.
- دکمه‌های ناوبری در جای نادرست قرار می‌گیرند.
- آیکون‌ها (مانند فلش‌های "بازگشت" و "بعدی") ممکن است جهت اشتباهی داشته باشند.

راه‌حل درست:
- تنظیم خودکار جهت متن متناسب با زبان کاربر.
- استفاده از سبک‌های منعطف (CSS) که به‌راحتی بین LTR و RTL تغییر کنند.

---

### بین‌المللی‌سازی فقط ترجمه نیست!
اگر می‌خواهید وب‌سایت شما برای کاربران سراسر دنیا واقعاً کارآمد باشد، باید به این موارد توجه کنید:
سیستم منعطف ترجمه – عدم استفاده از رشته‌های ثابت در کد
تطبیق رابط کاربری – متون ممکن است طولانی‌تر یا کوتاه‌تر از حد انتظار باشند
نمایش صحیح اعداد، تاریخ‌ها و ارزها – فرمت‌دهی خودکار بر اساس منطقه
رعایت گرامر زبان‌ها – پشتیبانی از حالت‌های مختلف دستوری و جمع‌بندی
پشتیبانی از جهت نوشتار RTL – سازگاری کامل با زبان‌های راست‌به‌چپ

💬 شما چه چالش‌هایی در بین‌المللی‌سازی تجربه کرده‌اید؟ تجربه خود را در نظرات به اشتراک بگذارید!

#️⃣#discussion
👥@IR_javascript_group
🆔@IR_javascript
👍5🤯2
### مراحل رندرینگ صفحه وب: از کد تا یک رابط کاربری زیبا

در ادامه بررسی موضوع «چه اتفاقی می‌افتد وقتی یک URL را وارد کرده و Enter را فشار می‌دهید؟»

---

### ۱. تجزیه (Parsing) محتوای HTML
هنگامی که مرورگر یک سند HTML را دریافت می‌کند، فرآیند تجزیه آن آغاز می‌شود:

ساخت درخت DOM (Document Object Model)
- تگ‌های HTML به اشیایی تبدیل شده و یک درخت از عناصر را تشکیل می‌دهند.
- هر تگ به یک گره (Node) در این درخت تبدیل می‌شود.

تشخیص منابع خارجی
- مرورگر هنگام پردازش HTML، تگ‌هایی مانند <link>`، <script> و <img>` را شناسایی می‌کند.
- این تگ‌ها منابعی مانند فایل‌های CSS، اسکریپت‌های جاوا اسکریپت و تصاویر را مشخص می‌کنند که برای نمایش صفحه مورد نیازند.

---

### ۲. بارگذاری منابع (Resource Loading)
بارگیری منابع خارجی توسط مرورگر می‌تواند همزمان با تجزیه HTML انجام شود. این منابع به دو دسته تقسیم می‌شوند:

منابع غیربلاک‌کننده (Non-blocking Resources)
- مانند تصاویر که به‌صورت موازی با پردازش HTML بارگیری می‌شوند و تأخیری در نمایش سایر بخش‌های صفحه ایجاد نمی‌کنند.

منابع بلاک‌کننده (Blocking Resources)
- شامل اسکریپت‌هایی که بدون ویژگی‌های `async` یا `defer` بارگیری می‌شوند.
- بارگیری این اسکریپت‌ها پردازش HTML را متوقف می‌کند تا ترتیب اجرای کدها حفظ شود و ساختار یا رفتار صفحه تحت تأثیر قرار نگیرد.

---

### ۳. ساخت مدل اشیای CSS (CSSOM - CSS Object Model)
به محض دریافت فایل‌های CSS، مرورگر شروع به پردازش استایل‌ها می‌کند:

ساخت درخت CSSOM
- تمامی قوانین و استایل‌های CSS به‌صورت یک درخت جداگانه سازماندهی می‌شوند.

ایجاد درخت رندر (Render Tree)
- پس از تکمیل DOM و CSSOM**، این دو ترکیب شده و **درخت رندر (Render Tree) تشکیل می‌شود.
- این درخت برای نمایش بصری عناصر در صفحه استفاده می‌شود.

🔹 نکته: فرآیند ساخت CSSOM هم‌زمان (Synchronous) است، یعنی مرورگر تا تکمیل این مرحله سایر مراحل پردازش صفحه را متوقف می‌کند.

---

### ۴. محاسبه طرح‌بندی (Layout - یا کامپوزیشن)
پس از تشکیل Render Tree**، مرورگر موقعیت و اندازه‌ی دقیق هر عنصر را در صفحه تعیین می‌کند:

**محاسبه موقعیت و ابعاد عناصر

- هر عنصر جایگاه و اندازه مشخصی روی صفحه دریافت می‌کند.
- این فرآیند Layout یا Compositing نام دارد.

---

### ۵. نقاشی (Painting) و نمایش نهایی صفحه
در مرحله پایانی، مرورگر عناصر را روی صفحه نمایش می‌دهد:

رندر پیکسل‌ها
- درخت رندر به پیکسل‌های رنگی ترجمه شده و محتوا به‌صورت گرافیکی نمایش داده می‌شود.

پردازش افکت‌ها و انیمیشن‌ها
- اگر صفحه شامل انیمیشن‌ها یا عناصر پویا باشد، مرورگر آن‌ها را پردازش کرده و نمایش می‌دهد.

---

### نتیجه‌گیری
همان‌طور که دیدیم، مرورگر طی مراحل گام‌به‌گام، مجموعه‌ای از HTML، CSS و JavaScript را به یک صفحه وب زیبا و کاربردی تبدیل می‌کند!



#️⃣#tip
👥@IR_javascript_group
🆔@IR_javascript
👍2
ایجاد اپلیکیشن‌های وب دسترس‌پذیر با Vue

یک کتابخانه متن‌باز شامل کامپوننت‌های خام و بدون استایل که همراه با نمونه‌های متنوع و کاربردهای آماده ارائه می‌شود و می‌توان آن را مستقیماً در پروژه‌های شما ادغام کرد.

🔗https://reka-ui.com/
#️⃣#npm_module
👥@IR_javascript_group
🆔@IR_javascript
👍2
ساخته‌شده توسط هوش مصنوعی با Three.js


کدنویسی وایب (Vibe Coding یا Vibecoding) یک تکنیک برنامه‌نویسی وابسته به هوش مصنوعی است که در آن، فرد مسئله را در چند جمله به‌عنوان یک پرومت برای یک مدل زبانی بزرگ (LLM) آموزش‌دیده در زمینه‌ی کدنویسی توصیف می‌کند. سپس، LLM کد نرم‌افزار را تولید می‌کند و نقش برنامه‌نویس از کدنویسی دستی به هدایت، آزمایش و اصلاح کد تولیدشده توسط هوش مصنوعی تغییر می‌یابد.

حامیان این روش معتقدند که Vibe Coding به برنامه‌نویسان آماتور اجازه می‌دهد بدون نیاز به آموزش و مهارت‌های عمیق مهندسی نرم‌افزار، نرم‌افزار تولید کنند.

🎮 هم‌اکنون بازی کنید:
summer-afternoon.vlucendo.com
🔗https://t.me/HamidrezaKaramianOfficial/2254
#️⃣#tool
👥@IR_javascript_group
🆔@IR_javascript
👍2
معرفی Motion برای Vue

‏Motion یک کتابخانه‌ی محبوب و قدرتمند برای انیمیشن است که بیشتر با React شناخته می‌شود، اما اکنون نسخه‌ی Vue آن نیز عرضه شده و کاملاً مجهز به تمام ویژگی‌ها است.

🔗https://motion.dev/docs/vue
#️⃣#npm_module
👥@IR_javascript_group
🆔@IR_javascript
👍3
اگر یک آرایه دارید که هر عنصر آن شامل یک مقدار computed است، بهتر است یک مقدار computed برای کل آرایه ایجاد کنید تا اینکه برای هر عنصر جداگانه یک مقدار computed داشته باشید.

### نمونه نامناسب
const rows = productRows.map(row => ({ 
...row,
total: computed(() => row.price * row.qty),
}));

### نمونه بهینه
const computedRows = computed(() => 
productRows.map(row => ({
...row,
total: row.price * row.qty,
}))
);

با این روش، محاسبات بهینه‌تر انجام می‌شود و از ایجاد چندین مقدار computed غیرضروری جلوگیری خواهد شد.

#️⃣#tip #vue
👥@IR_javascript_group
🆔@IR_javascript
👍1
ساده‌ترین روش برای نمایش «دیروز» در هر زبانی، استفاده از Relative TimeFormat است!

یکی از ابزارهای موردعلاقه من برای کار با تاریخ، Intl.RelativeTimeFormat است. این ابزار به شما امکان می‌دهد زمان نسبی را به‌صورت زیبا نمایش دهید، مانند: «۵ دقیقه پیش»، «دو روز بعد»، «فردا» — و همه این موارد به‌طور خودکار با توجه به تنظیمات زبانی کاربر انجام می‌شود!

### مثال پایه
فرض کنید می‌خواهیم زمان انتشار یک پست را نمایش دهیم:
const rtf = new Intl.RelativeTimeFormat('fa', { numeric: 'auto' });

console.log(rtf.format(-1, 'day')); // "دیروز"
console.log(rtf.format(2, 'week')); // "دو هفته بعد"

### پارامترهای کلیدی:
‏- 'fa' – زبان موردنظر که می‌توانید آن را به 'en'`، 'fr'، 'ja'` و ... تغییر دهید.
‏- `numeric: 'auto' – باعث می‌شود به‌جای «۱ روز پیش»، عبارت «دیروز» نمایش داده شود. مقدار پیش‌فرض این گزینه 'always' است، که در این صورت `format(-1, 'day') مقدار «۱ روز پیش» را برمی‌گرداند، نه «دیروز».

### پارامترهای متد `format`
✔️ آرگومان اول – عددی که مشخص می‌کند زمان چقدر از لحظه‌ی فعلی فاصله دارد.
✔️ آرگومان دوم – واحد زمانی مانند second`، `minute`، `hour`، `day`، `week`، `month`، `year.

### چند نمونه دیگر:
console.log(rtf.format(-3, 'month')); // "سه ماه پیش"
console.log(rtf.format(1, 'year')); // "سال آینده"

حالا دیگر نیازی به نوشتن توابع دستی برای نمایش «۵ دقیقه پیش» ندارید! می‌توانید به‌سادگی از Intl.RelativeTimeFormat استفاده کنید.

نظر شما درباره‌ی این ابزار چیست؟ تا به حال در پروژه‌هایتان از آن استفاده کرده‌اید؟

#️⃣#tip
👥@IR_javascript_group
🆔@IR_javascript
3🔥3
در هسته‌ی Nuxt قابلیت هیدراسیون تنبل (Lazy Hydration) اضافه شده است—پس دیگر نیازی به ساخت راهکارهای سفارشی نیست! اما بیایید قدم‌به‌قدم بررسی کنیم.

### 🚀 Vue 3.5 و ارتقای SSR
در Vue 3.5 SSR بهبود یافته و حالا کامپوننت‌های غیرهم‌زمان (Async Components) می‌توانند زمان هیدراته شدن خود را از طریق استراتژی hydrate در API `defineAsyncComponent()` کنترل کنند. مثلاً، می‌توان تعیین کرد که یک کامپوننت فقط هنگام نمایش در صفحه هیدراته شود:
import { defineAsyncComponent, hydrateOnVisible } from 'vue';

const AsyncComp = defineAsyncComponent({
loader: () => import('./Comp.vue'),
hydrate: hydrateOnVisible()
});

### 🔥 Nuxt 3.16 و پشتیبانی از Delayed Hydration
📅 در ۷ مارس ۲۰۲۵ نسخه‌ی Nuxt 3.16 منتشر شد که شامل ویژگی‌های جدید، بهبود عملکرد، ابزارهای توسعه‌ی بهتر و... بود، اما مهم‌ترین قابلیت اضافه‌شده، Delayed Hydration است. توسعه‌دهندگان Nuxt در وبلاگ خود جزئیات این قابلیت را توضیح داده‌اند.

#### مثال: هیدراته شدن کامپوننت هنگام ورود به ویوپورت
<!-- کامپوننت تنها زمانی هیدراته می‌شود که در محدوده‌ی دید کاربر قرار بگیرد -->
<LazyExpensiveComponent hydrate-on-visible />

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

🔹 همچنین می‌توان از hook جدید `hydrated` برای اجرای دستورات خاص پس از هیدراته شدن کامپوننت استفاده کرد:
<LazyComponent hydrate-on-visible @hydrated="onComponentHydrated" />

### 📌 چه خبر؟
🔹 این ویژگی تنظیمات مختلفی دارد که می‌توانید نحوه‌ی کارکرد آن را در مستندات Nuxt مطالعه کنید.
🔹 Nuxt به‌سرعت در حال پیشرفت است و وقت آن رسیده که این قابلیت را در محیط Production تست کنیم.
🔹 شاید وقتش رسیده که کامپوننت‌های سمت سرور (Server Components) را هم امتحان کنیم؟ (قابلیت آزمایشی nuxt-islands – اما این بحث را می‌گذاریم برای بعد 😉)

#️⃣#tip #vue
👥@IR_javascript_group
🆔@IR_javascript
👍1
معمولاً برای گروه‌بندی یک آرایه بر اساس یک کلید خاص، از متد reduce استفاده می‌شود.
اما اکنون متدهای جدیدی مانند Object.groupBy و Map.groupBy در دسترس هستند که این فرآیند را بسیار ساده‌تر می‌کنند.

مثال در جاوااسکریپت:

const users = [
{ id: ۱، name: "Alex"، role: "admin" },
{ id: ۲، name: "Anna"، role: "user" },
];

const grouped = Object.groupBy(users, ({ role }) => role);

// نتیجه:
// {
// admin: [{...}],
// user: [{...}]
// }


این روش، کدی خواناتر و ساخت‌یافته‌تر ارائه می‌دهد و مناسبِ زمانی‌ست که بخواهید داده‌ها را بر اساس یک ویژگی مشخص دسته‌بندی کنید، بدون نیاز به نوشتن منطق دستی در reduce.


#️⃣#tip
👥@IR_javascript_group
🆔@IR_javascript
🔥5
This media is not supported in your browser
VIEW IN TELEGRAM
هسته‌ی Jupyter برای javascript

‏Deno به‌صورت پیش‌فرض همراه با یک هسته‌ی Jupyter ارائه می‌شود که امکان نوشتن JavaScript و TypeScript را در محیط‌های تعاملی نوت‌بوک فراهم می‌سازد. در این محیط می‌توانید به‌سادگی از APIهای وب و Deno استفاده کرده و حتی بسته‌های npm را مستقیماً در نوت‌بوک خود وارد (import) کنید.

🔗https://docs.deno.com/runtime/reference/cli/jupyter/
#️⃣#tool
👥@IR_javascript_group
🆔@IR_javascript
🔥1
### چرا باید از استفاده کنترل‌نشده از watch پرهیز کرد؟

#### یک. برهم خوردن جریان واکنشی (Reactive Flow)
استفاده از watch گاهی باعث پنهان شدن منطق واکنش‌گرایی می‌شود. برای مثال:

watch(() => userId.value, async (newId) => {
const data = await fetchUserData(newId);
userData.value = data;
});


در ظاهر ساده است، اما وقتی همین کد در چند مؤلفه با داده‌های مشابه یا اثرات جانبی دیگر تکرار شود، کنترل جریان داده‌ها سخت‌تر شده و رفتار برنامه غیرقابل پیش‌بینی می‌شود.

Vue معماری واکنش‌گرای یک‌طرفه (از داده به UI) را ترویج می‌دهد، اما watch اغلب برای تبدیل داده به داده استفاده می‌شود که جریان را پیچیده می‌کند و اشکال‌زدایی را دشوارتر می‌سازد.

#### دو. کاهش خوانایی و دشواری در نگه‌داری

وقتی در یک مؤلفه چندین watch به‌صورت زنجیره‌ای نوشته شوند، ساختار کد به‌شکل کلاف سردرگمی درمی‌آید که فهم آن زمان‌بر است:

watch(() => settings.value.theme, applyTheme);
watch(() => settings.value.language, loadTranslations);
watch(() => settings.value.notifications, updateNotifications);


این نوع پراکندگی منطق در `watch`ها:

- خوانایی کد را پایین می‌آورد
- بازسازی (refactor) و انتقال منطق به composableها را دشوار می‌کند

در مقابل، می‌توان این رفتارها را در یک computed مجتمع یا composable مشخص و مستقل تعریف کرد.

#### سه. تأثیر منفی بر عملکرد

مثلاً اگر یک شیء عمیق را با deep: true مانیتور کنید:

watch(settings, (newVal, oldVal) => {
// مقایسه عمیق کل ساختار
}, { deep: true });


اگر settings شامل اشیاء یا آرایه‌های بزرگ باشد، عملکرد برنامه افت خواهد کرد. از طرفی، بسیاری از کاربردهای watch را می‌توان با computed حل کرد:

const isDarkTheme = computed(() => settings.value.theme === 'dark');


ویژگی‌های محاسبه‌شونده کش می‌شوند، سبک‌تر هستند و اجرای بهتری دارند.

#### چهار. خطر نشت حافظه (Memory Leak)

در مؤلفه‌هایی که با شرط v-if نمایش داده می‌شوند، اگر watch بدون مدیریت درست ایجاد شود:

<template>
<div v-if="showComponent">
<SomeComponent />
</div>
</template>


و در SomeComponent بنویسیم:

watch(async() => someReactiveValue.value, await doSomething);


اگر این watch هنگام نابودی مؤلفه (unmount) لغو نشود، به‌مرور زمان منابع حافظه را اشغال کرده و باعث نشت حافظه می‌شود.

---

### راهکار پیشنهادی

قبل از استفاده از `watch`، ابتدا این پرسش را از خودتان بپرسید:

> «آیا می‌توان این مسئله را با `computed`، `props` یا emit حل کرد؟»

در بسیاری از موارد پاسخ *بله* است، و انتخاب راه‌حل‌های اعلانی (declarative) باعث خوانایی، عملکرد بهتر و مدیریت ساده‌تر خواهد شد.



#️⃣#tip #vue
👥@IR_javascript_group
🆔@IR_javascript
👍1
سایت pollinations.ai یکی از رابط‌های برنامه‌نویسی (API) مناسب و ساده برای تولید تصاویر با استفاده از هوش مصنوعی است.

### مثال درخواست GET برای دریافت متن:
GET https://text.pollinations.ai/{prompt}


### نمونه استفاده در کد HTML:
<img 
src="https://image.pollinations.ai/prompt/A%20modern%20and%20vibrant%20digital%20illustration%20of%20a%20Telegram%20channel%20called%20%22IR_javascript%22,%20focused%20on%20web%20development,%20JavaScript,%20and%20coding.%20The%20scene%20features%20glowing%20code%20snippets,%20stylized%20browser%20windows,%20Iranian%20cultural%20motifs%20subtly%20integrated%20into%20the%20design,%20and%20futuristic%20UI%20elements.%20The%20background%20includes%20icons%20of%20JavaScript%20(JS),%20HTML,%20and%20CSS,%20with%20a%20dark%20mode%20theme%20and%20a%20sleek%20techy%20look.%20Emphasize%20community,%20learning,%20and%20innovation.%20Perfect%20for%20a%20tech%20advertisement%20poster."
alt="تصویری زیبا"
/>


در تصویر بالا، نمونه‌ای از تولید تصویر توسط مدل DALL-E نسخهٔ سوم نمایش داده شده که از متن داخل بلاک کد (prompt) استفاده کرده است.


#️⃣#tool
👥@IR_javascript_group
🆔@IR_javascript
🔥2
همه‌چیز درباره‌ی favicon
🔻 چرا این مقاله مهمه؟
چون ساخت آیکون برای سایت به یه کابوس تبدیل شده؛ بیش از ۲۰ فایل PNG با اندازه‌های مختلف فقط برای نمایش یه لوگوی ساده؟ واقعاً لازمه؟ نه!
### نسخه‌ی بسیار خلاصه
به‌جای ساخت ده‌ها آیکون، کافی‌ست تنها پنج آیکون و یک فایل JSON ایجاد کنیم.

کد HTML:

<link rel="icon" href="/favicon.ico" sizes="any"><!-- سی‌ودو در سی‌ودو پیکسل -->
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png"><!-- صد و هشتاد در صد و هشتاد پیکسل -->
<link rel="manifest" href="/manifest.webmanifest">


و فایل مانیفست وب:

// manifest.webmanifest
{
"icons": [
{ "src": "/icon-192.png", "type": "image/png", "sizes": "192x192" },
{ "src": "/icon-512.png", "type": "image/png", "sizes": "512x512" }
]
}


همین و بس. اگر مایلید بدانید چطور به این نتیجه رسیدم، ادامه‌ی مقاله را بخوانید.


#️⃣#tip
👥@IR_javascript_group
🆔@IR_javascript
### مجموعه ایده‌آل فاوآیکون‌ها

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

- مرورگرها فاوآیکون‌ها را در پس‌زمینه بارگذاری می‌کنند، بنابراین اندازه بزرگ فایل فاوآیکون تأثیری بر عملکرد وب‌سایت ندارد

‏- SVG روشی عالی برای کاهش اندازه آیکون‌ها برای تصاویری است که نباید به‌صورت شطرنجی باشند؛ برای بسیاری از لوگوها، فایل نهایی بسیار کوچک‌تر از PNG خواهد بود.

با داشتن تنها سه فایل PNG، می‌توان آن‌ها را با دقت بیشتری بهینه‌سازی کرد و با استفاده از تنظیمات مناسب در ابزارهای پیشرفته، کیفیت را حفظ نمود. این کار مشکل کاربرانی را که برای هر مگابایت اینترنت هزینه می‌پردازند، حل می‌کند.

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

‏### favicon.ico برای مرورگرهای قدیمی

فایل‌های ICO می‌توانند چندین نسخه از تصویر با اندازه‌های مختلف را در خود جای دهند. توصیه می‌کنم از یک تصویر با اندازه ۳۲×۳۲ پیکسل استفاده کنید. نسخه ۱۶×۱۶ را تنها در صورتی ایجاد کنید که لوگو در این اندازه وضوح خود را از دست ندهد یا طراح شما بتواند آن را به‌صورت دستی برای این اندازه بازطراحی کند.

بهتر است این فایل را در مسیر دقیق https://example.com/favicon.ico نگهداری کنید، بدون استفاده از مسیرهای پیچیده. برخی ابزارها، مانند خوانندگان RSS، مستقیماً /favicon.ico را از سرور درخواست می‌کنند و به دنبال آن در مکان دیگری نمی‌گردند.

برای رفع اشکال Chrome که به‌جای SVG، فایل ICO را انتخاب می‌کند، به sizes="any" برای تگ <link> مربوط به فایل .ico نیاز داریم.

### یک آیکون SVG با نسخه‌های روشن/تاریک برای مرورگرهای مدرن

SVG یک فرمت برداری است که به‌جای پیکسل‌ها، منحنی‌ها را توصیف می‌کند. در اندازه‌های بزرگ، این روش کارآمدتر از تصاویر شطرنجی است. در زمان نگارش این مقاله، ۷۲٪ از مرورگرها از آیکون‌های SVG پشتیبانی می‌کنند.

صفحه HTML شما باید دارای تگ <link> در بخش <head> با rel="icon"`، `type="image/svg+xml" و href باشد که به فایل SVG با این ویژگی‌ها اشاره می‌کند

‏SVG یک فرمت XML است و می‌تواند تگ <style> برای توصیف CSS داشته باشد. مانند هر CSS دیگری، می‌تواند شامل مدیا کوئری‌هایی مانند @media (prefers-color-scheme: dark) باشد. این امکان را می‌دهد که همان آیکون بین تم‌های روشن و تاریک سیستم تغییر کند.

### تصویر PNG با اندازه ۱۸۰×۱۸۰ برای دستگاه‌های اپل

آیکون Apple touch تصویری برای صفحه اصلی iPhone یا iPad است. در صفحه HTML شما باید تگی با <link rel="apple-touch-icon" href="apple-touch-icon.png"> در بخش <head> وجود داشته باشد.

از iOS 8 به بعد، برای iPad به تصویری با رزولوشن ۱۸۰×۱۸۰ نیاز است. دستگاه‌های قدیمی‌تر آن را به‌صورت خودکار مقیاس‌دهی می‌کنند.

نکته‌ای کوچک: آیکون Apple touch بهتر به نظر می‌رسد اگر حاشیه‌ای به اندازه ۲۰ پیکسل داشته باشد و رنگ پس‌زمینه‌ای اضافه شود. برای این کار می‌توانید از هر ویرایشگر گرافیکی استفاده کنید.

### مانیفست وب‌اپلیکیشن با آیکون‌های PNG ۱۹۲×۱۹۲ و ۵۱۲×۵۱۲ برای دستگاه‌های اندروید

- مانیفست وب‌اپلیکیشن یک فایل JSON است که تمامی جزئیات لازم برای نصب وب‌سایت شما به‌عنوان یک اپلیکیشن سیستمی را فراهم می‌کند. این فرمت توسط گوگل در چارچوب ابتکار PWA معرفی شده است.

- صفحه HTML شما باید تگی با <link rel="manifest" href="manifest.webmanifest"> داشته باشد که به فایل مانیفست اشاره می‌کند.
- در مانیفست باید فیلد icons وجود داشته باشد که به دو آیکون اشاره می‌کند: ۱۹۲×۱۹۲ برای نمایش در صفحه اصلی و ۵۱۲×۵۱۲ که به‌عنوان صفحه بارگذاری PWA استفاده می‌شود


{
"icons": [
{ "src": "/icon-192.png", "type": "image/png", "sizes": "192x192" },
{ "src": "/icon-512.png", "type": "image/png", "sizes": "512x512" }
]
}



#️⃣#tip
👥@IR_javascript_group
🆔@IR_javascript
«واترمارک» در سال ۲۰۲۵

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

امروز به شما نشان خواهم داد که چگونه به راحتی و به سرعت می‌توان یک واترمارک برای سایت خود با استفاده از SVG و جاوااسکریپت ایجاد کرد.

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

اگرچه HTML و CSS استاندارد باقی می‌مانند، این ویژگی ایجاد واترمارک است که توجه ویژه‌ای را می‌طلبد.

کد تابع ایجاد واترمارک SVG:

 function generateWatermarkSVG(text) {
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="220" height="100">
<text x="15" y="70"
transform="rotate(-15, 10, 10)"
fill="rgba(60, 90, 150, 0.16)"
font-size="18"
font-family="sans-serif">
${text}
</text>
</svg>
`.trim();

const encodedSvg = new TextEncoder().encode(svg);
return `data:image/svg+xml;base64,${btoa(String.fromCharCode(...encodedSvg))}`;
}

window.addEventListener("DOMContentLoaded", function() {
const watermarkText = "@IR_javascript";
const container = document.getElementById('watermark-container');

const encodedSvg = generateWatermarkSVG(watermarkText);
container.style.setProperty('background-image', `url(${encodedSvg})`);
});


چگونه کد کار می‌کند؟
ساختار SVG:
داخل رشته SVG یک عنصر SVG ایجاد می‌شود که متنی را نمایش می‌دهد، این متن با ویژگی‌های HTML از جمله موقعیت، چرخش و شفافیت تنظیم می‌شود؛
• کدگذاری در Base64:
برای استفاده از این SVG به عنوان پس‌زمینه، از کدگذاری Base64 استفاده می‌شود. این امکان را فراهم می‌کند که تصویر مستقیماً در CSS گنجانده شود، بدون نیاز به بارگذاری فایل جداگانه؛
• روش btoa:
تابع btoa() رشته را به فرمت Base64 تبدیل می‌کند. با این حال، برای کار درست با نویسه‌های سیریلیک و دیگر نمادهای غیر-ASCII، باید از روشی با استفاده از TextEncoder بهره گرفت. این روش به جلوگیری از مشکلات کدگذاری کمک خواهد کرد.



#️⃣#tip
👥@IR_javascript_group
🆔@IR_javascript
رابط برنامه‌نویسی View Transitions
نوآوری تازه‌ای است که امکان ایجاد انیمیشن‌های نرم و طبیعی بین حالت‌های مختلف یک صفحه را به سادگی فراهم می‌کند.

اکنون دیگر نیازی نیست تا انیمیشن‌های پیچیده CSS یا JavaScript را به‌صورت دستی کدنویسی کنید — مرورگر به‌صورت خودکار، گذار روانی بین وضعیت قبلی و وضعیت جدید ایجاد می‌کند. این کار باعث صرفه‌جویی در زمان شده و رابط‌های کاربری را دلپذیرتر و حرفه‌ای‌تر جلوه می‌دهد.

### نمونه‌ای ساده:

document.startViewTransition(() => 
updateTheDOMSomehow());


در اینجا متد
document.startViewTransition()
فراخوانی می‌شود و یک تابع که شامل تغییرات در DOM است به آن ارسال می‌گردد. ادامه‌ی فرآیند توسط مرورگر انجام می‌شود.

### این فرآیند چگونه عمل می‌کند؟
مرورگر ابتدا وضعیت فعلی صفحه را «ذخیره» می‌کند، سپس تغییرات را اعمال کرده و وضعیت قبلی و جدید را با یکدیگر مقایسه می‌کند. در نهایت، خود مرورگر عملیات انیمیشن بین این دو حالت را مدیریت می‌نماید.

آنچه این ویژگی را بسیار جذاب‌تر می‌سازد، امکان اتصال مستقیم انیمیشن‌های CSS از طریق شبه‌المان‌هایی مانند
::view-transition-old و ::view-transition-new
است. این قابلیت، انعطاف‌پذیری بیشتری را فراهم می‌کند: برای نمونه می‌توان برای حالت قبلی افکت محو شدن و برای حالت جدید افکت بزرگ‌نمایی در نظر گرفت.


🔗https://codepen.io/katrin_profrontend/pen/OPJqBOx
#️⃣#tip
👥@IR_javascript_group
🆔@IR_javascript
کلون‌کردن عمیق اشیاء واکنش‌پذیر در Vue 3

Vue 3 از *Proxy* برای واکنش‌پذیری استفاده می‌کند، که این مسئله زمانی که بخواهید اشیاء را کلون کنید، مشکلاتی ایجاد می‌کند. روش‌های استاندارد به صورت مورد انتظار عمل نمی‌کنند:

const state = reactive({ user: { name: "Al" } });
// مشکلات:
const badCopy1 = { ...state }; // حفظ ارجاع به Proxy
const badCopy2 = JSON.parse(JSON.stringify(state)); // از دست دادن متدها و Proxy


استفاده از عملگر `...` یا مشابه آن:
اگر شما از عملگر ... برای کپی کردن یک شیء واکنش‌پذیر استفاده کنید، این کپی فقط ارجاع به *Proxy* اصلی را می‌گیرد، نه خود داده‌ها. به عبارت دیگر، تغییرات در شیء جدید می‌تواند بر شیء اصلی تأثیر بگذارد.


استفاده از `JSON.stringify` و `JSON.parse`:
وقتی از JSON.stringify برای تبدیل شیء به یک رشته و سپس از JSON.parse برای بازگشایی آن استفاده می‌کنید، متدهای شیء واکنش‌پذیر از بین می‌روند و همچنین ارتباط با *Proxy* قطع می‌شود. این به این معناست که ویژگی‌های واکنش‌پذیری از دست می‌روند و این کپی دیگر به صورت واکنش‌پذیر نخواهد بود.


### چرا این اتفاق می‌افتد؟
Vue 3 از *Proxy* برای ایجاد واکنش‌پذیری استفاده می‌کند. *Proxy* در واقع یک "نظارت‌کننده" بر روی اشیاء است که هر تغییر در داده‌ها را شبیه‌سازی می‌کند و به Vue اطلاع می‌دهد تا رابط کاربری به‌روز شود. زمانی که شما از روش‌های استاندارد کپی استفاده می‌کنید، این ویژگی‌های *Proxy* نادیده گرفته می‌شوند و به همین دلیل نمی‌توانید از کپی‌های انجام‌شده به‌طور کامل استفاده کنید.

سه روش کاربردی برای این کار:

1. ترکیب toRaw و structuredClone

import { toRaw } from 'vue';
const original = reactive({ data: 123 });
const copy = structuredClone(toRaw(original));


2. کپی‌کردن عمیق دستی

function deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj;
const clone = Array.isArray(obj) ? [] : {};
for (const key in obj) {
clone[key] = deepClone(obj[key]);
}
return clone;
}
const copy = reactive(deepClone(toRaw(original)));


3. استفاده از کتابخانه‌های موجود

import { cloneDeep } from 'lodash-es';
const copy = reactive(cloneDeep(toRaw(obj)));


این روش‌ها به شما کمک می‌کنند تا اشیاء واکنش‌پذیر را به طور صحیح کلون کنید بدون اینکه ارجاع به *Proxy* حفظ شود یا متدهای آن از بین بروند.

#️⃣#tip #vue
👥@IR_javascript_group
🆔@IR_javascript
👍2
در این پست، API جدیدی با نام effectScope() برای پکیج @vue/reactivity معرفی می‌شود. یک نمونه از EffectScope می‌تواند به‌صورت خودکار، افکت‌هایی را که درون یک تابع هم‌زمان اجرا می‌شوند، گردآوری کند تا در زمان مناسب همگی با هم حذف شوند.

در تابع setup() درون یک کامپوننت Vue، افکت‌ها به‌صورت خودکار جمع‌آوری شده و به همان نمونه‌ی کامپوننت متصل می‌شوند. هنگام از بین رفتن کامپوننت، این افکت‌ها نیز به‌طور خودکار حذف می‌شوند. این ویژگی، ساده و شهودی است.

اما هنگامی که این افکت‌ها را خارج از کامپوننت‌ها یا در قالب یک پکیج مستقل استفاده کنیم، کار به این سادگی نیست. برای نمونه، حذف افکت‌های computed و watch به صورت دستی به شکل زیر خواهد بود:
const disposables = []

const counter = ref(۰)
const doubled = computed(() => counter.value * ۲)

disposables.push(() => stop(doubled.effect))

const stopWatch۱ = watchEffect(() => {
console.log(`counter: ${counter.value}`)
})

disposables.push(stopWatch۱)

const stopWatch۲ = watch(doubled, () => {
console.log(doubled.value)
})

disposables.push(stopWatch۲)


و برای حذف آن‌ها:
disposables.forEach((f) => f())
disposables = []


به‌ویژه در کدهای ترکیبی طولانی، جمع‌آوری دستی افکت‌ها کاری زمان‌بر و مستعد خطاست. فراموش‌کردن این کار یا نداشتن دسترسی به افکت‌های ایجادشده، می‌تواند منجر به نشتی حافظه یا رفتارهای غیرمنتظره شود.

اما با وجود EffectScope بصورت زیر میشود
// effect، computed، watch، و watchEffect که درون این scope ساخته می‌شوند، جمع‌آوری خواهند شد

const scope = effectScope()

scope.run(() => {
const doubled = computed(() => counter.value * ۲)

watch(doubled, () => console.log(doubled.value))

watchEffect(() => console.log('تعداد: ', doubled.value))
})

// برای حذف همه افکت‌ها در این scope
scope.stop()





‏### Scopeهای تودرتو

‏Scopeهای تودرتو نیز باید توسط scope والد جمع‌آوری شوند. زمانی که scope والد حذف شود، همه‌ی scopeهای فرزند نیز حذف خواهند شد:

const scope = effectScope()

scope.run(() => {
const doubled = computed(() => counter.value * ۲)

effectScope().run(() => {
watch(doubled, () => console.log(doubled.value))
})

watchEffect(() => console.log('تعداد: ', doubled.value))
})

// حذف تمام افکت‌ها، از جمله موارد در scopeهای تودرتو
scope.stop()


---

‏### Scopeهای جداشده (Detached)

پارامتر detached در effectScope اجازه می‌دهد تا scope به‌صورت جدا از scope والد ایجاد شود. این ویژگی برای سناریوهایی مانند «مقداردهی اولیه تنبل» (Lazy Initialization) مفید است.

let nestedScope

const parentScope = effectScope()

parentScope.run(() => {
const doubled = computed(() => counter.value * ۲)

nestedScope = effectScope(true /* detached */)
nestedScope.run(() => {
watch(doubled, () => console.log(doubled.value))
})

watchEffect(() => console.log('تعداد: ', doubled.value))
})

// فقط scope والد حذف می‌شود
parentScope.stop()

// scope تودرتو را در زمان مناسب حذف می‌کنیم
nestedScope.stop()


---

### تابع onScopeDispose

تابع onScopeDispose عملکردی مشابه onUnmounted دارد، اما برای scope فعلی (نه نمونه کامپوننت). این قابلیت به فانکشن‌های ترکیبی کمک می‌کند تا اثرات جانبی خود را همراه با scope مربوطه پاک‌سازی کنند.

import { onScopeDispose } from 'vue'

const scope = effectScope()

scope.run(() => {
onScopeDispose(() => {
console.log('پاک‌سازی شد!')
})
})

scope.stop() // چاپ: پاک‌سازی شد!


---

### دریافت scope فعلی

‏API جدید getCurrentScope() برای دریافت scope فعلی معرفی شده است:

import { getCurrentScope } from 'vue'

getCurrentScope() // EffectScope | undefined


🔗https://github.com/vuejs/rfcs/blob/master/active-rfcs/0041-reactivity-effect-scope.md
#️⃣#tip #vue
👥@IR_javascript_group
🆔@IR_javascript
### مثال‌ها

#### مثال الف: استفاده‌ی مشترک از composable

تابع useMouse() یک نمونه‌ی خوب برای ایجاد اثرات جانبی جهانی است:

function useMouse() {
const x = ref(۰)
const y = ref(۰)

function handler(e) {
x.value = e.x
y.value = e.y
}

window.addEventListener('mousemove', handler)

onUnmounted(() => {
window.removeEventListener('mousemove', handler)
})

return { x, y }
}


اگر useMouse() در چند کامپوننت فراخوانی شود، هرکدام یک listener جدید ایجاد کرده و refs جداگانه خواهند داشت. برای جلوگیری از این کار پرهزینه، می‌توانیم از scope جداشده و onScopeDispose استفاده کنیم:

ابتدا:

- onUnmounted(() => {
+ onScopeDispose(() => {
window.removeEventListener('mousemove', handler)
})


سپس یک تابع کمکی برای مدیریت اشتراک ایجاد می‌کنیم:

function createSharedComposable(composable) {
let subscribers = ۰
let state, scope

const dispose = () => {
if (scope && --subscribers <= ۰) {
scope.stop()
state = scope = null
}
}

return (...args) => {
subscribers++
if (!state) {
scope = effectScope(true)
state = scope.run(() => composable(...args))
}
onScopeDispose(dispose)
return state
}
}


اکنون:

const useSharedMouse = createSharedComposable(useMouse)


این نسخه از useMouse تنها یک بار listener را اضافه می‌کند و در صورت عدم نیاز، آن را حذف می‌نماید.

---

#### مثال ب: Scopeهای موقتی (Ephemeral)

export default {
setup() {
const enabled = ref(false)
let mouseState, mouseScope

const dispose = () => {
mouseScope && mouseScope.stop()
mouseState = null
}

watch(
enabled,
() => {
if (enabled.value) {
mouseScope = effectScope()
mouseState = mouseScope.run(() => useMouse())
} else {
dispose()
}
},
{ immediate: true }
)

onScopeDispose(dispose)
},
}


در اینجا scopeها به‌صورت موقتی ساخته و حذف می‌شوند. onScopeDispose اطمینان حاصل می‌کند که useMouse به‌درستی پاک‌سازی شود، درحالی‌که onUnmounted در این فرآیند فراخوانی نمی‌شود.

---

### تأثیر در هسته Vue

در حال حاضر، در @vue/runtime-dom توابع computed بازنویسی می‌شوند تا به نمونه‌ی کامپوننت متصل شوند. بنابراین موارد زیر معادل نیستند:

// متفاوت
import { computed } from '@vue/reactivity'
import { computed } from 'vue'


با پیاده‌سازی این RFC، دیگر نیازی به بازنویسی computed نخواهد بود و vue می‌تواند مستقیماً از نسخه‌ی @vue/reactivity استفاده کند.


🔗https://github.com/vuejs/rfcs/blob/master/active-rfcs/0041-reactivity-effect-scope.md
#️⃣#tip #vue
👥@IR_javascript_group
🆔@IR_javascript
درک تفاوت‌ها: File در برابر Blob
گاهی اوقات در کد به موقعیت‌هایی برمی‌خوریم که به‌دلیل درک نادرست از تفاوت میان Blob و File**، ابهاماتی به‌وجود می‌آید. سپس این پرسش‌ها مطرح می‌شوند:
«چرا نام فایل منتقل نمی‌شود؟»
«چرا تاریخ وجود ندارد؟»
یا «چطور می‌توان این را به‌درستی با FormData ارسال کرد؟»
زمان آن رسیده که موضوع را روشن کنیم!

### Blob چیست؟
**Blob
ساختاری داده‌ای است که یک شیء تغییرناپذیر شامل داده‌های دودویی را نمایش می‌دهد.

const blob = new Blob(["Hello, world!"], { type: "text/plain" });


نکات مهم:
- می‌تواند هر نوع داده‌ای را در خود جای دهد: تصویر، متن، ویدیو و ...
- اطلاعاتی درباره‌ی منبع فایل در خود ندارد؛
- فاقد نام (name) و تاریخ آخرین تغییر (lastModified) است؛
- اما می‌توان به اندازه (size) و نوع (type) آن دسترسی داشت.

کاربردهای رایج Blob:
- تولید فایل‌ها به‌صورت «لحظه‌ای» (مثلاً تولید فایل CSV یا JSON از رابط کاربری)
- ایجاد URL با استفاده از URL.createObjectURL()
- نگهداری موقت داده‌ها در حافظه

جمع‌بندی:
Blob روشی عمومی و انعطاف‌پذیر برای نمایش هرگونه داده‌ی «خام» در یک وب‌اپلیکیشن است.
در بسیاری از عملیات مربوط به محتوای دودویی در وب، پایه و اساس را Blob تشکیل می‌دهد.

---

### File چیست؟
File در واقع نسخه‌ی توسعه‌یافته‌ی Blob به همراه فراداده (metadata) است.

const file = new File(["Hello, world!"], "greeting.txt", {
type: "text/plain",
lastModified: Date.now()
});


نکات مهم:
- از Blob ارث‌بری می‌کند؛ یعنی تمام قابلیت‌های Blob را داراست؛
- افزوده‌هایی دارد مانند: name و lastModified;
- شیء File همان چیزی است که هنگام انتخاب فایل توسط کاربر از طریق <input type="file"> به‌دست می‌آید.

کاربردهای رایج File:
- مدیریت فایل‌هایی که کاربر در اختیار برنامه قرار می‌دهد؛
- خواندن محتوای فایل با FileReader
- ارسال فایل به سرور
- پیش‌نمایش فایل در مرورگر

---

### نتیجه‌گیری نهایی:
🔹 اگر فقط به یک جریان داده‌ی دودویی نیاز دارید: Blob را انتخاب کنید.
🔹 اگر نیاز به شیئی دارید که همانند یک فایل رفتار کند: File انتخاب بهتری است.

و همین!

#️⃣#tip
👥@IR_javascript_group
🆔@IR_javascript
👍2
مهاجرت مایکروسافت به موتور کرومیوم یک تصمیم بسیار هوشمندانه بود.

در آغوش بگیر، توسعه بده، و حذف کن — راهبردی که هنوز هم کار می‌کند.
#️⃣#tool
👥@IR_javascript_group
🆔@IR_javascript