Skip to content

智能云图库项目初始化

后端项目初始化

环境准备

JDK:jdk11

Mysql:5.7

新建项目

IDEA编译器创建项目,选择Mavenjava11、打包方式选择jar

添加依赖

Spring WebMyBatisMySQLLombok

配置文件修改

修改资源目录下的配置文件为application.yml,指定项目启动的端口号和访问地址前缀、项目名称、数据库配置等。

整合依赖

MyBatis-Plus

MyBatis-Plus 🚀 为简化开发而生

xml
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.9</version>
</dependency>

启动类添加注解(@MapperScan("com.lihui.picturebackend.mapper")

v3.5.9 起,PaginationInnerInterceptor 已分离出来。如需使用,则需单独引入 mybatis-plus-jsqlparser 依赖 , 具体请查看 安装 一章。

引入 `MyBatis-Plus` 之后请不要再次引入 `MyBatis` 以及 `mybatis-spring-boot-starter`和`MyBatis-Spring`,以避免因版本差异导致的问题

Hutool 工具库

Hutool是主流的Java 工具类库,集合了丰富的工具类,涵盖字符串处理、日期操作、文件处理、加解密、反射、正则匹配等常见功能。它的轻量化和无侵入性让开发者能够专注于业务逻辑而不必编写重复的工具代码。例如,DateUtil.formatDate(new Date())可以快速将当前日期格式化为字符串。

Hutool 工具库入门和安装

xml
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.26</version>
</dependency>

Knife4j 接口文档

Knife4j 是基于 Swagger 接口文档的增强工具,提供了更加友好的 API 文档界面和功能扩展,例如动态参数调试、分组文档等。它适合用于 Spring Boot 项目中,能够通过简单的配置自动生成接口文档,让开发者和前端快速了解和调试接口,提高写作效率。

快速开始 | Knife4j

由于使用的是 Spring Boot 2.x,注意要选择 OpenAPl 2 的版本。

xml
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
    <version>4.4.0</version>
</dependency>

其他依赖

AOP 切面编程:

java
<!--切面编程-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

启动类添加@EnableAspectJAutoProxy(exposeProxy = true)

exposeProxy = true 的作用:通过 Spring AOP 提供对当前代理对象的访问,使得可以在业务逻辑中访问到当前的代理对象。你可以在方法执行时通过AopContext.currentProxy()获取当前的代理对象。

通用基础代码

自定义异常

在exception包下新建错误码枚举类:

java
@Getter
public enum ErrorCode {

    SUCCESS(0, "ok"),
    PARAMS_ERROR(40000, "请求参数错误"),
    NOT_LOGIN_ERROR(40100, "未登录"),
    NO_AUTH_ERROR(40101, "无权限"),
    NOT_FOUND_ERROR(40400, "请求数据不存在"),
    FORBIDDEN_ERROR(40300, "禁止访问"),
    SYSTEM_ERROR(50000, "系统内部异常"),
    OPERATION_ERROR(50001, "操作失败");

    /**
     * 状态码
     */
    private final int code;

    /**
     * 信息
     */
    private final String message;

    ErrorCode(int code, String message) {
        this.code = code;
        this.message = message;
    }

}

一般不建议直接抛出Java 内置的 RuntimeException,而是自定义一个业务异常,和内置的异常类区分开,便于定制化输出错误信息:

java
@Getter
public class BusinessException extends RuntimeException {

    /**
     * 错误码
     */
    private final int code;

    public BusinessException(int code, String message) {
        super(message);
        this.code = code;
    }

    public BusinessException(ErrorCode errorCode) {
        super(errorCode.getMessage());
        this.code = errorCode.getCode();
    }

    public BusinessException(ErrorCode errorCode, String message) {
        super(message);
        this.code = errorCode.getCode();
    }

}

为了更方便地根据情况抛出异常,可以封装一个ThrowUtils,类似断言类,简化抛异常的代码:

java
public class ThrowUtils {

    /**
     * 条件成立则抛异常
     *
     * @param condition        条件
     * @param runtimeException 异常
     */
    public static void throwIf(boolean condition, RuntimeException runtimeException) {
        if (condition) {
            throw runtimeException;
        }
    }

    /**
     * 条件成立则抛异常
     *
     * @param condition 条件
     * @param errorCode 错误码
     */
    public static void throwIf(boolean condition, ErrorCode errorCode) {
        throwIf(condition, new BusinessException(errorCode));
    }

    /**
     * 条件成立则抛异常
     *
     * @param condition 条件
     * @param errorCode 错误码
     * @param message   错误信息
     */
    public static void throwIf(boolean condition, ErrorCode errorCode, String message) {
        throwIf(condition, new BusinessException(errorCode, message));
    }
}

响应包装类

一般情况下,每个后端接口都要返回调用码、数据、调用信息等,前端可以根据这些信息进行相应的处理。

我们可以封装统一的响应结果类,便于前端统一获取这些信息。

通用响应类:

java
@Data
public class BaseResponse<T> implements Serializable {

    private int code;

    private T data;

    private String message;

    public BaseResponse(int code, T data, String message) {
        this.code = code;
        this.data = data;
        this.message = message;
    }

    public BaseResponse(int code, T data) {
        this(code, data, "");
    }

    public BaseResponse(ErrorCode errorCode) {
        this(errorCode.getCode(), null, errorCode.getMessage());
    }
}

但之后每次接口返回值时,都要手动 new一个BaseResponse 对象并传入参数,比较麻烦,我们可以新建一个工具类,提供成功调用和失败调用的方法,支持灵活地传参,简化调用。

java
public class ResultUtils {

    /**
     * 成功
     *
     * @param data 数据
     * @param <T>  数据类型
     * @return 响应
     */
    public static <T> BaseResponse<T> success(T data) {
        return new BaseResponse<>(0, data, "ok");
    }

    /**
     * 失败
     *
     * @param errorCode 错误码
     * @return 响应
     */
    public static BaseResponse<?> error(ErrorCode errorCode) {
        return new BaseResponse<>(errorCode);
    }

    /**
     * 失败
     *
     * @param code    错误码
     * @param message 错误信息
     * @return 响应
     */
    public static BaseResponse<?> error(int code, String message) {
        return new BaseResponse<>(code, null, message);
    }

    /**
     * 失败
     *
     * @param errorCode 错误码
     * @return 响应
     */
    public static BaseResponse<?> error(ErrorCode errorCode, String message) {
        return new BaseResponse<>(errorCode.getCode(), null, message);
    }
}

全局异常处理器

为了防止意料之外的异常,利用AOP 切面全局对业务异常和 RuntimeException进行捕获:

java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public BaseResponse<?> businessExceptionHandler(BusinessException e) {
        log.error("BusinessException", e);
        return ResultUtils.error(e.getCode(), e.getMessage());
    }

    @ExceptionHandler(RuntimeException.class)
    public BaseResponse<?> runtimeExceptionHandler(RuntimeException e) {
        log.error("RuntimeException", e);
        return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系统错误");
    }
}

