Ionic开发App中重要的部分

写在前面

APP赶在了春节以前上线了,因此此次咱们分享一下使用Ionic3 + Angular5构建一个Hybird App过程当中的经验。什么是Hybird App以及一些技术的选型这里就不讨论了。我每次完成一个部分就写一部分,因此有文章有点长。若是有错误的地方感谢你们指正~css

为何选了Ionic ?

有些朋友说Angular/Ionic不大行,可是我觉的技术没有好坏之分,只有适合不适合。首先在我看来Ionic已经在Hybird App开发领域立足多年,已经至关的成熟了,我觉的比大部分的解决方案都要好。其次由于咱们的App是一个弱交互多展现类型的,Ionic
知足咱们的需求。最后是由于若是你想在没有Android团队和IOS团队支持的状况下独立完成一款APP,那么Ionic我觉的是不二之选。由于Ionic4还在beta版本,而且是公司项目因此依然选用了稳定的3.X版本。html

注意:非基础入门教程,因此在读这篇文章以前建议你最好先了解[Angular](https://www.angular.cn/guide/quickstart), [TS](https://www.tslang.cn/docs/home.html), [Ionic](https://ionicframework.com/docs/)的基础知识,这里主要是但愿你们在使用Ionic的时候能少走一些弯路。前端

因为我本身用的不是很熟练Rxjs这一块就没有写,等之后对Rxjs的理解更加深入了再加上vue

Angular汇总部分

既然是基于Angular那咱们首先来了解一下Angular,这个地方积累的是Angular中零散的部分。若是内容多的话后期会拆分为单独的部分java

Angular组件生命周期

Angular的生命周期android

Hooks官方介绍webpack

  • constructor() : 在任何其它生命周期钩子以前调用。能够用它来注入依赖项,但不要在这里作正事
  • ngOnChanges(changes: SimpleChanges) => void: 当被绑定的输入属性的值发生变化时调用,首次调用必定会发生在 ngOnInit() 以前
  • ngOnInit() => void: 在第一轮 ngOnChanges() 完成以后调用。只调用一次
  • ngDoCheck() => void: 在每一个变动检测周期中调用,ngOnChanges()ngOnInit() 以后
  • ngAfterContentInit() => voidAngular 把外部内容投影进组件/指令的视图以后调用。能够认为是外部内容初始化
  • ngAfterContentChecked() => voidAngular 完成被投影组件内容的变动检测以后调用。能够认为是外部内容更新
  • ngAfterViewInit() => void: 每当 Angular 初始化完组件视图及其子视图以后调用。只调用一次。
  • ngAfterViewChecked() => void:每当 Angular 作完组件视图和子视图的变动检测以后调用, ngAfterViewInit() 和每次 ngAfterContentChecked() 以后都会调用。
  • ngOnDestroy() => void:在 Angular 销毁指令/组件以前调用。

Angular中内容映射(插槽)的实现

  • <ng-content></ng-content>默认映射
    这个内容映射方向是由父组件映射到子组件中这个就至关于vue中的slot,用法也都是同样的:git

    <!-- 父组件 -->
    <child-component>
      我是父组件中的内容默认映射过来的
    </child-component>
    <!-- 子组件 -->
    <!-- 插槽 -->
      <ng-content>
        
      </ng-content>

    上面是最简单的默认映射使用方式github

  • 针对性映射(具名插槽)
    咱们也能够经过<ng-content>的select属性实现咱们的具名插槽。这个是能够根据条件进行填充。select属性支持根据CSS选择器(ELement, Class, [attribute]...)来匹配你的元素,若是不设置就所有接受,就像下面这样:web

    <!-- 父组件 -->
    <child-component>
      我是父组件中的内容默认映射过来的
      <header>
        我是根据header来映射的
      </header>
      <div class="class">
        我是根据class来映射的
      </div>
      <div name="attr">
        我是根据attr来映射的
      </div>
    </child-component>
    
    <!-- 子组件 -->
    <!-- 具名插槽 -->
    <ng-content select="header"></ng-content>
    <ng-content select=".class"></ng-content>
    <ng-content select="[name=attr]"></ng-content>
  • ngProjectAs
    上面那些都是映射都是做为直接子元素进行的映射,那要不是呢? 我想在外面再套一层呢?

    <!-- 父组件 -->
    <child-component>
      <!-- 这个时不是直接子节点了 这确定是不行的 那咱们就用到ngProjectAs了-->
      <div>
        <header>
          我是根据header来映射的
        </header>
      </div>
    </child-component>

    使用ngProjectAs,它能够做用于任何元素上。

    <!-- 父组件 -->
    <child-component>
      <div ngProjectAs="header">
        <header>
          我是根据ngProjectAs header来映射的
        </header>
      </div>
    </child-component>
  • ng-content有一个@ContentChild装饰器,能够用来调用和投影内容。可是要注意:只有在ngAfterContentInit声明周期中才能成功获取到经过ContentChild查询的元素。

既然提到了ng-content那咱们就来聊一聊ng-templateng-container

  • ng-template

    <ng-template> 元素是动态加载组件的最佳选择,由于它不会渲染任何额外的输出

    <div class="ad-banner-example">
      <h3>Advertisements</h3>
      <ng-template ad-host></ng-template>
    </div>
  • ng-container
    <ng-container> 是一个由 Angular 解析器负责识别处理的语法元素。 它不是一个指令、组件、类或接口,更像是 JavaScriptif 块中的花括号。通常用来把一些兄弟元素归为一组,它不会污染样式或元素布局,由于 Angular 压根不会把它放进 DOM 中。

    <p>
      I turned the corner
      <ng-container *ngIf="hero"><!-- ng-container不会被渲染 -->
        and saw {{hero.name}}. I waved
      </ng-container>
      and continued on my way.
    </p>

Angular指令

Angular中的指令分为组件,属性指令结构形指令属性型指令用于改变一个 DOM 元素的外观或行为,例如NgStyle结构型指令的职责是 HTML 布局。 它们塑造或重塑 DOM 的结构,好比添加、移除或维护这些元素,例如NgForNgIf

  1. 属性型指令

    • 经过Directive装饰符把一个类标记为 Angular 指令, 该选项提供配置元数据,用于决定该指令在运行期间要如何处理、实例化和使用。@Directive
    • 经过ElementRef获取绑定元素的DOM对象,ElementRef
    • 经过HostListener响应用户引起的事件,把一个事件绑定到一个宿主监听器,并提供配置元数据。 当宿主元素发出特定的事件时,Angular 就会执行所提供的处理器方法,并使用其结果更新所绑定到的元素。 若是该事件处理器返回 false,则在所绑定的元素上执行 preventDefaultHostListener
    • 经过Input装饰符把某个类字段标记为输入属性,而且提供配置元数据。 声明一个可供数据绑定的输入属性,在变动检测期间,Angular 会自动更新它,@Input
    @Input('appHighlight') highlightColor: string;

下面是一个完整的属性形指令的例子

import {Directive, ElementRef, HostListener, Input} from '@angular/core';

@Directive({
  selector: '[sxylight]'
})
export class SxylightDirective {
  constructor(private el: ElementRef) {
    el.nativeElement.style.backgroundColor = 'yellow';
  }
  // 指令绑定的值
  @Input('sxylight') highlightColor: string;
  // 在指令内部,该属性叫 highlightColor,在外部,你绑定到它地方,它叫 sxylight 这个是绑定的别名

  // 指令宿主绑定的值
  @Input() defaultColor: string;
  // 监听宿主事件
  @HostListener('mouseenter') onMouseEnter() {
    this.highlight(this.highlightColor || this.defaultColor || 'red');
  }
  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(null);
  }
  private highlight(color: string) {
    this.el.nativeElement.style.backgroundColor = color;
  }
}
  1. 结构型指令

    • 星号(*)前缀:这个东西实际上是语法糖,Angular*ngIf 属性 翻译成一个 <ng-template> 元素 并用它来包裹宿主元素。
    • <ng-template>: 它是一个 Angular 元素,用来渲染 HTML。 它永远不会直接显示出来。 事实上,在渲染视图以前,Angular 会把 <ng-template> 及其内容替换为一个注释。
    • <ng-container>: 它是一个分组元素,但它不会污染样式或元素布局,由于 Angular 压根不会把它放进 DOM 中。
    • TemplateRef: 可使用TemplateRef取得 <ng-template> 的内容,TemplateRef<any>
    • ViewContainerRef: 能够经过ViewContainerRef来访问这个视图容器,ViewContainerRef

