Angular 6 + Spring Boot. Некорректная загрузка приложения

124
09 сентября 2019, 11:40

Всем доброго времени суток. Подскажите пожалуйста, почему у меня могла возникнуть следующая проблема: После того, как было собрано и запущено приложение, окно в браузере выводит странный контент. Кроме этого, в исходниках видно, что отсутствуют скомпилированные скрипты.

Однако, если прописать в адресной строке .../index.html - приложение прогружается и начинает работать.

И все же, если обновить страницу браузера - в таком случае опять повторится ситуация, как на рисунке выше (если url был вида url/<application_context>/) или ошибка Whitelabel Error Page (если url был вида url/<application_context>/child/anotherChild...)

Я в некотором замешательстве, что может вызвать такие проблемы, поэтому даже не знаю, какие исходники следует приложить, чтобы помочь в решении. Вот некоторые из них:

package.json

{
  "name": "asterisk-prime-ui",
  "version": "1.0.0",
  "..": "main.ts",
  "scripts": {
    "ng": "ng",
    "start": "ng serve --proxy-config proxy.conf.json",
    "build-prod": "ng build --prod",
    "build-dev": "ng build --aot --build-optimizer --vendor-chunk",
    "extract": "ngx-translate-extract --input ./src/app --output ./src/assets/i18n/*.json --clean --sort --format namespaced-json --marker _",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "^6.1.7",
    "@angular/cdk": "^6.4.7",
    "@angular/common": "^6.1.7",
    "@angular/compiler": "^6.1.7",
    "@angular/core": "^6.1.7",
    "@angular/flex-layout": "^6.0.0-beta.18",
    "@angular/forms": "^6.1.7",
    "@angular/http": "^6.1.7",
    "@angular/material": "^6.4.7",
    "@angular/platform-browser": "^6.1.7",
    "@angular/platform-browser-dynamic": "^6.1.7",
    "@angular/router": "^6.1.7",
    "core-js": "^2.5.4",
    "hammerjs": "^2.0.8",
    "net": "^1.0.2",
    "rxjs": "^6.3.2",
    "rxjs-compat": "^6.3.2",
    "zone.js": "^0.8.29"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "~0.7.0",
    "@angular/cli": "~6.1.5",
    "@angular/compiler-cli": "^6.1.7",
    "@angular/language-service": "^6.1.7",
    "@biesbjerg/ngx-translate-extract": "^2.3.4",
    "@ngx-translate/core": "^10.0.2",
    "@ngx-translate/http-loader": "^3.0.1",
    "@types/jasmine": "~2.8.6",
    "@types/jasminewd2": "~2.0.3",
    "@types/node": "~8.9.4",
    "@types/sockjs-client": "^1.1.0",
    "@types/stompjs": "^2.3.4",
    "@types/yargs": "^12.0.5",
    "codelyzer": "^4.4.4",
    "jasmine-core": "~2.99.1",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "^3.0.0",
    "karma-chrome-launcher": "~2.2.0",
    "karma-coverage-istanbul-reporter": "^2.0.3",
    "karma-jasmine": "~1.1.1",
    "karma-jasmine-html-reporter": "^0.2.2",
    "protractor": "~5.4.0",
    "sockjs-client": "^1.1.5",
    "stompjs": "^2.3.3",
    "ts-node": "~5.0.1",
    "tslint": "~5.9.1",
    "typescript": "^2.9.2",
    "yargs": "^12.0.5"
  }
}

tsconfig.json

{
  "compileOnSave": false,
  "compilerOptions": {
    "outDir": "./dist/out-tsc",
    "baseUrl": "/",
    "target": "es5",
    "module": "es2015",
    "moduleResolution": "node",
    "sourceMap": true,
    "declaration": false,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "lib": [
      "es6",
      "es7",
      "dom"
    ],
    "typeRoots": [
      "node_modules/@types"
    ]
  },
  "exclude": [
    "../../node_modules"
  ]
}

