不Shawn樣的部落格

寫寫程式相關的學習筆記

0%

最近在寫功能的時候,常常會把一長串的程式封裝成幾個function,有的如果剛好裡面有需要await非同步的東西就需要再把該function包成async function,但就讓我產生一個疑問,究竟這樣多層的async await,在外層的catch能不能接到內層吐的error呢?

Demo Code

先把結論寫在前面

  1. 答案是可以的。
  2. 發現另一件事,如果內層的async funciton本身已經有catch error掉了,外層的catch是不會接到error的,且外層後面的程式還是可以繼續往下走!!

實驗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function promise1() {
return new Promise((res, rej)=>{
res('resolvePromise1')
})
}

function promise2() {
return new Promise((res, rej) => {
setTimeout(()=>{
rej('rejectPromise2')
},5000)
})
}

function promise3(){
return new Promise((res, rej) => {
setTimeout(()=>{
rej('rejectPromise3')
},5000)
})
}

async function awaitPromise1AndTestPromise1AndPromise2() {
try {
const resultPromise1 = await promise1()
console.log(resultPromise1)
const resultPromise2 = await awaitPromise2()
const resultPromise3 = await awaitPromise3()
} catch(error){
console.log('外層catch=>', error)
}
}

async function awaitPromise2() {
try {
return await promise2()
} catch(error) {
console.log('內層catch=>', error)
}
}

async function awaitPromise3() {
return await promise3()
}

這邊大概說明一下整個code:(但我覺得不用看說明,可能直接看code比較好懂)

  1. 建立了三個promise,promise1只回resolve,promise2跟promise3只回reject,而重點會是在後兩者,因為我就是要看多層async function的try catch是怎麼接到error的。

  2. 建了三個async function,awaitPromise1AndTestPromise1AndPromise2 > 拿來await另兩支async functionawaitPromise2 > 用來await promise2,並且自己有個catch在接errorawaitPromise2 > 用來await promise3,但自己沒有個catch在接error

  3. 接著整份code跑出來結果會是下面截圖這樣,3-a. 當呼叫awaitPromise1AndTestPromise1AndPromise2後,3-b. 程式執行到await awaitPromise2()時,因為他本身就有自己的try catch,所以錯誤訊息先在內層被印出來了,也就會印出內層catch=> rejectPromise2這段字樣,從這部分就可以知道原來內層catch如果把error接到了,外層的catch就不會接到error,並能把後面的程式繼續執行。(但其實我在工作寫功能的時候,我幾乎都是會統一在最外層catch接error就是了,因為我不會希望某個await async funtion沒執行成功,但卻又執行另一個await async function)3-c. 程式不會因為前面的內層catch接到了error導致外層沒有執行,而是會繼續執行下去,所以執行到await awaitPromise3()時,外層的catch就會接到另一個error,也就會印出外層catch=> rejectPromise3這段字樣,從這部分就可以知道原來外層catch是可以接到另一個async function吐出來的error的。

https://stackblitz.com/edit/catcherror-for-another-asyncfunction?embed=1&file=index.js

使用dialogflow SDK一直報錯UNAUTHENTICATED

最近在寫LINE chatbot並搭配dialogflow來判斷使用者輸入文字意圖,在本地跑是正常的可以獲得detectIntent後的回應,但是在串dialogflow的時候就一直出現UNAUTHENTICATED,查了半天終於找到原因,是private_key讀取有問題。
herokuo-config-variable-for-dialogflow-privatekey

原來是heroku對於環境變數中\n的判斷會有問題

不過我去查本地環境變數設的private_key和Heroku環境變數設的private_key是一樣的,再輾轉查到了一篇才知道是因為PRIVATE_KEY本身都是長大概這樣----BEGIN PRIVATE KEY-----\nMY-PRIVATE-KEY\n-----END PRIVATE KEY-----\n,而裡面的\n字元無法被判斷是什麼,所以就要經過(process.env.DIALOGFLOW_PRIVATE_KEY as string).replace(/\\n/g, '\n')去處理,最後也終於可以正常串接dialogflow了。

1
2
3
4
5
6
7
8
9
// dialogflow
const languageCode = 'zh-TW';
const projectId = 'my-line-chatbot-yutq';
const credentials = {
client_email: process.env.DIALOGFLOW_CLIENT_EMAIL,
private_key: (process.env.DIALOGFLOW_PRIVATE_KEY as string).replace(/\\n/g, '\n')
}