完整示例

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
/**
* Input, TemplateRef, ViewContainerRef 这三个模块是构建一个结构型指令必须的模块
* Input: 传值
* TemplateRef: 表示一个内嵌模板,它可用于实例化内嵌的视图。 要想根据模板实例化内嵌的视图,请使用 ViewContainerRef 的 createEmbeddedView() 方法。
* ViewContainerRef: 表示能够将一个或多个视图附着到组件中的容器。
*/
@Directive({
  selector: '[structure]' // Attribute selector
})
export class StructureDirective {
  private hasView = false
  @Input()
  set structure(contion: boolean) {
    console.log(contion)
    if (!contion && !this.hasView) {
      this.viewCon.createEmbeddedView(this.template) // 实例化内嵌视图并插入到容器中
      this.hasView = true
    } else if (contion && this.hasView) {
      this.viewCon.clear() // 销毁容器中的全部试图
      this.hasView = false
    }
  }

  constructor(
    private template: TemplateRef<any>,
    private viewCon: ViewContainerRef
  ) {
    console.log('Hello StructureDirective Directive');
  }

}

Angular中的Module

首先咱们来看看NgModule

interface NgModule {
  // providers: 这个选项是一个数组,须要咱们列出咱们这个模块的一些须要共用的服务
  //            而后咱们就能够在这个模块的各个组件中经过依赖注入使用了.
  providers : Provider[]
  // declarations: 数组类型的选项, 用来声明属于这个模块的指令,管道等等.
  //               而后咱们就能够在这个模块中使用它们了.
  declarations : Array<Type<any>|any[]>
  // imports: 数组类型的选项,咱们的模块须要依赖的一些其余的模块,这样作的目的使咱们这个模块
  //          能够直接使用别的模块提供的一些指令,组件等等.
  imports : Array<Type<any>|ModuleWithProviders|any[]>
  // exports: 数组类型的选项,咱们这个模块须要导出的一些组件,指令,模块等;
  //          若是别的模块导入了咱们这个模块,
  //          那么别的模块就能够直接使用咱们在这里导出的组件,指令模块等.
  exports : Array<Type<any>|any[]>
  // entryComponents: 数组类型的选项,指定一系列的组件,这些组件将会在这个模块定义的时候进行编译
  //                  Angular会为每个组件建立一个ComponentFactory而后把它存储在ComponentFactoryResolver
  entryComponents : Array<Type<any>|any[]>
  // bootstrap: 数组类型选项, 指定了这个模块启动的时候应该启动的组件.固然这些组件会被自动的加入到entryComponents中去
  bootstrap : Array<Type<any>|any[]>
  // schemas: 不属于Angular的组件或者指令的元素或者属性都须要在这里进行声明.
  schemas : Array<SchemaMetadata|any[]>
  // id: 字符串类型的选项,模块的隐藏ID,它能够是一个名字或者一个路径;用来在getModuleFactory区别模块,若是这个属性是undefined
  //     那么这个模块将不会被注册.
  id : string
}
  • app.module.ts
app.module.ts
└───@NgModule
    └───declarations                // 告诉Angular哪些模块属于NgModule
    │───imports                     // 导入须要使用的模块
    │───bootstrap                   // 启动模块
    │───entryComponents             // 定义组建时应该被编译的组件
    └───providers                   // 服务配置

entryComponents:Angular使用entryComponents来启用tree-shaking,即只编译项目中实际使用的组件,而不是编译全部在ngModule中声明但从未使用的组件。离线模板编译器(OTC)只生成实际使用的组件。若是组件不直接用于模板,OTC不知道是否须要编译。有了entryComponents,你能够告诉OTC也编译这些组件,以便在运行时可用。

Ionic工程目录结构

首先来看项目目录

Ionic-frame
│   build                   // 打包扩展
│   platforms               // Android/IOS 平台代码
│   plugins                 // cordova插件
│   resources
└───src                     // 业务逻辑代码
│   │   app                 // 启动组件
│   │   assets              // 资源
│   │   components          // 公共组件
│   │   config              // 配置文件
│   │   directive           // 公共指令
│   │   interface           // interface配置中心
│   │   pages               // 页面
│   │   providers           // 公共service
│   │   service             // 业务逻辑service
│   │   shared              // 共享模块
│   │   theme               // 样式模块
│   │   index.d.ts          // 声明文件
└───www                     // 打包后静态资源

Ionic视图生命周期

生命周期的重要性不用多说,这是Ionic官网的介绍

  • constrctor => void: 构造函数启动,构造函数在ionViewDidLoad以前被触发
  • ionViewDidLoad => void: 资源加载完毕时触发。ionViewDidLoad只在第一次进入页面时触发只触发一次
  • ionViewWillEnter => void: 页面即将给进入时触发每次都会触发
  • ionViewDidEnter => void: 进入视图以后出发每次都会触发
  • ionViewWillLeave => void: 即将离开(仅仅是触发要离开的动做)时触发每次都会触发
  • ionViewDidLeave => void: 已经离开页面时触发每次都会触发
  • ionViewWillUnload => void: 在页面即将被销毁并删除其元素时触发
  • ionViewCanEnter => boolean:在视图能够进入以前运行。 这能够在通过身份验证的视图中用做一种“保护”,您须要在视图能够进入以前检查权限
  • ionViewCanLeave => boolean:在视图能够离开以前运行。 这能够在通过身份验证的视图中用做一种“防御”,您须要在视图离开以前检查权限

注意: 当你想使用ionViewCanEnter/ionViewCanLeave进行对路由的拦截时,你须要返回一个Boolen。返回true进入下一个视图,返回fasle留在当前视图。

能够按照下面的代码感觉一下生命周期的顺序

constructor(public navCtrl: NavController) {
  console.log('触发构造函数')
}

