สัญญาณจะเปลี่ยนเชิงมุมให้ดีขึ้น
จาก NgRx ComponentStore ถึง SignalStore: ประเด็นสำคัญจากโปรเจ็กต์สาธิตของฉัน
เตรียมตัวอพยพอย่างไรให้มีประสิทธิภาพ
ฉันเชื่อว่า Signals ใน Angular จะเปลี่ยนวิธีการสร้างแอปพลิเคชัน Angular โดยพื้นฐาน บทความนี้เป็นส่วนแรกของซีรีส์ที่มีจุดมุ่งหมายเพื่อแสดงให้คุณเห็นถึงศักยภาพของฟีเจอร์ใหม่นี้ และในขณะเดียวกันก็ช่วยให้คุณเตรียมพร้อมสำหรับการเปลี่ยนแปลงนี้ได้อย่างมีประสิทธิภาพ ในขณะที่ Signals อยู่ในหน้าตัวอย่างสำหรับนักพัฒนาซอฟต์แวร์ และร้านค้าที่ใช้สัญญาณ NgRx เป็นเพียง ต้นแบบ คุณสามารถเริ่มสร้างและปรับโครงสร้างส่วนประกอบของคุณใหม่ในลักษณะที่จะทำให้การโยกย้ายราบรื่นสำหรับคุณ ในส่วนแรกนี้ ฉันจะแสดงให้คุณเห็นว่าฉันใช้แอปพลิเคชันสาธิตเพื่อแสดงความแตกต่างระหว่าง ComponentStore
และโมเดลที่ใช้สัญญาณใหม่ได้อย่างไร ในส่วนถัดไปของซีรีส์นี้ ฉันจะเสนอหลักเกณฑ์บางประการเกี่ยวกับวิธีจัดการกับการเปลี่ยนแปลงนี้ ก่อนอื่นให้ฉันแนะนำ Signalsและ NgRx SignalStore
สัญญาณเชิงมุมเป็นโมเดลปฏิกิริยาใหม่ใน Angular 16 สัญญาณช่วยให้เราติดตามการเปลี่ยนแปลงสถานะในแอปพลิเคชันของเราและทริกเกอร์การแสดงผลเทมเพลตที่ปรับให้เหมาะสมที่สุด อัปเดต หากคุณยังใหม่กับ Signals นี่คือบทความที่แนะนำเป็นอย่างยิ่งบางส่วน:
- เอกสารประกอบสัญญาณเชิงมุมอย่างเป็นทางการ
- “สัญญาณในเชิงมุม — วิธีเขียนโค้ดที่มีปฏิกิริยามากขึ้น” โดย Deborah Kurata
- “เชิงมุมและสัญญาณ ทุกสิ่งที่คุณจำเป็นต้องรู้” โดย Robin Goetz
ทีมงาน NgRx และ "Marko Stanimirović" เปิด "RFC ใหม่ (ขอความคิดเห็น) สำหรับโซลูชันการจัดการสถานะตามสัญญาณ SignalStore" มันมีแนวทางเดียวกันกับ @ngrx/component-store
การใช้งานครั้งแรกกับเอกสารประกอบ API มีอยู่ใน NgRx SignalStore Playground repo
ดังที่ได้กล่าวไปแล้ว ฉันมั่นใจว่า Signals จะเปลี่ยนวิธีที่เราพัฒนาแอปพลิเคชัน Angular เพื่อให้ได้รับความรู้เพิ่มเติมเกี่ยวกับฟีเจอร์ใหม่นี้และผลกระทบในอนาคต ฉันได้สร้างคอมโพเนนต์ "รายการบทความ" สองเวอร์ชัน ฉันได้สร้างอันที่ใช้ ComponentStore
ก่อน จากนั้นจึงย้ายมันไปยังอันที่ใช้ SignalStore
ในบทความนี้ ฉันจะอธิบายขั้นตอนการใช้งานและความแตกต่างหลักๆ ที่ฉันพบ เพื่อที่คุณจะได้เข้าใจวิธีการทำงานของ SignalStore
s ได้ดีขึ้น
ซอร์สโค้ดฉบับเต็มมีอยู่ที่นี่:
แอปพลิเคชันใช้สไตล์และแบ็กเอนด์ที่โฮสต์โดยสาธารณะจาก "โครงการ RealWorld"
แอปพลิเคชันมีคุณลักษณะต่อไปนี้:
- เมนู ง่ายๆ สำหรับการสลับระหว่างรายการบทความที่ใช้
ComponentStore
- และSignalStore
- สองรายการบทความ รายการหนึ่งเป็นแบบ
ComponentStore
และอีกรายการเป็นแบบSignalStore
โดยจะแสดงผู้เขียนบทความ วันที่ตีพิมพ์ จำนวนไลค์ แท็ก และโอกาสในการขาย พวกเขาโหลดรายการบทความจากเซิร์ฟเวอร์ จึงมีการโหลดและมีสถานะข้อผิดพลาด - องค์ประกอบ การแบ่งหน้า ด้านล่างรายการบทความแต่ละบทความ ผู้ใช้ยังสามารถเปลี่ยนการแบ่งหน้าตามพารามิเตอร์ URL เช่น:
http://localhost:4200/article-list-component-store?selectedPage=3&pageSize=2
หากผู้ใช้เปลี่ยนพารามิเตอร์ URL หรือคลิกที่องค์ประกอบการแบ่งหน้า รายการบทความจะถูกโหลดซ้ำ
สถาปัตยกรรมแอปพลิเคชัน
ฉันใช้ Angular v16 กับส่วนประกอบแบบสแตนด์อโลน เนื่องจาก Signals ยังไม่ทำงานในแอปพลิเคชันที่ไม่มีโซน ฉันจึงใช้กลยุทธ์การตรวจจับการเปลี่ยนแปลง OnPush
กับไปป์ async
แอปจะบู๊ต AppComponent
ด้วย router-outlet
และรายการเมนูสองรายการสำหรับรายการบทความทั้งสองเวอร์ชัน:
ArticleListComponent_CS
เป็นเวอร์ชันที่ใช้ComponentStore
ของรายการบทความ มันเชื่อมต่อกับArticleListComponentStore
ArticleListComponent_SS
เป็นเวอร์ชันที่ใช้SignalStore
ของรายการบทความ มันเชื่อมต่อกับArticleListSignalStore
การใช้งาน "รายการบทความ" ทั้งสองใช้ร้านค้าระดับส่วนประกอบและอาศัยส่วนประกอบ UI ต่อไปนี้:
UiArticleListComponent
แสดงรายการบทความ (UiArticleLisItemComponent
)UiPaginationComponent
จัดการการแบ่งหน้า
โครงสร้างไดเร็กทอรีเป็นดังนี้:
src/ |-- app/ | |-- article-list-ngrx-component-store/ => ArticleListComponent_CS | |-- article-list-ngrx-signal-store/ => ArticleListComponent_SS | |-- models/ | |-- services/ | |-- ui-components/ | |-- app.component.ts | |-- app.routes.ts |-- libs/signal-store/
ส่วนประกอบรายการบทความ
รหัสชั้นเรียนขององค์ประกอบรายการบทความทั้งสองเกือบจะเหมือนกัน:
- เราฉีดเราเตอร์และร้านค้า
- เราอัปเดตพารามิเตอร์การแบ่งหน้าในร้านค้าหลังจากสร้างส่วนประกอบแล้ว นอกจากนี้เรายังอัปเดตพารามิเตอร์หากพารามิเตอร์ใน URL เปลี่ยนแปลง
ข้อแตกต่างเพียงอย่างเดียวคือคลาสของร้านค้าที่ฉีด: ArticleListComponentStore
และ ArticleListSignalStore
:
export class ArticleListComponent_CS { readonly store = inject(ArticleListComponentStore); readonly route = inject(ActivatedRoute); constructor( ) { this.route.queryParams.pipe(takeUntilDestroyed()).subscribe( routeParams => { this.store.setPaginationSettings(routeParams); this.store.loadArticles(); }); } } export class ArticleListComponent_SS { readonly store = inject(ArticleListSignalStore); readonly route = inject(ActivatedRoute); constructor( ) { this.route.queryParams.pipe(takeUntilDestroyed()).subscribe( routeParams => { this.store.setPaginationSettings(routeParams); this.store.loadArticles(); }); } }
เทมเพลตของส่วนประกอบก็คล้ายกันเช่นกัน ความแตกต่างพื้นฐานคือวิธีที่เราอ่านข้อมูลจากร้านค้า:
- เราใช้
async
ไปป์เพื่ออ่านจาก ตัวเลือกของComponentStore
และ - เราเพียงแค่รับค่าของ สัญญาณ ใน
SignalStore
@Component({ selector: ‘app-article-list-cs’, // ... providers: [ArticleListComponentStore], template: ` <ng-container *ngIf="(store.httpRequestState$ | async) === ‘FETCHING’"> Loading... </ng-container> <ng-container *ngIf="store.httpRequestState$ | async | httpRequestStateErrorPipe as errorMessage"> {{ errorMessage }} </ng-container> <ng-container *ngIf="(store.httpRequestState$ | async) === ‘FETCHED’"> <ng-container *ngIf="store.articles$ | async as articles"> <app-ui-article-list [articles]="articles"/> </ng-container> <ng-container *ngIf="store.pagination$ | async as pagination"> <app-ui-pagination [selectedPage]="pagination.selectedPage" [totalPages]="pagination.totalPages" (onPageSelected)="store.setSelectedPage($event); store.loadArticles();" /> </ng-container> </ng-container> ` }) @Component({ selector: ‘app-article-list-ss’, // ... providers: [ArticleListSignalStore], template: ` <ng-container *ngIf="store.httpRequestState() === ‘FETCHING’"> Loading... </ng-container> <ng-container *ngIf="store.httpRequestState() | httpRequestStateErrorPipe as errorMessage"> {{ errorMessage }} </ng-container> <ng-container *ngIf="store.httpRequestState() === ‘FETCHED’"> <ng-container *ngIf="store.articles() as articles"> <app-ui-article-list [articles]="articles"/> </ng-container> <ng-container *ngIf="store.pagination() as pagination"> <app-ui-pagination [selectedPage]="pagination.selectedPage()" [totalPages]="pagination.totalPages()" (onPageSelected)="store.setSelectedPage($event); store.loadArticles();" /> </ng-container> </ng-container> ` })
สถานะ
ฉันใช้โครงสร้างข้อมูลที่ไม่เปลี่ยนรูปแบบเดียวกันสำหรับการจัดเก็บสถานะในร้านค้าทั้งสองแห่ง (HttpRequestState
และ Articles
เป็นประเภทที่ไม่เปลี่ยนรูปแบบด้วย):
export type ArticleListState = { readonly selectedPage: number, readonly pageSize: number, readonly httpRequestState: HttpRequestState, readonly articles: Articles, readonly articlesCount: number, }
คุณสมบัติ selectedPage
ระบุหน้าที่มองเห็นได้ในปัจจุบัน คุณสมบัติ pageSize
กำหนดจำนวนบทความที่มองเห็นได้ ผู้ใช้สามารถเปลี่ยนแปลงค่าเหล่านี้ได้โดยใช้ส่วนประกอบการแบ่งหน้าหรือโดยการใช้พารามิเตอร์ URL
คุณสมบัติ httpRequestState
มีสถานะคำขอของรายการบทความ:
export type HttpRequestState = DeepReadonly< 'EMPTY' | 'FETCHING' | 'FETCHED' | { errorMessage: string } >;
เริ่มแรกค่าของมันคือ `EMPTY`
เราเปลี่ยนเป็น `FETCHING`
ก่อนที่เราจะส่งคำขอไปยังเซิร์ฟเวอร์ เมื่อการตอบกลับของเซิร์ฟเวอร์มาถึง เราจะตั้งค่าเป็น `FETCHED`
หากเซิร์ฟเวอร์ส่งการตอบสนองข้อผิดพลาดหรือมีข้อผิดพลาดในระหว่างการร้องขอ เราจะตั้งค่าสถานะคำขอเป็นวัตถุ { errorMessage: string }
พร้อมข้อความแสดงข้อผิดพลาด
การตอบสนองของเซิร์ฟเวอร์ประกอบด้วยจำนวนบทความทั้งหมดและตัวบทความเอง เราจัดเก็บไว้ในคุณสมบัติ articlesCount
และ articles
หลังจากที่เราสร้างส่วนประกอบรายการบทความ ร้านค้าของพวกเขาจะมีสถานะเริ่มต้น:
export const initialArticleListState: ArticleListState = { selectedPage: 0, pageSize: 3, httpRequestState: ‘EMPTY’, articles: [], articlesCount: 0 }
ร้านค้า
ฉันขยาย ArticleListComponentStore
จาก ComponentStore
:
@Injectable() export class ArticleListComponentStore extends ComponentStore<ArticleListState> { readonly selectedPage$: Observable<number> = /* ... */; readonly pageSize$: Observable<number> = /* ... */; readonly httpRequestState$: Observable<HttpRequestState> = /* ... */; readonly articles$: Observable<DeepReadonly<Articles>> = /* ... */; readonly articlesCount$: Observable<number> = /* ... */; readonly totalPages$: Observable<number> = /* ... */; readonly pagination$: Observable<{ selectedPage: number, totalPages: number }> = /* ... */; readonly articlesService = inject(ArticlesService); constructor( ) { super(initialArticleListState); } setPaginationSettings = this.updater( (state, s: RouteParamsPaginatonState) => /* ... */); readonly loadArticles = this.effect<void>(/* ... */); setRequestStateLoading = this.updater( (state) => /* ... */); setRequestStateSuccess = this.updater( (state, params: ArticlesResponseType) => /* ... */); setRequestStateError = this.updater( (state, error: string): => /* ... */); setSelectedPage = this.updater( (state, selectedPage: number) => /* ... */); }
ฉันสร้าง `ArticleListSignalStore` ด้วยฟังก์ชัน `signalStore()` ยอมรับคุณสมบัติต่างๆ ของร้านค้าตามลำดับ ฉันจะอธิบายรายละเอียดเพิ่มเติมเหล่านี้:
export const ArticleListSignalStore = signalStore( { debugId: ‘ArticleListSignalStore’ }, withState<ArticleListState>(initialArticleListState), withComputed(({ articlesCount, pageSize }) => ({ /* ... */ })), withComputed(({ selectedPage, totalPages }) => ({ /* ... */ })), withUpdaters(({ update }) => ({ setPaginationSettings: (s: RouteParamsPaginatonState) => /* ... */, setRequestStateLoading: () => /* ... */ , setRequestStateSuccess: => /* ... */ , setRequestStateError: (error: string) => /* ... */ , setSelectedPage: (selectedPage: number) => /* ... */, withEffects( ( { selectedPage, pageSize, setRequestStateLoading, setRequestStateSuccess, setRequestStateError }, ) => { const articlesService = inject(ArticlesService) // ... } ) );
ตัวเลือก
ArticleListComponentStore
เก็บสถานะไว้ในหัวเรื่อง store$
หัวข้อนี้จะปล่อยค่าทุกครั้งที่มีการเปลี่ยนแปลงสถานะ หากต้องการสังเกตการเปลี่ยนแปลงคุณสมบัติของรัฐทีละรายการ เราจะสร้างตัวเลือกแยกต่างหากสำหรับคุณสมบัติแต่ละรายการเหล่านี้:
readonly selectedPage$: Observable<number> = this.select(state => state.selectedPage); readonly pageSize$: Observable<number> = this.select(state => state.pageSize); readonly httpRequestState$: Observable<HttpRequestState> = this.select(state => state.httpRequestState); readonly articles$: Observable<DeepReadonly<Articles>> = this.select(state => state.articles); readonly articlesCount$: Observable<number> = this.select(state => state.articlesCount);
SignalStore
จะสร้าง signal
แยกต่างหากโดยอัตโนมัติสำหรับคุณสมบัติรูททั้งหมดของสถานะ เราเรียกสิ่งเหล่านี้ว่าสถานะบางส่วน เราสามารถเข้าถึงสถานะบางส่วนเหล่านี้ได้โดย:
ArticleListSignalStore.selectedPage()
ArticleListSignalStore.pageSize()
ArticleListSignalStore.httpRequestState()
ArticleListSignalStore.articles()
และArticleListSignalStore.articlesCount()
ฉันสร้างตัวเลือกรวมเพิ่มเติมใน ArticleListComponentStore
เพื่อคำนวณจำนวนหน้า:
readonly totalPages$: Observable<number> = this.select( this.articlesCount$, this.pageSize$, (articlesCount, pageSize) => Math.ceil(articlesCount / pageSize));
หากต้องการทำเช่นเดียวกันใน ArticleListSignalStore
ฉันใช้ฟังก์ชัน withComputed()
ฉันใส่สัญญาณ articlesCount
และ pageSize
เป็นพารามิเตอร์ให้กับฟังก์ชัน และคำนวณจำนวนหน้าทั้งหมด:
withComputed(({ articlesCount, pageSize }) => ({ totalPages: computed(() => Math.ceil(articlesCount() / pageSize())), })),
นอกจากนี้เรายังจำเป็นต้องเพิ่มตัวเลือก "รูปแบบมุมมอง" ให้กับองค์ประกอบการแบ่งหน้า นี่คือรหัสสำหรับตัวเลือกใน ArticleListComponentStore
:
readonly pagination$: Observable<{ selectedPage: number, totalPages: number }> = this.select( this.selectedPage$, this.totalPages$, (selectedPage, totalPages) => ({ selectedPage, totalPages }) );
และนี่คือตัวเลือกเดียวกันใน ArticleListSignalStore
เช่นกัน:
withComputed(({ selectedPage, totalPages }) => ({ pagination: computed(() => ({ selectedPage, totalPages })), })),
ผู้อัปเดต
ภายใน updaters
ของ ComponentStore
เราจะสร้างออบเจ็กต์สถานะที่ไม่เปลี่ยนรูปแบบใหม่ด้วยค่าที่อัปเดตแล้วส่งคืนเสมอ ออบเจ็กต์สถานะที่ส่งคืนมีคุณสมบัติทั้งหมดจากสถานะ ทั้งที่อัปเดตและไม่ได้แก้ไข
ตัวอย่างเช่น นี่คือวิธีที่เราจัดการกับการตอบสนองของเซิร์ฟเวอร์:
setRequestStateSuccess = this.updater((state, params: ArticlesResponseType): ArticleListState => { return { ...state, httpRequestState: ‘FETCHED’, articles: params.articles, articlesCount: params.articlesCount } });
พารามิเตอร์ params
มีค่า articles
และ articlesCount
จากการตอบสนองของเซิร์ฟเวอร์:
export type ArticlesResponseType = { articles: Articles, articlesCount: number }
ใน ArticleListSignalStore
เราสร้าง updater
s ด้วยฟังก์ชัน withUpdaters()
ใน updater
s เหล่านี้ เราสร้างออบเจ็กต์ที่ไม่เปลี่ยนรูปใหม่จากคุณสมบัติที่อัปเดตเท่านั้น ดังนั้นจึงไม่มี …state
ที่นี่ SignalStore
อัปเดตสถานะบางส่วนด้วยการใช้ค่าคุณสมบัติที่ส่งคืนเหล่านี้:
withUpdaters(({ update }) => ({ setPaginationSettings: (s: RouteParamsPaginatonState) => update(() => ({ // ... setRequestStateSuccess: (params: ArticlesResponseType) => update(() => ({ httpRequestState: ‘FETCHED’, articles: params.articles, articlesCount: params.articlesCount })) // ... }))
ผลกระทบ
ร้านค้ามี effect
เดียวที่ดึงรายการบทความจากเซิร์ฟเวอร์ นี่คือ effect
จาก ArticleListComponentStore
:
readonly loadArticles = this.effect<void>((trigger$: Observable<void>) => { return trigger$.pipe( withLatestFrom(this.selectedPage$, this.pageSize$), tap(() => this.setRequestStateLoading()), switchMap(([, selectedPage, pageSize]) => { return this.articlesService.getArticles({ limit: pageSize, offset: selectedPage * pageSize }).pipe( tapResponse( (response) => { this.setRequestStateSuccess(response); }, (errorResponse: HttpErrorResponse) => { this.setRequestStateError(‘Request error’); } ), ); }), ); });
ใน SignalStore
เราใช้เอฟเฟกต์กับฟังก์ชัน withEffects()
SignalStores
รองรับเอฟเฟกต์สองประเภท: เอฟเฟกต์แบบ RxJs และเอฟเฟกต์แบบ Promise
เอฟเฟกต์แบบ RxJs ดูคล้ายกับเอฟเฟกต์ที่เราใช้ใน ComponentStore
มาก:
withEffects( ( { selectedPage, pageSize, setRequestStateLoading, setRequestStateSuccess, setRequestStateError }, ) => { const articlesService = inject(ArticlesService) return { loadArticles: rxEffect<void>( pipe( tap(() => setRequestStateLoading()), switchMap(() => articlesService.getArticles({ limit: pageSize(), offset: selectedPage() * pageSize() })), tapResponse( (response) => { setRequestStateSuccess(response); }, (errorResponse: HttpErrorResponse) => { setRequestStateError(‘Request error’); } ) ) ) } } )
เอฟเฟกต์ที่ใช้ Promise
จะมีประโยชน์เมื่อ Promise
มีฟังก์ชันการทำงานที่เพียงพอ และเราไม่ต้องการพลังของ RxJ ในกรณีที่ดึงข้อมูลจากเซิร์ฟเวอร์ มันมีข้อเสียเปรียบ: ไม่รองรับตรรกะการยกเลิก:
withEffects( ( { selectedPage, pageSize, setRequestStateLoading, setRequestStateSuccess, setRequestStateError }, ) => { const articlesService = inject(ArticlesService) return { async loadArticles() { setRequestStateLoading(); try { const response = await lastValueFrom(articlesService.getArticles({ limit: pageSize(), offset: selectedPage() * pageSize() })); setRequestStateSuccess(response); } catch(e) { setRequestStateError(‘Request error’); } } } } )
สรุป
โดยสรุป ความแตกต่างที่สำคัญระหว่าง ComponentStore
และ SignalStore
คือ:
ComponentStore
มีสถานะอยู่ในหัวเรื่องstate$
SignalStore
มีสัญญาณแยกต่างหากสำหรับคุณสมบัติรูททั้งหมดของรัฐ (สถานะบางส่วน)- ใน
SignalStore
เราไม่จำเป็นต้องมีตัวเลือกเพื่อเข้าถึงคุณสมบัติระดับรูทของรัฐ โดยจะเก็บข้อมูลเหล่านี้ไว้ในสัญญาณแยกกัน ดังนั้นจึงสามารถเข้าถึงได้โดยตรง - ทั้ง
ComponentStore
และSignalStore
รองรับเอฟเฟกต์แบบ RxJs และSignalStore
ยังรองรับเอฟเฟกต์แบบPromise
อีกด้วย
แม้ว่าการใช้งาน SignalStore
ในปัจจุบันจะเป็นเพียงต้นแบบ และอาจมีการเปลี่ยนแปลง API ในอนาคต แต่ฉันก็สนุกกับการทำงานกับมันมาก API มีความยืดหยุ่นและเข้าใจง่าย เนื่องจากเป็นไปตามแนวคิดพื้นฐานของ ComponentStore
แต่เป็นแนวทางขั้นสูงกว่า
เพื่อให้ง่ายต่อการแก้ไขข้อบกพร่องการเปลี่ยนแปลงสถานะ updaters
และ effects
ฉันจึงแพตช์โค้ด SignalStore
ดั้งเดิมด้วยโค้ดดีบักบางส่วนจากโปรเจ็กต์ ngx-ngrx-component-store-debug-tools ของฉัน
คำถามหลักที่ฉันมีตอนนี้คือจะสร้าง ComponentStores
อย่างไรในลักษณะที่เมื่อมีการเผยแพร่ SignalStore
ที่พร้อมสำหรับการใช้งานจริง จะช่วยให้กระบวนการย้ายข้อมูลสามารถจัดการได้ง่าย
ในส่วนถัดไปของชุดบทความ ฉันจะกำหนดหลักเกณฑ์บางประการที่จะช่วยให้เราบรรลุเป้าหมายนี้ นอกจากนี้ ฉันจะตรวจสอบสถานการณ์ที่ซับซ้อนและเปรียบเทียบสองวิธีนี้ เช่น วิธีการทำงานของการยกเลิกคำขอ HTTP ใน SignalStore
และ ComponentStore
ขอบคุณสำหรับการอ่าน ฉันหวังว่าคุณจะพบว่าบทความของฉันมีประโยชน์ โปรดแจ้งให้เราทราบหากคุณมีข้อเสนอแนะ!
👨💻เกี่ยวกับผู้เขียน
ฉันชื่อ "Gergely Szerovay" ฉันทำงานเป็นหัวหน้าบทการพัฒนาส่วนหน้า การสอน (และการเรียนรู้) เชิงมุมเป็นหนึ่งในความสนใจของฉัน ฉันบริโภคเนื้อหาที่เกี่ยวข้องกับ Angular ทุกวัน ไม่ว่าจะเป็นบทความ พอดแคสต์ การพูดคุยในการประชุม และอื่นๆ อีกมากมาย
ฉันสร้างจดหมายข่าว Angular Addict ขึ้นมาเพื่อที่ฉันจะได้ส่งแหล่งข้อมูลที่ดีที่สุดที่ฉันเจอในแต่ละเดือนไปให้คุณ ไม่ว่าคุณจะเป็น Angular Addict ที่มีประสบการณ์หรือเป็นมือใหม่ ฉันก็ช่วยคุณได้
ถัดจากจดหมายข่าว ฉันยังมีสิ่งพิมพ์ชื่อ — คุณเดาได้เลย — Angular Addicts เป็นการรวบรวมแหล่งข้อมูลที่ฉันพบว่ามีข้อมูลและน่าสนใจที่สุด แจ้งให้เราทราบหากคุณต้องการที่จะรวมเป็นนักเขียน
มาเรียนรู้ Angular ด้วยกัน! สมัครสมาชิกที่นี่ 🔥
ติดตามฉันที่ "Medium", "Twitter" หรือ "LinkedIn" เพื่อเรียนรู้เพิ่มเติมเกี่ยวกับ Angular!