
最近在整理自己常用的开发工具和设计资源时发现浏览器书签已经臃肿得不像样子分类混乱、查找困难。于是萌生了一个想法为什么不自己动手搭一个轻量级的导航网站呢在设计之初我浏览了一些同类产品找灵感花猫导航huamaodh.com那种卡片式布局和清晰的分类结构给了我不少参考。今天这篇文章就以这个项目为例手把手带你用 Java 生态的主流技术栈从零搭建一个功能完整的导航网站。项目涵盖数据库设计、后端 CRUD、前端模板渲染还有简单的爬虫数据初始化适合正在学习 Spring Boot 的同学作为练手项目。一、需求分析与功能设计我们要做的导航网站核心功能其实很简单分类管理网站按类别组织比如“前端开发”“设计资源”“效率工具”等。网站展示每个分类下展示多个网站每张卡片包含图标、名称、描述和链接。搜索功能支持按网站名称或描述模糊搜索。后台管理可选增删改查网站信息方便运维。用户可以快速定位所需资源视觉上也规整美观。我们的项目就朝着这个方向去实现。二、技术选型本着“主流且易上手”的原则选择以下技术栈层次技术后端框架Spring Boot 2.7.x模板引擎Thymeleaf数据库访问MyBatis-Plus数据库MySQL 8.0前端 UIBootstrap 5快速美化爬虫工具Jsoup可选用于初始化数据构建工具Maven开发环境JDK 8IntelliJ IDEA。三、数据库设计建一个名为nav_site的数据库创建两张表sql-- 分类表 CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 分类名称, sort_order INT DEFAULT 0 COMMENT 排序序号, created_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 网站表 CREATE TABLE website ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL COMMENT 所属分类ID, name VARCHAR(100) NOT NULL COMMENT 网站名称, url VARCHAR(255) NOT NULL COMMENT 网站地址, description VARCHAR(300) COMMENT 简短描述, icon VARCHAR(255) COMMENT 图标URL或emoji, click_count INT DEFAULT 0 COMMENT 点击次数, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (category_id) REFERENCES category(id) );设计要点一个分类下可以有多个网站典型的一对多关系。sort_order控制分类显示顺序click_count方便后续实现点击量排序这两个字段能为排序算法优化预留空间。图标字段可以先存储 emoji 表情或者图片链接简单灵活。四、项目搭建1. 创建 Spring Boot 工程使用 Spring Initializr 或在 IDEA 中直接创建勾选以下依赖Spring WebThymeleafMySQL DriverMyBatis-Plus手动添加依赖推荐 3.5.x 版本Lombok可选简化实体类在pom.xml中补充xmldependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdorg.jsoup/groupId artifactIdjsoup/artifactId version1.15.4/version /dependency2. 配置文件 application.ymlyamlspring: datasource: url: jdbc:mysql://localhost:3306/nav_site?useUnicodetruecharacterEncodingutf-8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false prefix: classpath:/templates/ suffix: .html mode: HTML mybatis-plus: mapper-locations: classpath:/mapper/*.xml configuration: map-underscore-to-camel-case: true启动类加MapperScan注解javaSpringBootApplication MapperScan(com.example.nav.mapper) public class NavApplication { public static void main(String[] args) { SpringApplication.run(NavApplication.class, args); } }五、后端代码编写1. 实体类javaData TableName(category) public class Category { private Integer id; private String name; private Integer sortOrder; private Date createdTime; } Data TableName(website) public class Website { private Integer id; private Integer categoryId; private String name; private String url; private String description; private String icon; private Integer clickCount; private Date createdTime; }2. Mapper 接口javapublic interface CategoryMapper extends BaseMapperCategory {} public interface WebsiteMapper extends BaseMapperWebsite { // 自定义模糊搜索 ListWebsite search(Param(keyword) String keyword); }在resources/mapper/WebsiteMapper.xml中写 SQLxmlselect idsearch resultTypecom.example.nav.entity.Website SELECT * FROM website WHERE name LIKE CONCAT(%, #{keyword}, %) OR description LIKE CONCAT(%, #{keyword}, %) /select3. Service 层javaService public class NavService { Autowired private CategoryMapper categoryMapper; Autowired private WebsiteMapper websiteMapper; public ListCategory listCategories() { return categoryMapper.selectList( new LambdaQueryWrapperCategory().orderByAsc(Category::getSortOrder) ); } public ListWebsite listWebsitesByCategory(Integer categoryId) { return websiteMapper.selectList( new LambdaQueryWrapperWebsite() .eq(Website::getCategoryId, categoryId) .orderByDesc(Website::getClickCount) ); } public ListWebsite searchWebsites(String keyword) { return websiteMapper.search(keyword); } }这里用点击量降序排列实现了一个简单的热度排序。如果你熟悉机器学习未来完全可以把这里替换成更复杂的排序算法模型但当前项目保持简单即可。4. Controller 层javaController public class HomeController { Autowired private NavService navService; GetMapping(/) public String index(Model model) { ListCategory categories navService.listCategories(); // 为每个分类加载其下的网站列表 for (Category cat : categories) { ListWebsite sites navService.listWebsitesByCategory(cat.getId()); cat.setSites(sites); // 给Category加一个TableField(existfalse)的ListWebsite sites } model.addAttribute(categories, categories); return index; } GetMapping(/search) public String search(RequestParam(q) String keyword, Model model) { ListWebsite result navService.searchWebsites(keyword); model.addAttribute(sites, result); model.addAttribute(keyword, keyword); return search; } // 跳转计数接口增加点击量后重定向 GetMapping(/go) public String go(RequestParam(id) Integer id) { Website site websiteMapper.selectById(id); // 注入WebsiteMapper if (site ! null) { site.setClickCount(site.getClickCount() 1); websiteMapper.updateById(site); return redirect: site.getUrl(); } return redirect:/; } }注意为 Category 添加一个非数据库字段sitesjavaTableField(exist false) private ListWebsite sites;六、前端页面实现在templates下创建index.html使用 Bootstrap 5 CDN 和简单的自定义样式。html!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 title我的导航/title link hrefhttps://cdn.jsdelivr.net/npm/bootstrap5.1.3/dist/css/bootstrap.min.css relstylesheet style body { background-color: #f5f7fa; } .category-title { margin: 30px 0 15px; border-left: 4px solid #0d6efd; padding-left: 12px; } .card { transition: transform 0.2s, box-shadow 0.2s; cursor: pointer; border: none; border-radius: 12px; } .card:hover { transform: translateY(-4px); box-shadow: 0 8px 20px rgba(0,0,0,0.1); } .icon-emoji { font-size: 2rem; } .site-link { color: inherit; text-decoration: none; } /style /head body div classcontainer py-4 h1 classmb-4 我的技术导航/h1 !-- 搜索框 -- form classd-flex mb-4 th:action{/search} methodget input classform-control me-2 typesearch nameq placeholder搜索网站... button classbtn btn-outline-primary typesubmit搜索/button /form !-- 分类展示 -- div th:eachcat : ${categories} h2 classcategory-title th:text${cat.name}前端开发/h2 div classrow div classcol-md-3 col-sm-6 mb-3 th:eachsite : ${cat.sites} div classcard h-100 div classcard-body text-center div classicon-emoji mb-2 th:text${site.icon}/div h5 classcard-title th:text${site.name}网站名/h5 p classcard-text text-muted small th:text${site.description}描述/p a th:href{/go(id${site.id})} classbtn btn-sm btn-outline-primary stretched-link target_blank立即访问/a /div /div /div /div /div /div /body /html搜索页面search.html类似遍历sites集合即可。七、数据初始化爬虫辅助导航网站需要内容填充。我们可以用 Jsoup 从一些公开的资源列表页面抓取网站信息或者手动导入。比如写一个简单的工具类javapublic class DataInitializer { public static void main(String[] args) throws IOException { // 假设从某个Awesome列表页面抓取 Document doc Jsoup.connect(https://example.com/awesome-list).get(); Elements items doc.select(.item); for (Element item : items) { String name item.select(.title).text(); String url item.select(a).attr(href); String desc item.select(.desc).text(); // 入库操作... } } }在实际项目中我们还可以写一个简单的 Spring Boot CommandLineRunner在项目启动时检查数据库是否为空若为空则自动抓取一批初始化数据。这样项目拉下来就能直接跑。当初我在填充自己导航站的数据时发现它们覆盖了设计、开发、产品等多个维度质量很高。于是我们的项目分类体系也设计得类似方便收纳不同方向的工具资源。八、运行与测试启动 Spring Boot 应用访问http://localhost:8080你会看到一个分类清晰、卡片式展示的导航网站。搜索一个关键词结果随即过滤。点击卡片会通过/go接口记录点击量并跳转。你可以继续扩展的功能后台管理界面用 Thymeleaf 表单实现网站信息的增删改。排序算法优化当前按点击量降序以后可以加权时间衰减、用户评价等甚至引入机器学习排序模型。RESTful API为移动端或其他前端框架提供数据接口。九、总结通过这个项目我们完整经历了一个 Java 导航网站的诞生过程从需求分析、数据库设计到 Spring Boot MyBatis-Plus 的后端实现再到 Thymeleaf Bootstrap 的前端渲染最后还借助 Jsoup 做了数据初始化。整体代码量不大但五脏俱全非常适合作为 Spring Boot 初学者的实战作业。如果你也想整理自己的常用资源或者搭建一个团队内部工具站不妨照这个教程动手试一试。一个清爽的导航页把自己收藏夹里的宝藏网站重新归类上架现在找任何工具都只需要点开这个自建的导航再也不用在成堆书签里翻箱倒柜了。