/**
 * 页面加载完成触发,这里的“加载完成”指的是页面所需的资源已经加载完成,但还没进入这个页面的状态(用户看到的仍是上一个页面)。全程只会调用一次
 */
ionViewDidLoad () {
  console.log(`Ionic触发ionViewDidLoad`);
  // Step 1: 建立 Chart 对象
  const chart = new F2.Chart({
    id: 'myChart',
    pixelRatio: window.devicePixelRatio // 指定分辨率
  })
  // Step 2: 载入数据源
  chart.source(data)
  chart.interval().position('genre*sold').color('genre')
  chart.render()
}
/**
 * 即将进入Ionic视图  这时对页面的数据进行预处理 每次都会触发
 */
ionViewWillEnter(){
  console.log(`Ionic触发ionViewWillEnter`)
}
/**
 * 已经进入Ionic视图 每次都会触发
 */
ionViewDidEnter(){
  console.log(`Ionic触发ionViewDidEnter`)
}
/**
 * 页面即将 (has finished) 离开时触发 每次都会触发
 */
ionViewWillLeave(){
  console.log(`Ionic触发ionViewWillLeave`)
}
/**
 * 页面已经 (has finished) 离开时触发,页面处于非激活状态了。 每次都会触发
 */
ionViewDidLeave(){
  console.log(`Ionic触发ionViewDidLeave`)
}
/**
 * 页面中的资源即将被销毁 通常用处不大
 */
ionViewWillUnload(){
  console.log(`Ionic触发ionViewWillUnload`)
}
//守卫导航钩子: 返回true或者false
/**
 * 在视图能够进入以前运行。 这能够在通过身份验证的视图中用做一种“保护”,您须要在视图能够进入以前检查权限
 */
ionViewCanEnter(){
  console.log(`Ionic触发ionViewCanEnter`)
  const date = new Date().getHours()
  console.log(date)
  if (date > 22) {
    return false
  }
  return true
}
/**
 * 在视图能够离开以前运行。 这能够在通过身份验证的视图中用做一种“防御”,您须要在视图离开以前检查权限
 */
ionViewCanLeave(){
  console.log(`Ionic触发ionViewCanLeave`)
  const date = new Date().getHours()
  console.log(date)
  if (date > 10) {
    return false
  }
  return true
}

项目配置文件设置

Ionic3.X中并无提供相应的的配置文件,因此咱们须要本身按照下面步骤手动去添加配置文件来对项目进行配置。

  1. 新增config目录
src
  |__config
      |__config.dev.ts
      |__config.prod.ts

config.dev.ts / config.prod.ts

export const CONFIG = {
  BASE_URL            : 'http://XXXXX/api', // API地址
  VERSION             : '1.0.0'
}
  1. 在根目录下新增build文件夹,在文件夹中新增webpack.config.js config文件
const fs = require('fs')
const chalk =require('chalk')
const webpack = require('webpack')
const path = require('path')
const defaultConfig = require('@ionic/app-scripts/config/webpack.config.js')

const env = process.env.IONIC_ENV
/**
 * 获取配置文件
 * @param {*} env 
 */
function configPath(env) {
  const filePath = `./src/config/config.${env}.ts`
  if (!fs.existsSync(filePath)) {
    console.log(chalk.red('\n' + filePath + ' does not exist!'));
  } else {
    return filePath;
  }
}
// 定位当前文件
const resolveDir = filename => path.join(__dirname, '..', filename)
// 其余文件夹别名
let alias ={
  "@": resolveDir('src'),
  "@components": resolveDir('src/components'),
  "@directives": resolveDir('src/directives'),
  "@interface": resolveDir('src/interface'),
  "@pages": resolveDir('src/pages'),
  "@service": resolveDir('src/service'),
  "@providers": resolveDir('src/providers'),
  "@theme": resolveDir('src/theme')
}
console.log("当前APP环境为:"+process.env.APP_ENV)
let definePlugin =  new webpack.DefinePlugin({
  'process.env': {
    APP_ENV: '"'+process.env.APP_ENV+'"'
  }
})
// 设置别名
defaultConfig.prod.resolve.alias = {
  "@config": path.resolve(configPath('prod')), // 配置文件
  ...alias
}
defaultConfig.dev.resolve.alias = {
  "@config": path.resolve(configPath('dev')),
  ...alias
}

// 其余环境
if (env !== 'prod' && env !== 'dev') {
  defaultConfig[env] = defaultConfig.dev
  defaultConfig[env].resolve.alias = {
    "@config": path.resolve(configPath(env))
  }
}
// 删除sourceMaps

module.exports = function () {
  return defaultConfig
}
  1. tsconfig.json配合,配置中新增以下内容 这个地方很扯 这个path相关的须要放在tsconfig.json的最上面
"baseUrl": "./src",
  "paths": {
    "@app/env": [
      "environments/environment"
    ]
  }
  1. 修改package.json。配置末尾新增以下内容
"config": {
  "ionic_webpack": "./config/webpack.config.js"
}
  1. 使用配置变量
import {CONFIG} from "@app/env"

若是过咱们想修改Ionic中其余的webpack配置, 那么能够像上面那种形式来进行修改。

// 拿到webpack 的默认配置 剩下的还不是随心所欲
const defaultConfig = require('@ionic/app-scripts/config/webpack.config.js');
// 像这样去修改配置
defaultConfig.prod.resolve.alias = {
  "@config": path.resolve(configPath('prod'))
}
defaultConfig.dev.resolve.alias = {
  "@config": path.resolve(configPath('dev'))
}

Ionic路由

  • 首页设置
    有时候咱们须要设置咱们第一次显示得页面。那这样咱们就须要使用NavController来设置

    // app.component.ts
    public rootPage: any = StartPage; //
  • 路由跳转

    1. href方式跳转:直接在dom中指定要跳转的页面,以tabs中的代码为例
    <!-- 单个跳转按钮  [root]="HomeRoot" 是最重要的 -->
    <ion-tab [root]="HomeRoot" tabTitle="Home" tabIcon="home"></ion-tab>
    import { HomePage } from '../home/home'
    export class TabsPage {
      // 声明变量地址
      HomeRoot = HomePage
      constructor() {
        
      }
    }
    1. 编程式导航:编程式导航咱们可能会用的更多,下面是一个基础的例子

编程式导航是由NavController控制

NavController是Nav和Tab等导航控制器组件的基类。 您可使用导航控制器导航到应用中的页面。 在基本级别,导航控制器是表示特定历史(例如Tab)的页面数组。 经过推送和弹出页面或在历史记录中的任意位置插入和删除它们,能够操纵此数组以在整个应用程序中导航。当前页面是数组中的最后一页,若是咱们这样想的话,它是堆栈的顶部。 将新页面推送到导航堆栈的顶部会致使新页面被动画化,而弹出当前页面将导航到堆栈中的上一页面。

除非您使用NavPush之类的指令,或者须要特定的NavController,不然大多数时候您将注入并使用对最近的NavController的引用来操纵导航堆栈。

