赞
踩
首先,一个网页大多会有一个公用的导航栏,因此在bootstrap中复制一个NavBar的example,稍作修改,若处于未登录状态,则右端显示登录,否则显示当前登录用户的用户名。导航栏的Vue代码如下:
<template> <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <div class="container"> <router-link class="navbar-brand" :to="{name: 'chat_index'}">聊天室</router-link> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarText"> <ul class="navbar-nav me-auto mb-2 mb-lg-0"> <li class="nav-item"> <router-link class="nav-link" aria-current="page" :to="{name:'chat_index'}">聊天</router-link> </li> </ul> <ul class="nav-item" v-if="$store.state.user.nickname !== ''"> <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle navbar-text" href="#" role="button" data-bs-toggle="dropdown"> {{ $store.state.user.nickname }} </a> <ul class="dropdown-menu"> <li><a class="dropdown-item" @click="logout">退出登录</a></li> </ul> </li> </ul> <span class="navbar-text" v-if="$store.state.user.nickname === null || $store.state.user.nickname === ''"> 登录 </span> </div> </div> </nav> </template> <script> import { useStore } from 'vuex'; import $ from 'jquery'; export default { setup() { const store = useStore(); const logout = () => { $.ajax({ url: "http://localhost:8081/logout", type: "get", success(resp) { if(resp.state === "success") { store.commit("updateUser",{ userId: -1,username: "",nickname: "" }); localStorage.clear(); window.location.href = "http://localhost:8080/login/"; } } }); }; return { logout, } } } </script>
导航栏样式如下所示(已登录状态):
导航栏样式如下所示(未登录状态):
如果用户已经登录,那么点击右边的用户名之后会弹出一个下拉框,可以退出登录。
要实现这个效果,可以用过vuex中的store实现,当用户登录之后后端会向前端返回已登录的状态存到session和localstorage里面,在vuex中从localstorage中取出这个bool值,当为true时显示用户名,否则显示登录按键。
实现这部分功能的vue代码如下:
<ul class="nav-item" v-if="$store.state.user.nickname !== ''">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle navbar-text" href="#" role="button" data-bs-toggle="dropdown">
{{ $store.state.user.nickname }}
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" @click="logout">退出登录</a></li>
</ul>
</li>
</ul>
<span class="navbar-text" v-if="$store.state.user.nickname === null || $store.state.user.nickname === ''">
登录
</span>
import { createStore } from 'vuex'; export default createStore({ state: { login: localStorage.getItem("isLogin"), userId: localStorage.getItem("userId"), user: { username: "", nickname: "" } }, getters: { }, mutations: { updateLogin(state, login) { state.login = login; }, updateUser(state, user) { state.user.username = user.username; state.user.nickname = user.nickname; } }, actions: { }, modules: { } })
接着使用bootstrap创建登录界面,首先创建一个card布局,将前端划分为一个个小的卡片,这样会显得稍微好看一些,因此,外面可以提取出一个公共的组件:ContentField。ContentField的vue代码如下:
<template> <div class="container content-field"> <div class="card"> <div class="card-body"> <slot></slot> </div> </div> </div> </template> <script> export default { setup() { }, } </script> <style scoped> div.content-field { margin-top: 20px; } </style>
创建公共组件之后,可以开始创建登录界面了,首先也是在bootstrap官网上,找到一个关于Form的example接着用我们刚刚创建的组件ContentField将其包裹,也就是说,这个登录界面是一个新的小卡片。vue代码如下:
<template>
<ContentField>
<div class="mb-3">
<label class="form-label">用户名</label>
<input v-model="username" type="text" class="form-control" name="username">
</div>
<div class="mb-3">
<label for="exampleInputPassword1" class="form-label">密码</label>
<input v-model="password" type="password" class="form-control" name="password" id="exampleInputPassword1">
</div>
<span style="color: red;">{{ message }}</span>
<br/>
<button @click="login" class="btn btn-primary">登录</button>
</ContentField>
</template>
可以注意到,我们登录的这个button会绑定上一个名为login的方法,这个方法会向后端发送ajax请求,实现登录,代码如下:
<script> import ContentField from '@/components/ContentField.vue'; import { ref } from 'vue'; import $ from 'jquery'; export default { setup() { // const store = useStore(); let message = ref(""); let username = ref(""); let password = ref(""); const login = () => { $.ajax({ url: "http://localhost:8081/login", type: "post", data:{ username: username.value, password: password.value }, success(resp) { if(resp.state === "fail") { message.value = "用户名或密码错误"; } else { localStorage.setItem("isLogin",true); localStorage.setItem("userId",resp.user.id); window.location.href = "http://localhost:8080/chat/"; } } }); }; return { message, username, password, login } }, components: { ContentField, } } </script>
可以看到,前端后向后端发送请求,请求的路径为http://localhost:8081/login,后端经过登录校验之后会向前端发送登录状态,将登录状态设置为true,并且将用户的信息返回给前端,将用户的id保存到localstorage,最后跳转至聊天界面。
首先在element-plus中寻找合适的example,最后选择了以一个表格的形式作为聊天界面,聊天界面如下:
vue代码如下:
<template > <el-container class="layout-container-demo" style="height: 500px"> <el-aside width="200px"> <el-scrollbar> <el-table :data="userList"> <el-table-column prop="nickname" label="用户列表" width="150" /> </el-table> </el-scrollbar> </el-aside> <el-container> <el-header style="text-align: right; font-size: 12px"> <div style="text-align: center;"> <span style="font-size:30px">聊天室</span> </div> </el-header> <el-main> <el-scrollbar> <el-table :data="tableData"> <el-table-column prop="date" label="时间" width="160" /> <el-table-column prop="name" label="发送者 " width="120" /> <el-table-column prop="message" label="消息" /> </el-table> </el-scrollbar> </el-main> <el-footer> <textarea class="form-control" v-model="textarea" @keydown="sendMsg" aria-label="With textarea"></textarea> </el-footer> </el-container> </el-container> </template> <script> import { onMounted, onUnmounted, ref } from 'vue' import $ from "jquery"; import { useStore } from 'vuex'; const debounce = (fn, delay) => { let timer = null; return function () { let context = this; let args = arguments; clearTimeout(timer); timer = setTimeout(function () { fn.apply(context, args); }, delay); }; }; // 解决ERROR ResizeObserver loop completed with undelivered notifications. const _ResizeObserver = window.ResizeObserver; window.ResizeObserver = class ResizeObserver extends _ResizeObserver { constructor(callback) { callback = debounce(callback, 16); super(callback); } }; export default { setup() { const store = useStore(); onMounted(() => { $.ajax({ url: "http://localhost:8081/getinfo", type: "get", data: { id: store.state.userId, }, success(resp) { store.commit("updateUser", { username: resp.username, nickname: resp.nickname }); } }); }); const socketUrl = `ws://localhost:8081/websocket/${store.state.userId}/`; let socket = new WebSocket(socketUrl); socket.onopen = () => { console.log("connect!"); }; onUnmounted(() => { socket.close(); }); const sendMsg = event => { if (event.shiftKey && event.keyCode === 13) { document.execCommand('insertLineBreak'); // 换行 event.preventDefault(); return false; } else if (event.keyCode === 13) { // 回车键 console.log("回车发送"); socket.send(JSON.stringify({ msg: textarea.value, })); textarea.value = ""; event.preventDefault(); return false; } }; let user = []; console.log(user); const textarea = ref(""); let dataList = []; const tableData = ref(dataList); const userList = ref(user); $.ajax({ url: "http://localhost:8081/getUserList", type: "get", success(resp) { for (let i = 0; i < resp.length; ++i) { userList.value.push({ "nickname": resp[i].nickname, "username": resp[i].username }); } } }); $.ajax({ url: "http://localhost:8081/getHistory", type: "get", success(resp) { for (let i = 0; i < resp.length; ++i) { tableData.value.push({ "date": resp[i].date, "name": resp[i].name ,"message": resp[i].message}); } } }); socket.onmessage = msg => { console.log("receive message" + msg.data); const data = JSON.parse(msg.data); tableData.value.push(data); }; return { tableData, textarea, userList, sendMsg, }; }, } </script> <style scoped> .layout-container-demo .el-header { position: relative; background-color: var(--el-color-primary-light-7); color: var(--el-text-color-primary); } .layout-container-demo .el-aside { color: var(--el-text-color-primary); background: var(--el-color-primary-light-8); } .layout-container-demo .el-menu { border-right: none; } .layout-container-demo .el-main { padding: 0; } .layout-container-demo .toolbar { display: inline-flex; align-items: center; justify-content: center; height: 100%; right: 20px; } </style>
其中template和style没什么好说的,基本上都是官网的例子然后随便改改,主要是script中的逻辑部分,首先,当该组件被挂载完成后会向后端发送请求,获取当前用户的信息获取用户的昵称,并且获取当前用户列表以及历史记录:
onMounted(() => {
$.ajax({
url: "http://localhost:8081/getinfo",
type: "get",
data: {
id: store.state.userId,
},
success(resp) {
store.commit("updateUser", { username: resp.username, nickname: resp.nickname });
}
});
});
$.ajax({ url: "http://localhost:8081/getUserList", type: "get", success(resp) { for (let i = 0; i < resp.length; ++i) { userList.value.push({ "nickname": resp[i].nickname, "username": resp[i].username }); } } }); $.ajax({ url: "http://localhost:8081/getHistory", type: "get", success(resp) { for (let i = 0; i < resp.length; ++i) { tableData.value.push({ "date": resp[i].date, "name": resp[i].name ,"message": resp[i].message}); } } });
还有最重要的一步是打开websocket连接:
const socketUrl = `ws://localhost:8081/websocket/${store.state.userId}/`;
let socket = new WebSocket(socketUrl);
socket.onopen = () => {
console.log("connect!");
};
建立websocket连接之后,当后端有消息发送过来时,以下方法会异步调用,处理后端接收到的消息。
socket.onmessage = msg => {
console.log("receive message" + msg.data);
const data = JSON.parse(msg.data);
tableData.value.push(data);
};
处理十分简单,后端按照指定的json格式将需要的数据发送给了前端,前端可以直接将数据放到表格中。形成一条新的聊天记录:
当用户在聊天消息框中输入消息后,按下Enter键之后,可以发送消息,如果按下Shift + Enter,可以换行。
const sendMsg = event => {
if (event.shiftKey && event.keyCode === 13) {
document.execCommand('insertLineBreak'); // 换行
event.preventDefault();
return false;
} else if (event.keyCode === 13) { // 回车键
console.log("回车发送");
socket.send(JSON.stringify({
msg: textarea.value,
}));
textarea.value = "";
event.preventDefault();
return false;
}
};
最后,当组件卸载时,会将websocket连接关闭:
onUnmounted(() => {
socket.close();
});
首先在idea中创建maven工程,接着导入依赖,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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.12</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.ljx</groupId> <artifactId>chat</artifactId> <version>0.0.1-SNAPSHOT</version> <name>chat-backend</name> <description>chat-backend</description> <properties> <java.version>11</java.version> </properties> <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.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.5.3</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.76</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> <repository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>https://repo.spring.io/snapshot</url> <releases> <enabled>false</enabled> </releases> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </pluginRepository> <pluginRepository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>https://repo.spring.io/snapshot</url> <releases> <enabled>false</enabled> </releases> </pluginRepository> </pluginRepositories> </project>
然后创建WebSocket的配置类
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
创建配置文件,写入数据库连接信息和后端端口号:
server.port=8081
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.url=jdbc:mysql://localhost:3306/chat?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
创建HistoryController,查询数据库,并且将日期格式化即可。
@CrossOrigin @RestController public class HistoryController { @Autowired private HistoryMapper historyMapper; @Autowired private UserMapper userMapper; @GetMapping("getHistory") public List getHistory() { ArrayList<Dto> list = new ArrayList<>(); List<History> historyList = historyMapper.selectList(null); for(int i = 0;i < historyList.size();i++) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String date = sdf.format(historyList.get(i).getSendTime()); QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.select("nickname").eq("id",historyList.get(i).getUserId()); User user = userMapper.selectOne(queryWrapper); String name = user.getNickname(); String message = historyList.get(i).getMessage(); Dto dto = new Dto(name,message,date); list.add(dto); } return list; } }
在UserController下实现四个接口,分别是登录,登出,根据id获取信息用户信息以及获取用户列表接口。这里功能比较复杂因此放在service中实现。Controller只完成前端发送来的数据的解析操作。
@CrossOrigin @RestController public class UserController { @Autowired private UserService userService; @PostMapping("login") public Map login(HttpServletRequest request, @RequestParam Map<String,String> data) { User user = new User(); user.setUsername(data.get("username")); user.setPassword(data.get("password")); System.out.println(user); return userService.login(request,user); } @GetMapping("logout") public Map logout(HttpServletRequest request) { request.getSession().removeAttribute("login"); Map<String,String> result = new HashMap<>(); result.put("state","success"); return result; } @GetMapping("getinfo") public Map getinfo(@RequestParam Integer id) { return userService.getinfo(id); } @GetMapping("getUserList") public List<User> getUserList() { return userService.getUserList(); } }
具体逻辑在service中:
@Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public Map login(HttpServletRequest request, User user) { Map<String,Object> result = new HashMap<>(); String username = user.getUsername(); LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getUsername, user.getUsername()).eq(User::getPassword, user.getPassword()); User user1 = userMapper.selectOne(wrapper); if(user1 == null) { result.put("state","fail"); System.out.println("失败!"); return result; } request.getSession().setAttribute("login", true); result.put("state","success"); user1.setPassword(""); result.put("user",user1); System.out.println("成功"); return result; } @Override public Map getinfo(Integer id) { User user = userMapper.selectById(id); Map result = new HashMap(); result.put("username",user.getUsername()); result.put("nickname",user.getNickname()); return result; } @Override public List<User> getUserList() { List<User> users = userMapper.selectList(null); users.forEach(user -> user.setPassword("")); return users; } }
在该类下,加了@OnOpen注解的方法是有连接建立之后调用的方法,当连接断开调用@OnCloss注解的方法,当后端发送消息,则调用加了@OnMessage注解的方法。逻辑十分清晰,因此不做过多解释,直接阅读代码即可。
@Component @ServerEndpoint("/websocket/{userId}") public class WebSocket { private Session session = null; private Integer userId; private static HistoryMapper historyMapper; @Autowired public void setHistoryMapper(HistoryMapper historyMapper) { WebSocket.historyMapper = historyMapper; } private static UserMapper userMapper; @Autowired public void setUserMapper(UserMapper userMapper) { WebSocket.userMapper = userMapper; } final private static CopyOnWriteArraySet<WebSocket> webSockets = new CopyOnWriteArraySet<>(); // 保存在线列表 @OnOpen public void onOpen(Session session, @PathParam("userId") String userId) { // 建立连接 this.session = session; System.out.println("connect:" + userId); this.userId = Integer.parseInt(userId); webSockets.add(this); } @OnClose public void onClose() { // 关闭链接 System.out.println("disconnected:" + userId); } @OnMessage public void onMessage(String message) { // 处理消息 JSONObject jsonObject = JSONObject.parseObject(message); // System.out.println(message); History history = new History(); history.setSendTime(new Date()); history.setUserId(userId); history.setMessage(jsonObject.get("msg").toString()); System.out.println(history); // 保存历史记录并且发消息 historyMapper.insert(history); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // System.out.println(sdf.format(historyMapper.selectById(4).getSend_time())); JSONObject resp = new JSONObject(); resp.put("date",sdf.format(history.getSendTime())); resp.put("name",userMapper.selectById(userId).getNickname()); resp.put("message",jsonObject.get("msg").toString()); sendAllMessage(resp.toJSONString()); } private void sendAllMessage(String message) { // 群发消息 for(WebSocket webSocket : webSockets) { try { if(webSocket.session.isOpen()) { webSocket.session.getAsyncRemote().sendText(message); } } catch (Exception e) { e.printStackTrace(); } } } @OnError public void onError(Session session, Throwable error) { error.printStackTrace(); } }
这里向为本课程做出贡献的各路豪杰致以崇高敬意,没有他们的例子妄图跟上课程进度,所花费的时间只怕三国都能统一三次了(玩笑话)。但并不能因为能跑通现成的项目就心安理得或者沾沾自喜,以为掌握了这门技术;而应该仔细研读,以求温故知新,创出自己的版本来,这样才能达到这门课该有的效果。想必这也是孟宁老师用心良苦所希望的。
第一次选修这样独具匠心的课尚属首次,这种别开生面的授课方式也给我带来了以往所收获不到的惊喜。摒除了其他课程一言堂式的教学方式,每个同学可以自由而全面的发展,并能够感受不同思想的碰撞,颇有一种百家争鸣的盛世场面。大家各自繁荣,还能各取所长。但各自发展又不是任意的,这个时候就体现出老师引导的重要性:什么时候该做尝试,什么时候该做取舍,对整个项目的进展有着举足轻重的作用。毫无疑问,孟宁老师始终发挥着把控全局的作用:不至于分崩离析,又能够遍地开花;既能保证项目的前进方向,也能兼顾不同版本的依次更新。将艺高人胆大诠释的淋漓尽致。
然而分别终究是要来到了。但是课程的结束并不意味着工程的止步,这一个多月的成果,将推动我们朝着更高的要求前进。希望在以后的工作中,我们能够怀揣着网络程序设计带来的理念和精神,在网络程序设计的领域中乘万里风,破万里浪!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。