AngularアプリをGradleでwarにパッケージングするには

2019年7月16日Angular, Gradle, プログラミング, 開発環境・ツール

こんにちは、ソリューション開発部 安田です。

私が現在携わっている案件では、サーバー側は Java, クライアント側は Angular を用いています。それぞれはGradle と Angular CLI でビルドできるようになっているのですが、せっかくなのでひとつの war にパッケージングしてリリースする仕組みを考えました。

ここでは、その具体的な方法を紹介したいと思います。

前提となる環境

以下の環境を前提としています。

  • Java: 1.8.0_151
  • Gradle: 5.2.1
  • Node: v10.15.0
  • Npm: 6.4.1
  • Angular CLI: 6.2.9

ただし、Gradle や Angular CLI の標準的な仕組みをベースとしていますので、多少バージョンが前後していても同じように対応できると思います。

前提となるWebアプリの説明

Webアプリが以下の2つのプロジェクトで構成されているとします。

  • sample-app-client: Angular アプリの実装。下記の Server の REST API を呼び出す画面がある。
  • sample-app-server: Java 製のWebアプリ。静的な画面は持たず、REST API のみ実装している。

今回の記事はアプリのビルドについての説明のため、具体的な実装は割愛します。

sample-app-client

Angular CLI で作成した標準的なディレクトリ構造で、かつ ng コマンドでビルドできる状態とします。

sample-app-client
├── .editorconfig
├── angular.json
├── e2e/
├── package.json
├── package-lock.json
├── src/
│   ├── app/
│   ├── assets/
│   ├── browserslist/
│   ├── environments/
│   ├── favicon.ico
│   ├── index.html
│   ├── karma.conf.js
│   ├── main.ts
│   ├── polyfills.ts
│   ├── styles.css
│   ├── test.ts
│   ├── tsconfig.app.json
│   ├── tsconfig.spec.json
│   └── tslint.json
├── tsconfig.json
└── tslint.json

package.json の内容

{
  "name": "sample-app-client",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "^6.1.0",
    "@angular/common": "^6.1.0",
    "@angular/compiler": "^6.1.0",
    "@angular/core": "^6.1.0",
    "@angular/forms": "^6.1.0",
    "@angular/http": "^6.1.0",
    "@angular/platform-browser": "^6.1.0",
    "@angular/platform-browser-dynamic": "^6.1.0",
    "@angular/router": "^6.1.0",
    "core-js": "^2.5.4",
    "rxjs": "~6.2.0",
    "zone.js": "~0.8.26"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "~0.8.0",
    "@angular/cli": "~6.2.9",
    "@angular/compiler-cli": "^6.1.0",
    "@angular/language-service": "^6.1.0",
    "@types/jasmine": "~2.8.8",
    "@types/jasminewd2": "~2.0.3",
    "@types/node": "~8.9.4",
    "codelyzer": "~4.3.0",
    "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.1",
    "karma-jasmine": "~1.1.2",
    "karma-jasmine-html-reporter": "^0.2.2",
    "protractor": "~5.4.0",
    "ts-node": "~7.0.0",
    "tslint": "~5.11.0",
    "typescript": "~2.9.2"
  }
}

ビルドする場合の実行例

$ npm run ng -- build --base-href="/sample-app/" --prod
> sample-app-client@0.0.0 ng C:\sample-project\sample-app-client
> ng "build" "--base-href=/sample-app/" "--prod"
Date: 2019-06-26T20:07:42.699Z
Hash: 5cc00df10d59e736224b
Time: 12206ms
chunk {0} runtime.06daa30a2963fa413676.js (runtime) 1.44 kB [entry] [rendered]
chunk {1} main.01984c0c10db1896e9c0.js (main) 170 kB [initial] [rendered]
chunk {2} polyfills.d64817aaf614d4221ef9.js (polyfills) 63.3 kB [initial] [rendered]
chunk {3} styles.3ff695c00d717f2d2a11.css (styles) 0 bytes [initial] [rendered]

sample-app-server

Gradle の標準的なディレクトリ構成に従っており、war タスクでビルドできるものとします。

sample-app-server
├── build.gradle
└── src/
    ├── main/
    │   ├── java/
    │   ├── resources/
    │   └── webapp/
    └── test/
        ├── java/
        └── resources/

build.gradle の内容

plugins {
    id 'war'
}
repositories {
    jcenter() 
}
dependencies {
    // ここでは Jersey を使用していますが、あくまで一例です。
    compile 'org.glassfish.jersey.containers:jersey-container-servlet:2.28'
    compile 'org.glassfish.jersey.inject:jersey-hk2:2.28'
    compile 'org.glassfish.jersey.media:jersey-media-json-jackson:2.28'
    testCompile 'junit:junit:4.12'
}

ビルドする場合の実行例

$ gradle build --console=plain
> Task :compileJava
> Task :processResources NO-SOURCE
> Task :classes
> Task :war
> Task :assemble
> Task :compileTestJava NO-SOURCE
> Task :processTestResources NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test NO-SOURCE
> Task :check UP-TO-DATE
> Task :build
BUILD SUCCESSFUL in 1s
2 actionable tasks: 2 executed

