"Angular7+NgRx+SSR Family Barrel" Develops QQ Music

Keywords: angular JSON npm React

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.

Posted by chrislead on Wed, 21 Aug 2019 20:01:08 -0700