请求包装类

对于"分页"、“删除某条数据”这类通用的请求,可以封装统一的请求包装类,用于接受前端传来的参数,之后相参数的请求就不用专门再新建一个类了。

分页请求包装类,接受页号、页面大小、排序字段、排序顺序参数:

java
@Data
public class PageRequest {

    /**
     * 当前页号
     */
    private int current = 1;

    /**
     * 页面大小
     */
    private int pageSize = 10;

    /**
     * 排序字段
     */
    private String sortField;

    /**
     * 排序顺序(默认降序)
     */
    private String sortOrder = "descend";
}

删除请求包装类,接受要删除数据的id 作为参数:

java
@Data
public class DeleteRequest implements Serializable {

    /**
     * id
     */
    private Long id;

    private static final long serialVersionUID = 1L;
}

全局跨域配置

跨域是指浏览器访问的URL(前端地址)和后端接口地址的域名(或端口号)不一致导致的,浏览器为了安全,默认禁止跨域请求访问。

为了开发调试方便,我们可以通过全局跨域配置,让整个项目所有的接口支持跨域,解决跨域报错。

新建config 包,用于存放所有的配置相关代码。全局跨域配置代码如下:

java
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 覆盖所有请求
        registry.addMapping("/**")
                // 允许发送 Cookie
                .allowCredentials(true)
                // 放行哪些域名(必须用 patterns,否则 * 会和 allowCredentials 冲突)
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .exposedHeaders("*");
    }
}

