如何使用ParcelJS在Spring Boot应用程序中打包前端 - codecentric AG Blog


在集成前端代码时,我们经常需要处理多种内容,例如:资源,HTML,CSS,JavaScript,打字稿,缩小等等 - 通常是通过复杂生成的构建脚本来实现,这些脚本很难调试。我一直在寻找一个简单的快速实验解决方案......现在我偶然发现了ParcelJS,它通过使用约定优于配置解决了部分问题。
ParcelJS是一个简单的Web应用程序捆绑器,它可以将您的前端代码打包为理想的默认值,这些默认值可以满足您的需求 - 至少在大多数情况下都是如此。非常适合小型和简单的项目或演示应用程序。在下面的文章中,我将描述如何在Spring Boot应用程序中捆绑和提供前端代码,而无需使用任何代理,专用开发服务器或复杂的构建系统!而且你还可以免费获得压缩,缩小和实时重载等酷炫功能。
听起来很有希望?然后继续阅读!

对于不耐烦的人,你可以在这里找到GitHub上的所有代码:thomasdarimont / spring-boot-micro-frontend-example

示例应用
示例应用程序使用Maven,由包含在第四个父模块中的三个模块组成:

  • acme-example-api
  • acme-example-ui
  • acme-example-app
  • spring-boot-micro-frontend-example (父)

第一个模块是acme-example-api包含后端API的,后端API只是一个简单的带@RestController注释的Spring MVC控制器。我们的第二个模块acme-example-ui包含我们的前端代码,并将Maven与Parcel结合使用来打包应用程序位。下一个模块acme-example-app托管实际的Spring Boot应用程序并将其他两个模块连接在一起。最后,该spring-boot-starter-parent模块用作聚合器模块并提供默认配置。

1.父模块
父模块本身使用spring-boot-starter-parentas parent并继承一些托管依赖项和默认配置。

