بلدي أشعل النار: من الخرق إلى الثروات

قبل التاريخ


لقد عملت كمطور أمامي لمدة عام واحد الآن. كان أول مشروع لي هو خلفية "العدو". يحدث أن هذه ليست مشكلة كبيرة عندما يتم تأسيس الاتصال.


ولكن في حالتنا لم يكن الأمر كذلك.


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


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


تم تكليف هذه الدراسة لي.


لقد قدمت قائمة بما أريد أن أكون في الأداة التي أود استخدامها للتحقق من صحة البيانات.


أهم نقاط الاختيار كانت النقاط التالية:


  • الوصف التعريفي (مخطط) التحقق من الصحة ، والذي يتم تحويله إلى وظيفة المدقق التي ترجع صواب / خطأ (صالحة ، غير صالحة)
  • عتبة دخول منخفضة ؛
  • تشابه البيانات المصادق عليها مع وصف التحقق من الصحة ؛
  • سهولة تكامل عمليات التحقق من الصحة المخصصة ؛
  • سهولة تكامل رسائل الخطأ المخصصة.

نتيجة لذلك ، وجدت العديد من مكتبات التحقق من الصحة ، بعد أن استعرضت TOP-5 (ajv ، joi ، roi ...). انهم جميعا جيدة جدا. لكن يبدو لي أنه من أجل حل 5 ٪ من الحالات المعقدة - حكم عليهم أن 95 ٪ من الحالات الأكثر تكرارًا تكون مطوّلة وضخمة.


لذلك ، فكرت: لماذا لا تطور شيئًا يناسبك.
بعد أربعة أشهر ، صدرت النسخة السابعة من مكتبة التحقق من الرباعية الخاصة بي.
لقد كانت نسخة مستقرة ، تم اختبارها بالكامل ، تنزيلات 11k على npm. استخدمناه في ثلاثة مشاريع في حملة لمدة ثلاثة أشهر.


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


ولكن تم تحديد العيوب أيضًا.


لذلك ، قررت تحليلها وإصدار نسخة جديدة مع تصحيحات لجميع الأخطاء التي حدثت أثناء التطوير.
سأتحدث عن هذه الأخطاء المعمارية وحلولها أدناه.


أشعل النار المعمارية


"Stroko" - لغة محددة من المخطط


سأقدم مثالاً على الإصدار القديم من المخطط لكائن الشخص.


const personSchema = { name: 'string', age: 'number', linkedin: ['string', 'null'] } 

يقوم هذا المخطط بالتحقق من صحة الكائن الذي يحتوي على ثلاث خصائص: الاسم - يجب أن يكون سلسلة ، والعمر - يجب أن يكون رقمًا ، أو رابطًا إلى حساب في LinkedIn - يجب أن يكون إما فارغًا (إذا لم يكن هناك حساب) أو سلسلة (إذا كان هناك حساب).


يفي هذا المخطط بمتطلبات قابليتي القراءة والتشابه مع البيانات التي تم التحقق من صحتها ، وأعتقد أن حد الدخول إلى تعلم كتابة هذه المخططات ليس مرتفعًا. علاوة على ذلك ، يمكن كتابة مثل هذا المخطط بسهولة مع تعريف نوع في typescript:


 type Person = { name: string age: number linkedin: string | null } 

(كما ترون - التغييرات أكثر ترجيحًا)


عندما اتخذت قرارًا ، ما الذي يجب استخدامه لخيارات التحقق الأكثر شيوعًا (على سبيل المثال ، تلك المستخدمة أعلاه). اخترت استخدام - سلاسل ، كما كانت ، أسماء المدققين.


لكن المشكلة في السلاسل هي أنها غير متوفرة للمترجم أو محلل الأخطاء. سلسلة "الرقم" بالنسبة لهم لا تختلف كثيرا عن "الرقم".


قرار


الإصدار الجديد من الرباعية 8.0.0. قررت حذف من الرباعية - استخدام السلاسل كأسماء المدققين داخل المخطط.


الشكل الآن يبدو كالتالي:


 const personSchema = { name: v.string age: v.number, linkedin: [v.string, null] } 

هذا التغيير له ميزتان كبيرتان:


  • المترجمون أو محللو الأخطاء - سوف يكونوا قادرين على اكتشاف أن اسم الطريقة مكتوبة بخطأ.
  • خطوط - لم تعد تستخدم كعنصر مخطط. هذا يعني أنه بالنسبة لهم يمكنك اختيار وظائف جديدة في المكتبة ، والتي سيتم وصفها أدناه.

دعم TypeScript


بشكل عام ، تم تطوير الإصدارات السبعة الأولى في Javascript الخالصة. عند التبديل إلى مشروع باستخدام Typescript ، كانت هناك حاجة لتكييف المكتبة بطريقة أو بأخرى. لذلك ، تمت كتابة تعريفات للمكتبة.


ولكن هذا كان ناقصًا - عند إضافة وظيفة ، أو عند تغيير بعض عناصر المكتبة ، كان من السهل دائمًا نسيان تحديث إعلانات النوع.


