專案說明
現在主要是做React開發,也是使用服務端渲染(DEMO),最近想用Angular寫一個專案體驗一下TypeScript大法,對比Angular對比React從開發體驗上來講個人覺得更加方便很多東西不需要你自己去單獨安裝.
線上地址:music.soscoon.com
Github: github.com/Tecode/angu…
目前還在努力開發中,目前完成了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
type到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";
複製程式碼
響應攔截器
這裡處理異常,對錯誤資訊進行統一捕獲,例如未登入全域性提示資訊,在這裡傳送請求時在訊息頭加入Token資訊,具體的需要根據業務來作變更.
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
中會接收到type是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的服務端渲染可以使用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
做的代理.執行npm start
才會成功.
新建檔案src/proxy.conf.json
target
要代理的ip或者是網址
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"
}
}
}
複製程式碼
到這裡先告一段落了,有什麼建議或意見歡迎大家提,之後有補充的我再加上.