编写示例接口

java
@RestController
@RequestMapping("/")
public class MainController {

    /**
     * 健康检查
     */
    @GetMapping("/health")
    public BaseResponse<String> health() {
        return ResultUtils.success("ok");
    }
}

前端项目初始化

环境准备

Node.js:20.3.0

创建项目

使用 Vue 官方推荐的脚手架 create-vue 快速创建 Vue3 的项目

快速上手 | Vue.js

shell
npm create vue@latest
shell
F:\code\project_yupi\picture>npm create vue@latest
Need to install the following packages:
  create-vue@3.14.1
Ok to proceed? (y) y

Vue.js - The Progressive JavaScript Framework

 请输入项目名称: ... picture-frontend
 是否使用 TypeScript 语法? ... /
 是否启用 JSX 支持? ... /
 是否引入 Vue Router 进行单页面应用开发? ... /
 是否引入 Pinia 用于状态管理? ... /
 是否引入 Vitest 用于单元测试? ... /
 是否要引入一款端到端(End to End)测试工具? » 不需要
 是否引入 ESLint 用于代码质量检测? »
 是否引入 Prettier 用于代码格式化? ... /

正在初始化项目 F:\code\project_yupi\picture\picture-frontend...

项目初始化完成,可执行以下命令:

  cd picture-frontend
  npm install
  npm run format
  npm run dev

依赖安装

sh
npm install

可以看到 Vue 脚手架提供了一个调试工具 devtools),你可以使用它来调试分析项目:

前端工程化配置

脚手架已经帮我们整合了 Prettier 代码美化、ESLint 自动校验、TypeScript 类型校验,无需再自行整合。

引入组件库

快速上手 - Ant Design Vue

sh
npm i --save ant-design-vue@4.x

改变主入口文件 main.ts,全局注册组件

js
import './assets/main.css'

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from './router'
import Antd from 'ant-design-vue'
import App from './App.vue'
import 'ant-design-vue/dist/reset.css'

const app = createApp(App)

app.use(createPinia())
app.use(router)
app.use(Antd)

app.mount('#app')

开发规范

建议遵循 Vue3 的组合式 API (Composition API),而不是 选项式 API,开发更自由高效一些。

示例代码:

vue
<template>
  <div id="xxPage">

  </div>
</template>

<script setup lang="ts">

</script>

<style scoped>
#xxPage {
}

</style>

页面基本信息

可以修改项目根目录下的 index.html 文件,来定义页面的元信息,比如修改标题等等

全局通用布局

基础布局结构

在 layouts 目录下新建一个布局 BasicLayout.vue, 在 App.vue 全局页面入口文件中引入。

App.vue 代码如下:

vue
<template>
  <div id="app">
    <BasicLayout />
  </div>
</template>

<script setup lang="ts">
import BasicLayout from "@/layouts/BasicLayout.vue";
</script>

可以移除页面内的默认样式、并且移除 main.ts 中默认引入的 main.css,防止样式污染:

css
<style>
#app {
}
</style>

选用 Ant Design 组件库的 Layout 组件 ,先把【上中下】布局编排好,然后再填充内容:

vue
<template>
  <div id="basicLayout">
    <a-layout style="min-height: 100vh">
      <a-layout-header>Header</a-layout-header>
      <a-layout-content>Content</a-layout-content>
      <a-layout-footer>Footer</a-layout-footer>
    </a-layout>
  </div>
</template>

<script setup lang="ts">
</script>

