آموزش جامع Webpack (بخش پنجم: ایمپورت دینامیک و افراز کد) – از صفر تا صد
برخی اوقات میخواهیم مطمئن شویم که اپلیکیشن ما کاملاً انعطافپذیر است، اما طراحی واکنشگرا کافی نیست و نهتنها UI باید سطح و ظاهر متفاوتی داشته باشد، بلکه رفتار آن روی پلتفرمهای مختلف نیز باید متفاوت باشد. در این بخش از سری مقالات آموزش جامع Webpack به بررسی مفاهیم ایمپورت دینامیک و افراز کد میپردازیم. برای مطالعه بخش قبلی روی لینک زیر کلیک کنید:
در چنین مواردی که در پاراگراف قبلی توضیح دادیم، بسته به اقدامات کاربر میتوان منطق تجاری را در زمان اجرا تعویض کرد. همچنین ممکن است اپلیکیشن شما بیش از حد بزرگ شده باشد و بخواهید موارد مختلف را تنها زمانی که ضروری هستند بارگذاری کنید.
در صورتی که هر کدام از مقاصد فوق را در نظر داشته باشید تنها یک راهحل برای شما وجود دارد و آن پیشنهاد ECMA است. Webpack از ایمپورتهای دینامیک با برخی تفاوتها از قبیل ایمپورت استاتیک رسمی پشتیبانی میکند و البته رفتار زیبای دیگری به صورت «افراز کد» (code splitting) را نیز به آن افزوده است.
توضیح طرز کار
ساختار ایمپورت دینامیک بسیار سرراست است. ما تابعی داریم که یک Promise بازگشت میدهد. تصور کنید یک ماژول به نام module-1.js داریم:
1export default () => "This function does nothing";
2
3export const useless = () => "neither this one!";
برای ایمپورت آن به صورت دینامیک باید کاری مانند زیر انجام دهیم:
1import("./module-1").then(mod => {
2 const nothing = mod.default();
3 const nothingToo = mod.useless();
4
5 // logs "This function does nothing and neither this one!"
6 console.log(`${nothing} and ${nothingToo}`);
7});
اما اگر آن را اجرا کنید مشکلی وجود خواهد داشت:
فعالسازی ایمپورتهای دینامیک روی Babel
قبل از مطالعه این بخش توجه کنید که اگر از Babel نسخه 7.5 و از babel/preset-env@ استفاده میکنید نیازی به مطالعه این بخش ندارید، چون این توضیح برای نسخههای قدیمیتر Babel عرضه شده است که امکان ساختار ایمپورت دینامیک در آنها تعریف نشده بود. اما اینک preset-env به صورت پیشفرض به Babel اضافه شده است.
مسئله این جا است که چون ایمپورتهای دینامیک تنها یک پیشنهاد ECMA هستند (در نسخههای قبل 7.5 چنین بودند) این احتمال وجود دارد که ساختار جاوا اسکریپت را حفظ کنند و از این رو BabelJS در زمان تحلیل کردن آنها اعتراض میکند. در واقع اگر قاعده babel-loader را حذف کنید می ببینید که Webpack به درستی و بدون خطا با آن کار میکند:
اما اگر به خطا توجه کنید میبینید که نکتهای را مطرح میکند:
Add @babel/plugin-syntax-dynamic-import (https://git.io/vb4Sv) to the ‘plugins’ section of your Babel config to enable parsing.
پس آن را نصب میکنیم:
yarn add @babel/plugin-syntax-dynamic-import –dev
سپس آن را در babelrc. فعال میکنیم:
1{
2 "presets": ["@babel/preset-env"],
3 "plugins": ["@babel/plugin-syntax-dynamic-import"]
4}
فعالسازی قاعده babel-loader برای بار دیگر و اجرای دستور yarn start:dev موجب میشود که همه چیز به درستی کار کند.
بر سر Bundle چه آمد؟
اگر به بررسی لاگهای build بپردازید، با چیزی مانند تصویر زیر مواجه میشوید:
عبارت js.1 به این معنی است که Webpack یک دسته کد ایجاد کرده و ماژول ما را که به صورت دینامیک ایمپورت میشود را داخل آن جای داده است.
اگر سرور dev را اجرا کنید و یک «نقطه توقف» (Breakpoint) روی ()import قرار دهید، فایل 1.js را در زبانه Network نخواهید دید، اما به محض آن که نقطه توقف را بردارید درخواست واکشی آن ارسال میشود.
بصریسازی مسئله
زمانی که پروژه رشد میکند احتمالاً لازم خواهد شد که Budnle نهایی را بررسی کنید و آن را بهینهسازی نمایید. اما همه افراد هکر نیستند که بتوانند این خروجی را خوانده و همه اطلاعات را از آن استخراج کنند.
با این حال Webpack گزینههای زیادی برای بصریسازی خروجی نهایی و خواناتر ساختن آن ارائه کرده است. راهحلی که ما استفاده میکنیم Webpack Bundle Analyser نام دارد. در ادامه به بررسی طرز کار آن میپردازیم. اما قبل از هر چیز باید آن را نصب کنیم:
yarn add webpack-bundle-analyzer –dev
کد زیر را به اسکریپتهای npm روی فایل package.json اضافه کنید:
1"scripts": {
2 "build": "webpack --mode=production",
3 "start:dev": "webpack-dev-server --mode=development",
4 "analyse": "yarn build --env.analyse"
5},
دو مسئله در این جا اتفاق میافتد:
ترکیببندی دستور: اسکریپت analyse موجب خروجی زیر میشود:
webpack --mode=production --env.analyse
ما یک متغیر محیطی به Webpack میدهیم و میتوانیم در این پیکربندی به آن دسترسی داشته باشیم. اکنون فایل webpack.config.js خود را تنظیم کرده و از پارامتر env.analyse-- روی آن استفاده میکنیم:
1//...
2const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
3
4// We set a default value to env, to avoid it being undefined
5module.exports = (env = {}, argv) => ({
6 //...
7 plugins: [
8 // Any parameter given to Webpack client can be captured on the "env"
9 env.analyse ? new BundleAnalyzerPlugin() : null,
10 //...
11 ].filter(pl => pl !== null),
12 //...
اینک کافی است اسکریپت npm را اجرا کنیم تا bundle analyzer به صورت خودکار زبانه جدیدی در مرورگر پیشفرض برای ما باز کند:
yarn analyse
در این مرحله با صفحه زیر مواجه خواهید شد:
بارگذاری با استفاده از عبارتها
اینک میتوانیم ببینیم که Webpack کد شما را به صورت پیشفرض زمانی که یک ایمپورت دینامیک اجرا میکنید افراز میکند، اما اجازه بدهید کمی پا را فراتر گذارده و آن را مورد بررسی قرار دهیم.
در این مرحله یک ماژول جدید به نام module-2.js ایجاد میکنیم که تنها این تابع را اکسپورت میکند:
1export default () => "I'm totally useless! Deal with it! ?";
میتوانید ببینید که یک الگو برای نامهای ماژول وجود دارد و هر ماژول با شمارهاش مشخص میشود. بدین ترتیب امکان استفاده از یک «عبارت» (Expression) برای ایمپورت کردن آنها وجود خواهد داشت:
1const outputs = [1, 2].map(modNum =>
2 import(`./module-${modNum}`).then(mod => mod.default())
3);
4
5Promise.all(outputs).then(outs => console.log(outs.join(" and ")));
بدین ترتیب تابعهای پیشفرض هر دو ماژول 1 و 2 به صورت یک نتیجه در خروجی عرضه میشوند:
This function does nothing and I’m totally useless! Deal with it! ?
اینک اگر bundle را parse کنید، خواهید دید که Webpack به صورت خودکار یک ساختار بر اساس عبارت شما میسازد:
بدین ترتیب با قدرت «برنامهنویسی تابعی» (functional programming) آشنا میشوید. در واقع ما اساساً یک مجموعه اعداد را به خروجیهای تابع اکسپورت پیشفرض ماژولشان تبدیل میکنیم. تا زمانی که ماژولها این «امضا» (signature) را حفظ کنند، میتوان به این شیوه عمل کرد.
بدین ترتیب ظرفیتهای بسیار زیادی پیش روی ما قرار میگیرند. برای نمونه چنان که قبلاً انجام دادیم، میتوانیم کامپوننتهای UI را بسته به پلتفرم (یا مرورگر) بارگذاری کنیم، پس از این کاربر مقادیری را انتخاب یا در اپلیکیشن وارد میکند، اقدامات متفاوتی را اجرا کنیم و مواردی از این دست.
بارگذاری کُند (Lazy Loading)
در اغلب موارد ما میخواهیم بارگذاری ماژول را طوری به تعویق بیندازیم که بتوانیم تنها در موارد ضروری از آن استفاده کنیم. به این منظور برخی تکنیکها وجود دارند که در ادامه آنها را بررسی میکنیم.
بارگذاری مبتنی بر رویداد
یک فایل به نام lazy-one.js ایجاد کنید که یک رشته اکسپورت میکند:
1export default "?";
در انتهای فایل نیز index.js کد زیر را وارد کنید:
1const lazyButton = document.createElement("button");
2lazyButton.innerText = "?";
3lazyButton.style = "margin: 10px auto; display: flex; font-size: 4rem";
4lazyButton.onclick = () =>
5 import("./lazy-one").then(mod => (lazyButton.innerText = mod.default));
6
7document.body.appendChild(lazyButton);
ما هماینک یک دکمه با یک ایمپورت دینامیک روی رویداد click به آن اضافه کردیم.
اگر صفحه را بارگذاری کنید و به زبانه Network بخش توسعهدهندگان مرورگر بروید، میبینید که صفحه لود میشود و دو دسته داده از قبل درخواست شده است:
سپس با کلیک روی دکمه، دسته دیگری لود میشود:
کامنتهای جادویی Webpack
با توجه به این که وب پک این قابلیت ایمپورت دینامیک را اضافه کرده است چه بهتر که بتوانیم روی آن کنترل نیز داشته باشیم. وب پک روشی برای پیکربندی این ایمپورتها از طریق کامنتهایی که از الگوی خاصی پیروی میکنند معرفی کرده است.
نام دسته
شاید متوجه شده باشید که نامگذاری فایلها به صورت 2.js کمک زیادی به درک این که در داخل آن چه چیزی وجود دارد نمیکند. بنابراین نام این فایل را به myAwesomeLazyModule تغییر میدهیم:
1import(/* webpackChunkName: "myAwesomeLazyModule" */ "./lazy-one")
2 .then(mod => {
3 // ...
4 });
بدین ترتیب با چیزی مانند تصویر زیر مواجه میشویم:
بارگذاری قبلی (Preload) ماژول
فرض کنید lazy-module ما بسیار مهم است و نمیخواهیم تغییر محتوای دکمه پس از کلیک کردن کاربر روی آن به تأخیر بیفتد.
در این حالت میتوانیم این منابع را از قبل بارگذاری کنیم، زیرا کاملاً مطمئن هستیم که در صفحه جاری استفاده خواهد شد:
1import(
2 /* webpackChunkName: "myAwesomeLazyModule" */
3 /* webpackPreload: true */
4 "./lazy-one"
5);
این کار موجب میشود که webpack یک `<"link rel=”preload>` در ابتدای صفحه ایجاد کند و موجب میشود که منبع پیش از main.js بارگذاری شود (اما اجرا نمیشود).
این وضعیت برای بارگذاری منابع کوچک که کاربران با تأخیر شبکه بالایی روبرو هستند بسیار مفید است چون در این حالت تأخیر زیادی در اجرای دستههای کوچک کد ایجاد میشود که نه ناشی از اندازه آنها بلکه ناشی از فرایند پردازش پروتکل HTTP است. این وضعیت در پروتکل HTTPS به دلیل وجود مرحله handshake بدتر هم میشود.
Prefetch کردن ماژول
شاید از خود بپرسید که Preload و Prefetch چه تفاوتی با هم دارند؟ گرچه این دو مشابه هم به نظر میرسند اما Prefetch پیش از بسته/دسته کدی که این منبع را لود میکند، بارگذاری نخواهد شد، بلکه پس از این که مرورگر بیکار شد به بارگذاری میپردازد.
بدین ترتیب به ابزاری مناسب برای بارگذاری منابعی تبدیل میشود که احتمالاً در آینده مورد استفاده قرار خواهند گرفت. برای نمونه دادههایی (از طریق آنالایتیکز) در اختیار داریم که اغلب کاربران ثبت نام نکرده که از صفحه products بازدید میکنند به احتمال بالا به صفحه subscribe نیز خواهند رفت. از آنجا که توسعهدهنده هوشمندی هستید یک ایمپورت Prefetch روی products تعیین میکنید که باعث میشود مرورگر این منبع را واکشی کرده و در زمان بیکاری آن را کش کند:
1// Pseudo code for products page
2page("products", () => {
3 /*
4 * We don't want to execute the import, just the prefetch
5 * That's why we wrap it on a function
6 */
7 () => import(/* webpackPrefetch: true */ "./subscribe-page");
8 // ...
9});
چنان که میبینید عملکرد آن مشابه preload است و صرفاً با افزودن کامنت جادویی فوق فعال میشود. نکته: اگر چند منبع prefetch یا preload دارید میتوانید اولویت آنها را نیز تعیین کنید:
1/* webpackPrefetch: 42 */
حالت Chunk
شاید از خود بپرسید: من 100 مورد از این {modules-${id دارم و آنها را مستقیماً در صفحه درخواست میکنیم اما نمیخواهم 100 فایل مختلف را درخواست کنم، چون کاربران از تأخیر شبکه بالا رنج میبرند. به همین دلیل است که ما روش وب پک برای resolve کردن و افراز دستهها را جهت ایمپورتهای دینامیک با استفاده از کامنت جادویی webpackMode کنترل میکنیم. بر اساس مستندات حالتهای مختلف زیر وجود دارد:
- Lazy (پیشفرض): یک دسته کد تولید میکند که میتواند برای هر ماژول ایمپورت شده به صورت lazy بارگذاری شود.
- lazy-once: یک دسته کد منفرد به صورت قابل بارگذاری lazy تولید میکند که میتواند به همه فراخوانیهای ()import پاسخ دهد. این دسته کد در نخستین فراخوانی ()import واکشی میشود و فراخوانیهای بعدی به ()import را با همان پاسخ شبکه استفاده میکند. توجه کنید که این وضعیت تنها زمانی معنیدار است که یک گزاره دینامیک ناقص مانند زیر وجود داشته باشد:
import(`./locales/${language}.json`)
که چندین مسیر ماژول داشته باشد و احتمال درخواست برای آن برود.
- eager: یک دسته کد دیگر تولید میکند همه ماژولها در دسته کد کنونی include شدهاند و هیچ درخواست شبکه دیگری مورد نیاز نیست. در این حالت، همچنان یک Promise بازگشت مییابد، اما از قبل resolve شده است. برعکس ایمپورت استاتیک این ماژول تا زمانی که فراخوانی به ()import صورت نگرفته است اجرا نمیشود.
- weak: در صورتی که تابع ماژول از قبل به روشی دیگر بارگذاری شده باشد، یعنی دسته کد دیگری آن را ایمپورت کرده باشد یا یک اسکریپت شامل ماژول بارگذاری شده باشد، تلاش میکند تا آن را لود کند. یک Promise همچنان بازگشت مییابد، اما تنها در صورتی با موفقیت resolve میشود که کدها از قبل روی کلاینت باشند. اگر ماژول در دسترس نباشد، Promise رد میشود. در این حالت هیچ درخواست شبکه هرگز اجرا نخواهد شد. این حالت زمانی مفید است که دسته کدهای مورد نیاز همواره به طور دستی در درخواستهای اولیه برای رندرینگ سراسری عرضه شوند، اما در حالتی که ناوبری اپلیکیشن موجب اجرای یک ایمپورت شود و از ابتدا عرضه نشد باشد مفید نخواهد بود. در ادامه کدی را میبینید که lazy-once را روی ایمپورت دینامیک با یک عبارت امتحان میکند:
1const outputs = [1, 2].map(modNum =>
2 import(/* webpackMode: "lazy-once" */ `./module-${modNum}`).then(mod =>
3 mod.default()
4 )
5);
نتیجه به صورت زیر است:
چنان که میبینید همه دسته کدها که قبلاً جدا بودند اینک در دسته کد مشترکی قرار دارند و بدین ترتیب کار اپلیکیشن آسان شده است، چون همه ماژولها را با یک درخواست منفرد بارگذاری میکند.
اگر به جای آن از eagar استفاده کنیم، همه این ماژولها درون یک ماژول که آنها را ایمپورت میکند و در این مورد main.js است جای میگیرند.
سخن پایانی
در این بخش از سری مقالههای آموزش جامع Webpack به بررسی ایمپورتهای دینامیک پرداختیم و آنها را عملاً مورد استفاده قرار دادیم. همچنین روشی که کد در این فرایند افراز میشود را دستکاری کردیم. بدین ترتیب کار ما تقریباً به پایان رسیده است و تنها کاری که باقی مانده است بهره گرفتن از مزیت آن چه آموختیم در React است که در بخش بعدی آن را مطالعه خواهیم کرد. برای مطالعه بخش بعدی (پایانی) روی لینک زیر کلیک کنید:
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای JavaScript (جاوا اسکریپت)
- مجموعه آموزشهای برنامهنویسی
- ساخت اپلیکیشنهای مدرن با Webpack — به زبان ساده
- آموزش جامع Webpack (بخش اول) — از صفر تا صد
- توسعه وب اپلیکیشن با جاوا اسکریپت و Webpack — راهنمای کاربردی
==