تقديم CLI باني

تقديم CLI باني

في هذه المقالة ، سوف نلقي نظرة على API Angular CLI الجديدة ، والتي سوف تسمح لك بتوسيع ميزات CLI الحالية وإضافة ميزات جديدة. سنناقش كيفية العمل مع واجهة برمجة التطبيقات هذه ونقاط امتدادها الموجودة والتي تسمح بإضافة وظائف جديدة إلى CLI.

القصة


منذ حوالي عام ، قدمنا ​​ملف مساحة العمل ( angular.json ) في Angular CLI وأعدنا التفكير في العديد من المبادئ الأساسية لتنفيذ أوامره. لقد توصلنا إلى حقيقة أننا وضعنا الفرق في "الصناديق":

  1. الأوامر التخطيطية - "الأوامر التخطيطية" . الآن ، ربما تكون قد سمعت بالفعل عن Schematics ، المكتبة التي يستخدمها CLI لإنشاء وتعديل التعليمات البرمجية الخاصة بك. لقد ظهر في الإصدار 5 ويستخدم حاليًا في معظم الأوامر التي تتعلق بكودك ، مثل الجديد ، إنشاء ، إضافة ، وتحديث .
  2. أوامر متنوعة - "فرق أخرى" . هذه هي الأوامر التي لا ترتبط مباشرة بمشروعك: المساعدة ، الإصدار ، التكوين ، الوثيقة . في الآونة الأخيرة ، ظهرت أيضًا تحليلات ، وكذلك بيض عيد الفصح (Shhh! ليست كلمة لأحد!).
  3. أوامر المهام - "أوامر المهام" . هذه الفئة ، إلى حد كبير ، "تطلق العمليات المنفذة على رمز الآخرين." - على سبيل المثال ، build عبارة عن بناء مشروع ، و lint يعمل على تصحيح الأخطاء والاختبار قيد الاختبار.

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

على الرغم من حقيقة أن المهندس المعماري لم يكن مدعومًا رسميًا ، إلا أنه كان شائعًا بين المطورين الذين أرادوا تخصيص مجموعة المشاريع ، وكذلك بين مكتبات الجهات الخارجية التي كانت بحاجة إلى التحكم في سير العمل. استخدمها Nx لتنفيذ أوامر Bazel ، واستخدمها Ionic لتشغيل اختبارات الوحدات على Jest ، ويمكن للمستخدمين توسيع تكوينات Webpack الخاصة بهم باستخدام أدوات مثل ngx-build-plus . وكانت تلك مجرد البداية.

يتم استخدام إصدار مدعوم رسميًا ومستقر ومحسّن من واجهة برمجة التطبيقات هذه في Angular CLI الإصدار 8.

مفهوم


يوفر Architect API أدوات لجدولة وتنسيق المهام التي يستخدمها Angular CLI لتنفيذ أوامره. ويستخدم وظائف تسمى
"البنائين" - "هواة الجمع" الذين يمكنهم القيام بدور المهام أو المخططين لهواة الجمع الآخرين. بالإضافة إلى ذلك ، يستخدم angular.json كمجموعة من الإرشادات لهواة الجمع أنفسهم.

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

جامعي


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

تأخذ وظيفة المجمّع وسيطين: قيمة الإدخال (أو الخيارات) والسياق الذي يوفر العلاقة بين CLI والمجمع نفسه. تقسيم المسؤولية هنا هو نفسه في Schematics - حيث يحدد مستخدم CLI الخيارات ، و API مسؤولة عن السياق ، وتقوم أنت (المطور) بتعيين السلوك اللازم. يمكن تنفيذ السلوك بشكل متزامن أو غير متزامن أو ببساطة عرض عدد معين من القيم. يجب أن يكون الإخراج من النوع BuilderOutput ، والذي يحتوي على نجاح الحقل المنطقي وخطأ الحقل الاختياري الذي يحتوي على رسالة الخطأ.

ملف مساحة العمل والمهام


يعتمد API API على angular.json ، وهو ملف مساحة عمل لتخزين المهام وإعداداتها.

angular.json تقسم مساحة العمل إلى مشاريع ، وهي بدورها إلى مهام. على سبيل المثال ، تطبيقك الذي تم إنشاؤه باستخدام الأمر ng new هو أحد هذه المشاريع. إحدى مهام هذا المشروع ستكون مهمة البناء ، والتي يمكن إطلاقها باستخدام الأمر ng build . بشكل افتراضي ، تحتوي هذه المهمة على ثلاثة مفاتيح:

  1. باني - اسم المجمع الذي سيتم استخدامه لإكمال المهمة ، بالتنسيق PACKAGE_NAME: ASSEMBLY_NAME .
  2. خيارات - الإعدادات المستخدمة عند بدء المهمة بشكل افتراضي.
  3. التكوينات - الإعدادات التي سيتم تطبيقها عند بدء مهمة مع التكوين المحدد.

