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)
}

 

  

  

 

 

   

  

posted @ 2018-06-14 17:21  Artimis  阅读(308)  评论(0)    收藏  举报