// 引入NavController
import { NavController } from 'ionic-angular';
import { NewsPage } from '../news/news'
export class HomePage {
  // 注入NavController
constructor(public navCtrl: NavController) {
  // this.navCtrl.push(LoginPage)
}
goNews () {
    this.navCtrl.push(NewsPage, {
      title : '测试传参'
    })
  }
}
  • 相关经常使用API

    1. navCtrl.push(OtherPage, param): 跳转页面
    2. navCtrl.pop(): Removing a view 移除当前View,至关于返回上一个页面
    3. 路由中参参数相关

      • push(Page, param)传参: 这个很简单也很明白
      this.navCtrl.push(NewsPage, {
        title : '测试传参'
      })
      • [navParams]属性:和HTML配合进行传参
      import {LoginPage } from'./login';
      @Component()
      class MyPage {
        params;
        pushPage: any;
        constructor(){
          this.pushPage= LoginPage;
          this.params ={ 
            id:123,
            name: "Carl"
          }
        }
      }
      <button ion-button [navPush]="pushPage" [navParams]="params">
        Go
      </button>
      <!-- 同理在root page上传递参数就是下面这种方式 -->
      <ion-tab [root]="tab1Root"  tabTitle="home" tabIcon="home"  [rootParams]="userInfo">
      </ion-tab
      • 获取参数
      //NavController就是用来管理和导航页面的一个controller
      constructor(public navCtrl: NavController, public navParams: NavParams) {
        //1: 经过NavParams get方法获取到单个对象
        this.titleName = navParams.get('name')
        //2: 直接获取全部的参数
        this.para = navParams.data
      }

provider(service)使用

当重复的须要一个类中的方法时,可封装它为服务类,以便重复使用,如http。

provider,也叫service。前者是ionic的叫法,后者是ng的叫法。建议仔细得学一下Angular

  • 建立Provider

Ionic提供了建立指令

ionic g provider http

自动建立的Provider会自主动在app.module中导入注意这个须要在app.module中注入
首先导入装饰器,再用装饰器装饰,这样,该类就能够做为提供者注入到其余类中以使用:

import { Injectable } from '@angular/core';
@Injectable()

export class StorageService {
  constructor() {
    console.log('Hello StorageService');
  }
  myAlert(){
    alert("服务类的方法")
  }
}
  • 使用provider

若是是顶级的服务(全局通用服务),须要在app.module.tsproviders中注册后而后使用

import { StorageService } from './../../service/storage.service';
export class LoginPage {

  userName: string = 'demo'
  password: string = '123456'

  constructor(
    public storageService: StorageService
    ) {
    
  }
  doLogin () {
    const para = {
      userName: this.userName,
      password:  this.password
    }
    console.log(para)
    if (para.userName === 'demo' && para.password === '123456') {
      this.storageService.setStorage('user', para)
    }
    setTimeout(() => {
      console.log(this.storageService.getStorage('user'))
    }, 3000)
  }
}

Ionic事件系统

Events是一个 发布-订阅样式事件系统,用于在您的应用程序中发送和响应应用程序级事件。

这个是不一样页面之间交流的核心。主要用于组件的通讯。你也能够用events传递数据到任何一个页面。

Events实例方法

  • publish(topic, eventData): 发布一个event
  • subscribe(topic, handler): 订阅一个event
  • unsubscribe(topic, handler) 取消订阅一个event
// 发布event login.ts
// 发布event事件
submitEvent (data) {
  console.log(1)
  this.event.publish('user:login', data)
}
// 订阅页面  message.ts
constructor(public event: Events ) {
  // 订阅event事件
  event.subscribe('user:login', (data) => {
    console.log(data)
    let obj = {
      url: 'assets/imgs/logo.png',
      name: data.username
    }
    this.messages.push(obj)
  })
}

注意点: <font color="red">1: 订阅必须再发布以前,否则接收不到。打个比喻:好比微信公众号,你要先关注才能接收到它的推文,否则它再怎么发推文,你也收不到。2: subscribe中得this指向是有点问题的,这里须要注意一下。</font>

用户操做事件

Basic gestures can be accessed from HTML by binding to tap, press, pan, swipe, rotate, and pinch events.

Ionic对手势事件的解释基本是一笔带过。

组件间通讯

组件之间的通讯:要把一个组件化的框架给玩6了。组件以前的通讯搞明白了是个前提。在Ionic中,咱们使用Angular中的方式来实现。

  • 父 => 子@Input()

    • 经过输入型绑定把数据从父组件传到子组件:这个用途最普遍和常见,和recat中的props很是类似
    // 父组件定义值(用来传递)
    export class NewsPage {
      father: number = 1 // 父组件数据
      /**
       * Ionic生命周期函数
      */
      ionViewDidLoad() {
        // 父组件数据更改
        setTimeout(() => {
          this.father ++ 
        }, 2000)
      }
    }
    // 子组件定义属性(用来接收)
    @Input() child: number // @Input装饰器标识child是一个输入性属性
    <!-- 父组件使用 -->
    <backtop [child]="father"></backtop>
    <!-- 子组件定义 -->
    <div class="backtop">
      <p (click)="click()">back</p>
      father数据: {{child}}
    </div>
    • 经过get, set在子组件中对父组件得数据进行拦截来达到咱们想要得结果
    // 拦截父组件得值
    private _showContent: string 
    @Input()
    // set value
    set showContent(name: string) {
      if (name !== '4') {
        this._showContent = 'no'
      } else {
        this._showContent = name
      }
    }
    // get value
    get showContent () :string {
      return this._showContent
    }
    • 经过ngOnChanges监听值得变化
    // 监听全部属性值得变化
    ngOnChanges(changes: SimpleChange): void {
      /**
       * 从旧值到新值得一次变动
       * class SimpleChange {
          constructor(previousValue: any, currentValue: any, firstChange: boolean)
          previousValue: any // 变化前得值
          currentValue: any // 当前值
          firstChange: boolean
          isFirstChange(): boolean // 检查该新值是否从首次赋值得来的。
        }
       */
      // changes props集合对象
      console.log(changes['child'].currentValue) // 
    }
    • 父组件与子组件经过本地变量互动
    父组件不能使用数据绑定来读取子组件的属性或调用子组件的方法。但能够在父组件模板里, 新建一个本地变量来表明子组件,而后利用这个变量来读取子组件的属性和调用子组件的方法.

经过#childComponent定义这个组件。而后直接使用childComponent.XXX去调用。这个的话就有点强大了,可是这个交流时页面级别的。仅限于在html定义本地变量而后在html中进行操做和通讯。也就是父组件-子组件的链接必须所有在父组件的模板中进行。父组件自己的代码对子组件没有访问权。

<!-- 父组件 -->
<button ion-button color="secondary" full  (click)="childComponent.fromFather()">测试本地变量</button>
<backtop #childComponent [child]="father" [showContent] = "father" (changeChild)="childCome($event)"></backtop>
// 子组件
// 父子组件经过本地变量交互
fromFather () {
  console.log(`I am from father`)
  this.show  = !this.show
}
  • 父组件调用@ViewChild()互动

    若是父组件的类须要读取子组件的属性值或调用子组件的方法,能够把子组件做为 ViewChild,注入到父组件里面。

也就是说@ViewChild()是为了解决上面的短板而出现的。