يتم تطبيق الإعدادات على النحو التالي: عند بدء المهمة ، يتم أخذ الإعدادات من كتلة الخيارات ، ثم إذا تم تحديد تكوين ، تتم كتابة إعداداته أعلى الإعدادات الموجودة. بعد ذلك ، إذا تم تمرير إعدادات إضافية ، كتلة التجاوزات ، إلى schedTarget () ، فستتم كتابتها أخيرًا. عند استخدام Angular CLI ، يتم تمرير وسيطات سطر الأوامر للتجاوزات . بعد نقل جميع الإعدادات إلى المجمع ، يقوم بفحصها وفقًا لمخططه ، وفقط إذا كانت الإعدادات تتوافق معه ، سيتم إنشاء السياق وسيبدأ المجمع في العمل.

مزيد من المعلومات حول مساحة العمل هنا .

إنشاء جامع الخاصة بك


كمثال ، دعنا ننشئ أداة تجميع تقوم بتشغيل أمر في سطر الأوامر. لإنشاء أداة تجميع ، استخدم مصنع createBuilder وأعد كائن BuilderOutput :

import { BuilderOutput, createBuilder } from '@angular-devkit/architect'; export default createBuilder((options, context) => { return new Promise<BuilderOutput>(resolve => { resolve({ success: true }); }); }); 

الآن ، دعونا نضيف بعض المنطق إلى جامعنا: نريد التحكم في المجمع من خلال الإعدادات ، وإنشاء عمليات جديدة ، والانتظار حتى تكتمل العملية ، وإذا تم إكمال العملية بنجاح (أي رمز الإرجاع 0) ، فقم بالإشارة إلى هذا Architect:

 import { BuilderOutput, createBuilder } from '@angular-devkit/architect'; import * as childProcess from 'child_process'; export default createBuilder((options, context) => { const child = childProcess.spawn(options.command, options.args); return new Promise<BuilderOutput>(resolve => { child.on('close', code => { resolve({ success: code === 0 }); }); }); }); 

معالجة الانتاج


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

للقيام بذلك ، يمكننا استخدام Logger ، المتاح في كائن السياق ، والذي سيتيح لنا إعادة توجيه مخرجات العملية:

 import { BuilderOutput, createBuilder } from '@angular-devkit/architect'; import * as childProcess from 'child_process'; export default createBuilder((options, context) => { const child = childProcess.spawn(options.command, options.args, { stdio: 'pipe' }); child.stdout.on('data', (data) => { context.logger.info(data.toString()); }); child.stderr.on('data', (data) => { context.logger.error(data.toString()); }); return new Promise<BuilderOutput>(resolve => { child.on('close', code => { resolve({ success: code === 0 }); }); }); }); 

تقارير الأداء والحالة


الجزء الأخير من واجهة برمجة التطبيقات المتعلقة بتنفيذ أداة التجميع الخاصة بك هو تقارير التقدم والحالة الحالية.

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

 import { BuilderOutput, createBuilder } from '@angular-devkit/architect'; import * as childProcess from 'child_process'; export default createBuilder((options, context) => { context.reportStatus(`Executing "${options.command}"...`); const child = childProcess.spawn(options.command, options.args, { stdio: 'pipe' }); child.stdout.on('data', (data) => { context.logger.info(data.toString()); }); child.stderr.on('data', (data) => { context.logger.error(data.toString()); }); return new Promise<BuilderOutput>(resolve => { context.reportStatus(`Done.`); child.on('close', code => { resolve({ success: code === 0 }); }); }); }); 

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

التحقق من صحة المدخلات


يتم فحص كائن الخيارات الذي تم تمريره إلى المجمع باستخدام JSON Schema. هذا يشبه الرياضيات إذا كنت تعرف ما هو عليه.