<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi=
"http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation=
"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.github.thomasdarimont.training</groupId>
    <artifactId>acme-example</artifactId>
    <version>1.0.0.0-SNAPSHOT</version>
    <packaging>pom</packaging>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.2.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
 
    <modules>
        <module>acme-example-api</module>
        <module>acme-example-ui</module>
        <module>acme-example-app</module>
    </modules>
 
    <properties>
        <java.version>11</java.version>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
        <maven.compiler.release>${java.version}</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
 
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.github.thomasdarimont.training</groupId>
                <artifactId>acme-example-api</artifactId>
                <version>${project.version}</version>
            </dependency>
 
            <dependency>
                <groupId>com.github.thomasdarimont.training</groupId>
                <artifactId>acme-example-ui</artifactId>
                <version>${project.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
 
    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <configuration>
                        <executable>true</executable>
                    </configuration>
                    <executions>
                        <execution>
                            <goals>
                                <goal>build-info</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
                <plugin>
                    <groupId>pl.project13.maven</groupId>
                    <artifactId>git-commit-id-plugin</artifactId>
                    <configuration>
                        <generateGitPropertiesFile>true</generateGitPropertiesFile>
                        <!-- enables other plugins to use git properties -->
                        <injectAllReactorProjects>true</injectAllReactorProjects>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

2.API模块
acme-example-api模块中的GreetingController 

 

@Slf4j
@RestController
@RequestMapping("/api/greetings")
class GreetingController {
 
    @GetMapping
    Object greet(@RequestParam(defaultValue =
"world") String name) {
        Map<String, Object> data = Map.of(
"greeting", "Hello " + name, "time", System.currentTimeMillis());
        log.info(
"Returning: {}", data);
        return data;
    }
}

pom.xml配置:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns=
"http://maven.apache.org/POM/4.0.0"
    xmlns:xsi=
"http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation=
"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.github.thomasdarimont.training</groupId>
        <artifactId>acme-example</artifactId>
        <version>1.0.0.0-SNAPSHOT</version>
    </parent>
    <artifactId>acme-example-api</artifactId>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
 
</project>

APP模块
acme-example-app模块的App类是Spring Boot启动类

package com.acme.app;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
public class App {
 
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

对于我们的应用程序,我们希望从Spring Boot应用程序中提供前端资源。因此,我们在cme-example-app模块中WebMvcConfiga定义以下ResourceHandler和ViewController内容:

package com.acme.app.web;
 
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
import lombok.RequiredArgsConstructor;
 
@Configuration
@RequiredArgsConstructor
class WebMvcConfig implements WebMvcConfigurer {
 
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/app/**").addResourceLocations("classpath:/public/");
    }
 
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController(
"/app/").setViewName("forward:/app/index.html");
    }
}

为了让这个例子更逼真,我们将使用/acme一个自定义的context-path,配置application.yml:

server:
  servlet:
    context-path:/ acme

我们acme-example-app模块的Maven pom.xml看起来有点罗嗦,因为它将其他模块拉到一起:

<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi=
"http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation=
"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.github.thomasdarimont.training</groupId>
        <artifactId>acme-example</artifactId>
        <version>1.0.0.0-SNAPSHOT</version>
    </parent>
    <artifactId>acme-example-app</artifactId>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jetty</artifactId>
        </dependency>
 
        <dependency>
            <groupId>com.github.thomasdarimont.training</groupId>
            <artifactId>acme-example-api</artifactId>
        </dependency>
 
        <dependency>
            <groupId>com.github.thomasdarimont.training</groupId>
            <artifactId>acme-example-ui</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

UI模块
现在出现了一个有趣的部分:acme-example-ui包含我们的前端代码的Maven模块。

该acme-example-ui模块在pom.xml使用com.github.eirslett:frontend-maven-pluginMaven插件触发标准的前端构建工具,在这种情况下使用node和yarn。

<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi=
"http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation=
"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.github.thomasdarimont.training</groupId>
        <artifactId>acme-example</artifactId>
        <version>1.0.0.0-SNAPSHOT</version>
    </parent>
    <artifactId>acme-example-ui</artifactId>
 
    <properties>
        <node.version>v10.15.1</node.version>
        <yarn.version>v1.13.0</yarn.version>
        <frontend-maven-plugin.version>1.6</frontend-maven-plugin.version>
    </properties>
 
    <build>
        <plugins>
            <plugin>
                <groupId>pl.project13.maven</groupId>
                <artifactId>git-commit-id-plugin</artifactId>
                <!-- config inherited from parent -->
            </plugin>
 
            <plugin>
                <groupId>com.github.eirslett</groupId>
                <artifactId>frontend-maven-plugin</artifactId>
                <version>${frontend-maven-plugin.version}</version>
                <configuration>
                    <installDirectory>target</installDirectory>
                    <workingDirectory>${basedir}</workingDirectory>
                    <nodeVersion>${node.version}</nodeVersion>
                    <yarnVersion>${yarn.version}</yarnVersion>
                </configuration>
 
                <executions>
                    <execution>
                        <id>install node and yarn</id>
                        <goals>
                            <goal>install-node-and-yarn</goal>
                        </goals>
                    </execution>
 
                    <execution>
                        <id>yarn install</id>
                        <goals>
                            <goal>yarn</goal>
                        </goals>
                        <configuration>
                                                        <!-- this calls yarn install -->
                            <arguments>install</arguments>
                        </configuration>
                    </execution>
 
                    <execution>
                        <id>yarn build</id>
                        <goals>
                            <goal>yarn</goal>
                        </goals>
                        <configuration>
                                                        <!-- this calls yarn build -->
                            <arguments>build</arguments>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
 
        <pluginManagement>
            <plugins>
                <!--This plugin's configuration is used to store Eclipse m2e settings 
                    only. It has no influence on the Maven build itself. -->
                <plugin>
                    <groupId>org.eclipse.m2e</groupId>
                    <artifactId>lifecycle-mapping</artifactId>
                    <version>1.0.0</version>
                    <configuration>
                        <lifecycleMappingMetadata>
                            <pluginExecutions>
                                <pluginExecution>
                                    <pluginExecutionFilter>
                                        <groupId>com.github.eirslett</groupId>
                                        <artifactId>frontend-maven-plugin</artifactId>
                                        <versionRange>[0,)</versionRange>
                                        <goals>
                                            <goal>install-node-and-yarn</goal>
                                            <goal>yarn</goal>
                                        </goals>
                                    </pluginExecutionFilter>
                                    <action>
                                        <!-- ignore yarn builds triggered by eclipse -->
                                        <ignore />
                                    </action>
                                </pluginExecution>
                            </pluginExecutions>
                        </lifecycleMappingMetadata>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

在目录/acme-example-ui/src/main/frontend下前端结构:

└── frontend
    ├── index.html
    ├── main
    │   └── main.js
    └── style
        └── main.css


index.html只包含纯HTML引用我们的JavaScript代码和资产:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv=
"X-UA-Compatible" content="IE=edge">
    <title>Acme App</title>
    <meta name=
"description" content="">
    <meta name=
"viewport" content="width=device-width, initial-scale=1">
    <link rel=
"stylesheet" href="./style/main.css">
</head>
<body>
    <h1>Acme App</h1>
 
    <button id=
"btnGetData">Fetch data</button>
    <div id=
"responseText"></div>
    <script src=
"./main/main.js" defer></script>
</body>
</html>

main.js中javascript代码调用之前的REST GreetingController :

import "@babel/polyfill";
 
function main(){
    console.log(
"Initializing app...")
 
    btnGetData.onclick = async () => {
 
        const resp = await fetch(
"../api/greetings");
        const payload = await resp.json();
        console.log(payload);
 
        responseText.innerText=JSON.stringify(payload);
    };
}
 
main();

这里使用了ES7语法,在main.css中CSS:

body {
    --main-fg-color: red;
    --main-bg-color: yellow;
}
 
h1 {
    color: var(--main-fg-color);
}
 
responseText {
    background: var(--main-bg-color);
}

请注意,我正在使用“新”原生CSS变量支持。

注意package.json配置:

{
    "name": "acme-example-ui-plain",
    
"version": "1.0.0.0-SNAPSHOT",
    
"private": true,
    
"license": "Apache-2.0",
    
"scripts": {
        
"clean": "rm -rf target/classes/public",
        
"start": "parcel --public-url ./ -d target/classes/public src/main/frontend/index.html",
        
"watch": "parcel watch --public-url ./ -d target/classes/public src/main/frontend/index.html",
        
"build": "parcel build --public-url ./ -d target/classes/public src/main/frontend/index.html"
    },
    
"devDependencies": {
        
"@babel/core": "^7.0.0-0",
        
"@babel/plugin-proposal-async-generator-functions": "^7.2.0",
        
"babel-preset-latest": "^6.24.1",
        
"parcel": "^1.11.0"
    },
    
"dependencies": {
        
"@babel/polyfill": "^7.2.5"
    }
}

为了支持ES7特性,比如async,我们需要通过.babelrc文件配置babel transpiler :

{
   "presets": [
      [
"latest"]
   ],
   
"plugins": []
}

ParcelJS 设置
我们定义了一些脚本clean,start,watch并且build,这是为了能够通过`yarn`或`npm`调用它们。

下一个技巧是parcel的配置。让我们看一个具体的例子来看看这里发生了什么:

parcel build --public-url ./ -d target/classes/public src/main/frontend/index.html

这行做了几件事:

  • --public-url ./这指示parcel生成相对于我们将从中提供应用程序资源的路径的链接。
  • -d target/classes/public这告诉Parcel将前端工件放在target/classes/public文件夹中,它们可以在类路径中找到
  • src/main/frontend/index.html最后一部分是显示Parcel,在这种情况下,我们的应用程序的入口点src/main/frontend/index.html。请注意,您可以在此处定义多个入口点。
  •  

下一个技巧是将此配置与Parcel的监视模式相结合,可以通过parcel watch命令启动。与许多其他Web应用程序捆绑工具一样,watch允许在我们更改代码时自动且透明地重新编译和重新打包前端工件。

因此,我们要做的就是拥有一个流畅的前端开发人员体验,就是在/acme-example-ui文件夹中启动`yarn watch`进程。
生成的资源将显示在下面target/classes/public,如下所示:

$ yarn watch                          
yarn run v1.13.0
$ parcel watch --public-url ./ -d target/classes/public src/main/frontend/index.html
 Built in 585ms.

$ ll target/classes/public            
total 592K
drwxr-xr-x. 2 tom tom 4,0K  8. Feb 22:59 ./
drwxr-xr-x. 3 tom tom 4,0K  8. Feb 22:59 ../
-rw-r--r--. 1 tom tom  525  8. Feb 23:02 index.html
-rw-r--r--. 1 tom tom 303K  8. Feb 23:02 main.0632549a.js
-rw-r--r--. 1 tom tom 253K  8. Feb 23:02 main.0632549a.map
-rw-r--r--. 1 tom tom  150  8. Feb 23:02 main.d4190f58.css
-rw-r--r--. 1 tom tom 9,5K  8. Feb 23:02 main.d4190f58.js
-rw-r--r--. 1 tom tom 3,6K  8. Feb 23:02 main.d4190f58.map

$ cat target/classes/public/index.html:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta http-equiv=
"X-UA-Compatible" content="IE=edge">
        <title>Acme App</title>
        <meta name=
"description" content="">
        <meta name=
"viewport" content="width=device-width, initial-scale=1">
        <link rel=
"stylesheet" href="main.d4190f58.css">
    <script src=
"main.d4190f58.js"></script></head>
    <body>
        <h1>Acme App</h1>
 
        <button id=
"btnGetData">Fetch data</button>
        <div id=
"responseText"></div>
        <script src=
"main.0632549a.js" defer=""></script>
    </body>
</html>

下一个技巧是只使用Spring Boot devtools启用了Live-reload。如果您访问任何前端代码,这将自动重新加载包内容。您可以启动com.acme.app.AppSpring Boot应用程序并通过http://localhost:8080/acme/app/在浏览器中输入URL 来访问应用程序。

添加Typescript 
现在我们的设置工作正常,我们可能想要使用Typescript而不是纯JavaScript。使用Parcel这很容易。只需在src/main/frontend/main下添加新文件hello.ts即可:

interface Person {
    firstName: string;
    lastName: string;
}
 
function greet(person: Person) {
    return "Hello, " + person.firstName + " " + person.lastName;
}
 
let user = { firstName:
"Buddy", lastName: "Holly" };
 
console.log(greet(user));


然后在index.html引用:

<script src="./main/hello.ts" defer></script>

由于我们正在运行yarn watch,parcel工具将发现我们需要一个基于.ts我们引用文件的文件扩展名的Typescript编译器。因此ParcelJS会自动添加"typescript": "^3.3.3"到我们devDependencies的package.json文件中。

使用less用于CSS
我们现在可能想要使用less而不是普通css。同样,所有我们在这里做的是重新命名main.css,以main.less并参考它在index.html通过的文件

<link rel="stylesheet" href="./style/main.less">

ParcelJS将自动添加"less": "^3.9.0"到我们的产品中,devDependencies并为您提供随时可用的配置。

请注意,默认情况下ParcelJS支持许多其他资产类型

最后:你可以做一个maven verify,它会自动建立你acme-example-api和acme-example-ui模块和acme-example-app的可执行文件打包的JAR包

​​​​​​​下次你想快速构建一些东西或者只是稍微破解一下,那么ParcelJS和Spring Boot可能非常适合你。