Angular 和 Cordova 的混合应用程序:动态内容未在 IOS 中呈现

Hybrid app with Angular and Cordova: Dynamic content not rendered in IOS

我已按照本指南将 Angular 项目转换为混合应用程序: https://medium.com/@christof.thalmann/convert-angular-project-to-android-apk-in-10-steps-c49e2fddd29

对于 Android 我没有 运行 解决很多问题,应用程序在该平台上按预期运行。

上IOS我运行陷入多重困境。首先,为了显示内容,我需要将 Angular LocationStrategy 更改为 HashLocation,如本 SO 主题中所述:

Why Angular Router 8 router ...

尽管我现在确实获取了要渲染的内容,但我仍然无法正确获取动态内容(即需要在渲染前调用 Web 服务器的内容)。

我的应用程序有一个经典的导航栏,可以从一个组件切换到另一个组件。如果我通过单击导航按钮激活一个组件,则静态内容会正常显示。但是,动态内容不存在。我可以通过查看源代码来验证这一点:持有动态内容是空的。如果我再次单击同一个导航按钮,则会添加动态内容。我在 iPhone 模拟器和真实设备上得到了相同的效果。

这是组件之一的html

<div class="card bg-opaque border-light">
    <div class="card-body">
        <div>
            <h2 class="text-light text-opaque">Einsparungen von {{ user }}</h2>
        </div>

        <div *ngIf="monthScore" class="card border-primary bg-opaque-light"> <!-- THIS BLOCK NOT VISIBLE -->
            <div class="card-body text-center">
                <h3 class="card-title">Current Month</h3>
                <ul class="list-unstyled">
                    <li>
                        <strong>Savings:</strong> {{ monthScore.savings | number: '1.1-2' }} kg
                    </li>
                    <li>
                        <strong>Position:</strong> {{ monthScore.rank }}
                        <span *ngIf="!monthScore.rank">???</span>
                        <span *ngIf="monthScore.rank == 1">
                            <mat-icon class="text-warning">emoji_events</mat-icon>
                        </span>
                    </li>
                    <li>
                        <strong>Captured:</strong> {{ monthScore.captured | number: '1.1-2' }} kg
                    </li>
                </ul>
                <div *ngIf="showMonthButton" >
                    <button class="btn btn-outline-primary" (click)="toggleMonthGraph()">
                        <mat-icon>bar_chart</mat-icon>
                    </button>
                </div>
                <div *ngIf="!showMonthButton" (click)="toggleMonthGraph()">
                    <canvas
                        baseChart
                        [chartType]="'bar'"
                        [datasets]="monthChartData"
                        [labels]="monthChartLabels"
                        [options]="chartOptions"
                        [legend]="false">
                    </canvas>
                </div>
            </div>
        </div>

        <div *ngIf="yearScore" class="card border-primary bg-opaque-light"> <!-- THIS BLOCK NOT VISIBLE -->                <div class="card-body text-center">
                <h3 class="card-title">Current year</h3>
                <ul class="list-unstyled">
                    <li>
                        <strong>Savings:</strong> {{ yearScore.savings | number: '1.1-2' }} kg
                    </li> 
                    <li>
                        <strong>Position:</strong> {{ yearScore.rank }}
                        <span *ngIf="!yearScore.rank">???</span>
                        <span *ngIf="yearScore.rank == 1">
                            <mat-icon class="text-warning">emoji_events</mat-icon>
                        </span>
                    </li>
                    <li>
                        <strong>Captured:</strong> {{ yearScore.captured | number: '1.1-2' }} kg
                    </li>
                </ul>
                <div *ngIf="showYearButton" >
                    <button class="btn btn-outline-primary" (click)="toggleYearGraph()">
                        <mat-icon>bar_chart</mat-icon>
                    </button>
                </div>
                <div *ngIf="!showYearButton" (click)="toggleYearGraph()">
                    <canvas
                        baseChart
                        [chartType]="'bar'"
                        [datasets]="yearChartData"
                        [labels]="yearChartLabels"
                        [options]="chartOptions"
                        [legend]="false">
                    </canvas>
                </div>
            </div>
        </div>
    </div>
</div>

<app-inpage></app-inpage>

.ts 文件:

import { Component, OnInit } from '@angular/core';

import { SummaryService} from '../summary.service';
import { AuthService } from '../../auth/auth.service';
import { Score } from '../summary';
import { MAT_RIPPLE_GLOBAL_OPTIONS } from '@angular/material/core';

@Component({
  selector: 'app-score',
  templateUrl: './score.component.html',
  styleUrls: ['./score.component.scss']
})
export class ScoreComponent implements OnInit {

  monthScore: Score;
  yearScore: Score;
  user: string;

  // Histogramm per Consumer
  chartOptions = {
    responsive: true,
    scales: {
      xAxes: [{
          gridLines: {
              drawOnChartArea: false
          }
      }],
      yAxes: [{
          gridLines: {
              drawOnChartArea: false
          }
      }]
  }
  };

  yearChartData = [];
  yearChartLabels = [];
  yearChartTitle: string;
  showYearChart: boolean = false;
  showYearButton: boolean = true;

