به عنوان یک فرانتاند دولوپر زیاد پیش میاد داخل یک وبسایت قسمتی وجود داشته باشه که کاربر بخواد تصویری رو آپلود بکنه (مثلا آپلود تصویر پروفایل)؛ یکی از اقدامات رایج جهت بهبود تجربهی کاربر اینه که وقتی تصویری رو از روی سیستمش انتخاب کرده بهش پیشنمایشی از اون تصویر رو نشون بدیم تا ببینه چیزی که انتخاب کرده چیه و اگه اشتباه بود سریعا متوجه بشه. غیر از بحث تجربه کاربر، نشون دادن تصویر انتخاب شدهی کاربر پیش از آپلود یا ذخیره (به جای یه دکمهی ساده یا فقط نمایش نام فایل) باعث میشه اون آپلودر و یا فایل سلکتوری که استفاده شده از نظر ظاهری هم جذاب تر باشه.
اگه تا به حال تجربهی پیادهسازی چنین چیزی رو داشته باشین طبیعتا میدونین که نمیشه عکس انتخابی کاربر رو مستقیما در تگ img مربوط به پیشنمایش استفاده کنید چرا که تگ img نیاز داره یک URL (و یا Data URL) از عکس رو داشته باشه تا قادر به نمایش اون عکس باشه ولی input ما عکس رو با فرمت File بهمون میده. همچنین به دلایل امنیتی، مرورگر امکان دسترسی به فایل های سیستم کاربر رو به وبسایت نمیده پس نمیشه آدرس لوکال فایل رو برای این منظور استفاده کرد.
پس ما برای اینکه بتونیم پیشنمایش یک تصویر رو نشون بدیم به نحوی یک URL و یا Data URL برای فایل عکس ایجاد کنیم و اون رو به وسیلهی تگ img نمایش بدیم. برای این منظور دو تا روش هست که میخوام یه خلاصهای از هر دو روش بگم و با هم مقایسهشون کنم تا ببینیم بهتره از کدوم روش استفاده کنیم: ۱- استفاده از کلاس FileReader و ۲- استفاده از کلاس URL
اولین روشی که فک میکنم رایج ترین روش هم باشه استفاده از کلاس FileReader جاوااسکریپته. این کلاس با استفاده از متدهایی که دراختیارمون قرار میده قادره فایل ها رو با فرمت مد نظر ما بخونه (مثلا Text, Binary, ArrayBuffer و DataURL) و برامون خروجی بده.
تو این مورد ما میتونیم از متد readAsDataURL این کلاس استفاده کنیم تا فایل رو برامون درقالب DataURL خروجی بده و بعد ما میتونیم از خروجی اون برای تگ img استفاده کنیم. یه کد ساده از نحوهی پیادهسازی این روش نوشتم که توی عکس میتونید ببینید:
همونطور که داخل عکس مشخصه این متد به صورت async کار میکنه (یعنی با کال کردن متد readAsDataURL بلافاصله جواب رو به ما برنمیگردونه) علتش هم اینه که این متد میاد کل فایل رو میخونه و طبیعتا زمانی که میبره با توجه به حجم فایل متغیره. کلاسهایی که متدهاشون به صورت async عمل میکنن معمولا یا از Promise ها برای برگردوندن خروجی استفاده میکنن و یا ایونتهایی برای نتیجههای مختلفشون تعریف میکنن. تو این مورد هم برای گرفتن خروجی متد، لازمه که از ایونت load مخصوص این کلاس استفاده کنیم تا وقتی عمل خوندن فایل تموم شد بتونیم نتیجه رو بگیریم. این ایونت رو هم میشه مشابه عکس با استفاده از پراپرتی استفاده کرد و هم میشد مثلا روشی که برای input پیاده کردم از addEventListener استفاده کنیم.
خب نهایتا با این روش ما تونستیم یه پیش نمایش از عکس انتخابی کاربر ایجاد کنیم. اما این روش یه مشکل مهم داره، اونم اینه که که برای تولید DataURL لازمه کل فایل read بشه توسط کلاس FileReader و این عمل با توجه به حجم فایل ما میتونه زمانبر باشه (مثلا اگه عکس انتخابی حجمش زیاد باشه چند ثانیه طول بکشه تا پیشنمایش تولید بشه). همچنین از نظر پرفورمنس هم این روش خیلی بهینه نیست (البته با توجه به هدف که تولید یه پیش نمایش سادهاست).
یه روش دومی هم هست که از نظر زمان و پرفورمنس خیلی بهتر از کلاس FileReader برای این مقصود عمل میکنه.
جاوااسکریپت یه کلاس دیگهای تحت عنوان URL داره که برای تولید، parse کردن و تغییر دادن URL ها استفاده میشه معمولا. اما این کلاس یک متد استاتیک (یعنی بدون ساختن شی و new کردن قابل استفاده است) هم داره که تو این مورد میتونیم ازشون برای تولید پیشنمایش عکسمون استفاده کنیم.
متد استاتیک createObjectURL که روی کلاس URL تعریف شده به ما این امکان رو میده که برای یک فایل، یک آدرس URL موقت ایجاد کنیم و از اون آدرس مشابه یک لینک مستقیم به اون فایل استفاده کنیم.
نحوهی استفادهش رو تو این عکس میتونید ببینید:
همونطور که توی عکس واضحه، هم از نظر حجم کد خلاصه و مفیدتره و هم اینکه این متد برخلاف FileReader async اجرا نمیشه و در لحظه خروجی میده. علتش هم اینه که این تابع نیازی به read کردن فایل نداره و صرفا یه لینک موقت و یکتا واسه فایل ایجاد میکنه (از نوع blob) و اینجوری دیگه وابسته به حجم فایل هم نیست. در مجموع این روش نسبت به روش قبل سادهتر و سریعتر هستش.
این تابع اینه که با هربار کال کردنش یه URL جدید تولید میکنه، یعنی اگه برای یک فایل ده بار این تابع رو کال کنیم هربار یه لینک جدید خروجی میده.
یه نکتهی مهم که لازمه در نظر بگیرید اینه که عمر این لینکها هم وابسته به document هستش و تا وقتی که document از بین نرفته (یعنی unload نشده) این آدرس های تولید شده پابرجا میمونن و طبیعتا یه ذره از حافظه رو هم برای خودشون اشغال میکنن. این معنیش این میشه که لازمه خود ما وقتی یه آدرسی رو دیگه نیاز نداشتیم به صورت دستی آزاد کنیم تا یه موقع دچار memory leak نشیم.
برای آزاد کردن آدرسهای بلااستفاده یه متد استاتیک دیگه روی کلاس URL وجود داره به نام revokeObjectURL که یدونه از اون Object URL های تولید شده رو میگیره و اون رو حذف میکنه. حالا کی باید ازش استفاده کنیم؟ در حالت سادهش اگه فقط میخوایم یه پیشنمایش ساده از یک فایل داشته باشیم میتونی روی ایونت load مربوط به تگ img اینکار رو بکنیم. یعنی به تگ img میگیم وقتی این عکس رو لود کردی و نشون دادی بیا لینکشو هم revoke کن. تو عکس ببینید:
یا هم اگه هدفی علاوه بر نمایش دادن داریم (مثلا میخوایم قابل دانلود هم باشه یا نیازه چند جا استفاده بشه) میتونیم هر موقع که خودمون صلاح دونستیم و دیدم دیگه لازم نیست اون رو revoke کنیم (مثلا اگه از فریمورکهایی مثل react یا vue استفاده میکنیم تو چرخهی destroy شدن کامپوننتمون اینکارو بکنیم).
فقط این نکته رو دوباره تاکید کنم که حتی وقتی برای یک فایل یکسان تابع createObjectURL رو کال کنیم هربار آدرس جدیدی تولید میکنه، پس اگه میخوایم آدرس تولید شده برای یک فایل رو revoke کنیم لازمه که اون آدرس رو جایی ذخیره کنیم و همونو به تابع revokeObjectURL بدیم (مشابه عکس که imageEl.src رو برای این کار استفاده کردم).
پینوشت: برای این که خودم خودم رو ترغیب کنم بیشتر مطلب بنویسم سعی کردم تو نوشتن این پست سخت نگیرم و بدون وسواس اینکارو بکنم، برای همین احتمال اینکه اشتباهی داشته باشم یا همه چیز رو پوشش نداده باشم هست. هر جا موردی بود خوشحال میشم کامنت بذارین و نظرتون رو بگین.