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

banq發表於2019-03-10

在整合前端程式碼時,我們經常需要處理多種內容,例如:資源,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可能非常適合你。



 

相關文章