كان هناك أيضا إزعاج بسيط من هذا النوع:


 const checkPerson = v(personSchema) // (0) // ... const person: any = await axios.get('https://myapi.com/person/42') if (!checkPerson(person)) {// (1) throw new TypeError('Invalid person response') } console.log(person.name) // (2) 

عندما أنشأنا مصدق الكائن على السطر (0). نود بعد التحقق من الإجابة الحقيقية من الواجهة الخلفية (1) ومعالجة الخطأ. على الخط (2) بحيث يكون هذا person النوع Person. لكن هذا لم يحدث. لسوء الحظ ، لم يكن مثل هذا التحقق حارس نوع.


قرار


قررت إعادة كتابة مكتبة الرباعية بأكملها إلى Typescript بحيث شارك المترجم في التحقق من مراسلات المكتبة بأنواعها. على طول الطريق ، نضيف إلى الدالة التي تُرجع المدقق المترجم معلمة نوع تحدد حارس النوع لأي نوع من هذه المدقق.


مثال يشبه هذا:


 const checkPerson = v<Person>(personSchema) // (0) // ... const person: any = await axios.get('https://myapi.com/person/42') if (!checkPerson(person)) {// (1) throw new TypeError('Invalid person response') } console.log(person.name) // (2) 

الآن على الخط (2) person من النوع Person .


قراءة


كانت هناك حالتان كانت قراءة الرمز فيهما سيئة: التحقق من التوافق مع مجموعة معينة من القيم (التحقق من التعدادات) والتحقق من خصائص الكائن الأخرى.


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


 const personSchema = { name: 'string', age: 'number', linkedin: ['null', 'string'], sex: v.enum('male', 'female') } 

الخيار قابل للقراءة للغاية. ولكن كالعادة ، ذهب كل شيء خارج الخطة قليلا.
وجود التعداد المصرح به في البرنامج ، على سبيل المثال ، هذا:


 enum Sex { Male = 'male', Female = 'female' } 

بطبيعة الحال ، أريد استخدامها داخل الدائرة. لذلك عند تغيير إحدى القيم (على سبيل المثال ، "ذكر" - "m" ، "أنثى" -> "f") ، يجب أيضًا تغيير مخطط التحقق من الصحة.


لذلك ، دائمًا ما يتم التحقق من صحة التعداد على النحو التالي:


 const personSchema = { name: 'string', age: 'number', linkedin: ['null', 'string'], sex: v.enum(...Object.values(Sex)) } 

التي تبدو ضخمة جدا.


ب) التحقق من الخصائص المتبقية للكائن


لنفترض أننا قمنا بإضافة مثل هذه الخاصية إلى كائننا - يمكن أن تحتوي على حقول إضافية ، ولكن يجب أن تكون جميعها روابط إلى شبكات اجتماعية - وهذا يعني أنها يجب أن تكون إما null أو تكون سلسلة.


