إعادة تحميل بنية أدوات مطوّري البرامج: النقل إلى وحدات JavaScript

Tim van der Lippe
Tim van der Lippe

كما تعلم، أدوات مطوّري البرامج في Chrome هي تطبيق ويب مكتوب باستخدام HTML وCSS وJavaScript. بمرور السنين، أصبحت أدوات مطوري البرامج تضم المزيد من الميزات وأكثر ذكاءً ودراية بشأن منصة الويب الأوسع نطاقًا. على الرغم من أنّ أدوات المطوّرين قد توسّعت على مرّ السنين، إلا أنّ بنيتها تشبه إلى حد كبير البنية الأصلية عندما كانت لا تزال جزءًا من WebKit.

هذه المشاركة هي جزء من سلسلة من المشاركات على المدونة التي توضّح التغييرات التي نجريها على بنية DevTools وكيفية إنشائها. سنوضّح آلية عمل DevTools في السابق، والفوائد والقيود التي كانت متوفّرة، والإجراءات التي اتّخذناها لتخفيف هذه القيود. لذلك، لنلقِ نظرة عن كثب على أنظمة الوحدات وكيفية تحميل الرموز البرمجية وكيفية استخدام وحدات JavaScript.

في البداية، لم يكن هناك

على الرغم من أنّ الواجهة الأمامية الحالية تتضمّن مجموعة متنوّعة من أنظمة الوحدات التي تتضمّن أدوات مبنية حولها، بالإضافة إلى تنسيق وحدات JavaScript الموحّد حاليًا، لم يكن أيّ من هذه الأنظمة متاحًا عند إنشاء "أدوات مطوري البرامج" لأول مرة. تم إنشاء أدوات المطوّرين استنادًا إلى رمز تم إرساله في البداية مع WebKit قبل أكثر من 12 عامًا.

نشأ أول إشارة إلى نظام وحدات في "أدوات مطوري البرامج" في عام 2012، وكان هذا الأسلوب عبارة عن تقديم قائمة بالوحدات مع قائمة بالمصادر المرتبطة بها. كان هذا جزءًا من بنية Python الأساسية التي استُخدمت في ذلك الوقت لتجميع أدوات مطوّري البرامج وإنشاؤها. أدّى تغيير في المتابعة إلى استخراج جميع الوحدات في ملف frontend_modules.json منفصل (commit) في عام 2013، ثم إلى ملفات module.json منفصلة (commit) في عام 2014.

مثال على ملف module.json:

{
  "dependencies": [
    "common"
  ],
  "scripts": [
    "StylePane.js",
    "ElementsPanel.js"
  ]
}

منذ عام 2014، يتم استخدام النمط module.json في "أدوات مطوري البرامج" لتحديد الوحدات وملفات المصدر. وفي الوقت نفسه، تطورت المنظومة المتكاملة للويب بسرعة وتم إنشاء تنسيقات متعددة للوحدات، بما في ذلك UMD وCommonJS ووحدات JavaScript المتوافقة مع المعايير في النهاية. ومع ذلك، تعذّر استخدام تنسيق "module.json" في "أدوات مطوري البرامج".

على الرغم من أنّ أدوات مطوري البرامج ظلت تعمل، كان هناك بعض الجوانب السلبية لاستخدام نظام وحدات فريد وغير موحّد:

  1. كان تنسيق module.json يتطلّب أدوات إنشاء مخصّصة، مثل أدوات تجميع الحِزم الحديثة.
  2. لم يحدث دمج بيئة التطوير المتكاملة (IDE)، ما تطلب أدوات مخصَّصة لإنشاء ملفات يمكن لبيئات IDE الحديثة فهمها (النص البرمجي الأصلي لإنشاء ملفات jsconfig.json لملف VS Code).
  3. تم وضع الدوال والفئات والكائنات في النطاق العام لتسهيل المشاركة بين الوحدات.
  4. كانت الملفات تعتمد على الطلب، ما يعني أنّ ترتيب إدراج sources كان مهمًا. ولم يكن هناك ما يضمن تحميل الرمز الذي تعتمد عليه، إلا أنّ أحد المراجعين قد تحقّق منه.

