في أحدث بث مباشر من سلسلة 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.
أراكم في المرة القادمة!