[HTTP request in Angular] - JSONP detailed explanation

        JSONP (JSON with Padding) is a "usage mode" of JSON that can be used to solve the problem of cross-domain data access by mainstream browsers. Data requests based on XMLHttpRequest will be restricted by the same-origin policy, while JSONP implemented in the form of a <script> tag will be loaded by the browser as a static resource request, thereby skipping the restrictions of the same-origin policy.

1. How to use Angular JSONP

        In the Angular project, using JSONP to achieve cross-domain data access, we need to introduce the HttpClientModule and HttpClientJsonpModule modules, which are generally imported in the root module AppModule .

import { HttpClientJsonpModule, HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, HttpClientModule, HttpClientJsonpModule, AppRoutingModule],
  bootstrap: [AppComponent]
})
export class AppModule { }

        After the module is imported, you can initiate a request through HttpClient in the component:

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

@Component({
  selector: 'app-main',
  templateUrl: './main.component.html',
  styleUrls: ['./main.component.css']
})
export class MainComponent implements OnInit {
    
  constructor(private http: HttpClient) { }
  
  ngOnInit(): void {
    this.jsonp();
  }

  jsonp() {
    let term = `王力宏`;
    let url = `https://itunes.apple.com/search?term=${term}&media=music&limit=10`;
    this.http.jsonp<any>(url, "callback").subscribe(data => {
      console.log(data.results.map((d: any) => d.trackName));
    });
  }
}

        Here is a demonstration of requesting itunes to query the song information data with the keyword term through JSONP , and the result is only the name of the input song. The debugging and running print is as shown in the figure:

 2. Introduction to jsonp method

        View the declaration of the jsonp method:

    /**
     * Constructs a `JSONP` request for the given URL and name of the callback parameter.
     *
     * @param url The resource URL.
     * @param callbackParam The callback function name.
     *
     * You must install a suitable interceptor, such as one provided by `HttpClientJsonpModule`.
     * If no such interceptor is reached,
     * then the `JSONP` request can be rejected by the configured backend.
     *
     * @return An `Observable` of the response object, with response body in the requested type.
     */
    jsonp<T>(url: string, callbackParam: string): Observable<T>;

        Note that the first parameter url refers to the address of the request, and the second parameter callbackParam refers to the parameter name specified by the server for passing the name of the callback method. Take the appeal code as an example, the value of callbackParam is "callback" , and different servers can agree on their own parameter names for passing the callback method name.

        View the source code of the method:

    jsonp(url, callbackParam) {
        return this.request('JSONP', url, {
            params: new HttpParams().append(callbackParam, 'JSONP_CALLBACK'),
            observe: 'body',
            responseType: 'json',
        });
    }

        It can be found that the jsonp method only adds the parameters of the specified callback method as additional parameters to the parameter dictionary of the request, which is equivalent to the request in the above example.

        https://itunes.apple.com/search?term=Wang Leehom&media=music&limit=10 became

        https://itunes.apple.com/search?term=Wang Leehom&media=music&limit=10&callback=JSONP_CALLBACK

        Then call the HttpCliet.request() method to make a request. The call process of HttpCliet.request()  has been explained in [HTTP Request in Angular] - HttpClient Detailed Explanation , and will not be introduced in detail here.

3. The independent process of jsonp

        It is still necessary to look at the processing flow of HttpHandler in HttpCliet.request():

        After the request is processed by the interceptor, it is finally processed by httpBackend. In the example project, the provider of the interceptor is not added to the AppModule.

        Combined with the previous introduction about interceptors, we know that HttpClientModule comes with a default interceptor HttpXsrfInterceptor, which adds XSRF tokens to qualified requests and has no effect on JSONP request methods.

        Looking at the code, you can find that HttpClientJsonpModule also comes with an interceptor JsonpInterceptor

         View the source code of JsonpInterceptor:

class JsonpInterceptor {
    constructor(jsonp) {
        this.jsonp = jsonp;
    }
    /**
     * Identifies and handles a given JSONP request.
     * @param req The outgoing request object to handle.
     * @param next The next interceptor in the chain, or the backend
     * if no interceptors remain in the chain.
     * @returns An observable of the event stream.
     */
    intercept(req, next) {
        if (req.method === 'JSONP') {
            return this.jsonp.handle(req);
        }
        // Fall through for normal HTTP requests.
        return next.handle(req);
    }
}
JsonpInterceptor.decorators = [
    { type: Injectable }
];
JsonpInterceptor.ctorParameters = () => [
    { type: JsonpClientBackend }
];

        It can be found that for JSONP requests, the interceptor directly hands the request to JsonpClientBackend ( this.jsonp ) for processing, and no longer calls the next.hanle() method to pass it to the subsequent interceptor.

        Therefore, for all JSONP requests in the Angular project, there are only two default interceptors that can work, and other manually added interceptors will not work.

4. Detailed explanation of JsonpClientBackend

        Check out the declaration of JsonpClientBackend:

