angular 6 使用http时未定义函数

angular 6 undefined function when using http

我正在尝试通过 api(我自己写的,它有效)从我的数据库中获取一个帐户对象(在 json 中)。服务中与 api 交互的方法有效。这是 api 交互服务的代码:

import { Injectable } from '@angular/core';
import {HttpClient, HttpHeaders} from '@angular/common/http';
import {Account} from './classes/account';
import {Observable} from 'rxjs';

@Injectable({
    providedIn: 'root'
})
/* get from database:
Address: "awdawda"
CheckedInCamping: 0
CheckedInEvent: 0
DateOfBirth: "2000"
Email: "test@c.com"
Gender: "male"
Name: "firstname"
Password: "123456"
Phone: "+3161234567"
RFID: null
TicketId: 2
*/
export class ApiInteractionService {

private URLgeneral: string = 'http://local.propapi.com/api/';
constructor(private http: HttpClient) {
}

public getAccount(email: string, password: string): any { //Observable<Account>
    this.http.get<Account[]>(this.URLgeneral + 'account/' + email + '/' + password).subscribe(a => {
        if (a[0] == null){
            console.log('(getAccount() - api service) got no data');
            return null;
        }
        else {
            console.log('(getAccount() - api service) data found:');
            console.log(a[0]);
            return a[0]; //returning the observable
        }
    });
}

public postAccount(a: Account){
    console.log('(getAccount() - api service) before api post');
    return this.http.post(this.URLgeneral, a);
}

}

这个方法没有问题。这可能不是最好的方法,但没关系。授权并不重要,安全性也不重要。 (学校项目)

我在 loginService 中使用 getAccount 方法:

import {Injectable} from '@angular/core';
import {Account} from './classes/account';
import {ApiInteractionService} from './api-interaction.service';

@Injectable({
    providedIn: 'root'
})
export class LoginService {

constructor(private api: ApiInteractionService) {
}

public logIn(email, password){
    this.api.getAccount(email, password).subscribe(a => {
        if (a == undefined){ //no account found
            console.log('(logIn() - loginService) passed account was null');
            return a; //should be null
        }
        else { //account found. setting sessionstorage and reloading the page
            sessionStorage.setItem('account', JSON.stringify(a)); //set session variable
            window.alert('Logged in as ' + a.name + '.');
            location.replace('/landing');
            return a;
        }
    });
}

public logOut(): void{
    sessionStorage.removeItem('account'); //remove session variable with key 'account'
    window.alert('Logged out.');
    location.reload();
    }
}

此方法从 component.ts 调用,数据来自表单:
登录。component.ts:

import {Component, OnInit} from '@angular/core';
import {LoginService} from '../login.service';
import {Account} from '../classes/account';

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

email: string = 'test@c.com';
password: string = '123456';

// email: string;
// password: string;

constructor(private LoginService: LoginService) { }

ngOnInit() {
}

logIn(){
    return this.LoginService.logIn(this.email, this.password);
  }
}

login.component.html:

    <div id="login" class="container-fluid text-center pt-5 text-light"> <!--main container-->

<div id="loginitems" class="w-25 flex-wrap flex-column rounded mx-auto p-3"> <!--div containing the items-->

    <h1 class="mb-5">Log in</h1> <!--title-->

    <!--region login form-->
    <div class="d-flex py-2 px-3 w-100 mx-auto">

          <span class="flex-column d-flex w-100">
            <label class="my-2 form-row w-100">Email: <input class="ml-auto rounded border-0 bg-dark text-light p-1" type="email" (keydown.enter)="logIn()" [(ngModel)]="email" name="email"> </label> <!--email input-->
            <label class="my-2 form-row mb-5 w-100">Password: <input class="ml-auto rounded border-0 bg-dark text-light p-1" type="password" (keydown.enter)="logIn()" [(ngModel)]="password" name="password"></label> <!--password input-->
            <button class="flex-row mx-auto w-50 mt-5 btn-dark border-0 text-light py-2 rounded" (click)="logIn()">Log in</button> <!--log in button-->
          </span>

    </div>
    <!--endregion-->

    <!--region registration shortcut-->
    <div class="mt-4">
        <label>Don't have an account yet? Register <a routerLink="/register">here</a>!</label>
    </div>
    <!--endregion-->

</div>

当尝试单击按钮以使用数据登录时(在数据库中 100% 确定)它在 Web 控制台中给出了一个错误,但在错误之后我记录了它收到的帐户,这是正确的数据:

ERROR TypeError: "this.api.getAccount(...) is undefined"  
logIn http://localhost:4200/main.js:1093:9  
logIn http://localhost:4200/main.js:1180:16  
View_LoginComponent_0 ng:///AppModule/LoginComponent.ngfactory.js:109:23  
handleEvent http://localhost:4200/vendor.js:43355:41  
callWithDebugContext http://localhost:4200/vendor.js:44448:22  
debugHandleEvent http://localhost:4200/vendor.js:44151:12  
dispatchEvent http://localhost:4200/vendor.js:40814:16  
renderEventHandlerClosure http://localhost:4200/vendor.js:41258:38  
decoratePreventDefault http://localhost:4200/vendor.js:59150:36  
invokeTask http://localhost:4200/polyfills.js:2743:17  
onInvokeTask http://localhost:4200/vendor.js:36915:24  
invokeTask http://localhost:4200/polyfills.js:2742:17  
runTask http://localhost:4200/polyfills.js:2510:28  
invokeTask http://localhost:4200/polyfills.js:2818:24  
invokeTask http://localhost:4200/polyfills.js:3862:9  
globalZoneAwareCallback http://localhost:4200/polyfills.js:3888:17  
LoginComponent.html:13:16  
ERROR CONTEXT   
Object { view: {…}, nodeIndex: 22, nodeDef: {…}, elDef: {…}, elView: {…} }  
LoginComponent.html:13:16  
(getAccount() - api service) data found: api-interaction.service.ts:35:16
{…}  

