Project Description
Now it's mainly about React development and also using server-side rendering. DEMO ) Recently, I want to write a project with Angular to experience the TypeScript method. Compared with Angular versus React, it's more convenient for me to develop. Many things don't need to be installed by myself.
Online address: https://music.soscoon.com
Github: https://github.com/Tecode/angular-music-player/tree/QQ-music
At present, we are still working hard to develop, and we have completed 80%...
low source
technology stack
- Angular 7.2.0
- pm2 3.4.1
- better-scroll 1.15.1
- rxjs 6.3.3
- ngrx 7.4.0
- hammerjs 2.0.8
NgRx Configuration
Actions
As with Vuex and Redux, you need to define some actionType s first. Here's an example
src/store/actions/list.action.ts
import { Action } from '@ngrx/store'; export enum TopListActionTypes { LoadData = '[TopList Page] Load Data', LoadSuccess = '[TopList API] Data Loaded Success', LoadError = '[TopList Page] Load Error', } // get data export class LoadTopListData implements Action { readonly type = TopListActionTypes.LoadData; } export class LoadTopListSuccess implements Action { readonly type = TopListActionTypes.LoadSuccess; } export class LoadTopListError implements Action { readonly type = TopListActionTypes.LoadError; constructor(public data: any) { } }
Merge ActionType
src/store/actions/index.ts
export * from './counter.action'; export * from './hot.action'; export * from './list.action'; export * from './control.action';
Reducers
Store data management to modify status based on ActionType
src/store/reducers/list.reducer.ts
import { Action } from '@ngrx/store'; import { TopListActionTypes } from '../actions'; export interface TopListAction extends Action { payload: any, index: number, size: number } export interface TopListState { loading?: boolean, topList: Array<any>, index?: 1, size?: 10 } const initState: TopListState = { topList: [], index: 1, size: 10 }; export function topListStore(state: TopListState = initState, action: TopListAction): TopListState { switch (action.type) { case TopListActionTypes.LoadData: return state; case TopListActionTypes.LoadSuccess: state.topList = (action.payload.playlist.tracks || []).slice(state.index - 1, state.index * state.size); return state; case TopListActionTypes.LoadErrhammerjsor: return state; default: return state; } }
Merge Reducer
src/store/reducers/index.ts
import { ActionReducerMap, createSelector, createFeatureSelector } from '@ngrx/store'; //import the weather reducer import { counterReducer } from './counter.reducer'; import { hotStore, HotState } from './hot.reducer'; import { topListStore, TopListState } from './list.reducer'; import { controlStore, ControlState } from './control.reducer'; //state export interface state { count: number; hotStore: HotState; topListStore: TopListState; controlStore: ControlState; } //register the reducer functions export const reducers: ActionReducerMap<state> = { count: counterReducer, hotStore, topListStore, controlStore, }
Effects
Processing asynchronous requests, similar to redux-sage redux-thunk, is an example of sending two requests at the same time and sending HotActionTypes.LoadSuccesstype to reduce to process data when both requests are completed.
Use catchError to capture errors when they occur, and send new LoadError() to process the status of the data.
LoadError
export class LoadError implements Action { readonly type = HotActionTypes.LoadError; constructor(public data: any) { } }
import { Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { map, mergeMap, catchError } from 'rxjs/operators'; import { HotActionTypes, LoadError, LoadSongListError } from '../actions'; import { of, forkJoin } from 'rxjs'; import { HotService } from '../../services'; @Injectable() export class HotEffects { @Effect() loadHotData$ = this.actions$ .pipe( ofType(HotActionTypes.LoadData), mergeMap(() => forkJoin([ this.hotService.loopList() .pipe(catchError(() => of({ 'code': -1, banners: [] }))), this.hotService.popularList() .pipe(catchError(() => of({ 'code': -1, result: [] }))), ]) .pipe( map(data => ({ type: HotActionTypes.LoadSuccess, payload: data })), catchError((err) => { //call the action if there is an error return of(new LoadError(err["message"])); }) )) ) constructor( private actions$: Actions, private hotService: HotService ) { } }
Merge Effect
Merge multiple Effect files together
src/store/effects/hot.effects.ts
import { HotEffects } from './hot.effects'; import { TopListEffects } from './list.effects'; export const effects: any[] = [HotEffects, TopListEffects]; export * from './hot.effects'; export * from './list.effects';
Inject Effect Reducer to app.module
src/app/app.module.ts
import { StoreModule } from '@ngrx/store'; import { EffectsModule } from "@ngrx/effects"; import { reducers, effects } from '../store'; imports: [ ... StoreModule.forRoot(reducers), EffectsModule.forRoot(effects), ... ],
Request Processing
Use HttpClient
post get delate put requests are supported HttpClient Details
src/services/list.service.ts
import { Injectable } from '@angular/core'; import { HttpClient } from "@angular/common/http"; @Injectable({ providedIn: 'root' }) export class TopListService { constructor(private http: HttpClient) { } // Carousel Map topList() { return this.http.get('/api/top/list?idx=1'); } }
src/services/index.ts
export * from "./hot.service"; export * from "./list.service";
Response Interceptor
Exceptions are handled here to uniformly capture error information, such as not logging in to the global prompt, where Token information is added to the message header when sending a request, and changes are made to the business.
import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpRequest, HttpResponse, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http'; import { Observable, throwError } from 'rxjs'; import { map, catchError } from 'rxjs/operators'; @Injectable() export class HttpConfigInterceptor implements HttpInterceptor { // constructor(public errorDialogService: ErrorDialogService) { } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { let token: string | boolean = false; // Compatible server-side rendering if (typeof window !== 'undefined') { token = localStorage.getItem('token'); } if (token) { request = request.clone({ headers: request.headers.set('Authorization', 'Bearer ' + token) }); } if (!request.headers.has('Content-Type')) { request = request.clone({ headers: request.headers.set('Content-Type', 'application/json') }); } request = request.clone({ headers: request.headers.set('Accept', 'application/json') }); return next.handle(request).pipe( map((event: HttpEvent<any>) => { if (event instanceof HttpResponse) { // console.log('event--->>>', event); // this.errorDialogService.openDialog(event); } return event; }), catchError((error: HttpErrorResponse) => { let data = {}; data = { reason: error && error.error.reason ? error.error.reason : '', status: error.status }; // this.errorDialogService.openDialog(data); console.log('Errors Captured by Interceptors', data); return throwError(error); })); } }
Interceptor Dependent Injection
src/app/app.module.ts
Interceptors need to be injected into app.module to take effect
// http interceptor, catch exceptions, add Token import { HttpConfigInterceptor } from '../interceptor/httpconfig.interceptor'; ... providers: [ { provide: HTTP_INTERCEPTORS, useClass: HttpConfigInterceptor, multi: true }, ... ],
Send a request
The project uses NgRx, so I request this.store.dispatch(new LoadHotData()) with NgRx, and in Effect I receive the type HotActionTypes.LoadData, which sends the request through Effect.
Set hotStore$to Observable Type Public hotStore$: Observable <HotState>, see the following code for details:
This completes the data request
import { Component, OnInit, ViewChild, ElementRef } from '@angular/core'; import { Store, select } from '@ngrx/store'; import { Observable } from 'rxjs'; import { LoadHotData } from '../../store'; import { HotState } from '../../store/reducers/hot.reducer'; @Component({ selector: 'app-hot', templateUrl: './hot.component.html', styleUrls: ['./hot.component.less'] }) export class HotComponent implements OnInit { // Set hotStore$to an observable type public hotStore$: Observable<HotState>; public hotData: HotState = { slider: [], recommendList: [] }; @ViewChild('slider') slider: ElementRef; constructor(private store: Store<{ hotStore: HotState }>) { this.hotStore$ = store.pipe(select('hotStore')); } ngOnInit() { // Send requests to get banner and list data this.store.dispatch(new LoadHotData()); // Subscribe to hotStore$to get changed data this.hotStore$.subscribe(data => { this.hotData = data; }); } }
Server-side rendering
Server-side rendering of Angular You can use angular-cli to create ng add @nguniversal/express-engine --clientProject Your project name will be the same as the name in package.json
The angular-music-player project has already run. Do not run any more
ng add @nguniversal/express-engine --clientProject angular-music-player // Package Run npm run build:ssr && npm run serve:ssr
After running, you will see that package.json's scripts have some more server-side packaging and running commands
"scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e", "compile:server": "webpack --config webpack.server.config.js --progress --colors", "serve:ssr": "node dist/server", "build:ssr": "npm run build:client-and-server-bundles && npm run compile:server", "build:client-and-server-bundles": "ng build --prod && ng run angular-music-player:server:production", "start:pro": "pm2 start dist/server" }
Angular introduces hammerjs
Hamerjs requires a window object when it is introduced, errors will occur when rendering on the server side, and no errors will occur when packaging. After packaging, run npm run serve:ssr to report ReferenceError: window is not defined.
Solutions introduced using require
!! Remember to add declare var require: any; otherwise ts will return error typescript get error ts2304: cannot find name'require', which we can use for other plug-ins that need to be injected on the server side.
src/app/app.module.ts
declare var require: any; let Hammer = { DIRECTION_ALL: {} }; if (typeof window != 'undefined') { Hammer = require('hammerjs'); } export class MyHammerConfig extends HammerGestureConfig { overrides = <any>{ // override hammerjs default configuration 'swipe': { direction: Hammer.DIRECTION_ALL } } } // Injecting hammerjs configuration providers: [ ... { provide: HAMMER_GESTURE_CONFIG, useClass: MyHammerConfig } ], ...
Module load on demand
Create list-component
ng g c list --module app or ng generate component --module app
After running successfully, you will find that there is one more folder and four more files in it
Create module
ng generate module list --routing
Two more files list-routing.module.ts and list.module.ts will run successfully
Configure src/app/list/list-routing.module.ts
Import ListComponent Configuration Route
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { ListComponent } from './list.component'; const routes: Routes = [ { path: '', component: ListComponent } ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) export class ListRoutingModule { }
Configure src/app/list/list.module.ts
Registering ListComponent in NgModule allows you to use <app-list><app-list> within the template. Note here that when we use ng g c list --module app to create a component, it will help us declare it once in app.module.ts and we need to delete it or we will get an error.
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ListRoutingModule } from './list-routing.module'; import { ListComponent } from './list.component'; import { BigCardComponent } from '../common/big-card/big-card.component'; import { ShareModule } from '../share.module'; @NgModule({ declarations: [ ListComponent, BigCardComponent ], imports: [ CommonModule, ListRoutingModule, ShareModule ] }) export class ListModule { }
Configure src/app/list/list.module.ts
This was the case before it was configured
After Configuration
const routes: Routes = [ { path: '', pathMatch: 'full', redirectTo: '/hot' }, { path: 'hot', loadChildren: './hot/hot.module#HotModule' }, { path: 'search', component: SearchComponent }, { path: 'profile', component: ProfileComponent }, { path: 'list', loadChildren: './list/list.module#ListModule' }, { path: 'smile', loadChildren: './smile/smile.module#SmileModule' }, ];
Open your browser and you'll see an extra list-list-module.js file
Loading on demand is done here
Why src/app/share.module.ts is needed
First look at what's written
src/app/share.module.ts declares some public components, such as <app-scroll> </app-scroll>, which we need to import into the module you need when we are ready
src/app/app.module.ts src/app/list/list.module.ts src/app/hot/hot.module.ts are available, so you can pull the source code to see it and slowly discover the secret.
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { HammertimeDirective } from '../directive/hammertime.directive'; import { ScrollComponent } from './common/scroll/scroll.component'; import { SliderComponent } from './common/slider/slider.component'; import { FormatTimePipe } from '../pipes/format-time.pipe'; @NgModule({ declarations: [ ScrollComponent, HammertimeDirective, SliderComponent, FormatTimePipe ], imports: [ CommonModule ], exports: [ ScrollComponent, HammertimeDirective, SliderComponent, FormatTimePipe ] }) export class ShareModule { }
Cross-domain processing
Here I want to explain that I only configure cross-domain processing of the development environment in my project. The production environment does not. I use a proxy made by nginx. Running npm start will succeed.
New file src/proxy.conf.json
ip or web address to proxy for target
Path Rewrite Path Rewrite
{ "/api": { "target": "https://music.soscoon.com/api", "secure": false, "pathRewrite": { "^/api": "" }, "changeOrigin": true } }
Examples of requests
songListDetail(data: any) { return this.http.get(`/api/playlist/detail?id=${data.id}`); }
Configure angular.json
Restart the project cross-domain to configure successfully
"serve": { "builder": "@angular-devkit/build-angular:dev-server", "options": { "browserTarget": "angular-music-player:build", "proxyConfig": "src/proxy.conf.json" }, "configurations": { "production": { "browserTarget": "angular-music-player:build:production" } } }
It's a short time here. Any suggestions or comments are welcome. I'll add them later.