项目说明
现在主要是做React开发,也是使用服务端渲染(DEMO),最近想用Angular写一个项目体验一下TypeScript大法,对比Angular对比React从开发体验上来讲个人觉得更加方便很多东西不需要你自己去单独安装.
线上地址:https://music.soscoon.com
Github: https://github.com/Tecode/angular-music-player/tree/QQ-music
目前还在努力开发中,目前完成了80%...
预览图
image image技术栈
- 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
运行成功以后你会发现多了一个文件夹出来,里面还多了四个文件
image
创建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
没有配置之前是这样的
image配置以后
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"
}
}
}
到这里先告一段落了,有什么建议或意见欢迎大家提,之后有补充的我再加上.
网友评论