現時点では、まだ wa rの中に index.html や *.js は含まれません。

$ unzip -Z1 build/libs/sample-app-server.war
META-INF/
META-INF/MANIFEST.MF
WEB-INF/
WEB-INF/classes/
WEB-INF/classes/com/
WEB-INF/classes/com/example/
WEB-INF/classes/com/example/MyResource.class
WEB-INF/lib/
WEB-INF/lib/jersey-container-servlet-2.28.jar
WEB-INF/lib/jersey-hk2-2.28.jar
WEB-INF/lib/jersey-media-json-jackson-2.28.jar
WEB-INF/lib/jersey-container-servlet-core-2.28.jar
WEB-INF/lib/jersey-server-2.28.jar
WEB-INF/lib/jersey-client-2.28.jar
WEB-INF/lib/jersey-media-jaxb-2.28.jar
WEB-INF/lib/jersey-common-2.28.jar
WEB-INF/lib/jersey-entity-filtering-2.28.jar
WEB-INF/lib/jakarta.ws.rs-api-2.1.5.jar
WEB-INF/lib/hk2-locator-2.5.0.jar
WEB-INF/lib/jackson-module-jaxb-annotations-2.9.8.jar
WEB-INF/lib/jackson-databind-2.9.8.jar
WEB-INF/lib/jackson-annotations-2.9.8.jar
WEB-INF/lib/hk2-api-2.5.0.jar
WEB-INF/lib/hk2-utils-2.5.0.jar
WEB-INF/lib/jakarta.inject-2.5.0.jar
WEB-INF/lib/jakarta.annotation-api-1.3.4.jar
WEB-INF/lib/osgi-resource-locator-1.0.1.jar
WEB-INF/lib/validation-api-2.0.1.Final.jar
WEB-INF/lib/aopalliance-repackaged-2.5.0.jar
WEB-INF/lib/javassist-3.22.0-CR2.jar
WEB-INF/lib/jackson-core-2.9.8.jar
WEB-INF/web.xml

serverとClientを組み合わせる方法

以下の2ステップで sample-app-client と sample-app-server を組み合わせることができます。

sample-app-client を Gradle でビルドできるようにする

sample-app-client/build.gradle を作成します。このとき、gradle-node-plugin を用いて、Angular アプリを Gradle でビルドできるようにします。

plugins {
    id "com.moowork.node" version "1.3.1"
}
ext {
    contextPath = '/sample-app/'
}
node {
    version = '10.15.0'
    npmVersion = '6.4.1'
}
task build(type: NpmTask, dependsOn: npmInstall) {
    // ng build に必要なパラメータはここで渡しておく
    // https://angular.io/cli/build
    args = ['run', 'ng', '--', 'build', "--base-href=$contextPath", '--prod']
}

sample-app-client が gradle build で ビルドできるようになったことを確認します。

$ gradle build --console=plain
> Task :nodeSetup SKIPPED
> Task :npmSetup UP-TO-DATE
> Task :npmInstall
npm WARN rollback Rolling back readable-stream@2.3.6 failed (this is probably harmless): EPERM: operation not permitted, lstat 'C:\sample-project\sample-app-client\node_modules\fsevents\node_modules
'
npm WARN ajv-keywords@3.4.0 requires a peer of ajv@^6.9.1 but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
audited 40995 packages in 10.078s
found 2 low severity vulnerabilities
  run `npm audit fix` to fix them, or `npm audit` for details
> Task :build
> sample-app-client@0.0.0 ng C:\sample-project\sample-app-client
> ng "build" "--base-href=/sample-app/" "--prod"
Date: 2019-06-26T20:38:50.332Z
Hash: 5cc00df10d59e736224b
Time: 14496ms
chunk {0} runtime.06daa30a2963fa413676.js (runtime) 1.44 kB [entry] [rendered]
chunk {1} main.01984c0c10db1896e9c0.js (main) 170 kB [initial] [rendered]
chunk {2} polyfills.d64817aaf614d4221ef9.js (polyfills) 63.3 kB [initial] [rendered]
chunk {3} styles.3ff695c00d717f2d2a11.css (styles) 0 bytes [initial] [rendered]
BUILD SUCCESSFUL in 33s
3 actionable tasks: 2 executed, 1 up-to-date

server と client をまとめてマルチプロジェクト構成にする

Gradleの Multi Project Builds という仕組みを利用して、server と client を一度にビルドできるようにします。

Multi Project Builds には階層的に配置する方法(Hierarchical layouts)と、フラットに配置する方法(Flat layouts) がありますが、今回は後者を採用しました。

まずは sample-app-client と sample-app-server と同階層に sample-app プロジェクトを作成し、そこに build.gradle を格納します。

├── sample-app/
│   ├── build.gradle
│   └── settings.gradle
├── sample-app-client/
└── sample-app-server/

sample-app/settings.gradle には sample-app が マルチモジュールプロジェクトであることを定義します。