// 父组件
import { Component, ViewChild } from '@angular/core';
export class NewsPage {
  //定义子组件数据
  @ViewChild(BacktopComponent)
  private childComponent: BacktopComponent
  ionViewDidLoad() {
    setTimeout(() => {
      // 经过child调用子组件方法
      this.childComponent.formChildView()
    }, 2000)
  }
}
  • 子 => 父: @Output(): 最经常使用的方法
子组件暴露一个 EventEmitter 属性,当事件发生时,子组件利用该属性 emits(向上弹射)事件。父组件绑定到这个事件属性,并在事件发生时做出回应。
// 父组件
// 接收儿子组件得来得值 并把儿子得值赋给父亲
childCome (data: number) {
  this.father =  data
}
// 字组件
// 子向父传递得事件对象
@Output() changeChild: EventEmitter<number> = new EventEmitter() // 定义事件传播器对象
// 执行子组件向父组件通讯
click () {
  this.changeChild.emit(666)
}
<!-- 父组件 -->
<backtop [child]="father" [showContent] = "father" (changeChild)="childCome($event)"></backtop>

获取父组件实例

有的时候咱们也能够暴力一点获取父组件的实例去使用它(未验证)。

constructor(
    // 注册父组件
    @Host() @Inject(forwardRef(() => NewsPage)) father: NewsPage
  ) {
    this.text = 'Hello World';
    setTimeout(() => {
      // 直接经过对象来修改父组件
      father.father++
    }, 3000)
  }
  • 父 <=> 子父子组件经过服务来通讯

    若是咱们把一个服务实例的做用域被限制在父组件和其子组件内,这个组件子树以外的组件将没法访问该服务或者与它们通信。父子共享一个服务,那么咱们能够利用该服务在家庭内部实现双向通信

    // service
    import { Injectable } from '@angular/core'; // 标记元数据
    // 使用service进行父子组件的双向交流
    @Injectable()
    export class MissionService {
      familyData: string = 'I am family data'
    }
    // father component
    import { MissionService } from './../../service/mission.service';
    export class NewsPage {
      constructor( public missionService: MissionService) {
      }
      ionViewDidLoad() {
        // 父组件数据更改
        setTimeout(() => {
          // 调用修改service中的数据 这个时候父子组件中的service都会改变
          this.missionService.familyData = 'change familyData'
        }, 2000)
      }
    }
    // child component
    import { Component} from '@angular/core';
    import { MissionService } from './../../service/mission.service';
    @Component({
      selector: 'backtop',
      templateUrl: 'backtop.html'
    })
    export class BacktopComponent {
      constructor(
        public missionService:MissionService
      ) {
        console.log(missionService)
        this.text = 'Hello World';
      }
      // 执行子组件向父组件通讯
      click () {
        // 修改共享信息
        this.missionService.familyData = 'change data by child'
      }
    }
    <!-- 父组件直接使用 -->
    {{missionService.familyData}}
    <!-- 子组件 -->
    <div>
      servicedata: {{missionService.familyData}}
    </div>

    service中使用订阅也能够一样的实现数据的通讯

    // mission.service.ts
    import { Subject } from 'rxjs/Subject';
    import { Injectable } from '@angular/core'; // 标记元数据
    // 使用service进行父子组件的双向交流
    @Injectable()
    export class MissionService {
      familyData: string = 'I am family data'
      // 订阅式的共享数据
      private Source = new Subject()
      Status$=this.Source.asObservable()
      statusMission (msg: string) {
        this.Source.next(msg)
      }
    }
    
    // 父组件
    // 经过service的订阅提交信息
    emitByService () {
      this.missionService.statusMission('emitByService')
    }
    // 子组件
    // 返回一个订阅器
    this.subscription = missionService.Status$.subscribe((msg:string) => {
      this.text = msg
    })
    ionViewWillLeave(){
      // 取消订阅
      this.subscription.unsubscribe()
    }
  • 高级通讯

    1. 咱们可使用ionic-angular中的Events模块来进行 父 <=> 子 , 兄 <=> 弟的高级通讯。Events模块在通讯方面具备得天独厚的优点。具体能够看上面的示例
    2. 使用EventEmitter模块
    // service
    import { EventEmitter } from '@angular/core'; // 标记元数据
    // 使用service进行父子组件的双向交流
    @Injectable()
    export class MissionService {
      // Event通讯 来自angular
      serviceEvent = new EventEmitter()
    }
    
    // 父组件
    // 经过Events 模块高级通讯 接收信息
    this.missionService.serviceEvent.subscribe((msg: string) => {
      this.messgeByEvent = msg
    })
    
    // 子组件
    // 经过emit 进行高级通讯 发送新
    emitByEvent () {
      this.missionService.serviceEvent.emit('emit by event')
    }

Shared组件

公共组件设置,Angular倡导的是模块化开发,因此公共组件的注册可能稍有不一样。

在这里咱们根据Angular提供的CommonModule共享模块,咱们要知道他干了什么事儿:

  1. 它导入了 CommonModule,由于该模块须要一些经常使用指令。
  2. 它声明并导出了一些工具性的管道、指令和组件类。
  3. 它从新导出了 CommonModuleFormsModule
  4. CommonModuleFormsModule能够代替BrowserModule去使用
  • 定义

shared文件夹下新建shared.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; 

// 经过从新导出 CommonModule 和 FormsModule,任何导入了这个 SharedModule 的其它模块,就均可以访问来自 CommonModule 的 NgIf 和 NgFor 等指令了,也能够绑定到来自 FormsModule 中的 [(ngModel)] 的属性了。
// 自定义的模块和指令
import { ComponentsModule } from './../components/components.module';
import { DirectivesModule } from './../directives/directives.module';

@NgModule({
  declarations: [],
  imports: [
    CommonModule,
    FormsModule
  ],
  exports:[
    // 导出模块
    CommonModule,
    FormsModule,
    ComponentsModule,
    DirectivesModule
  ],
  entryComponents: [

  ]
})
export class SharedModule {}

注意: 服务要经过单独的依赖注入系统进行处理,而不是模块系统

使用了shared模块仅仅须要在xxx.module.ts中引用便可,而后又就可使用shared中全部引入的公共模块。

import { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { XXXPage } from './findings';
import { SharedModule } from '@shared/shared.module';

@NgModule({
  declarations: [
    XXXPage,
  ],
  imports: [
    SharedModule,
    IonicPageModule.forChild(FindingsPage),
  ]
})
export class XXXPageModule {}

http部分

Ionic中的http模块是直接采用的HttpClient这个模块。这个没什么可说的,咱们只须要根据咱们的需求对service进行修改便可,例如能够把http改为了更加灵活的Promise模式。你也能够用Rxjs的模式来实现。下面这个是个简单版本的实现

import { TokenServie } from './token.service';
import { StorageService } from './storage.service';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'
import { Injectable, Inject } from '@angular/core'
import {ReturnObject, Config} from '../interface/index' // 返回数据类型和配置文件
/*
Generated class for the HttpServiceProvider provider.
*/
@Injectable()
export class HttpService{
  /**
   * @param CONFIG 
   * @param http 
   * @param navCtrl 
   */
  constructor(
    @Inject("CONFIG") public CONFIG:Config, 
    public storage: StorageService,
    public tokenService: TokenServie,
    public http: HttpClient
    ) {
      console.log(this.CONFIG)
  }
  /**
   * key to 'name='qweq''
   * @param key 
   * @param value 
   */
  private toPairString (key, value): string {
    if (typeof value === 'undefined') {
      return key
    }
    return `${key}=${encodeURIComponent(value === null ? '' : value.toString())}`
  }
  /**
   * objetc to url params
   * @param param 
   */
  private toQueryString (param, type: string = 'get') {
    let temp = []
    for (const key in param) {
      if (param.hasOwnProperty(key)) {
        let encodeKey = encodeURIComponent(key)
        temp.push(this.toPairString(encodeKey, param[key]))
      }
    }
    return `${type === 'get' ? '?' : ''}${temp.join('&')}`
  }
  /**
   * set http header
   */
  private getHeaders () {
    let token = this.tokenService.getToken()
    return new HttpHeaders({
      'Content-Type':  'application/x-www-form-urlencoded',
      'tokenheader': token ? token : ''
    })
  }
  /**
   * http post请求 for promise
   * @param url
   * @param body
   */
  public post (url: string, body ? : any): Promise<ReturnObject> {
    const fullUrl = this.CONFIG.BASE_URL + url
    console.log(this.toQueryString(body, 'post'))
    return new Promise<ReturnObject>((reslove, reject) =>{
      this.http.post(fullUrl, body, {
        // params,
        headers: this.getHeaders()
      }).subscribe((res: any) => {
        reslove(res)
      }, err => {
        // this.handleError(err)
        reject(err)
      })
    })
  }
  /**
   * get 请求 return promise
   * @param url 
   * @param param 
   */
  public get(url: string, params: any = null): Promise<ReturnObject> {
    const fullUrl = this.CONFIG.BASE_URL + url
    let realParams = new HttpParams()
    for (const key in params) {
      if (params.hasOwnProperty(key)) {
        realParams.set(`${key}`, params[key])
      }
    }
    // add time map
    realParams.set(
      'timestamp', (new Date().getTime()).toString()
    )
    return new Promise<ReturnObject>((reslove, reject) =>{
      this.http.get(fullUrl, {
        params,
        headers: this.getHeaders()
      }).subscribe((res: any) => {
        console.log(res)
        reslove(res)
      }, err => {
        // this.handleError(err)
        reject(err)
      })
    })
  }
}

Cordova插件使用

Ionic提供了丰富的基于cordova的插件,官网介绍,使用起来也很简单。

下载Cordova插件

cordova add plugin plugin-name -D
npm install @ionic-native/plugin-name

使用插件(@ionic-native/plugin-name中导入)

import { StatusBar } from '@ionic-native/status-bar';
constructor(private statusBar: StatusBar) {
    //沉浸式而且悬浮透明
    statusBar.overlaysWebView(true);
    // 设置状态栏颜色为默认得黑色 适合浅色背景
    statusBar.styleDefault() 
    // 浅色状态栏 适合深色背景
    // statusBar.styleLightContent() 
}

优化部分

项目写完了,不优化一下 内心怪难受的。

  • App启动页体验优化

Ionic App毕竟是个混合App,毕竟尚未达到秒开级别。因此这个时候咱们须要启动页来帮助咱们提高用户体验,首先在config.xml种配子咱们的启动页相关配置

<preference name="ShowSplashScreenSpinner" value="false" /> <!-- 隐藏加载时的loader -->
<preference name="ScrollEnabled" value="false" /> <!-- 禁用启动屏滚动 -->
<preference name="SplashMaintainAspectRatio" value="true" /> <!-- 若是值设置为 true,则图像将不会伸展到适合屏幕。若是设置为 false ,它将被拉伸 -->
<preference name="FadeSplashScreenDuration" value="1000" /><!-- fade持续时长 -->
<preference name="FadeSplashScreen" value="true" /><!-- fade动画 -->
<preference name="SplashShowOnlyFirstTime" value="false" /><!-- 是否只第一次显示 -->
<preference name="AutoHideSplashScreen" value="false" /><!-- 自动隐藏SplashScreen -->
<preference name="SplashScreen" value="screen" />
<platform name="android">
    <allow-intent href="market:*" />
    <icon src="resources/android/icon/icon.png" />
    <splash src="resources/android/splash/screen.png" /><!-- 启动页路径 -->
    <!-- 下面是各个分辨率的兼容 -->
    <splash height="800" src="resources/android/splash/screenh.png" width="480" />
    <splash height="1280" src="resources/android/splash/screenm.png" width="720" />
    <splash height="1600" src="resources/android/splash/screenxh.png" width="960" />
    <splash height="1920" src="resources/android/splash/screenxxh.png" width="1280" />
    <splash height="2048" src="resources/android/splash/screenxxxh.png" width="1536" />
</platform>

我在这里关闭了自动隐藏SplashScreen,由于她的断定条件是一旦App出事还完毕就隐藏,这显然不符合咱们的要求。咱们须要的是咱们的Ionic WebView程序启动以后再隐藏。因此咱们在app.component.ts中借助@ionic-native/splash-screen来进行这个操做.

platform.ready().then(() => {
      // 延迟1s隐藏启动屏幕
      setTimeout(() => { 
        splashScreen.hide()
      }, 1000)
    })

这样一来咱们就能够完美的欺骗用户,体验能好点。

打包优化

  • 新增--prod参数

    "build:android": "ionic cordova build android --prod --release",
    • 预(AOT)编译:预编译 Angular 组件的模板。
    • 生产模式:启用生产模式部署到生产环境。
    • 打捆(Bundle):把这些模块串接成一个单独的捆文件(bundle)。
    • 最小化:移除没必要要的空格、注释和可选令牌(Token)。
    • 混淆:使用短的、无心义的变量名和函数名来重写代码。
    • 消除死代码:移除未引用过的模块和未使用过的代码.

App打包

我认为打包APK对于一些不了解服务端和Android的前端工程师来讲仍是比较费劲的。下面咱们来仔细的说一说这个部分。

环境配置

第一步进行各个环境的配置

  1. Node安装/配置环境变量(我相信这个你已经弄完了)
  2. jdk安装 (无需配置环境变量)

    jdk是java的开发环境支持,你能够在这里下载, 提取码:9p74

    下载完成后,解压,直接按照提示安装,全局点肯定,不出意外,最后的安装路径为:C:\Program Files\Javajdk安装完成,在cmd中,输入java -version验证是否安装成功。我这边是修改了安装路径,若是你不熟悉的话仍是不要修改安装路径。出现了下面的log表示安装成功

clipboard.png

  1. SDK安装/配置环境变量:这一部分是重点,稍微麻烦一些。

    下载
    解压后将重命名的文件夹,跟jdk放在一个父目录,便于查找:C:\Program Files\SDK
    接着配置环境变量,个人电脑——右键属性——-高级系统设置——-环境变量
    在下面的系统变量(s)中,新建,键值对以下:

    name: ANDROID_HOME
    key: C:\Program Files\SDK

    clipboard.png

    新建完系统变量以后在path中加入全局变量。

clipboard.png

在控制台中输入android -h,出现下面的日志,表示sdk安装成功

clipboard.png

接下来咱们使用Android Studio进行SDK下载Adnroid Studio下载地址studio安装完以后就要安装Android SDK Tools,Android SDK platform-tools,Android SDK Build-tools这些工具包和SDK platform

clipboard.png

clipboard.png

  1. gradle安装/配置环境变量

    SDK都安装完了以后咱们再进行gradle的安装和配置。

    先在官网或者在这里下载

    而后一样安装在JDK,SDK的目录下,便于查找。
    SDK一样的配置环境变量:

    GRADLE_HOME=C:\Program Files\SDK\gradle-4.1
    ;%GRADLE_HOME%\bin

    测试命令(查看版本):gradle -v 出现下面的日志,表示安装成功

    clipboard.png

进行打包

打包以前的环境准备工做都已经作完了,接下来咱们进行打包`apk。

  1. 安装cordova
npm i cordova -g
  1. 在项目中建立Android工程,在Ionic项目中执行下面命令
ionic cordova platform add android

clipboard.png

这多是一个很漫长的过程,你要耐心等待,毕竟曙光就在眼前了。

  1. 建立完Android项目以后项目的platform文件夹下会多出来一个android文件夹。这下接着执行打包命令。
ionic cordova build android

而后你会看到控制台疯狂输出,最后出现下图代表你已经打包出来一个未签名的安装包

  1. APK签名

APK不签名是无法发布的。这个有两种方法

  • 使用jdk签名,这里很少说,想了解的能够看这篇文章
  • 使用Android Studio打签名包。

    AS上方工具栏build中选取Generate Signed APK首先建立一个签名文件

    clipboard.png

    生成完以后能够直接用AS打签名包

    clipboard.png

点击locate就能看到咱们的apk包了~ 至此咱们的Android就ok了,IOS的以后再补上。

简单APP服务器更新(简单示例)

因为Android的要求不如苹果那么严,咱们也能够经过本身的服务器进行程序的更新。下面就是实现一个比较简单的更新Service

更新咱们主要是使用到下面几个Cordova插件

  • cordova-plugin-file-transfer / @ionic-native/file-transfer: 线上文件的下载和存储(官方推荐使用XHR2,有兴趣的能够看一看)
  • cordova-plugin-file-opener2 / @ionic-native/file-opener: 用于打开APK文件
  • cordova-plugin-app-version / @ionic-native/app-version: 用于获取app的版本号
  • cordova-plugin-file / @ionic-native/file:操做app上的文件系统
  • cordova-plugin-device / @ionic-native/device:获取当前设备信息,主要用于平台的区分

在下载完插件以后咱们来实现一个比较简陋的版本更新service,具体解释我会写在代码注释中,主要分红两部分,一部分是具体的更新操做update.service.ts, 另外一部分是用于存放数据的data.service.ts
data.service.ts

/*
 * @Author: etongfu
 * @Description: 设备信息
 * @youWant: add you want info here
 */
import { Injectable } from '@angular/core';
import { Device } from '@ionic-native/device';
import { File } from '@ionic-native/file';
import { TokenServie } from './token.service';
import { AppVersion } from '@ionic-native/app-version';

@Injectable()
export class DataService {
  /******************************APP数据模块******************************/
  // app 包名
  private packageName: string = '' 
  // app 版本号
  private appCurrentVersion: string =  '---'
  // app 版本code
  private appCurrentVersionCode:number = 0
  // 当前程序运行平台
  private currentSystem: string
  // 当前userId
  // app 下载资源存储路径
  private savePath: string
  //  当前app uuid
  private uuid: string

  /******************************通用数据模块******************************/
  constructor (
    public device: Device,
    public file: File,
    public app: AppVersion,
    public token: TokenServie,
    public http: HttpService
  ) {
    // 必须在设备准备完以后才能进行获取
    document.addEventListener("deviceready", () => {
      // 当前运行平台
      this.currentSystem = this.device.platform
      // console.log(this.device.platform)
      // app版本相关信息
      this.app.getVersionNumber().then(data => {
        //当前app版本号  data,存储该版本号
        if (this.currentSystem) {
          // console.log(data)
          this.appCurrentVersion = data
        }
      }, error => console.error(error))
      this.app.getVersionCode().then((data) => {
        //当前app版本号数字代码 
        if (this.currentSystem) {
          this.appCurrentVersionCode = Number(data)
        }
      }, error => console.error(error))
      // app 包名
      this.app.getPackageName().then(data => {
          //当前应用的packageName:data,存储该包名
          if (this.currentSystem) {
            this.packageName = data;
          }
      }, error => console.error(error))
      // console.log(this.currentSystem)
      // file中的save path 根据平台进行修改地址
      this.savePath = this.currentSystem === 'iOS' ? this.file.documentsDirectory : this.file.externalDataDirectory;

    }, false);
  }
  /**
   * 获取app 包名
   */
  public getPackageName () {
    return this.packageName
  }
  /**
   * 获取当前app版本号
   * @param hasV 是否加上V标识
   */
  public getAppVersion (hasV: boolean = true): string {
    return hasV ? `V${this.appCurrentVersion}` : this.appCurrentVersion
  }
  /**
   * 获取version 对应的nuamber 1.0.0 => 100
   */
  public getVersionNumber ():number {
    const temp = this.appCurrentVersion.split('.').join('')
    return Number(temp)
  }
  /**
   * 获取app version code 用于比较更新使用
   */
  public getAppCurrentVersionCode (): number{
    return this.appCurrentVersionCode
  }
  /**
   * 获取当前运行平台
   */
  public getCurrentSystem (): string {
    return this.currentSystem
  }
  /**
   * 获取uuid
   */
  public getUuid ():string {
    return this.uuid
  }
  /**
   * 获取存储地址
   */
  public getSavePath ():string {
    return this.savePath
  }
}

update.service.ts

/*
 * @Author: etongfu
 * @Email: 13583254085@163.com
 * @Description: APP简单更新服务
 * @youWant: add you want info here
 */
import { HttpService } from './../providers/http.service';
import { Injectable, Inject } from '@angular/core'
import { AppVersion } from '@ionic-native/app-version';
import { PopSerProvider } from './pop.service';
import { DataService } from './data.service';
import {Config} from '@interface/index'
import { FileTransfer, FileTransferObject } from '@ionic-native/file-transfer';
import { FileOpener } from '@ionic-native/file-opener';
import { LoadingController } from 'ionic-angular';

@Injectable()
export class AppUpdateService {

  constructor (
    @Inject("CONFIG") public CONFIG:Config, 
    public httpService: HttpService,
    public appVersion: AppVersion,
    private fileOpener: FileOpener,
    private transfer: FileTransfer,
    private popService: PopSerProvider, // 这就是个弹窗的service
    private dataService: DataService,
    private loading:LoadingController
  ) {

  }
  /**
   * 经过当前的字符串code去进行判断是否有更新
   * @param currentVersion 当前app version
   * @param serverVersion 服务器上版本
   */
  private hasUpdateByCode (currentVersion: number, serverVersion:number):Boolean {
    return serverVersion > currentVersion
  }
  /**
   * 查询是否有可更新程序
   * @param noUpdateShow  没有更新时显示提醒
   */
  public checkForUpdate (noUpdateShow: boolean = true) {
    // 拦截平台
    return new Promise((reslove, reject) => {
      // http://appupdate.ymhy.net.cn/appupdate/app/findAppInfo?appName=xcz&regionCode=370000
      // 查询app更新
      this.httpService.get(this.CONFIG.CHECK_URL, {}, true).then((result: any) => {
        reslove(result)
        if (result.succeed) {
          const data = result.appUpload
          const popObj = {
            title: '版本更新',
            content: ``
          }
          console.log(`当前APP版本:${this.dataService.getVersionNumber()}`)
          // 存在更新的状况下
          if (this.hasUpdateByCode(this.dataService.getVersionNumber(), data.versionCode)) {
          // if (this.hasUpdateByCode(101, data.versionCode)) {
            let title = `新版本<b>V${data.appVersion}</b>可用,是否当即下载?<h5 class="text-left">更新日志</h5>`
            // 更新日志部分
            let content = data.releaseNotes
            popObj.content = title + content
            // 生成弹窗
            this.popService.confirmDIY(popObj, data.isMust === '1' ? true: false, ()=> {
              this.downLoadAppPackage(data.downloadPath)
            }, ()=> {
              console.log('取消');
            })
          } else {
            popObj.content = '已经是最新版本!'
            if(!noUpdateShow) {
              this.popService.confirmDIY(popObj, data.isMust === '1' ? true: false)
            }
          }
        } else {
          // 接口响应出现问题 直接提醒默认最新版本
          if(!noUpdateShow) {
            this.popService.alert('版本更新', '已经是最新版本!')
          }
        }
        }).catch((err) => {
          console.error(err)
          reject(err)
        })
      })
  }
  /**
   * 下载新版本App
   * @param url: string 下载地址
   */
  public downloadAndInstall (url: string) {
    let loading = this.loading.create({
      spinner: 'crescent',
      content: '下载中'
    })
    loading.present()
    try {
      if (this.dataService.getCurrentSystem() === 'iOS') {
        // IOS跳转相应的下载页面
        // window.location.href = 'itms-services://?action=download-manifest&url=' + url;
      } else {
        const fileTransfer: FileTransferObject = this.transfer.create();
        fileTransfer.onProgress(progress =>{
          // 展现下载进度
          const present = new Number((progress.loaded / progress.total) * 100);
          const presentInt = present.toFixed(0);
          if (present.toFixed(0) === '100') {
            loading.dismiss()
          } else {
            loading.data.content = `已下载 ${presentInt}%`
          }
        })
        const savePath = this.dataService.getSavePath() + 'xcz.apk';
        // console.log(savePath)
        // 下载而且保存
        fileTransfer.download(url,savePath).then((entry) => {
          //
          this.fileOpener.open(entry.toURL(), "application/vnd.android.package-archive")
          .then(() => console.log('打开apk包成功!'))
          .catch(e => console.log('打开apk包失败!', e))
        }).catch((err) => {
          console.error(err)
          console.log("下载失败");
          loading.dismiss()
          this.popService.alert('下载失败', '下载异常')
        })
      }
    } catch (error) {
      this.popService.alert('下载失败', '下载异常')
      // 有异常直接取消dismiss
      loading.dismiss()
    }
  }
}

以上咱们就能够根据直接调用service去进行更新
app.component.ts

// 调用更新
this.appUpdate.checkForUpdate()

App真机调试

说实在的,Hybird真机调试是真的痛苦。目前比较流行的方式是如下两种调试方式

  • Chrome Inspect调试

依靠chrome的强大能力,咱们能够把App中的WebView中的内容彻底的显示在chrome端。能够在web端控制咱们的app中的网页,仍是先当的炫酷的。如下是操做步骤

  1. 在chrome中打开chrome://inspect/#devices

clipboard.png

  1. 链接设备,注意第一次链接的话,是须要fan墙的,不然会出现404等等的问题

clipboard.png

  1. 在链接的设备中安装须要调试的App,接着Chrome会自动找到须要调试的WebView
  2. 愉快的开始调试

clipboard.png

  • 使用VConsole进行调试

    这个就更简单了,直接npm install vconsole这个库, 而后在app.component.ts进行引用

    import VConsole from 'vconsole'
    export class MyApp {
    constructor() {
        platform.ready().then(() => {
          console.log(APP_ENV)
          // 调试程序
          APP_ENV === 'debug' && new VConsole()
        })
      }
    }

    效果以下

clipboard.png

Ionic中的特殊部分(坑)

  • 静态资源路径问题

若是在打完包以后静态路径出来问题,没有加载出来的话要注意如下状况

<!-- html中的img标签直接引用图片处理   -->
<img src="./assets/xxx.jpg"/>
<!-- 或者这样 -->
<img src="assets/imgs/timeicon.png" style="width: 1rem;">
/*scss文件中要使用绝对路径*/
.bg{
  background-image: url("../assets/xxx.jpg")
}
  • Android API版本修改

Ionic中如今默认的SDK版本过高了,有些低版本的机器没发安装须要修改的有如下这么几个部分

<!-- platforms/android/project.properties  -->
target=android-26
<!-- 和platforms/android/CordovaLib/project.properties  -->
target=android-26
  • 关于SDKcordova插件中的坑(暂时不写)

这个东西真的是坑的一塌糊涂,以cordova-plugin-file-opener2为例

  • AS3.0打包以后Android7.0如下的手机没法安装

这个不能算是Ionic的坑,要算也得是Android Studio3.0的坑,以前由于不了解在打包的时候下面的选项并无勾选上

clipboard.png

不加上的时候一直在Android7.0如下都无法安装,一直觉得是项目代码的问题,没想到是设置的问题,加上了V1选项以后打也就能够了,查了一下缘由以下。

上图中提供的选项实际上是签名版本选择,在AS3.0的时候新增的选项。

Android 7.0中引入了APK Signature Scheme v2v1呢是jar Signature来自JDK
V1:应该是经过ZIP条目进行验证,这样APK 签署后可进行许多修改 - 能够移动甚至从新压缩文件。
V2:验证压缩文件的全部字节,而不是单个 ZIP 条目,所以,在签名后没法再更改(包括 zipalign)。正因如此,如今在编译过程当中,咱们将压缩、调整和签署合并成一步完成。好处显而易见,更安全并且新的签名可缩短在设备上进行验证的时间(不须要费时地解压缩而后验证),从而加快应用安装速度。

若是不勾选V1,那么在7.0如下会直接安装完显示未安装,7.0以上则使用了V2的方式验证。若是勾选了V1,那么7.0以上就不会使用更加安全的快速的验证方式。

也能够在app目录下的build.gradle中进行配置

signingConfigs {
    debug {
        v1SigningEnabled true
        v2SigningEnabled true
    }
    release {
        v1SigningEnabled true
        v2SigningEnabled true
    }
}

总结

这么一番折腾下来,越到了很多坑。可是也都一一解决了。使用Ionic最大的感触就是TS+Angular的模块化开发模式很舒服。并且开发速度上也不至于太慢,对Angular感兴趣的朋友我认为仍是能够一试的。

示例代码请稍后

春节立刻到了,祝各位开发者春节快乐远离BUG~😁😁😁

原文地址 若是以为有用得话给个⭐吧

相关文章
相关标签/搜索