Ionic開發App中重要的部分

MarkMan發表於2019-01-30

寫在前面

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>
    複製程式碼

    上面是最簡單的預設對映使用方式

  • 針對性對映(具名插槽) 我們也可以通過的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

    元素是動態載入元件的最佳選擇,因為它不會渲染任何額外的輸出

    <div class="ad-banner-example">
      <h3>Advertisements</h3>
      <ng-template ad-host></ng-template>
    </div>
    複製程式碼
  • 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-container>: 它是一個分組元素,但它不會汙染樣式或元素佈局,因為 Angular 壓根不會把它放進 DOM 中。
  • TemplateRef: 可以使用TemplateRef取得 <ng-template> 的內容,TemplateRef
  • 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)
  })
}
複製程式碼

注意點: 1: 訂閱必須再發布之前,不然接收不到。打個比喻:比如微信公眾號,你要先關注才能接收到它的推文,不然它再怎麼發推文,你也收不到。2: subscribe中得this指向是有點問題的,這裡需要注意一下。

使用者操作事件

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表示安裝成功

Ionic開發App中重要的部分

  1. SDK安裝/配置環境變數:這一部分是重點,稍微麻煩一些。

下載

解壓後將重新命名的資料夾,跟jdk放在一個父目錄,便於查詢:C:\Program Files\SDK

接著配置環境變數,我的電腦——右鍵屬性——-高階系統設定——-環境變數

在下面的**系統變數(s)**中,新建,鍵值對如下:

name: ANDROID_HOME
key: C:\Program Files\SDK
複製程式碼

Ionic開發App中重要的部分

新建完系統變數之後在path中加入全域性變數。

Ionic開發App中重要的部分

在控制檯中輸入android -h,出現下面的日誌,表示sdk安裝成功

Ionic開發App中重要的部分

接下來我們使用Android Studio進行SDK下載Adnroid Studio下載地址studio安裝完之後就要安裝Android SDK Tools,Android SDK platform-tools,Android SDK Build-tools這些工具包和SDK platform

Ionic開發App中重要的部分

Ionic開發App中重要的部分

  1. gradle安裝/配置環境變數

SDK都安裝完了之後我們再進行gradle的安裝和配置。

先在官網或者在這裡下載

然後同樣安裝在JDK,SDK的目錄下,便於查詢。 和SDK同樣的配置環境變數:

GRADLE_HOME=C:\Program Files\SDK\gradle-4.1
;%GRADLE_HOME%\bin
複製程式碼

測試命令(檢視版本):gradle -v 出現下面的日誌,表示安裝成功

Ionic開發App中重要的部分

進行打包

打包之前的環境準備工作都已經做完了,接下來我們進行打包`apk。

  1. 安裝cordova
npm i cordova -g
複製程式碼
  1. 在專案中建立Android工程,在Ionic專案中執行下面命令
ionic cordova platform add android
複製程式碼

Ionic開發App中重要的部分

這可能是一個很漫長的過程,你要耐心等待,畢竟曙光就在眼前了。

  1. 建立完Android專案之後專案的platform資料夾下會多出來一個android資料夾。這下接著執行打包命令。
ionic cordova build android
複製程式碼

然後你會看到控制檯瘋狂輸出,最後出現下圖表明你已經打包出來一個未簽名的安裝包

  1. APK簽名 APK不簽名是沒法釋出的。這個有兩種方法
  • 使用jdk簽名,這裡不多說,想了解的可以看這篇文章
  • 使用Android Studio打簽名包。

AS上方工具欄build中選取Generate Signed APK首先建立一個簽名檔案

Ionic開發App中重要的部分

生成完之後可以直接用AS打簽名包

Ionic開發App中重要的部分

點選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
      Ionic開發App中重要的部分
    2. 連線裝置,注意第一次連線的話,是需要fan牆的,否則會出現404等等的問題
      Ionic開發App中重要的部分
    3. 在連線的裝置中安裝需要除錯的App,接著Chrome會自動找到需要除錯的WebView
    4. 愉快的開始除錯
      Ionic開發App中重要的部分
  • 使用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()
        })
      }
    }
    複製程式碼

    效果如下

Ionic開發App中重要的部分

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的坑,之前因為不瞭解在打包的時候下面的選項並沒有勾選上

Ionic開發App中重要的部分

不加上的時候一直在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~???

示例程式碼請稍後

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

相關文章