في أحدث بث مباشر من سلسلة Supercharged، نفّذنا ميزة "تقسيم الرموز" وميزة "تقسيم المحتوى حسب المسار". باستخدام HTTP/2 ووحدات ES6 الأصلية، ستصبح هذه الأساليب ضرورية لتفعيل loading وcaching لموارد النصوص البرمجية بكفاءة.
نصائح وحيل متنوعة في هذه الحلقة
asyncFunction().catch()
معerror.stack
: 9:55- الوحدات وسمة
nomodule
في علامات<script>
: 7:30 promisify()
في العقدة 8: 17:20
TL;DR
كيفية تقسيم الرموز البرمجية من خلال تقسيمها إلى أجزاء استنادًا إلى المسار:
- الحصول على قائمة بنقاط الدخول
- استخرِج تبعيات الوحدة لكل نقاط الدخول هذه.
- العثور على التبعيات المشتركة بين جميع نقاط الدخول
- حِزم التبعيات المشتركة
- إعادة كتابة نقاط الدخول
تقسيم الرموز البرمجية مقابل تجميع الصفحات المستند إلى المسار
يرتبط تقسيم الرموز البرمجية والتجميع المستنِد إلى المسار ارتباطًا وثيقًا، ويتم استخدامهما غالبًا بشكل تبادلي. وقد أدّى ذلك بدوره إلى حدوث بعض الالتباس. لنوضّح الأمر:
- تقسيم الرموز البرمجية: تقسيم الرموز البرمجية هو عملية تقسيم الرمز البرمجي إلى حِزم متعددة. إذا لم تكن بصدد إرسال حِزمة كبيرة واحدة تتضمّن كل ملفّات JavaScript إلى العميل، يعني ذلك أنّك بصدد تقسيم الرموز البرمجية. من الطرق المحدّدة لتقسيم الرمز هو استخدام تقسيم الرمز المستنِد إلى المسار.
- تقسيم البيانات استنادًا إلى المسار: يؤدي تقسيم البيانات استنادًا إلى المسار إلى إنشاء حِزم مرتبطة بمسارات تطبيقك. من خلال تحليل مسارات التطبيق وتبعياتها، يمكننا تغيير الوحدات التي تُضاف إلى الحِزمة.
ما أهمية تقسيم الرموز البرمجية؟
الوحدات غير المُثبَّتة
باستخدام وحدات ES6 الأصلية، يمكن لكل وحدة JavaScript استيراد التبعيات الخاصة بها. عندما يتلقّى المتصفّح
وحدة، ستؤدي جميع عبارات import
إلى عمليات جلب إضافية للحصول على
الوحدات اللازمة لتشغيل الرمز. ومع ذلك، يمكن أن تحتوي كل هذه الوحدات على تبعيات
خاصة بها. يكمن الخطر في أنّ المتصفح ينتهي به المطاف بسلسلة من عمليات الجلب التي تستمر لعدة
عمليات ذهاب وإياب قبل أن يتم تنفيذ الرمز أخيرًا.
التجميع
إنّ التجميع، الذي يتمثل في تضمين جميع وحداتك في حِزمة واحدة، سيؤدّي إلى التأكّد من توفّر كل الرموز البرمجية التي يحتاجها المتصفّح بعد جولة واحدة، ويمكنه بدء تنفيذ الرمز البرمجي بشكل أسرع. ومع ذلك، يفرض هذا الإجراء على المستخدم تنزيل الكثير من الرموز البرمجية غير الضرورية، ما يؤدي إلى إهدار النطاق الزمني وسرعة نقل البيانات. بالإضافة إلى ذلك، سيؤدي كل تغيير في إحدى وحداتنا الأصلية إلى تغيير في الحِزمة، ما يؤدي إلى إلغاء صلاحية أي إصدار محفوظ مؤقتًا من الحِزمة. على المستخدمين إعادة تنزيل التطبيق بالكامل.
تقسيم الرموز البرمجية
ويعدّ تقسيم الرموز البرمجية خيارًا وسطًا. نحن على استعداد لإجراء عمليات تبادل بيانات إضافية ذهابًا وإيابًا للحصول على فعالية في الشبكة من خلال تنزيل ما نحتاجه فقط، وفعالية أفضل في التخزين المؤقت من خلال تقليل عدد الوحدات لكل حزمة. إذا تم إجراء عملية التجميع بشكل صحيح، سيكون إجمالي عدد التنقّلات في جولة واحدة أقل بكثير من عدد التنقّلات في حال استخدام وحدات غير مرتبطة ببعضها. أخيرًا، يمكننا الاستفادة من آليات التحميل المُسبَق مثل link[rel=preload]
لتوفير وقت إضافي في عمليات التحميل الثلاثية إذا لزم الأمر.
الخطوة 1: الحصول على قائمة بنقاط الدخول
هذه ليست سوى إحدى الطرق العديدة، ولكن في الحلقة، حلّلنا
sitemap.xml
للموقع الإلكتروني من أجل الحصول على نقاط الدخول إلى موقعنا الإلكتروني. وعادةً ما يتم استخدام ملف JSON مخصّص يسرد جميع نقاط الدخول.
استخدام Babel لمعالجة JavaScript
يُستخدَم Babel بشكل شائع لعملية "التحويل البرمجي": استخدام رمز JavaScript ذي الإصدار الأحدث وتحويله إلى إصدار أقدم من JavaScript حتى يتمكّن المزيد من المتصفّحات من تنفيذ الرمز. الخطوة الأولى هنا هي تحليل JavaScript الجديد باستخدام محلل (يستخدم Babel babylon) الذي يحوّل الرمز إلى ما يُعرف باسم "شجرة نحو تجريدي" (AST). بعد إنشاء AST، تعمل سلسلة من المكوّنات الإضافية على تحليل AST وتحويله.
سنستخدم babel بشكل كبير لرصد عمليات استيراد ملف برمجي JavaScript (ومعالجتها لاحقًا). قد تميل إلى اللجوء إلى التعبيرات العادية، ولكن التعبيرات العادية ليست فعّالة بما يكفي لتحليل لغة بشكل صحيح ومن الصعب الحفاظ عليها. سيساعدك الاعتماد على أدوات مجرّبة مثل Babel في حلّ العديد من المشاكل.
في ما يلي مثال بسيط على تشغيل Babel باستخدام مكوّن إضافي مخصّص:
const plugin = {
visitor: {
ImportDeclaration(decl) {
/* ... */
}
}
}
const {code} = babel.transform(inputCode, {plugins: [plugin]});
يمكن أن يقدّم المكوّن الإضافي عنصر visitor
. يحتوي الزائر على دالة لأي نوع عقدة يريد الإضافة التعامل معها. عند العثور على عقدة من هذا النوع أثناء التنقّل في AST، سيتم استدعاء الدالة المقابلة في عنصر visitor
باستخدام هذه العقدة كمَعلمة. في المثال
أعلاه، سيتم استدعاء الطريقة ImportDeclaration()
لكل بيان import
فيملف. للتعرّف أكثر على أنواع العقد وAST، يمكنك الاطّلاع على
astexplorer.net.
الخطوة 2: استخراج تبعيات الوحدة
لإنشاء شجرة التبعيات لوحدة، سنحلِّل هذه الوحدة وننشئ قائمة بكل الوحدات التي تستوردها. نحتاج أيضًا إلى تحليل هذه التبعيات، لأنّها قد تتضمّن بدورها تبعيات أخرى. مثال كلاسيكي على التكرار
async function buildDependencyTree(file) {
let code = await readFile(file);
code = code.toString('utf-8');
// `dep` will collect all dependencies of `file`
let dep = [];
const plugin = {
visitor: {
ImportDeclaration(decl) {
const importedFile = decl.node.source.value;
// Recursion: Push an array of the dependency’s dependencies onto the list
dep.push((async function() {
return await buildDependencyTree(`./app/${importedFile}`);
})());
// Push the dependency itself onto the list
dep.push(importedFile);
}
}
}
// Run the plugin
babel.transform(code, {plugins: [plugin]});
// Wait for all promises to resolve and then flatten the array
return flatten(await Promise.all(dep));
}
الخطوة 3: العثور على التبعيات المشتركة بين جميع نقاط الدخول
بما أنّ لدينا مجموعة من أشجار التبعية، أي غابة تبعية، يمكننا العثور على تبعيات المشترَكة من خلال البحث عن العقد التي تظهر في كل شجرة. سنزيل تكرار العناصر في الغابة ونفلترها للاحتفاظ بالعناصر التي تظهر في جميع الأشجار فقط.
function findCommonDeps(depTrees) {
const depSet = new Set();
// Flatten
depTrees.forEach(depTree => {
depTree.forEach(dep => depSet.add(dep));
});
// Filter
return Array.from(depSet)
.filter(dep => depTrees.every(depTree => depTree.includes(dep)));
}
الخطوة 4: تجميع التبعيات المشتركة
لتجميع مجموعة التبعيات المشتركة، يمكننا ببساطة تسلسل جميع ملفات الوحدات. تحدث مشكلتان عند استخدام هذا النهج: المشكلة الأولى هي أنّ الحِزمة ستظل تحتوي على عبارات
import
التي ستجعل المتصفّح يحاول جلب الموارد. المشكلة الثانية هي
عدم تجميع المكوّنات التابعة للمكوّنات الأخرى. بما أنّنا سبق أن فعلنا ذلك، سنكتفي
بكتابة مكوّن إضافي آخر من Babel.
تتشابه التعليمات البرمجية إلى حدٍ كبير مع المكوّن الإضافي الأول، ولكن بدلاً من استخراج عمليات الاستيراد فقط، سنزيلها أيضًا ونُدخِل نسخة مجمّعة من الملف المستورَد:
async function bundle(oldCode) {
// `newCode` will be filled with code fragments that eventually form the bundle.
let newCode = [];
const plugin = {
visitor: {
ImportDeclaration(decl) {
const importedFile = decl.node.source.value;
newCode.push((async function() {
// Bundle the imported file and add it to the output.
return await bundle(await readFile(`./app/${importedFile}`));
})());
// Remove the import declaration from the AST.
decl.remove();
}
}
};
// Save the stringified, transformed AST. This code is the same as `oldCode`
// but without any import statements.
const {code} = babel.transform(oldCode, {plugins: [plugin]});
newCode.push(code);
// `newCode` contains all the bundled dependencies as well as the
// import-less version of the code itself. Concatenate to generate the code
// for the bundle.
return flatten(await Promise.all(newCode)).join('\n');
}
الخطوة 5: إعادة كتابة نقاط الدخول
في الخطوة الأخيرة، سنكتب مكوّن Babel إضافيًا. ووظيفته هي إزالة جميع عمليات استيراد الوحدات التي تكون في الحِزمة المشتركة.
async function rewrite(section, sharedBundle) {
let oldCode = await readFile(`./app/static/${section}.js`);
oldCode = oldCode.toString('utf-8');
const plugin = {
visitor: {
ImportDeclaration(decl) {
const importedFile = decl.node.source.value;
// If this import statement imports a file that is in the shared bundle, remove it.
if(sharedBundle.includes(importedFile))
decl.remove();
}
}
};
let {code} = babel.transform(oldCode, {plugins: [plugin]});
// Prepend an import statement for the shared bundle.
code = `import '/static/_shared.js';\n${code}`;
await writeFile(`./app/static/_${section}.js`, code);
}
إنهاء
لقد كانت رحلة رائعة، أليس كذلك؟ يُرجى تذكُّر أنّ هدفنا من هذه الحلقة هو شرح ميزة "تقسيم الرموز البرمجية" وإزالة الغموض عنها. تعمل النتيجة، ولكنّها خاصة بموقعنا الإلكتروني التجريبي وستتحطم بشكلٍ فظيع في الحالة العامة. بالنسبة إلى مرحلة الإنتاج، أنصح بالاعتماد على أدوات راسخة مثل WebPack وRollUp وما إلى ذلك.
يمكنك العثور على الرموز البرمجية في مستودع GitHub.
أراكم في المرة القادمة!