<style scoped>
#basicLayout {
}
</style>

全局底部栏

通常用于展示版权信息:

vue
<a-layout-footer class="footer">
    <a href="https://lihuibear.cn" target="_blank">
        云图库 by lihui 
    </a>
</a-layout-footer>

样式:

vue
#basicLayout .footer {
  background: #efefef;
  padding: 16px;
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  text-align: center;
}

动态替换内容

项目使用了 Vue Router 路由库,可以在 router/index.ts 配置路由,能够根据访问的页面地址找到不同的文件并加载渲染。

修改 BasicLayout 内容部分的代码如下:

vue
<a-layout-content class="content">
  <router-view />
</a-layout-content>

修改样式,要和底部栏保持一定的外边距,否则内容会被遮住:

vue
<style scoped>
#basicLayout .content {
  background: linear-gradient(to right, #fefefe, #fff);
  margin-bottom: 28px;
  padding: 20px;
}
</style>

全局顶部栏

由于顶部栏的开发相对复杂,可以基于 Ant Design 的菜单组件 来创建 GlobalHeader 全局顶部栏组件,组件统一放在 components 目录中

先直接复制现成的组件示例代码到 GlobalHeader 中即可。

在基础布局中引入顶部栏组件:

vue
<a-layout-header class="header">
  <GlobalHeader />
</a-layout-header>

引入代码如下:

vue
<script setup lang="ts">
import GlobalHeader from "@/components/GlobalHeader.vue";
</script>

可以修改下全局 Header 的样式,清除一些默认样式(比如背景色等),样式代码如下:

css
#basicLayout .header {
  padding-inline: 20px;
  margin-bottom: 16px;
  color: unset;
  background: white;
}

接下来要修改 GlobalHeader 组件,完善更多内容。

1)给菜单外套一层元素,用于整体控制样式
vue
<div id="globalHeader">
  <a-menu v-model:selectedKeys="current" mode="horizontal" :items="items" />
</div>
2)根据我们的需求修改菜单配置,key 为要跳转的 URL 路径
vue
<script lang="ts" setup>
import { h, ref } from 'vue'
import { HomeOutlined } from '@ant-design/icons-vue'
import { MenuProps } from 'ant-design-vue'

const current = ref<string[]>(['home'])
const items = ref<MenuProps['items']>([
  {
    key: '/',
    icon: () => h(HomeOutlined),
    label: '主页',
    title: '主页',
  },
  {
    key: '/about',
    label: '关于',
    title: '关于',
  },
  {
    key: 'others',
    label: h('a', { href: 'https://www.codefather.cn', target: '_blank' }, '编程导航'),
    title: '编程导航',
  },
])
</script>
3)完善全局顶部栏,左侧补充网站图标和标题

修改 GlobalHeader 代码,补充 HTML:

tsx
<RouterLink to="/">
  <div class="title-bar">
    <img class="logo" src="../assets/logo.png" alt="logo" />
    <div class="title">鱼皮云图库</div>
  </div>
</RouterLink>

其中,RouterLink 组件的作用是支持超链接跳转(不刷新页面)。

补充 CSS 样式:

css
<style scoped>
.title-bar {
  display: flex;
  align-items: center;
}

.title {
  color: black;
  font-size: 18px;
  margin-left: 16px;
}

.logo {
  height: 48px;
}
</style>
4)完善顶部导航栏,右侧展示当前用户的登录状态(暂时用登录按钮代替)
vue
<div class="user-login-status">
  <a-button type="primary" href="/user/login">登录</a-button>
</div>
5)优化导航栏的布局,采用 栅格组件的自适应布局(左中右结构,左侧右侧宽度固定,中间菜单栏自适应)
vue
<a-row :wrap="false">
  <a-col flex="200px">
    <RouterLink to="/">
      <div class="title-bar">
        <img class="logo" src="../assets/logo.png" alt="logo" />
        <div class="title">鱼皮云图库</div>
      </div>
    </RouterLink>
  </a-col>
  <a-col flex="auto">
    <a-menu
      v-model:selectedKeys="current"
      mode="horizontal"
      :items="items"
    />
  </a-col>
  <a-col flex="120px">
    <div class="user-login-status">
      <a-button type="primary" href="/user/login">登录</a-button>
    </div>
  </a-col>
