环境配置
maven/springboot/mysql
node.js/vue/element-ui
安装navicat数据库可视化/postman发送http请求
后端Springboot
自顶向下开发(需求先行,分工明确)
controller->service->serviceIml->mapper->mapper.xml
基本curd操作:create update read delete,其中增删改是返回被影响的行数 接受类型为int或void 一般行数大于0就代表sql操作成功
这里重点讨论查询 返回类型为一行记录 对应到后端就是一个实体对象或map键值对 一般查询结果集ResultSet还有下一个元素就代表查询结果不为空
User user 或 List<User> list 或 Map<String,Object> map; 亦或是List<Map<String,Object>> lists 其中list和lists差不多User适合业务固定的场景,Map更灵活范围广
因为map作为键值对(key-value)的集合,又因为一张表对应一个类,一条记录对应一个对象,那么表中的记录不一定都是string类型的,所以值用Object可以自动类型转换,支持范围广
如json数据:{
"name" : "小明",
"age" : 22,
"birth" : "2003/11/10"
}
这里的姓名,年龄,出生日期分别为字符串,短整型,日期型,用object接收比string更加灵活可靠
数据库设计中年龄实际上为冗余字段,可由出生日期计算得出(题外话)
关键字 模糊like concat('%',#{key},'%');分页limit page;联结on key join a.id = b.id 属性in列表
聚合函数 max最大 min最小 sum总和 count行数 avg平均 group对查询结果分组
过滤条件having match against全文搜索 公用表达式CTE with()
事务管理 开始begin 提交commit 回滚rollback 复合索引 on表名(属性一,属性二)
触发器 create trigger 名称 before curd操作 on 表名 set属性值..
springboot+vue项目
创建项目
maven更改setting.xml 换为淘宝或者阿里云镜像仓库
idea在设置中搜索maven,更改settings.xml路径和本地仓库路径
SpringIniticalizr 如果路径不对,url更改为:https://start.aliyun.com 语言java 打包方式jar
勾选maven配置 java文件夹下新建包com.demo group公司名 选择web依赖:spring web
选择springboot版本号 选择项目保存位置 finish完成 自动导入依赖
snapshot快照代表不稳定的开发版本 release代表稳定的发行版本
新建启动类SpringbootApplication添加@SpringBootApplication注解 mapper扫描换成自己的包名
@SpringBootApplication
@MapperScan("com.example.mapper")
public class SpringbootApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootApplication.class, args);
@RequestMapping("/")
String home(){
return "hello world";
}
}
}
这里写一个根路由,执行home方法打印hello world;
然后pom.xml引入版本 添加依赖dependency 选择配置aliyun镜像 提高jar包下载速度 如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.9</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version> <!-- 请检查版本的兼容性 -->
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<repositories>
<repository>
<id>public</id>
<name>aliyun nexus</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
</repository>
</repositories>
项目主方法main()启动后,默认8080端口,浏览器访问 localhost:8080
恭喜启动成功
配置数据库连接:
新建 application.properties 或 application.yml ,可以配置端口;mybatis配置;数据库连接参数,比如数据库的 URL、用户名、密码等:
server.port = 9090 //配置端口
//mybatis指向正确mapper路径
mybatis.mapper-locations = classpath:mapper/*.xml
//properties使用 点 指代下一级
spring.datasource.url=jdbc:mysql://localhost:3306/你的数据库名称
spring.datasource.username=你的账号
spring.datasource.password=你的密码
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
//yml使用 冒号 层级划分
spring:
datasource:
url: jdbc:mysql://localhost:3306/你的数据库名称
username: 你的账号
password: 你的密码
driver-class-name: com.mysql.cj.jdbc.Driver
一:控制器层
控制器一般要做三件事:确定对应请求/参数类型,执行crud业务代码,返回数据给前端展示
@RestController
:表示该类是一个 RESTful 风格的控制器,通常用于处理返回 JSON 或 XML 等数据格式的请求,它会自动在方法上应用@ResponseBody
注解,因此不需要显式地添加。@Controller
:用于传统的 MVC 控制器,返回视图(如 JSP 页面、HTML 页面等)时使用,需要配合@ResponseBody
或@RequestMapping
的返回值类型来确定返回内容。
如果想要后端返回json数据而不是视图,应该使用@RestController注解
请求类型
get post put delete对应sql语句中的select insert update delete
@Get/Post/Put/DeleteMapping是映射前端发送的http请求到java方法
参数类型
@RequestBody 传递json请求体如:{"id":1,"name":"john",sex:"male"}
@RequestParam 传递参数 指定默认值(defaultValue="1") 路由("/select")上可用 ?参数=value 传参 若有多个注释,可以用&拼接参数
例如: /select?id=1&id=2&id=3 spring会将参数自动转化为list列表 List<Integer> ids
这样在mapper.xml里参数ids可以用foreach标签遍历每个元素<foreach >
@PathVariable路径传参 路由("/select/{id}") 如: /select/1 这里1就指带id=1
@DeleteMapping("/delete/{id}")
public Result deleteById(@PathVariable Integer id ){
userService.deleteById(id);
return Result.success();
}
这里定义一个路径变量id,靠前端this.$request.delete('/delete/${this.id}')发送delete请求删除该行记录 id与前端输入框数据双向绑定下面会用到
返回类型
根据业务确定返回类型 如果查询一条记录Entity对象或Map<String,Object>键值对即可
多条记录将键值对再封装成列表List<Map<String,Object>>
传递数据方式
执行业务代码就是调用service定义的方法,选择传入前端请求发送的参数
二:业务逻辑层
先创建接口(方法都是抽象的) 再定义其实现类
调用mapper定义的方法,执行mapper定义的方法
查询一般根据 id查询返回自定义实体类型 查询全部记录,由于多条记录List<自定义实体类型>
分页查询返回类型Page<Info>步骤为,开启分页->查询全部记录并保存->展示该页的数据
@Transcation类注解 用于事务管理
数据库操作全部成功则提交事务 全部失败则回滚事务 防止数据库中数据不一致
日常更新,未完待续
三:数据访问层
xml文件必备表头
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
resultType指定sql查询返回结果的返回类型 resultType指定sql查询传入参数的类型
通常查询指定返回类型,增改指定传入参数类型,而删除无需指定
<select id="selectAll" resultType="com.example.entity.表名">
select
*
from 表名
<where>
<if test="title!=null">and title like concat('%',#{title},'%')</if>
</where>
order by id desc
</select>
这个案例中 id作为标识符是mapper定义的方法与sql语句的映射,selectAll方法会执行sql语句
将查询的结果会转化为自定义实体entity类型的对象
where后跟条件 concat拼接字符串,%表示零个或多个字符(0,n),下划线_表示一个字符(1)
这里在title前后添加若干个字符,like模糊匹配任意位置的字符串
表示模糊(like)查询含有(not null)title字符串的记录
order by表示以什么属性排序 desc表示降序
多表联合查询 一般用join连接表 如果返回类型不用resultMap手动映射就得在
返回的自定义实体类型中将要查询的属性及其getter/setter方法定义出来
这里推荐resultMap手动映射,毕竟一个实体类对应一张表,不要将其他表的属性添加进去
<select id="selectAll" resultType="com.对应包名.实体类包.对应实体类名">
select
blog.* ,category.name as categoryName ,user.name as userName
from blog
left join category
on blog.category_id = category.id
left join user
on blog.user_id = user.id
<where>
<if test="title!=null">and blog.title like concat('%',#{title},'%')</if>
<if test="categoryName!=null">and category.name like concat('%',#{categoryName},'%')</if>
<if test="userName!=null">and user.name like concat('%',#{userName},'%')</if>
</where>
order by id desc
</select>
查询了博客表的全部属性,类别表的名称,用户表的名称
as起别名 left join左连接 这里是三表blog/user/category联查
由于blog表的外键category_id是category表的主键id blog表的外键user_id是user表的主键id
所以on指代连接条件外键与主键相等;
推荐使用postman发送http请求给后端,传递json数据
后端日志常见报错
DuplicateKeyException 异常 重复插入数据报错插入前,你可以检查 字段的值 是否已存在,避免插入重复的字段;
返回类型报错:插入操作返回值为int,即被影响行数,controller层插入方法返回类型要保持一致, 定义为Integer;
No getter for property没有属性的getter方法,可以查看实体类和Mapper.xml里的属性和数据库字段是否一致;
BindingException,表示 MyBatis 找不到定义的方法,可能mapper标签没有加命名空间namespace或sql操作没有定义id,或没有定义mapper.xml位置:mybatis.mapper-locations = classpath:mapper/*.xml;
500 Internal Server Error 使用@RestController注解,@Controller
400 Bad Request 请求体格式有错误,如果控制器定义http映射的方法接收的数据类型是@RequestBody类型,那么前端需要发送json类型的数据;
Failed to configure a DataSource,无法正确配置数据源(DataSource)application.在 application.properties
文件中,所有的配置项都应该使用 =
来连接键和值,不应该等于号(=)和冒号(:)交替使用 ,这样 Spring Boot 才能正确读取这些配置
AnnotationFormatError 注释格式错误或者类似的问题,可能是因为 MyBatis 版本与 Spring Boot 版本不兼容,或者 @MapperScan
配置不正确。pom.xml添加版本2.2.0的jar包:mybatis-spring-boot-starter
常见注解:自动注入、事务管理、日志记录、异常处理等
自动注入:
@Autowried按照类型查找bean @Resource按照名称查找bean
@Autowired
是 Spring 提供的,按 类型 自动注入,自动注入依赖,减少实例化对象的麻烦。
@Resource
是 JavaEE 的注解,按 名称 自动注入,只有当名称匹配失败时,才会按类型注入。
例如,在传统的 Java 编程中,你可能会这样手动创建对象:
public Class UserService{
private UserMapper userMapper;
public UserService(){
this.userMapper = new UserMapper();
}
}
而在 Spring 中,你可以依赖注入 UserRepository,让 Spring 容器来管理它的生命周期,减少手动创建对象的工作:
@Service
public class UserService {
@Autowired
private UserMapper userMapper; // 通过注入的方式获得依赖
}
事务管理:
在 Service 层使用 @Transactional
默认情况下,@Transactional
会在遇到 RuntimeException
或 Error
时回滚事务
//事务默认回滚规则
@Transactional(rollbackFor=Exception.class):当方法抛出指定类型的异常时,事务就会回滚
//事务传播行为
@Transactional(propagation=PROPAGATION.REQUIRES_NEW)
默认为required:没有事务则创建,如果存在当前事务则加入该事务
requires_new:总是新建事务,挂起当前事务
nested:如果存在当前事务,则嵌套当前事务
//事务传播级别
隔离级别决定当前事务能否看到另一个事务未提交的修改
@Transactional(isolation = Isolation.SERIALIZABLE) //最高级别 事务完全隔离
级别由低到高:READ_UNCOMMITTED READ_COMMITTED REPEATABLE_READ SERIALIZABLE
日志记录:
application .yml or .properties 文件配置
logging.level.org.springframework.web=DEBUG
logging.level.com.example=INFO
logging:
level:
org:
springframework:
web:DEBUG
com:
example:INFO
public void createActivity(Activity activity) {
logger.info("create activity with name:{}"+activity.getName());
try{
logger.debug("debug:{}",activity);
}catch (Exception e){
logger.error("error:{}",e.getMessage());
}
}
logger.info()记录信息级别日志;logger.debug()记录调试级别日志;logger.error()记录错误日志;
异常处理:
首先新建自定义异常类
public class ResourceNotFound extends RuntimeException{
public ResourceNotFound(String message){
super(message);
}
}
然后控制器抛出异常
@GetMapping("/search/{id}")
public Result search(@PathVariable Integer id){
if(id==null){
throw new ResourceNotFound("Not found with id:"+id);
}
return Result.success();
}
最后使用@ControllerAdvice来全局处理异常 @ExceptionHandler填入自定义异常类
当控制器中的方法抛出异常时 会自动触发GlobalHandler中的异常处理方法,返回一个404状态码和错误消息;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFound.class)
public ResponseEntity<String> handleException(ResourceNotFound e){
return new ResponseEntity<> (e.getMessage(), HttpStatus.NOT_FOUND);
}
}
Spring Boot 的自动配置、Spring 容器的使用
Spring Security(权限管理)Spring Batch(批处理)
Spring Security和Json Web token(JWT)安全认证差不多吗?
Spring Security 可以使用 JWT 来处理认证(验证用户身份),通常将 JWT 存储在客户端(例如,浏览器的 LocalStorage 或 HTTP Header 中),每次客户端请求时,将 JWT 作为 Authorization Header 发送给服务器。
Spring Security 负责验证这个 JWT 是否有效、是否过期、是否授权访问某些资源等。
日常更新,未完待续
前端Vue
一:创建项目
安装脚手架,快速搭建项目 npm install -g @vue/cli
cmd输入vue --version有版本号即可
进入指定目录创建项目 vue create 项目名
进入文件夹 cd 项目名
运行项目 npm run serve
默认本地主机8080端口运行
二:vue项目开发流程
需求分析(功能模块,ui风格,API 设计、数据格式、接口规范),项目初始化(vue cli / vite), 组件化开发(组件管理状态和视图),接口调用与 数据处理(将后端返回的数据存储在vue的data()中), 路由设计(确保不同的url展示不同的component组件页面),
ui交互与状态管理(v-model数据双向绑定,监听用户的点击@click与输入), 页面布局与样式(element-plus组件库快速搭建), 前后端联调(确保前端通过api请求得到正确数据,错误根据状态码debug), 性能优化与部署(lazy loading懒加载缩短页面加载时间,设置Nginx服务器部署反向代理负载均衡)
了解el组件和js的发送请求到后端的methods,开发一张表的管理功能流程,复制粘贴模板表,table-column对应表增改字段名,form-item表单增改input/upload/radio-group/date-picker
ctrl+r全部替换接口路径至现改controller下存在的,index.js下添加Manager.vue下menu菜单中添加当前页面,类似curd页面复制模板页面更改字段
数据单向/双向绑定
v-bind数据单向绑定: 标签里写变量 js里修改变量值 vue.js流向dom元素
v-model的实现本质上是一个语法糖,将数据的更新和视图的更新封装成一条指令
语法糖是简化代码的方式,实际上v-model与下列方式简单等价
<input v-model="test"/>
//等价于 绑定值value 输入时更新数据
<input :value="test" @input="message = $event.target.value"/>
绑定值:value与变量绑定在一起,value反映了变量的值; 数据监听:vue会在输入框中监听input事件,当用户在输入框修改值时自动触发input事件,更改所绑定变量的值
vue2中数据一般靠data()返回,通过this.属性名访问和修改值,this.方法名()调用方法
data(){
return {
list : ['1','2','3','4','5']
},
}
this.list.push('6') //添加一个元素
vue3中ref定义基本数据类型的响应式对象 ,响应式数据存储在value中,通过list.value访问和修改
import ref form 'vue'
const list = ref(['1','2','3','4','5'])
list.value.push('6') //添加一个元素
发布-订阅模式(Observer Pattern)
当数据发生变化时,Vue 会触发相应的 setter,并通知相关的订阅者,使得视图自动更新。
每个组件都有一个与之关联的订阅者(Watchers),这些订阅者会观察数据的变化并作出响应。
<template>
<div>
<a v-bind:href="url">网页路径</a>
<img v-bind:src="imageUrl" alt="图片">
</div>
</template>
<script>
export default {
name: "test",
data() {
return {
url: "https://www.bing.com", //url : "https://www.baidu.com"
imageUrl: "https://www.baidu.com/img/flexible/logo/pc/result.png"
}
}
}
</script>
v-bind:href="url" v-bind将路径绑定到href属性
v-bind:src="imageUrl" v-bind将图片路径绑定src属性
data(){ url:'test.com',imageUrl:'testImage.com' } 如果js中链接或图片链接改变时
由于vue.js中的数据 与dom中的src,url,class,style等属性绑定,dom元素也会改变
vue是如何将数据双向绑定的? v-model和js中数据绑定 vue.js数据和dom元素互通
<template>
<div>
<a>{
{message}}</a>
<el-input v-model="message" placeholder="请输入信息"></el-input>
</div>
</template>
<script>
export default {
name: "test",
data() {
return {
message: 'first input'
}
}
}
</script>
例子:
即后端返回包含数据列表和分页信息的json数据 通过vue的v-model双向绑定来将 返回的数据实时渲染在页面上
响应式数据
element-plus组件
el-upload上传文件组件,浏览器原生<input type="file">,触发一个文件选择器对话框,允许用户从本地选择文件 :action定义了根路径和后端处理文件上传的接口路径,选择文件后发送到服务器接口 :headers定义了http请求头,token用于身份验证 :list-type设置显示类型为图片,默认是显示为列表附带文件名 :on-success上传成功后调用定义的回调函数
<el-form>
<el-form-item form-item label="内容" prop="cover">
<el-upload
:action="$baseUrl + '/files/upload'"
list-type="picture"
:on-success="handleCoverSuccess"
drag
>
<div>将图片拖拽到此区域,或点击上传</div>
<el-button>上传</el-button>
</el-upload>
<input type="file" placeholder="test"></input>
</el-form-item>
</el-form>
//定义表单的图片属性,确保显示正常
data(){
form:{
cover:'',
},
}
//将上传成功后的响应数据 res.data赋值给vue实例 form.cover
//由于prop属性用于数据绑定了组件 el-upload 上传后cover字段会更新
function handleSuccessCover(res){
this.form.cover = res.data;
}
el-table-column操作列,可定义子组件 如修改/删除按钮
点击按钮触发编辑方法,将当前行的数据对象scope.row传给handleEdit()方法,该方法会接受当前行数据作为参数,设置弹窗为true,编辑当前行数据
el-dialog弹窗组件 fromVisible控制弹窗的显示和隐藏
弹窗有标题 取消和确定按钮 确定会触发保存方法
操作列,编辑某行的数据,弹窗显示为真
<el-table-column label="操作" width="180" align="center">
<template v-slot="scope">
<el-button plain type="primary" @click="handleEdit(scope.row)" size="mini">编辑</el-button>
<el-button plain type="danger" size="mini" @click=del(scope.row.id)>删除</el-button>
</template>
</el-table-column>
el-select下拉选择和el-checkbox-group复选框(multiple支持多选/filterable允许用户输入关键字过滤标签/allow-create允许用户回车或点击来自定义标签/default-first-option默认选中第一个值)
<el-form-item label="下拉选择" prop="selectTags">
<el-select v-model="selectArray" multiple filterable allow-create default-first-option style="width: 100%">
<el-option value="A" label="A"></el-option>
<el-option value="B" label="B"></el-option>
<el-option value="C" label="C"></el-option>
<el-option value="D" label="D"></el-option>
</el-select>
</el-form-item>
<el-form-item label="复选框选择" prop="checkboxTags">
<el-checkbox-group v-model="checkArray">
<el-checkbox label="A"></el-checkbox>
<el-checkbox label="B"></el-checkbox>
<el-checkbox label="C"></el-checkbox>
<el-checkbox label="D"></el-checkbox>
</el-checkbox-group>
</el-form-item>
//注意不要忘记 初始化双向绑定的数组对象
data(){
return{
selectArray:[],
checkArray:[],
}
}
假设有个编辑按钮,点击后会触发表单可见,显示编辑弹窗
那么form表单的数据都要双向绑定为form.数据名;对应的保存方法需要根据编辑的是不是空记录判断,空记录对应post新增请求及其路由,有表单的id则证明不是空记录对应put修改请求及其路由,如果有像标签一样的js数据,还有将其转化为JSON数据,控制器返回的状态码为200的时候证明成功,可使弹窗不可见,加载第一页的记录,$request()发送http请求,$message()显示小弹窗
save(){
this.$refs.formRef.$validate(valid=>(){
if(valid){
this.form.tags = JSON.stringify(this.tagsArr);
this.$request({
url: this.form.id ? '/test/update' : '/test/insert';
methods: this.form.id ? 'PUT' : 'POST';
data: this.form
}).then(res=>(){
if(res.code='200'){
this.$message.success('成功');
this.load(1);
this.formVisible = false;
}else{
this.$message.error('失败是成功之母,找到原因,解决问题,复盘收获');
}
})
}
})
},
封装axios请求
新建工具包util,创建request.js文件来封装axios请求
//引入发送http请求的依赖 路由管理依赖
import axios from 'axios'
import router from '@/router'
//创建axios实例 axios.create()
const request = axios.create({
baseURL : process.env.VUE_APP_BASEURL,//后端接口地址 ip:port
timeout: 30000 //30s请求超时
})
//请求拦截器 在请求发送之前对请求进行处理
request.interceptors.request.use(config => {
config.headers['Content-Type'] = 'application/json;charset=utf-8'; //设置请求头 表示请求的是json格式的数据 中文编码
let user = JSON.parse(localStorage.getItem('user') || '{}') //获取本地缓存的用户信息 没有该角色则为空对象
config.headers['token'] = user.token //请求头 一律带上token方便验证
//拦截器返回修改后的config
return config
},error => {
console.error('request error'+error)
return Promise.reject(error)
}
);
//响应拦截器
request.interceptors.response.use(
response => {
let res = response.data;
//若不是json数据 字符串数据也兼容
if(typeof res === "string"){ //parse将json数据转化为js对象 stringify将js对象转化为json数据
res = res ?JSON.parse(res) : res;
}
if(res.code === '401'){ //用户未认证 或认证过期
router.push('/login') //路由跳转回登录页
}
return res
},
error => { //请求响应失败时 执行回调函数 记录错误并返回失败的 Promise
console.error('response error' + error)
return Promise.reject(error)
}
)
//导出request实例
export default request
根目录下新建.env文件配置环境变量
VUE_APP_BASEURL='http://localhost:更换为后端端口号' //http请求发送给后端控制器controller
//vue会自动加载.env文件中的变量
在main.js文件中导入requets.js并将其挂载到vue实例中, 使得每个组件都可以通过this.$request访问
import request from "@/utils/request";
//将request挂载到vue实例的原型上
Vue.prototype.$request = request;
//将开发环境中的基地址挂载到vue实例的原型上,便于全局访问
Vue.prototype.$baseUrl = process.env.VUE_APP_BASEURL
封装好了自然就要使用,学以致用,知行合一;
分页查询
举个例子:假如有个信息管理页面,页面创建时会执行分页查询,显示第一页的数据;
created(){
this.paging(1);
}
前端发送get数据请求如下:
paging(pageNum){
if(pageNum) this.pageNum = pageNum //如果传入了pageNum,为真,更新当前页码
this.$request.get('/selectPage',{
params:{ //传递查询条件
pageNum : this.pageNum,//当前页码
pageSize : this.pageSize,//每页几条记录
condition : this.condition //额外的查询条件
}
})
},
//假设前端get请求的参数是:
{
pageNum :1,
pageSize : 10,
condition : ''
}
后端接口返回的json数据如下:
{
"data":{
"list":[
{"id":1,"name":"Alice","age":24,"address":"BeiJing"},
{"id":2,"name":"Bob","age":30,"address":"ShangHai"},
{"id":3,"name":"Charlie","age":28,"address":"GuangZhou"},
.......
],
"total":100
}
}
前端通过then处理响应,获取后端返回的json分页数据:
.then(res =>{
this.tableData = res.data?.list
this.total = res.data?.total
})
tableData拿到后就可以在el-table中展示了;
:data会将数组中的每一个数据绑定到表格每一行; stripe可以让行有斑马条纹背景色效果; 每一列的数据通过el-table-column的prop属性定义
<el-table :data="tableData" stripe @selection-change="selectionChange">
<!--@selection-change是选择当前行数据时触发方法 -->
<el-table-column prop="id" label="id"></el-table-column>
<el-table-column prop="name" label="姓名"></el-table-column>
<el-table-column prop="age" label="年龄"></el-table-column>
<el-table-column prop="address" label="地址"></el-table-column>
</el-table>
通常,当表格数据量比较大时,为了实现分页功能,需要使用 el-pagination 组件来处理分页
<el-pagination
background
@current-change="currentChange"
:current-page="pageNum"
:page-sizes="[5, 10, 20]"
:page-size="pageSize"
layout="total, prev, pager, next"
:total="total">
</el-pagination>
当用户切换页码时,根据新的页码重新请求后端接口获取新的数据,更新tableData
methods: {
handleCurrentChange(pageNum) {
// 重新请求数据,根据新的 pageNum 和 pageSize 获取数据
axios.get('/api/data', {
params: {
page: pageNum,
pageSize: this.pageSize
}
}).then(res => {
this.tableData = res.data?.list; // 更新表格数据
this.total = res.data?.total; // 更新总数据条数
});
}
}
发送http请求
使用this.$request发送http请求(post,delete,update,get)来与后端controller的请求映射交互(参数类型用@PathVariable路径传参标记如{id},@RequestBody处理请求体如json数据,@RequestParam获取查询字符串的参数如name="john"&age=30)
这里需要区分一下路径和路由的区别,路径单指url,如:'/home' ; 路由包含路径和组件,如:{'/home',component: homePage};
{ path: 'test', name: 'Test', meta: { name: '测试' }, component: () => import('../views/manager/Test') }, //这里的path是路径 如el-menu-item:index="/test" 导入的组件是Test.vue的相对位置
<template>
<div>
<a>{
{id}}</a>
<el-input v-model="id" placeholder="请输入要删除的记录"></el-input>
<el-button @click="deleteItem">删除</el-button>
</div>
</template>
<script>
export default{
data(){
return{
id:null,
}
},
methods:{
deleteItem(){
if(!this.id){
this.$message.warning("请输入要删除记录的id");
return; //return 返回不会继续下列语句
}
this.$confirm('确定删除该记录吗?','确定删除',{type:'warning'})
.then(()=>{
this.$request.delete(`/user/delete/${this.id}`);
this.$message.success("删除成功");
this.id =null;
})
},
}
}
</script>
@cllick按钮监听,点击后执行方法
js中数据data(){}初始化定义变量id,方法methods:{}定义点击后执行的方法,this.$request发送http请求,this.$confirm返回true or false.
判断如果输入框为空,输出warning函数内的文本,return返回不会继续执行下列语句;this.$confirm(参数一是弹窗文本message,参数二是弹窗标题title,参数三是消息类型type)
this.$router进行路由跳转 this.$router.push('/index')跳转首页
前后端分离
避免前端路由和后端接口路径起冲突,可以设置接口的统一前缀路径,content-path: /api
前端路由设置(index.js):
const routes = [
{
path:'/', //根路径,访问路径加载组件
name:'Manager', //路由名称,命名利于路由跳转 router.push({name:'Manager'})
component:() => import('../views/Manager.vue'), //路由懒加载,动态方式加载组件
redirect:'/home', //访问根路由会重定向到首页
children:[ //子路由
{path:'test',name:'Test',meta:{name:'测试界面'},component:() => import('../views/Test')},//meta:路由元信息,存储路由状态或标识
]
}
]
后端接口映射(controller.java)
@RestController
@RequestMapping("/test")
public class TestController {
@Resource
private UserService userService;
@GetMapping("/test/select")
public List<User> getUsers(@RequestParam User user){
//获取所有用户
List<User> list = userService.selectAll(user);
return list;
}
@PostMapping("/test/insert")
public void createUser(@RequestBody User user){
//创建角色
userService.add(user);
}
@DeleteMapping("/test/delete/{id}")
public void deleteUserById(@PathVariable Integer id){
//删除指定id角色
userService.selectById(id);
}
}
发送http请求的方式(异步发送请求,不影响用户操作)
ajax(传统的XMLHttpRequest)如:
test.json: {"userName":"张三"}
const xhr = new XMLHttpRequest();
//请求成功调用onload方法,请求失败onerror
xhr.onload = ()=> {
if(xhr.status >=200 && xhr.status < 300) {
console.log(xhr.responseText)
} //200-299表示htpp响应成功状态
}
xhr.open.('GET','text.json');//默认为true异步请求
xhr.send();//默认空参数
axios如: axios.get('/api/data').then(response => {this.data = response.data;});
fetch api如:fetch('/api/data').then(response => response.json());
fetch('test.json')
.then(response => response.json())//解析json格式的响应
.then(data => console.log(data)) //打印数据
.catch(error => console.error('Request failed',error));//处理错误
简化http请求,基于axios的封装
import axios from 'axios'
install(Vue){
Vue.prototype.$request = axios.create({
baseURL:'http://localhost:8080/api',
timeout:10000,//请求超时时间
})
},
可以在vue组件里通过this.$request. post()/delete()/put()/get() 进行请求
如v-model双向绑定输入框内的数据id,可以在delete(`/test/deleteById/${this.id}`)
删除指定id的角色
表单弹窗新增信息:
<el-button type="primary" plain @click="insertButton()">新增</el-button>
<el-dialog title="信息" :visible.sync="formVisible1" >
<el-form :model="form" title="活动信息">
<el-form-item label="活动名称" prop="name">
<el-input v-model="form.name" placeholder="活动名称"></el-input>
</el-form-item>
<el-form-item label="简介" prop="descr">
<el-input v-model="form.descr" placeholder="简介"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="formVisible1=flase">取消</el-button>
<el-button type="primary" @click="save">确定</el-button>
</div>
</el-dialog>
insertButton(){
form={};
formVisible1 = true;
},
表单通过:rules定义规则"validate",例如是否为必填项,最小/最大长度等,this.$refs.formRef.validate()返回valid;如果不通过则valid为false,只有当验证通过后才会进行数据处理;
发送http请求通常是:
this.$request.post("/activity/add",this.form);
这里不确定是否存在该数据,如果有id是put更新请求及其对应路径,没有id则是post新增请求及其对应路径,发送表单数据data:this.form;
分析并展示数据
el-table数据展示 el-pagination分页展示
{ "code" : "200",
"msg" : "success",
"data" : { "list" : [ {"id":1,"name":"分类A"}, {"id":2,"name":"分类B"}, ], "total":50 } }
逐层分析 最外层code msg data都是字符串 而值的类型不确定,
可用object自动转换 Map<String,Object> map = new HashMap<>(); data里有一个list数组和一个total整数 list里是多条map键值对数据 Map<String,List<Map<String,Object>>>
map对象可以用put方法,向其后添加 组合为复合数据结构 map.put("list",List<Map<String,Object>>); map.put("total",Integer);
三:简单案例todolist任务列表
明确前端页面组成元素,js中定义的变量和方法
页面元素
一级标题 <h1>; 例如标题"Vue ToDo List"
文字盒子 <div>{ {remaining}}</div> 显示未完成任务的数量 ,由于remaining封装在计算属性computed中,因此可以直接调用方法名引用来返回值;
输入框双向绑定输入任务值 回车事件和 按钮点击事件 都会触发添加任务方法;
任务列表:ul是无序列表 li是每一行的值 v-for会遍历任务列表 有几个对象显示几行数据 单选框checkbox双向绑定任务的完成状态 <span>表示文本 每个对象的文本text会显示出来 删除按钮会将当前对象的索引参数传给删除指定任务方法,同样是点击调用
清除已完成任务按钮 其封装在div盒子中,如果任务列表还有任务 就将已完成任务清除
变量和方法
newTodo是当前输入框添加的新任务;item是列表的一个对象;todos是代表任务列表的数组 任务的id要清晰且唯一不与数组索引混淆,因此索引从1开始,每添加一个新元素索引值都要+1; completed是任务的完成状态 值为true代表已完成;
data()需要返回 任务数组列表 新增任务字符串 初始索引
methods:{}定义addTodo添加新任务,当字符串前后为空时不添加,不为空设置任务id,任务文本text,未完成状态,nextId++方便赋值给下一个任务,newTodo为空字符串清空输入框;
filter()方法筛选出符合条件为真的对象 定义removeTodo(参数是id) 通过单选框传入指定id的任务 id是选中的 this.todos.filter(item => item.id !== id)将未选中的id筛选出来;定义clearCompleted()方法
由于别的方法通过鼠标或键盘事件监听触发,而数据展示还是适合在计算属性computed中引用方法名直接调用
F11打开控制台 添加一个任务 可见组件的生命周期created(),mounted(),updated()和监听:
watch用于监听数据的初始化和变化,当任务列表todos发生变化时,打印新任务列表
css样式
定义了 completed类 .是类选择器 该类有删除线和灰色属性 在页面dom元素中使用:class="{completed:true/false}" 值为true会应用样式
关键点解释
v-model双向绑定,输入框的v-model="newTodo"使得输入框内容与newTodo变量双向绑定,用户输入内容时,newTodo变量会自动更新;
v-for和:key,v-for遍历todos数组生成每个任务的<li>元素,key键属性确保vue在渲染时能够有效识别每个任务;
v-if="todos.length > 0"判断为true时vue将该元素添加到dom中,判断为flase时则不会渲染该元素及其子元素,当一条任务都不存在时,按钮是多余的,也避免显示不必要的ui元素(奥卡姆剃刀原理:如无必要,勿增实体)
动态样式绑定,<span :class="{completed : item.completed}">动态绑定css类,让任务被标记已完成true时,应用如删除线等样式;
@click="方法名" @keyup.native.enter="方法名" 绑定回车键弹起和鼠标点击事件,触发指定方法;由于el-input可能封装了一些原生事件,导致@keyup无法绑定,因此使用.native修饰符可直接监听原生keyup事件
F11打开控制台 添加一个任务 可见组件的生命周期created(),mounted(),updated()和监听:
具体代码
<template>
<div class="app">
<h1>Vue ToDo List</h1> <div>未完成任务:{
{remaining}}</div>
<div>
<el-input v-model="newTodo" @keyup.native.enter="addTodo" placeholder="添加新任务"/>
<el-button @click="addTodo">添加</el-button>
</div>
<ul>
<li v-for="item in todos" :key="item.id">
<input type="checkbox" v-model="item.completed"/>
<span :class="{completed:item.completed}" >{
{item.text}}</span> <!--:class动态绑定一个类,如果item.completed为true则绑定该类 表示该任务已经完成 -->
<el-button @click="removeTodo(item.id)">删除</el-button>
</li>
<div v-if="todos.length > 0">
<el-button @click="clearCompleted">清除已完成任务</el-button>
</div>
</ul>
</div>
</template>
<script>
export default{
data(){
return{
newTodo:'',
todos:[],
nextId:1,
}
},
methods:{
addTodo() {
if (this.newTodo.trim()) {
// 新增任务并且更新 `nextId`
this.todos.push({
id: this.nextId,
text: this.newTodo.trim(),//this.newTodo代表当前输入的任务 trim方法去除字符串前后的空格 代表输入不能为空
completed: false,
});
this.nextId++; // 在添加任务后递增 nextId
this.newTodo = ''; // 清空输入框
}
},
removeTodo(id){
this.todos = this.todos.filter(item =>item.id !== id);//过滤掉选中id的任务,筛选出未选中id的任务数组
},
clearCompleted() {
// 清除已完成的任务
this.todos = this.todos.filter(item => item.completed == false);//filter方法遍历todos数组,过滤掉那些item.completed为true已完成的任务,筛选出来的的都是未完成任务的数组
}
},
computed:{ // 将remaining方法作为计算属性来设置
remaining(){
return this.todos.filter(item => !item.completed).length;
},
},
created(){
console.log('组件已经创建');
},
mounted(){
console.log('组件已经挂载');
},
updated(){
console.log('组件已经更新');
},
watch:{
todos(newTodos){
console.log("todos列表更新:",newTodos);
},
}
}
</script>
<style scoped>
.completed{
text-decoration:line-through;
color:grey;
}
</style>
这里发现刷新页面数据丢失,使用浏览器的localRestorage数据持久化,在页面挂载时加载数据,监听数据当其变化时存储数据,或者页面变化时存储数据 JSON.stringify()/JSON.parse()
保存到saveto本地:js对象=>json字符串 加载自loadfrom本地 json字符串=>js对象
saveToLocalStorage(){
localStorage.setItem('todos',JSON.stringify(this.todos));//存储键值对,JSON.stringify()将对象转化成字符串
},
loadFromLocalStorage(){
const saveTodos = localStorage.getItem('todos'); //获取key名对应的value 类型为字符串
if(saveTodos){ //如果存在 将字符串转换为js对象
this.todos = JSON.parse(saveTodos);
this.nextId = this.todos.length ? Math.max(...this.todos.map(item => item.id)) +1 : 1;
// 无数据默认id为1 有数据就 计算下一项的id,确保数据在删除后 不会与现有数据发生冲突
// 如: 123中3被删除 添加一个id++为4 但Math.Max(1,2,4)会返回4+1=5,保障数据不会重复
}
}
分别在生命周期为挂载mounted()时调用/监听watch数据变化时调用
存储与加载实现 刷新页面或重启项目数据依然存在
scope是vue的插槽v-slot传递的上下文对象,如scope.row当前行的数据 v-slot允许父组件访问和使用子组件传递的数据; 传递v-slot="scoped" 调用scoped.row.data 传值this.data = data;
将表单输入的值赋给本地变量 data : this.data ; 将后端响应的值赋值给本地变量:
this.data = res.data;
若查询的数据包含多条记录需要表格显示 this.tableData = res.data?.list;
?.是可选链操作符,避免访问的时候出现null或undefined,代码直接抛出 undefined of 属性值的错误
前后端分离项目
(行是知之诚)
日常更新,未完待续
项目优化
redis 缓存
redis(remote dictionary server)是一个开源的内存数据结构存储系统,redis不像传统的数据库存储在硬盘上,而是将数据存储在内存上
缓存常用于存储访问频繁,访问成本高或计算高耗时的数据 例如电商网站的商品详情页,用户登陆后的个人信息,搜索引擎的热点信息
将经常访问的数据和服务存储在 Redis 这样的高速缓存中,以便快速响应用户请求、减少对主数据库的压力、提高系统性能
常用redis命令(key键,List列表排序,Set集合去重)
cmd输入redis-cli启动redis 加上--raw不处理数据 中文字符
set key value get key del key 1表示成功 exists key 0表示不存在
keys * 遍历所用键值 keys *name 遍历所有以name结尾的键值
expire key seconds(以秒为单位)
ttl key 查看键的剩余生存时间
persist key 移除键的生存时间,使其永久存在
rename key newname 修改键名称
set user:1 value get user:1 value
set user:2:address "earth" get user:2:address
理论上reids可以用无限个' : '叠加创建直观的层级结构,
且每个键的值可以用任意数据类型
set school:college:major:class:zh-year "二零二五"
mset key1 value1 key2 value2 …批量设置键值对
mget key1 key2 …批量获取键对应的值
append user:1 value 将值追加到指定键值之后
List列表操作
lpush key value 将1个或多个值 插入列表的左侧
rpush key value 将1个或多个值 插入列表的右侧
lpop key value 移除并返回列表左侧 第一个元素
rpop key value 移除并返回列表右侧 第一个元素
lrange key start stop
例如lrange key 0 1 返回 列表第一个到第二个元素
llen key 返回列表的长度
集合Set操作
sadd key value1 value2 添加一个或多个元素
smembers key 遍历集合所有成员
sismember key value 判断值 是否是 集合成员
srem key value 移除集合成员
scard key 获取集合成员个数
elasticSearch 搜索
kafaka 消息队列
Spring Cloud 微服务
springboot整合redis
llm本地大语言模型部署
项目上线
nginx服务器
nginx即Engine-X,是一个高性能的web服务器,配置反向代理与负载均衡; ngrok内网穿透到公网
反向代理:客户端发送http请求给代理服务器,代理服务器转发给后端服务器,后端服务器处理请求并返回响应,代理服务器接收到响应后转发给客户端(客户端不知道请求被转发到了哪里,所有的请求和响应都要经过代理服务器转发)
客户端仅仅与nginx 代理服务器交互,后端服务器对客户端来说是不可见的,提高安全性;
SSL终止,客户端通过https请求与代理服务器交互,代理服务器通过http请求与后端服务器交互,减轻了后端服务器加密解密的负担;
负载均衡:将客户端的请求发送到多个服务器,确保不会有一台服务器因为处理过多的请求而过载,提高网站系统或应用程序的性能和可靠性 用户的请求首先到达nginx服务器,nginx根据负载均衡策略将请求分配给后端多个应用服务器 常见的负载均衡策略: 轮询:将请求依次分配给每个服务器 加权轮询:根据服务器的性能,给服务器设置不同的权重,权重高的服务器接收更多的客户端请求 Ip Hash:根据请求的ip地址分配服务器,相同的ip地址始终访问同一台服务器 最少连接:将请求分配给当前连接数最少的服务器
(提高应用的吞吐量,避免单台服务器故障,提高应用的可用性)
docker容器
部署前后端分离项目 数据库迁移到服务器上,build打包前端,maven(package)导出jar包,jar包拷贝到服务器上,编写dockerfile文件,dockerfile部署运行jar包(构建镜像运行容器) 前后端由于部署不同端口,解决前后端端口不同的跨域问题(通过vue配置请求转发访问): 跨域问题通常发生在前端应用(如 Vue.js)和后端 API 服务不在同一域名和端口下时。假设你在开发过程中,前端服务运行在 http://localhost:8080
,而后端服务运行在 9090端口,浏览器会认为这些请求是跨域的,会因为浏览器的 同源策略 被阻止 ;
1.开发环境下通过环境变量 解决跨域:
-
在
.env.employment
和.env.production
文件中,设置VUE_APP_BASEURL
为http://localhost:9090
,这个 URL 会作为 API 请求的基础地址
2.vue3在项目根目录下新建vue.config.js配置请求转发/将前端请求转发到后端api端口
module.exports = {
devServer: {
proxy: {
'/api': {
target: process.env.VUE_APP_BASEURL, // 使用环境变量或者后端ip地址
// target: "http://localhost:9090",
changeOrigin: true,
pathRewrite: {
'^/api': '', // 路径重写
},
},
},
},
};
这样前端请求会被转发到http://localhost:9090/api下,不需要直接向浏览器发送跨域请求,后端注意配置接口全局路径/api
日常更新,未完待续