rootProject.name = 'sample-app'
includeFlat 'sample-app-server', 'sample-app-client'

build.gradle には sample-app-server の war に sample-app-client の成果物が含まれるように定義します。

// sample-app-server より先に sample-app-client を評価する
project(':sample-app-server').evaluationDependsOn(':sample-app-client')
project('sample-app-server') {
    afterEvaluate {
        // war タスクの設定を変更し clientの成果物を含むようにする
        project.war {
            archiveName = 'sample-app.war'
            from "${project(':sample-app-client').projectDir}/dist/sample-app-client"
        }
    }
}

sample-app プロジェクトで gradle build すると、sample-app-server/libs/sample-app.war が出力されます。

$ gradle build --console=plain
> Task :sample-app-client:nodeSetup SKIPPED
> Task :sample-app-client:npmSetup UP-TO-DATE
> Task :sample-app-client:npmInstall UP-TO-DATE
> Task :sample-app-client:build
> sample-app-client@0.0.0 ng C:\sample-project\sample-app-client
> ng "build" "--base-href=/sample-app/" "--prod"
Date: 2019-06-26T20:46:44.738Z
Hash: 5cc00df10d59e736224b
Time: 13636ms
chunk {0} runtime.06daa30a2963fa413676.js (runtime) 1.44 kB [entry] [rendered]```
chunk {1} main.01984c0c10db1896e9c0.js (main) 170 kB [initial] [rendered]
chunk {2} polyfills.d64817aaf614d4221ef9.js (polyfills) 63.3 kB [initial] [rendered]
chunk {3} styles.3ff695c00d717f2d2a11.css (styles) 0 bytes [initial] [rendered]
> Task :sample-app-server:compileJava
> Task :sample-app-server:processResources NO-SOURCE
> Task :sample-app-server:classes
> Task :sample-app-server:war
> Task :sample-app-server:assemble
> Task :sample-app-server:compileTestJava NO-SOURCE
> Task :sample-app-server:processTestResources NO-SOURCE
> Task :sample-app-server:testClasses UP-TO-DATE
> Task :sample-app-server:test NO-SOURCE
> Task :sample-app-server:check UP-TO-DATE
> Task :sample-app-server:build
BUILD SUCCESSFUL in 21s
5 actionable tasks: 3 executed, 2 up-to-date

war ファイルの内容を確認すると、index.html や *.js が含まれるようになったことがわかります。

$ unzip -Z1 sample-app-server/build/libs/sample-app.war
META-INF/
META-INF/MANIFEST.MF
WEB-INF/
WEB-INF/classes/
WEB-INF/classes/com/
WEB-INF/classes/com/example/
WEB-INF/classes/com/example/MyResource.class
WEB-INF/lib/
WEB-INF/lib/jersey-container-servlet-2.28.jar
WEB-INF/lib/jersey-hk2-2.28.jar
WEB-INF/lib/jersey-media-json-jackson-2.28.jar
WEB-INF/lib/jersey-container-servlet-core-2.28.jar
WEB-INF/lib/jersey-server-2.28.jar
WEB-INF/lib/jersey-client-2.28.jar
WEB-INF/lib/jersey-media-jaxb-2.28.jar
WEB-INF/lib/jersey-common-2.28.jar
WEB-INF/lib/jersey-entity-filtering-2.28.jar
WEB-INF/lib/jakarta.ws.rs-api-2.1.5.jar
WEB-INF/lib/hk2-locator-2.5.0.jar
WEB-INF/lib/jackson-module-jaxb-annotations-2.9.8.jar
WEB-INF/lib/jackson-databind-2.9.8.jar
WEB-INF/lib/jackson-annotations-2.9.8.jar
WEB-INF/lib/hk2-api-2.5.0.jar
WEB-INF/lib/hk2-utils-2.5.0.jar
WEB-INF/lib/jakarta.inject-2.5.0.jar
WEB-INF/lib/jakarta.annotation-api-1.3.4.jar
WEB-INF/lib/osgi-resource-locator-1.0.1.jar
WEB-INF/lib/validation-api-2.0.1.Final.jar
WEB-INF/lib/aopalliance-repackaged-2.5.0.jar
WEB-INF/lib/javassist-3.22.0-CR2.jar
WEB-INF/lib/jackson-core-2.9.8.jar
WEB-INF/web.xml
3rdpartylicenses.txt
favicon.ico
index.html
main.01984c0c10db1896e9c0.js
polyfills.d64817aaf614d4221ef9.js
runtime.06daa30a2963fa413676.js
styles.3ff695c00d717f2d2a11.css

あとは、できあがった war ファイルをデプロイするだけです。

まとめ

いかがでしたでしょうか。

今回は説明を割愛しましたが、実際の案件では他にも Gradle から開発用サーバーを立ち上げるなどのカスタマイズをしているので、また別の機会に紹介したいと思います。

それでは素敵な Gradle & Angular ライフを。

  • 株式会社アークシステムの来訪管理・会議室予約システム BRoomHubs
  • 低コスト・短納期で提供するまるごとおまかせZabbix