Развертывание веб-приложения Angular 17 в Google App Engine не работает

У меня есть проект веб-приложения Angular (v17.2), который прекрасно создается и работает локально, но когда я пытаюсь развернуть (gcloud app deploy) его в Google App Engine, приложение не обслуживается должным образом.

Очевидно, GAE может его построить, но когда подается index.html, я получаю «404 не найден».

Единственный и единственный журнал сборки GAE, который меня заинтриговал, гласит следующее:

Используя конфигурацию appstart.Config{Runtime:"nodejs22", Entrypoint:appstart.Entrypoint{Type:"Default", Command:"/serve", WorkDir:""}, MainExecutable:""}

MainExecutable:"" ... это правда?


Я прочитал много документации о файлах конфигурации app.yaml и angular.json и tsconfig.json и их возможных атрибутах, но я не смог найти, что еще может быть, что я все еще могу делать неправильно.

Я даже создал новый пример проекта Angular (с ng new) и попытался развернуть его с абсолютными настройками по умолчанию, но это тоже дало тот же эффект.


My config files are as follows:

app.yaml:

runtime: nodejs22

service: [app-name]

handlers:

- url: /(.*\.css)
  static_files: dist/[app-name]/\1
  upload: dist/[app-name]/(.*\.css)

- url: /(.*\.html)
  static_files: dist/[app-name]/\1
  upload: dist/[app-name]/(.*\.html)

# Serve the root file
- url: /
  static_files: dist/[app-name]/index.html
  upload: dist/[app-name]/index.html

# Catch-all rule, responsible from handling Angular application routes (deeplinks).
- url: /.*
  static_files: dist/[app-name]/index.html
  upload: dist/[app-name]/index.html
  login: admin
  redirect_http_response_code: 301
  secure: always

Что-то не так со значениями и/или порядком обработчиков?


angular.json:

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "adm-portal": {
      "projectType": "application",
      "schematics": {},
      "root": "",
      "sourceRoot": "src",
      "prefix": "app",
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist/[app-name]",
            "index": {
              "input": "src/index.html",
              "output": "index.html"
            },
            "main": "src/main.ts",
            "polyfills": [
              "zone.js"
            ],
            "tsConfig": "tsconfig.app.json",
            "assets": [
              "src/assets"
            ],
            "styles": [
              "src/styles.css"
            ],
            "scripts": [],
            "allowedCommonJsDependencies": [
              "sweetalert2"
            ]
          },
          "configurations": {
            "production": {
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "500kb",
                  "maximumError": "1mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "2kb",
                  "maximumError": "4kb"
                }
              ],
              "outputHashing": "all"
            },
            "staging": {
              "optimization": false,
              "extractLicenses": false,
              "sourceMap": true,
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.staging.ts"
                }
              ]
            },
            "development": {
              "optimization": false,
              "extractLicenses": false,
              "sourceMap": true,
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.development.ts"
                }
              ]
            }
          },
          "defaultConfiguration": "production"
        },
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "configurations": {
            "production": {
              "buildTarget": "[app-name]:build:production"
            },
            "staging": {
              "buildTarget": "[app-name]:build:staging"
            },
            "development": {
              "buildTarget": "[app-name]:build:development"
            }
          },
          "defaultConfiguration": "development"
        },
        "extract-i18n": {
          "builder": "@angular-devkit/build-angular:extract-i18n",
          "options": {
            "buildTarget": "[app-name]:build"
          }
        },
        "test": {
          "builder": "@angular-devkit/build-angular:karma",
          "options": {
            "polyfills": [
              "zone.js",
              "zone.js/testing"
            ],
            "tsConfig": "tsconfig.spec.json",
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "src/styles.css"
            ],
            "scripts": []
          }
        }
      }
    }
  },
  "cli": {
    "analytics": false
  }
}

tsconfig.json:

{
  "compileOnSave": false,
  "compilerOptions": {
    "outDir": "./dist/out-tsc",
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "sourceMap": true,
    "declaration": false,
    "experimentalDecorators": true,
    "moduleResolution": "node",
    "importHelpers": true,
    "target": "ES2022",
    "module": "ES2022",
    "useDefineForClassFields": false,
    "lib": [
      "ES2022",
      "dom"
    ]
  },
  "angularCompilerOptions": {
    "enableI18nLegacyMessageIdFormat": false,
    "strictInjectionParameters": true,
    "strictInputAccessModifiers": true,
    "strictTemplates": true
  }
}


tsconfig.app.json:

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./out-tsc/app",
    "types": []
  },
  "files": [
    "src/main.ts"
  ],
  "include": [
    "src/**/*.d.ts"
  ]
}

package.json:

{
  "name": "[app-name]",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "watch": "ng build --watch --configuration development",
    "test": "ng test"
  },
  "private": true,
  "dependencies": {
    "@abacritt/angularx-social-login": "^2.2.0",
    "@angular/animations": "^17.2.0",
    "@angular/common": "^17.2.0",
    "@angular/compiler": "^17.2.0",
    "@angular/core": "^17.2.0",
    "@angular/forms": "^17.2.0",
    "@angular/platform-browser": "^17.2.0",
    "@angular/platform-browser-dynamic": "^17.2.0",
    "@angular/router": "^17.2.0",
    "rxjs": "~7.8.0",
    "sweetalert2": "^11.12.3",
    "tslib": "^2.3.0",
    "zone.js": "~0.14.3"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "^17.2.0",
    "@angular/cli": "^17.2.0",
    "@angular/compiler-cli": "^17.2.0",
    "@types/jasmine": "~5.1.0",
    "jasmine-core": "~5.1.0",
    "karma": "~6.4.0",
    "karma-chrome-launcher": "~3.2.0",
    "karma-coverage": "~2.2.0",
    "karma-jasmine": "~5.1.0",
    "karma-jasmine-html-reporter": "~2.1.0",
    "typescript": "~5.3.2"
  }
}


