Git Product home page Git Product logo

ck110.github.io's People

Contributors

ck110 avatar

Watchers

 avatar  avatar

ck110.github.io's Issues

Angular何时需要Unsubscribe

Unsubscribe

使用rxjs的时候,您通常需要在某个点取消订阅才能释放系统中的内存。否则,你将有内存泄漏,让我们看看在ngOnDestroy生命周期钩子中需要退订的最常见情况。

需要unsubscribe的常见情况

Forms

export class TestComponent {

  ngOnInit() {
    this.form = new FormGroup({...});
    this.valueChanges  = this.form.valueChanges.subscribe(console.log);
    this.statusChanges = this.form.statusChanges.subscribe(console.log);
  }

  ngOnDestroy() {
    this.valueChanges.unsubscribe();
    this.statusChanges.unsubscribe();
  }

}

form control中同理

The Router

export class TestComponent {
  constructor(private route: ActivatedRoute, private router: Router) { }

  ngOnInit() {
    this.route.params.subscribe(console.log);
    this.route.queryParams.subscribe(console.log);
    this.route.fragment.subscribe(console.log);
    this.route.data.subscribe(console.log);
    this.route.url.subscribe(console.log);
    
    this.router.events.subscribe(console.log);
  }

  ngOnDestroy() {
    // You should unsubscribe from each observable here
  }

}

根据官方文档,Angular应该自动unsubscribe,但这里面有个bug

Renderer Service

export class TestComponent {
constructor(private renderer: Renderer2, 
            private element : ElementRef) { }

  ngOnInit() {
    this.click = this.renderer.listen(this.element.nativeElement, "click", handler);
  }

  ngOnDestroy() {
    this.click.unsubscribe();
  }

}

Infinite Observables

export class TestComponent {

  constructor(private element : ElementRef) { }

  interval: Subscription;
  click: Subscription;

  ngOnInit() {
    this.interval = Observable.interval(1000).subscribe(console.log);
    this.click = Observable.fromEvent(this.element.nativeElement, 'click').subscribe(console.log);
  }

  ngOnDestroy() {
    this.interval.unsubscribe();
    this.click.unsubscribe();
  }

}

Redux Store

export class TestComponent {

  constructor(private store: Store) { }

  todos: Subscription;

  ngOnInit() {
     this.todos = this.store.select('todos').subscribe(console.log);  
  }

  ngOnDestroy() {
    this.todos.unsubscribe();
  }

}

不需要 Unsubscribe 的情况

Async pipe

@Component({
  selector: 'test',
  template: `<todos [todos]="todos$ | async"></todos>`
})
export class TestComponent {

  constructor(private store: Store) { }

  ngOnInit() {
     this.todos$ = this.store.select('todos');
  }

}

当组件被销毁时,async管道自动取消订阅,以避免潜在的内存泄漏。

@HostListener

export class TestDirective {

  @HostListener('click')
  onClick() {
    ....
  }
}

Finite Observable

当你有一个有限的序列,通常你不需要unsubscribe,例如当使用HTTPservice或timer observable。

export class TestComponent {

  constructor(private http: Http) { }

  ngOnInit() {
    Observable.timer(1000).subscribe(console.log);
    this.http.get('http://api.com').subscribe(console.log);
  }
}

小建议

不要过多的调用unsubscribe方法,RxJS: Don’t Unsubscribe

takeUntil
它发出源 Observable 的值,然后直到第二个 Observable (即 notifier )发出项,它便完成。

export class TestComponent {

  constructor(private store: Store) { }

  private componetDestroyed: Subject = new Subject();
  todos: Subscription;
  posts: Subscription;

  ngOnInit() {
     this.todos = this.store.select('todos').takeUntil(this.componetDestroyed).subscribe(console.log); 

     this.todos = this.store.select('posts').takeUntil(this.componetDestroyed).subscribe(console.log); 
  }

  ngOnDestroy() {
    this.componetDestroyed.next(); // componetDestroyed 发出值后,todos,todos会completed
    this.componetDestroyed.unsubscribe();
  }

}

When to Unsubscribe in Angular

理解Angular的Reactive Form

官方文档并没有说明Template-driven FormReactive Form 哪一个更好。由于之前开发过一个Ionic2项目,使用的是Template-driven Form,光是校验就有一坨代码,维护与开发简直惨不忍睹,所以个人更加推荐使用Reactive Form

基本概念

使用Reactive Form(同步),我们会在代码中创建整个表单 form control 树。我们可以立即更新一个值或者深入到表单中的任意节点,因为所有的 Form control 都始终是可用的。而且因为是同步,有利于单元测试。

Template-driven Form(异步)中,我们是通过指令来创建 form control 的。我们在操作一个Form control之前,必须要经历一个变化检测周期。

FormControl、FormGroup、FormArray

