Всем доброго времени суток. Подскажите пожалуйста, почему у меня могла возникнуть следующая проблема: После того, как было собрано и запущено приложение, окно в браузере выводит странный контент. Кроме этого, в исходниках видно, что отсутствуют скомпилированные скрипты.
Однако, если прописать в адресной строке .../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;
}
}
Всем заранее спасибо за любую помощь.
Решение потребовало внести изменения и в backend (Spring Boot), и в frontend (Angular 6).
В части 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;
}
}
В части 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.
Всем большое спасибо за всю оказанную помощь, за комментарии и попытки хотя-бы помочь в направлении поиска проблемы.
Виртуальный выделенный сервер (VDS) становится отличным выбором
Нужно прокликать все ссылки в менюНа странице это выглядит так:
Необходимо сжать картинку к размеру 204 800 КБРеализую сжатие при помощи thumbnailator
Приложение на sprign+jpa и базой oracle11g Настроен параметр генерации таблицы на основе предложенных сущностей JPA
Планирую создать Android-приложениеЗнаю, что есть множество различных способов хранения информации