Address: "awdawda"
CheckedInCamping: 0
CheckedInEvent: 0
DateOfBirth: "2000"
Email: "test@c.com"
Gender: "male"
Name: "firstname"
Password: "123456"
Phone: "+3161234567"
RFID: null
TicketId: 2
<prototype>: Object { … }

为什么会出现此错误?

您的问题是您在 ApiInteractionService.getAccount() 内订阅。事实上,这个函数根本 return 不是一个 Observable,它 return 最初什么都不是,因为你所有的 return 语句都在 .subscribe() 中。因此 this.api.getAccount(),它显然期待一个 Observable 而不是什么都没有 returned 并且是未定义的。

几点建议:

  • 使用 Typescript 的类型检查。您已在行中将其注释掉:

    public getAccount(email: string, password: string): any { //Observable<Account>
    

    并将类型替换为 any,有效地关闭了 TypeScript 为您所做的所有良好的类型检查。如果你把它留在里面,那么 TypeScript 会给你一个错误,比如 "A function whose declared type is neither 'void' nor 'any' must return a value.",告诉你这个函数不是 returning 任何东西,更不用说一个 Observable 了。我建议你重构为这样的东西:

    public getAccount(email: string, password: string): Observable<Account> { //
        return this.http.get<Account[]>(this.URLgeneral + 'account/' + email + '/' + password).pipe(
            map(a => {
                if (a[0] == null){
                    console.log('(getAccount() - api service) got no data');
                    return null; // mapping resultant observable value to null
                }
                else {
                    console.log('(getAccount() - api service) data found:');
                    console.log(a[0]);
                    return a[0]; //mapping the resultant observable return value to the first array element
                }
            })
        );
    }
    

    请注意,这 return 是 http.get() 的结果,它是一个 Observable。另请注意,此功能将不再立即执行任何操作。它将 return 一个 Observable,需要订阅它才能真正做任何工作...

  • 您正在(再次)订阅您的 LoginService.login() 方法。请注意,通常在服务中订阅 Observable 是一个坏主意……如果您发现自己这样做,您应该认真考虑这是否是您真正想要做的。也许这就是你希望在这种情况下做的,但从概念上讲我会重新考虑这一点。毕竟,当此 Observable 完成其异步工作并执行某些操作(例如使用某些信息更新视图)时,您可能希望通知该组件。如果该假设是正确的,那么您可能还想重构此函数以继续 returning Observable 而无需订阅,如下所示:

    public logIn(email, password) :Observable<Account>{
        return this.api.getAccount(email, password).pipe(
            map(a => {
                if (a == undefined){ //no account found
                    console.log('(logIn() - loginService) passed account was null');
                    return a; //should be null
                }
                else { //account found. setting sessionstorage and reloading the page
                    sessionStorage.setItem('account', JSON.stringify(a)); //set session variable
                    window.alert('Logged in as ' + a.name + '.');
                    location.replace('/landing');
                    return a;
                }
            })
        );
    }
    

    但是,现在我注意到您的逻辑在这两种情况下都只是 returns 'a',没有进行任何转换,所以您真正关心的只是打开警报的副作用window。顺便说一句 - 我真的很怀疑 LoginService 是否是您想要显示该警报的地方,但我不会重构它。 :) 这意味着 map 可以简单地替换为 tap 并删除不必要的 return,如下所示:

    public logIn(email, password) :Observable<Account>{
        return this.api.getAccount(email, password).pipe(
            tap(a => {
                if (a == undefined){ //no account found
                    console.log('(logIn() - loginService) passed account was null');
                }
                else { //account found. setting sessionstorage and reloading the page
                    sessionStorage.setItem('account', JSON.stringify(a)); //set session variable
                    window.alert('Logged in as ' + a.name + '.');
                    location.replace('/landing');
                }
            })
        );
    }
    
  • 最后,如果您进行了最后的更改,您将需要重构 LoginComponent.login() 方法以在点击时实际进行订阅。像这样:

    subscription: Subscription;
    
    logIn() {
        this.subscription = this.LoginService.logIn(this.email, this.password).subscribe(
            result => { /* here is where you should update your view */ },
            err => { /* handle any errors */ }
        );
    }
    
    ngOnDestroy() {
        if (this.subscription) { this.subscription.unsubscribe() }
    }
    

    请注意,我创建了一个 class-scope 变量来跟踪订阅,以便在组件被销毁时取消订阅,这只是最佳实践。

最后,这里是我在上面引用的一些文件中需要的一些导入:

import { Observable, Subscription } from 'rxjs';
import { map, tap } from 'rxjs/operators';

希望以上内容对您有所帮助。如果这不是一个学校项目,我还建议找出一种方法来使用 async 管道在模板中订阅 observable。 :)