

AngularアプリをGradleでwarにパッケージングするには
こんにちは、ソリューション開発部 安田です。
私が現在携わっている案件では、サーバー側は 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 ライフを。