</a-row>

路由

修改路由配置

按需修改 router/index.ts 文件的 routes 配置,定义我们需要的页面路由,每个 path 对应一个 component(要加载的组件):P6MV6kCfQn5HwKPoo6nCijgumQHaR2dh69P0kMIKZLc=

vue
routes: [
  {
    path: '/',
    name: 'home',
    component: HomeView,
  },
  {
    path: '/about',
    name: 'about',
    // route level code-splitting
    // this generates a separate chunk (About.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import('../views/AboutView.vue'),
  },
],

观察上述代码,会发现 component 支持直接传入组件、或者使用 import 按需懒加载组件,按需加载是一种优化首次打开站点性能的方式。

路由跳转

给 GlobalHeader 的菜单组件绑定跳转事件:

vue
import { useRouter } from "vue-router";
const router = useRouter();

// 路由跳转事件
const doMenuClick = ({ key }: { key: string }) => {
  router.push({
    path: key,
  });
};

修改 HTML 模板,绑定事件:

vue
<a-menu
  v-model:selectedKeys="current"
  mode="horizontal"
  :items="items"
  @click="doMenuClick"
/>

高亮同步

刷新页面后,你会发现当前菜单项并没有高亮,所以需要同步路由的更新到菜单项高亮。

  1. 点击菜单时,Ant Design 组件已经通过 v-model 绑定 current 变量实现了高亮。
  2. 刷新页面时,需要获取到当前 URL 路径,然后修改 current 变量的值,从而实现同步。

使用 Vue Router 的 afterEach 路由钩子实现,每次改变路由或刷新页面时都会自动更新 current 的值,从而实现高亮:

vue
const router = useRouter();
// 当前选中菜单
const current = ref<string[]>([]);
// 监听路由变化,更新当前选中菜单
router.afterEach((to, from, next) => {
  current.value = [to.path];
});

请求

一般情况下,前端只负责界面展示和动效交互,尽量避免写复杂的逻辑;当需要获取数据时,通常是向后端提供的接口发送请求,由后端执行操作(比如保存数据)并响应数据给前端。

前端如何向后端发送请求呢?最传统的方式是使用 AJAX 技术。但其代码有些复杂,我们可以使用第三方的封装库,来简化发送请求的代码,比如主流的请求工具库 Axios。

请求工具库

安装请求工具类 Axios,参考官方文档:https://axios-http.com/docs/intro

sh
npm install axios

全局自定义请求

需要自定义全局请求地址等,参考 Axios 官方文档,编写请求配置文件 request.ts。包括全局接口请求地址、超时时间、自定义请求响应拦截器等。

响应拦截器的应用场景:我们需要对接口的 通用响应 进行统一处理,比如从 response 中取出 data;或者根据 code 去集中处理错误。这样不用在每个接口请求中都去写相同的逻辑。

比如可以在全局响应拦截器中,读取出结果中的 data,并校验 code 是否合法,如果是未登录状态,则自动登录。

示例代码如下,其中 withCredentials: true 一定要写,否则无法在发请求时携带 Cookie,就无法完成登录。

自动生成请求代码

如果采用传统开发方式,针对每个请求都要单独编写代码,很麻烦。

推荐使用 OpenAPI 工具,直接自动生成即可:https://www.npmjs.com/package/@umijs/openapi

按照官方文档的步骤,先安装:

sh
npm i --save-dev @umijs/openapi

项目根目录新建 openapi.config.js,根据自己的需要定制生成的代码:

typescript
import { generateService } from '@umijs/openapi'

generateService({
  requestLibPath: "import request from '@/request'",
  schemaPath: 'http://localhost:8123/api/v2/api-docs',
  serversPath: './src',
})

