无法设置回调函数名时,如何向静态文件发送 Angular 的 JSONP 请求?

How to do a JSONP request with Angular to a static file when you can't set the callback function name?

各位问题解决者大家好!我希望能从你身上学到一些东西。我 运行 遇到了一个挑战,它可能会教会我更多关于 Angular 的知识,我将不胜感激任何人的意见。

背景

我的公司在许多网站中共享一个共同的页脚,我们使用 JSONP 将其延迟加载到每个网站中。页脚内容由生成静态 HTML 的系统管理,以实现高性能、低成本和高正常运行时间。当我们将该页脚延迟加载到页面中时,我们无法控制响应中包含的回调函数调用的名称。

我们的页脚可能看起来像这样 footer.json 文件:

    footerCallback(["<footer>FROM THE FOOTER</footer>"]);

我们使用简单的纯 Javascript 代码将其延迟加载到 HTML 页面中:

    <div id="footer"></div>
    <script>
      function footerCallback(json_data){
        document.getElementById('footer').outerHTML = json_data[0];
      }
      window.onload = function() {
        var script = document.createElement('script');
        script.src = 'assets/static-footer.json'
        script.async = true;
        document.getElementsByTagName('head')[0].appendChild(script);
      }
    </script>

我在 GitHub、here. Unfortunately I couldn't post it on Stackblitz to make it easier to run, since it uses JSONP requests. This version includes the shared footer in exactly the way that I described above, right here 上发布了一个工作演示。

挑战

我们需要在 Angluar 应用程序中包含相同的中央页脚,因此我正在尝试创建一个 footer 组件,将延迟加载的 JSONP 页脚插入到 Angular 应用程序中。

方法

created 一个 footer 组件和一个 footer 服务,我正在使用 HttpClientJsonpModule 做一个 JSONP 请求。那部分有效。该服务发送 JSONP 请求,我可以在 Chrome.

的检查器中看到响应

问题

不起作用的部分是回调。我无法从 main index.html 中删除原来的全局回调函数,因为我不知道如何用 JSONP 响应可以触发的函数替换它。我能够 move 回调函数到组件中,但只能通过继续全局定义它:

    function footerCallback(json_data){
      document.getElementById('footer').outerHTML = json_data[0];
    }
    const _global = (window) as any
    _global.footerCallback = footerCallback

Angular 期望能够告诉 JSONP 服务器要包含在 JSONP 响应中的回调函数的名称。但是没有服务器,它只是一个静态文件。

Angular 足够灵活,我可以告诉它使用 URL 参数来指定回调函数。如果我用 this.http.jsonp(this.footerURL,'callback') 指定“回调”作为回调函数名称,那么它会发出这样的请求:

    https://...footer.min.json?callback=ng_jsonp_callback_0

这是一个不错的功能,但我需要自定义的是 ng_jsonp_callback_0,而不是 URL 参数。因为服务器无法调整其响应以包含对函数 ng_jsonp_callback_0 的引用。因为没有服务器,所以只是一个静态文件。

丑陋的解决方案

我的解决方法是在全局范围内定义回调函数。这样,我就可以支持 API 或静态文件所需的任何回调函数名称。

这至少使我能够将与延迟加载页脚相关的所有内容封装到一个具有自己服务的页脚组件中。但是我觉得我并没有真正按照 Angular 的方式来做这件事,如果我污染了全局范围的话。

问题

有没有办法让我自己手动指定回调函数的名称,而不是让 Angular 选择名称 ng_jsonp_callback_0 或其他名称?

如果没有,那么有没有其他优雅的方法来处理这个问题?除了使用全局回调函数之外?

嗯..首先是 Angular 硬编码了回调,如果你检查源代码你会看到这样的东西:

/**
     * Get the name of the next callback method, by incrementing the global `nextRequestId`.
     */
    nextCallback() {
        return `ng_jsonp_callback_${nextRequestId++}`;
    }

