مقدمه
در این مقاله قصد دارم به شما یاد بدهم که چگونه مقداری جاوا اسکریپت را در مرورگر بارگذاری کنید و آن را اجرا کنید.
نه صبر کن برگرد! می دانم که ساده و پیش پا افتاده به نظر می رسد، اما به یاد داشته باشید، این اتفاق در مرورگر رخ می دهد، جایی که ساده از نظر تئوری تبدیل به یک حفره عجیب و غریب می شود. دانستن این ویژگیها به شما این امکان را میدهد تا سریعترین و کم اختلالترین راه را برای بارگیری اسکریپتها انتخاب کنید. اگر در برنامه فشرده هستید، به مرجع سریع بروید.
برای شروع، در اینجا این است که مشخصات راههای مختلفی را که یک اسکریپت میتواند دانلود و اجرا کند، تعریف میکند:
مانند تمام مشخصات WHATWG، در ابتدا مانند عواقب یک بمب خوشه ای در یک کارخانه اسکربل به نظر می رسد، اما هنگامی که آن را برای پنجمین بار خواندید و خون را از چشمان خود پاک کردید، در واقع بسیار جالب است:
اولین فیلمنامه من شامل
<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>
آه، سادگی مبارک. در اینجا مرورگر هر دو اسکریپت را به صورت موازی دانلود کرده و در اسرع وقت با حفظ ترتیب آنها را اجرا می کند. "2.js" تا زمانی که "1.js" اجرا نشود (یا انجام نشود) اجرا نمی شود، "1.js" تا زمانی که اسکریپت یا شیوه نامه قبلی اجرا نشود و غیره اجرا نمی شود.
متأسفانه، مرورگر رندر بیشتر صفحه را در حالی که همه این اتفاق میافتد مسدود میکند. این به دلیل APIهای DOM از «دوران اول وب» است که اجازه میدهد رشتههایی به محتوایی که تجزیهکننده در حال جویدن است، مانند document.write
اضافه شود. مرورگرهای جدیدتر به اسکن یا تجزیه سند در پسزمینه ادامه میدهند و بارگیریها را برای محتوای خارجی که ممکن است به آن نیاز داشته باشد (js، تصاویر، css و غیره) راهاندازی میکنند، اما رندر همچنان مسدود است.
به همین دلیل است که بهترین و خوبی های دنیای عملکرد توصیه می کنند عناصر اسکریپت را در انتهای سند خود قرار دهید، زیرا تا حد امکان محتوای کمتری را مسدود می کند. متأسفانه به این معنی است که اسکریپت شما تا زمانی که تمام HTML شما را دانلود نکند توسط مرورگر دیده نمی شود و تا آن لحظه شروع به دانلود محتوای دیگر مانند CSS، تصاویر و iframes می کند. مرورگرهای مدرن به اندازه کافی هوشمند هستند که به جاوا اسکریپت بر تصاویر اولویت دهند، اما ما می توانیم بهتر عمل کنیم.
با تشکر IE! (نه، من طعنه آمیز نیستم)
<script src="//other-domain.com/1.js" defer></script>
<script src="2.js" defer></script>
مایکروسافت این مشکلات مربوط به عملکرد را تشخیص داد و "تعویق" را در اینترنت اکسپلورر 4 معرفی کرد. این اساساً می گوید: "من قول می دهم با استفاده از چیزهایی مانند document.write
موارد را به تجزیه کننده تزریق نکنم. اگر من این قول را زیر پا بگذارم، شما آزادید که مرا به هر نحوی که صلاح می دانید مجازات کنید.» این ویژگی آن را به HTML4 تبدیل کرد و در مرورگرهای دیگر ظاهر شد.
در مثال بالا، مرورگر هر دو اسکریپت را به صورت موازی دانلود کرده و درست قبل از روشن شدن DOMContentLoaded
اجرا می کند و نظم آنها را حفظ می کند.
مانند یک بمب خوشه ای در کارخانه گوسفند، «تاخیر» به یک آشفتگی پشمی تبدیل شد. بین ویژگیهای «src» و «defer» و تگهای اسکریپت در مقابل اسکریپتهای اضافهشده بهصورت پویا، ما 6 الگوی اضافه کردن یک اسکریپت داریم. البته مرورگرها در مورد دستوری که باید اجرا کنند به توافق نرسیدند. موزیلا در سال 2009 یک قطعه عالی در مورد این مشکل نوشت .
WHATWG این رفتار را به صراحت بیان کرد و اعلام کرد که «به تعویق انداختن» تأثیری بر اسکریپتهایی که به صورت پویا اضافه شدهاند یا فاقد «src» هستند، ندارد. در غیر این صورت، اسکریپتهای معوق باید پس از تجزیه سند، به ترتیبی که اضافه شدهاند، اجرا شوند.
با تشکر IE! (باشه الان دارم طعنه میزنم)
می دهد، می برد. متأسفانه یک باگ بد در IE4-9 وجود دارد که می تواند باعث شود اسکریپت ها به ترتیب غیرمنتظره ای اجرا شوند . این چیزی است که اتفاق می افتد:
1.js
console.log('1');
document.getElementsByTagName('p')[0].innerHTML = 'Changing some content';
console.log('2');
2.js
console.log('3');
با فرض وجود پاراگراف در صفحه، ترتیب مورد انتظار گزارشها [1، 2، 3] است، اگرچه در IE9 و پایینتر [1، 3، 2] را دریافت میکنید. عملیات DOM خاص باعث می شود IE اجرای اسکریپت فعلی را متوقف کند و اسکریپت های معلق دیگر را قبل از ادامه اجرا کند.
با این حال، حتی در پیادهسازیهای بدون اشکال، مانند IE10 و سایر مرورگرها، اجرای اسکریپت تا زمانی که کل سند دانلود و تجزیه شود به تأخیر میافتد. اگر به هر حال میخواهید منتظر DOMContentLoaded
باشید، این میتواند راحت باشد، اما اگر میخواهید در عملکرد واقعاً تهاجمی باشید، میتوانید زودتر شروع به اضافه کردن شنوندگان و راهاندازی کنید…
HTML5 برای نجات
<script src="//other-domain.com/1.js" async></script>
<script src="2.js" async></script>
HTML5 یک ویژگی جدید به ما داد، "async"، که فرض میکند شما از document.write
استفاده نمیکنید، اما منتظر نمیماند تا سند برای اجرا تجزیه شود. مرورگر هر دو اسکریپت را به صورت موازی دانلود کرده و در اسرع وقت اجرا می کند.
متأسفانه، چون قرار است در اسرع وقت اجرا شوند، «2.js» ممکن است قبل از «1.js» اجرا شود. اگر آنها مستقل باشند خوب است، شاید "1.js" یک اسکریپت ردیابی باشد که هیچ ربطی به "2.js" ندارد. اما اگر "1.js" شما یک کپی CDN از jQuery است که "2.js" به آن وابسته است، صفحه شما با خطاها پوشانده می شود، مانند یک بمب خوشه ای در یک... نمی دانم... من چیزی برای آن ندارم. این یکی
من می دانم به چه چیزی نیاز داریم، یک کتابخانه جاوا اسکریپت!
جام مقدس مجموعهای از اسکریپتها را فوراً بدون مسدود کردن رندر بارگیری میکند و در اسرع وقت به ترتیبی که اضافه شدهاند اجرا میشوند. متأسفانه HTML از شما متنفر است و به شما اجازه این کار را نمی دهد.
این مشکل توسط جاوا اسکریپت در چند طعم حل شد. برخی از شما از شما خواسته اند که تغییراتی را در جاوا اسکریپت خود ایجاد کنید و آن را در یک callback که کتابخانه به ترتیب صحیح فراخوانی می کند قرار دهید (مثلا RequireJS ). دیگران از XHR برای دانلود به صورت موازی و سپس eval()
به ترتیب صحیح استفاده میکنند، که برای اسکریپتهای دامنه دیگر کار نمیکند، مگر اینکه هدر CORS داشته باشند و مرورگر از آن پشتیبانی کند. حتی برخی از هک های فوق جادویی مانند LabJS استفاده کردند.
هکها شامل فریب مرورگر برای دانلود منبع به روشی بود که باعث ایجاد یک رویداد در پایان میشد، اما از اجرای آن اجتناب میکرد. در LabJS، اسکریپت با یک نوع mime نادرست اضافه می شود، به عنوان مثال <script type="script/cache" src="...">
. هنگامی که همه اسکریپت ها دانلود شدند، دوباره با یک نوع صحیح اضافه می شوند، به این امید که مرورگر آنها را مستقیماً از حافظه پنهان دریافت کند و فوراً به ترتیب آنها را اجرا کند. این به رفتار راحت اما نامشخص بستگی داشت و زمانی که HTML5 اعلام کرد مرورگرها نباید اسکریپتهایی را با نوع ناشناخته دانلود کنند خراب شد. شایان ذکر است که LabJS خود را با این تغییرات تطبیق داده و اکنون از ترکیبی از روش های این مقاله استفاده می کند.
با این حال، بارگذارهای اسکریپت برای خود مشکلی در عملکرد دارند، شما باید منتظر بمانید تا جاوا اسکریپت کتابخانه بارگیری و تجزیه شود تا هر یک از اسکریپت هایی که مدیریت می کند شروع به دانلود کند. همچنین، چگونه می خواهیم اسکریپت لودر را بارگذاری کنیم؟ چگونه میخواهیم اسکریپتی را بارگذاری کنیم که به اسکریپت لودر میگوید چه چیزی را بارگذاری کند؟ چه کسی Watchmen را تماشا می کند؟ چرا من برهنه هستم؟ اینها همه سوالات دشواری هستند.
اساساً، اگر مجبور باشید قبل از اینکه به دانلود اسکریپتهای دیگر فکر کنید، یک فایل اسکریپت اضافی را دانلود کنید، در همانجا نبرد عملکرد را باختهاید.
DOM به نجات
پاسخ در واقع در مشخصات HTML5 است، اگرچه در پایین بخش بارگذاری اسکریپت پنهان است.
بیایید آن را به "زمینی" ترجمه کنیم:
[
'//other-domain.com/1.js',
'2.js'
].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
document.head.appendChild(script);
});
اسکریپت هایی که به صورت پویا ایجاد می شوند و به سند اضافه می شوند به طور پیش فرض ناهمگام هستند ، رندر را مسدود نمی کنند و به محض دانلود اجرا می شوند، به این معنی که ممکن است به ترتیب اشتباه بیرون بیایند. با این حال، میتوانیم صریحاً آنها را بهعنوان غیرهمگام علامتگذاری کنیم:
[
'//other-domain.com/1.js',
'2.js'
].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
script.async = false;
document.head.appendChild(script);
});
این به اسکریپت های ما ترکیبی از رفتار می دهد که با HTML ساده قابل دستیابی نیست. اسکریپت ها با عدم همگام نبودن صریح به یک صف اجرا اضافه می شوند، همان صفی که در اولین مثال ساده HTML ما به آن اضافه شده است. با این حال، با ایجاد پویا، خارج از تجزیه سند اجرا میشوند، بنابراین رندر در حین دانلود مسدود نمیشود (بارگیری اسکریپت غیر همگام با همگامسازی XHR را اشتباه نگیرید، که هرگز چیز خوبی نیست).
اسکریپت بالا باید به صورت خطی در سر صفحه قرار داده شود، بارگیری اسکریپت را در اسرع وقت بدون ایجاد اختلال در رندر پیشرو در صف قرار دهد، و در اسرع وقت به ترتیبی که شما مشخص کرده اید اجرا شود. "2.js" قبل از "1.js" برای دانلود رایگان است، اما تا زمانی که "1.js" با موفقیت دانلود و اجرا نشود، یا موفق به انجام هر یک از آنها نشود، اجرا نخواهد شد. هورا! async-download اما دستور-اجرا!
بارگیری اسکریپت ها به این روش توسط هر چیزی که از ویژگی async پشتیبانی می کند پشتیبانی می شود، به استثنای Safari 5.0 (5.1 خوب است). علاوه بر این، تمام نسخههای فایرفاکس و اپرا بهعنوان نسخههایی که از ویژگی async پشتیبانی نمیکنند، اسکریپتهای اضافهشده بهصورت پویا را بهراحتی به ترتیبی که به سند اضافه میشوند، اجرا میکنند.
این سریعترین راه برای بارگیری اسکریپت هاست درست است؟ درسته؟
خوب، اگر به صورت پویا تصمیم می گیرید که کدام اسکریپت ها را بارگیری کنید، بله، در غیر این صورت، شاید نه. با مثال بالا، مرورگر باید اسکریپت را تجزیه و اجرا کند تا بفهمد کدام اسکریپت ها را دانلود کند. این اسکریپت های شما را از اسکنرهای پیش بارگذاری پنهان می کند. مرورگرها از این اسکنرها برای کشف منابع در صفحاتی که احتمالاً بعداً از آنها بازدید می کنید استفاده می کنند، یا منابع صفحه را در حالی که تجزیه کننده توسط منبع دیگری مسدود شده است، کشف می کنند.
با قرار دادن این مورد در سر سند می توانیم قابلیت کشف را دوباره به آن اضافه کنیم:
<link rel="subresource" href="//other-domain.com/1.js">
<link rel="subresource" href="2.js">
این به مرورگر می گوید که صفحه به 1.js و 2.js نیاز دارد. link[rel=subresource]
شبیه link[rel=prefetch]
است، اما با معنایی متفاوت . متأسفانه در حال حاضر فقط در کروم پشتیبانی می شود، و شما باید اعلام کنید که کدام اسکریپت ها را دو بار بارگیری کنید، یک بار از طریق عناصر پیوند، و دوباره در اسکریپت خود.
تصحیح: من در ابتدا اعلام کردم که اینها توسط اسکنر پیش بارگیری برداشته شده است، آنها نیستند، آنها توسط تجزیه کننده معمولی برداشت می شوند. با این حال، اسکنر پیش بارگذاری می تواند این موارد را انتخاب کند، اما هنوز این کار را نکرده است، در حالی که اسکریپت های موجود در کد اجرایی هرگز نمی توانند از قبل بارگذاری شوند. با تشکر از Yoav Weiss که من را در نظرات تصحیح کرد.
به نظر من این مقاله مایوس کننده است
وضعیت افسرده کننده است و شما باید احساس افسردگی کنید. هیچ راه غیر تکراری و در عین حال بیانی برای دانلود سریع و ناهمزمان اسکریپت ها در حین کنترل ترتیب اجرا وجود ندارد. با HTTP2/SPDY میتوانید سربار درخواست را تا حدی کاهش دهید که تحویل اسکریپتها در چندین فایل کوچک جداگانه با قابلیت ذخیرهسازی سریعترین راه باشد. تصور کنید:
<script src="dependencies.js"></script>
<script src="enhancement-1.js"></script>
<script src="enhancement-2.js"></script>
<script src="enhancement-3.js"></script>
…
<script src="enhancement-10.js"></script>
هر اسکریپت بهبود با یک جزء صفحه خاص سروکار دارد، اما به توابع ابزار در dependencies.js نیاز دارد. در حالت ایدهآل، میخواهیم همه را بهصورت ناهمزمان دانلود کنیم، سپس اسکریپتهای بهبود را در اسرع وقت، به هر ترتیبی، اما پس از dependencies.js اجرا کنیم. این افزایش پیشرونده است! متأسفانه هیچ راهی برای رسیدن به این هدف وجود ندارد مگر اینکه خود اسکریپت ها برای ردیابی وضعیت بارگذاری dependencies.js اصلاح شوند. حتی async=false هم این مشکل را حل نمی کند، زیرا اجرای enhancement-10.js در 1-9 مسدود می شود. در واقع، تنها یک مرورگر وجود دارد که این کار را بدون هک ممکن میسازد…
IE یک ایده دارد!
اینترنت اکسپلورر اسکریپت ها را به گونه ای متفاوت با سایر مرورگرها بارگذاری می کند.
var script = document.createElement('script');
script.src = 'whatever.js';
اینترنت اکسپلورر اکنون شروع به دانلود "whatever.js" می کند، مرورگرهای دیگر دانلود را شروع نمی کنند تا زمانی که اسکریپت به سند اضافه شود. IE همچنین دارای یک رویداد "readystatechange" و ویژگی "readystate" است که پیشرفت بارگذاری را به ما می گوید. این واقعاً مفید است، زیرا به ما امکان می دهد بارگذاری و اجرای اسکریپت ها را به طور مستقل کنترل کنیم.
var script = document.createElement('script');
script.onreadystatechange = function() {
if (script.readyState == 'loaded') {
// Our script has download, but hasn't executed.
// It won't execute until we do:
document.body.appendChild(script);
}
};
script.src = 'whatever.js';
میتوانیم با انتخاب زمان اضافه کردن اسکریپتها به سند، مدلهای وابستگی پیچیده بسازیم. اینترنت اکسپلورر از این مدل از نسخه 6 پشتیبانی میکند. بسیار جالب است، اما همچنان از همان مشکل پیشبارگذاری مانند async=false
رنج میبرد.
بس است! چگونه باید اسکریپت ها را بارگیری کنم؟
باشه باشه اگر میخواهید اسکریپتها را به گونهای بارگذاری کنید که رندر را مسدود نکند، شامل تکرار نباشد، و از مرورگر پشتیبانی عالی داشته باشد، پیشنهاد من در اینجاست:
<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>
که در انتهای عنصر بدنه. بله، توسعهدهنده وب بودن بسیار شبیه پادشاه سیزیف بودن است (بووم! 100 امتیاز هیپستر برای مرجع اساطیر یونان!). محدودیتها در HTML و مرورگرها باعث میشود ما بهتر عمل کنیم.
امیدوارم ماژولهای جاوا اسکریپت با ارائه روشی غیرمسدود کننده برای بارگیری اسکریپتها و کنترل ترتیب اجرا، ما را نجات دهند، اگرچه این امر مستلزم نوشتن اسکریپتها به صورت ماژول است.
اوه، باید چیزی بهتر از این وجود داشته باشد که بتوانیم از آن استفاده کنیم؟
به اندازه کافی منصفانه است، برای امتیاز جایزه، اگر میخواهید در مورد عملکرد واقعاً تهاجمی باشید و کمی از پیچیدگی و تکرار مهم نیست، میتوانید چند ترفند بالا را ترکیب کنید.
ابتدا اعلان منبع فرعی را برای پیش بارگذاری کننده ها اضافه می کنیم:
<link rel="subresource" href="//other-domain.com/1.js">
<link rel="subresource" href="2.js">
سپس، به صورت خطی در سر سند، اسکریپتهای خود را با جاوا اسکریپت بارگذاری میکنیم، با استفاده از async=false
، به بارگذاری اسکریپت مبتنی بر آماده IE برمیگردیم، و به عقب میاندازیم.
var scripts = [
'1.js',
'2.js'
];
var src;
var script;
var pendingScripts = [];
var firstScript = document.scripts[0];
// Watch scripts load in IE
function stateChange() {
// Execute as many scripts in order as we can
var pendingScript;
while (pendingScripts[0] && pendingScripts[0].readyState == 'loaded') {
pendingScript = pendingScripts.shift();
// avoid future loading events from this script (eg, if src changes)
pendingScript.onreadystatechange = null;
// can't just appendChild, old IE bug if element isn't closed
firstScript.parentNode.insertBefore(pendingScript, firstScript);
}
}
// loop through our script urls
while (src = scripts.shift()) {
if ('async' in firstScript) { // modern browsers
script = document.createElement('script');
script.async = false;
script.src = src;
document.head.appendChild(script);
}
else if (firstScript.readyState) { // IE<10
// create a script and add it to our todo pile
script = document.createElement('script');
pendingScripts.push(script);
// listen for state changes
script.onreadystatechange = stateChange;
// must set src AFTER adding onreadystatechange listener
// else we'll miss the loaded event for cached scripts
script.src = src;
}
else { // fall back to defer
document.write('<script src="' + src + '" defer></'+'script>');
}
}
چند ترفند و کوچک سازی بعداً، 362 بایت + URL های اسکریپت شما است:
!function(e,t,r){function n(){for(;d[0]&&"loaded"==d[0][f];)c=d.shift(),c[o]=!i.parentNode.insertBefore(c,i)}for(var s,a,c,d=[],i=e.scripts[0],o="onreadystatechange",f="readyState";s=r.shift();)a=e.createElement(t),"async"in i?(a.async=!1,e.head.appendChild(a)):i[f]?(d.push(a),a[o]=n):e.write("<"+t+' src="'+s+'" defer></'+t+">"),a.src=s}(document,"script",[
"//other-domain.com/1.js",
"2.js"
])
آیا ارزش بایت های اضافی را در مقایسه با یک اسکریپت ساده دارد؟ اگر در حال حاضر از جاوا اسکریپت برای بارگذاری مشروط اسکریپت ها استفاده می کنید، همانطور که بی بی سی انجام می دهد ، ممکن است از شروع زودتر آن دانلودها نیز بهره مند شوید. در غیر این صورت، شاید نه، از روش ساده انتهای بدن استفاده کنید.
وای، اکنون می دانم که چرا بخش بارگیری اسکریپت WHATWG بسیار وسیع است. من به یک نوشیدنی نیاز دارم.
مرجع سریع
عناصر اسکریپت ساده
<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>
Spec می گوید: با هم دانلود کنید، بعد از هر CSS معلق به ترتیب اجرا کنید، رندر را تا کامل شدن مسدود کنید. مرورگرها می گویند: بله قربان!
به تعویق انداختن
<script src="//other-domain.com/1.js" defer></script>
<script src="2.js" defer></script>
Spec می گوید: با هم دانلود کنید، درست قبل از DOMContentLoaded به ترتیب اجرا کنید. در اسکریپتهای بدون «src»، «به تعویق انداختن» را نادیده بگیرید. IE < 10 می گوید: من ممکن است 2.js را در نیمه راه اجرای 1.js اجرا کنم. این سرگرم کننده نیست؟ مرورگرهای قرمز رنگ میگویند: من نمیدانم این چیز «به تعویق انداختن» چیست، من اسکریپتها را طوری بارگذاری میکنم که انگار آنجا نیستند. مرورگرهای دیگر می گویند: بسیار خوب، اما من ممکن است "تعویق" را در اسکریپت های بدون "src" نادیده بگیرم.
همگام
<script src="//other-domain.com/1.js" async></script>
<script src="2.js" async></script>
Spec می گوید: با هم دانلود کنید، به هر ترتیبی که دانلود می کنند اجرا کنید. مرورگرهای قرمز رنگ می گویند: 'async' چیست؟ من می خواهم اسکریپت ها را طوری بارگذاری کنم که انگار آنجا نبود. مرورگرهای دیگر می گویند: بله، خوب.
همگام سازی نادرست
[
'1.js',
'2.js'
].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
script.async = false;
document.head.appendChild(script);
});
مشخصات می گوید: با هم دانلود کنید، به محض دانلود به ترتیب اجرا کنید. فایرفاکس < 3.6، Opera میگوید: من نمیدانم این چیز «ناسینک» چیست، اما اتفاقاً اسکریپتهای اضافهشده از طریق JS را به ترتیبی که اضافه میشوند، اجرا میکنم. Safari 5.0 می گوید: من "async" را می فهمم، اما تنظیم آن را روی "false" با JS درک نمی کنم. من اسکریپت های شما را به محض اینکه فرود آمدند، به هر ترتیبی اجرا می کنم. IE < 10 می گوید: هیچ ایده ای در مورد "async" وجود ندارد، اما یک راه حل با استفاده از "onreadystatechange" وجود دارد. سایر مرورگرهای قرمز رنگ می گویند: من این چیز "ناهمگام" را نمی فهمم، اسکریپت های شما را به محض اینکه فرود آمدند، به هر ترتیبی که باشد، اجرا می کنم. همه چیز دیگر می گوید: من دوست شما هستم، ما این کار را با کتاب انجام می دهیم.