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