«Семейное ведро Angular7+NgRx+SSR» для разработки музыки QQ

внешний интерфейс внешний фреймворк
«Семейное ведро Angular7+NgRx+SSR» для разработки музыки QQ

инструкция проекта

Сейчас я в основном занимаюсь React-разработкой, но также использую рендеринг на стороне сервера (DEMO), я недавно хотел использовать Angular, чтобы написать проект, чтобы испытать Дафа TypeScript. По сравнению с Angular и React, я лично считаю, что это более удобно с точки зрения опыта разработки. Многие вещи не нужно устанавливать самостоятельно.

Интернет-адрес:music.soscoon.com

Github: GitHub.com/TE-код/Кишитани…

Он все еще находится в стадии разработки, и в настоящее время завершено 80%...

предварительный просмотр

стек технологий

  • 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

Actions

а такжеVuex,ReduxВсем нужно сначала определить некоторый actionType, вот пример

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',
}

//  获取数据
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) { }
}

сливатьсяActionType

src/store/actions/index.ts

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

Reducers

Хранить данные управления данными в соответствии с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;
  }
}

сливаться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

Обрабатывать асинхронные запросы, подобныеredux-sage redux-thunk, следующий пример — отправить два запроса одновременно, дождаться завершения обоих запросов и отправитьHotActionTypes.LoadSuccessвведите вreducerданные обработки.

Используйте при возникновении ошибкиcatchErrorпоймать ошибки и отправитьnew LoadError()Статус обработанных данных.

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

сливатьсяEffect

поставить несколькоEffectобъединить файлы вместе

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

инъекцияEffect Reducerприбыть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),
    ...
],

Обработка запроса

Используя httpclient.

post get delate putзапросы поддерживаются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) {
  }
  // 轮播图
  topList() {
    return this.http.get('/api/top/list?idx=1');
  }
}

src/services/index.ts

export * from "./hot.service";
export * from "./list.service";

перехватчик ответа

Здесь обрабатываются исключения, а информация об ошибках фиксируется единообразно. Например, глобальная информация о приглашении не регистрируется. Информация о токене добавляется в заголовок сообщения при отправке запроса здесь. Конкретные данные необходимо изменить в зависимости от бизнеса.

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;
    // 兼容服务端渲染
    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('拦截器捕获的错误', data);
        return throwError(error);
      }));
  }
}

Внедрение зависимостей перехватчика

src/app/app.module.ts

Перехватчик должен быть введен вapp.moduleВступит в силу

// http拦截器,捕获异常,加Token
import { HttpConfigInterceptor } from '../interceptor/httpconfig.interceptor';
...
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: HttpConfigInterceptor,
      multi: true
    },
    ...
  ],

Отправить запрос

В проекте используется NgRx, поэтому я использую NgRx для выполнения запросов.this.store.dispatch(new LoadHotData()),существуетEffectполучит типHotActionTypes.LoadData,пройти черезEffectпослать запрос.

настраиватьhotStore$длянаблюдаемый тип, который также меняется при изменении данныхpublic hotStore$: Observable<HotState>, подробности см. в следующем коде:

Это завершает запрос данных

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 {
  // 将hotStore$设置为可观察类型
  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() {
    // 发送请求,获取banner数据以及列表数据
    this.store.dispatch(new LoadHotData());
    // 订阅hotStore$获取改变后的数据
    this.hotStore$.subscribe(data => {
      this.hotData = data;
    });
  }
}

рендеринг на стороне сервера

Угловой рендеринг на стороне сервераможно использоватьangular-cliСоздайтеng add @nguniversal/express-engine --clientProject 你的项目名称И чтобыpackage.jsonвнутриnameТакой же

Проект angular-music-player уже запущен, не запускайте его снова

ng add @nguniversal/express-engine --clientProject angular-music-player

// 打包运行
npm run build:ssr && npm run serve:ssr

После запуска вы увидитеpackage.jsonизscriptsБольше серверной упаковки и запуска команд

  "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 представляет HammerJS

Hammerjs требуется при импортеwindowОбъект сообщит об ошибке при рендеринге на стороне сервера, но не сообщит об ошибке при упаковке и запустится после завершения упаковки.npm run serve:ssrгазетаReferenceError: window is not defined.

Использование обходного путиrequireпредставлять

!! Не забудьте добавитьdeclare var require: any;В противном случае ts возвращает ошибкуtypescript getting error TS2304: cannot find name ' require', Мы можем использовать этот метод для других плагинов, которые необходимо внедрить на стороне сервера.

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 }
  }
}
// 注入hammerjs配置
providers: [
...
    {
      provide: HAMMER_GESTURE_CONFIG,
      useClass: MyHammerConfig
    }
  ],
...

Модули загружаются по запросу

Создайтеlist-component

ng g c list --module app 或 ng generate component --module app

После успешного завершения операции вы обнаружите, что вышла еще одна папка, а в ней еще четыре файла.

Создайтеmodule

ng generate module list --routing

Если операция успешна, будет еще два файлаlist-routing.module.tsа такжеlist.module.ts

настроитьsrc/app/list/list-routing.module.ts

импортListComponentНастроить маршрутизацию

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

настроитьsrc/app/list/list.module.ts

БудуListComponentзарегистрироваться наNgModule, вы можете использовать его в шаблоне<app-list><app-list>, здесь следует отметить, что когда мы используемng g c list --module appСоздайтеcomponentпоможет нам вapp.module.tsВ заявлении один раз нам нужно удалить его, иначе он сообщит об ошибке.

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

настроитьsrc/app/list/list.module.ts

Так было до настройки

после настройки

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' },
];

Откройте браузер и проверьте его, вы увидите еще одинlist-list-module.jsдокумент

Здесь все для загрузки по запросу

зачем нужноsrc/app/share.module.tsэтот модуль

Сначала посмотрите, что написано

src/app/share.module.tsОбъявлены некоторые общедоступные компоненты, такие как<app-scroll></app-scroll>, нам нужно добавить это, когда мы хотимmoduleимпортировать в нужные вам модули

src/app/app.module.ts src/app/list/list.module.ts src/app/hot/hot.module.tsЕсть, вы можете пойти, чтобы проверить исходный код, и постепенно вы найдете тайну.

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

Междоменная обработка

Здесь, чтобы объяснить, я только настроил междоменную обработку среды разработки в проекте, а не производственной среды, я используюnginxсделать прокси.runnpm startбудет успешным.

создать новый файлsrc/proxy.conf.json

targetIP или URL для прокси

pathRewriteпуть переписать

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

Пример запроса

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

настроитьangular.json

Перезапустите проект. Междоменная конфигурация выполнена успешно.

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

Это конец дня.Если у вас есть какие-либо предложения или комментарии, вы можете упомянуть их, и я добавлю их позже, если у вас есть какие-либо дополнения.