5 hal yang ingin saya ketahui ketika saya mulai menggunakan Angular

Modern Angular adalah kerangka kerja yang kuat dengan banyak fitur, bersamaan dengan itu sekilas konsep dan mekanisme kompleks muncul. Ini terutama terlihat bagi mereka yang baru saja mulai bekerja di front-end pada prinsipnya, dan dengan Angular pada khususnya.


Saya juga menghadapi masalah yang sama ketika saya datang ke Tinkoff ke posisi Junior Frontend Developer sekitar dua tahun yang lalu dan terjun ke dunia Angular. Karena itu, saya menawarkan kepada Anda sebuah cerita pendek tentang lima hal, yang pengertiannya akan sangat memudahkan pekerjaan saya pada awalnya.



Ketergantungan Injeksi (DI)


Awalnya saya masuk ke komponen dan melihat ada beberapa argumen di konstruktor kelas. Saya melakukan sedikit analisis tentang pekerjaan metode kelas, dan menjadi jelas bahwa ini adalah beberapa dependensi eksternal. Tapi bagaimana mereka masuk kelas? Di mana konstruktor dipanggil?


Saya sarankan segera untuk memahami contoh, tetapi untuk ini kita perlu kelas. Jika dalam OOP JavaScript "normal" hadir dengan "peretasan" tertentu, maka bersama dengan ES6 ada sintaks "nyata". Angular menggunakan TypeScript langsung dari kotak, di mana sintaksnya hampir sama. Karena itu, saya mengusulkan untuk menggunakannya lebih lanjut.


Bayangkan bahwa ada kelas JokerService di aplikasi kita yang mengelola lelucon. Metode getJokes() mengembalikan daftar lelucon. Misalkan kita menggunakannya di tiga tempat. Bagaimana cara membuat lelucon di tiga tempat berbeda dalam kode? Ada beberapa cara:


  1. Buat instance kelas di setiap tempat. Tetapi mengapa kita perlu menyumbat memori dan membuat begitu banyak layanan yang identik? Dan apakah ada 100 kursi?
  2. Jadikan metode statis dan ambil data menggunakan JokerService.getJokes ().
  3. Terapkan salah satu pola desain. Jika kita membutuhkan layanan untuk menjadi satu untuk seluruh aplikasi, maka ini akan menjadi Singleton. Tetapi untuk ini, Anda perlu menulis logika baru di kelas.

Jadi, kami memiliki tiga opsi yang cukup berhasil. Yang pertama tidak cocok untuk kita - dalam hal ini tidak efektif. Kami tidak ingin membuat salinan tambahan, karena mereka akan sepenuhnya identik. Masih ada dua opsi.


Mari menyulitkan tugas untuk memahami metode mana yang paling cocok untuk kita. Misalkan, di tempat ketiga, kita perlu untuk beberapa alasan untuk membuat layanan kita sendiri dengan parameter tertentu. Ini mungkin penulis tertentu, panjang lelucon, bahasa, dan banyak lagi. Apa yang akan kita lakukan?


Dalam hal metode statis, Anda harus melewati pengaturan dengan setiap panggilan, karena kelas umum untuk semua tempat. Artinya, dalam setiap panggilan ke getJokes() kami akan meneruskan semua parameter unik ke tempat ini. Tentu saja, lebih baik untuk meneruskannya ketika Anda instantiate dan kemudian panggil saja metode getJokes() .


Ternyata opsi kedua tidak cocok untuk kita: itu akan membuat kita selalu menduplikasi banyak kode di setiap tempat. Tetap hanya Singleton, yang lagi-lagi perlu memperbarui logika, tetapi dengan variasi. Tetapi bagaimana cara memahami opsi mana yang kita butuhkan?


Jika Anda berpikir bahwa Anda hanya dapat membuat objek dan menggunakan kunci untuk mengambil layanan yang diinginkan, saya dapat memberi selamat kepada Anda: Anda baru menyadari bagaimana Dependency Injection bekerja secara umum. Tapi mari kita sedikit lebih dalam.


Untuk memastikan bahwa suatu mekanisme diperlukan untuk membantu kami mendapatkan contoh yang diperlukan, bayangkan bahwa JokerService membutuhkan dua layanan lain, salah satunya adalah opsional, dan yang kedua harus memberikan hasil khusus di tempat tertentu. Itu tidak sulit.


