在集成前端代码时,我们经常需要处理多种内容,例如:资源,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可能非常适合你。