عنوان:

‫ایجاد پروژه‌ی «کتابخانه» توسط Angular CLI 6.0


نویسنده: وحید نصیری
تاریخ: ۱۳۹۷/۰۲/۱۹ ۱۹:۳۰
آدرس: www.dntips.ir
یکی از مواردی که با Angular CLI 6.0 به شدت ساده شده‌است، ایجاد پروژه‌های «کتابخانه» Angular است. برای مثال شاید در حین استفاده‌ی از بعضی از کتابخانه‌ی ثالث تهیه شده‌ی برای Angular با خطای ذیل مواجه شده باشید:
Please open an issue in the library repository to alert its author and ask them to 
package the library using the Angular Package Format (https://goo.gl/jB3GVv).
این خطا زمانی رخ می‌دهد که تهیه کننده‌ی کتابخانه، فرمت بسته‌های Angular را رعایت نکرده باشد و ... رعایت کردن آن نیز کار بسیار مشکلی است. نگارش 6 در پشت صحنه، پروژه‌ی موفق ng-packagr را به مجموعه‌ی CLI اضافه کرده‌است و از این پس توسط خود CLI می‌توان کتابخانه‌های استاندارد Angular را تولید کرد. این مورد، مزیت استاندارد سازی کتابخانه‌ها‌ی npm حاصل را نیز به همراه دارد. مشکلی که گاهی از اوقات به علت عدم رعایت این ساختار با بسته‌های فعلی npm مخصوص Angular وجود دارند؛ مانند خطایی که عنوان شد. برای مثال بدون استفاده‌ی از این ابزار، نیاز است مستندات چند صفحه‌ای ساخت کتابخانه‌های Angular را سطر به سطر پیاده سازی کنید که توسط CLI 6.0 به صورت خودکار ایجاد و مدیریت می‌شود.


مراحل ایجاد یک پروژه‌ی «کتابخانه» توسط Angular CLI 6.0

مرحله‌ی اول ایجاد یک پروژه‌ی کتابخانه، مانند قبل، توسط دستور ng new و ایجاد یک پروژه‌ی دلخواه جدید است:
 ng new my-lib-test
به همراه Angular CLI 6.0، فرمت تنظیمات آن نیز تغییر کرده‌است و مفهوم workspace به آن اضافه شده‌است که در آن می‌توان چندین پروژه را تعریف کرد.
پس از ایجاد پروژه‌ی my-lib-test توسط دستور فوق و وارد شدن به پوشه‌ی اصلی آن توسط خط فرمان، می‌توان با اجرای دستور زیر، پروژه‌های دیگری را به پروژه‌ی جاری افزود:
 ng generate application my-app-name
اما اگر در اینجا بجای ذکر application، از نام library استفاده کنیم، یک کتابخانه را بجای یک برنامه، به workspace جاری اضافه می‌کند:
 ng generate library my-lib
پس از اجرای این دستور اگر به فایل angular.json دقت کنیم، این پروژه در ذیل projects اضافه شده‌است:


همچنین یک پوشه‌ی جدید به نام projects نیز ایجاد شده و پروژه‌ی my-lib داخل آن قرار گرفته‌است.


فایل جدید public_api.ts

پس از ایجاد کتابخانه‌ی جدید «my-lib»، فایل جدیدی به نام projects\my-lib\src\public_api.ts نیز به آن اضافه شده‌است:


با این محتوا:
/*
* Public API Surface of my-lib
*/
export * from './lib/my-lib.service';
export * from './lib/my-lib.component';
export * from './lib/my-lib.module';
هر خروجی که در اینجا ذکر شود توسط استفاده کنندگان از این کتابخانه قابل دسترسی خواهد بود. برای مثال دستور «ng generate library my-lib» مطابق تصویر فوق، یک سرویس جدید را به نام my-lib.service، یک کامپوننت جدید را به نام my-lib.component و یک ماژول جدید را به نام my-lib.module به صورت پیش‌فرض ایجاد کرده و درون پوشه‌ی lib قرار داده‌است. اکنون آن‌ها را توسط فایل public_api.ts، به نحوی که مشاهده می‌کنید در معرض دید استفاده کنندگان قرار می‌دهد.
برای مثال اگر فایل جدید projects\my-lib\src\lib\my-lib.models.ts را به این کتابخانه اضافه کنیم که شامل تعدادی مدل و اینترفیس قابل دسترسی توسط استفاده کنندگان باشد، باید یک سطر زیر را به انتهای فایل public_api.ts اضافه کنیم:
 export * from './lib/my-lib.models';

این پروژه‌ی کتابخانه حتی به همراه فایل‌های package.json, tsconfig.json, tslint.json مخصوص به خود نیز می‌باشد تا بتوان آن‌ها را صرفا جهت این پروژه سفارشی سازی کرد.


ساختار my-lib.service پیش‌فرض یک پروژه‌ی کتابخانه

اگر به فایل projects\my-lib\src\lib\my-lib.service.ts دقت کنیم:
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class MyLibService {

  constructor() { }
}
تمام قسمت‌های آن مانند قبل است، منهای 'providedIn: 'root آن. این مورد تنظیم جدیدی است که در پروژه‌های Angular 6 قابل استفاده‌است. هدف از آن، ارائه‌ی یک سرویس، بدون نیاز به ثبت صریح آن در قسمت providers یک NgModule است.
شاید بپرسید چرا؟ هدف اصلی از آن، بهبود فرآیند tree-shaking یا حذف کدهای مرده و استفاده نشده‌است. ممکن است سرویسی را تعریف کنید، اما در برنامه استفاده نشود. این حالت خصوصا در پروژه‌های کتابخانه‌های ثالث ممکن است زیاد رخ دهد. به همین جهت با ارائه‌ی این قابلیت، امکان حذف ساده‌تر سرویس‌هایی که در برنامه استفاده نشده‌اند از خروجی نهایی کامپایل شده، وجود خواهد داشت.


چگونه به پروژه‌ی کتابخانه‌ی جدید، یک کامپوننت جدید را اضافه کنیم؟

تمام دستورات Angular CLI، در اینجا نیز کار می‌کنند. تنها تفاوت آن‌ها، ذکر صریح نام پروژه‌ی مورد استفاده است:
 ng generate component show-data --project=my-lib
دستور فوق کامپوننت جدید show-data را به پروژه‌ی my-lib اضافه خواهد کرد؛ به همراه به روز رسانی خودکار فایل projects/my-lib/src/lib/my-lib.module.ts این پروژه، جهت ثبت کامپوننت اضافه شده.
البته در اینجا باید فایل my-lib.module.ts را اندکی ویرایش کرد و ShowDataComponent را به قسمت exports نیز افزود:
@NgModule({
  imports: [
    CommonModule,
    HttpClientModule
  ],
  declarations: [MyLibComponent, ShowDataComponent],
  exports: [MyLibComponent, ShowDataComponent]
})
export class MyLibModule { }
به صورت پیش‌فرض، کامپوننت جدید را در قسمت declarations معرفی می‌کند. یک چنین کامپوننتی فقط داخل همان lib قابل استفاده‌است. اگر قرار است خارج از این lib نیز به آن دسترسی داشته باشیم، باید آن‌را در قسمت exports نیز قید کنیم.
همچنین قسمت imports آن نیز به صورت پیش‌فرض خالی است. اگر نیاز است با ngIf کار کنید، باید CommonModule را در اینجا قید کنید و اگر نیاز است تبادلات HTTP وجود داشته باشد، ذکر HttpClientModule نیز ضروری است.


مرحله‌ی ساخت پروژه

پیش از استفاده‌ی از این پروژه‌ی کتابخانه، باید آن‌را build کرد:
 ng build my-lib
در اینجا نیز دستور ng build مانند قبل است، با این تفاوت که نام پروژه‌ی کتابخانه نیز در اینجا ذکر شده‌است.
پس از اجرای این دستور، خروجی ذیل مشاهده می‌شود:
Building Angular Package
Building entry point 'my-lib'
Rendering Stylesheets
Rendering Templates
Compiling TypeScript sources through ngc
Downleveling ESM2015 sources through tsc
Bundling to FESM2015
Bundling to FESM5
Bundling to UMD
Minifying UMD bundle
Remap source maps
Relocating source maps
Copying declaration files
Writing package metadata
Removing scripts section in package.json as it's considered a potential security vulnerability.
Built my-lib
Built Angular Package!
- from: D:\my-lib-test\projects\my-lib
- to: D:\my-lib-test\dist\my-lib
همانطور که ملاحظه می‌کنید، پس از طی مراحل خاص تولید یک کتابخانه، خروجی نهایی آن‌را در پوشه‌ی dist\my-lib قرار داده‌است.


استفاده‌ی از کتابخانه‌ی تولید شده

پس از پایان موفقیت آمیز مرحله‌ی Build، اکنون نوبت به استفاده‌ی از این کتابخانه است. استفاده‌ی از آن نیز همانند تمام کتابخانه‌ها و وابستگی‌های ثالثی است که تا پیش از این از آن‌ها استفاده کرده‌ایم. برای مثال ماژول آن‌را در قسمت imports مربوط به NgModule کلاس AppModule معرفی می‌کنیم. برای این منظور به فایل src\app\app.module.ts مراجعه کرده و MyLibModule را به نحو ذیل اضافه می‌کنیم:
import { MyLibModule } from "my-lib";

@NgModule({
  imports: [
    BrowserModule,
    MyLibModule
  ]
})
export class AppModule { }
نکته‌ی مهمی که در اینجا باید به آن دقت داشت این است که هرچند در این پروژه، MyLibModule داخل پوشه‌ی projects\my-lib\src\lib قرار دارد، اما نباید مسیر نسبی آن‌را در اینجا ذکر کرد و باید صرفا نام پوشه‌ی my-lib واقع در پوشه‌ی node_modules را در اینجا در حین مسیر دهی import آن معرفی کرد (همانند تمام وابستگی‌های ثالث دیگر).
اما سؤال اینجا است که آیا این پوشه پس از build، داخل پوشه‌ی node_modules نیز کپی شده‌است؟ پاسخ آن خیر است و برای مدیریت خودکار آن، به صورت زیر عمل شده‌است:
اگر به فایل tsconfig.json اصلی و واقع در ریشه‌ی workspace دقت کنید، پس از اجرای دستور «ng generate library my-lib»، قسمت paths آن نیز به صورت خودکار ویرایش شده‌است:
{
  "compilerOptions": {
    "paths": {
      "my-lib": [
        "dist/my-lib"
      ]
    }
  }
}
معنای آن این است که هرگاه import ایی در برنامه به my-lib اشاره کند، کامپایلر TypeScript می‌داند که باید آن‌را از پوشه‌ی dist/my-lib دریافت و پردازش کند. به همین جهت در اینجا دیگر نیازی به کپی دستی این پوشه، به پوشه‌ی node_modules وجود ندارد.

برای نمونه اگر شاره‌گر ماوس را بر روی my-lib قرار دهید، به درستی مسیر خوانده شدن آن، تشخیص داده می‌شود.

به این ترتیب مسیر این import‌، چه در این پروژه‌ی محلی و چه برای کسانیکه پوشه‌ی dist/my-lib را به صورت یک بسته‌ی npm جدید دریافت کرده‌اند، یکی خواهد بود.

در ادامه اگر به فایل app.component.html مراجعه کرده و selector کامپوننت show-data را به آن اضافه کنیم:
 <lib-show-data></lib-show-data>
می‌توان محتویات این کامپوننت دریافت شده‌ی از کتابخانه را مشاهده کرد.


توزیع کتابخانه‌ی ایجاد شده برای عموم

برای اینکه این کتابخانه‌ی تولیدی را در اختیار عموم، در سایت npm قرار دهیم، ابتدا باید کتابخانه را در حالت production build تولید و سپس آن‌را publish کرد:
ng build my-lib --prod
cd dist/my-lib
npm publish
سطر اول، کتابخانه‌ی my-lib را در حالت production تواید می‌کند. سپس به پوشه‌ی فایل‌های نهایی تولید شده وارد می‌شویم و دستور npm publish را صادر می‌کنیم.
البته دستور آخر نیاز به ایجاد یک اکانت در سایت npm و وارد شدن به آن‌را دارد. جزئیات بیشتر آن در اینجا.

نظرات

  • رستمی رضا در ۱۳۹۷/۰۲/۲۱ ۱۲:۲۷
    با تشکر؛ بازنویسی مطلب «سازماندهی برنامه‌های Angular توسط ماژول‌ها» بر اساس این مقاله، شامل چه تغییراتی یا پیشنهاداتی می‌شود؟
    • وحید نصیری در ۱۳۹۷/۰۲/۲۱ ۱۲:۴۱
      - بجای قسمت declarations مربوط به shared.module می‌توان تمام components/directives/pipes اشتراکی آن‌را به یک کتابخانه‌ی مجزا منتقل کرد.
      - بجای قسمت providers مربوط به core.module، می‌توان این سرویس‌های اشتراکی را هم به یک کتابخانه‌ی مجزا منتقل کرد.
      در کل ساختار فایل my-lib.module.ts پیش‌فرض یک کتابخانه بسیار شبیه‌است به ساختار فایل shared.module.ts
  • امیر محریزی در ۱۳۹۷/۱۰/۲۲ ۱۱:۵۹
    با سلام. اگر بخواهیم یک کتابخانه را با توجه به راه حل بالا، به وجود بیاوریم و سپس از آن در یک پروژه دیگر استفاه کنیم، به چه صورت باید عمل کنیم؟ با تشکر
    • وحید نصیری در ۱۳۹۷/۱۰/۲۲ ۱۲:۱۱
      مراحل همان قسمت «استفاده‌ی از کتابخانه‌ی تولید شده » مطلب فوق است که به صورت عمومی توضیح داده شده، نه وابسته‌ی به پروژه‌ی خاصی.
  • ابوالفضل روشن ضمیر در ۱۳۹۸/۰۲/۲۹ ۱۶:۵۲
    اگر در پروژه  angular-template-driven-forms-lab که از مسیر‌های مطلق استفاده شده است و baseUrl به src  تنظیم شده است (در فایل  tsconfig.json ) کتابخانه ای اضافه کنیم
    ng generate library my-lib
    در زمان import کردن ماژول my-lib ناشناخته است . اگر baseUrl را به (  /.  ) یا (  . ) تنظیم کنیم باز مشکل وجود دارد.
     baseUrl باید به چه مقداری تنظیم کرد ؟
    • وحید نصیری در ۱۳۹۸/۰۲/۲۹ ۱۸:۵۱
      این تنظیم بر روی import کتابخانه‌ها تاثیری ندارد و نباید داشته باشد. باید به یک کتابخانه همانند بسته‌های npm نگاه کنید که import آن‌ها وابسته به مسیرهای پروژه‌ی استفاده کننده‌ی از آن نیست و کاملا مستقل از آن است. در اینجا جهت سهولت کار « .... قسمت paths آن نیز به صورت خودکار ویرایش شده‌است ...» تا مسیر کتابخانه را بتوانید همانند سطر سوم تصویری که ارسال شد، تعیین کنید (این مسیر نسبی نیست) و اگر شناسایی نمی‌شود، تنظیم compilerOptions مربوط به paths را که در متن جاری ذکر شده، بررسی کنید:

      • ابوالفضل روشن ضمیر در ۱۳۹۸/۰۲/۳۰ ۱۶:۳۷
        حق با شماست . مساله این است که در زمان تنظیم Path  در tsconfig.json   برای مسیر‌های مطلق باید مقدار baseUrl  را به src  تنظیم کنیم ( همانطور که در متن و بخش نظرات هم عنوان شده است ) در این حالت همه چیز به درستی کار می‌کند .  
            "paths": {
              "@app/*": [
                "app/*"
              ],
              "@app/core/*": [
                "app/core/*"
              ],
              "@app/shared/*": [
                "app/shared/*"
              ],
              "@env/*": [
                "environments/*"
              ]
            }

        حالا یک کتابخانه را طبق مراحل گفته شده در متن ایجاد می‌کنیم سپس آن را در app.module  ایمپورت کنیم .  اکنون با این خطا روبرو می‌شویم :  


        در فایل tsconfig  مقدارbaseUrl  را از src  به /. تغیر می‌دهیم حالا اگر  به فایل app.module  مراجعه کنیم خطای  پیدا نشدن ماژول برطرف شده است 
            "baseUrl": "./",
            "paths": {
              "@app/*": [
                "app/*"
              ],
              "@app/core/*": [
                "app/core/*"
              ],
              "@app/shared/*": [
                "app/shared/*"
              ],
              "@env/*": [
                "environments/*"
              ],
              "my-lib": [
                "dist/my-lib"
              ],
              "my-lib/*": [
                "dist/my-lib/*"
              ]
            }
         ولی مشکل اینجاست که جاهای دیگری که آدرس ماژول یا کلاس یا .. که با app@  یا app/shared@  یا app/core@  شروع شده اند با خطا روبرو می‌شوند .  مثلا تا قبل از تغیر baseUrl  به ( /.)  در زمان import  کردن کلاس FormErrorModel خطای وجود نداشت ! 

        • وحید نصیری در ۱۳۹۸/۰۲/۳۰ ۲۰:۱۱
          با تنظیم baseUrl به پوشه‌ی src، ریشه‌ی بررسی از داخل این پوشه شروع می‌شود. بنابراین برای اشاره‌ی به پوشه‌ای که یک سطح بالاتر است (یا همان پوشه‌ی dist که حاوی فایل‌های کامپایل شده‌ی کتابخانه است)، به این صورت عمل کنید:
              "baseUrl": "src",
              "paths": {
                "@app/*": [
                  "app/*"
                ],
                "@app/core/*": [
                  "app/core/*"
                ],
                "@app/shared/*": [
                  "app/shared/*"
                ],
                "@env/*": [
                  "environments/*"
                ],
                "my-lib": [
                  "../dist/my-lib"
                ],
                "my-lib/*": [
                  "../dist/my-lib/*"
                ]
              }
  • ابوالفضل روشن ضمیر در ۱۳۹۸/۰۲/۳۱ ۱۳:۳۴
    یک نکته‌ی تکمیلی
    اگر بعد از ng build --prod با خطای زیر روبرو شدید:   
    ENOTEMPTY: directory not empty, rmdir  F:\...\dist\my-lib\lib
    فایل angular.json  را باز کرده و در بخش  
    projects/YourProjectName/architect/build/options
    برای کلید  outputPath مقدار (dist/YourProjectName) را تعیین کنید. دلیل خطا این است که در زمان  ng build --prod، محتویات پوشه dist  پاک می‌شود و در پوشه dist کتابخانه my-lib قرار دارد. به همین دلیل خطای ذکر شده رخ می‌دهد. با تغییر مسیر از dist  به dist/YourProjectName دیگر این خطا صادر نمی‌شود. اینبار خروجی نهایی پروژه بجای dist در dist/YourProjectName قرار دارد.