Project description

Now I mainly do React development, and also use server-side rendering (DEMO). Recently, I want to write a project with Angular to experience TypeScript. Compared with Angular and React, I think it is more convenient for development experience.

Online :music.soscoon.com

Making: github.com/Tecode/angu…

It is still in development, 80% complete at present…

preview

Technology stack

  • Presents 7.2.0
  • Pm2 3.4.1 track
  • Better – scroll 1.15.1
  • RXJS 6.3.3
  • NGRX 7.4.0
  • Hammerjs mid-atlantic moved

NgRx configuration

Actions

Just like Vuex and Redux, you need to define some actionType first. Here is 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 the 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) {}}Copy the code

Merge ActionType

src/store/actions/index.ts

export * from './counter.action';
export * from './hot.action';
export * from './list.action';
export * from './control.action';
Copy the code

Reducers

Store data Manage data and change the 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 interfaceTopListState { 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:
      returnstate; }}Copy the code

Merger of 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,
}
Copy the code

Effects

Asynchronous request processing, similar to the story – sage redux – thunk, the following example is sending two requests at the same time, wait until after the completion of the two requests are sent HotActionTypes. LoadSuccesstype to process the data in the reducer.

Catch an error with a catchError when it occurs, and send new LoadError() to handle the status of the data.

LoadError

export class LoadError implements Action {
    readonly type = HotActionTypes.LoadError;
    constructor(public data: any) {}}Copy the code
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(a)export class HotEffects {

  @Effect()
  loadHotData$ = this.actions$
    .pipe(
      ofType(HotActionTypes.LoadData),
      mergeMap((a)= >
        forkJoin([
          this.hotService.loopList()
            .pipe(catchError((a)= > of({ 'code': - 1, banners: [] }))),
          this.hotService.popularList()
            .pipe(catchError((a)= > 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
  ) {}}Copy the code

Combined 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';

Copy the code

injectionEffect Reducertoapp.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), ... ] .Copy the code

The request processing

Use HttpClient

Post Get delate PUT requests are supported by HttpClient

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) {}/ / round figure
  topList() {
    return this.http.get('/api/top/list? idx=1'); }}Copy the code

src/services/index.ts

export * from "./hot.service";
export * from "./list.service";
Copy the code

Response interceptor

In this case, exceptions are handled and error messages are uniformly captured, such as the global message of not logging in. In this case, the Token information is added to the message header when sending a request. The specific changes can be made according to the service requirements.

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(a)export class HttpConfigInterceptor implements HttpInterceptor {
  // constructor(public errorDialogService: ErrorDialogService) { }
  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any> > {let token: string | boolean = false;
    // Compatible with 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 caught by interceptors', data);
        returnthrowError(error); })); }}Copy the code

Interceptor dependency injection

src/app/app.module.ts

Interceptors need to be injected into app.module to take effect

// HTTP interceptor, catch exception, add Token
import { HttpConfigInterceptor } from '.. /interceptor/httpconfig.interceptor'; . providers: [ { provide: HTTP_INTERCEPTORS, useClass: HttpConfigInterceptor, multi:true},... ] .Copy the code

Send a request

The project uses NgRx, so I will send the request this.store.Dispatch (new LoadHotData()) with NgRx. In Effect, I will receive the type hotActionTypes.loadData and send the request through Effect.

Public hotStore$: Observable

public hotStore$: Observable

This completes the request for data

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 observable
  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 the request to get the banner data and the list data
    this.store.dispatch(new LoadHotData());
    // Subscribe to hotStore$to get the changed data
    this.hotStore$.subscribe(data= > {
      this.hotData = data; }); }}Copy the code

Server-side rendering

Angular server rendering can be created using Angular-CLI ng add@nguniversal /express-engine –clientProject Your project name should be the same as the name in package.json

The Angular-music-player project has already been run. Do not run it again

NPM run build: SSR && NPM Run serve: SSR. NPM run build: SSR && NPM Run serve: SSRCopy the code

When you’re done running the package.json scripts, you’ll see 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"
  }
Copy the code

Presents the introduction of hammerjs

Hammerjs needs window objects when they are introduced, which will report errors when the server renders them, but not when they are packaged. NPM Run serve: SSR ReferenceError: Window is not defined when the packaging is complete.

Solution userequireThe introduction of

!!!!! Declare var require: any; Typescript getting Error TS2304: Cannot find name ‘require’ we can use this method for any other plug-in that needs to be injected on the server.

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 }
  }
}
// Inject the HammerJS configuration
providers: [
...
    {
      provide: HAMMER_GESTURE_CONFIG,
      useClass: MyHammerConfig
    }
  ],
...
Copy the code

Modules are loaded on demand

createlist-component

Ng g c list -- Module app or ng generate component -- Module appCopy the code

After running it successfully, you will find that there is an extra folder with four files in it

createmodule

ng generate module list --routing
Copy the code

Two more files (list-routing.module.ts and list.module.ts) are displayed

configurationsrc/app/list/list-routing.module.ts

Import the ListComponent to configure routes

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 { }

Copy the code

configurationsrc/app/list/list.module.ts

To register the ListComponent in an NgModule, use

in the template. Note that when we use ng g c list –module When the app creates a Component, it declares it once in app.module.ts for us. We need to delete it or we’ll 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 { }
Copy the code

configurationsrc/app/list/list.module.ts

This was true before the configuration

After the 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'},];Copy the code

Open your browser and you’ll see an extra list-list-module.js file

At this point the on-demand loading is all done

Why do you need itsrc/app/share.module.tsThis module

What does it say first

SRC /app/share.module.ts specifies a common component, such as
, that you need to import into your desired module

SRC/app/app. The module. Ts SRC/app/list/list module. The ts SRC/app/hot/hot module. Ts, can look up to pull the source code, the mysteries of slowly will find it.

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 { }

Copy the code

Cross domain processing

To be clear, I only configured cross-domain processing for the development environment in the project, not for the production environment. I used the nginx agent. Running NPM start will be successful.

The new filesrc/proxy.conf.json

The IP or web address that target will proxy for

PathRewrite Path rewriting

{
  "/api": {
    "target": "https://music.soscoon.com/api"."secure": false."pathRewrite": {
      "^/api": ""
    },
    "changeOrigin": true}}Copy the code

Request for example

songListDetail(data: any) {
    return this.http.get(`/api/playlist/detail? id=${data.id}`);
}
Copy the code

configurationangular.json

Restart the project and the cross-domain configuration is successful

"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"}}}Copy the code

Here is the end of the first, what suggestions or comments are welcome to mention, after the supplement I add.