  monthChartData = [];
  monthChartLabels = [];
  monthChartTitle: string;
  showMonthChart: boolean = false;
  showMonthButton: boolean = true;

  constructor(private service: SummaryService, private authService: AuthService) { }

  ngOnInit(): void {

    this.user = this.authService.user

    this.getMonthScore();
    this.getYearScore();
  }

  getMonthScore(): void {
    this.service.getScore('month').subscribe(score => {
      this.monthScore = score;
      this.createMonthGraph();
    })
  }

  getYearScore(): void {
    console.log('GETTING SCORE')
    this.service.getScore('year').subscribe(score => {
      this.yearScore = score;
      this.createYearGraph();
    })
  }

  private createYearGraph(): void {
    this.service.getTimeline('year').subscribe(timelines => {
      let data: number[] = [];
      let label: string[] = [];
      for (let i = 0; i < timelines.length; i++){
        data.push(timelines[i].user_savings);
        label.push(timelines[i].period.toString());
      }

      this.yearChartData = [{data: data, label: 'Savings', barThickness: 2, backgroundColor: 'rgba(0, 0, 0, 0.5' }]
      this.yearChartLabels = label

    })
  }

  private createMonthGraph(): void {
    this.service.getTimeline('month').subscribe(timelines => {
      let data: number[] = [];
      let label: string[] = [];
      for (let i = 0; i < timelines.length; i++){
        data.push(timelines[i].user_savings);
        label.push(timelines[i].period.toString());
      }

      this.monthChartData = [{data: data, label: 'Savings', barThickness: 2, backgroundColor: 'rgba(0, 0, 0, 0.5' }]
      this.monthChartLabels = label

    })
  }

  toggleYearGraph(): void {
    this.showYearChart = !this.showYearChart;
    this.showYearButton = !this.showYearButton;
  }

  toggleMonthGraph(): void {
    this.showMonthButton = !this.showMonthButton;
  }
}

我的config.xml

<?xml version='1.0' encoding='utf-8'?>
<widget id="com.ticumwelt.co2" version="0.2.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
    <name>Tracker</name>
    <description>
        An app to track your savings.
    </description>
    <author email="mymail@example.com" href="https://example.com">
        Developer Team
    </author>
    <content src="index.html" />
    <access origin="*" />
    <allow-intent href="http://*/*" />
    <allow-intent href="https://*/*" />
    <allow-intent href="tel:*" />
    <allow-intent href="sms:*" />
    <allow-intent href="mailto:*" />
    <allow-intent href="geo:*" />
    <platform name="android">
        <allow-intent href="market:*" />
    </platform>
    <platform name="ios">
        <allow-intent href="itms:*" />
        <allow-intent href="itms-apps:*" />
        <!-- iOS 8.0+ -->
        <!-- iPhone 6 Plus  -->
        <icon src="res/ios/icons/icon-60@3x.png" width="180" height="180" />
        <!-- iOS 7.0+ -->
        <!-- iPhone / iPod Touch  -->
        <icon src="res/ios/icons/icon-60.png" width="60" height="60" />
        <icon src="res/ios/icons/icon-60@2x.png" width="120" height="120" />
        <!-- iPad -->
        <icon src="res/ios/icons/icon-76.png" width="76" height="76" />
        <icon src="res/ios/icons/icon-76@2x.png" width="152" height="152" />
        <!-- Spotlight Icon -->
        <icon src="res/ios/icons/icon-40.png" width="40" height="40" />
        <icon src="res/ios/icons/icon-40@2x.png" width="80" height="80" />
        <!-- iOS 6.1 -->
        <!-- iPhone / iPod Touch -->
        <icon src="res/ios/icons/icon.png" width="57" height="57" />
        <icon src="res/ios/icons/icon@2x.png" width="114" height="114" />
        <!-- iPad -->
        <icon src="res/ios/icons/icon-72.png" width="72" height="72" />
        <icon src="res/ios/icons/icon-72@2x.png" width="144" height="144" />
        <!-- iPad Pro -->
        <icon src="res/ios/icons/icon-167.png" width="167" height="167" />
        <!-- iPhone Spotlight and Settings Icon -->
        <icon src="res/ios/icons/icon-small.png" width="29" height="29" />
        <icon src="res/ios/icons/icon-small@2x.png" width="58" height="58" />
        <icon src="res/ios/icons/icon-small@3x.png" width="87" height="87" />
        <!-- iPad Spotlight and Settings Icon -->
        <icon src="res/ios/icons/icon-50.png" width="50" height="50" />
        <icon src="res/ios/icons/icon-50@2x.png" width="100" height="100" />
        <!-- iPad Pro -->
        <icon src="res/ios/icons/icon-83.5@2x.png" width="167" height="167" />
        
        <splash src="res/ios/screen/default@2x~universal~anyany.png" />

        <preference name="WKWebViewOnly" value="true" />

        <feature name="CDVWKWebViewEngine">
            <param name="ios-package" value="CDVWKWebViewEngine" />
        </feature>

        <preference name="CordovaWebViewEngine" value="CDVWKWebViewEngine" />
        <preference name="WKSuspendInBackground" value="false" />
    </platform>
    <edit-config target="NSLocationWhenInUseUsageDescription" file="*-Info.plist" mode="merge">
        <string>need location access to find things nearby</string>
    </edit-config>
