1. 概述

Hilla 是一个基于 Java 的全栈 Web 框架。它允许我们在 Spring Boot 应用中添加 React 视图,并通过类型安全的 RPC 从 TypeScript 调用后端 Java 服务

该框架使用 Vaadin UI 组件集,并与 Vaadin Flow 兼容。两者都属于 Vaadin 平台。本教程将介绍 Hilla 开发的基础知识。

2. 创建 Hilla 项目

可通过以下两种方式创建项目:

  • 在 Spring Initializr 添加 Vaadin 依赖
  • 在 Vaadin Start 下载定制启动器

若需将 Hilla 集成到现有 Spring Boot 项目,需在 Maven 的 pom.xml 中添加以下 BOM(物料清单):

<properties>
    <vaadin.version>24.4.10</vaadin.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.vaadin</groupId>
            <artifactId>vaadin-bom</artifactId>
            <version>${vaadin.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

然后添加包含 Hilla 的 Vaadin 平台依赖:

<dependency>
    <groupId>com.vaadin</groupId>
    <artifactId>vaadin-spring-boot-starter</artifactId>
</dependency>

最后创建主题配置文件 src/main/frontend/themes/hilla/theme.json

{
    "lumoImports" : [ "typography", "color", "sizing", "spacing", "utility" ]
}

更新 Spring Boot 主类,添加 @Theme 注解并实现 AppShellConfigurator

@Theme("hilla") // 需与主题文件夹名称匹配
@SpringBootApplication
public class DemoApplication implements AppShellConfigurator {
    // 应用代码省略
}

3. 启动应用

Hilla 将 React 视图和 Java 后端源码统一在一个全栈项目中。视图需定义在 src/main/frontend/views 目录下。

创建 src/main/frontend/views/@index.tsx 文件:

export default function Index() {
    return (
        <h1>Hello world</h1>
    );
}

通过 mvn spring-boot:run 或 IDE 启动应用:

⚠️ 首次启动较慢(需下载 Maven/npm 依赖并启动 Vite 开发服务器),后续启动会更快。

访问 localhost:8080 可看到 "Hello world":

浏览器显示 "Hello world" 文本

4. 使用 @BrowserCallable 调用服务端方法

Hilla 的独特之处在于前后端通信方式。与传统 Spring Boot + React 分离架构不同,Hilla 采用统一的 RPC 调用 Java 服务,遵循 BFF 架构

Contact 实体为例:

@Entity
public class Contact {
    @Id
    @GeneratedValue
    private Long id;
    @Size(min = 2)
    private String name;
    @Email
    private String email;
    private String phone;
    // 构造器、getter/setter 省略
}

配合 Spring Data JPA 仓库:

public interface ContactRepository extends 
    JpaRepository<Contact, Long>, 
    JpaSpecificationExecutor<Contact> {
}

@BrowserCallable 注解服务类,@AnonymousAllowed 允许匿名访问:

@BrowserCallable
@AnonymousAllowed
public class ContactService {
    private final ContactRepository contactRepository;

    public ContactService(ContactRepository contactRepository) {
        this.contactRepository = contactRepository;
    }

    public List<Contact> findAll() {
        return contactRepository.findAll();
    }

    public Contact findById(Long id) {
        return contactRepository.findById(id).orElseThrow();
    }

    public Contact save(Contact contact) {
        return contactRepository.save(contact);
    }
}

空值处理优化:在服务包中创建 package-info.java 简化 TypeScript 类型:

@NonNullApi
package com.example.application;
import org.springframework.lang.NonNullApi;

更新 @index.tsx 显示联系人列表:

export default function Contacts() {
    const contacts = useSignal<Contact[]>([]);

    async function fetchContacts() {
        contacts.value = await ContactService.findAll();
    }

    useEffect(() => {
        fetchContacts();
    }, []);

    return (
        <div className="p-m flex flex-col gap-m">
            <h2>Contacts</h2>
            <Grid items={contacts.value}>
                <GridSortColumn path="name">
                    {({item}) => <NavLink to={`contacts/${item.id}/edit`}>{item.name}</NavLink>}
                </GridSortColumn>
                <GridSortColumn path="email"/>
                <GridSortColumn path="phone"/>
            </Grid>
        </div>
    );
}

Frontend/generated 导入 ContactContactService,Hilla 使用基于 Preact signals 的信号机制。

浏览器显示联系人数据网格

5. 配置视图与布局

Hilla 采用基于文件的路由系统,所有视图位于 src/main/frontend/views 目录。

5.1 视图命名规范

文件/文件夹名 路由映射 说明
@index.tsx / 目录首页
@layout.tsx - 目录布局文件
view-name.tsx /view-name 普通视图
{parameter} - 路径参数捕获
{parameter}.tsx /:parameter 参数化视图
{{parameter}}.tsx /:parameter? 可选参数视图
{...wildcard}.tsx /* 通配符匹配

5.2 视图配置

导出 ViewConfig 类型的 config 常量:

export const config: ViewConfig = {
    title: "Contact List",
    menu: {
        order: 1,
    }
}

export default function Index() {
    // 代码省略
}

5.3 定义布局

src/main/frontend/views 创建 @layout.tsx

export default function MainLayout() {
    return (
        <div className="p-m h-full flex flex-col box-border">
            <header className="flex gap-m pb-m">
                <h1 className="text-l m-0">My Hilla App</h1>
                {createMenuItems().map(({to, title}) => (
                    <NavLink to={to} key={to}>{title}</NavLink>
                ))}
            </header>
            <Suspense>
                <Outlet/>
            </Suspense>
        </div>
    );
}

createMenuItems() 自动生成导航菜单。

浏览器显示带导航菜单的联系人视图

6. 构建表单与输入验证

创建编辑视图 views/contacts/{id}/edit.tsx

export default function ContactEditor() {
    const {id} = useParams();
    const navigate = useNavigate();

    const {field, model, submit, read} = useForm(ContactModel, {
        onSubmit: async contact => {
            await ContactService.save(contact);
            navigate('/');
        }
    })

    async function loadUser(id: number) {
        read(await ContactService.findById(id))
    }

    useEffect(() => {
        if (id) {
            loadUser(parseInt(id))
        }
    }, [id]);

    return (
        <div className="flex flex-col items-start gap-m">
            <TextField label="Name" {...field(model.name)}/>
            <TextField label="Email" {...field(model.email)}/>
            <TextField label="Phone" {...field(model.phone)}/>
            <div className="flex gap-s">
                <Button onClick={submit} theme="primary">Save</Button>
                <Button onClick={() => navigate('/')} theme="tertiary">Cancel</Button>
            </div>
        </div>
    );
}

关键点

  • useForm 自动绑定字段和验证规则
  • field() 方法同步 Java 实体验证注解
  • submit() 触发表单提交

浏览器显示联系人编辑表单(邮箱字段验证错误)

7. 使用 AutoCrud 实现自动化 CRUD

Hilla 作为全栈框架,可自动化生成 CRUD 操作。更新服务类:

@BrowserCallable
@AnonymousAllowed
public class ContactService extends CrudRepositoryService<Contact, Long, ContactRepository> {
    public List<Contact> findAll() {
        return getRepository().findAll();
    }
    public Contact findById(Long id) {
        return getRepository().findById(id).orElseThrow();
    }
}

创建 frontend/views/auto-crud.tsx

export default function AutoCrudView() {
    return <AutoCrud service={ContactService} model={ContactModel} className="h-full"/>
}

仅需一行代码即可生成完整的增删改查界面!

浏览器显示左侧联系人列表,右侧编辑表单

替代方案

  • 仅需列表:使用 AutoGrid 组件
  • 仅需表单:使用 AutoForm 组件

8. 生产环境构建

使用 production Maven 配置文件构建:

<profile>
    <id>production</id>
    <dependencies>
        <dependency>
            <groupId>com.vaadin</groupId>
            <artifactId>vaadin-core</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>com.vaadin</groupId>
                    <artifactId>vaadin-dev</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>com.vaadin</groupId>
                <artifactId>vaadin-maven-plugin</artifactId>
                <version>${vaadin.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>build-frontend</goal>
                        </goals>
                        <phase>compile</phase>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</profile>

执行构建命令:

./mvnw package -Pproduction

构建效果: ✅ 优化前端 JavaScript 包
❌ 禁用开发调试功能

9. 总结

本文介绍了 Hilla 框架的核心特性:

  • 在 Spring Boot 中集成 React 视图
  • 通过 @BrowserCallable 实现类型安全的 RPC 通信
  • 基于文件的路由系统
  • 自动化表单验证和 CRUD 生成
  • 生产环境优化方案

Hilla 简化了全栈开发流程,特别适合需要快速构建企业级应用的场景。完整代码示例可在 GitHub 获取。


原始标题:Introduction to the Hilla Framework | Baeldung