項目說明
現(xiàn)在主要是做React開發(fā),也是使用服務(wù)端渲染(DEMO),最近想用Angular寫一個項目體驗一下TypeScript大法,對比Angular對比React從開發(fā)體驗上來講個人覺得更加方便很多東西不需要你自己去單獨安裝.
線上地址:https://music.soscoon.com
Github: https://github.com/Tecode/angular-music-player/tree/QQ-music
目前還在努力開發(fā)中,目前完成了80%...
預(yù)覽圖
技術(shù)棧
- 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',
}
// 獲取數(shù)據(jù)
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
存儲數(shù)據(jù)管理數(shù)據(jù),根據(jù)ActionType
修改狀態(tài)
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
,下面這個例子是同時發(fā)送兩個請求,等到兩個請求都完成后派遣HotActionTypes.LoadSuccess
type到reducer
中處理數(shù)據(jù).
當(dāng)出現(xiàn)錯誤時使用catchError
捕獲錯誤,并且派遣new LoadError()
處理數(shù)據(jù)的狀態(tài).
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詳細(xì)說明
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";
響應(yīng)攔截器
這里處理異常,對錯誤信息進(jìn)行統(tǒng)一捕獲,例如未登錄全局提示信息,在這里發(fā)送請求時在消息頭加入Token信息,具體的需要根據(jù)業(yè)務(wù)來作變更.
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;
// 兼容服務(wù)端渲染
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
},
...
],
發(fā)送一個請求
項目使用了NgRx,所以我就用NgRx發(fā)請求this.store.dispatch(new LoadHotData())
,在Effect
中會接收到type是HotActionTypes.LoadData
,通過Effect
發(fā)送請求.
設(shè)置hotStore$
為可觀察類型,當(dāng)數(shù)據(jù)改變時也會發(fā)生變化public hotStore$: Observable<HotState>
,詳細(xì)見以下代碼:
到此就完成了數(shù)據(jù)的請求
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$設(shè)置為可觀察類型
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() {
// 發(fā)送請求,獲取banner數(shù)據(jù)以及列表數(shù)據(jù)
this.store.dispatch(new LoadHotData());
// 訂閱hotStore$獲取改變后的數(shù)據(jù)
this.hotStore$.subscribe(data => {
this.hotData = data;
});
}
}
服務(wù)端渲染
Angular的服務(wù)端渲染可以使用angular-cli
創(chuàng)建ng add @nguniversal/express-engine --clientProject 你的項目名稱
要和package.json
里面的name
一樣
angular-music-player項目已經(jīng)運行過了不要再運行
ng add @nguniversal/express-engine --clientProject angular-music-player
// 打包運行
npm run build:ssr && npm run serve:ssr
運行完了以后你會看見package.json
的scripts
多了一些服務(wù)端的打包和運行命令
"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
對象,在服務(wù)端渲染時會報錯,打包的時候不會報錯,打包完成以后運行npm run serve:ssr
報ReferenceError: window is not defined
.
解決方法使用require
引入
!!記得加上declare var require: any;
不然ts回報錯typescript getting error TS2304: cannot find name ' require'
,對于其它的插件需要在服務(wù)端注入我們都可以使用這樣的方法.
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
}
],
...
模塊按需加載
創(chuàng)建list-component
ng g c list --module app 或 ng generate component --module app
運行成功以后你會發(fā)現(xiàn)多了一個文件夾出來,里面還多了四個文件
創(chuàng)建module
ng generate module list --routing
運行成功會多出兩個文件list-routing.module.ts
和list.module.ts
配置src/app/list/list-routing.module.ts
導(dǎo)入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
中,在模板內(nèi)就可以使用<app-list><app-list>
,在這里要注意一下,當(dāng)我們使用ng g c list --module app
創(chuàng)建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
的文件
到這里按需加載就已經(jīng)都結(jié)束
為什么需要src/app/share.module.ts
這個模塊
先看看寫的什么
src/app/share.module.ts
聲明了一些公共的組件,例如<app-scroll></app-scroll>
,我們要時候的時候需要將這個module
導(dǎo)入到你需要的模塊中
src/app/app.module.ts
src/app/list/list.module.ts
src/app/hot/hot.module.ts
都有,可以去拉取源碼查看,慢慢的會發(fā)現(xiàn)其中的奧秘.
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 { }
跨域處理
這里要說明一下,我在項目中只配置了開發(fā)環(huán)境的跨域處理,生產(chǎn)環(huán)境沒有,我使用的是nginx
做的代理.運行npm start
才會成功.
新建文件src/proxy.conf.json
target
要代理的ip或者是網(wǎng)址
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"
}
}
}
到這里先告一段落了,有什么建議或意見歡迎大家提,之后有補充的我再加上.