angular.json

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "asterisk-prime-ui": {
      "root": "",
      "sourceRoot": "src",
      "projectType": "application",
      "prefix": "app",
      "schematics": {
        "@schematics/angular:component": {
          "styleext": "less"
        }
      },
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "../../../../asterisk-prime/src/main/resources/static/asterisk-prime-ui",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "src/tsconfig.app.json",
            "assets": [
              {
                "glob": "**/*",
                "input": "src/assets",
                "output": "/assets"
              },
              {
                "glob": "favicon.ico",
                "input": "src",
                "output": "/"
              }
            ],
            "styles": [
              {
                "input": "node_modules/@angular/material/prebuilt-themes/indigo-pink.css"
              },
              {
                "input": "src/styles/global.scss"
              }
            ],
            "scripts": []
          },
          "configurations": {
            "production": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "extractCss": true,
              "namedChunks": false,
              "aot": true,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true
            }
          }
        },
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "browserTarget": "asterisk-prime-ui:build"
          },
          "configurations": {
            "production": {
              "browserTarget": "asterisk-prime-ui:build:production"
            }
          }
        },
        "extract-i18n": {
          "builder": "@angular-devkit/build-angular:extract-i18n",
          "options": {
            "browserTarget": "asterisk-prime-ui:build"
          }
        },
        "test": {
          "builder": "@angular-devkit/build-angular:karma",
          "options": {
            "main": "src/test.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "src/tsconfig.spec.json",
            "karmaConfig": "src/karma.conf.js",
            "styles": [
              {
                "input": "node_modules/@angular/material/prebuilt-themes/indigo-pink.css"
              },
              {
                "input": "src/styles/global.scss"
              }
            ],
            "scripts": [],
            "assets": [
              {
                "glob": "**/*",
                "input": "src/assets",
                "output": "/assets"
              },
              {
                "glob": "favicon.ico",
                "input": "src",
                "output": "/"
              }
            ]
          }
        },
        "lint": {
          "builder": "@angular-devkit/build-angular:tslint",
          "options": {
            "tsConfig": [
              "src/tsconfig.app.json",
              "src/tsconfig.spec.json"
            ],
            "exclude": [
              "**/node_modules/**"
            ]
          }
        }
      }
    },
    "asterisk-prime-ui-e2e": {
      "root": "e2e/",
      "projectType": "application",
      "architect": {
        "e2e": {
          "builder": "@angular-devkit/build-angular:protractor",
          "options": {
            "protractorConfig": "e2e/protractor.conf.js",
            "devServerTarget": "asterisk-prime-ui:serve"
          },
          "configurations": {
            "production": {
              "devServerTarget": "asterisk-prime-ui:serve:production"
            }
          }
        },
        "lint": {
          "builder": "@angular-devkit/build-angular:tslint",
          "options": {
            "tsConfig": "e2e/tsconfig.e2e.json",
            "exclude": [
              "**/node_modules/**"
            ]
          }
        }
      }
    }
  },
  "defaultProject": "asterisk-prime-ui",
  "schematics": {
    "@schematics/angular:component": {
      "styleext": "scss"
    }
  }
}

index.html

<!doctype html>
<html lang="en">
<head>
  <base href=".">
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>AsteriskPrimeUI</title>
  <meta name="description" content="">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
  <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet" />
</head>
<body>
<app-root>Loading...</app-root>
</body>
</html>

Frontend часть собирается и выкладывается в static-ресурсы spring-boot приложения (см. "outputPath": "../../../../asterisk-prime/src/main/resources/static/asterisk-prime-ui"). Далее все это собирается в war-архив.

WebMvcConfig.java

@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer
{
    @Value("#{'${web.mvc.crossOrigins}'.split(',')}")
    private String[] crossOrigins;
    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
            "classpath:/META-INF/resources/",
            "classpath:/resources/",
            "classpath:/static/",
            "classpath:/public/",
            "classpath:/static/asterisk-prime-ui/"
    };
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry)
    {
        if (!registry.hasMappingForPattern("/webjars/**"))
        {
            registry.addResourceHandler("/webjars/**")
                    .addResourceLocations("/webjars/");
        }
        if (!registry.hasMappingForPattern("/**"))
        {
            registry.addResourceHandler("/**")
                    .addResourceLocations(CLASSPATH_RESOURCE_LOCATIONS);
        }
    }
    @Override
    public void addCorsMappings(CorsRegistry registry)
    {
        registry.addMapping("/api/**")
                .allowedOrigins(crossOrigins)
                .allowCredentials(true)
                .maxAge(3600);
    }
    @Bean
    public ViewResolver urlViewResolver()
    {
        UrlBasedViewResolver viewResolver = new UrlBasedViewResolver();
        viewResolver.setViewClass(InternalResourceView.class);
        return viewResolver;
    }
}

Всем заранее спасибо за любую помощь.

Answer 1

