ck110 / ck110.github.io Goto Github PK
View Code? Open in Web Editor NEWsanshine’blog
sanshine’blog
使用
rxjs
的时候,您通常需要在某个点取消订阅才能释放系统中的内存。否则,你将有内存泄漏,让我们看看在ngOnDestroy生命周期钩子中需要退订的最常见情况。
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中同理
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。
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();
}
}
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();
}
}
export class TestComponent {
constructor(private store: Store) { }
todos: Subscription;
ngOnInit() {
this.todos = this.store.select('todos').subscribe(console.log);
}
ngOnDestroy() {
this.todos.unsubscribe();
}
}
@Component({
selector: 'test',
template: `<todos [todos]="todos$ | async"></todos>`
})
export class TestComponent {
constructor(private store: Store) { }
ngOnInit() {
this.todos$ = this.store.select('todos');
}
}
当组件被销毁时,async
管道自动取消订阅,以避免潜在的内存泄漏。
export class TestDirective {
@HostListener('click')
onClick() {
....
}
}
当你有一个有限的序列,通常你不需要unsubscribe
,例如当使用HTTP
service或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();
}
}
官方文档并没有说明
Template-driven Form
与Reactive Form
哪一个更好。由于之前开发过一个Ionic2
项目,使用的是Template-driven Form
,光是校验就有一坨代码,维护与开发简直惨不忍睹,所以个人更加推荐使用Reactive Form
。
使用Reactive Form
(同步),我们会在代码中创建整个表单 form control
树。我们可以立即更新一个值或者深入到表单中的任意节点,因为所有的 Form control
都始终是可用的。而且因为是同步,有利于单元测试。
在Template-driven Form
(异步)中,我们是通过指令来创建 form control
的。我们在操作一个Form control
之前,必须要经历一个变化检测周期。
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),而FormControl的结构就是表单模型(form model)。
组件必须把数据模型中的英雄值复制到表单模型中。这里隐含着两个非常重要的点。
个人经验:
formControl
。提交的时候不需要手动组装数据。Template-driven Form
,需要手动组装提交的数据,而且并没有严格区分数据模型与表单模型,后期维护时,代码很乱。any
,不然维护的时候,这酸爽!!!setValue
: 使用的时候需要每个from control
都要设置值。否则,ERROR Error: Must supply a value for form control with name: 'xxxxx'
patchValue
: 类似打补丁,不需要每个from control
都要设置值。想标题真是一件很麻烦的事,所以就借用<权利的游戏>的经典台词:Winter Is Coming。最近看了看ionic4的相关文档,初步总结一下。
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 plugin 版本之间对应的管理。比如Ionic Native的keyboard的哪些版本对于cordva plugin的哪些版本,个人感觉其实挺混乱的,需要自己去辨别吧。
capacitor虽说可以使用cordova,但是不支持参数,需要自己在代码中去写。关注点就不仅仅是web端了,需要hook去修改生成的原生项目,或者原生源码加入版本管理。
还是很希望capacitor能解决一些原生的问题的,比如keyboard的一些问题。
个人推荐使用 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提供的组件库。
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
这是在stackoverflow讨论比较多的问题。大多数答案都集中在这两者的用法之间的区别,本文在组件初始化过程进行一个更全面的比较。
让我们从与语言本身有关的最明显的区别开始。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总是被执行,不管你是否在类上实现一个构造函数。
从组件初始化阶段来看,两者之间存在巨大的差异。Angular引导程序(bootstrap)由两个主要阶段组成:
Angular构造组件树时调用该组件的构造函数。包括ngOnInit
的所有生命周期钩子都作为接下来的change detection
阶段的一部分进行调用。通常,组件初始化逻辑需要一些DI providers
或可用的input bindings
或rendered 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 Angular和The 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
现在让我们看看从使用上的差异。
构造函数主要用于注入依赖关系。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.。
然而,通常的做法是构造函数中写尽可能少的逻辑。
正如我们在上面学习Angular调用ngOnInit
时已经完成创建一个组件DOM,通过构造函数注入了所有需要的依赖关系并处理了输入绑定。所以在这里你可以获得所有必需的信息,这使得它成为执行初始化逻辑的好地方。
即使这个逻辑不依赖于DI,DOM或输入绑定,使用ngOnInit
来执行初始化逻辑也是常见的做法。
获取宿主元素的对应的属性值。
@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({
selector: 'greet',
template: 'Hello {{name}}!'
})
class Greet {
name: string = 'World';
}
和@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 {}
和@ContentChild
类似,获取ng-content
中符合的view DOM
,只有ngAfterContentInit
后才会有view DOM
,QueryList
才会初始化
@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({
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; }
}
从当前先上获取符合的父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 {}
设置宿主元素的属性绑定。
@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 {}
宿主元素的事件监听。
@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 {}
指明依赖。
@Component({
selector: 'cmp',
template: `
cmp
`
})
export class DIComponent {
constructor(@Inject(Dependency) public dependency) {}
}
声明类可以被DI
使用。
@Injectable()
export class WidgetService {
constructor(
public authService: AuthService) { }
}
定义组件的属性。
@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');
}
}
定义module。
@NgModule({
imports: [ CommonModule ],
declarations: [
DIComponent,
HostBindingDirective,
HostDI
],
providers: [{ provide: Dependency, useClass: ParentDependency}],
exports: [
DIComponent,
HostBindingDirective,
HostDI
]
})
export class DemoModule {}
依赖可选
class OptionalDependency {}
@Component({
selector: 'cmp',
template: `
cmp
`,
})
export class DIComponent {
constructor(@Optional() public dependency: OptionalDependency) {}
}
定义组件的事件。
@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({
name: 'isNull'
})
export class IsNullPipe implements PipeTransform {
transform (value: any): boolean {
return isNull(value);
}
}
只使用自身的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 {}
自身组件中定义的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 {}
和@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);
}
}
和@ViewChild
类似,获取view DOM
集合,ngAfterViewInit
后QueryList
才会初始化。
@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));
}
}
我们需要不同的环境下,需要不同的参数,比如后端api接口什么的,使用Angular Cli,可以很容易的实现这点。在ionic3中,与angular中不一样的,还有cordova这一层壳。
假设我们需要3个环境:dev
、uat
、prod
IONIC_ENV
: dev、prod,代表2种打包模式(简单说为aot与非aot)。NODE_ENV
: dev、uat、prod,代表3中不同的环境,webapp使用的参数。比如后端server_url
不一样。cordova而言,比如jpush的key不一样。区分这两种概念之后,就可以理解下面的组合了。
NODE_ENV
与IONIC_ENV
的组合:
增加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"
}
}
}
{
"compilerOptions": {
...
"baseUrl": "./src",
"paths": {
"@env/environment": [ "environments/environment.prod"]
},
...
},
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;
};
增加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);
}
}
定义一个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');
增加<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"
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.