في المجمل، عند تقييم الحالة الحالية لنظام الوحدات في "أدوات مطوّري البرامج" وتنسيقات الوحدات الأخرى (الأكثر استخدامًا)، توصّلنا إلى أنّ نمط module.json كان يتسبب في حدوث مشاكل أكثر مما تم حلّها وكان الوقت قد حان للتخطيط للابتعاد عنه.

مزايا المعايير

من بين أنظمة الوحدات الحالية، اخترنا وحدات JavaScript كنظام لنقل البيانات إليه. في وقت اتّخاذ هذا القرار، كانت وحدات JavaScript لا تزال مضمّنة في علامة في Node.js، ولم تكن هناك حِزم وحدات JavaScript متوفّرة في NPM يمكننا استخدامها. ومع ذلك، توصّلنا إلى أنّ وحدات JavaScript هي الخيار الأفضل.

تتمثل الفائدة الأساسية من وحدات JavaScript في أنّها تنسيق الوحدة المُعيار لـ JavaScript. عندما أدرجنا الجوانب السلبية لـ module.json (انظر أعلاه)، أدركنا أنّ جميعها تقريبًا كانت مرتبطة باستخدام تنسيق وحدة فريد وغير موحّد.

يعني اختيار تنسيق وحدة غير موحّد أنّنا علينا تخصيص الوقت لبناء عمليات الدمج باستخدام أدوات الإنشاء والأدوات التي استخدمها القائمون على صيانة الإصدارات.

وغالبًا ما كانت عمليات الدمج هذه غير متينة ولا تتيح استخدام الميزات، ما يتطلّب وقتًا إضافيًا للصيانة، ويؤدي أحيانًا إلى ظهور أخطاء طفيفة يتم طرحها في النهاية للمستخدمين.

وبما أنّ وحدات JavaScript كانت هي المعيار، كان ذلك يعني أنّ أدوات تطوير البرامج المتكاملة، مثل VS Code، ومحرّرات أخطاء النوع، مثل Closure Compiler/TypeScript، وأدوات الإنشاء، مثل Rollup/المُصغّرات، يمكنها فهم رمز المصدر الذي كتبناه. بالإضافة إلى ذلك، عندما ينضم مشرف جديد إلى فريق أدوات المطوّرين، لن يضطر إلى قضاء الوقت في تعلُّم تنسيق module.json محمي بحقوق الملكية، في حين أنّه (على الأرجح) سيكون على دراية بوحدات JavaScript.

بالطبع، لم تكن أي من المزايا المذكورة أعلاه متوفّرة عند إنشاء "أدوات مطوّري البرامج" في البداية. لقد استغرق الأمر سنوات من العمل في مجموعات المعايير وعمليات التنفيذ في وقت التشغيل والمطوّرين الذين يستخدمون وحدات JavaScript لتقديم الملاحظات للوصول إلى المرحلة التي وصلنا إليها الآن. ولكن عندما أصبحت وحدات JavaScript متاحة، كان علينا اتخاذ قرار: إما مواصلة صيانة التنسيق الخاص بنا أو الاستثمار في نقل البيانات إلى التنسيق الجديد.

تكلفة الجهاز الجديد

على الرغم من أنّ وحدات JavaScript تحتوي على الكثير من المزايا التي نرغب في استخدامها، بقينا في عالم module.json غير العادي. للاستفادة من مزايا وحدات JavaScript، كان علينا الاستثمار بشكل كبير في تصفية الديون الفنية، وإجراء عملية نقل قد تؤدي إلى إيقاف الميزات وظهور أخطاء انحدار.

في هذه المرحلة، لم يكن السؤال هو "هل نريد استخدام وحدات JavaScript؟"، بل كان السؤال هو "ما هو السعر المُكلف لاستخدام وحدات JavaScript؟". هنا، كان علينا الموازنة بين مخاطر تعرّض المستخدمين للانحدار، وتكلفة المهندسين الذين يقضون (قدرًا كبيرًا) من الوقت في نقل البيانات، والحالة الأسوأ مؤقتًا التي قد نعمل فيها.

