امروزه پروژه هایی مانند create-react-app و Vue CLI باعث شده اند کاربران از پیکربندی دستی webpack خلاص شوند و دیگر نیازی به اعمال تنظیمات مختلف برای شروع پروژه خود نداشته باشند. در عین حال این موضوع نباید شما را از یادگیری webpack (حداقل در سطح متوسط) باز دارد. ما در این مقاله می خواهیم به صورت سریع و فشرده، مهم ترین مفاهیم و نکات مربوط به آموزش webpack را بررسی کنیم تا شما نیز بتوانید در صورت نیاز webpack را در پروژه خود پیکربندی کنید.
حتما با مفهوم «ماژول» یا module ها در جاوا اسکریپت آشنا هستید (ماژول هایی مانند AMD modules و Common JS و ES modules و غیره). کار webpack این است که یک module bundler است، بدین معنی که تمام ماژول ها را گرفته و در یک فایل ادغام می کند.
سطح مقاله: برای مطالعه مقاله آموزش رایگان Webpack، آشنایی با مفاهیم ساده توسعه وب مانند npm توصیه می شود اما الزامی نیست. این مقاله هیچ ملزومات دیگری ندارد.
قبل از اینکه بخواهیم وارد بحث آموزش رایگان Webpack شویم باید با چند بحث ابتدایی آشنا شویم:
Entry point یا «نقطه ورود»: وبپک همیشه نیاز به یک نقطه ورود دارد. وبپک از این نقطه وارد برنامه شما شده و آن را پوشه و فایل اصلی برنامه شما تلقی می کند بنابراین دریافت تمام وابستگی ها (dependency) از این بخش پردازش می شوند. نقطه ورودی پیش فرض وبپک آدرس src/index.js است اما شما می توانید آن را تغییر داده یا چند نقطه ورود دیگر را نیز اضافه کنید.
output یا مسیر خروجی: همانطور که گفتم وبپک یک module bundler است بنابراین تمام ماژول های شما را گرفته و در یک فایل می گذارد. حالا این فایل خروجی در چه مسیری قرار می گیرد؟ output پیش فرض در وبپک پوشه ای به نام dist در مسیر اصلی پروژه شما است و طبیعتا قابل تغییر است. معمولا زمانی که پروژه شما تمام شود به این مسیر رفته و فایل های آن را در سرور خود قرار می دهید چرا که آن ها فایل های نهایی هستند.
loader یا بارگذارنده: وبپک به صورت پیش فرض نمی تواند با تمام فایل ها و پسوند ها کار کند بنابرین به افزونه هایی نیاز دارد که این قابلیت را فعال کنند. به طور مثال فایل های تصویری jpeg یا فایل های css برای وبپک قابل پردازش نیستند و اگر می خواهید از آن ها در پروژه خود استفاده کنید باید loader مناسب آن را دانلود کنید.
plugin یا پلاگین: پلاگین های وبپک مانند هر پلاگین دیگری، قابلیت جدیدی به وبپک اضافه می کنند یا رفتار آن را به شکلی تغییر می دهند. به طور مثال پلاگین هایی برای استخراج HTML و CSS وجود دارند.
Mode یا حالت: وبپک دو mode یا دو حالت اجرا دارد: توسعه (development) و بهره برداری (production). تا زمانی که در حالت توسعه باشید، هدف کامپایل کردن سریع کدها است اما اگر به حالت بهره برداری بروید وبپک کار هایی نظیر minification کدها و بهینه سازی کدهای جاوا اسکریپت را انجام خواهد داد.
Code Splitting یا شکستن کد: Code Splitting نام دیگری برای Lazy loading یا بارگذاری تنبل است. در این روش کدهایمان را به چند بخش تقسیم می کنیم تا فایل نهایی یک فایل بسیار بزرگ نباشد سپس هر بخش از کد را فقط در مواقع خاصی بارگذاری می کنیم. مثلا اگر بخشی از کدها مربوط به صفحه فروشگاه باشد، آن کد را فقط زمانی بارگذاری می کنیم که کاربر به صفحه فروشگاه برود. به هر کدام از این بخش های تقسیم شده یک chunk می گویند.
در قدم اول یک پوشه برای خودتان بسازید. من نام پوشه را roxo_training گذاشته ام. حالا درون این پوشه ترمینال خود (یا CMD برای کاربران ویندوز) را باز دستور زیر را اجرا کنید:
npm init -y
برای اجرای این دستور باید node.js و npm در سیستم شما نصب باشد. با اجرای این دستور نتیجه زیر را دریافت می کنید:
{ "name": "roxo_training", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }
یعنی فایلی به نام package.json ایجاد شده است که محتوای بالا را دارد. در مرحله بعدی باید وبپک را در پروژه خودمان نصب کنیم:
npm i webpack webpack-cli webpack-dev-server --save-dev
پکیج webpack هسته وبپک را برای ما نصب می کند، پکیج webpack-cli به ما اجازه می دهد از طریق ترمینال با وبپک تعامل داشته باشیم و پکیج webpack-dev-server یک سرور توسعه محلی را در اختیار ما قرار می دهد. حالا برای ساده تر عملیات صدا زدن وبپک، فایل package.json را باز کرده و یک اسکریپت dev را تعریف می کنیم:
{ "name": "roxo_training", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "dev": "webpack --mode development", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }
از این به بعد هر زمانی که بخواهیم وبپک را اجرا کنیم فقط دستور npm run dev را اجرا می کنیم. در حال حاضر اگر npm run dev را در ترمینال خود اجرا کنید، خطایی طولانی می گیرد که بخش ابتدایی آن بدین شکل است:
ERROR in main Module not found: Error: Can't resolve './src'
در اینجا وبپک به دنبال نقطه ورودی پیش فرض یا همان src/index.js می گردد و از آنجایی که آن را پیدا نمی کند به خطا برخورد می کنیم. من در کنار فایل package.json (درون پوشه roxo_training) پوشه ای به نام src را می سازم که درون خود یک فایل ساده جاوا اسکریپتی به نام index.js را با محتوای زیر داشته باشد:
console.log("Hello Webpack from index.js");
حالا دوباره دستور npm run dev را اجرا کنید. این بار به جای خطا باید چنین نتیجه ای را بگیرید:
asset main.js 1.23 KiB [emitted] (name: main) ./src/index.js 43 bytes [built] [code generated] webpack 5.36.2 compiled successfully in 111 ms
حالا اگر به پوشه roxo_training نگاهی بیندازید متوجه حضور پوشه جدیدی به نام dist می شوید. در این پوشه فایلی به نام main.js وجود دارد که همان bundle نهایی شما است.
برای عملیات های ساده نیازی به پیکربندی نیست اما معمولا هیچ پروژه ای در وبپک ساده نیست بنابراین یادگیری قوانین پیکربندی آن بسیار مهم است. برای پیکربندی وبپک باید فایلی به نام webpack.config.js را در مسیر اصلی پروژه (یعنی roxo_training) ایجاد کنید. نام این فایل قابل تغییر نبوده و اختیاری نیست. در مرحله بعدی باید دستورات پیکربندی را در این فایل بنویسیم. دستورات پیکربندی وبپک بسیار زیاد و طولانی هستند، شما می توانید نمونه ای کامل از آن را در documentation رسمی وبپک مشاهده کنید. در این فایل می توانید تمام موارد زیر را تنظیم کنید:
یکی از بخش های فایل پیکربندی module.exports می باشد که یک شیء جاوا اسکرپیتی به شکل زیر است:
module.exports = { // اطلاعات پیکربندی };
وظیفه module.exports پیکربندی مواردی مانند entry و output و غیره است. به طور مثال:
const path = require("path"); module.exports = { entry: { index: path.resolve(__dirname, "source", "index.js") } };
این فایل با node.js اجرا می شود بنابراین path که یکی از ماژول های node.js است را وارد کرده ایم. برای مشخص کردن نقطه ورود از کلیدواژه entry استفاده کرده ایم و یک شیء را به آن داده ایم. این شیء یک خصوصیت به نام index را دارد که مسیر فایل ورودی و نقطه شروع برنامه را دریافت می کند. من با استفاده از path.resolve مسیر را به source/index.js تغییر داده ام. dirname__ آدرس پوشه فعلی در سیستم است. از این به بعد وبپک در مسیر source/index.js به دنبال نقطه شروع می گردد.
بیایید یک مثال دیگر را بررسی کنیم. فرض کنید بخواهیم مسیر خروجی از پوشه dist به پوشه ای به نام build تغییر کند:
const path = require("path"); module.exports = { output: { path: path.resolve(__dirname, "build") } };
این کار با خصوصیت output و شیء ای داری کلید path انجام می شود. من هیچ کدام از این تغییرات را ذخیره نمی کنم و با همان مسیر src جلو می روم.
همانطور که توضیح دادم وبپک به طور پیش فرض فقط با جاوا اسکریپت کار می کند بنابراین زبان هایی مانند HTML را نمی شناسد. برای حل این مشکل به یک پلاگین به نام html-webpack-plugin نیاز داریم که با دستور زیر و از طریق ترمینال نصب می شود:
npm i html-webpack-plugin --save-dev
نصب یک پلاگین به تنهایی هیچ کاری انجام نمی دهد. زمانی که یک پلاگین را نصب کردیم باید به webpack.config.js رفته و آن را به پیکربندی اضافه کنیم. چطور؟ در همان بخش module.exports خصوصیتی به نام plugins وجود دارد:
const HtmlWebpackPlugin = require("html-webpack-plugin"); const path = require("path"); module.exports = { mode: "development", plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, "src", "index.html") }) ] };
هر پلاگین نحوه راه اندازی خاص خودش را دارد بنابراین باید به صفحه توضیحات آن پلاگین رفته و آن توضیحات را مطالعه کنید. در مورد این پلاگین باید آن را وارد فایل پیکربندی کرده (دستور require) که به ما یک constructor می دهد بنابراین ما نیز با استفاده از new یک نمونه از آن را می سازیم و template را برایش مشخص کرده ایم که یعنی از مسیر src/index.html یک فایل HTML را بارگذاری کند. در ضمن من mode را نیز روی development گذاشته ام که یعنی در حال توسعه هستیم. این مسئله باعث می شود که bundle نهایی در پوشه dist ساخته نشود بلکه در مموری سیستم شما ساخته خواهد شد تا سرعت افزایش پیدا کند. هر زمان که خواستید نتیجه نهایی را ببینید آن را به production تغییر بدهید.
در واقع هدف اصلی این پلاگین نیز همین است که تمام فایل های HTML را بارگذاری کرده و سپس تمام آن ها را در یک فایل قرار بدهد. با این حساب یک فایل index.html در پوشه src ایجاد کنید و محتوای ساده ای را در آن بنویسید، مثلا:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Roxo Webpack</title> </head> <body> <p>This is a paragraph element in the index.html file.</p> </body> </html>
حالا چطور می توانیم این فایل html را در مرورگر بارگذاری کنیم؟
اگر یادتان باشد ما در ابتدای این مقاله پکیجی به نام webpack-dev-server را نصب کردیم و به شما گفتم که این پکیج یک سرور توسعه محلی را در اختیار ما می گذارد. برای ساده تر کردن اجرای این سرور به فایل package.json رفته و اسکریپت جدیدی به نام start را در آن تعریف می کنیم:
"scripts": { "dev": "webpack --mode development", "test": "echo \"Error: no test specified\" && exit 1", "start": "webpack serve --open google-chrome" },
من از لینوکس استفاده می کنم به همین دلیل برای باز کردن مرورگر خودم (گوگل کروم) از نام google-chrome استفاده کرده ام. برای باز کردن فایرفاکس باید به شکل زیر عمل کنید:
"start": "webpack serve --open 'Firefox'"
همچنین اگر از ویندوز استفاده می کنید ممکن است به جای google-chrome از Google Chrome یا Chrome یا امثال آن استفاده کنید. هر سیستمی متفاوت است بنابراین باید بگردید و مقدار مناسب برای سیستم خودتان را پیدا کنید.
پس از تعریف اسکریپت بالا حالا می توانید npm start را اجرا کنید. با انجام این کار مرورگر در آدرس http://localhost:8080 باز می شود و فایل HTML شما را نشان می دهد که برای من یک پاراگراف ساده به شکل زیر بود:
This is a paragraph element in the index.html file.
همانطور که گفتم loader ها افزونه هایی هستند که به وبپک اجازه کار با فایل های مختلفی را می دهند. معمولا loader ها پیکربندی پیچیده تری دارند. این پیکربندی معمولا ساختار کلی زیر را دارد:
module.exports = { module: { rules: [ { test: /\.filename$/, use: ["loader-b", "loader-a"] } ] }, // };
به عبارتی ابتدا کلید module را داریم و درون آن هر گروه از loader ها را پیکربندی می کنیم. در واقع هر rules یک گروه loader جداگانه است. چرا گروه؟ به دلیل اینکه loader ها در بسیاری از مواقع در کنار هم استفاده می شوند و برای کار با فایلی خاص معمولا چند loader داریم (مانند مثال بالا که loader-a و loader-b را داشتیم). ساختار درون rules معمولا دو بخش اصلی دارد:
نکته: test در واقع کاری می کند که وبپک، پسوند های خاص را به چشم یک ماژول جاوا اسکریپتی ببیند! در ادامه این موضوع برایتان روشن تر می شود.
بیایید این موضوع را با فایل های CSS تمرین کنیم.
برای تست کردن کدهای CSS در وبپک ابتدا به پوشه src رفته و یک فایل به نام style.css را در آن بسازید. حالا چند استایل ساده را به آن اضافه می کنیم:
h1 { color: orange; } p { font-size: 18px; font-style: italic; }
یعنی تگ های h1 نارنجی رنگ بوده و تگ های p نیز به صورت مورب نمایش داده شوند. حالا به فایل index.html می رویم تا یک تگ <h1> را تعریف کنیم:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Roxo Webpack</title> </head> <body> <h1>The Title</h1> <p>This is a paragraph element in the index.html file.</p> </body> </html>
در آخرین مرحله باید فایل CSS خود را درون فایل index.js (فایل جاوا اسکریپتی) وارد کنیم. چرا؟ همانطور که گفتم loader ها کاری می کنند که کدهای شما به عنوان یک ماژول جاوا اسکریپتی دیده شود بنابراین به index.js رفته و می گوییم:
import "./style.css"; console.log("Hello Webpack from index.js");
اما قبل از اینکه بخواهیم صفحه را تست کنیم باید دو loader برای زبان CSS را نصب و پیکربندی کنیم:
برای نصب این دو loader دستور زیر را اجرا کنید:
npm i css-loader style-loader --save-dev
پس از اتمام نصب به فایل webpack.config.js می رویم تا این دو loader را نیز پیکربندی کنیم:
const HtmlWebpackPlugin = require("html-webpack-plugin"); const path = require("path"); module.exports = { mode: "development", plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, "src", "index.html") }) ], module: { rules: [ { test: /\.css$/i, use: ["style-loader", "css-loader"] } ] } };
همانطور که می بینید کلیدی به نام module را تعریف کرده ایم که یک rules دارد. در بخش test پسوند css را با regular expression ها مشخص کرده ایم و دو loader خود را نیز پاس داده ایم. حالا دستور npm start را دوباره اجرا کنید (اگر در حال اجرا است، آن را متوقف کرده و دوباره اجرا کنید). با این کار دوباره آدرس http://localhost:8080 در مرورگر باز شده و این بار عنوان نارنجی رنگ و پاراگراف نیز مورب است.
این loader ها کاری کرده اند که وبپک فایل CSS ما را به عنوان یک ماژول جاوا اسکریپتی ببیند. برای اینکه این مسئله به شما ثابت شود می توانید به فایل index.js رفته و خط import را کامنت کنید:
// import "./style.css"; console.log("Hello Webpack from index.js");
با انجام این کار دیگر از استایل ها خبری نیست:
تنها مسئله باقی مانده این است که در حال حاضر تمام کدهای CSS ما مستقیما در تگ <head> فایل index.html قرار می گیرند و در فایلی جداگانه نیستند. اگر می خواهید کدهای CSS را در یک فایل CSS جداگانه داشته باشید باید از پلاگین هایی مانند MiniCssExtractPlugin استفاده کنید. در قدم اول آن را نصب می کنیم:
npm install --save-dev mini-css-extract-plugin
سپس به webpack.config.js می رویم و این پلاگین را به آن اضافه می کنیم:
const HtmlWebpackPlugin = require("html-webpack-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const path = require("path"); module.exports = { mode: "development", plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, "src", "index.html") }), new MiniCssExtractPlugin() ], module: { rules: [ { test: /\.css$/i, use: [MiniCssExtractPlugin.loader, "css-loader"] } ] } };
توجه کنید که من loader دیگرمان به نام style-loader را حذف کرده ام چرا که با پلاگین MiniCssExtractPlugin تداخل دارد. چرا؟ به دلیل اینکه استایل ها یا باید توسط یک فایل جداگانه بارگذاری شوند و یا اینکه به صورت inline باشند، راه سومی نداریم. در نهایت با دستور npm run dev می توانید کدهای خود را کامپایل کرده و نتیجه را در پوشه dist مشاهده کنید. من فعلا به همان style-loader برمی گردم.
در وبپک ترتیب loader های شما اهمیت بسیار زیادی دارد و ممکن است باعث خراب شدن کدهایتان شود. به کد ساده زیر توجه کنید:
module.exports = { module: { rules: [ { test: /\.css$/, use: ["css-loader", "style-loader"] } ] }, // };
اگر این کد را در تنظیمات پیکربندی خود بنویسید به مشکل برخواهید خورد. چرا؟ به دلیل اینکه ابتدا از style-loader استفاده کرده ایم! شاید بگویید style-loader که عضو دوم آرایه است، چرا من می گویم اول است؟ loader ها در وبپک از راست به چپ بارگذاری می شوند. بسیاری از افراد این نکته مهم را نمی دانند و در درک loader ها دچار مشکل می شوند. وظیفه style-loader تزریق کردن کدهای CSS درون HTML است اما قبل از css-loader آمده است که وظیفه اش بارگذاری کدهای CSS است. با این حساب css-loader باید همیشه در ابتدا باشد تا اول کدهای CSS را بارگذاری کند و سپس به style-loader پاس بدهد و آن هم کدها را در HTML تزریق کند.
برای حل مشکل بالا می توانیم این دو loader را جا به جا کنیم:
module.exports = { module: { rules: [ { test: /\.css$/, use: ["style-loader", "css-loader"] } ] }, // };
با انجام این کار مشکل به طور کامل برطرف می شود.
همانطور که می دانید SASS یک پیش پردازنده برای زبان CSS است و فایل های آن نیز پسوند scss را دارند. سوال اینجاست که چطور می توانیم آن ها را در وبپک جای بدهیم؟ برای انجام این کار ابتدا فایل src/style.scss را در پروژه خود ایجاد کنید. من محتویات این فایل را به شکل زیر نوشته ام اما شما می توانید هر کد SASS دیگری را نیز بنویسید:
@import url("https://fonts.googleapis.com/css?family=Karla:weight@400;700&display=swap"); $font: "Karla", sans-serif; $primary-color: #3e6f9e; body { font-family: $font; color: $primary-color; }
همانطور که می بینید من رنگ متون درون body را روی آبی کم رنگ گذاشته ام و فونت را نیز عوض کرده ام اما کار خاص دیگری انجام نداده ام. یادتان باشد که همه چیز در وبپک یک ماژول است بنابراین باید این فایل را نیز در فایل index.js (پوشه src) وارد کنیم:
import "./style.css"; import "./style.scss"; console.log("Hello Webpack from index.js");
حالا برای اینکه بتوانیم این کدها را برای کامپایل شدن به وبپک بدهیم نیاز به خود پکیج SASS برای node.js و سه loader زیر داریم:
دو مورد از این loader ها را از قبل داریم. برای نصب همه آن ها در یک دستور به شکل زیر عمل می کنیم:
npm i css-loader style-loader sass-loader sass --save-dev
پس از اتمام فرآیند نصب به فایل webpack.config.js رفته و loader ها را برای فایل های scss پیکربندی می کنیم:
const HtmlWebpackPlugin = require("html-webpack-plugin"); const path = require("path"); module.exports = { mode: "development", plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, "src", "index.html") }), ], module: { rules: [ { test: /\.css$/i, use: ["style-loader", "css-loader"] }, { test: /\.scss$/i, use: ["style-loader", "css-loader", "sass-loader"] } ], } };
همانطور که می بینید من یک شیء جدید را در قسمت rules اضافه کرده ام که test آن مربوط به شناسایی فایل های scss می باشد. در این بخش باید اهمیت ویژه ای به ترتیب این loader ها بدهید: در ابتدا sass-loader اجرا می شود و کدهای sass شما را می خواند و به css ساده کامپایل می کند، سپس css-loader کدهای css را به صورت ماژول می خواند و در نهایت style-loader آن کدها را به HTML تزریق می کند.
در حال حاضر اگر npm start را اجرا کرده و به مرورگر نگاهی بیندازید، متوجه آبی بودن متن پاراگراف (تگ <p>) خواهید شد بنابراین کدهای SASS ما به درستی عمل کرده اند.
یکی دیگر از مزایای اصلی وبپک این است که به شما اجازه می دهد کدهای جاوا اسکریپتی خود را در طبق آخرین نسخه های آن (ECMASCRIPT جدید) بنویسید. اگر در حالت عادی از نسخه های جدید جاوا اسکریپت استفاده کنید، ممکن است هنوز در بسیاری از مرورگرها پشتیبانی نشده باشد و به مشکل برخورد کنید اما در وبپک می توانید از Babel استفاده کنید تا کدهایتان را به نسخه های قدیمی تر جاوا اسکریپت تبدیل کنید. در این حالت شما می توانید کدنویسی راحت تری داشته باشید چرا که از بهترین و آخرین قابلیت ها استفاده می کنید و در عین حال پشتیبانی کاملی از تمام مرورگرها خواهید داشت.
البته همانطور که گفتم وبپک به تنهایی نمی تواند این کار را انجام بدهد و از پکیج دیگری به نام Babel (یا به طور دقیق تر از یک loader به نام babel-loader) استفاده می کند. قبل از شروع کار باید پکیج های زیر را نصب کنیم:
برای نصب این پکیج ها ترمینال خود را باز کرده و می گوییم:
npm i @babel/core babel-loader @babel/preset-env --save-dev
پس از اتمام فرآیند نصب باید Babel را پیکربندی کنیم. پیکربندی Babel از پیکربندی وبپک جدا است بنابراین نیاز به یک فایل دیگر به نام babel.config.json داریم. اگر بخواهیم از این فایل استفاده کنیم باید تمام فرآیند پیکربندی را از صفر انجام بدهیم که کار خسته کننده ای است. برای حل این مشکل پکیجی به نام babel preset env وجود دارد که بالاتر نیز توضیح دادم. این پکیج پیکربندی ساده babel را به صورت خودکار انجام می دهد بنابراین فایل babel.config.json ایجاد کنید و محتوای زیر را در آن قرار دهید:
{ "presets": [ "@babel/preset-env" ] }
از این به بعد پیکربندی توسط preset-env انجام می شود. در مرحله بعدی باید به وبپک بگوییم که برای خواندن و بارگذاری فایل های جاوا اسکریپت از babel استفاده کند بنابراین به webpack.config.js می رویم:
const HtmlWebpackPlugin = require("html-webpack-plugin"); const path = require("path"); module.exports = { mode: "development", plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, "src", "index.html") }), ], module: { rules: [ { test: /\.css$/i, use: ["style-loader", "css-loader"] }, { test: /\.scss$/i, use: ["style-loader", "css-loader", "sass-loader"] }, { test: /\.js$/i, exclude: /node_modules/, use: ["babel-loader"] } ], } };
همانطور که مشخص است تنها باید فایل های js را هدف بگیرید و حتما node_modules را exclude کنید. یعنی چه؟ زمانی که در test می گوییم فایل های js باید هدف گرفته شوند، وبپک در کل پروژه به دنبال فایل هایی با پسوند js می گردد. بخشی از پروژه ما در پوشه ای به نام node_modules است که دارای هزاران فایل js از کتابخانه های نصب شده و وابستگی هایشان می باشد. خصوصیت exclude به شما اجازه می دهد یک پوشه خاص را «نادیده» بگیرید بنابراین بدین شکل وبپک فایل های درون پوشه node_modules را بررسی نمی کند. در نهایت برای loader نیز از babel-loader استفاده کرده ایم.
برای اینکه مطمئن شویم babel کار می کند به فایل index.js می رویم و در آنجا از چند ویژگی مدرن جاوا اسکریپت استفاده می کنیم:
import "./style.css"; import "./style.scss"; console.log("Hello Webpack from index.js"); const fancyFunc = () => { return [1, 2]; }; const [a, b] = fancyFunc();
من در اینجا از arrow function ها و destructuring استفاده کرده ام که هر دو از قابلیت های مدرن جاوا اسکریپت هستند. حالا دستور npm run dev را در ترمینال اجرا کنید تا فایل خروجی شما در پوشه dist ایجاد شود. در این پوشه فایلی به نام main.js خواهید داشت و اگر در آن به دنبال تعریف تابع fancyFunc بگردید، چنین کدی را می بینید:
\n\nvar fancyFunc = function fancyFunc() {\n return [1, 2];\n};\n\nvar _fancyFunc = fancyFunc(),\n _fancyFunc2 = _slicedToArray(_fancyFunc, 2),\n a = _fancyFunc2[0],\n b = _fancyFunc2[1];\n\n//# sourceURL=webpack:///./src/index.js?"
همانطور که می بینید تعریف تابع ما به نسخه ES5 برگشته است. اگر از babel استفاده نمی کردیم، کدهای نهایی ما به همان صورت نسخه جدید باقی می ماند:
\n\nconsole.log(\"Hello webpack!\");\n\nconst fancyFunc = () => {\n return [1, 2];\n};\n\nconst [a, b] = fancyFunc();\n\n\n//# sourceURL=webpack:///./src/index.js?");
توجه داشته باشید که استفاده از babel فقط زمانی نیاز است که بخواهید کدهایتان را به نسخه های قدیمی تر کامپایل کنید یا به ویژگی خاصی از babel نیاز دارید. در غیر این صورت وبپک به تنهایی کافی است.
اگر از توسعه دهندگان react باشید حتما نام پروژه create-react-app را شنیده اید. این پروژه یک پروژه آماده شده از قبل است که در آن تنظیمات وبپک، babel و غیره انجام شده است. در بیشتر پروژه های react استفاده از این پکیج پیشنهاد می شود چرا که کارتان را بسیار ساده می کند اما در برخی از اوقات نیاز به تغییر برخی تنظیمات خاص را دارید که در create-react-app در دسترس نیست بنابراین مجبور به پیکربندی دستی آن می شوید.
خوشبختانه این کار آنقدر پیچیده نیست. در ابتدا باید پکیج های مورد نیاز را نصب کنیم:
npm i @babel/core babel-loader @babel/preset-env @babel/preset-react --save-dev
در نظر داشته باشید که علاوه بر preset-env از preset-react نیز استفاده کرده ام. ما به هر دو پکیج نیاز داریم. در مرحله بعدی به babel.config.json رفته و preset-react را به آن اضافه می کنیم:
{ "presets": ["@babel/preset-env", "@babel/preset-react"] }
پس از انجام این کار می توانیم شروع به نصب react کنیم:
npm i react react-dom
پس از اتمام فرآیند نصب، می توانیم به فایل index.js برویم تا یک کامپوننت react را در آن تعریف کنیم. من برای شلوغ نشدن این فایل، کدهای بخش babel (تابع fancyFunc و غیره) را از index.js حذف می کنم و سپس شروع به تعریف یک کامپوننت react می کنیم:
import "./style.css"; import "./style.scss"; import React, { useState } from "react"; import { render } from "react-dom"; console.log("Hello Webpack from index.js"); function App() { const [state, setState] = useState("CLICK ME"); return <button onClick={() => setState("CLICKED")}>{state}</button>; } render(<App />, document.getElementById("root"));
برای درک کد بالا باید با react آشنا باشید. ما در کد بالا کامپوننت App خودمان را در عنصری با آیدی root نمایش می دهیم. این کامپوننت فقط یک دکمه ساده است که با کلیک روی آن State خاصی تغییر پیدا می کند. من در فایل index.html هیچ عنصری ندارم که آیدی root داشته باشد بنابراین به این فایل می رویم و یک div با این آیدی را تعریف می کنیم:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Roxo Webpack</title> </head> <body> <h1>The Title</h1> <p>This is a paragraph element in the index.html file.</p> <div id="root"></div> </body> </html>
حالا کارمان تمام شده است. برای تست این کد، دستور npm start را در ترمینال خود اجرا کنید تا مرورگر صفحه وب شما را باز کند. اگر همه چیز را درست انجام داده باشید، یک دکمه را در صفحه خود می بینید که متن آن Click Me است و با کلیک روی آن به Clicked تغییر پیدا می کند. بدین ترتیب متوجه می شویم که کدهای ما به درستی کار می کنند.
همانطور که بارها در این مقاله توضیح داده ام، وبپک همه چیز را به عنوان ماژول جاوا اسکریپتی در نظر می گیرد اما هنوز در مورد اصل ماجرا یا ماژول های جاوا اسکریپتی واقعی (ES modules) صحبت نکرده ایم. احتمالا شما نیز نام هایی مانند AMD modules یا UMD یا Common JS را شنیده باشید. این ماژول ها در طول سال های مختلف برای حل یک مشکل بزرگ درجاوا اسکریپت ارائه شده بودند: عدم وجود روشی برای استفاده چند باره از یک کد. تمام این سیستم های نصفه و نیمه تا قبل از سال ۲۰۱۵ ارائه شده بودند تا راه حلی برای این مشکل باشند اما بالاخره در سال ۲۰۱۵ و با معرفی ES Modules جاوا اسکریپت یک سیستم ماژول واقعی را به وجود آورد.
اگر بخواهید از مرورگرهای قدیمی تر نیز پشتیبانی کنید، نمی توانید مستقیما ES Modules را در پروژه های خود به کار ببرید چرا که از سال ۲۰۱۵ مدتی طول کشید تا مرورگرهای مختلف این قابلیت را پیاده سازی کنند. اینجاست که وبپک می تواند مشکل شما را حل کند. برای تمرین ابتدا به پوشه src رفته و یک پوشه دیگر به نام common را ایجاد کنید. در این پوشه فایلی به نام usersAPI.js را بسازید که محتوای زیر را داشته باشد:
const ENDPOINT = "https://jsonplaceholder.typicode.com/users/"; export function getUsers() { return fetch(ENDPOINT) .then(response => { if (!response.ok) throw Error(response.statusText); return response.json(); }) .then(json => json); }
همانطور که می بینید این یک کد ساده است که از سرور تمرینی jsonplaceholder لیست ساده ای از کاربران را دریافت می کند. توجه کنید که من این تابع را export کرده ام بنابراین می توانیم به فایل index.js برگشته و آن را وارد فایل کنیم:
import "./style.css"; import "./style.scss"; import React, { useState } from "react"; import { render } from "react-dom"; import { getUsers } from "./common/usersAPI"; getUsers().then(json => console.log(json)); console.log("Hello Webpack from index.js"); function App() { const [state, setState] = useState("CLICK ME"); return <button onClick={() => setState("CLICKED")}>{state}</button>; } render(<App />, document.getElementById("root"));
همانطور که می بینید من getUsers را import کرده ام و حالا می توانم به راحتی از آن استفاده کنم. حالا اگر npm start را اجرا کنید، مثل همیشه مرورگر برایتان باز می شود و مشکلی نداریم اما در صورتی که console را از dev tools مرورگر باز کنید، ۱۰ کاربر دریافت شده از JSONPlaceholder را خواهید دید:
طبیعتا ما در حال development یا توسعه هستیم اما اگر می خواهید کدهای production تولید شوند (یعنی minification و بهینه سازی ها انجام شوند) بهتر است در package.json یک اسکریپت جداگانه را برایش تعریف کنید:
"scripts": { "dev": "webpack --mode development", "start": "webpack serve --open 'Firefox'", "build": "webpack --mode production" },
از این به بعد با اجرای npm run build نتیجه minify شده کدهایتان را در پوشه src می بینید. ما بعدا از این اسکریپت استفاده می کنیم بنابراین حتما آن را به package.json اضافه کنید.
شکستن کدها به تکنیکی گفته می شود که در آن دو هدف اصلی را دنبال می کنیم:
در حال حاضر هیچ محدودیت حجمی رسمی برای فایل های جاوا اسکریپت در نظر گرفته نشده است اما جامعه وبپک حداکثر حجم فایل اولیه جاوا اسکرپتی یک سایت را ۲۰۰ یا بعضا ۲۴۴ کیلوبایت مشخص می کنند. چرا؟ به دلیل اینکه جاوا اسکریپت از نظر زمان، هزینه بر است. طبق آمار وب سایت v8.dev در سال ۲۰۱۹ حدود ۱۰ الی ۳۰ درصد کل زمان بارگذاری صفحات وب مربوط به جاوا اسکریپت بوده است.
در وبپک سه راه اصلی برای انجام code splitting وجود دارد:
روش اول (تعریف چند نقطه ورودی) معمولا برای پروژه های کوچک بدون اشکال است اما در پروژه های بزرگ باعث مشکلات فراوانی می شود به همین دلیل من اصلا به آن نمی پردازم و در این بخش به سراغ روش های دوم و سوم می رویم.
احتمالا نام کتابخانه بزرگی به نام Moment.js را شنیده باشید. این کتابخانه یک پکیج کامل برای نمایش و ویرایش تاریخ و زمان در پروژه های جاوا اسکریپتی شما است. ما فرض می کنیم پروژه ما از Moment.js استفاده می کند بنابراین آن را نصب می کنیم:
npm i moment
حالا تمام محتوای فایل src/index.js را حذف کنید و فقط کتابخانه moment را در آن import کنید:
import moment from "moment";
حالا دستور npm run build را اجرا کنید. اجرای این دستور چند لحظه طول می کشد اما نهایتا گزارشی به شکل زیر دریافت می کنید:
asset main.js 290 KiB [compared for emit] [minimized] [big] (name: main) 1 related asset asset index.html 375 bytes [compared for emit] runtime modules 786 bytes 4 modules modules by path ./node_modules/moment/locale/*.js 499 KiB 135 modules ./src/index.js 28 bytes [built] [code generated] ./node_modules/moment/moment.js 170 KiB [built] [code generated] ./node_modules/moment/locale/ sync ^\.\/.*$ 3.21 KiB [optional] [built] [code generated] WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB). This can impact web performance. Assets: main.js (290 KiB) WARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance. Entrypoints: main (290 KiB) main.js WARNING in webpack performance recommendations: You can limit the size of your bundles by using import() or require.ensure to lazy load some parts of your application. For more info visit https://webpack.js.org/guides/code-splitting/ webpack 5.36.2 compiled with 3 warnings in 9830 ms
همانطور که در گزارش بالا مشاهده می کنید چند خطا در رابطه با حجم بالای فایل جاوا اسکریپتی خود گرفته ایم که فعلا اهمیتی ندارد. بخش مهم، خط اول این گزارش است که می گوید حجم bundle نهایی یا فایل جاوا اسکریپتی نهایی ما ۲۹۰ کیلوبایت بوده است. شاید تعجب کرده باشید! ما که فقط چند خط کد جاوا اسکریپتی نوشته بودیم!چطور ممکن است ۲۹۰ کیلوبایت حجم آن باشد؟
مشکل اینجاست که کل کتابخانه moment.js در پروژه ما قرار گرفته است چرا که جزئی از پروژه ما است. ما می توانیم با استفاده از optimization.splitChunks کتابخانه moment.js را از فایل اصلی جاوا اسکریپتی خودمان خارج کنیم. برای این کار ابتدا به فایل webpack.config.js رفته و کلید optimization را به این فایل اضافه می کنیم:
const HtmlWebpackPlugin = require("html-webpack-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const path = require("path"); module.exports = { mode: "development", plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, "src", "index.html") }), new MiniCssExtractPlugin() ], module: { rules: [ { test: /\.css$/i, use: [MiniCssExtractPlugin.loader, "css-loader"] }, { test: /\.scss$/i, use: ["style-loader", "css-loader", "sass-loader"] }, { test: /\.js$/i, exclude: /node_modules/, use: ["babel-loader"] } ], }, optimization: { splitChunks: { chunks: "all" } }, };
دقت کنید که من خصوصیت optimization را خارج از module اضافه کرده ام. در این بخش نیز به chunks مقدار all را داده ام که یعنی تمام chunk های جاوا اسکریپتی را از هم جدا کند. حالا دوباره npm run build را اجرا می کنیم. این بار نتیجه زیر را می گیرید:
asset 762.js 285 KiB [emitted] [minimized] [big] (id hint: vendors) 1 related asset asset main.js 5.23 KiB [emitted] [minimized] (name: main) asset index.html 419 bytes [emitted] // بقیه گزارش و هشدار ها
همانطور که می بینید این بار دو فایل را داریم؛ فایل اول (js.762) همان کتابخانه های مربوطه است و فایل دوم main.js یا کدهای نوشته شده توسط خودمان می باشد که فقط ۵ کیلوبایت حجم گرفته است. بدین صورت مطمئن می شویم که در ابتدا فقط فایل ضروری (main.js) را بارگذاری می کنیم و بارگذاری فایل های دیگر را در پس زمینه انجام می دهیم.
نکته: کتابخانه moment.js یک کتابخانه بسیار بزرگ است (حجم تقریبا ۳۰۰ کیلوبایتی غیر قابل قبول است). من پیشنهاد می کنم هیچگاه از این کتابخانه در پروژه هایتان استفاده نکنید مگر اینکه مجبور باشید. گزینه های بهتر برای مدیریت تاریخ و زمان در پروژه هایتان، کتابخانه هایی مانند date-fns و luxon هستند.
روش قوی تری از بخش قبلی وجود دارد که پیشینه ای طولانی در وبپک دارد اما به تازگی (ECMAScript 2020) به جاوا اسکریپت اضافه شده است بنابراین پشتیبانی از آن تقریبا صفر است. کتابخانه هایی مانند Vue و React به شدت از این روش برای import هایشان استفاده می کنند گرچه React روش خاص خودش را دارد اما مفهوم کلی هنوز یکی است.
Code Splitting با این روش معمولا در دو سطح رخ می دهد:
برای مشاهده این مسئله در قالب یک مثال به فایل src/index.html رفته و تمام محتویات آن را پاک کنید تا از ابتدا شروع کنیم. من می خواهم یک فایل HTML ساده داشته باشم که درون بدنه اش دکمه ای قرار داشته باشد:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Dynamic imports</title> </head> <body> <button id="btn">Load!</button> </body> </html>
آیدی این دکمه بسیار مهم است بنابراین آن را فراموش نکنید. در مرحله بعدی مطمئن شوید که هنوز محتویات فایل usersAPI.js در پوشه common را دارید و چیزی را تغییر نداده اید:
const ENDPOINT = "https://jsonplaceholder.typicode.com/users/"; export function getUsers() { return fetch(ENDPOINT) .then(response => { if (!response.ok) throw Error(response.statusText); return response.json(); }) .then(json => json); }
حالا به فایل src/index.js بروید و تمام محتوای قبلی آن را پاک کنید، سپس یک event listener را برای دکمه موجود در فایل HTML تعریف کنید:
const btn = document.getElementById("btn"); btn.addEventListener("click", () => { // });
در حال حاضر اگر دستور npm run start را در ترمینال اجرا کنید، مثل همیشه مرورگر برایتان باز شده و صفحه HTML را به شما نشان می دهد اما طبیعتا با کلیک روی دکمه هیچ اتفاقی نمی افتد چرا که ما در event listener بالا هیچ کاری نکرده ایم. تصور کنید که هدف ما بارگذاری کاربران از JSONPlaceholder پس از کلیک روی دکمه HTML ای باشد. ساده ترین راه این است که از import های پویا استفاده نکرده و به شکل عادی import را انجام بدهیم:
import { getUsers } from "./common/usersAPI"; const btn = document.getElementById("btn"); btn.addEventListener("click", () => { getUsers().then(json => console.log(json)); });
مشکل روش بالا این است که ES module ها استاتیک یا ایستا هستند. یعنی چه؟ یعنی ما نمی توانیم آن ها را در runtime (زمانی که کدها در حال اجرا شدن هستند) تغییر داده یا ویرایش کنیم. این در حالی است که با dynamic import ها می توانیم زمان بارگذاری و import شدن یک ماژول را نیز مشخص کنیم:
const getUserModule = () => import("./common/usersAPI"); const btn = document.getElementById("btn"); btn.addEventListener("click", () => { getUserModule().then(({ getUsers }) => { getUsers().then(json => console.log(json)); }); });
همانطور که می بینید من یک تابع به نام getUserModule را تعریف کرده ام که از تابعی به نام import استفاده می کند. این تابع به صورت پویا ماژول ما را بارگذاری می کند و یک promise را به ما برمی گرداند. با انجام این کار ماژول فقط زمانی بارگذاری می شود که کاربر روی دکمه کلیک کرده باشد. برای تست این موضوع دوباره دستور npm start را اجرا کنید و هنگامی که مرورگر باز شد از dev tools به سربرگ console بروید. حالا روی دکمه HTML خودمان در صفحه کلیک کنید. با انجام این کار پس از گذشت زمان اندکی ۱۰ کاربر برایتان بارگذاری می شوند که از سربرگ console قابل مشاهده هستند.
ممکن است ساختار بالا کمی برایتان عجیب باشد بنابراین بگذارید آن را بشکنم. ما ابتدا event listener را تعریف کرده ایم و سپس ماژول getUser را به صورت پویا بارگذاری کرده ایم. طبیعتا این بارگذاری مدتی طول می کشد بنابراین یک promise برمی گرداند:
btn.addEventListener("click", () => { getUserModule().then(/**/); });
در قدم بعدی از قابلیت object destructuring در جاوا اسکریپت استفاده کرده ایم تا متد getUsers را از ماژول مورد نظر استخراج کنیم:
btn.addEventListener("click", () => { getUserModule().then(({ getUsers }) => { // }); });
و نهایتا آن را صدا زده و نتیجه اش را در کنسول مرورگر چاپ کرده ایم:
btn.addEventListener("click", () => { getUserModule().then(({ getUsers }) => { getUsers().then(json => console.log(json)); }); });
اگر در هنگام کلیک روی دکمه، سربرگ network از developer tools مرورگر خود را باز کنید، متوجه حضور یک فایل جاوا اسکریپتی دیگر می شوید که همان ماژول شما است. نام این فایل جاوا اسکریپتی معمولا به صورت خودکار انتخاب می شود و نامی عجیب است:
برای انتخاب نام آن می توانیم از webpackChunkName به صورت کامنت استفاده کنیم:
const getUserModule = () => import(/* webpackChunkName: "usersAPI" */ "./common/usersAPI"); const btn = document.getElementById("btn"); btn.addEventListener("click", () => { getUserModule().then(({ getUsers }) => { getUsers().then(json => console.log(json)); }); });
با قرار دادن این کامنت در دستور import، وبپک می داند که منظور ما نام ماژول وارد شده در این صفحه است:
امیدوارم مقاله آموزش رایگان Webpack به درک شما از وبپک کمک کرده باشد. باید در نظر داشته باشید که دنیای وبپک بزرگتر از این تک مقاله است. به طور مثال مباحثی مانند caching (کش کردن ماژول ها در مرورگر) یا preloading (پیش بارگذاری ماژول ها) باقی مانده است که از مباحث پیشرفته تر هستند اما مفاهیم اصلی و پایه ای بحث همین مواردی بود که خدمت شما توضیح دادم. از این به بعد باید بتوانید به راحتی پروژه های وبپک خود را راه بیندازید و در صورت نیاز به مباحث پیشرفته تر به documentation رسمی آن مراجعه کنید.
منبع: وب سایت valentinog
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.