Ketergantungan Injeksi dalam Sudut


Seperti yang dikatakan dalam dokumentasi , DI adalah pola desain yang penting untuk suatu aplikasi. Angular memiliki kerangka kerja dependensi sendiri, yang digunakan dalam Angular sendiri untuk meningkatkan efisiensi dan modularitas.


Secara umum, Injeksi Ketergantungan adalah mekanisme yang kuat di mana kelas menerima dependensi yang diperlukan dari suatu tempat di luar, daripada membuat instance sendiri.


Biarkan sintaks dan file dengan ekstensi html tidak membingungkan Anda. Setiap komponen dalam Angular adalah objek JavaScript biasa, turunan dari sebuah kelas. Secara umum: ketika Anda memasukkan komponen ke dalam templat, turunan dari kelas komponen dibuat. Dengan demikian, pada saat ini, Anda dapat meneruskan dependensi yang diperlukan ke konstruktor. Sekarang pertimbangkan sebuah contoh:


 @Component({ selector: 'jokes', template: './jokes.template.html', }) export class JokesComponent { private jokes: Observable<IJoke[]>; constructor(private jokerService: JokerService) { this.jokes = this.jokerService.getJokes(); } } 

Dalam konstruktor komponen, kami cukup menunjukkan bahwa kami membutuhkan JokerService . Kami tidak membuatnya sendiri. Jika ada lima komponen lagi yang menggunakannya, maka mereka semua akan merujuk ke contoh yang sama. Semua ini memungkinkan kita menghemat waktu, menghilangkan pelat tungku dan menulis aplikasi yang sangat produktif.


Penyedia