تبيّن أنّ هذه النقطة الأخيرة مهمة جدًا. على الرغم من أنّه يمكننا نظريًا الوصول إلى وحدات JavaScript، إلا أنّنا سننتهي خلال عملية نقل البيانات إلى رمز يجب أن يأخذ في الاعتبار كلّا من module.json ووحدات JavaScript. لم يكن هذا الإجراء صعبًا من الناحية الفنية فحسب، بل كان يعني أيضًا أنّ جميع المهندسين الذين يعملون على DevTools يحتاجون إلى معرفة كيفية العمل في هذه البيئة. وسيحتاجون إلى أن يسألوا أنفسهم باستمرار "بالنسبة إلى هذا الجزء من قاعدة البيانات، هل هو module.json أو وحدات JavaScript وكيف يمكنني إجراء تغييرات؟".

لمحة سريعة: كانت التكلفة الخفية لإرشاد زملائنا في مجال الصيانة خلال عملية نقل البيانات أكبر مما توقعنا.

بعد تحليل التكلفة، استنتجنا أنّ نقل البيانات إلى وحدات JavaScript لا يزال مفيدًا. وبالتالي، كانت أهدافنا الرئيسية هي التالية:

  1. تأكَّد من الاستفادة إلى أقصى حدّ ممكن من استخدام وحدات JavaScript.
  2. تأكَّد من أنّ عملية الدمج مع النظام الحالي المستنِد إلى module.json آمنة ولا تؤثّر سلبًا في المستخدمين (مثل الأخطاء الناتجة عن الرجوع إلى إصدار سابق أو إحباط المستخدمين).
  3. يجب إرشاد جميع مشرفي "أدوات مطوري البرامج" خلال عملية نقل البيانات، وذلك باستخدام عمليات التحقق والتوازنات المضمَّنة لمنع الأخطاء غير المقصودة.

جداول البيانات والتحويلات والديون الفنية

وعلى الرغم من أنّ الهدف كان واضحًا، أثبتت القيود التي فرضها تنسيق module.json أنّه من الصعب حلّها. استغرق الأمر عدة تكرارات ونماذج أولية وتغييرات في التصميم قبل أن نتوصّل إلى حلّ يناسبنا. لقد كتبنا مستند تصميم يتضمّن استراتيجية نقل البيانات التي انتهينا إليها. يسرد مستند التصميم أيضًا تقديرنا الزمني الأولي: من أسبوعين إلى 4 أسابيع.

ملاحظة مُهمّة: استغرق الجزء الأكثر كثافة من عملية نقل البيانات 4 أشهر، واستغرقت العملية بأكملها 7 أشهر.

في المقابل، صمَّمت الخطة الأوّلية وقت الاختبار: سيتم تدريب وقت تشغيل "أدوات مطوري البرامج" على تحميل جميع الملفات المدرَجة في المصفوفة scripts في ملف module.json باستخدام الطريقة القديمة، بينما كنّا نعلّم جميع الملفات المدرَجة في المصفوفة modules باستخدام الاستيراد الديناميكي لوحدات JavaScript. أي ملف يكون مضمّنًا في صفيف modules يمكنه استخدام عمليات الاستيراد/التصدير في Elasticsearch.

بالإضافة إلى ذلك، سنُجري عملية النقل على مرحلتَين (في النهاية، سنقسّم المرحلة الأخيرة إلى مرحلتَين فرعيتَين، كما هو موضّح أدناه): المرحلتان export وimport. تم تتبُّع حالة الوحدة التي ستكون في المرحلة التي تم تحديدها في جدول بيانات كبير:

جدول بيانات نقل وحدات JavaScript

يمكنك الاطّلاع على مقتطف من جدول التقدّم هنا.

export-phase