FormControl是最小单位(C),FormGroup类似于一个由FormControl(C)组件的object对象(G),FormArray(A)是一个由FormGroup(G)的Array数组。它们之间可以互相嵌套,以应对各式各样的表单模型(Form Model)。

  addForm: FormGroup;

  constructor(public formBuilder: FormBuilder) {
    this.orderForm = this.formBuilder.group({
      name: ['', [Validators.required]],
      description: ['', [Validators.required]],
      other: this.formBuilder.group({
        name: ['', [Validators.required]],
        description: ['', [Validators.required]]
      }),
      items: this.formBuilder.array([
        this.formBuilder.group({
          name: ['', [Validators.required]],
          description: ['', [Validators.required]],
        }),
        this.formBuilder.group({
          name: ['', [Validators.required]],
          description: ['', [Validators.required]],
        }),
        this.formBuilder.group({
          name: ['', [Validators.required]],
          description: ['', [Validators.required]],
        })
      ])
    });
  }
  

通过this.addForm.value获取的值:

{
  name:'',
  description:'',
  other: {
    name:'',
    description:'',
  },
  items: [
    {
      name:'',
      description:'',
    },
    {
      name:'',
      description:'',
    },
    {
      name:'',
      description:'',
    }
  ]
}

它们三者之间的关系如下:

formGroup = 
{
  formControlName:formControl,
  formControlName:formControl,
  formControlName:formControl,
}

formArray = [
  formGroup,
  formGroup,
]= [
    {
      formControlName:formControl,
      formControlName:formControl,
      formControlName:formControl,
    },
    {
      formControlName:formControl,
      formControlName:formControl,
      formControlName:formControl,
    }
]

对于使用Reactive Form时,动态增加formControl也是很方便的。这种在,比如添加出差明细等情况下很适合。
代码示例参考

data model与form model

来自服务器就是数据模型(data model),而FormControl的结构就是表单模型(form model)。

组件必须把数据模型中的英雄值复制到表单模型中。这里隐含着两个非常重要的点。

  • 开发人员必须理解数据模型是如何映射到表单模型中的属性的。
  • 用户修改时的数据流是从DOM元素流向表单模型的,而不是数据模型。表单控件永远不会修改数据模型。

个人经验:

  1. 按照如此的划分,从来可以不依赖后端的数据结构(毕竟后端的数据格式是千奇百怪的)。
  2. 表单模型最好和要提交的数据格式一样,数据的修改都是操作表单模型的 formControl。提交的时候不需要手动组装数据。
  3. 由于之前的项目使用的是Template-driven Form,需要手动组装提交的数据,而且并没有严格区分数据模型与表单模型,后期维护时,代码很乱。
  4. 尽量使用类型系统,不要图方便使用any,不然维护的时候,这酸爽!!!

setValue 与 patchValue

  • setValue: 使用的时候需要每个from control都要设置值。否则,ERROR Error: Must supply a value for form control with name: 'xxxxx'
  • patchValue: 类似打补丁,不需要每个from control都要设置值。

Ionic4 Is Coming

想标题真是一件很麻烦的事,所以就借用<权利的游戏>的经典台词:Winter Is Coming。最近看了看ionic4的相关文档,初步总结一下。

Ionic Components

Ionic4 使用stencil来构建webComponet,对于webComponet而言,是没有双向绑定这些的,所以为了适配Angular,在这些webComponet上面做了一层封装。Angular用户而言只用使用@ionic/angular就可以了。

组件的API层面变化挺大,具体可看BREAKING。有大量的属性重命名,感觉以后项目升级改动不小,至少<ion-nav>的去除,就要修改很多页面。

路由