const sessionClient = new dialogflow.SessionsClient({ projectId, credentials });

參考資料:https://stackoverflow.com/questions/39492587/escaping-issue-with-firebase-privatekey-as-a-heroku-config-variable/41044630

Demo Code

最近在寫一個多步驟的問卷功能,而問卷其中一個步驟的其中一題是必填,但該題是多選又要限制使用者最多只能選三個選項,上網查了有點久原來可以用fomArray來處理。

一、使用mat-stepper建立步驟的驗證

1.import要用到的幾個套件(記得在ngModule的imports要記得引入)

1
2
3
4
5
6
// app.module.ts
import { FormsModule } from '@angular/forms';
import { ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'

2.建立template(裡面有一些變數到後面會提到)

這個部分不特別說明了,就是建立三個步驟的問卷,而我把主要要demo用的複選題放在了步驟1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// app.component.html
<mat-horizontal-stepper [linear]="isLinear" #stepper>
<mat-step [stepControl]="firstFormGroup">
<form [formGroup]="firstFormGroup">
<ng-template matStepLabel>Fill out your name</ng-template>
<mat-form-field>
<mat-label>Name</mat-label>
<input matInput placeholder="Last name, First name" formControlName="firstCtrl" required>
</mat-form-field>
<div *ngFor="let option of a1OptionsToArray; let i=index">
<input type="checkbox" [value]="option" (change)="[a1CheckboxChange($event),a1OptionLimitTo3()]"
[disabled]="a1Disabled && !a1OptionsToObject[option]['checked']">
{{a1OptionsToObject[option]['name']}}
</div>
<div>
<button mat-button matStepperNext>Next</button>
</div>
</form>
</mat-step>
<mat-step [stepControl]="secondFormGroup" label="Fill out your address">
<form [formGroup]="secondFormGroup">
<mat-form-field>
<mat-label>Address</mat-label>
<input matInput formControlName="secondCtrl" placeholder="Ex. 1 Main St, New York, NY"
required>
</mat-form-field>
<div>
<button mat-button matStepperPrevious>Back</button>
<button mat-button matStepperNext>Next</button>
</div>
</form>
</mat-step>
<mat-step>
<ng-template matStepLabel>Done</ng-template>
<p>You are now done.</p>
<div>
<button mat-button matStepperPrevious>Back</button>
<button mat-button>Reset</button>
</div>
</mat-step>
</mat-horizontal-stepper>

3. 建立formGroup並設定好要驗證的欄位

雖然有三個步驟,但只有前面兩個步驟需要欄位驗證,所以建立兩個formGroup,其中有發現還多了個a1CheckedArray比較特別,等於是在formGroup下建了一個a1CheckedArray欄位且必須驗證是否任何一個選項有被勾選了才能驗證通過。

1
2
3
4
5
6
7
8
9
10
// app.component.ts
ngOnInit() {
this.firstFormGroup = this._formBuilder.group({
firstCtrl: ['', Validators.required],
a1CheckdArray: this._formBuilder.array([], Validators.required),
})
this.secondFormGroup = this._formBuilder.group({
secondCtrl: ['', Validators.required]
});
}

二、透過typescript的interface導入問卷題目,並且轉成後續要用的陣列跟物件

因為問卷的選項未來可能會常常改,所以選項的部分不是寫死在HTML裡面,而是寫在interface上方便未來維護,這一段可能要花點時間理解,會有點繞喔~

1.建立interface並寫兩個get方法,分別取用interface這個物件的keys跟values給後面要用

1
2
3
4
5
6
7
8
9
10
11
// app.component.ts
enum OrderA1 {
opt1 = '舒適性',
opt2 = '續航力',
opt3 = '價格經濟',
opt4 = '外型亮眼',
opt5 = '其他',
}

get OrderA1Keys() { return Object.keys(OrderA1) }
get OrderA1() { return Object.values(OrderA1); }

2.把從interface拿到的陣列存到this.a1OptionsArray這個變數中

this.a1OptionsArray最後會是長這樣: [‘op1’,op2,…]這會用在HTML那邊要透過ngFor列印時,塞value到每個選項的input使用,並且也用於要比對該選項是否有被checked是否需要disabled時使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// app.component.ts
a1OptionsToArray: Array<string>;
a1OptionsToObject: {
[optionKey: string]: { name: string, checked: boolean }
} = {};

ngOnInit() {
this.a1OptionsToArray = this.OrderA1Keys;
this.a1OptionsToArray.forEach(
optionKey => {
this.a1OptionsToObject[optionKey] = { name: OrderA1[optionKey], checked: false }
}
)
}

3.再把這個陣列取出來加工,一個一個搭配checked屬性存到this.a1OptionsObject這個物件中。

this.a1OptionsObject會是以原本的keys繼續當作key值,去組成新的物件,最後會是長這樣:{op1: {name:’舒適性’, checked: false}, op2: {name:’舒適性’, checked: false}…}這會用在兩個地方,HTML那邊來比對該選項是否有被checked是否需要disabled時使用;在ts邊則是用在比對出中文字存到formArray裡面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// app.component.ts
a1OptionsToArray: Array<string>;
a1OptionsToObject: {
[optionKey: string]: { name: string, checked: boolean }
} = {};

ngOnInit() {
this.a1OptionsToArray = this.OrderA1Keys;
this.a1OptionsToArray.forEach(
optionKey => {
this.a1OptionsToObject[optionKey] = { name: OrderA1[optionKey], checked: false }
}
)
}

三、建立a1CheckboxChange函式把選擇的選項結果直接轉成乾淨的字串陣列

1.透過此一函式去獲得HTML那邊傳進來的value

這個vlaue其實就會是a1OptionsToArray的op1或op2或op3…

2.判斷傳進來的是不是有被勾選checked了

特別補充說明一下,這傳進來的checked跟我們前面在建立物件時對應的checked不一樣喔,他這個checked就是HTML自己原生的屬性checked,不要搞混了。

3.如果被勾選了,才會去把value丟進去前面建立的a1OptionsToObject物件比對出中文字,並塞到a1CheckdArray,到時候輸出整個formgroup的時候就會看到這一欄是可以方便取用的中文字陣列了。

同時也會把該選項在物件內的checked寫成true,這樣HTML那邊的diabled才知道甚麼時候要把其他選項鎖起來。

4.如果沒有被勾選,就會拿出a1CheckdArray去跟現在送進來取消勾選的選項本身的中文字逐一做比對,如果有比對到第幾次,就相對能推算出相同的中文字是在陣列中的第幾位,就能順勢將a1CheckdArray裡的中文字刪除掉了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// app.component.ts
a1CheckboxChange(event) {
const a1CheckdArray: FormArray = this.firstFormGroup.get('a1CheckdArray') as FormArray;
const value = event.target.value;
if (event.target.checked) {
a1CheckdArray.push(new FormControl(this.a1OptionsToObject[value]['name']));
this.a1OptionsToObject[value]['checked'] = true;
} else {
let i = 0;
a1CheckdArray.controls.forEach((item: FormControl) => {
if (item.value === this.a1OptionsToObject[value]['name']) {
a1CheckdArray.removeAt(i);
this.a1OptionsToObject[value]['checked'] = false;
return;
}
i++;
});
}
}

四、建立a1OptionLimitTo3限制最多只能選三個選項

這邊概念就簡單多了,就是直接驗證前面建立的那個formGroup裡面那個a1CheckArray陣列裡面有被塞的值是不是已經滿三個了,如果是的話就會把整個欄位鎖起來,才能使其他本來在物件裡的checked是false的選項都被鎖起來無法選。

1
2
3
4
5
6
7
8
// app.component.ts
a1OptionLimitTo3() {
if (this.firstFormGroup.get('a1CheckdArray').value.length === 3) {
this.a1Disabled = true;
} else {
this.a1Disabled = false;
}
}

以上說明如果看不懂,可能直接去拿我stackblitz上的那個code去操作會比較容易理解XD

我有在Cloud Functions上寫了一支OnRequest的API,要讓我的網頁去呼叫,並且會回傳Excel檔案讓我下載,可是一開始就出現了CORS問題,最後又出現HttpErrorResponse問題,整整花了一個下午才解決。

但這是我第一次寫OnRequest的API,以前functions上都是架OnCall API,或在Cloud Run上面架API。

解決CORS⇒以Axios解決

CORS

一開始以為是cloud functions那邊CORS沒寫好,但其實我有裝CORS套件,在我的API裡面也有寫res.set()。

1
2
3
4
5
6
7
8
// index.ts
import * as cors from 'cors';
const app = express();
app.use(cors({ origin: true }));

// 在我的API上也有寫res.set()
res.set('Access-Control-Allow-Origin', '*');
res.set('Access-Control-Allow-Methods', 'POST');

而我一開始也都是用HttpClient去呼叫。

1
2
3
4
5
6
7
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';

await this.httpClient.post(
`https://xxx.com/download_something`,
{
name: 123
}).toPromise();

最後轉換思考方向,改用Axios來呼叫API,就沒有CORS問題了,如下。

1
await this.ajaxPost('https://xxx.com/download_something',{name:123})

有解決CORS問題,但還是沒辦法下載Excel,報HttpErrorResponse⇒以form.submit()解決

雖然CORS解決了,可是還是報HttpErrorResponse,如下圖。

httpErrorResponse

這時候想到以前用form.submit()呼叫onRequest function去下載Excel,都可以順利下載到檔案,那就乾脆改用form.submit()吧。

最後就成功了,可以順利下載Excel!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function submitForm(url: string, data: any) {
const formName = 'DynamicForms';

const form = document.createElement('FORM') as HTMLFormElement;
form.name = formName;
form.action = url;
form.method = 'POST';

const formInput: HTMLInputElement[] = [];

let count = 0;
_.forEach(data, (item, name) => {
formInput[count] = document.createElement('INPUT') as HTMLInputElement;
formInput[count].type = 'HIDDEN';
formInput[count].name = name;
formInput[count].value = item.toString();
form.appendChild(formInput[count]);
++count;
});

document.body.appendChild(form);
form.submit();
document.querySelector(`form[name=${formName}]`).remove();
}

this.submitDynamicForm('https://xxx.com/download_something',{name: 123})

補充說明: 後端如何產出Excel檔案

怕忘記,紀錄一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import xlsx from 'node-xlsx';	

res.set('Access-Control-Allow-Origin', '*');
res.set('Access-Control-Allow-Methods', 'POST');

const titleList = 每欄標題的陣列資料;
const titleWidthList = 寬度陣列資料;

const dataList = 這個是雙重陣列資料,就是陣列裡面的每個元素又都是陣列;
const fileName = 下載時的檔案名稱;
const options = { '!cols': titleWidthList.map(o => { return { wch: o } }) };
const result = xlsx.build([{ name: 'sheet', data: [titleList, ...dataList] }], options);

res.setHeader('Content-Type', 'application/vnd.openxmlformats');
res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`);
res.end(result, 'binary');

以前開發前端頁面的時候,都會建兩個hosting,一個是正式環境、一個是測試環境像下面這樣。

但現在不用這麼麻煩了,只需要建立一個hosting,至於想要在同一個hosting下產出多少個測試環境都沒關係都不會影響到正式環境,因為他會依據我們建立的每個測試環境再產出一個全新的網址出來。

會發現這個是因為最近在幫客戶改功能,但我們PM把正式環境網址跟測試環境網址都提供給客戶了,所以需要再建一個hosting作為真正的內部開發環境來用,可是還需要改個firebase.json.firebaserc,因此也在無意間發現了Firebase Hosting Preview Channel這個不曾用過的功能。

firebase_hosting

Firebase Hosting Preview Channel

詳細官方文件可以看這,以前我們直接deploy上去hosting的時候,都是在Live Channel上,但如下圖現在我們可以deploy很多個Preview channel,每個Preview channel都會有獨立的網址可以用,甚至可以限制該網址的有效期限。

firebase_hosting_preview_channel_structure

下channel指令部署到特定的hosting之下

1
firebase hosting:channel:deploy --only TARGET_NAME CHANNEL_ID --expires 1d
  • TARGET_NAME: 看你原本部署在你要的hosting都是下什麼TARGET_NAME,那就採用TARGET_NAME即可。
  • CHANNEL_ID: 取一個自己想要的名字就好,這個會出現在最後產出的新網址上。
  • —expires 1d: 1d就是一天,看要設個12h, 7d, 2w都可,就算現在設完想要改也可以直接去firebase hosting後台改。

部署完成

部署完後就會拿到新網址,https://aler-dev–[bbb]-[ccc].web.app

  • aler-dev: 就是原本部署指定的TARGET_NAME所指向的hosting
  • bbb: 就是CHANNEL_ID,也就是preview
  • ccc: 自動產的亂數隨機碼

hosting後台也可以看到多了一個channel如下圖。
preview_channel_success

因為需要寫一個頁面,裡面會有個按鍵按下後,會跳出一個彈窗就包著簽名板,該簽名板上會提供確認送出以及清除的功能。

基本有建立了兩個元件,一個是demopage,一個是signaturepad。
就是在demopage元件寫一個打開簽名板的按鍵,該按鍵按下去後,就會透過MatDialog以pop-up方式打開sinaturepad元件。

demo連結:https://stackblitz.com/edit/angular-signaturepad?file=src%2Fapp%2Fdemopage%2Fdemopage.component.html

(打開後會發現套件會報錯,但如果把code弄到自己的angular專案上是可以成功運行的,所以這個demo連結就當作放code的地方就好,沒有實際demo的效果。)

在demopage上

寫一個按鍵透過MatDialog以pop-up方式打開signaturepad元件

如前所述,我的需求是在既有頁面上以pop-up方式開一個簽名板。

將圖檔從base64轉成blob

之所以要轉成blob是因為我要上傳到firebase storage上時不能是base64,所以當signaturepad元件關閉時,他送過來我這裡的會是base64檔,因此必須使用ngx-image-cropper裡面的base64ToFile來轉成blob檔。

程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// html
<button (click)="openSignatureDialog()">Open SignaturePad</button>

// javascript
import { Component, OnInit } from "@angular/core";
import { MatDialog, MatDialogConfig } from "@angular/material/dialog";
import { base64ToFile } from "ngx-image-cropper";
import { SignaturepadComponent } from "../signaturepad/signaturepad.component";

@Component({
selector: "app-demopage",
templateUrl: "./demopage.component.html",
styleUrls: ["./demopage.component.css"]
})
export class DemopageComponent implements OnInit {
constructor(public dialog: MatDialog) {}

ngOnInit() {}

openSignatureDialog() {
const dialogConfig = new MatDialogConfig();
dialogConfig.disableClose = false;

const dialogRef = this.dialog.open(SignaturepadComponent, dialogConfig);
// 接收來自signaturepad元件emit出來的資料
dialogRef.afterClosed().subscribe(async (imageDataBase64: string) => {
// 轉成blob檔並用新增的service上傳成圖檔
const blob = base64ToFile(imageDataBase64);
console.log(blob);
});
}
}

在signaturepad元件上

引入angular2-signaturepad的SignaturePad

設定好簽名板的大小

簽名板本身的基底其實就是html的canvas畫布,至於畫布的大小就看要設多大多小?
是在signaturePadOptions輸入參數設定。

在ngAfterViewInit

這裡就幾乎都是按照官方說的了

不過比較重要的是在drawComplete函式時,我有特別把畫布上的圖存成另一個變數this.imageDataBase64,方便我要傳給demopage元件時用。

程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// html
<div class="closeBtn" (click)="closeDialog()">X</div>

<h2>簽名板</h2>
<div class="board">
<signature-pad #signatureCanvas [options]="signaturePadOptions" id="signatureCanvas" (onBeginEvent)="drawStart()"
(onEndEvent)="drawComplete()">
</signature-pad>
</div>

<div class="stepperA text-align-center buttonBlock">
<button mat-button (click)="emitToFather()" class="prevStep">確認</button>
<button (click)="clear()" class="clear">清除</button>
</div>

// javascript
import { Component, ViewChild, AfterViewInit } from "@angular/core";
import { SignaturePad } from "angular2-signaturepad/signature-pad";
import { MatDialogRef } from "@angular/material/dialog";

@Component({
selector: "app-signaturepad",
templateUrl: "./signaturepad.component.html",
styleUrls: ["./signaturepad.component.css"]
})
export class SignaturepadComponent implements AfterViewInit {
public imageDataBase64: string;
public signaturePadOptions: object = {
// passed through to szimek/signature_pad constructor
minWidth: 1,
canvasWidth: 800,
canvasHeight: 400,
backgroundColor: "rgb(255, 255, 255)",
penColor: "rgb(0, 0, 0)"
};
@ViewChild("signatureCanvas", { static: true }) signaturePad: SignaturePad;

constructor(private dialogRef: MatDialogRef<SignaturepadComponent>) {}

// 以下全都是針對手寫板寫的
ngAfterViewInit() {
// this.signaturePad is now available
// this.signaturePad.set('minWidth', 1); // set szimek/signature_pad options at runtime
if (window.innerWidth <= 768) {
this.signaturePad.set("canvasWidth", window.innerWidth - 40);
this.signaturePad.set("canvasHeight", window.innerHeight - 250);
} else {
this.signaturePad.set("canvasWidth", 800);
this.signaturePad.set("canvasHeight", 400);
}
this.signaturePad.clear(); // invoke functions from szimek/signature_pad API
}

drawComplete() {
this.imageDataBase64 = this.signaturePad.toDataURL("image/jpeg");
// will be notified of szimek/signature_pad's onEnd event
}

drawStart() {
// will be notified of szimek/signature_pad's onBegin event
console.log("begin drawing");
}

clear() {
this.signaturePad.clear();
this.imageDataBase64 = "";
}

emitToFather() {
this.dialogRef.close(this.imageDataBase64);
}

closeDialog() {
this.dialogRef.close();
}
}

* 一直遇到簽名檔無法清除的問題

這個查了有點久,一直刪不掉簽名檔,最後發現其他人有提到要改寫法
而我提供的demo code則是已經按照他們說的改好了,也確實可行喔。

在app.module上

要記得引入angular2-signaturePad。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.
.
.
import { SignaturePadModule } from "angular2-signaturepad";

@NgModule({
imports: [
.
.
.
SignaturePadModule
],
.
.
.
})

最近在串Webex直播,但發現iOS 13以下(包含13)就都沒辦法顯示視訊畫面,所以必須判斷13以下的都要另外跳另外的alert,問了Google大神,看了幾篇終於找到以下這個寫法去判斷iOS版本。

基本上就是透過navigator.appVersion拿到使用者的裝置資料,而iOS的版本號都是3個數字組成,像是iPhone OS 4_3_3,所以必須用正則表示式把這三個數字切出來,至於我們要判斷的其實只要判斷第一個數字即可。

而navigator.platform那一段我先註解掉了,因為我不需要先判斷他是不是iPhone或iPod或iPad,我只要確定他的appVersion是OS開頭的即可。

參考連結: https://stackblitz.com/edit/angular-detect-ios-version-is-14

function iOSversion() {
  // if (/iP(hone|od|ad)/.test(navigator.platform)) {
  // iOS version will be like iPhone OS 4_3_3, you will get three digits
  var v = navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/);
  return [
    parseInt(v[1], 10),
    parseInt(v[2], 10),
    parseInt(v[3] || (0 as any), 10)
  ];
  // }
}

const ver = iOSversion();

if (ver[0] >= 14) {
  alert("iOS version is" + ver);
} else {
  alert("iOS version less than 14");
}

因為先前有個新專案快要上線,PM就開始猛測試,發現某個表單用了iPhone的Safari去送出就會失敗,但Android手機的其他人用Chrome卻可以送出表單。

其實只要是Safari,手機遇到的問題,Mac上也會有

不過我們沒有iPhone的轉接線可以轉接到電腦上看到底是報了什麼錯,只好不斷改code不斷寫alert不斷部署上去,用iPhone看看alert出來的資料,最後發現有個日期的部分印出來是NaN,而該日期是透過new Date()產生的,因此就開始往new Date()跟Safari的方向研究。

(題外話,其實不用那麼蠢的說…因為我一開始聽信PM說Mac上的Safari沒問題,唯獨iPhone的Safari有問題,我只好一直不斷部署上去再用iPhone去測,最後當我找到問題時我才想到用一下Mac上的Safari去測一下,發現其實也是有問題的,如果我早點用Mac的Safari測,就可以馬上看到報錯是什麼,免去我抓問題抓那麼久Q”Q)

慎用new Date()

底下開始正式說明問題。我的需求是希望塞一個特定的日期時間進去new Date(),去產出一個我想要的date像是這樣new Date('2013-05-13 03:00'),但這樣的寫法在Chrome或其他瀏覽器上產出來的值確實是正確的date格式。

可到了Safari上就會轉不過去了,上網查到這篇,必須要寫成new Date('2013-05-13T03:00:00Z'),Safari才會順利讓new Date()產出我要的date格式。

Safari對於new Date()使用較嚴格的感覺

總之是Safari比較嚴格,會需要你輸入new Date()裡面輸入的值要非常完整,至於Chrome之類其他的瀏覽器就沒那麼嚴格,輸入大致的時間格式就可以清楚幫你轉換了。

結論:日後有日產出特定date格式的需求,盡量以moment套件來做

但其實要補中間的T跟後面的Z我覺得有點麻煩,最後是直接以moment套件寫moment('2013-05-13 03:00').toDate(),去產出我要的date格式,而以moment套件產出的date經實測確實可以讓Safari或Chrome等等瀏覽器都可以正常運行。