ستكون المرحلة الأولى هي إضافة عبارات export لجميع الرموز التي كان من المفترض مشاركتها بين الوحدات/الملفات. سيتم إجراء عملية التحويل آليًا من خلال تشغيل نص برمجي لكل مجلد. بما أنّ الرمز التالي سيكون متوفّرًا في عالم module.json:

Module.File1.exported = function() {
  console.log('exported');
  Module.File1.localFunctionInFile();
};
Module.File1.localFunctionInFile = function() {
  console.log('Local');
};

(هنا، Module هو اسم الوحدة وFile1 هو اسم الملف. في شجرة المصدر، سيكون ذلك front_end/module/file1.js.)

سيتم تحويل ذلك إلى ما يلي:

export function exported() {
  console.log('exported');
  Module.File1.localFunctionInFile();
}
export function localFunctionInFile() {
  console.log('Local');
}

/** Legacy export object */
Module.File1 = {
  exported,
  localFunctionInFile,
};

في البداية، كانت خطتنا هي إعادة كتابة عمليات استيراد الملفات نفسها خلال هذه المرحلة أيضًا. في المثال أعلاه، سنعيد كتابة Module.File1.localFunctionInFile إلى localFunctionInFile. ومع ذلك، تبيّن لنا أنّه سيكون من الأسهل إجراء عملية التحويل هذه آليًا وتطبيقها بأمان أكبر إذا فصلنا بين هذين التحويلَين. وبالتالي، ستصبح "نقل كل الرموز في الملف نفسه" المرحلة الفرعية الثانية من المرحلة import.

بما أنّ إضافة الكلمة الرئيسية export في ملف تحوّل الملف من "نص برمجي" إلى "وحدة"، كان لا بد من تعديل الكثير من البنية الأساسية لواجهة أدوات المطوّرين وفقًا لذلك. ويشمل ذلك وقت التشغيل (مع الاستيراد الديناميكي)، بالإضافة إلى أدوات مثل ESLint للتشغيل في وضع الوحدة.

ومن بين الاكتشافات التي توصّلنا إليها أثناء العمل على حلّ هذه المشاكل أنّه كان يتم تنفيذ اختباراتنا في الوضع "غير الدقيق". بما أنّ وحدات JavaScript تشير إلى تشغيل الملفات في وضع "use strict"، سيؤثّر ذلك أيضًا في اختباراتنا. كما اتضح، كان هناك عدد غير تافه من الاختبارات يعتمد على هذا التقلّص، بما في ذلك اختبار يستخدم عبارة with بالرقم.

في النهاية، استغرق تعديل المجلد الأول لتضمين عبارات export حوالي أسبوع وعدة محاولات باستخدام إعادة الربط.

import-phase

بعد تصدير جميع الرموز باستخدام عبارات export وبقائها في النطاق العام (القديم)، كان علينا تعديل جميع الإشارات إلى الرموز في جميع الملفات لاستخدام عمليات الاستيراد في ES. سيكون الهدف النهائي هو إزالة جميع "عناصر التصدير القديمة" وتنظيف النطاق العالمي. سيتم إجراء عملية التحويل آليًا من خلال تشغيل نص برمجي لكل مجلد.

على سبيل المثال، بالنسبة إلى الرموز التالية المتوفّرة في module.json:

Module.File1.exported();
AnotherModule.AnotherFile.alsoExported();
SameModule.AnotherFile.moduleScoped();

سيتم تحويلها إلى:

import * as Module from '../module/Module.js';
import * as AnotherModule from '../another_module/AnotherModule.js';

import {moduleScoped} from './AnotherFile.js';

Module.File1.exported();
AnotherModule.AnotherFile.alsoExported();
moduleScoped();

