Angular中PayPal按钮约50%概率加载失败求助
问题现象
- 按钮约50%概率无法加载,连续刷新30次出现9次成功→11次失败→10次成功的交替规律
- 测试环境与生产环境表现一致
失败时错误信息
TypeError: Cannot read properties of undefined (reading 'nativeElement')
at deposittest.component.ts:218:36
at _ZoneDelegate.invoke (zone.js:368:26)
at Object.onInvoke (core.mjs:26321:33)
at _ZoneDelegate.invoke (zone.js:367:52)
at Zone.run (zone.js:129:43)
at zone.js:1257:36
at _ZoneDelegate.invokeTask (zone.js:402:31)
at core.mjs:25998:55
at AsyncStackTaggingZoneSpec.onInvokeTask (core.mjs:25998:36)
at _ZoneDelegate.invokeTask (zone.js:401:60)
相关代码
组件TS文件
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; import { loadScript, PayPalNamespace } from "@paypal/paypal-js"; @Component({ selector: 'app-deposittest', templateUrl: './deposittest.component.html', styleUrls: ['./deposittest.component.css'] }) export class DeposittestComponent implements OnInit { depositState$: Observable<State<CustomHttpResponse<User>>>; private dataSubject = new BehaviorSubject<CustomHttpResponse<User>>(null); private isLoadingSubject = new BehaviorSubject<boolean>(false); isLoading$ = this.isLoadingSubject.asObservable(); DataState = DataState; private isLoggedInSubject = new BehaviorSubject<boolean>(false); isLoggedIn$ = this.isLoggedInSubject.asObservable(); PAYPAL_CLIENT_ID: string = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; myAmount: any @ViewChild('paypalRef', { static: false }) private paypalRef!: ElementRef; constructor(private notificationService: NotificationService, private router: Router, private userService: UserService) { } ngOnInit(): void { this.depositState$ = this.userService.depositInit$() .pipe( map(response => { console.log(response); this.dataSubject.next(response); return { dataState: DataState.LOADED, appData: response }; }), startWith({ dataState: DataState.LOADING }), catchError((error: string) => { return of({ dataState: DataState.ERROR, error }) }) ) this.renderPaypalButton(); } renderPaypalButton() { let orderRef: string | null = null; loadScript({ "clientId": this.PAYPAL_CLIENT_ID, currency: 'USD', disableFunding: 'paylater,credit,card,venmo', }) .then((loadedPaypal) => { if (loadedPaypal) { const paypal: PayPalNamespace = loadedPaypal; paypal.Buttons({ style: { layout: 'vertical', color: 'blue', shape: 'rect', label: 'paypal', }, createOrder: (data, actions) => { const postData = { amount: this.myAmount, state: this.state }; return fetch(AppSettings.API_ENDPOINT + '/user/create_paypal_transaction', { method: "post", headers: { 'Authorization': 'Bearer ' + localStorage.getItem(Key.TOKEN), 'Accept': 'application/json, text/plain, */*', 'Content-Type': 'application/json' }, body: JSON.stringify(postData) }) .then((response) => response.json()) .then((response) => { return response.data.order.id; }); }, onApprove: (data, actions) => { return fetch(AppSettings.API_ENDPOINT + `/user/paypal/capture_order/${data.orderID}`, { method: "post", headers: { 'Authorization': 'Bearer ' + localStorage.getItem(Key.TOKEN), 'Accept': 'application/json, text/plain, */*', 'Content-Type': 'application/json', }, }) .then((response) => response.json()) .then((response) => { this.paypalRedirect(response.data.order.status, orderRef); }); }, onCancel: () => { // }, onError: (err) => { this.notificationService.onError('Error processing deposit.'); this.paypalRedirect('ERROR', orderRef); } }).render(this.paypalRef.nativeElement); } else { console.error(""); } }) .catch((error) => { console.error("", error); }); } paypalRedirect(status: string, orderRef: string | null) { switch (status) { case 'COMPLETED': if (orderRef) { this.notificationService.onDefault('Payment Successful.'); this.router.navigate(['/paypal/result/success/']); } else { console.error("Missing success redirect"); } break; case 'ERROR': if (orderRef) { this.notificationService.onError('Payment could not be completed.'); this.router.navigate(['/']); } else { console.error("Missing failure redircte"); } break; default: console.error("redirection."); break; } } }
组件HTML文件
<ng-container *ngIf="(depositState$ | async) as state" [ngSwitch]="state.dataState"> <ng-container *ngSwitchCase="DataState.LOADED"> <app-navbar [user]="state?.appData?.data?.user"></app-navbar> <section> <div id="payment" class="mt-3"> <div class="col-md-12"> <div class="form-group"> <div style="font-weight: bold;">Name on file: {{ state?.appData?.data?.user.firstName+' '+state?.appData?.data?.user.lastName}} </div> <div class="mt-2" style="font-style: italic;font-size: 1rem;">Your name on file must match the name associated with your form of payment.</div> </div> </div> <div id="input" class="form-group mt-3"> <label style="font-size: .8rem;" class="me-2">Enter Amount: $10 - $999</label> <input type="text" (keypress)="decimalFilter($event)" placeholder="Enter amount here" ngModel name="myAmount" (ngModelChange)="myAmount=$event" value="{{myAmount }}" class="form-control mb-2"> </div> <div #paypalRef></div> </div> </section> </ng-container> <ng-container *ngSwitchCase="DataState.LOADING"> <div>Loading...</div> </ng-container> <ng-container *ngSwitchCase="DataState.ERROR"> <div>{{ state.error }}</div> </ng-container> <app-authfooter></app-authfooter> </ng-container>
核心原因
错误指向this.paypalRef.nativeElement未定义,本质是PayPal脚本加载完成时,#paypalRef元素还未被Angular渲染。因为#paypalRef仅在DataState.LOADED状态下才会被渲染,而当前在ngOnInit中直接调用renderPaypalButton(),脚本加载的异步过程与组件数据加载的异步过程未同步,谁先完成完全随机,导致一半概率元素未渲染就被访问。
修复方案
方案1:数据加载完成后再渲染按钮(优先推荐)
将renderPaypalButton()的调用移到depositState$数据流的LOADED状态回调中,确保元素已渲染:
ngOnInit(): void { this.depositState$ = this.userService.depositInit$() .pipe( map(response => { console.log(response); this.dataSubject.next(response); // 数据加载完成,此时#paypalRef已渲染,调用按钮渲染 this.renderPaypalButton(); return { dataState: DataState.LOADED, appData: response }; }), startWith({ dataState: DataState.LOADING }), catchError((error: string) => { return of({ dataState: DataState.ERROR, error }) }) ) // 移除原ngOnInit中的this.renderPaypalButton(); }
方案2:添加元素存在性检查(备选)
如果方案1仍有偶发问题,可在renderPaypalButton中先检查元素是否存在,不存在则延迟重试:
renderPaypalButton() { // 先检查元素是否存在,不存在则延迟重试 if (!this.paypalRef?.nativeElement) { setTimeout(() => this.renderPaypalButton(), 100); return; } // 原有的loadScript逻辑... }
额外优化点
- 在
renderPaypalButton的异常分支添加具体错误信息,方便排查:.catch((error) => { console.error("PayPal脚本加载失败:", error); }); // ... } else { console.error("PayPal脚本未成功加载"); } - 添加标志位
isPaypalRendered,防止多次调用renderPaypalButton导致重复创建按钮。
内容的提问来源于stack exchange,提问作者T M

