angular4 一个电信项目的实践
初来公司的时候,公司使用ng1做了一个电信相关的项目。
主要是给MVNE中间运营商提供一些服务。包括一些资源(号码/SIM/IMSI)的管理,资费(品牌/服务/产品)的管理,MVNE的管理,便于MVNE更好的售卖服务给MVNO。
解释:
MNO:Mobile Network Operator =>运营商(类似移动/联通/电信)
MVNE:Mobile Virtual Network Enabler =>中间商
MVNO:Mobile Virtual Network Operator =>代理商(类似小米/腾讯)
由于几乎是后台人员去搭建的这个项目,导致出现了以下问题:
1. 项目里没有使用任何前端手段(代码分割/按需加载/ui组件的复用/功能组件的复用);
2. 整个项目目录结构混乱,且Controller/Directive/Service/Factory混用;
3. 插件的二次封装没有统一,多人开发的情况下,各自在各自的controller里面封装,不仅浪费人力物力,而且不统一;
4. css方面是各种插件样式+基础样式,然后后面就是不断地覆盖前面;
5. 整个UI也不统一,每个人负责不同页面,不同的页面下布局都有差异,风格不统一;
而且所有开发人员各自的插件包含的js/css都引入到了index.html(加上controller的js大概光js就有上百个吧),导致整个首页的加载大概是在20S左右,
最后由于UI不统一的缘故,也不知道各自页面的用法,不培训上手,几乎不知道怎么使用。
后来在我的提议下,公司决定使用angular-cli 1.6.3脚手架去重做这个项目,在这个项目里有以下改进:
1. 使用脚手架去管理项目,目录结构更加明朗;
2.减少了插件的使用,加上ng4本身也支持其中一些插件,不需要单独引入;
3.样式方面尽量精简(而且ng4的机制是一个组件下,对应的scss文件下的样式的作用域就只在这个组件下),这样更加方便开发;
4.所有的ui组件(按钮/布局等)和功能组件(tree组件/日期组件/上传组件/table组件等)都做统一封装,可以针对不同需求去使用组件(暴露不同的参数和方法给外面)
5. http请求/鉴权机制(路由里的)/自定义过滤器/缓存快照机制的封装,使得开发效率得到了极大的提高
下面首先介绍搭建过程
1.确认node和angular-cli已全局安装成功

2.新建项目文件夹 => 用webstorm打开文件夹 => 打开Terminal
执行 ng new demo-test
如果新建项目成功,Terminal会显示:

同时会生成以下目录结构:

其中:
node_modules目录:存放项目所需的依赖包。
src目录:项目的关键代码都在这里
angular-cli.json:项目的参数配置 (项目名称/项目入口文件/源文件目录/编译后目录)
package.json:展示项目所需的依赖包,可以手动指定依赖包的版本(dependencies和devDependencies)
3.进入项目 => 启动项目
执行 ng serve -open

