【问题标题】:Angular Universal (SSR), with Leaflet and ngx-leafletAngular Universal (SSR),带有 Leaflet 和 ngx-leaflet
【发布时间】:2020-10-11 14:45:30
【问题描述】:

我最近使用 Angular(不是说菜鸟),并且由于 Leaflet 的问题,我正在努力将标准 Angular 应用程序传递给 Angular Universal,我发现许多项目示例运行良好,但我无法管理让它在我的项目中以同样的方式工作。 我设法隔离了这个问题,但我无法解决它。 我删除了所有组件和模块中对传单的所有引用,并将 package.json 保留如下:

  "name": "vyv-angular",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e",
    "dev:ssr": "ng run vyv-angular:serve-ssr",
    "serve:ssr": "node dist/vyv-angular/server/main.js",
    "build:ssr": "ng build --prod && ng run vyv-angular:server:production",
    "prerender": "ng run vyv-angular:prerender"
  },
  "private": true,
  "dependencies": {
    "@agm/core": "3.0.0-beta.0",
    "@angular/animations": "10.1.5",
    "@angular/cdk": "10.2.4",
    "@angular/common": "10.1.5",
    "@angular/compiler": "10.1.5",
    "@angular/core": "10.1.5",
    "@angular/fire": "6.0.3",
    "@angular/forms": "10.1.5",
    "@angular/localize": "10.1.5",
    "@angular/material": "10.2.4",
    "@angular/platform-browser": "^10.1.5",
    "@angular/platform-browser-dynamic": "10.1.5",
    "@angular/platform-server": "^10.1.5",
    "@angular/router": "10.1.5",
    "@asymmetrik/ngx-leaflet": "8.1.0",
    "@ng-bootstrap/ng-bootstrap": "7.0.0",
    "@ng-bootstrap/schematics": "2.0.0-alpha.1",
    "@ng-select/ng-select": "5.0.3",
    "@ngrx/store": "10.0.0",
    "@nguniversal/express-engine": "10.1.0",
    "@nguniversal/module-map-ngfactory-loader": "8.2.6",
    "@ngx-translate/core": "13.0.0",
    "@ngx-translate/http-loader": "6.0.0",
    "@swimlane/ngx-datatable": "18.0.0",
    "acorn": "8.0.1",
    "agm-direction": "0.8.7",
    "body-parser": "1.19.0",
    "bootstrap": "4.0.0",
    "classlist.js": "1.1.20150312",
    "core-js": "3.6.5",
    "cors": "2.8.5",
    "d3": "6.1.1",
    "datatables.net": "1.10.22",
    "date-fns": "2.16.1",
    "express": "4.17.1",
    "firebase": "7.23.0",
    "google-libphonenumber": "^3.2.13",
    "hopscotch": "0.3.1",
    "intl": "1.2.5",
    "intl-tel-input": "17.0.6",
    "leaflet": "1.7.1",
    "leaflet-ant-path": "1.3.0",
    "material-design-icons": "3.0.1",
    "md5-typescript": "1.0.5",
    "moment": "2.28.0",
    "ng-apexcharts": "1.5.1",
    "ng-click-outside": "7.0.0",
    "ngx-bootstrap": "^6.1.0",
    "ngx-custom-validators": "9.1.0",
    "ngx-device-detector": "2.0.0",
    "ngx-intl-tel-input": "^3.0.3",
    "ngx-perfect-scrollbar": "10.0.1",
    "ngx-quill": "12.0.1",
    "ngx-spinner": "10.0.1",
    "ngx-take-until-destroy": "5.4.0",
    "ngx-toastr": "13.0.0",
    "ngx-ui-switch": "10.0.2",
    "node-sass": "4.14.1",
    "nodemailer": "6.4.13",
    "nouislider": "14.6.2",
    "popper.js": "1.16.1",
    "postcss-rtl": "1.7.3",
    "prismjs": "1.21.0",
    "pug": "3.0.0",
    "quill": "1.3.7",
    "rxjs": "6.6.3",
    "rxjs-compat": "^6.6.3",
    "screenfull": "5.0.2",
    "tslib": "2.0.1",
    "web-animations-js": "2.3.2",
    "zone.js": "0.11.1"
  },
  "devDependencies": {
    "@angular-devkit/architect": "0.1001.6",
    "@angular-devkit/build-angular": "0.1001.6",
    "@angular/cli": "10.1.6",
    "@angular/compiler-cli": "10.1.5",
    "@nguniversal/builders": "10.1.0",
    "@types/chartist": "0.11.0",
    "@types/cors": "2.8.8",
    "@types/d3-shape": "1.3.2",
    "@types/express": "4.17.0",
    "@types/google-maps": "3.2.2",
    "@types/googlemaps": "3.39.12",
    "@types/jasmine": "3.5.0",
    "@types/jasminewd2": "2.0.3",
    "@types/jquery": "3.5.2",
    "@types/node": "14.11.8",
    "@types/nodemailer": "6.4.0",
    "codelyzer": "6.0.1",
    "firebase-admin": "9.2.0",
    "firebase-functions": "3.6.0",
    "firebase-functions-test": "0.2.2",
    "firebase-tools": "8.12.1",
    "fuzzy": "0.1.3",
    "inquirer": "6.2.2",
    "inquirer-autocomplete-prompt": "1.0.1",
    "jasmine-core": "3.6.0",
    "jasmine-spec-reporter": "4.2.1",
    "karma": "5.0.0",
    "karma-chrome-launcher": "3.1.0",
    "karma-coverage-istanbul-reporter": "2.1.0",
    "karma-jasmine": "3.0.1",
    "karma-jasmine-html-reporter": "1.4.2",
    "open": "7.3.0",
    "protractor": "7.0.0",
    "rxjs-compat": "6.6.3",
    "tslint": "6.1.0",
    "typescript": "4.0.3"
  }
}

