转载

如何使用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=<font>"http://maven.apache.org/POM/4.0.0"</font><font>
    xmlns:xsi=</font><font>"http://www.w3.org/2001/XMLSchema-instance"</font><font>
    xsi:schemaLocation=</font><font>"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"</font><font>>
    <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><b>true</b></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><b>true</b></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><b>true</b></generateGitPropertiesFile>
                        <!-- enables other plugins to use git properties -->
                        <injectAllReactorProjects><b>true</b></injectAllReactorProjects>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>
</font>

2.API模块

acme-example-api模块中的GreetingController

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

pom.xml配置:

<?xml version=<font>"1.0"</font><font> encoding=</font><font>"UTF-8"</font><font>?>
<project xmlns=</font><font>"http://maven.apache.org/POM/4.0.0"</font><font>
    xmlns:xsi=</font><font>"http://www.w3.org/2001/XMLSchema-instance"</font><font>
    xsi:schemaLocation=</font><font>"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"</font><font>>
    <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>
</font>

APP模块

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

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

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

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

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

server:
  servlet:
    context-path:/ acme

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

<project xmlns=<font>"http://maven.apache.org/POM/4.0.0"</font><font>
    xmlns:xsi=</font><font>"http://www.w3.org/2001/XMLSchema-instance"</font><font>
    xsi:schemaLocation=</font><font>"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"</font><font>>
    <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><b>true</b></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>
</font>

UI模块

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

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

<project xmlns=<font>"http://maven.apache.org/POM/4.0.0"</font><font>
    xmlns:xsi=</font><font>"http://www.w3.org/2001/XMLSchema-instance"</font><font>
    xsi:schemaLocation=</font><font>"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"</font><font>>
    <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>
                                                        <!-- <b>this</b> calls yarn install -->
                            <arguments>install</arguments>
                        </configuration>
                    </execution>
 
                    <execution>
                        <id>yarn build</id>
                        <goals>
                            <goal>yarn</goal>
                        </goals>
                        <configuration>
                                                        <!-- <b>this</b> 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>
</font>

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

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

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

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

body {
    --main-fg-color: red;
    --main-bg-color: yellow;
}
 
h1 {
    color: <b>var</b>(--main-fg-color);
}
 
#responseText {
    background: <b>var</b>(--main-bg-color);
}

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

注意package.json配置:

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

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

{
   <font>"presets"</font><font>: [
      [</font><font>"latest"</font><font>]
   ],
   </font><font>"plugins"</font><font>: []
}
</font>

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 --<b>public</b>-url ./ -d target/classes/<b>public</b> src/main/frontend/index.html
 Built in 585ms.

$ ll target/classes/<b>public</b>            
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=<font>"utf-8"</font><font>>
        <meta http-equiv=</font><font>"X-UA-Compatible"</font><font> content=</font><font>"IE=edge"</font><font>>
        <title>Acme App</title>
        <meta name=</font><font>"description"</font><font> content=</font><font>""</font><font>>
        <meta name=</font><font>"viewport"</font><font> content=</font><font>"width=device-width, initial-scale=1"</font><font>>
        <link rel=</font><font>"stylesheet"</font><font> href=</font><font>"main.d4190f58.css"</font><font>>
    <script src=</font><font>"main.d4190f58.js"</font><font>></script></head>
    <body>
        <h1>Acme App</h1>
 
        <button id=</font><font>"btnGetData"</font><font>>Fetch data</button>
        <div id=</font><font>"responseText"</font><font>></div>
        <script src=</font><font>"main.0632549a.js"</font><font> defer=</font><font>""</font><font>></script>
    </body>
</html>
</font>

下一个技巧是只使用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即可:

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

然后在index.html引用:

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

由于我们正在运行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可能非常适合你。

原文  https://www.jdon.com/51886
正文到此结束
Loading...