使用「Gradle」来管理Node.js领域展开

对不起…
首先
在负责处理Oak Fan大量数据的后端系统中,经常采用Kotlin + Spring Boot + Gradle的架构。以前,这种架构主要用于开发批处理程序和Web API,但最近还开始使用Vue.js添加用户界面,创建带有单页应用程序(SPA)的Web应用。
使用Spring Initializr创建Spring Boot项目时,附带了Gradle Wrapper。这样,在开发环境中只需准备好JDK,就能轻松执行项目的测试和构建等操作。
然而,由于添加了JavaScript框架Vue.js到项目中,必须另外准备Node.js环境,这非常麻烦,而且在要求项目成员搭建开发环境时经常会出现问题。
在这种情况下,通常会介绍Docker,但在这里,我故意施加不使用Docker的限制,并透露使用已经在项目中的Gradle进行问题解决的方法。
个人来说,由于一直在使用Maven和Gradle(Ant有点过时…)来开发Java和Kotlin项目,所以我希望能够从有经验的角度来对付不熟悉的Node.js,也就是由npm(yarn也不错)控制的环境,这是我的初衷。
项目的前提设定如下。
-
- 開発環境に必要なのは JDK のみ
-
- Node.js まわりで必要なものは Gradle Plugin for Node で調達
- 幅広い OS (ここでは macOS、Linux、Windows を想定) で開発可能
本次样本项目的搭建将按照以下顺序进行。
-
- 创建Spring Boot项目
-
- 将Spring Boot项目转化为子项目
-
- 添加Vue.js子项目
-
- 实现Spring Boot和Vue.js子项目之间的协作
- 构建和运行项目
在示例中,我们使用了Spring Boot和Vue.js,但只要是Gradle和Node.js项目,无论使用其他什么技术,应该都可以应用本次的方法。
该命令的格式是按照 macOS 和 Linux 的方式进行编写的,但如果将路径的指定从斜杠 (/) 更改为反斜杠 (\),则可以在 Windows 上执行。
创建Spring Boot项目
首先,通过Spring Initializr生成Spring Boot的模板项目。
请在Web浏览器上访问https://start.spring.io/,然后按照以下方式进行选择(Group、Artifact等可以适当更改,没有问题)。

点击生成按钮下载并解压项目。
$ unzip spring-boot-vue-app.zip
项目的目录结构如下。

Gradle的初始配置文件settings.gradle.kts和build.gradle.kts的内容如下:
rootProject.name = "spring-boot-vue-app"
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "2.5.7"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.5.31"
kotlin("plugin.spring") version "1.5.31"
}
group = "io.aucfan.sample"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "11"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
初始项目由一个平面项目构成,但将其分割为以下的子项目。
spring-boot-vue-app
├── web-flux-server (Spring Boot サブプロジェクト)
│ ├── build.gradle.kts
│ └── src
├── web-vue2-ui (Vue.js サブプロジェクト)
│ └── build.gradle.kts
├── build.gradle.kts
└── settings.gradle.kts
将Spring Boot项目进行子项目化
将初始的Spring Boot项目移动到web-flux-server子项目中。按照以下方式创建web-flux-server目录,并将src目录移动到创建的目录下。(为了以YAML格式编写配置文件,我将application.properties暗中更改为application.yml。)