至此项目已顺利启动。
下面介绍技术点
1.router 路由
路由是angular的核心,允许我们通过控制不同的路由,获取不同的数据,从而渲染不同的页面。
在项目中部署路由需要实现以下步骤:
1.在AppModule中导入RouterModule
2.根路由模块中使用 RouterModule.forRoot(ROUTES对象)定义路由,子路由模块中使用 RouterModule.forChild(ROUTES对象)定义路由
3.配置 ROUTES 对象时,常用参数有path/component/loadChildren/canActivate
path:定义路由的匹配路径
component:定义路由时需要加载的组件
loadChildren:告诉路由从另一个模块中获取子路由
canActive:认证身份,路由守卫
4.使用router-outlet指令告诉angular在哪加载组件
ng匹配到指定路径,就会去加载指定的组件,并插入到router-outlet的位置
//app-routing.module.ts import {NgModule} from '@angular/core'; import {Routes,RouterModule} from '@angular/router' import {AuthRouteService} from "./shared/core/auth/auth-route.service"; import {NotFoundComponent} from './not-found/not-found.component'; const routes:Routes=[ {path:'',loadChildren:'./home/home.module#HomeModule',canActive:[AuthRouteService]}, {path: 'login', loadChildren: './login/login.module#LoginModule'}, {path: 'notfound', component: NotFoundComponent}, {path: '**', redirectTo: 'notfound'} ] @NgModule({ imports:[RouterModule.forRoot(routes,{useHash:true})], exports:[RouterModule], provides:[] }) export class AppRoutingModule { }
//auth-route.service.ts import {Injectable} from '@angular/core'; import {CanActivate,Route} from '@angular/router'; import {Observable} from 'rxjs/Rx'; import {SessionManager} from './session-manager.service' @Injectable() export class AuthRouteService implements CanActive{ constructor(private router:Router,pricate sessionManager:SessionManager){} canActivate():Observable<boolean> | boolean{ if(this.sessionManager.getSession()){ //如果能取到token,才可以跳转 return true; } this.router.navigate(['/login']); //如果取不到token,跳转到登录页 return false; } }
2.缓存快照机制
其实就是RouteReuseStrategy => 路由复用策略
提供了五个通俗易懂的方法 shouldDetach/store/shouldAttach/retrieve/shouldReuseRoute
具体实现就是:
当把A路由设置为允许复用(shouldDetach),就会把A路由的快照存起来(store),
当shouldReuseRoute成立时,表示需要复用路由,再次遇到A路由时,先判断是否允许还原(shouldAttach),最后拿到路由快照并构建组件(retrieve)
在项目中部署RouteReuseStrategy需要实现以下步骤:
1.创建策略
定义一个对象storeRoutes用于缓存路由快照
shouldDetach:直接返回true表示对所有路由都允许复用
store:当路由离开时会触发,按照path作为key存储路由快照(path等同于RouterModule.forRoot中的配置)
shouldAttach:若path的路由快照在storeRoutes对象中,都认为允许还原路由
retrieve:从storeRoutes对象中获取快照,若无则返回null
shouldReuseRoute:进入路由触发,判断是否同一路由
2.将策略注册到根模块AppModule中
providers:[
{ provide : RouteReuseStrategy , useClass : ExtensionRouteReuseStrategy }
]
//app.module.ts import {ExtensionRouteReuseStrategy} from './shared/core/route/extension-route-reuse-strategy'; //...
@NgModule({ //... providers:[ {provide: RouteReuseStrategy,useClass: ExtensionRouteReuseStrategy} ] })
3.自定义组件封装
自定义组件的封装大概分为两类:
第一种是不管外界是什么样,我就是这个样
第二种是外界传给我让我什么样,我就变成什么样
在第二种里面需要注意的点:
给机会外面传参数进来去控制显示样式,
同时里面也会在某些操作里面传事件出去,你去自定义要在这些事件里去干些什么
1.点击+号一次会增加一个input,点击-号会减少一个input ,属于第一种

2. 点击小三角会显示价格单位,点击小三角出来的内容不定,属于第一种

3. 点击展开所有节点,可以单选可以多选可以不选,属于第二种
图1:多选,点击父节点会勾选子节点
<nw-treeview #catalogTreeComp formArrayName="catalogCodeList" [url]="catalogTreeUrl" [httpMethod] = "httpMethod"> </nw-treeview>
图2:单选,只能选择子节点

<nw-treeview #channelComponent formControlName="channelId" [url]="channelUrl" httpMethod="GET" singleSelect="true" [onlyLeafSelectable]="true" > </nw-treeview>
图3:不能选择,只能展示

<nw-treeview #catalogTreeviewComponent [url]="catalogTreeUrl" [httpMethod]="treeHttpMethod" [singleSelect]="true" dropdown="false" showCheckbox="false" (whenSelected)="catalogChanged($event)" (whenUnSelected)="unSelectedCatalog()"> </nw-treeview>
以下是nw-treeview(树控件)的部分代码:
//nw-treeview.component.ts export class NwTreeviewComponent implements OnInit{ @Input() dropdown:boolean=true; //默认下拉 @Input() singleSelect:boolean=false; //默认多选 @Input() showCheckbox:any=true; //默认显示多选框 @Input() showRoot:boolean=true; //默认选中根节点 @Input() onlyLeafSelectable:boolean=false; //默认父子节点都可选 @Output() whenSelected=new EventEmitter<any>(); //当节点被选中 @Output() whenUnSelected=new EventEmitter<any>(); //当节点被取消选择 @OutPut() whenBuildTreeCompleted=new EventEmitter<any>(); //当节点树加载完毕 ngOnInit() { this.createTree(); } private createTree(){ this.whenBuildTreeCompleted.emit(node); } onNodeSelected(event,node){ this.whenSelected.emit(node); } onNodeUnselected(event,node){ this.whenUnSelected.emit(node); } }
4.自定义过滤器
必须满足:
1 实现PipeTransform接口的transform方法
2 在使用的模块里 imports 对应过滤器模块
//nw-json.pipe.ts import {Pipe,PipeTransform} from '@angular/core'; @Pipe({ name:'nwJson' }) export class NwJsonPipe implements PipeTransform { transform(value:any,args?:any):any{ let reg = /^\{.*\}$/; if(reg.test(value)){ return JSON.parse(value); } return value; } }
5.表单构建
项目里使用的是响应式表单,也就是FormGroup+FormBuilder+FormValidation这一套在用。
formGroup:用于跟踪一组实例的值和验证状态
FormBuilder:简化formGroup构建对象的过程
FormValidation:统一管理验证错误
//xx.component.html <form [formGroup]="thatForm" novalidate> <nw-input formControlName="aa" [hasErrors]="formGuard.msgs.catalog.errors"> </form>
//xx.cpmponent.ts import {Component, OnInit, ViewChild} from '@angular/core'; import {FormBuilder, FormGroup, Validators} from "@angular/forms"; import {xxFormValidation} from "../xx-form-validation"; @Component({ selector: 'xx', templateUrl: './xx.component.html', styleUrls: ['./xx.component.scss'] }) export class xxComponent implements OnInit { thatForm: FormGroup; formGuard: xxFormValidation = new xxFormValidation(); constructor(private formBuilder: FormBuilder){ } ngOnInit() { this.thatForm = this.formBuilder.group({ aa:['',Validators.required] }) }
//xx-form-validation.ts import {FormValidation} from "../../../shared"; export class xxFormValidation extends FormValidation { constructor() { super(); } msgs = { aa:{ errors: '', messages: { required: '请输入aa.' } } }
6.HTTP服务封装
分为get请求和post请求
1. get请求一般是直接请求数据而不是提交数据,所以一般不需要确认弹窗
@Injectable() export class HttpService{ private headers=new Headers({'Content-Type':'application/json;charset=UTF-8'}); constructor(private http:Http,private nwswal:NWSWAL,private httpMonitorNotify:HttpMonitorNotifyService){ } private httpGet(url:string, search:any=null, showLoading:boolean=false, showSuccess:boolean=false, showSuccessHandler?:Function, showError:boolean=false, showErrorHandler?:function){ this.doHttpRequest(url,new RequestOptions({ method:RequestMethod.Get, search:search, headers:this.headers }),showLoading).then(res=>this.processThen(res,showSuccess,showSuccessHandler,showError,showErrorHandler)) }
2. post请求一般是提交数据,所以一般需要确认弹窗
@Injectable() export class HttpService{ private headers=new Headers({'Content-Type':'application/json;charset=UTF-8'}); constructor(private http:Http,private nwswal:NWSWAL){ } private httpPost(url: string, body: any = null, showLoading: boolean = false, showSuccess: boolean = false, showSuccessHandler?: Function, showError: boolean = false, showErrorHandler?: Function, showConfirm: boolean = false) { if (showConfirm) { this.nwswal.showConfirm(() => { this.doHttpRequest(url, new RequestOptions({ method: RequestMethod.Get, body: body, headers: this.headers }), showLoading).then(res => this.processThen(res, showSuccess, showSuccessHandler, showError, showErrorHandler)) }) }else{ this.doHttpRequest(url, new RequestOptions({ method: RequestMethod.Get, body: body, headers: this.headers }), showLoading).then(res => this.processThen(res, showSuccess, showSuccessHandler, showError, showErrorHandler)) } }
3. 公共代码
public doHttpRequest(url:string, options:RequestOptionsArgs,showLoading:boolean=false):Promise<any>{ this.refreshToken(); if(showLoading){ this.nwswal.showLoading()} return this.http.request(url,options) .toPromise() .then(response=>{ if(showLoading){this.nwswal.close()} //请求成功就关闭loading return response; }) .catch(e=>{this.handleError(e)}) } public processThen(res,showSuccess?:boolean,showSuccessHandler?:Function,showError?:boolean,showErrorHandler?:Function){ if(res && res.retCode && res.retCode==='000'){ if(showSunccess && showSuccessHandler){this.nwswal.showSuccess(showSuccessHandler)} //请求成功如果有成功并有成功执行函数,则弹出成功弹出并执行后续成功函数, else if(!showSuccess && showSuccessHandler){showSuccessHandler(res)} }else{ if(showError && showErrorHandler){this.nwswal.showError(res.retMesg,showErrorHandler)} } } private refreshToken(){ this.headers.delete('Authorization'); this.headers.append('Authorization',this.getToken()); } private handleError():Promise<any>{ this.httpMonitorNotify.notify(error.status); //去监听返回的错误状态,比如如果是0:提示重连弹窗,1:提示重新登录弹窗,2:显示错误弹窗 return Promise.reject(error.message || error) }

浙公网安备 33010602011771号