如今主要是作React開發,也是使用服務端渲染(DEMO),最近想用Angular寫一個項目體驗一下TypeScript大法,對比Angular對比React從開發體驗上來說我的以爲更加方便不少東西不須要你本身去單獨安裝.html
線上地址:https://music.soscoon.comnode
Github: https://github.com/Tecode/ang...react
目前還在努力開發中,目前完成了80%...webpack
和Vuex
,Redux
同樣都須要先定義一些actionType,這裏舉了一個例子nginx
src/store/actions/list.action.ts
git
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
github
src/store/actions/index.ts
web
export * from './counter.action'; export * from './hot.action'; export * from './list.action'; export * from './control.action';
存儲數據管理數據,根據ActionType
修改狀態typescript
src/store/reducers/list.reducer.ts
express
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, }
處理異步請求,相似於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), ... ],
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" }
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" } } }
到這裏先告一段落了,有什麼建議或意見歡迎你們提,以後有補充的我再加上.