在 settings.gradle.kts 文件的末尾添加子项目的信息。
rootProject.name = "spring-boot-vue-app"
include(
"web-flux-server",
)
将位于项目根目录下的 build.gradle.kts 文件拆分为以下内容,并分别应用到子项目 web-flux-server/build.gradle.kts 中。变更内容如下所示。
org.springframework.boot と io.spring.dependency-management のプラグインの行の末尾に apply false を追記
group と version の定義を allprojects 内に移動
dependencies 以降を subprojects 内に移動
java.sourceCompatibility 行を subprojects 内に移動
repositories を subprojects 内にもコピー
subprojects 内で Kotlin 関連のプラグインを apply
web-flux-server ディレクトリ内にサブプロジェクト用の build.gradle.kts を作成
親プロジェクト build.gradle.kts の subprojects から Spring 関連の dependencies と tasks を web-flux-server/build.gradle.kts へ移動
web-flux-server/build.gradle.kts で Spring 関連のプラグインを apply
web-flux-server/build.gradle.kts に WebFlux や開発用、デプロイ用の dependencies と tasks を追加
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
// apply false を付加してデフォルトでは術式を発動させないようにする
id("org.springframework.boot") version "2.5.7" apply false
id("io.spring.dependency-management") version "1.0.11.RELEASE" apply false
kotlin("jvm") version "1.5.31"
kotlin("plugin.spring") version "1.5.31"
}
allprojects {
group = "io.aucfan.sample"
version = "0.0.1-SNAPSHOT"
}
repositories {
mavenCentral()
}
// サブプロジェクト共通設定
subprojects {
// Kotlin 関連のプラグインを発動させる
apply(plugin = "kotlin")
apply(plugin = "org.jetbrains.kotlin.plugin.spring")
java.sourceCompatibility = JavaVersion.VERSION_11
repositories {
mavenCentral()
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "11"
}
}
tasks.withType<Jar> {
// JAR ファイル名の基本部分が <プロジェクト名>-<サブプロジェクト名> となるように設定
archiveBaseName.set(listOf(rootProject.name, project.name).joinToString("-"))
}
}
// Spring 関連のプラグインを発動させる
apply(plugin = "org.springframework.boot")
apply(plugin = "io.spring.dependency-management")
dependencies {
// サブプロジェクトでは developmentOnly がそのままでは呼び出せないので強制召喚
val developmentOnly = configurations.getByName("developmentOnly")
// WebFlux に必要
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
// 開発ツール
developmentOnly("org.springframework.boot:spring-boot-devtools")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
tasks.withType<Test> {
useJUnitPlatform()
}
// JAR に起動スクリプトを埋め込んで単体で実行可能にする
tasks.withType<org.springframework.boot.gradle.tasks.bundling.BootJar> {
launchScript()
}
添加Vue.js的子项目
由于Spring Boot项目可以作为子项目移动,因此我们将类似地添加Vue.js子项目。
虽然本来是属于Node.js领域的,但我们将拓展至Gradle领域。
为了使用Gradle管理Node.js相关的内容,我们需要引入Gradle Plugin for Node。
创建一个名为 web-vue2-ui 的目录,并创建一个用于子项目的 build.gradle.kts 文件。

将 web-vue2-ui 子项目添加到 settings.gradle.kts 文件中。
rootProject.name = "spring-boot-vue-app"
include(
"web-flux-server",
"web-vue2-ui",
)
在亲项目的build.gradle.kts文件中,将Gradle Plugin for Node以”false”的方式添加。
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "2.5.7" apply false
id("io.spring.dependency-management") version "1.0.11.RELEASE" apply false
// Gradle Plugin for Node を追加 (デフォルトでは発動させない)
id("com.github.node-gradle.node") version "3.1.1" apply false
kotlin("jvm") version "1.5.31"
kotlin("plugin.spring") version "1.5.31"
}
// (後略)
将以下内容写入Vue.js子项目的web-vue2-ui/build.gradle.kts文件中。在这里,我们设置Node.js、npm和yarn在web-vue2-ui/.cache/目录下下载并配置。
import com.github.gradle.node.NodeExtension
import com.github.gradle.node.npm.proxy.ProxySettings
// Gradle Plugin for Node を発動させる
apply(plugin = "com.github.node-gradle.node")
// サブプロジェクトでは node が呼び出せないので強制召喚
configure<NodeExtension> {
download.set(true)
version.set("14.18.1")
npmVersion.set("6.14.15")
yarnVersion.set("1.22.17")
distBaseUrl.set("https://nodejs.org/dist")
npmInstallCommand.set("ci")
workDir.set(file("${project.projectDir}/.cache/nodejs"))
npmWorkDir.set(file("${project.projectDir}/.cache/npm"))
yarnWorkDir.set(file("${project.projectDir}/.cache/yarn"))
nodeProjectDir.set(file("${project.projectDir}"))
nodeProxySettings.set(ProxySettings.SMART)
}
通过Gradle Plugin for Node,我们可以预先使用yarn命令,将Node.js、npm和yarn的主体保存在.cache目录中。
$ ./gradlew :web-vue2-ui:yarn
成功将nodejs、npm、yarn目录按照设定的方式创建,并成功地将其封装在Node.js环境中。