注意,要将 schemaPath 改为自己后端服务提供的 Swagger 接口文档的地址。

在 package.json 的 script 中添加 "openapi": "node openapi.config.js"

执行即可生成请求代码,还包括 TypeScript 类型:

全局状态管理

全局状态管理:所有页面全局共享的变量,而不是局限在某一个页面中。

适合作为全局状态的数据:已登录用户信息(每个页面几乎都要用)

Pinia 是一个主流的状态管理库,相比于 Vuex 来说使用更简单,可参考 入门文档 进行引入。

1、引入 Pinia

此处由于 create-vue 脚手架已经帮我们整合了 Pinia,无需手动引入,直接使用即可。

2、定义状态

在 src/stores 目录下定义 user 模块,定义了用户的存储、远程获取、修改逻辑:

js
import { defineStore } from "pinia";
import { ref } from "vue";

export const useLoginUserStore = defineStore("loginUser", () => {
  const loginUser = ref<any>({
    userName: "未登录",
  });

  async function fetchLoginUser() {
    // todo 由于后端还没提供接口,暂时注释
    // const res = await getCurrentUser();
    // if (res.data.code === 0 && res.data.data) {
    //   loginUser.value = res.data.data;
    // }
  }

  function setLoginUser(newLoginUser: any) {
    loginUser.value = newLoginUser;
  }

  return { loginUser, setLoginUser, fetchLoginUser };
});

使用状态

可以直接使用 store 中导出的状态变量和函数。

在首次进入到页面时,一般我们会尝试获取登录用户信息。修改 App.vue,编写远程获取数据代码:

可以直接使用 store 中导出的状态变量和函数。

在首次进入到页面时,一般我们会尝试获取登录用户信息。修改 App.vue,编写远程获取数据代码:

vue
const loginUserStore = useLoginUserStore()
loginUserStore.fetchLoginUser()

在任何页面中都可以使用数据,比如 GlobalHeader 全局顶部栏组件中直接展示:

typescript
{{ JSON.stringify(loginUserStore.loginUser) }}

修改全局顶部栏组件,在右侧展示登录状态:

vue
<div class="user-login-status">
  <div v-if="loginUserStore.loginUser.id">
    {{ loginUserStore.loginUser.userName ?? '无名' }}
  </div>
  <div v-else>
    <a-button type="primary" href="/user/login">登录</a-button>
  </div>
</div>

测试全局状态管理

在 userStore 中编写测试代码,测试用户状态的更新:

typescript
async function fetchLoginUser() {
  // 测试用户登录,3 秒后登录
  setTimeout(() => {
    loginUser.value = { userName: '测试用户', id: 1 }
  }, 3000)
}

页面开发流程

我们通过开发一个简易的示例页面,来了解页面开发的流程。

1)新建 src/pages 目录,用于存放所有的页面文件

然后在 pages 目录下新建页面文件,将所有页面按照 url 层级进行创建,并且页面名称尽量做到“见名知意”。

其中,/user/login 地址就对应了 UserLoginPage。

此处我们新建 HomePage.vue 即可。

2)每次新建页面时,需要在 router/index.ts 中配置路由,比如欢迎页的路由为

js
const routes: Array<RouteRecordRaw> = [
  {
    path: "/",
    name: "home",
    component: HomeView,
  },
  ...
]

然后在路由文件中,引入页面 HomePage:

typescript
import HomePage from "@/pages/HomePage.vue";

const routes: Array<RouteRecordRaw> = [
  {
    path: "/",
    name: "home",
    component: HomePage,
  },
  ...
]

任意修改页面代码:

vue
<template>
  <div id="homePage">
    <h1>{{ msg }}</h1>
  </div>
</template>

<script setup lang="ts">
const msg = "欢迎参观~";
</script>

<style scoped>
#homePage {
}
</style>

Ant Design Vue 改为中文

vue
import zhCN from 'ant-design-vue/es/locale/zh_CN';
const locale = zhCN

<a-config-provider :locale="locale">

</a-config-provider>

Released under the MIT License.