Ionic開發App中重要的部分

寫在前面

APP趕在了春節之前上線了,所以這次我們分享一下使用Ionic3 + Angular5構建一個Hybird App過程中的經驗。什麼是Hybird App以及一些技術的選型這裏就不討論了。我每次完成一個部分就寫一部分,所以有文章有點長。如果有錯誤的地方感謝大家指正~

爲什麼選了Ionic ?

有些朋友說Angular/Ionic不大行,但是我覺的技術沒有好壞之分,只有適合不適合。首先在我看來Ionic已經在Hybird App開發領域立足多年,已經相當的成熟了,我覺的比大部分的解決方案都要好。其次因爲我們的App是一個弱交互多展示類型的,Ionic
滿足我們的需求。最後是因爲如果你想在沒有Android團隊和IOS團隊支持的情況下獨立完成一款APP,那麼Ionic我覺的是不二之選。因爲Ionic4還在beta版本,並且是公司項目所以依然選用了穩定的3.X版本。

注意:非基礎入門教程,所以在讀這篇文章之前建議你最好先了解[Angular](https://www.angular.cn/guide/quickstart), [TS](https://www.tslang.cn/docs/home.html), [Ionic](https://ionicframework.com/docs/)的基礎知識,這裏主要是希望大家在使用Ionic的時候能少走一些彎路。

由於我自己用的不是很熟練Rxjs這一塊就沒有寫,等以後對Rxjs的理解更加深刻了再加上

Angular彙總部分

既然是基於Angular那我們首先來了解一下Angular,這個地方積累的是Angular中零散的部分。如果內容多的話後期會拆分爲單獨的部分

Angular組件生命週期

Angular的生命週期

Hooks官方介紹

  • 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,用法也都是一樣的:

    <!-- 父組件 -->
    <child-component>
      我是父組件中的內容默認映射過來的
    </child-component>
    <!-- 子組件 -->
    <!-- 插槽 -->
      <ng-content>
        
      </ng-content>

    上面是最簡單的默認映射使用方式

  • 針對性映射(具名插槽)
    我們也可以通過<ng-content>的select屬性實現我們的具名插槽。這個是可以根據條件進行填充。select屬性支持根據CSS選擇器(ELement, Class, [attribute]...)來匹配你的元素,如果不設置就全部接受,就像下面這樣:

    <!-- 父組件 -->
    <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: [email protected]
 * @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~😁😁😁

原文地址 如果覺得有用得話給個⭐吧

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章