</widget>

还有我的package.json

{
  "name": "com.example.tracker",
  "displayName": "Tracker",
  "version": "0.2.1",
  "description": "An app to track your savings",
  "main": "index.js",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "keywords": [
    "ecosystem:cordova"
  ],
  "author": "My Name",
  "license": "Apache-2.0",
  "private": true,
  "dependencies": {
    "@angular-material-components/datetime-picker": "^2.0.4",
    "@angular/animations": "~9.0.3",
    "@angular/cdk": "^9.2.4",
    "@angular/common": "~9.0.3",
    "@angular/compiler": "~9.0.3",
    "@angular/core": "~9.0.3",
    "@angular/forms": "~9.0.3",
    "@angular/localize": "~9.0.3",
    "@angular/material": "^9.2.4",
    "@angular/platform-browser": "~9.0.3",
    "@angular/platform-browser-dynamic": "~9.0.3",
    "@angular/router": "~9.0.3",
    "@ionic-native/background-geolocation": "^5.29.0",
    "@ionic-native/core": "^5.29.0",
    "@ionic/angular": "^5.4.1",
    "@ng-bootstrap/ng-bootstrap": "^6.2.0",
    "bootstrap": "^4.4.0",
    "chart.js": "^2.9.4",
    "cordova-plugin-splashscreen": "6.0.0",
    "material-design-icons-iconfont": "^6.1.0",
    "ng-connection-service": "^1.0.4",
    "ng2-charts": "^2.4.2",
    "ngx-cookie-service": "^10.1.1",
    "rxjs": "~6.5.4",
    "tslib": "^1.10.0",
    "zone.js": "~0.10.2"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "^0.1002.0",
    "@angular/cli": "~9.0.4",
    "@angular/compiler-cli": "~9.0.3",
    "@angular/language-service": "~9.0.3",
    "@globules-io/cordova-plugin-ios-xhr": "^1.2.0",
    "@mauron85/cordova-plugin-background-geolocation": "^3.1.0",
    "@types/jasmine": "~3.5.0",
    "@types/jasminewd2": "~2.0.3",
    "@types/node": "^12.11.1",
    "codelyzer": "^5.1.2",
    "cordova-ios": "^6.1.1",
    "cordova-plugin-geolocation": "^4.1.0",
    "cordova-plugin-whitelist": "^1.3.4",
    "jasmine-core": "~3.5.0",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "~4.3.0",
    "karma-chrome-launcher": "~3.1.0",
    "karma-coverage-istanbul-reporter": "~2.1.0",
    "karma-jasmine": "~2.0.1",
    "karma-jasmine-html-reporter": "^1.4.2",
    "protractor": "~5.4.3",
    "ts-node": "~8.3.0",
    "tslint": "~5.18.0",
    "typescript": "~3.7.5"
  },
  "cordova": {
    "plugins": {
      "cordova-plugin-whitelist": {},
      "cordova-plugin-geolocation": {
        "GPS_REQUIRED": "true"
      },
      "cordova-plugin-background-geolocation": {
        "ALWAYS_USAGE_DESCRIPTION": "This app always requires location tracking",
        "MOTION_USAGE_DESCRIPTION": "This app requires motion detection"
      },
      "@globules-io/cordova-plugin-ios-xhr": {}
    },
    "platforms": [
      "ios"
    ]
  }
}

我怀疑是和Apple的WKWebView有关。因为这是我第一次真正在 Apple World 中开发某些东西,所以我感觉某些 Apple 安全功能正在阻止某些东西。

更新:

我做了 2 次额外检查:

  1. 为了检查是否有任何样式问题导致了问题,我删除了所有样式。但是,同样的问题。

  2. 为了在启动组件时检查动态数据是否真的从服务器获取,我添加了一个 console.log() 以在获取数据后打印数据。已正确获取,但屏幕未更新以显示数据。

更新 2:

从Angular9更新到Angular10也没有解决问题。

经过大量的反复试验和搜索,我找到了解决方案。

我在这里找到了提示:

https://github.com/angular/angular/issues/7381[1]

由于我还不完全理解的原因,应用程序似乎在对服务器的异步调用期间切换区域。因此 UI 更改机制未被触发,屏幕未更新。

通过将变量的更改包装到 NgZone.run()屏幕正确更新。

更新后的 .ts 文件

import { Component, OnInit, NgZone } from '@angular/core';

// ...

constructor(private service: SummaryService, private authService: AuthService, private zone: NgZone) { }

// ...

getMonthScore(): void {
    this.service.getScore('month').subscribe(score => {
      this.zone.run(() => {
        this.monthScore = score;
        this.createMonthGraph();
        console.log('GOT MONTH SCORE');
        console.log(score);
      });
      
    })
  }

只有在使用 Cordova 构建 iOS 应用程序时才需要这样做。似乎没有必要构建 Android 应用程序或使用浏览器。