ومع ذلك، كانت هناك بعض المحاذير بشأن هذا النهج:

  1. لم يتم تسمية كل رمز باسم Module.File.symbolName. تم تسمية بعض الرموز Module.File أو Module.CompletelyDifferentName فقط. وقد أدّى هذا التناقض إلى أن ننشئ تعيينًا داخليًا من الكائن العام القديم إلى الكائن المستورَد الجديد.
  2. في بعض الأحيان قد يكون هناك تعارض بين أسماء الوحدات النمطية. من بين الممارسات الأكثر بروزًا التي استخدمناها، نذكر استخدام نمط لتعريف أنواع معيّنة من Events، حيث تم تسمية كل رمز باسم Events فقط. وهذا يعني أنّه إذا كنت تستمع إلى أنواع متعدّدة من الأحداث التي تمّ الإعلان عنها في ملفات مختلفة، سيحدث تعارض في الأسماء في عبارة import لهذه Events.
  3. تبيّن أنّ هناك تبعيات دائرية بين الملفات. وكان هذا مناسبًا في سياق النطاق العام، لأنّ استخدام الرمز كان بعد تحميل كل الرموز البرمجية. أمّا إذا كنت بحاجة إلى import، فسيتم استخدام التبعية الدائرية بشكل صريح. لا يشكّل ذلك مشكلة فورية، ما لم يكن لديك استدعاءات وظائف ذات تأثيرات جانبية في رمز النطاق العام، وهو ما كان متوفّرًا أيضًا في DevTools. باختصار، تطلّب الأمر إجراء بعض العمليات الجراحية وإعادة صياغة لجعل عملية التحويل آمنة.

عالم جديد تمامًا باستخدام وحدات JavaScript

في شباط (فبراير) 2020، بعد 6 أشهر من بدء عملية التنظيف في أيلول (سبتمبر) 2019، تم إجراء عمليات التنظيف الأخيرة في مجلد ui/. وبذلك، انتهت عملية نقل البيانات بشكل غير رسمي. بعد أن هدأت الأمور، وضعنا علامة على عملية نقل البيانات بأنّها انتهت في 5 آذار (مارس) 2020. 🎉

في الوقت الحالي، تستخدم جميع الوحدات في "أدوات مطوري البرامج" وحدات JavaScript لمشاركة الرمز. لا نزال نضع بعض الرموز على النطاق الشامل (في ملفات module-legacy.js) لاختباراتنا القديمة أو للدمج مع أجزاء أخرى من بنية DevTools. ستتم إزالة هذه الشروط بمرور الوقت، ولكنّنا لا نعتبرها عائقًا أمام التطوير المستقبلي. لدينا أيضًا دليل أسلوب لاستخدامنا لحِزم JavaScript.

الإحصاءات

يُرجى العِلم أنّ التقديرات المعتدلة لعدد متغيّرات التصميم التراكمية (CLs) (اختصار CLs ) المستخدَم في Gerrit والذي يمثِّل التغيُّر، يشبه طلب سحب GitHub) المستخدَم في عملية النقل هذه، يبلغ حوالي 250 CL، ويتم تنفيذ معظم هذه القيم بواسطة مهندسَين. لا تتوفّر لدينا إحصاءات حاسمة عن حجم التغييرات التي تم إجراؤها، ولكنّ التقدير المتحفظ للخطوط التي تم تغييرها (يتم احتسابها على أنّها مجموع الفرق المطلق بين عمليات الإدراج والحذف لكل رمز برمجي) يقارب 30,000 (أي %20 تقريبًا من جميع رموز واجهة مستخدم DevTools).

تم تضمين أول ملف يستخدم export في الإصدار 79 من Chrome، وتم إصداره في الإصدار الثابت في كانون الأول (ديسمبر) 2019. تم تضمين آخر تغيير لنقل البيانات إلى import في الإصدار 83 من Chrome، الذي تم إصداره في الإصدار الثابت في أيار (مايو) 2020.

ندرك أنّه تم طرح تراجُع واحد في الإصدار الثابت من Chrome، وقد تم طرحه كجزء من عملية نقل البيانات هذه. تعطّل ميزة الإكمال التلقائي للمقتطفات في قائمة الأوامر بسبب تصدير default غير مرغوب فيه. لقد واجهنا العديد من حالات التراجع الأخرى، ولكنّ مجموعات الاختبار المبرمَجة ومستخدمو Chrome Canary أبلغوا عن هذه المشاكل وتم إصلاحها قبل أن تؤثر في مستخدمي الإصدار الثابت من Chrome.