所以当 ng_jsonp_callback_ 没有被调用时,Angular 会抛出一个错误:

 this.resolvedPromise.then(() => {
                    // Cleanup the page.
                    cleanup();
                    // Check whether the response callback has run.
                    if (!finished) {
                        // It hasn't, something went wrong with the request. Return an error via
                        // the Observable error path. All JSONP errors have status 0.
                        observer.error(new HttpErrorResponse({
                            url,
                            status: 0,
                            statusText: 'JSONP Error',
                            error: new Error(JSONP_ERR_NO_CALLBACK),
                        }));
                        return;
                    }
                    // Success. body either contains the response body or null if none was
                    // returned.
                    observer.next(new HttpResponse({
                        body,
                        status: 200,
                        statusText: 'OK',
                        url,
                    }));
                    // Complete the stream, the response is over.
                    observer.complete();
                });

Angular 只是用他们定义的回调加载 url 并使用 window 对象或空对象,如您在源代码中所见:

/**
 * Factory function that determines where to store JSONP callbacks.
 *
 * Ordinarily JSONP callbacks are stored on the `window` object, but this may not exist
 * in test environments. In that case, callbacks are stored on an anonymous object instead.
 *
 *
 */
export function jsonpCallbackContext() {
    if (typeof window === 'object') {
        return window;
    }
    return {};
}

既然我们知道了这一点,我们将执行以下操作:

  1. 调用 ng_jsonp_callback_ 这样我们就可以跳过错误并订阅响应。我们需要为此插入脚本。
  2. 使用 Renderer2 操作 DOM。
  3. 在组件中注入 ElementRef
  4. 删除html我们不需要的内容。
  5. 创建一个带有 json_data 的元素并将其插入到组件的 elementRef 中

现在在组件中执行以下操作:

export class FooterComponent implements AfterViewInit {

  // Steps 2 and 3
  constructor(private footerService: FooterService,
    private elementRef: ElementRef,
    private renderer: Renderer2) { }

  ngAfterViewInit() {
    this.addScript();
    this.footerService.getFooter().subscribe((data: string[]) => { this.footerCallback(data) });
  }
 
 // step 1
  private addScript() {
    // Wrap the ng_jsonp_callback_0 with your footerCallback 
    const scriptSrc = `window.footerCallback = function(json_data) {window.ng_jsonp_callback_0(json_data);}`;
    const scriptElement: HTMLScriptElement = this.renderer.createElement('script');
    scriptElement.innerHTML = scriptSrc;
    this.renderer.appendChild(this.elementRef.nativeElement, scriptElement);
  }

  // Insert a new Element with the json_data
  private footerCallback(json_data: string[]) {
    const footerElement: HTMLElement = this.renderer.createElement('div');
    footerElement.innerHTML = json_data[0];
    this.renderer.appendChild(this.elementRef.nativeElement, footerElement);
  }
}

就是这样,希望对你有帮助。

编辑 别忘了清理。

为了任何通过搜索词偶然发现此问题的人的利益,我正在添加另一个答案:

Oscar Ludick 对问题的 是正确的。这对我帮助很大。但是问题是错误的

如果您正在使用 Angular,并且如果您希望通过延迟加载 HTML 到您知道会在首屏以下的页面(例如, , 页脚)然后你 想要设置 Angular 服务来获取内容。

原因是服务端渲染。当您在服务器端呈现页面时,您不想包含延迟加载的内容。因为这会将 HTML 插入到发送到浏览器的初始页面有效负载中,而不是在客户端呈现初始页面后延迟加载它。

您也不想简单地将用于延迟加载的 Javascript 放到 Angular 组件的 .ts 文件中。因为那会产生相同的效果:它会在服务器端呈现期间 运行,而您不希望这样。就算你能做到,也不是你想要的。

Oscar 帮助我了解了如何使用 Renderer2 将用于延迟加载内容的脚本插入 DOM,并推迟该脚本的执行,直到页面在浏览器中呈现。在服务器端 Angular 渲染过程中,脚本不会 运行。

由于 windowglobal 的问题,让它在服务器端工作是一件令人头疼的事情。我学到了很多关于 Angular Universal 如何使用 Domino 伪造 DOM 的知识,这很有趣。但是当我在努力的时候,我慢慢意识到这是一个错误的问题。那个奥斯卡已经给我指路了。

这是示例项目的 Git 提交,展示了我如何删除 FooterService 并使用 Renderer2 将延迟加载代码插入渲染输出:https://github.com/endymion/angular-jsonp-problem/commit/5f91efa36f33a699da49044db63d22e3c7892a4d

通过所有这些我学到了很多东西,我希望这些笔记有一天能对其他人有所帮助。再次感谢 Oscar,祝您周末愉快。