.gcloudignore:

# This file specifies files that are *not* uploaded to Google Cloud
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style
# file at that point).
#
# For more information, run:
#   $ gcloud topic gcloudignore

.gcloudignore

# If you would like to upload your .git directory, .gitignore file
# or files from your .gitignore file, remove the corresponding line below:
.git
.gitignore
#!include:.gitignore

# Ensure the dist directory is included (where build output goes)
!dist/

# backup files typically ending with a tilde (~)
*~

# node_modules directory, which contains package dependencies
/node_modules/

# e2e directory used for end-to-end tests
/e2e/

# hidden files and directories (e.g., .env, .gitconfig) and configuration files
^(.*/)?\..*$

# all JSON files
#^(.*/)?.*\.json$

# all Markdown files (README/text files)
^(.*/)?.*\.md$

# all YAML files
**/*.yaml$

# except for `app.yaml`
!app.yaml

# LICENSE file
^LICENSE

Если есть какая-либо другая информация/журнал, возможно, вам понадобится ее лучше проанализировать, дайте мне знать.

Любая помощь очень ценится!

У вас есть service: [app-name] в вашем app.yaml файле. Означает ли это, что у вас есть несколько сервисов? Другими словами, есть ли у вас в том же проекте еще одна служба, которая является службой по умолчанию?

NoCommandLine 02.08.2024 02:22

Да, @NoCommandLine. Это верно. В том же проекте на App Engine работает еще один сервис, который в настоящее время называется default.

Nick Fanelli 02.08.2024 04:16
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Angular и React для вашего проекта веб-разработки?
Angular и React для вашего проекта веб-разработки?
Когда дело доходит до веб-разработки, выбор правильного front-end фреймворка имеет решающее значение. Angular и React - два самых популярных...
Эпизод 23/17: Twitter Space о будущем Angular, Tiny Conf
Эпизод 23/17: Twitter Space о будущем Angular, Tiny Conf
Мы провели Twitter Space, обсудив несколько проблем, связанных с последними дополнениями в Angular. Также прошла Angular Tiny Conf с 25 докладами.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
Мое недавнее углубление в Angular
Мое недавнее углубление в Angular
Недавно я провел некоторое время, изучая фреймворк Angular, и я хотел поделиться своим опытом со всеми вами. Как человек, который любит глубоко...
Освоение Observables и Subjects в Rxjs:
Освоение Observables и Subjects в Rxjs:
Давайте начнем с основ и постепенно перейдем к более продвинутым концепциям в RxJS в Angular
0
2
56
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Просто любопытно, выполнили ли вы шаги, перечисленные здесь: https://cloud.google.com/run/docs/troubleshooting?hl=en#404

Я раньше не видел этот документ, поэтому проверю, как только вернусь к компьютеру. Однако, похоже, это специфично для Cloud Run. Знаете ли вы, применимо ли это и к App Engine?

Nick Fanelli 02.08.2024 15:17
Ответ принят как подходящий

После целой недели попыток отладить эту проблему я смог найти проблему.

Оказывается, в какой-то момент я пришел к неправильному выводу, что Google App Engine будет собирать проект самостоятельно всякий раз, когда будет выпущен gcloud app deploy. По этой причине я решил, что могу прокомментировать строку в .gcloudignore, в которой указано учитывать папку dist при загрузке развертывания (!dist/). Частично я благодарю @Дейвида Дренхана за его вопрос, заданный много лет назад: его упоминание о части skip_files в его файле побудило меня пойти проверить мой.

Еще одна вещь, которую я изменил, но не уверен, было ли это необходимо или нет, — это элемент moduleResolution в файле tsconfig.json с "node" на "bundler". Судя по всему, это то, что используется по умолчанию при создании нового проекта в Angular 18, поэтому я подумал об этом. Кроме того, после прочтения документации это приобрело смысл. Поскольку так много моментов было исследовано и скорректировано, есть вероятность, что другие вещи были неправильными или, по крайней мере, неидеальными, что и способствовало возникновению проблемы.

Возможно, стоит также упомянуть, что при развертывании в среде Flex можно подключиться через SSH к виртуальной машине, на которой работает ваше приложение. Это будет моя следующая попытка на случай, если я все еще не смогу понять, в чем причина проблемы.


Мой окончательный файл app.yaml выглядит так:

runtime: nodejs22

service: [app-name]

handlers:

- url: /([^.]+)/?$  # URls with no dot in them (e.g. /index, /about, etc)
  static_files: dist/index.html
  upload: dist/index.html

# Serve index.html for the root URL ('/')
- url: /
  static_files: dist/index.html
  upload: dist/index.html
  secure: always
  redirect_http_response_code: 301
  login: admin

# Serve CSS files from the root 'dist/' directory
- url: /(.+)\.css
  static_files: dist/\1.css
  upload: dist/.+\.css

# Serve JavaScript files from the root 'dist/' directory
- url: /(.+)\.js
  static_files: dist/\1.js
  upload: dist/.+\.js

# Serve HTML files from the root 'dist/' directory
- url: /(.+)\.html
  static_files: dist/\1.html
  upload: dist/.+\.html

# Serve files from the 'assets/' directory
- url: /assets/(.*)
  static_files: dist/assets/\1
  upload: dist/assets/(.*)

# Catch-all rule to serve index.html for any path not matched above
- url: /.*
  static_files: dist/index.html
  upload: dist/index.html
  secure: always
  redirect_http_response_code: 301
  login: admin




Предложения по улучшению, подсказки, комментарии и/или исправления того, что я в конечном итоге сказал неправильно, по-прежнему приветствуются.

Другие вопросы по теме