Dan sekarang saya mengusulkan untuk menangani kasus ini ketika Anda perlu mendapatkan contoh layanan yang berbeda. Pertama, lihatlah layanan itu sendiri:


 @Injectable({ providedIn: 'root', //   ,   «»  }) export class JokerService { getJokes(): Observable<IJoke[]> { //     } } 

Ketika layanan adalah satu untuk seluruh aplikasi, opsi ini akan cukup. Tetapi bagaimana jika kita memiliki, katakanlah, dua implementasi JokerService ? Atau apakah hanya karena alasan tertentu bahwa komponen tertentu memerlukan instance layanannya sendiri? Jawabannya sederhana: provider .


Untuk kenyamanan, saya akan memanggil provider penyedia , dan proses penggantian nilai ke dalam kelas akan diperiksa . Jadi, kami dapat menyediakan layanan dengan cara dan tempat yang berbeda. Mari kita mulai dengan yang terakhir. Ada tiga opsi yang tersedia:


  • Ke seluruh aplikasi - tentukan provideIn: 'root' di dekorator layanan itu sendiri.
  • Dalam modul - tentukan penyedia di dekorator layanan sebagai provideIn: JokesModule atau di dekorator modul @NgModule providers: [JokerService] .
  • Dalam komponen - tentukan penyedia di dekorator komponen, seperti dalam modul.

Tempat ini dipilih tergantung kebutuhan Anda. Kami menemukan tempat itu, mari beralih ke mekanisme itu sendiri. Jika kita hanya menetapkan provideIn: root di layanan, ini akan setara dengan entri berikut dalam modul:


 @NgModule({ // ...     providers: [{provide: JokerService, useClass: JokerService}], }) //   

Ini bisa dibaca seperti ini: "Jika JokerService diminta, maka berikan instance dari kelas JokerService» Dari sini Anda bisa mendapatkan contoh spesifik dengan berbagai cara:


  • Dengan token - Anda harus menentukan InjectionToken dan mendapatkan layanannya. Perhatikan bahwa dalam contoh di bawah ini, Anda dapat melewati token yang sama:


     const JOKER_SERVICE_TOKEN = new InjectionToken<string>('JokerService'); // ...     [{provide: JOKER_SERVICE_TOKEN, useClass: JokerService}]; 

  • Berdasarkan kelas - Anda dapat mengganti kelas. Misalnya, kami akan meminta JokerService , dan memberikan - JokerHappyService :


     [{provide: JokerService, useClass: JokerHappyService}]; 

  • Berdasarkan nilai - Anda dapat segera mengembalikan instance yang diinginkan:


     [{provide: JokerService, useValue: jokerService}]; 

  • Dengan pabrik - Anda dapat mengganti kelas dengan pabrik yang akan membuat instance yang diinginkan ketika diakses:


     [{provide: JokerService, useFactory: jokerServiceFactory}]; 


Itu saja. Yaitu, untuk memecahkan contoh dengan contoh khusus, Anda dapat menggunakan salah satu metode di atas. Pilih yang paling cocok untuk kebutuhan Anda.


Omong-omong, DI bekerja tidak hanya untuk layanan, tetapi secara umum untuk entitas apa pun yang Anda dapatkan di konstruktor komponen. Ini adalah mekanisme yang sangat kuat yang harus digunakan secara maksimal.


Ringkasan kecil


Untuk pemahaman yang lengkap, saya mengusulkan untuk mempertimbangkan mekanisme Injeksi Ketergantungan yang disederhanakan dalam Angular dalam langkah-langkah menggunakan contoh layanan:


  1. Saat menginisialisasi aplikasi, layanan memiliki token. Jika kami tidak menentukannya secara khusus di penyedia, maka ini adalah JokerService.
  2. Ketika suatu layanan diminta dalam suatu komponen, mekanisme DI memeriksa apakah token yang ditransfer ada.
  3. Jika token tidak ada, maka DI akan melempar kesalahan. Dalam kasus kami, token ada dan JokerService terletak di sana.
  4. Ketika komponen dibuat, turunan dari JokerService diteruskan ke konstruktor sebagai argumen.

Ubah deteksi


Kita sering mendengar, sebagai argumen untuk menggunakan kerangka kerja, sesuatu seperti “Kerangka kerja akan melakukan segalanya untuk Anda - lebih cepat dan lebih efisien. Anda tidak perlu memikirkan apa pun. Kelola saja datanya. ” Mungkin ini benar dengan aplikasi yang sangat sederhana. Tetapi jika Anda harus bekerja dengan input pengguna dan terus beroperasi pada data, maka Anda hanya perlu tahu bagaimana proses mendeteksi perubahan dan rendering bekerja.


Di Angular, Change Detection bertanggung jawab untuk memeriksa perubahan. Sebagai hasil dari berbagai operasi - mengubah nilai properti kelas, menyelesaikan operasi asinkron, menanggapi permintaan HTTP, dan sebagainya - proses verifikasi dimulai di seluruh pohon komponen.


Karena tujuan utama dari proses ini adalah untuk memahami cara merender ulang suatu komponen, intinya adalah memverifikasi data yang digunakan dalam templat. Jika berbeda, maka templat ditandai sebagai "diubah" dan akan digambar ulang.


Zone.js


Memahami bagaimana Angular melacak properti kelas dan operasi sinkron sangat sederhana. Tapi bagaimana cara melacak asinkron? Perpustakaan Zone.js, yang dibuat oleh salah satu pengembang Angular, bertanggung jawab untuk ini.


Inilah dia. Zona itu sendiri adalah "konteks eksekusi," untuk membuatnya terus terang, tempat dan negara di mana kode dieksekusi. Setelah operasi asinkron selesai, fungsi panggilan balik dieksekusi di zona yang sama tempat terdaftarnya. Jadi Angular mencari tahu di mana perubahan itu terjadi dan apa yang harus diperiksa.


Zone.js menggantikan dengan implementasinya hampir semua fungsi dan metode asinkron asli. Oleh karena itu, ia dapat melacak kapan callback fungsi asinkron akan dipanggil. Yaitu, Zone memberi tahu Angular kapan dan di mana untuk memulai proses validasi perubahan.


Ubah Strategi Deteksi


Kami menemukan bagaimana Angular memonitor komponen dan menjalankan pemeriksaan perubahan. Sekarang bayangkan Anda memiliki aplikasi besar dengan banyak komponen. Dan untuk setiap klik, setiap operasi yang tidak sinkron, setiap permintaan yang berhasil dieksekusi, sebuah cek diluncurkan di seluruh pohon komponen. Kemungkinan besar, aplikasi semacam itu akan memiliki masalah kinerja yang serius.


Pengembang sudut memikirkan hal ini dan memberi kami kesempatan untuk membangun strategi deteksi perubahan, pilihan yang tepat yang dapat secara signifikan meningkatkan produktivitas.


Ada dua opsi untuk dipilih:


  • Default - seperti namanya, ini adalah strategi default ketika CD diluncurkan untuk setiap tindakan.
  • OnPush adalah strategi di mana CD diluncurkan hanya dalam beberapa kasus:
    • jika nilai @Input() telah berubah;
    • jika suatu peristiwa telah terjadi di dalam komponen atau turunannya;
    • jika pemeriksaan dimulai secara manual;
    • jika acara baru tiba di Async Pipe.

Berdasarkan pengalaman pengembangan saya sendiri pada Angular, serta pengalaman rekan-rekan saya, saya dapat mengatakan dengan pasti bahwa selalu lebih baik untuk menentukan strategi OnPush , kecuali jika default benar-benar diperlukan. Ini akan memberi Anda beberapa keuntungan:


  • Pemahaman yang jelas tentang bagaimana proses CD bekerja.
  • Rapi bekerja dengan properti @Input() .
  • Keuntungan kinerja.

Bekerja dengan @Input()


Seperti kerangka kerja populer lainnya, Angular menggunakan aliran data hilir. Komponen menerima parameter input yang ditandai dengan dekorator @Input() . Pertimbangkan sebuah contoh:


 interface IJoke { author: string; text: string; } @Component({ selector: 'joke', template: './joke.template.html', }) export class JokeComponent { @Input() joke: IJoke; } 

Misalkan ada komponen yang dijelaskan di atas yang menampilkan teks lelucon dan penulis. Masalah dengan penulisan ini adalah bahwa Anda dapat secara tidak sengaja atau secara khusus mengubah objek yang ditransfer. Misalnya, menimpa teks atau penulis.


 setAuthorNameOnly() { const name = this.joke.author.split(' ')[0]; this.joke.author = name; } 

Saya segera mencatat bahwa ini adalah contoh yang buruk, tetapi jelas menunjukkan apa yang mungkin terjadi. Untuk melindungi dari kesalahan tersebut, Anda perlu membuat parameter input hanya-baca. Berkat ini, Anda akan memiliki pemahaman tentang cara bekerja dengan data dengan benar dan membuat CD. Berdasarkan ini, cara terbaik untuk menulis kelas akan terlihat seperti ini:


 @Component({ selector: 'joke', template: './joke.template.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class JokeComponent { @Input() readonly joke: IJoke; @Output() updateName = new EventEmitter<string>(); setAuthorNameOnly() { const name = this.joke.author.split(' ')[0]; this.updateName.emit(name); } } 

Pendekatan yang dijelaskan bukan aturan, tetapi hanya rekomendasi. Ada banyak situasi di mana pendekatan ini tidak nyaman dan tidak efektif. Seiring waktu, Anda akan belajar untuk memahami dalam hal mana Anda dapat menolak metode yang diusulkan untuk bekerja dengan input.


Rxjs


Tentu saja, saya bisa saja salah, tetapi sepertinya ReactiveX dan pemrograman reaktif secara umum adalah tren baru. Angular menyerah pada tren ini (atau mungkin membuatnya) dan menggunakan RxJS secara default. Logika dasar dari seluruh kerangka berjalan di perpustakaan ini, sehingga sangat penting untuk memahami prinsip-prinsip pemrograman reaktif.


Tapi apa itu RxJS? Ini menggabungkan tiga ide yang akan saya ungkapkan dalam bahasa yang cukup sederhana dengan beberapa kelalaian:


  • Pola "Pengamat" adalah entitas yang menghasilkan peristiwa, dan ada pendengar yang menerima informasi tentang peristiwa ini.
  • Pola Iterator - memungkinkan Anda untuk mendapatkan akses berurutan ke elemen-elemen objek tanpa mengungkapkan struktur internalnya.
  • Pemrograman fungsional dengan koleksi adalah pola di mana logika berdetak menjadi komponen kecil dan sangat sederhana, yang masing-masing memecahkan hanya satu masalah.

Menggabungkan pola-pola ini memungkinkan kita untuk secara sederhana menggambarkan algoritma yang kompleks pada pandangan pertama, misalnya:


 private loadUnreadJokes() { this.showLoader(); //   fromEvent(document, 'load') .pipe( switchMap( () => this.http .get('/api/v1/jokes') //   .pipe(map((jokes: any[]) => jokes.filter(joke => joke.unread))), //   ), ) .subscribe( (jokes: any[]) => (this.jokes = jokes), //   error => { /*   */ }, () => this.hideLoader(), //       ); } 

Hanya 18 baris dengan semua lekukan yang indah. Sekarang cobalah menulis ulang contoh ini pada Vanilla atau setidaknya jQuery. Hampir 100% dari ini akan membawa Anda setidaknya dua kali lebih banyak ruang dan tidak akan begitu ekspresif. Di sini Anda cukup mengikuti garis dengan mata Anda dan membaca kode seperti buku.


Diamati


Memahami bahwa data apa pun dapat direpresentasikan sebagai aliran tidak datang dengan segera. Karena itu, saya mengusulkan untuk pindah ke analogi sederhana. Bayangkan aliran adalah array data yang diurutkan berdasarkan waktu. Misalnya, dalam perwujudan ini:


 const observable = []; let counter = 0; const intervalId = setInterval(() => { observable.push(counter++); }, 1000); setTimeout(() => { clearInterval(intervalId); }, 6000); 

Kami akan menganggap nilai terakhir dalam array relevan. Setiap detik angka akan ditambahkan ke array. Bagaimana kita bisa mengetahui di tempat lain dalam aplikasi bahwa elemen telah ditambahkan ke array? Dalam situasi normal, kita akan memanggil semacam callback dan memperbarui nilai array di atasnya, dan kemudian mengambil elemen terakhir.


Berkat pemrograman reaktif, tidak perlu tidak hanya menulis banyak logika baru, tetapi juga berpikir tentang memperbarui informasi. Ini dapat dibandingkan dengan pendengar sederhana:


 document.addEventListener('click', event => {}); 

Anda dapat menempatkan banyak EventListener di seluruh aplikasi, dan mereka akan berhasil, kecuali, tentu saja, Anda menangani hal yang berlawanan dengan sengaja.


Pemrograman reaktif juga berfungsi. Di satu tempat, kami cukup membuat aliran data dan secara berkala menjatuhkan nilai-nilai baru di sana, dan di tempat lain, kami berlangganan aliran ini dan hanya mendengarkan nilai-nilai ini. Artinya, kami selalu belajar tentang pembaruan dan dapat mengatasinya.


Sekarang mari kita lihat contoh nyata:


 export class JokesListComponent implements OnInit { jokes$: Observable<IJoke>; authors$ = new Subject<string[]>(); unread$ = new Subject<number>(); constructor(private jokerService: JokerService) {} ngOnInit() { //  ,    subscribe()    this.jokes$ = this.jokerService.getJokes(); this.jokes$.subscribe(jokes => { this.authors$.next(jokes.map(joke => joke.author)); this.unread$.next(jokes.filter(joke => joke.unread).length); }); } } 

Berkat logika ini, ketika mengubah data dalam jokes , kami secara otomatis memperbarui data pada jumlah lelucon yang belum dibaca dan daftar penulis. Jika Anda memiliki beberapa komponen lagi, salah satunya mengumpulkan statistik tentang jumlah lelucon yang dibaca oleh seorang penulis, dan yang kedua menghitung panjang rata-rata lelucon, maka keuntungannya menjadi jelas.


Diuji


Cepat atau lambat, pengembang mengerti bahwa jika proyek tersebut bukan MVP, maka Anda perlu menulis tes. Dan semakin banyak tes yang akan ditulis, semakin jelas dan lebih rinci uraiannya, semakin mudah, lebih cepat, dan lebih dapat diandalkan untuk melakukan perubahan dan menerapkan fungsi baru.


Angular mungkin meramalkan ini dan memberi kami alat pengujian yang kuat. Banyak pengembang pada awalnya mencoba untuk menguasai beberapa jenis teknologi "dari awal" tanpa masuk ke dokumentasi. Saya melakukan hal yang sama, itulah sebabnya saya menyadari sangat terlambat semua kemampuan pengujian tersedia “out of the box”.


Anda dapat menguji apa pun dalam Angular, tetapi jika Anda hanya perlu instantiate dan mulai memanggil metode untuk menguji kelas atau layanan reguler, situasi dengan komponen benar-benar berbeda.


Seperti yang sudah kita ketahui, terima kasih kepada dependensi DI yang diambil di luar komponen. Di satu sisi, ini sedikit menyulitkan seluruh sistem, di sisi lain, ini memberi kita peluang besar untuk melakukan tes dan memeriksa banyak kasus. Saya mengusulkan untuk memahami contoh komponen:


 @Component({ selector: 'app-joker', template: '<some-dependency></some-dependency>', styleUrls: ['./joker.component.less'], }) export class JokerComponent { constructor( private jokesService: JokesService, @Inject(PARTY_TOKEN) private partyService: PartyService, @Optional() private sleepService: SleepService, ) {} makeNewFriend(): IFriend { if (this.sleepService && this.sleepService.isSleeping) { this.sleepService.wakeUp(); } const joke = this.jokesService.generateNewJoke(); this.partyService.goToParty('Pacha'); this.partyService.toSay(joke.text); const laughingPeople = this.partyService.getPeopleByReaction('laughing'); const girl = laughingPeople.find(human => human.sex === 'female'); const friend = this.partyService.makeFriend(girl); return friend; } } 

Jadi, dalam contoh saat ini ada tiga layanan. Satu diimpor dengan cara biasa, satu dengan token dan layanan lain bersifat opsional. Bagaimana cara mengkonfigurasi modul tes? Saya akan segera menampilkan tampilan yang sudah selesai:


 beforeEach(async(() => { TestBed.configureTestingModule({ imports: [SomeDependencyModule], declarations: [JokerComponent], //  ,    providers: [{provide: PARTY_TOKEN, useClass: PartyService}], }).compileComponents(); fixture = TestBed.createComponent(JokerComponent); component = fixture.componentInstance; fixture.detectChanges(); //    ,     })); 

TestBed memungkinkan kita untuk membuat simulasi lengkap dari modul yang diperlukan. Anda dapat menghubungkan ke dalamnya layanan apa pun, mengganti modul, mendapatkan instance kelas dari suatu komponen, dan banyak lagi. Sekarang kita sudah memiliki modul yang sudah dikonfigurasi, mari kita beralih ke kemungkinan.


Ketergantungan yang tidak perlu dapat dihindari


Aplikasi Angular terdiri dari modul, yang dapat mencakup modul lain, layanan, arahan, dan banyak lagi. Dalam pengujian, kita perlu, pada kenyataannya, untuk menciptakan kembali operasi modul. Jika dalam contoh kami, kami menggunakan <some-dependency></some-dependency> dalam templat, ini berarti bahwa kami juga harus mengimpor SomeDependencyModule ke dalam pengujian. Dan apakah ada kecanduan di sana? Jadi, mereka juga perlu diimpor.
Jika aplikasi ini rumit, akan ada banyak dependensi seperti itu. Mengimpor semua dependensi akan mengarah pada fakta bahwa dalam setiap pengujian seluruh aplikasi akan ditemukan dan semua metode akan dipanggil. Mungkin ini tidak cocok untuk kita.


Setidaknya ada satu cara untuk menghilangkan dependensi yang diperlukan - cukup tulis ulang template. Misalkan Anda memiliki pengujian tangkapan layar atau pengujian integrasi dan tidak perlu menguji penampilan komponen. Maka itu cukup dengan hanya memeriksa metode. Dalam hal ini, Anda dapat menulis konfigurasi sebagai berikut:


 TestBed.configureTestingModule({ declarations: [JokerComponent], providers: [{provide: PARTY_TOKEN, useClass: PartyService}], }) .overrideTemplate(JokerComponent, '') //   ,   .compileComponents(); 

, . , . , . , , , , . — .



Injection Token , . . , , .


ts-mockito , , . Angular « ».


 //    export class MockPartyService extends PartyService { meetFriend(): IFriend { return {} as IFriend; } goToParty() {} toSay(some: string) { console.log(some); } } // ... TestBed.configureTestingModule({ declarations: [JokerComponent, MockComponent], providers: [{provide: PARTY_TOKEN, useClass: MockPartyService}], //    }).compileComponents(); 

Itu saja. .



. , — , — . , :


  • .
  • — , . — .

— . , . — .


Ringkasan


Angular, . , , «».


, Angular - . HTTP-, , lazy-loading . Angular .

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


All Articles