يمكنك الاطّلاع على الرحلة الكاملة (لا يتم إرفاق جميع عمليات الربط بهذا الخطأ، ولكن يتم إرفاق معظمها) المسجّلة على crbug.com/1006759.

الاستنتاجات التي توصّلنا إليها

  1. يمكن أن يكون للقرارات التي تم اتخاذها في الماضي تأثير طويل الأمد على مشروعك. على الرغم من توفّر وحدات JavaScript (وتنسيقات الوحدات الأخرى) لبعض الوقت، لم تكن أدوات مطوّري البرامج في Chrome في وضع يسمح لها بتبرير نقل البيانات. إنّ تحديد الوقت المناسب لنقل البيانات والوقت غير المناسب لذلك أمر صعب ويستند إلى تخمينات مدروسة.
  2. كانت تقديرات الوقت الأولية بأسابيع بدلاً من أشهر. ويعود ذلك إلى حدّ كبير إلى أنّنا واجهنا مشاكل غير متوقّعة أكثر مما توقّعنا في تحليل التكلفة الأولي. على الرغم من أنّ خطة نقل البيانات كانت قوية، كان الدين الفني هو العامل الذي كان يعرقل عملية النقل (في كثير من الأحيان أكثر مما أردنا).
  3. تضمنت عملية نقل وحدات JavaScript قدرًا كبيرًا من عمليات تنظيف الديون الفنية (التي تبدو غير ذات صلة). من خلال نقل البيانات إلى تنسيق وحدة موحّد حديث، تمكّنا من إعادة مواءمة أفضل ممارسات الترميز مع أسلوب تطوير الويب الحديث. على سبيل المثال، تمكّنا من استبدال أداة تجميع Python المخصّصة لدينا بإعدادات Rollup بسيطة.
  4. على الرغم من التأثير الكبير في قاعدة الرموز البرمجية لدينا (تم تغيير% 20 تقريبًا من الرموز البرمجية)، تم الإبلاغ عن عدد قليل جدًا من حالات التراجع. على الرغم من أنّنا واجهنا العديد من المشاكل في نقل أول ملفَّين، إلا أنّنا بعد فترة قصيرة وضعنا سير عمل مُحكمًا ومُتمتَدًا جزئيًا. وهذا يعني أنّ تأثير عملية نقل البيانات هذه في المستخدمين الذين يستخدمون الإصدارات الثابتة كان ضئيلًا.
  5. إنّ تعليم تفاصيل عملية نقل بيانات معيّنة إلى مطوّرين آخرين أمر صعب وفي بعض الأحيان مستحيل. من الصعب متابعة عمليات نقل البيانات بهذا النطاق، وتتطلّب الكثير من المعرفة بالمنتدى. إنّ نقل هذه المعرفة الخاصة بالنطاق إلى الآخرين الذين يعملون في قاعدة البيانات نفسها ليس مرغوبًا فيه بحد ذاته للعمل الذي يُجرونه. إن معرفة ما يجب مشاركته والتفاصيل التي لا يجب مشاركتها هو فن، ولكنه ضروري. لذلك، من المهم تقليل عدد عمليات نقل البيانات الكبيرة، أو على الأقل عدم تنفيذها في الوقت نفسه.

تنزيل قنوات المعاينة

ننصحك باستخدام إصدار Canary أو Dev أو الإصدار التجريبي من Chrome كمتصفّح التطوير التلقائي. تتيح لك قنوات المعاينة هذه الوصول إلى أحدث ميزات DevTools، وتتيح لك اختبار واجهات برمجة تطبيقات منصات الويب المتطوّرة، وتساعدك في العثور على المشاكل في موقعك الإلكتروني قبل أن يعثر عليها المستخدمون.

التواصل مع فريق "أدوات مطوّري البرامج في Chrome"

استخدِم الخيارات التالية لمناقشة الميزات الجديدة أو التحديثات أو أي شيء آخر مرتبط بـ "أدوات مطوّري البرامج".