然后

ng run dev:ssr

运行良好,应用程序可以与服务器端渲染一起使用(当然,映射部分除外,因为它已被删除)。

那我开始一步一步地重新引入ngx-leaflet模块。 第一步是简单的在 app.module.ts 中添加模块

import {LeafletModule} from '@asymmetrik/ngx-leaflet';

在导入部分:

 imports: [
LeafletModule,

只是添加这个,没有在任何组件中使用(我将我的地图组件留空) 给我以下错误:

    > ng run vyv-angular:serve-ssr

****************************************************************************************
This is a simple server for use in testing or debugging Angular applications locally.
It hasn't been reviewed for security issues.

DON'T USE IT FOR PRODUCTION!
****************************************************************************************
Hash: c84349b0ffb562a756f2
Time: 40944ms
Built at: 2020-10-11 9:17:28 ├F10: AM┤
      Asset      Size  Chunks                          Chunk Names
    main.js  13.3 MiB    main  [emitted]        [big]  main
main.js.map  13.2 MiB    main  [emitted] [dev]         main
Entrypoint main [big] = main.js main.js.map
chunk {main} main.js, main.js.map (main) 11.4 MiB [entry] [rendered]

chunk {main} main.js, main.js.map (main) 1.36 MB [initial] [rendered]
chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 147 kB [initial] [rendered]
chunk {runtime} runtime.js, runtime.js.map (runtime) 6.15 kB [entry] [rendered]
chunk {styles} styles.css, styles.css.map (styles) 1.36 MB [initial] [rendered]
chunk {vendor} vendor.js, vendor.js.map (vendor) 8.77 MB [initial] [rendered]
Date: 2020-10-11T14:17:45.082Z - Hash: bde98637bf23c20bf894 - Time: 50331ms

Compiled successfully.
F:\GitaLab\vyv-angular\dist\vyv-angular\server\main.js:65133
  var requestFn = window.requestAnimationFrame || getPrefixed('RequestAnimationFrame') || timeoutDefer;
                  ^

ReferenceError: window is not defined
    at F:\GitaLab\vyv-angular\dist\vyv-angular\server\main.js:65133:19
    at F:\GitaLab\vyv-angular\dist\vyv-angular\server\main.js:64914:11
    at Object.4R65 (F:\GitaLab\vyv-angular\dist\vyv-angular\server\main.js:64916:2)
    at __webpack_require__ (F:\GitaLab\vyv-angular\dist\vyv-angular\server\main.js:20:30)
    at F:\GitaLab\vyv-angular\dist\vyv-angular\server\main.js:116999:129
    at Object.RlJC (F:\GitaLab\vyv-angular\dist\vyv-angular\server\main.js:117001:2)
    at __webpack_require__ (F:\GitaLab\vyv-angular\dist\vyv-angular\server\main.js:20:30)
    at Module.ZAI4 (F:\GitaLab\vyv-angular\dist\vyv-angular\server\main.js:134326:82)
    at __webpack_require__ (F:\GitaLab\vyv-angular\dist\vyv-angular\server\main.js:20:30)
    at Module.24aS (F:\GitaLab\vyv-angular\dist\vyv-angular\server\main.js:54859:69)

A server error has occurred.
node exited with 1 code.
connect ECONNREFUSED 127.0.0.1:58155

我发现“var requestFn = window.requestAnimationFrame || getPrefixed('RequestAnimationFrame') || timeoutDefer;”行在leaflet-src.js内,在node-modules/下,所以它是对窗口的传单javascript引用失败,这是正常的,因为在服务器上运行,但是如果我什至不触发,为什么会出现这个错误脚本的任何用法? 我设法克隆了工作示例的存储库,但 package.json 、 app.module.ts 和其他组件与我的相同。所以问题出在其他地方,但我不知道在哪里。 我只是以错误的方式将我的标准 Angular 应用程序迁移到 Angular Universal 吗? 我遵循了这次跑步的标准指南:

ng add @nguniversal/express-engine

一切顺利。

然后我也跑了:

ng add @angular/fire

为了能够在 firebase 中部署为一个函数,但目前问题不是部署部分,SSR 服务器不会从 app.module.ts 中导入的 ngx-leaflet 模块开始。

我应该在这里添加任何其他文件来帮助吗? ng add @nguniversal/express-engine 为服务器和 ts 配置创建了新文件,但我没有修改它们......

欢迎任何让我走上正轨的想法或评论!

编辑:

我克隆的工作示例和我的工作示例之间的唯一区别是 ngx 模块的导入是在仅通过路由器加载的子模块中完成的。 我尝试在这些项目中将导入添加到主 app.modules.ts 中,但发生了同样的错误! 所以这个问题似乎只发生在这个模块的导入处于顶层时。 我将尝试在一个单独的模块中实现我的地图组件,如果我找到一个可行的解决方案,我会更新这篇文章......

【问题讨论】:

    标签: angular server-side-rendering angular-universal ngx-leaflet


    【解决方案1】:

    经过几天的痛苦,我决定放弃 ngx-leaflet 模块。 根据本期 Sergey 的想法,我设法让传单地图与原始 javascript 库一起工作:How to use @angular/universal with Leaflet?。 这意味着对组件进行一些修改,因为 ngx-leaflet 提供的 angular 内的传单易于集成丢失了,但这是值得的。

    正如 Sergey 解释的那样,解决方案是将对传单脚本的访问包装在服务中,但是在我的项目中,无法识别服务中使用的“require”javascript方法,即使“@types/节点”已正确安装。下一个问题的解决方案是修改 tsconfig.app.json 并在编译器选项中添加以下内容:“types”:[“node”],在我的情况下结果如下:

      "compilerOptions": {
        "outDir": "./out-tsc/app",
        "types": [ "node" ],
        "typeRoots": [ "../node_modules/@types" ]
      },
    

    因为它已经存在于 tsconfig.server.json 中,但这还不够。

    然后在你的组件中(也正如 Sergei 解释的那样),注意不要使用或导入任何 Leflet 方法,但“@types/leaflet”很好。 所有对“L”传单脚本的引用都必须通过服务完成,嵌入在测试浏览器是否存在的测试中:

      if (isPlatformBrowser(this.platformId)) {
         this.leafletService.L.
      }
    

    【讨论】:

    • 谢谢,帮了大忙。很高兴您为自己的问题提供了答案。
    【解决方案2】:

    您必须将传单导入包装到服务中。工作示例可以从这里的 github 下载:https://github.com/NickToony/universal-starter-leaflet-example

    import { Injectable, PLATFORM_ID, Inject } from '@angular/core';
    import { isPlatformBrowser } from '@angular/common';
    
    @Injectable()
    export class MapService {
    
      public L = null;
    
      constructor(@Inject(PLATFORM_ID) private platformId: Object) {
        if (isPlatformBrowser(platformId)) {
          this.L = require('leaflet');
        }
      }
    
    }
    

    【讨论】:

      【解决方案3】:

      错误信息很清楚,这是因为您使用了 window 对象。只有在浏览器内部时,您才能识别浏览器并执行代码,如下所示:

      import { isPlatformBrowser } from '@angular/common';
      import { Component, Inject, OnInit, PLATFORM_ID } from '@angular/core';
      
      @Component({
        selector: 'app-yourcomponent',
        templateUrl: './app.yourcomponent.html',
      })
      export class YourComponent implements OnInit {
        constructor(
          @Inject(PLATFORM_ID) private _platformId
        ) {}
      
        ngOnInit() {
          if (isPlatformBrowser(this._platformId)) {
            // Whatever you want to run inside the browser
            window.scrollTo(0, 0);
          }
        }
      }
      

      【讨论】:

      • 我想到了这一点,并且已经删除了所有组件中对传单的所有访问权限,工作 SSR 和不工作之间的唯一区别是在 app.modules.ts 中导入了 ngx-leaflet 模块,所以我无处可测试浏览器是否存在...当 SSR 服务器将仅使用导入运行时,我将使用其内容重新设置地图组件,但目前它们是空组件。 ..
      • 是的,谢谢 Ahmad,我看到了,但本质上它说 ngx-leaflet 不会导致任何 SSR 问题,这是传单的错误,但根据我的测试,它看起来像导致问题的插件内传单的实现。我几乎找到了解决方案,但我不得不退出使用这个插件,我直接按照这里解释的方式使用传单:link(谢尔盖的回答)