Решение потребовало внести изменения и в backend (Spring Boot), и в frontend (Angular 6).

1. Backend

В части Spring Boot все изменения коснулись корректирования настроек WebMvc. В частности, изменил ViewResolver:

@Bean
public ViewResolver internalResourceViewResolver()
{
    InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
    viewResolver.setViewClass(InternalResourceView.class);
    return viewResolver;
}

А так же добавил "переброс" на index.html при мапинге / (как выяснилось, это, в своем роде, стандартный механизм при использовании одностраничного приложения (с использованием Angular, React или подобных им):

@Override
public void addViewControllers(ViewControllerRegistry registry)
{
    registry.addViewController("/").setViewName("index.html");
}

Окончательный вид конфигурации:

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.prime.asterisk.web.controller"})
public class WebMvcConfig implements WebMvcConfigurer
{
    @Value("#{'${web.mvc.crossOrigins}'.split(',')}")
    private String[] crossOrigins;
    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
            "classpath:/META-INF/resources/",
            "classpath:/resources/",
            "classpath:/static/",
            "classpath:/public/",
            "classpath:/static/asterisk-prime-ui/"
    };
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry)
    {
        if (!registry.hasMappingForPattern("/**"))
        {
            registry.addResourceHandler("/**")
                    .addResourceLocations(CLASSPATH_RESOURCE_LOCATIONS);
        }
    }
    @Override
    public void addCorsMappings(CorsRegistry registry)
    {
        registry.addMapping("/api/**")
                .allowedOrigins(crossOrigins)
                .allowCredentials(true)
                .maxAge(3600);
    }
    @Override
    public void addViewControllers(ViewControllerRegistry registry)
    {
        registry.addViewController("/").setViewName("index.html");
    }
    @Bean
    public ViewResolver internalResourceViewResolver()
    {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setViewClass(InternalResourceView.class);
        return viewResolver;
    }
}

2. Frontend

В части UI так же необходимо было внести свои правки.

В первую очередь, добавить атрибуты base-href и deploy-url для скриптов сборки. В моем случае использован шаблон: /<application-context>/, где <application-context> - asterisk-prime. В целом, в большинстве случаев достаточно было бы поставить атрибут base-href, однако в моем случае этого было мало, т.к. assets проекта не использовали base-href в качестве префикса src-атрибута. Возможно есть другое решение, однако в моем случае достаточно было добавить атрибут deploy-url. Окончательный вид скриптов:

...
"build-prod": "ng build --prod --base-href /asterisk-prime/ --deploy-url /asterisk-prime/",
...
"build-dev": "ng build --aot --build-optimizer --base-href /asterisk-prime/ --deploy-url /asterisk-prime/",
...

Далее не менее важным действием было изменить стратегию роутинга приложения. А именно, активировать параметр useHash=true в соответствующем модуле:

...
RouterModule.forRoot(AppConfigRoutesFactory.getRoutes(), {useHash: true})
...

P.S.: AppConfigRoutesFactory - класс со статическим методом отдачи Routes (просто передайте туда свои Routes)

Окончательный вид AppRoutesModule:

@NgModule({
  imports: [
    RouterModule.forRoot(AppConfigRoutesFactory.getRoutes(),
      {
        useHash: true,
        scrollPositionRestoration: 'enabled'
      })
  ],
  exports: [
    RouterModule
  ]
})
export class AppRoutingModule {
}

Далее просто необходимо импортировать этот модуль в ваш AppModule. Использовать hash в роутинге следует, если при обновлении страницы выходит ошибка Whitelabel Error Page.

Всем большое спасибо за всю оказанную помощь, за комментарии и попытки хотя-бы помочь в направлении поиска проблемы.

READ ALSO
Как прокликать все элементы списка?

Как прокликать все элементы списка?

Нужно прокликать все ссылки в менюНа странице это выглядит так:

134
Алгоритм сжатия картинок

Алгоритм сжатия картинок

Необходимо сжать картинку к размеру 204 800 КБРеализую сжатие при помощи thumbnailator

106
Ошибка such unique or primary key already exists in the table при использовании jpa?

Ошибка such unique or primary key already exists in the table при использовании jpa?

Приложение на sprign+jpa и базой oracle11g Настроен параметр генерации таблицы на основе предложенных сущностей JPA

122
Хранение данных в Android-приложении

Хранение данных в Android-приложении

Планирую создать Android-приложениеЗнаю, что есть множество различных способов хранения информации

140