export declare class JsonpClientBackend implements HttpBackend {
    private callbackMap;
    private document;
    /**
     * A resolved promise that can be used to schedule microtasks in the event handlers.
     */
    private readonly resolvedPromise;
    constructor(callbackMap: ɵangular_packages_common_http_http_b, document: any);
    /**
     * Get the name of the next callback method, by incrementing the global `nextRequestId`.
     */
    private nextCallback;
    /**
     * Processes a JSONP request and returns an event stream of the results.
     * @param req The request object.
     * @returns An observable of the response events.
     *
     */
    handle(req: HttpRequest<never>): Observable<HttpEvent<any>>;
}

        Looking at the code, you can know that ɵangular_packages_common_http_http_b in the construction method is actually JsonpCallbackContext.

export { ..., NoopInterceptor as ɵangular_packages_common_http_http_a, JsonpCallbackContext as ɵangular_packages_common_http_http_b, ... };

        From the providers of HttpClientJsonpModule, it can be found that JsonpCallbackContext actually uses jsonpCallbackContext.

 { provide: JsonpCallbackContext, useFactory: jsonpCallbackContext },

        Check its source code again and find that jsonpCallbackContext is the window object:

function jsonpCallbackContext() {
    if (typeof window === 'object') {
        return window;
    }
    return {};
}

        Look at the source code of the JsonpClientBackend.handle() method:

    handle(req) {
        if (req.method !== 'JSONP') {
            throw new Error(JSONP_ERR_WRONG_METHOD);
        }
        else if (req.responseType !== 'json') {
            throw new Error(JSONP_ERR_WRONG_RESPONSE_TYPE);
        }
        return new Observable((observer) => {
            const callback = this.nextCallback();  //生成唯一的回调方法名称
            // 首次调用生成的回调方法名称为 ng_jsonp_callback_0
            
            const url = req.urlWithParams.replace(/=JSONP_CALLBACK(&|$)/, `=${callback}$1`);
            //这一句替换请求url中的回调方法名称,以上面的例子来说
            // 替换前:https://itunes.apple.com/search?term=王力宏&media=music&limit=10&callback=JSONP_CALLBACK
            // 替换后:https://itunes.apple.com/search?term=王力宏&media=music&limit=10&callback=ng_jsonp_callback_0
 
            const node = this.document.createElement('script');  //创建script标签
            node.src = url;                                      //指定url
            
            let body = null;
            ......
 
            // 指定回调方法
            // 按上述回到方法名称 相当于 window["ng_jsonp_callback_0"] = (data) => { ... };
            this.callbackMap[callback] = (data) => {
                delete this.callbackMap[callback];  //回调方法调用后删除方法  delete window["ng_jsonp_callback_0"]
                ...
                body = data;        //将回调方法接收到的数据作为body通过HTTPResponse返回
                finished = true;
            };
            const cleanup = () => {  ...  };
            const onLoad = (event) => {
                ......
                this.resolvedPromise.then(() => {
                    ......
                    if (!finished) {
                        observer.error(new HttpErrorResponse({
                            url,
                            status: 0,
                            statusText: 'JSONP Error',
                            error: new Error(JSONP_ERR_NO_CALLBACK),
                        }));
                        return;
                    }
                    observer.next(new HttpResponse({
                        body,
                        status: 200 /* Ok */,
                        statusText: 'OK',
                        url,
                    }));
                    observer.complete();
                });
            };
            const onError = (error) => { ...... };
            node.addEventListener('load', onLoad);
            node.addEventListener('error', onError);
            this.document.body.appendChild(node);     //将 script 标签添加到页面上
            observer.next({ type: HttpEventType.Sent });
            
            // Cancellation handler. 指定取消订阅时的处理逻辑
            return () => {
                // Track the cancellation so event listeners won't do anything even if already scheduled.
                cancelled = true;
                // Remove the event listeners so they won't run if the events later fire.
                node.removeEventListener('load', onLoad);
                node.removeEventListener('error', onError);
                // And finally, clean up the page.
                cleanup();
            };
        });
    }

        The method where the unique callback method name is generated:

    nextCallback() {
        return `ng_jsonp_callback_${nextRequestId++}`; // nextRequestId 从0自增
    }

        You can see that the way JsonpClientBackend.handle() implements jsonp requests is consistent with the way we use JavaScript to implement them. The difference is that we generally use jsonp requests for each request. The callback method is fixed and has been written on the page. JsonpClientBackend is dynamic. Each time the nextCallback() method is used to obtain a new callback method name, the method is dynamically added through window["xxxxxx"]=(data)=>{} , and the callback method is called once to delete itself . More importantly, the whole process is encapsulated according to the RxJS responsive programming specification, and the observable object Observable is returned.

5 Conclusion

        The above is the introduction to using JSONP in Angular. You can see that if JSONP is used, the interceptor written by oneself cannot be used. Moreover, different back-end languages ​​require additional encoding to support requests in JSONP format. JSONP is not used in many cross-domain solutions. More cross-domain requests are implemented through CORS or proxies.

Guess you like

Origin blog.csdn.net/evanyanglibo/article/details/122383545