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

Tim van der Lippe
Tim van der Lippe

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

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

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

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

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

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

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

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

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

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

بوجهٍ عام، عند تقييم الحالة الحالية لنظام الوحدات في DevTools وأشكال الوحدات الأخرى (الأكثر استخدامًا)، توصّلنا إلى أنّ نمط 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 أشهر.

ومع ذلك، تمكّنت الخطة الأولية من اجتياز اختبار الزمن: سنعلّم وقت تشغيل DevTools تحميل جميع الملفات المدرَجة في صفيف 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. 🎉

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

الإحصاءات

تشير التقديرات المتحفظة لعدد قوائم التغييرات (اختصارًا لـ changelist - المصطلح المستخدَم في Gerrit والذي يمثّل تغييرًا - مشابهًا لطلب سحب على GitHub) المُدرَجة في عملية نقل البيانات هذه إلى أنّها تبلغ حوالي 250 قائمة تغييرات، تم تنفيذها إلى حد كبير من قِبل مهندسَين. لا تتوفّر لدينا إحصاءات حاسمة عن حجم التغييرات التي تم إجراؤها، ولكنّ التقدير المتحفظ للخطوط التي تم تغييرها (يتم احتسابها على أنّها مجموع الفرق المطلق بين عمليات الإدراج والحذف لكل رمز برمجي) يقارب 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"

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