سيبدو المخطط القديم كما يلي:


 const personSchema = { name: 'string', age: 'number', linkedin: ['null', 'string'], sex: v.enum(...Object.values(Sex)), ...v.rest(['null', 'string']) // Rest props are string | null } 

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


قرار


كما هو موضح أعلاه ، لم تعد السلاسل جزءًا من مخططات التحقق من الصحة. ظلت ثلاثة أنواع فقط من قيم Javascript مخطط التحقق من الصحة. كائن - لوصف مخطط التحقق من صحة الكائن. صف للوصف - عدة خيارات للصحة. وظيفة (مكتبة ولدت أو العرف) - لجميع خيارات التحقق من الصحة الأخرى.


سمح هذا الحكم بإضافة وظيفة ، والتي سمحت بزيادة إمكانية قراءة الدائرة عدة مرات.


في الواقع ، ماذا لو أردنا مقارنة القيمة بالسلسلة "ذكر". هل نحن حقا بحاجة لمعرفة أي شيء آخر إلى جانب القيمة نفسها وسلسلة "ذكر".


لذلك ، تقرر إضافة قيم الأنواع البدائية كعنصر في الدائرة. لذلك ، عندما تقابل قيمة مبدئية في المخطط ، فهذا يعني أن هذه هي القيمة الصحيحة التي يجب أن يتحقق منها المدقق وفقًا لهذا المخطط. أفضل إعطاء مثال:


إذا كنا بحاجة إلى التحقق من الرقم للمساواة 42 العقل. ثم نكتبها مثل هذا:


 const check42 = v(42) check42(42) // => true check42(41) // => false check42(43) // => false check42('42') // => false 

دعونا نرى كيف يؤثر ذلك على مخطط الشخص (دون مراعاة الخصائص الإضافية):


 const personSchema = { name: v.string, age: v.number, linkedin: [null, v.string], // null is primitive value sex: ['male', 'female'] // 'male', 'female' are primitive values } 

باستخدام التعدادات المحددة مسبقًا ، يمكننا إعادة كتابتها كما يلي:


 const personSchema = { name: v.string, age: v.number, linkedin: [null, v.string], sex: Object.values(Sex) // same as ['male', 'female'] } 

في هذه الحالة ، تمت إزالة الاحتفالات غير الضرورية في شكل استخدام طريقة التعداد واستخدام عامل الانتشار لإدراج قيم صالحة من الكائن كمعلمات في هذه الطريقة.


ما يعتبر قيمة بدائية: الأرقام ، الأوتار ، الشخصيات ، true ، false ، null وغير undefined .


وهذا هو ، إذا كنا بحاجة إلى مقارنة القيمة معهم ، فإننا ببساطة نستخدم هذه القيم بأنفسهم. ومكتبة للتحقق من الصحة - ستقوم بإنشاء مدقق يقارن بدقة القيمة مع تلك المحددة في المخطط.


للتحقق من الخصائص المتبقية ، تم اختيار استخدام خاصية خاصة لجميع حقول الكائن الأخرى:


 const personSchema = { name: v.string, age: v.number, linkedin: [null, v.string], sex: Object.values(Sex), [v.rest]: [null, v.string] } 

وبالتالي ، فإن الدائرة تبدو أكثر قابلية للقراءة. وأكثر مثل الإعلانات Typescript.


يرتبط المدقق بالوظيفة التي أنشأته


في الإصدارات الأقدم ، لم تكن توضيحات الخطأ جزءًا من أداة التحقق. تم إضافتهم إلى صفيف داخل الوظيفة v .


في السابق ، من أجل الحصول على شرح لأخطاء التحقق من الصحة ، كان عليك أن تحصل على مدقق معك (للتدقيق) و v (للحصول على تفسير للبطلان). كل هذا بدا كما يلي:

أ) نضيف تفسيرات إلى المخطط


 const checkPerson = v({ name: v('string', 'wrong name') age: v('number', 'wrong age'), linkedin: v(['null', 'string'], 'wrong linkedin'), sex: v( v.enum(...Object.values(Sex)), 'wrong sex value' ), ...v.rest( v( ['null', 'string'], 'wrong social networks link' ) ) // Rest props are string | null }) 

إلى أي عنصر من عناصر الدائرة - يمكنك إضافة شرح للخطأ باستخدام الوسيطة الثانية لوظيفة v compiler.


ب) مسح مجموعة التفسير


قبل التحقق من الصحة ، كان من الضروري مسح هذه المجموعة العالمية التي سجلت فيها جميع التفسيرات أثناء التحقق من الصحة.


 v.clearContext() // same as v.explanations = [] 

ج) التحقق من صحة


 const isPersonValid = checkPerson(person) 

أثناء هذا الفحص ، إذا تم العثور على صلاحية ، وفي مرحلة إنشاء الدائرة - تم تقديم تفسير لها ، يتم وضع هذا التفسير في الصفيف العام لـ v.explanation .


د) خطأ في التعامل


 if (!isPersonValid) { throw new TypeError('Invalid person response: ' + v.explanation.join('; ')) } // ex. Throws 'Invalid person response: wrong name; wrong age' 

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


قرار


تم حل هذه المشكلة على النحو التالي: أصبحت التفسيرات جزءًا من وظيفة التحقق من الصحة نفسها. ما يمكن فهمه من نوعه:
اكتب Validator = (value: any ، تفسيرات؟: any []) => منطقية


الآن إذا كنت بحاجة إلى شرح للخطأ ، يمكنك تمرير الصفيف الذي تريد إضافة الشرح إليه.


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


الآن التحقق من صحة مع تفسيرات يبدو مثل هذا:


 const checkPerson = v<Person>({ name: v(v.string, 'wrong name'), age: v(v.number, 'wrong age'), linkedin: v([null, v.string], 'wrong linkedin') sex: v(Object.values(Sex), 'wrong sex') [v.rest]: v([null, v.string], 'wrong social network') }) // ... const explanations = [] if (!checkPerson(person, explanation)) { throw new TypeError('Wrong person: ' + explanations.join('; ')) } // OR const getExplanation = v.explain(checkPerson) const explanations = getExplanation(person) if (explanations) { throw new TypeError('Wrong person: ' + explanations.join('; ')) } 

خاتمة


لقد سلطت الضوء على ثلاثة مباني بسببها اضطررت إلى إعادة كتابة كل شيء:


  • الأمل في أن الناس لا يخطئون عند كتابة السطور
  • استخدام المتغيرات العامة (في هذه الحالة ، صفيف v.explanation)
  • الاختبار بأمثلة صغيرة أثناء التطوير - لم يُظهر المشكلات التي تنشأ عند استخدامها في الحالات الكبيرة الحقيقية.

لكن يسعدني أنني أجريت تحليلًا لهذه المشكلات ، وأن الإصدار الذي تم إصداره مستخدم بالفعل في مشروعنا. وآمل أن يكون مفيدًا لنا على الأقل من السابق.


شكرا لكم جميعا على القراءة ، وآمل أن تجربتي ستكون مفيدة لك.

Source: https://habr.com/ru/post/ar453502/


All Articles