现在我们可以调用yarn命令了,所以我们将开始创建Vue.js项目。这次我们将使用Vue CLI创建一个简单的Vue.js 2项目。通常情况下,Vue CLI会被全局安装,但在这里我们将在当前的临时项目中进行安装。
请在 web-vue2-ui 目录中按照以下方式使用 yarn 命令创建一次性项目。
(对于 Windows 系统,yarn 目录略有不同,可能是 .cache\yarn\yarn-v1.22.17\yarn。)
$ cd web-vue2-ui
$ .cache/yarn/yarn-v1.22.17/bin/yarn init
yarn init v1.22.17
question name (web-vue2-ui): spring-boot-vue-app-ui
question version (1.0.0): 0.0.1
question description: Spring Boot + Vue application
question entry point (index.js):
question repository url:
question author:
question license (MIT):
question private:
success Saved package.json
将Vue CLI添加到已创建的一次性项目中。如上所述,通常会使用yarn global add @vue/cli命令,但由于Vue CLI会逃离Gradle领域,所以在这里不指定global。
$ .cache/yarn/yarn-v1.22.17/bin/yarn add @vue/cli
由于vue命令暂时可用,我们可以按照以下方式创建一个简单的Vue.js 2项目。
$ ./node_modules/.bin/vue create spring-boot-vue-app-ui
Vue CLI v4.5.15
? Please pick a preset: Default ([Vue 2] babel, eslint)
? Pick the package manager to use when installing dependencies: Yarn
由于在 web-vue2-ui/spring-boot-vue-app-ui 下生成了项目,需要将其移动到上一级目录。在此过程中,将删除不再需要的临时性项目的 node_modules、package.json 和 yarn.lock 文件。另外,在创建的 Vue.js 项目内会生成一个名为 .git 的隐藏目录,不需要移动,直接删除,并将 .gitignore 文件移动。
$ rm -rf node_modules package.json yarn.lock
$ mv spring-boot-vue-app-ui/* .
$ mv spring-boot-vue-app-ui/.gitignore .
$ rm -rf spring-boot-vue-app-ui
为了将.cache目录排除在Git管理之外,我们需要在web-vue2-ui/.gitignore中添加相应的定义。
.DS_Store
node_modules
/dist
# 以下を追記
/.cache
# (後略)
最后,web-vue2-ui 子项目的目录结构将如下所示。(根据创建的 Vue.js 项目而有所变化。)

生成的 package.json 文件的内容如下,可以使用 vue 命令。
{
"name": "spring-boot-vue-app-ui",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.6.5",
"vue": "^2.6.11"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"vue-template-compiler": "^2.6.11"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
我会启动开发服务器并确认其正常运行。
$ .cache/yarn/yarn-v1.22.17/bin/yarn serve
当您在Web浏览器中访问http://localhost:8080/,将显示如下屏幕。

为了避免冲突,之后启动的Spring Boot Web服务器默认使用8080端口,所以我们要通过Ctrl + C停止开发服务器。(虽然我们可以使用”./gradlew :web-vue2-ui:yarn_serve”启动开发服务器,但是无法通过Ctrl + C停止进程,所以这里不使用它。)
Spring Boot 和 Vue.js 子项目之间的协作
回到主项目的目录,并执行Git仓库的初始化。
$ cd ..
$ git init
将web-vue2-ui/package.json文件更改如下。
{
"name": "spring-boot-vue-app-ui",
"version": "0.0.1",
"private": true,
"scripts": {
"serve": "vue-cli-service serve --port 8081 --watch --mode development",
"build": "vue-cli-service build --dest ../web-flux-server/src/main/resources/static/",
"lint": "vue-cli-service lint"
},
(中略)
}
在这部分中,我们正在配置Vue.js开发服务器。通过–port 8081将端口号由8080更改为8081。这是为了避免与之前提到的Spring Boot的WebFlux服务器默认使用的8080端口冲突。使用–watch来监视源文件的更改,并使用–mode development设置为开发环境。
将”vue-cli-service build –dest ../web-flux-server/src/main/resources/static/”部分更改为将Vue.js的构建文件的输出目录从默认的dist更改为Spring Boot子项目的资源目录。
另外,我們也根據母專案的版本,將版本號改為0.0.1。
在父项目的 .gitignore 文件中添加以下内容,以便将 Vue.js 的构建文件排除在 Git 的版本控制之外。
# (前略)
# Vue.js build files
/web-flux-server/src/main/resources/static/
我将尝试使用Gradle Plugin for Node构建Vue.js子项目。
$ ./gradlew :web-vue2-ui:yarn_build
在Spring Boot的子项目web-flux-server/src/main/resources/static/目录下会生成用于Web页面的文件。

因为Vue.js的子项目已经成功构建,所以需要设置Gradle任务之间的依赖关系。
首先,我们确保在 yarn build 之前执行 yarn install。(虽然不需要每次都执行 yarn install,但是如果所需的软件包已经安装好了,它会什么也不做,所以在这里我们可以放心地进行这个设置。)
import com.github.gradle.node.NodeExtension
import com.github.gradle.node.npm.proxy.ProxySettings
apply(plugin = "com.github.node-gradle.node")
// サブプロジェクトでは node が呼び出せないので強制召喚
configure<NodeExtension> {
download.set(true)
version.set("14.18.1")
npmVersion.set("6.14.15")
yarnVersion.set("1.22.17")
distBaseUrl.set("https://nodejs.org/dist")
npmInstallCommand.set("ci")
workDir.set(file("${project.projectDir}/.cache/nodejs"))
npmWorkDir.set(file("${project.projectDir}/.cache/npm"))
yarnWorkDir.set(file("${project.projectDir}/.cache/yarn"))
nodeProjectDir.set(file("${project.projectDir}"))
nodeProxySettings.set(ProxySettings.SMART)
}
// 以下を追記
// yarn build 前に yarn install を実行する (Gradle Plugin for Node 経由の実行なので _ を付加)
tasks.getByName("yarn_build") {
dependsOn("yarn_install")
}
然后,在Spring Boot子项目中,在处理资源之前,构建Vue.js子项目。
import org.springframework.boot.gradle.tasks.bundling.BootJar
apply(plugin = "org.springframework.boot")
apply(plugin = "io.spring.dependency-management")
dependencies {
// サブプロジェクトでは developmentOnly がそのままでは呼び出せないので強制召喚
val developmentOnly = configurations.getByName("developmentOnly")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
developmentOnly("org.springframework.boot:spring-boot-devtools")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
tasks.withType<Test> {
useJUnitPlatform()
}
tasks.withType<BootJar> {
launchScript()
}
// 以下を追記
// リソース処理の前に Vue.js サブプロジェクトのビルドを実行
tasks.withType<ProcessResources> {
dependsOn(":web-vue2-ui:yarn_build")
}
由于仅凭这些还无法公开在Spring Boot子项目内的资源中配置的文件,因此将创建以下的IndexHandler.kt和IndexRouterConfiguration.kt文件。

在 IndexHandler.kt 中,我們會記錄以下內容。
package io.aucfan.sample.spring.boot.vue
import org.springframework.beans.factory.annotation.Value
import org.springframework.core.io.Resource
import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse
import org.springframework.web.reactive.function.server.bodyValueAndAwait
@Component
class IndexHandler {
@Value("classpath:/static/index.html")
private lateinit var indexHtml: Resource
suspend fun index(request: ServerRequest): ServerResponse =
ServerResponse.ok()
.contentType(MediaType.TEXT_HTML)
.bodyValueAndAwait(indexHtml)
}
在IndexRouterConfiguration.kt中,写入以下代码。
package io.aucfan.sample.spring.boot.vue
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.io.ClassPathResource
import org.springframework.web.reactive.function.server.RouterFunctions
import org.springframework.web.reactive.function.server.coRouter
@Configuration
class IndexRouterConfiguration {
@Bean
fun indexRouter(indexHandler: IndexHandler) = coRouter {
GET("/", indexHandler::index)
}
fun staticResourceRouter() = RouterFunctions.resources("/**", ClassPathResource("static/"))
}
项目的构建和执行
既然准备工作已完成,现在在父项目目录下执行构建(无需进行测试)。
$ ./gradlew clean build -x test
Spring Boot的web-flux-server子项目将具现化为spring-boot-vue-app-web-flux-server-0.0.1-SNAPSHOT.jar文件,位于build/libs/目录下。

我要试着将 spring-boot-vue-app-web-flux-server-0.0.1-SNAPSHOT.jar 倒入 JVM 并执行。
$ cd web-flux-server/build/libs/
$ java -jar spring-boot-vue-app-web-flux-server-0.0.1-SNAPSHOT.jar
在Web浏览器中访问 http://localhost:8080/,将再次显示以下画面。(此时,前述的Vue.js开发服务器已更改为 http://localhost:8081/ 上的访问。)

在这个项目的构建过程中,Spring Boot的WebFlux服务器并没有实现任何的Web API。接下来,我们会使用Kotlin来实现Web API,并且在开发过程中使用axios从Vue.js中进行调用。
最後に yī)
在我们介绍的项目中部署所创建的Web应用的最简单方法如下所示。
-
- 将 JDK 引入开发环境
-
- 克隆项目的 git
-
- 在项目目录下进行 ./gradlew clean build -x test
-
- 将生成的 JAR 文件放置到运行环境中
-
- 将 JRE 引入运行环境
- 运行 JAR 文件(也可以将其服务化)
在实际项目中,还将添加从JAR文件外部读取的环境依赖配置文件的位置以及将其作为Docker镜像进行自动部署等各种功能。
由於能夠在Gradle領域中巧妙地將Node.js相關的儀式封閉起來,我們現在能夠輕鬆且順利地進行部署工作,仍然保持著心神安定。(當然,在開發過程中我們還是需要進入Node.js領域進行相應的操作…)
如果未来的 SPA 项目中有适合的场景,我会考虑采用它,因为它还有以下优点。
-
- 開発環境構築時も同様に Node.js 環境を別途用意する必要がない
-
- Vue.js 2 から Vue.js 3 や Nuxt.js、React、Next.js などに変更する場合も別領域サブプロジェクトを展開して対応可能
- Node.js から見ると領域は外界と隔絶されているので、UI のサブプロジェクト内では異なる Node.js のバージョンを採用可能
最终…
「呪術廻戦 0」電影版
將於12月24日上映,非常值得一看。