ionic4中推荐使用Angular Router,并且实现了自己的`。

对于lazylaod而言,@IonicPage的方式将会废弃。

不过这种方式固有的缺陷就是状态保存,对于移动App而言,返回上一页除了需要保存数据之外,页面的滚动状态也是需要保存的。

ionic3中基于stack的方式,本质上是z-index的增加。页面的dom结构并不会删除,所以返回上一页时和之前完全一样。
Angular的路由是会删除dom的,最多也只是数据的保存。不知道官方说推荐使用Angular Router是处于什么原因,看来一下ionic4的源码,也没有发现什么特别的处理。

不过,ionic4还是支持stack的方式的。

Ionic Native cordova capacitor

Ionic Native 会支持cordova capacitor。

Ionic Native 与 cordova plugin 版本之间对应的管理。比如Ionic Native的keyboard的哪些版本对于cordva plugin的哪些版本,个人感觉其实挺混乱的,需要自己去辨别吧。

capacitor虽说可以使用cordova,但是不支持参数,需要自己在代码中去写。关注点就不仅仅是web端了,需要hook去修改生成的原生项目,或者原生源码加入版本管理。

还是很希望capacitor能解决一些原生的问题的,比如keyboard的一些问题。

Ionic Cli VS Angular Cli

个人推荐使用 Angular cli。目前做的项目大概有300个 component。之前--prod打包经常内存溢出。后来换了一台16G内存的电脑,打包时间大概有40分钟,主要是webpack编译阶段很慢。

ionic cli的功能基本等于 angular cli + cordova cli + ionic pro(收费) 的封装,对于我们而言,完全没有必要使用ionic cli。直接使用 angular cli + cordova cli,在Angular cli生成的项目中新建cordova文件夹,里面初始化一个cordova项目,只需要把Angular生成的项目Copy进去,写一些脚本自动化就可以了。

默认ionic cli 生成的项目,css处理,使用selector来隔离样式的,并且是单独打包的。既然使用了angular,我们完全可以使用angular的style encapsulation

使用Angular cli的一个好处就是可以直接使用environments。而Ionic CLI 则要做很多,具体可看ionic3 中使用 environments

之前测试打包问题。发现在ionic cli上--prod能通过的,在Angular Cli上却不能,个人还是相信Angular官方推荐的工具吧

总之web的东西就交给Angular,我们只是使用ionic提供的组件库。

运行官方源码的Angular Demo

git clone https://github.com/ionic-team/ionic.git

// 生成 @ionic/angular
cd angular
npm install
npm run build

// 运行demo
cd test/nav
npm install
npm run copy-ionic-angular
npm run serve

资源链接

Ionic4
capacitor
stencil

Angular中的Constructor和ngOnInit的本质区别

这是在stackoverflow讨论比较多的问题。大多数答案都集中在这两者的用法之间的区别,本文在组件初始化过程进行一个更全面的比较。

Difference related to JS/TS language

让我们从与语言本身有关的最明显的区别开始。ngOnInit只是一个类的方法,在结构上与类的其他方法没有区别。只是Angular团队决定以这种方式来命名,但也可以是其他任何名称:

class MyComponent {
  ngOnInit() { }
  otherNameForNgOnInit() { }
}

是否在组件类中实现该方法,这完全取决于您。编译期间,Angular编译器检查组件是否实现了此方法,并用适当的标志标记该类:

export const enum NodeFlags {
  ...
  OnInit = 1 << 16,

然后使用此标志来决定是否在change detection期间调用组件类实例上的方法:

if (def.flags & NodeFlags.OnInit && ...) {
  componentClassInstance.ngOnInit();
}

而构造函数又是另一回事。无论你是否在TypeScript类中实现它,在创建一个类的实例时就会被调用。这是因为一个typecript类的构造函数被转换成一个JavaScript构造函数

class MyComponent {
  constructor() {
    console.log('Hello');
  }
}

转译为:

function MyComponent() {
  console.log('Hello');
}

要创建一个类实例,使用new运算符调用该函数:

const componentInstance = new MyComponent(

所以,如果你省略了一个类的构造函数,它就被转换成一个空的函数:

class MyComponent { }

转换成空的函数

function MyComponent() {}

这就是为什么我说constructor总是被执行,不管你是否在类上实现一个构造函数。

Difference related to the component initialization process

组件初始化阶段来看,两者之间存在巨大的差异。Angular引导程序(bootstrap)由两个主要阶段组成:

  • constructing components tree
  • running change detection

Angular构造组件树时调用该组件的构造函数。包括ngOnInit的所有生命周期钩子都作为接下来的change detection阶段的一部分进行调用。通常,组件初始化逻辑需要一些DI providers或可用的input bindingsrendered DOM。这些都可以在Angular Bootstrap过程的不同阶段使用。

当Angular构造一个组件树时,根模块注入器已经被配置,所以你可以注入任何全局依赖。另外,当Angular实例化一个子组件类时,父组件的注入器也已经被设置好了,所以你可以注入在父组件上定义的providers,包括父组件本身也可以被注入。组件构造函数是在上下文中注入器调用的唯一方法,所以构造函数是获取依赖注入的唯一方法。@Input通信机制作为接下来的change detection 阶段的一部分进行处理,因此输入绑定在构造函数中不可用。

当Angular开始 change detection时,组件树已经被构造并且组件树中所有组件的构造函数也已经被执行。同时,每个组件的template nodes(模板节点)都被添加到DOM中。现在,您可以使用初始化组件所需的所有数据 - DI providers, DOM , input bindings。

change detection的具体细节,请阅读Everything you need to know about change detection in AngularThe mechanics of property bindings update in Angular

我们用一个简单的例子来演示这些阶段。假设您有以下模板:

<my-app>
   <child-comp [i]='prop'>

Angular开始引导应用程序。如上所述,它首先为每个组件创建类。所以它调用MyAppComponent构造函数。当执行组件构造函数时,Angular解析了注入到MyAppComponent构造函数中的所有依赖项,并将它们作为参数提供。它还创建了一个DOM节点,它是my-app组件的host element。然后继续为child-comp创建一个host element并调用ChildComponent构造函数。在这个阶段,Angular不关心我的输入绑定i和任何生命周期钩子。所以当这个过程完成后,Angular以下面的组件视图树结束:

MyAppView
  - MyApp component instance
  - my-app host element data
       ChildComponentView
         - ChildComponent component instance
         - child-comp host element data

只有这样,Angular运行才会运行change detection并更新my-app的输入绑定,并调用MyAppComponent实例上的ngOnInit。然后继续更新child-comp的绑定,并调用ChildComponent类的ngOnInit。

关于view的知识,请阅读Here is why you will not find components inside Angular

Difference related to the usage

现在让我们看看从使用上的差异。

Constructor

构造函数主要用于注入依赖关系。Angular把这个构造器注入模式称为DI。详细请看Constructor Injection vs. Setter Injection

但是,构造函数的使用不限于DI。例如,@angular/router模块的router-outlet指令使用它在路由器生态系统中注册自身及其location(viewContainerRef)。详细请看Here is how to get ViewContainerRef before @ViewChild query is evaluated.

然而,通常的做法是构造函数中写尽可能少的逻辑。

NgOnInit

正如我们在上面学习Angular调用ngOnInit时已经完成创建一个组件DOM,通过构造函数注入了所有需要的依赖关系并处理了输入绑定。所以在这里你可以获得所有必需的信息,这使得它成为执行初始化逻辑的好地方。

即使这个逻辑不依赖于DI,DOM或输入绑定,使用ngOnInit来执行初始化逻辑也是常见的做法。

原文链接
相关链接

Angular的19个装饰器基本用法

image

@Attribute

获取宿主元素的对应的属性值。

@Directive({
  selector: '[test]'
})
export class TestDirective {
  constructor(@Attribute('type') type ) {
    console.log(type); // text
  }
}
  
@Component({
  selector: 'my-app',
  template: `
    <input type="text" test>
  `,
})
export class App {}

@component

声明组件。

@Component({
  selector: 'greet', 
  template: 'Hello {{name}}!'
})
class Greet {
  name: string = 'World';
}

@ContentChild

@ContentChildren类似,只返回第一个符合的view DOM

@Component({
  selector: 'tabs',
  template: `
    <ng-content></ng-content>
  `,
})
export class TabsComponent {
 @ContentChild("divElement") div: any;
 
 ngAfterContentInit() {
   console.log(this.div);
 }
}

@Component({
  selector: 'my-app',
  template: `
    <tabs>
     <div #divElement>Tada!</div>
    </tabs>
  `,
})
export class App {}

@ContentChildren

@ContentChild类似,获取ng-content中符合的view DOM,只有ngAfterContentInit后才会有view DOMQueryList才会初始化

@Component({
  selector: 'tab',
  template: `
    <p>{{title}}</p>
  `,
})
export class TabComponent {
  @Input() title;
}

@Component({
  selector: 'tabs',
  template: `
    <ng-content></ng-content>
  `,
})
export class TabsComponent {
 @ContentChildren(TabComponent) tabs: QueryList<TabComponent>
 
 ngAfterContentInit() {
   this.tabs.forEach(tabInstance => console.log(tabInstance))
 }
}

@Component({
  selector: 'my-app',
  template: `
    <tabs>
     <tab title="One"></tab>
     <tab title="Two"></tab>
    </tabs>
  `,
})
export class App {}

@directive

声明指令。

@Directive({
  selector: '[my-button]',
  host: {
    '[class.valid]': 'valid', // 动态属性绑定
    'role': 'button', // 静态属性绑定
    '(click)': 'onClick($event.target)' // 事件监听
  }
})
class NgModelStatus {
  constructor(public control:NgModel) {}
  get valid { return this.control.valid; }
  get invalid { return this.control.invalid; }
}

@host

从当前先上获取符合的父view DOM,知道最顶层的宿主元素。

@Component({
  selector: 'cmp',
  template: `
    cmp
  `,
})
export class DIComponent {}

@Directive({
  selector: "[host-di]"
})
export class HostDI {
 constructor(@Host() cmp: DIComponent) {
   console.log(cmp);
 }
}

@Component({
  selector: 'my-app',
  template: `
    <cmp host-di></cmp>
  `,
})
export class App {}

@HostBinding

设置宿主元素的属性绑定。

@Directive({
  selector: '[host-binding]'
})
export class HostBindingDirective {
  @HostBinding("class.tooltip1") tooltip = true; // 设置 "tooltip1" 样式类
  
  @HostBinding("class.tooltip2") // 设置 "tooltip2" 样式类
  get tooltipAsGetter() {
    // your logic
    return true;
  };
   
  @HostBinding() type = "text"; // 直接设置 type="text"
}

@Component({
  selector: 'my-app',
  template: `
    <input type="text" host-binding> // 在这个宿主元素上增加 "tooltip" 样式类
  `,
})
export class App {}

@HostListener

宿主元素的事件监听。

@Directive({
  selector: '[count]'
})
export class HostListenerDirective {
  numClicks = 0;
  numClicksWindow = 0;
  @HostListener("click", ["$event"]) // 当前宿主元素,即input
  onClick(event) {
    console.log(this.numClicks++);
  }
  
  @HostListener("window:click", ["$event"])  //还可以支持 window,document,body 上的事件
  onClick(event) {
    console.log("Num clicks on the window:", this.numClicksWindow++);
  }
}

@Component({
  selector: 'my-app',
  template: `
    <input type="button" count value="+">
  `,
})
export class App {}

@Inject

指明依赖。

@Component({
  selector: 'cmp',
  template: `
    cmp
  `
})
export class DIComponent {
  constructor(@Inject(Dependency) public dependency) {}
}

@Injectable

声明类可以被DI使用。

@Injectable()
export class WidgetService {
  constructor(
    public authService: AuthService) { }
}

@input

定义组件的属性。

@Component({
  selector: 'my-button',
  template: `
    <button (click)="click($event)">{{name}}</button>
  `
})
export class DI2Component {

  @Input() name ;

  @Output() myClick: EventEmitter<any> = new EventEmitter();

  click() {
    this.myClick.emit('click');
  }
}

@NgModule

定义module。

@NgModule({
  imports: [ CommonModule ],
  declarations: [
    DIComponent,
    HostBindingDirective,
    HostDI
  ],
  providers: [{ provide: Dependency, useClass: ParentDependency}],
  exports: [
    DIComponent,
    HostBindingDirective,
    HostDI
  ]
})
export class DemoModule {}

@optional

依赖可选

class OptionalDependency {}

@Component({
  selector: 'cmp',
  template: `
    cmp
  `,
})
export class DIComponent {
  constructor(@Optional() public dependency: OptionalDependency) {}
}

@output

定义组件的事件。

@Component({
  selector: 'my-button',
  template: `
    <button (click)="click($event)">{{name}}</button>
  `
})
export class DI2Component {

  @Input() name ;

  @Output() myClick: EventEmitter<any> = new EventEmitter();

  click() {
    this.myClick.emit('click');
  }
}

@pipe

定义管道。

@Pipe({
  name: 'isNull'
})
export class IsNullPipe implements PipeTransform {
  
  transform (value: any): boolean {
    
    return isNull(value);
  }
}

@self

只使用自身的providers的定义,不通过inject tree查找依赖。

class Dependency {}

class ChildDependency {
 constructor() {
   console.log("ChildDependency");
 }
}

class ParentDependency {
 constructor() {
   console.log("ParentDependency");
 }
}

@Component({
  selector: 'cmp',
  template: `
    cmp
  `,
  providers: [{ provide: Dependency, useClass: ChildDependency }]
})
export class DIComponent {
  constructor(@Self() public dependency: Dependency) {} // 注入的为 ChildDependency
}

@Component({
  selector: 'my-app',
  template: `
    <cmp></cmp>
  `,
})
export class App {}

@NgModule({
  imports: [ BrowserModule ],
  declarations: [ App,  DIComponent],
  providers: [{ provide: Dependency, useClass: ParentDependency }],
  bootstrap: [ App ]
})
export class AppModule {}

@SkipSelf

自身组件中定义的providers无效,从parent injector中查找依赖。

class Dependency {}

class ChildDependency {
 constructor() {
   console.log("ChildDependency");
 }
}

class ParentDependency {
 constructor() {
   console.log("ParentDependency");
 }
}

@Component({
  selector: 'cmp',
  template: `
    cmp
  `,
  providers: [{ provide: Dependency, useClass: ChildDependency }]
})
export class DIComponent {
  constructor(@SkipSelf() public dependency: Dependency) {}
}

@Component({
  selector: 'my-app',
  template: `
    <cmp></cmp>
  `,
})
export class App {}

@NgModule({
  imports: [ BrowserModule ],
  declarations: [ App,  DIComponent],
  providers: [{ provide: Dependency, useClass: ParentDependency }],
  bootstrap: [ App ]
})
export class AppModule {}

@ViewChild

@ViewChildren类似,不同点是只获取第一个符合的view DOM,不能获取ng-content中的内容。

@Component({
  selector: 'alert',
  template: `
    {{type}}
  `,
})
export class AlertComponent {
  @Input() type: string = "success";
}

@Component({
  selector: 'my-app',
  template: `
    <alert></alert>
    <div #divElement>Tada!</div>
  `,
})
export class App {
  // This will return the native element
  @ViewChild("divElement") div: any;
  // This will return the component instance
  @ViewChild(AlertComponent) alert: AlertComponent;
  
  ngAfterViewInit() {
    console.log(this.div);
    console.log(this.alert);
  }
}

@ViewChildren

@ViewChild类似,获取view DOM集合,ngAfterViewInitQueryList才会初始化。

@Component({
  selector: 'alert',
  template: `
    {{type}}
  `,
})
export class AlertComponent {
  @Input() type: string = "success";
}

@Component({
  selector: 'my-app',
  template: `
    <alert></alert>
    <alert type="danger"></alert>
    <alert type="info"></alert>
  `,
})
export class App {
  // 获取组件实例
  @ViewChildren(AlertComponent) alerts: QueryList<AlertComponent>
  
  // 获取DOM element
  @ViewChildren(AlertComponent, { read: ElementRef }) alerts2: QueryList<AlertComponent>

  // 需要动态创建组件或者模板时,需要获取 ViewContainerRef
  @ViewChildren(AlertComponent, { read: ViewContainerRef }) alerts3: QueryList<AlertComponent>
  
  ngAfterViewInit() {
    this.alerts.forEach(alertInstance => console.log(alertInstance));
    this.alerts2.forEach(alertInstance => console.log(alertInstance));
  }
}

ionic3 中使用 environments

ionic

我们需要不同的环境下,需要不同的参数,比如后端api接口什么的,使用Angular Cli,可以很容易的实现这点。在ionic3中,与angular中不一样的,还有cordova这一层壳。

假设我们需要3个环境:devuatprod

web

  • IONIC_ENV: dev、prod,代表2种打包模式(简单说为aot与非aot)。
  • NODE_ENV: dev、uat、prod,代表3中不同的环境,webapp使用的参数。比如后端server_url不一样。cordova而言,比如jpush的key不一样。

区分这两种概念之后,就可以理解下面的组合了。

NODE_ENVIONIC_ENV的组合:

  1. dev dev: environments: environment.dev.ts , 非aot方式打包
  2. dev prod: environments: environment.dev.ts , aot方式打包
  3. uat dev: environments: environment.uat.ts , 非aot方式打包
  4. uat prod: environments: environment.uat.ts , 非aot方式打包
  5. prod dev: environments: environment.prod.ts , 非aot方式打包
  6. prod prod: environments: environment.prod.ts , 非aot方式打包

第一步: 在src目录下新建environments文件夹

增加environment.dev.ts,environment.uat.ts,environment.prod.ts

// environment.dev.ts
export const ENV = {
  "mode": "Dev",
  "database": "data.db",
  "server_url": "https://xxxx.com",
  "cordova":{
    "id":"io.ionic.starter.dev",
    "version":"0.0.1",
    "ios":{
      "CodePushServerUrl": "dev",
      "CodePushDeploymentKey":"1234567890"
    },
    "android":{
      "CodePushServerUrl": "dev",
      "CodePushDeploymentKey":"0987654321"
    }
  }
}

第二步: 修改 tsconfig.json

{
  "compilerOptions": {
    ...
    "baseUrl": "./src",
    "paths": {
      "@env/environment": [ "environments/environment.prod"]
    },
    ...
  },

第三步: 增加 config/webpack.config.js 文件

var chalk = require("chalk");
var fs = require('fs');
var path = require('path');
var useDefaultConfig = require('@ionic/app-scripts/config/webpack.config.js');

var env = process.env.NODE_ENV || 'dev';
var IONIC_ENV = process.env.IONIC_ENV

console.log('NODE_ENV:'+ env);

console.log('IONIC_ENV:'+IONIC_ENV);

if(env === 'dev'){
  if(IONIC_ENV == 'dev'){
    useDefaultConfig.dev.resolve.alias = {
      "@env/environment": path.resolve(environmentPath('dev'))
    };
  };
  if(IONIC_ENV == 'prod'){
    useDefaultConfig.prod.resolve.alias = {
      "@env/environment": path.resolve(environmentPath('dev'))
    };
  };
}

if(env === 'uat'){
  if(IONIC_ENV == 'dev'){
    useDefaultConfig.dev.resolve.alias = {
      "@env/environment": path.resolve(environmentPath('uat'))
    };
  };
  if(IONIC_ENV == 'prod'){
    useDefaultConfig.prod.resolve.alias = {
      "@env/environment": path.resolve(environmentPath('uat'))
    };
  };
}

if(env === 'prod'){
  if(IONIC_ENV == 'dev'){
    useDefaultConfig.dev.resolve.alias = {
      "@env/environment": path.resolve(environmentPath('prod'))
    };
  };
  if(IONIC_ENV == 'prod'){
    useDefaultConfig.prod.resolve.alias = {
      "@env/environment": path.resolve(environmentPath('prod'))
    };
  };
}

function environmentPath(env) {
  var filePath = 'src/environments/environment.' + env + '.ts';
  console.log("use env file:"+filePath);
  if (!fs.existsSync(filePath)) {
    console.log(chalk.red('\n' + filePath + ' does not exist!'));
  } else {
    return filePath;
  }
}

module.exports = function () {
  return useDefaultConfig;
};

第四步: 修改 package.json

增加config配置

  ....
  "config": {
    "ionic_webpack": "./config/webpack.config.js"
  }
  ....

使用方式

import { Component } from '@angular/core';
import {ENV} from '@env/environment'

@Component({
  selector: 'page-page1',
  templateUrl: 'page1.html'
})
export class Page1Page {
  constructor() {
    console.log(ENV.mode);
  }
}

cordova

定义一个config.tpl.xml,使用hooks:before_prepare,在执行cordova prepare之前替换config.xml

第一步: 下载 es6-template-strings

npm install es6-template-strings --save-dev

第二步: 创建config/hooks/config.tpl.xml

<?xml version='1.0' encoding='utf-8'?>
<widget id="${id}" version="${version}" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
    <name>zoe</name>
    <description>An awesome Ionic app(zoe).</description>
    <author email="[email protected]" href="http://iszoe.com/">CK</author>
    <content src="index.html" />
    <access origin="*" />
    <allow-intent href="http://*/*" />
    <allow-intent href="https://*/*" />
    <allow-intent href="tel:*" />
    <allow-intent href="sms:*" />
    <allow-intent href="mailto:*" />
    <allow-intent href="geo:*" />
    <preference name="ScrollEnabled" value="false" />
    <preference name="android-minSdkVersion" value="16" />
    <preference name="BackupWebStorage" value="none" />
    <preference name="SplashMaintainAspectRatio" value="true" />
    <preference name="FadeSplashScreenDuration" value="300" />
    <preference name="SplashShowOnlyFirstTime" value="false" />
    <preference name="SplashScreen" value="screen" />
    <preference name="SplashScreenDelay" value="3000" />
    <hook src="config/hooks/before_prepare.js" type="before_prepare" />
    <platform name="android">
        <allow-intent href="market:*" />
        <icon density="ldpi" src="resources/android/icon/drawable-ldpi-icon.png" />
        <icon density="mdpi" src="resources/android/icon/drawable-mdpi-icon.png" />
        <icon density="hdpi" src="resources/android/icon/drawable-hdpi-icon.png" />
        <icon density="xhdpi" src="resources/android/icon/drawable-xhdpi-icon.png" />
        <icon density="xxhdpi" src="resources/android/icon/drawable-xxhdpi-icon.png" />
        <icon density="xxxhdpi" src="resources/android/icon/drawable-xxxhdpi-icon.png" />
        <splash density="land-ldpi" src="resources/android/splash/drawable-land-ldpi-screen.png" />
        <splash density="land-mdpi" src="resources/android/splash/drawable-land-mdpi-screen.png" />
        <splash density="land-hdpi" src="resources/android/splash/drawable-land-hdpi-screen.png" />
        <splash density="land-xhdpi" src="resources/android/splash/drawable-land-xhdpi-screen.png" />
        <splash density="land-xxhdpi" src="resources/android/splash/drawable-land-xxhdpi-screen.png" />
        <splash density="land-xxxhdpi" src="resources/android/splash/drawable-land-xxxhdpi-screen.png" />
        <splash density="port-ldpi" src="resources/android/splash/drawable-port-ldpi-screen.png" />
        <splash density="port-mdpi" src="resources/android/splash/drawable-port-mdpi-screen.png" />
        <splash density="port-hdpi" src="resources/android/splash/drawable-port-hdpi-screen.png" />
        <splash density="port-xhdpi" src="resources/android/splash/drawable-port-xhdpi-screen.png" />
        <splash density="port-xxhdpi" src="resources/android/splash/drawable-port-xxhdpi-screen.png" />
        <splash density="port-xxxhdpi" src="resources/android/splash/drawable-port-xxxhdpi-screen.png" />
    </platform>
    <platform name="ios">
        <allow-intent href="itms:*" />
        <allow-intent href="itms-apps:*" />
        <icon height="57" src="resources/ios/icon/icon.png" width="57" />
        <icon height="114" src="resources/ios/icon/[email protected]" width="114" />
        <icon height="40" src="resources/ios/icon/icon-40.png" width="40" />
        <icon height="80" src="resources/ios/icon/[email protected]" width="80" />
        <icon height="120" src="resources/ios/icon/[email protected]" width="120" />
        <icon height="50" src="resources/ios/icon/icon-50.png" width="50" />
        <icon height="100" src="resources/ios/icon/[email protected]" width="100" />
        <icon height="60" src="resources/ios/icon/icon-60.png" width="60" />
        <icon height="120" src="resources/ios/icon/[email protected]" width="120" />
        <icon height="180" src="resources/ios/icon/[email protected]" width="180" />
        <icon height="72" src="resources/ios/icon/icon-72.png" width="72" />
        <icon height="144" src="resources/ios/icon/[email protected]" width="144" />
        <icon height="76" src="resources/ios/icon/icon-76.png" width="76" />
        <icon height="152" src="resources/ios/icon/[email protected]" width="152" />
        <icon height="167" src="resources/ios/icon/[email protected]" width="167" />
        <icon height="29" src="resources/ios/icon/icon-small.png" width="29" />
        <icon height="58" src="resources/ios/icon/[email protected]" width="58" />
        <icon height="87" src="resources/ios/icon/[email protected]" width="87" />
        <icon height="1024" src="resources/ios/icon/icon-1024.png" width="1024" />
        <splash height="1136" src="resources/ios/splash/Default-568h@2x~iphone.png" width="640" />
        <splash height="1334" src="resources/ios/splash/Default-667h.png" width="750" />
        <splash height="2208" src="resources/ios/splash/Default-736h.png" width="1242" />
        <splash height="1242" src="resources/ios/splash/Default-Landscape-736h.png" width="2208" />
        <splash height="1536" src="resources/ios/splash/Default-Landscape@2x~ipad.png" width="2048" />
        <splash height="2048" src="resources/ios/splash/Default-Landscape@~ipadpro.png" width="2732" />
        <splash height="768" src="resources/ios/splash/Default-Landscape~ipad.png" width="1024" />
        <splash height="2048" src="resources/ios/splash/Default-Portrait@2x~ipad.png" width="1536" />
        <splash height="2732" src="resources/ios/splash/Default-Portrait@~ipadpro.png" width="2048" />
        <splash height="1024" src="resources/ios/splash/Default-Portrait~ipad.png" width="768" />
        <splash height="960" src="resources/ios/splash/Default@2x~iphone.png" width="640" />
        <splash height="480" src="resources/ios/splash/Default~iphone.png" width="320" />
        <splash height="2732" src="resources/ios/splash/Default@2x~universal~anyany.png" width="2732" />
    </platform>
    <platform name="android">
        <preference name="CodePushDeploymentKey" value="${android.CodePushDeploymentKey}" />
    </platform>
    <platform name="ios">
        <preference name="CodePushDeploymentKey" value="${ios.CodePushDeploymentKey}" />
    </platform>
    <plugin name="cordova-plugin-whitelist" spec="1.3.3" />
    <plugin name="cordova-plugin-device" spec="2.0.1" />
    <plugin name="cordova-plugin-splashscreen" spec="5.0.2" />
    <plugin name="cordova-plugin-ionic-webview" spec="1.1.16" />
    <plugin name="cordova-plugin-ionic-keyboard" spec="2.0.5" />
    <plugin name="cordova-plugin-code-push" spec="1.11.7" />
    <engine name="ios" spec="4.5.4" />
    <engine name="android" spec="7.1.0" />
</widget>

第三步: 创建config/hooks/before_prepare.js

#!/usr/bin/env node
var fs = require('fs');
var path = require('path');
var compile = require('es6-template-strings/compile');
var resolveToString = require('es6-template-strings/resolve-to-string');

var ROOT_DIR = path.resolve(__dirname, '../../')

var env = process.env.NODE_ENV || 'dev';
var envFile = 'src/environments/environment.' + env + '.ts';

var FILES = {
  SRC: "config/hooks/config.tpl.xml",
  DEST: "config.xml"
};

console.log('hooks-start: before_prepare','envFile:'+envFile);

var srcFileFull = path.join(ROOT_DIR, FILES.SRC);
var destFileFull = path.join(ROOT_DIR, FILES.DEST);
var configFileFull = path.join(ROOT_DIR, envFile);

var templateData = fs.readFileSync(srcFileFull, 'utf8');
var configData = fs.readFileSync(configFileFull, 'utf8').toString().split('ENV =')[1];
var config = JSON.parse(configData)['cordova'];

var compiled = compile(templateData);
var content = resolveToString(compiled, config);

fs.writeFileSync(destFileFull, content);

console.log('hooks-end: before_prepare');

第四步,修改现有config.xml

增加<hook src="config/hooks/before_prepare.js" type="before_prepare" />

<?xml version='1.0' encoding='utf-8'?>
<widget id="io.ionic.starter" version="0.0.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
    <name>zoe</name>
    <description>An awesome Ionic app(zoe).</description>
    <author email="[email protected]" href="http://iszoe.com/">CK</author>
    <content src="index.html" />
    <access origin="*" />
    <allow-intent href="http://*/*" />
    <allow-intent href="https://*/*" />
    <allow-intent href="tel:*" />
    <allow-intent href="sms:*" />
    <allow-intent href="mailto:*" />
    <allow-intent href="geo:*" />
    <preference name="ScrollEnabled" value="false" />
    <preference name="android-minSdkVersion" value="16" />
    <preference name="BackupWebStorage" value="none" />
    <preference name="SplashMaintainAspectRatio" value="true" />
    <preference name="FadeSplashScreenDuration" value="300" />
    <preference name="SplashShowOnlyFirstTime" value="false" />
    <preference name="SplashScreen" value="screen" />
    <preference name="SplashScreenDelay" value="3000" />
  +  <hook src="config/hooks/before_prepare.js" type="before_prepare" />
  ....

命令

uat为例:

  "serve:uat":  "NODE_ENV=uat ionic build"
  "prepare:uat:prod": "NODE_ENV=uat ionic cordova prepare --prod",
  "build:ios:uat:prod": "NODE_ENV=uat ionic cordova build ios --prod",
  "build:android:uat:prod": "NODE_ENV=uat ionic cordova build android --prod"

示例

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.