في مثال أداة التجميع الخاصة بنا ، نتوقع أن تكون معلماتنا كائنًا يتلقى مفتاحين: command - command (string) و args - arguments (array of strings). سيبدو مخطط التحقق الخاص بنا كما يلي:

 { "$schema": "http://json-schema.org/schema", "type": "object", "properties": { "command": { "type": "string" }, "args": { "type": "array", "items": { "type": "string" } } } 

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

إنشاء حزمة بناء


يوجد ملف رئيسي واحد نحتاج إلى إنشائه لجامعنا الخاص من أجل جعله متوافقًا مع Angular CLI - builders.json ، المسؤولة عن العلاقة بين تنفيذنا للمجمع واسمه ومخطط التحقق. الملف نفسه يشبه هذا:

 { "builders": { "command": { "implementation": "./command", "schema": "./command/schema.json", "description": "Runs any command line in the operating system." } } } 

بعد ذلك ، في ملف package.json ، نضيف مفتاح builders ، مشيرًا إلى ملف builders.json :

 { "name": "@example/command-runner", "version": "1.0.0", "description": "Builder for Architect", "builders": "builders.json", "devDependencies": { "@angular-devkit/architect": "^1.0.0" } } 

سيؤدي هذا إلى إخبار Architect بالبحث عن ملف تعريف المجمع.

وبالتالي ، فإن اسم جامعنا هو "@ example / command-runner: command" . الجزء الأول من الاسم ، قبل النقطتين (:) هو اسم الحزمة ، المعرفة باستخدام package.json . الجزء الثاني هو اسم المجمع ، المعرفة باستخدام ملف builders.json .

اختبار بناة الخاصة بك


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

لتبسيط الأنماط ، فكرنا في طريقة بسيطة لإنشاء مثيل Architect: أولاً ، يمكنك إنشاء JsonSchemaRegistry (لاختبار المخطط) ، ثم TestingArchitectHost ، وأخيراً ، مثيل Architect . الآن يمكنك ترجمة ملف التكوين builders.json .

فيما يلي مثال على تشغيل المجمّع ، الذي يقوم بتشغيل الأمر ls ويتحقق من إكمال الأمر بنجاح. يرجى ملاحظة أننا سوف نستخدم الإخراج القياسي للعمليات في المسجل .

 import { Architect, ArchitectHost } from '@angular-devkit/architect'; import { TestingArchitectHost } from '@angular-devkit/architect/testing'; import { logging, schema } from '@angular-devkit/core'; describe('Command Runner Builder', () => { let architect: Architect; let architectHost: ArchitectHost; beforeEach(async () => { const registry = new schema.CoreSchemaRegistry(); registry.addPostTransform(schema.transforms.addUndefinedDefaults); //  TestingArchitectHost –    . //     ,   . architectHost = new TestingArchitectHost(__dirname, __dirname); architect = new Architect(architectHost, registry); //      NPM-, //    package.json  . await architectHost.addBuilderFromPackage('..'); }); //      Windows it('can run ls', async () => { //  ,     . const logger = new logging.Logger(''); const logs = []; logger.subscribe(ev => logs.push(ev.message)); // "run"    ,       . const run = await architect.scheduleBuilder('@example/command-runner:command', { command: 'ls', args: [__dirname], }, { logger }); // "result" –    . //    "BuilderOutput". const output = await run.result; //  . Architect     //   ,    ,    . await run.stop(); //   . expect(output.success).toBe(true); // ,     . // `ls $__dirname`. expect(logs).toContain('index_spec.ts'); }); }); 

لتشغيل المثال أعلاه ، تحتاج إلى حزمة ts-node . إذا كنت تنوي استخدام Node ، فأعد تسمية index_spec.ts إلى index_spec.js .

باستخدام المجمع في المشروع


دعنا نخلق angular.json بسيط يوضح كل ما تعلمناه عن المجمعات. على افتراض أننا قمنا بتعبئة جامعنا في المثال / command-runner ثم أنشأنا تطبيقًا جديدًا باستخدام ng new builder-test ، فقد يبدو ملف angular.json بهذا الشكل (تمت إزالة بعض المحتوى للإيجاز):

 { // ...   . "projects": { // ... "builder-test": { // ... "architect": { // ... "build": { "builder": "@angular-devkit/build-angular:browser", "options": { // ...   "outputPath": "dist/builder-test", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "src/tsconfig.app.json" }, "configurations": { "production": { // ...   "optimization": true, "aot": true, "buildOptimizer": true } } } } 

إذا قررنا إضافة مهمة جديدة لتطبيقها (على سبيل المثال) الأمر touch على الملف (تحديث تاريخ تعديل الملف) باستخدام أداة التجميع الخاصة بنا ، فسنقوم بتشغيل npm install example / command-runner ، ثم إجراء تغييرات على angular.json :

 { "projects": { "builder-test": { "architect": { "touch": { "builder": "@example/command-runner:command", "options": { "command": "touch", "args": [ "src/main.ts" ] } }, "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist/builder-test", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "src/tsconfig.app.json" }, "configurations": { "production": { "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "optimization": true, "aot": true, "buildOptimizer": true } } } } } } } 

يحتوي Angular CLI على أمر تشغيل ، وهو الأمر الرئيسي لتشغيل المجمعات. كحجة أولى ، يستغرق سلسلة من التنسيق المشروع: TASK [: CONFIGURATION] . لتشغيل مهمتنا ، يمكننا استخدام ng run builder-test: touch command.

الآن قد نرغب في إعادة تعريف بعض الحجج. لسوء الحظ ، لا يمكننا إعادة تعريف المصفوفات من سطر الأوامر حتى الآن ، ومع ذلك يمكننا تغيير الأمر نفسه للتوضيح : ng run builder-test: touch --command = ls . - سيؤدي ذلك إلى إخراج ملف src / main.ts.

مشاهدة الوضع


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

  1. يجب على المُرجع إرجاع كائن BuilderOutput بعد كل تنفيذ. بعد الانتهاء ، يمكن أن يدخل وضع المراقبة الناتج عن حدث خارجي ، وإذا بدأ مرة أخرى ، فسيتعين عليه استدعاء دالة context.reportRunning () لإخبار المهندس المعماري بأن المجمع يعمل مرة أخرى. سيؤدي ذلك إلى حماية المجمع من إيقافه بواسطة المهندس في مكالمة جديدة.
  2. يقوم المهندس نفسه بإلغاء الاشتراك من Observable عندما يتوقف المجمع (باستخدام run.stop () ، على سبيل المثال) ، باستخدام منطق Teardown - خوارزمية التدمير. سيتيح لك ذلك إيقاف التجميع ومسحه إذا كانت هذه العملية قيد التشغيل بالفعل.

تلخيص ما سبق ، إذا قام جامعك بمراقبة الأحداث الخارجية ، فهو يعمل على ثلاث مراحل:

  1. التنفيذ. على سبيل المثال ، تجميع Webpack. تنتهي هذه الخطوة عند انتهاء Webpack للبناء وإرسال أداة التجميع الخاصة بك إلى BuilderOutput إلى Observable .
  2. الملاحظة. - بين إطلاقين ، يتم رصد الأحداث الخارجية. على سبيل المثال ، يراقب Webpack نظام الملفات عن أي تغييرات. تنتهي هذه الخطوة عندما يستأنف Webpack الإنشاء ويتم استدعاء context.reportRunning () . بعد هذه الخطوة ، تبدأ الخطوة 1 مرة أخرى.
  3. الانتهاء. - اكتملت المهمة بالكامل (على سبيل المثال ، كان من المتوقع أن يبدأ Webpack عددًا معينًا من المرات) أو أن بداية المجمع قد توقفت (باستخدام run.stop () ). في هذه الحالة ، يتم تنفيذ خوارزمية التدمير القابلة للملاحظة ، ويتم مسحها.

استنتاج


فيما يلي ملخص لما تعلمناه في هذا المنشور:

  1. نحن نقدم واجهة برمجة تطبيقات جديدة تتيح للمطورين تغيير سلوك أوامر CLI Angular وإضافة أخرى جديدة باستخدام المجمعات التي تنفذ المنطق اللازم.
  2. يمكن أن تكون أدوات التجميع متزامنة وغير متزامنة وتستجيب للأحداث الخارجية. يمكن أن يطلق عليهم عدة مرات ، وكذلك من قبل هواة جمع العملات الآخرين.
  3. تتم قراءة المعلمات التي يتلقاها المجمع عند بدء المهمة لأول مرة من ملف angular.json ، ثم يتم استبدالها بواسطة المعلمات من التكوين ، إن وجدت ، ثم يتم الكتابة فوقها بواسطة علامات سطر الأوامر إذا تمت إضافتها.
  4. الطريقة الموصى بها لاختبار أدوات التجميع هي من خلال اختبارات التكامل ، ومع ذلك يمكنك إجراء اختبار الوحدة بشكل منفصل عن منطق المجمع.
  5. إذا قام المجمع بإرجاع ملاحظة ، فيجب إزالته بعد المرور بخوارزمية التدمير.

في المستقبل القريب ، سيزداد تواتر استخدام واجهات برمجة التطبيقات هذه. على سبيل المثال ، يرتبط تطبيق Bazel ارتباطًا وثيقًا بهم.

نرى بالفعل كيف ينشئ المجتمع جامعي CLI جديدين للاستخدام ، على سبيل المثال ، المزاح والسرو للاختبار.

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


All Articles