0%

基于聊天室项目的总结

用node中的express搭建服务器,写接口,登录方式采用jwt登录验证,用mysql存储各个人员信息,用soket.io实现聊天功能,webpack打包项目,以及动态展示功能,支持修改密码,注册用户,一对一实时聊天,发表情包,图片等功能,各个页面之间的跳转用vue-router实现,跳转使用路由懒加载,只会在第一次进入页面时才会获取这个函数,然后使用缓存数据。搭配使用Weui样式库。

jwt登录认证的实现:

jwt组成部分:

头部.有效荷载.签名

Header.PayLoad.Signature

头部:包含了JWT类型和使用的Hash算法

负载:包含了一些声明,通常是一个User信息

签名:是对前两部分的签名,防止数据篡改

原理:

当用户使用凭据成功登录后,将返回一个json web token,由于token是凭据,不应该将token保留超过所需要的时间,也不应该将敏感数据存储在浏览器存储中,token在Authotization标头中放,跨域资源共享不会成为问题,因为它不使用cookie,使用jwt认证,程序可以使用access token去访问受保护的资源,比如在express中的使用,可以

1
2
3
4
5
//以/api/开头的不用权限,配置成功了express-jwt这个中间件,就可以把解析出来的用户信息挂载到req.user上
app.use(expressJWT({
secret:secretkey,
algorithms: ['HS256'],
}).unless({path:[/^\/api\//]}))

JWT优点:

不需要在服务端保存会话信息,所以易于应用的扩展,即信息不保存在服务端,不会存在Session扩展不方便的情况

JWT中的负载可以存储常用信息,用于信息交换,有效地使用JWT,可以降低服务端查询数据库的次数

JWT缺点:

到期问题:由于服务器不保存Session状态,因此无法在使用过程中废除某个Token,或者更改token的权限,也就是说,一旦JWT一旦签发,在到期之前就会始终有效,除非服务器部署额外的逻辑

关于jwt认证:

https://zhuanlan.zhihu.com/p/164696755

jwt与token区别:

Token和JWT(JSON Web Token)是两种常见的身份验证和授权机制,它们之间存在一些区别。

  1. 格式:Token通常是一个简单的字符串,以某种特定的格式表示。而JWT是一个基于JSON的标准,它将用户的相关信息以JSON对象的形式进行编码,并使用签名来保证数据的完整性。

  2. 安全性:JWT具有更好的安全性。JWT使用数字签名或加密算法来验证和保护数据的完整性,并确保只有经过授权的人可以访问。而Token通常只是一个简单的字符串,可能更容易受到伪造或篡改的风险。

  3. 可扩展性:JWT具有更好的可扩展性。由于JWT是基于JSON的标准,可以在其Payload中添加自定义的字段来存储额外的用户信息。而Token通常只包含最基本的身份验证信息。

  4. 无状态性:JWT是无状态的,即服务器端不需要存储任何信息来验证或跟踪用户。服务器只需通过验证JWT的签名即可确认其有效性。而Token通常需要服务器端存储相关的信息,来验证和跟踪用户。

综上所述,JWT是一种更强大、更安全、更可扩展的身份验证和授权机制,它基于JSON的标准,使用数字签名来验证和保护数据的完整性。而Token通常是一个简单的字符串,没有JWT那么多的安全和扩展特性。

注意:json web token怎么解决客户端请求想要提前失效的功能

  1. 设置短过期时间,在生成JWT时,将其过期时间设置得比较短,例如只有几分钟或者几个小时,然后在每次请求服务器时,重新获取新的JWT,如果客户端需要让当前JWT失效 只需要不再使用它
  2. 使用黑名单机制,在服务端维护一个有效JWT黑名单,当客户端想要让某个JWT失效时,就将该JWT添加进黑名单,在每次验证JWT时,先检查该JWT是否在黑名单中,如果在则拒绝该jWT,这种方式需要额外的存储空间和逻辑复杂度。

性能优化:

路由懒加载

1 什么叫路由懒加载?

也叫延迟加载,即在需要的时候进行加载

2 为什么需要路由懒加载?

  • 首先,路由通常会定义很多不同的页面
  • 这个页面在项目build打包后,一般情况下,会放在一个单独的js文件中
  • 但是,如果很多页面都放在同一个js文件中,必然会造成这个页面非常大
  • 如果我们一次性地从服务器中请求这个页面,可能会花费一定的时间,用户体验不好
  • 为了避免这种情况,我们把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这就是路由懒加载

实现方式:

Vue异步组件

1
2
3
4
5
{
path:'/problem',
name:'problem',
component:resolve=>require(['../pages/problemList'],resolve)
}

ES6中的import()—推荐使用

1
2
3
4
5
6
routes:[{
path:'/',
name:"通讯录",
component:()=>Promise.resolve(import("../components/contact/contact.vue"))
},
]

webpack的require.ensure()

多个路由指定相同的chunkName,会合并打包成一个js文件,require.ensure可实现按需加载资源,包括js,css,它会给里面的require文件单独打包,不会和主文件打包在一起。

第一个参数是数组,表明第二个参数里需要加载的模块,这些会提前加载,

第二个是回调函数,在这个回调函数里面requrie的文件会被单独打包成一个chunk,不会和主文件打包在一起,这样就生成两个chunk,第一次加载时只加载主文件

第三个参数是错误回调

第四个参数是单独打包的chunk的文件名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Vue from 'vue';
import Router from 'vue-router';
const HelloWorld=resolve=>{
require.ensure(['@/components/HelloWorld'],()=>{
resolve(require('@/components/HelloWorld'))
})
}
Vue.use('Router')
export default new Router({
routes:[{
{path:'./',
name:'HelloWorld',
component:HelloWorld
}
}]
})

element-plus按需加载:配置vue.config.js

1
2
3
4
5
6
7
configureWebpack:{
plugins:[
require('unplugin-element-plus/webpack')({

}),
]
}

UglifyPlugin Webpack Plugin 插件用来缩小js文件

vue.config.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
configureWebpack:{
plugins:[
require('unplugin-element-plus/webpack')({

}),
//代码压缩
new UglifyJsPlugin({
uglifyOptions:{
compress:{
drop_debugger:true,
drop_console:true,
pure_funcs:['console.log']
}
},
sourceMap:false,
parallel:true
})
],
},

封装axios:

特征:

  • 从浏览器创建XMLHttpRequests
  • 从node.js创建http请求
  • 支持Promise API
  • 拦截请求和响应
  • 转换请求和响应数据
  • 取消请求
  • 自动转换JSON数据
  • 客户端支持防御XSRF

具体:https://coloey.github.io/2022/04/11/Vue%E4%B8%AD%E5%B0%81%E8%A3%85axios%E8%AF%B7%E6%B1%82/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import axios from "axios";
import { ElLoading, ElMessage } from "element-plus";
import router from "../router";
import store from "../store";
import CHAT from "../client";
//创建一个axios实例
var instance = axios.create({
baseURL: "http://127.0.0.1:3007",
timeout: 10000, //设置超时
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
});
let loading;
//多次请求时
let requestCount = 0;
//显示Loading
const showLoading = () => {
if (requestCount === 0 && !loading) {
//第一次发送请求并且没有loading加载loaing
loading = ElLoading.service({
text: "Loading",
background: "rgba(0,0,0,0.7)",
spinner: "el-icon-loading",
});
}
requestCount++; //多次请求
};
//隐藏loading
const hideLoading = () => {
requestCount--;
if (requestCount === 0) {
loading.close(); //直到请求都结束Loading才关闭
}
};
//请求拦截器
instance.interceptors.request.use(
(config) => {
showLoading();
//每次发送请求前判断是否存在token如果存在则在header加上token
const token = window.localStorage.getItem("token");
token && (config.headers.Authorization = token);

return config;
},
(error) => {
Promise.reject(error);
}
);

//响应拦截器
instance.interceptors.response.use(
(response) => {
hideLoading();
//响应成功
const status = response.data.status;
if (status != 1) {
//策略模式
let stragtegy = {
0: function (res) {
//响应成功后如果是登录成功有token把token存储在本地
if (res.data.token !== undefined)
window.localStorage.setItem("token", res.data.token);
},
200: function (res) {
//获取用户信息成功后存储在localStorage里和store
store.commit("saveUserInfo", res.data.data);
window.localStorage.setItem(
"userInfo",
JSON.stringify(res.data.data)
);
},
401: function () {
//登录过期,清空token,跳转到登录页面
console.log("1");
window.localStorage.removeItem("token");
window.localStorage.removeItem("userInfo");
CHAT.logout();
router.push("/login");
},
201: function () {
//退出登录清空token,跳转登录页面
window.localStorage.removeItem("token");
const userInfo = window.localStorage.getItem("userInfo");
if (userInfo) {
CHAT.logout();
window.localStorage.removeItem("userInfo");
}
router.push("/login");
},
};
stragtegy[status] && stragtegy[status](response);
if (response.data.message) ElMessage.success(response.data.message);
return Promise.resolve(response);
} else {
ElMessage.error(response.data.message);
return Promise.reject(response);
}
},
(error) => {
let stragtegy = {
500: function () {
return "服务器错误(500)";
},
501: function () {
return "服务未实现(501)";
},
502: function () {
return "网络错误(502)";
},
503: function () {
return "服务不可用(503)";
},
504: function () {
return "网络超时(504)";
},
505: function () {
return "HTTP版本不受支持(505)";
},
};
//响应错误
if (error.response && error.response.status) {
const status = error.response.status;
let message =
(stragtegy[status] && stragtegy[status]()) || `连接出错(${status})!`;
ElMessage.error(message);
return Promise.reject(error);
}
return Promise.reject(error);
}
);
export default instance;

request.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import instance from "./index"
const axios=({
method,
url,
data,
config
})=>{
method=method.toLowerCase();
if(method=='post'){
return instance.post(url,data,{...config})
}else if(method=='get'){
return instance.get(url,{
params:data,
...config
})
}else if(method=='delete'){
return instance.delete(url,{
params:data,
...config
})
}else if(method=='put'){
return instance.put(url,data,{...config})
}else{
console.log('未知的方法'+method)
return false
}
}
export default axios

加缓存的axios封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import axios, { AxiosResponse, AxiosRequestConfig, AxiosInstance } from 'axios';
import { trackFMP, trackBeforeFMP } from '@/fmp';
import router from '@/router';
import { NOT_LOGIN_RESULTS } from './const';
import { handleLogin } from './utils/user';

interface CustomConfig {
cacheTime: number;
}

let instance: AxiosInstance;
export function initInstance() {
instance = axios.create({
timeout: 10 * 1000,
withCredentials: true,
});

const errorHandler = (error: Error) => {
throw error;
};

const requestHandler = (config: AxiosRequestConfig) => {
return config;
};

const responseHandler = (response: AxiosResponse<Response>) => {
return new Promise((resolve) => {
const config = response.config;
const body = response.data;
resolve(body);
}).then(null, errorHandler);
};

const notLoginResponseHandler = (
response: AxiosResponse<{
result: number;
}>,
) => {
if (response.config.useDefaultLoginHandle && NOT_LOGIN_RESULTS.includes(response.data.result)) {
handleLogin();
}
return response;
};

instance.interceptors.request.use(requestHandler);
instance.interceptors.response.use(notLoginResponseHandler);
instance.interceptors.response.use(responseHandler);
}

const baseUrl = '';
//TODO 等正式上线后修改为线上域名,前端打散
// const serverUrl = ['https://qatar2022-c.staging.kuaishou.com'];
// function getServerUrl(): string {
// //这里后续可以增加策略,目前只是简单随机,可以参考cdn的,通过url下发权重
// return serverUrl[Math.floor(Math.random() * serverUrl.length)] as string;
// }
//立即执行,同一个页面中的接口走同一个域名,减少重复计算
// const baseUrl = process.env.NODE_ENV === 'production' ? getServerUrl() : '';

const axiosQuee: {
[params: string]: Promise<any>;
} = {};

export const get = <T>(
url: string,
params?: object | string,
config?: AxiosRequestConfig,
custConfig?: CustomConfig,
) => {
url = baseUrl + url;
const key = url + JSON.stringify(params || {});
url && !config?.preload && trackBeforeFMP(router, url as string);
if (!axiosQuee[key]) {
axiosQuee[key] = instance.get<T, Promise<T>>(url, { params, ...config }).then(
(res) => {
if (custConfig && custConfig.cacheTime > 0) {
axiosQuee[key] = new Promise((resolve) => {
resolve(res);
});
setTimeout(() => {
delete axiosQuee[key];
}, custConfig.cacheTime);
} else {
delete axiosQuee[key];
}

return res;
},
() => {
delete axiosQuee[key];
},
);
}
return (axiosQuee[key] as Promise<T>).then((res) => {
url && !config?.preload && trackFMP(router, url as string);
return res;
});
};

export const post = <T>(url: string, params?: any, config?: AxiosRequestConfig, custConfig?: CustomConfig) => {
url = baseUrl + url;
const key = url + JSON.stringify(params || {});
url && !config?.preload && trackBeforeFMP(router, url as string);
if (!axiosQuee[key]) {
axiosQuee[key] = instance.post<T, Promise<T>>(url, params, config).then(
(res) => {
if (custConfig && custConfig.cacheTime > 0) {
axiosQuee[key] = new Promise((resolve) => {
resolve(res);
});
setTimeout(() => {
delete axiosQuee[key];
}, custConfig.cacheTime);
} else {
delete axiosQuee[key];
}
return res;
},
() => {
delete axiosQuee[key];
},
);
}
return (axiosQuee[key] as Promise<T>).then((res) => {
url && !config?.preload && trackFMP(router, url as string);
return res;
});;
};

跨域处理方法:

服务端配置cors:

1
2
3
4
5
6
7
const cors=require('cors')
const io=require('socket.io')(server,{
//服务端配置cors
cors: {
origin: "https://coloey.github.io"
}
})

本地配置proxy代理转发路由表:

原理:在本地运行的npm run serve 等命令实际上是用node运行了一个服务器,因此,proxyTable实际上是将请求转发给自己的服务器,再由服务器转发给后台服务器,做了一层代理,vue的proxyTable用了proxy-middleware中间件,不会出现跨域问题

如果接口里面有个公共的名字,比如security,可以拿出来做来做跨域配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module.exports = {
devServer: {
open: true,
host: 'localhost',
port: 8000,
https: false,
//以上的ip和端口是我们本机的;下面为需要跨域的
proxy: { //配置跨域
'/security': {
target: 'http://127.0.0.1:8000/', //要代理到的api地址
ws: true,
changOrigin: true, //允许跨域
pathRewrite: {
'^/security/': '/security'
}
}
}
}
}

如果接口中没有公共部分,另外设置一个代理名称,用api设置代理转发,接口请求就需要带上api.HTTP请求代理中多了ws:false属性,如果不加这个属性,浏览器控制台会一直报连接不上socket的错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

let proxyObj = {};
proxyObj['/ws'] = {
ws: true,
target: "ws://localhost:8081",//目标服务器
};
proxyObj['/api'] = {
ws: false,
target: "http://localhost:8081",
changeOrigin: true,
pathRewrite: {
'^/api': '/api'
}
};
module.exports = {
devServer: {
host: 'localhost',
port: 8080,
proxy: proxyObj

这个设置只有在本地的时候是起作用的,当项目编译打包时,配置不会打包进去。

生产环境让后台配置cors或者nginx将前端代码拷贝到后端,在nginx.conf中配置,以Vue为例,如果是SPA应用,项目打包后是一个index.html还有几个js,css等文件,这些静态文件上传到服务器,在nginx.conf中配置静态资源访问:

1
2
3
4
location ~ .*\.(js|css|ico|png|jpg|eot|svg|ttf|woff|html|txt|pdf|) {
root /usr/local/nginx/html/;#所有静态文件直接读取硬盘
expires 30d; #缓存30
}

即后缀为js,css,ico等文件统统不进行请求转发,直接从本地的/usr/local/nginx/html目录下读取并返回到前端(我们需要将静态资源文件上传到/usr/local/nginx/html/目录下)

聊天功能实现:

socket.io的主要靠emit()和on()实现实时聊天,关键在于设计对话的数据结构,对话用一个对象保存,里面包含发送者姓名,接收者姓名,信息,将对话展示在对话页面中主要靠fromUser,toUser识别,根据路由参数显示对话列表,每个用户登录将用户名作为参数通过emit触发服务端addUser,服务端用一个onLineUsers对象存储socket对象,键为用户名,值为用户socket对象,每个socket对象是唯一的,服务端on监听sendMessage事件,客户端emit触发sendMessage事件,携带obj对象参数,包含对话信息和用户信息,当服务端的sendMessage事件被客户端触发后回调函数执行,以用户名为回调函数名,将obj对象作为参数,触发客户端展示消息的函数,表情包发送是通过写一个emoji表情包子组件,通过和对话列表父组件进行通信,点击表情包Icon,展示一个表情包列表,点击里面任意一个表情,子组件触发emit(chooseEmoji,emoji),传递emoji信息给父组件的chooseEmoji函数,父组件就能在输入框中显示被选中的emoji

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
//const store=require("../src/store")
const express = require("express");
const cors = require("cors");
const app = express();
const Joi = require("joi");
app.use(cors());
//解析application/x-www-form-urlencoded数据
app.use(express.urlencoded({ extended: false }));
//在路由之前封装res.cc函数,中间件处理函数处理响应
app.use((req, res, next) => {
res.cc = function (resData, status = 1) {
res.send({
status,
message: resData instanceof Error ? resData.message : resData, //resData的值可能是一个错误对象也可能是一个描述字符串
});
};
next();
});

//解析token的中间件
//api开头的接口都不需要进行token解析,配置成功了express-jwt这个中间件,可以把解析出来的用户信息挂载到req.user上
const expressJWT = require("express-jwt");
const config = require("./config");
app.use(
expressJWT({
secret: config.jwtSecrectKey,
algorithms: ["HS256"],
}).unless({ path: [/^\/api\//] })
);

//导入用户路由模块
const userRouter = require("./router/user");
app.use("/api", userRouter);
//导入并使用用户信息模块
const userInfoRouter = require("./router/userInfo");
app.use("/my", userInfoRouter);
//错误中间件
app.use(function (err, req, res, next) {
//数据验证失败
if (err instanceof Joi.ValidationError) return res.cc(err);
if (err.name === "UnauthorizedError")
return res.send({ status: 401, message: "登录已过期,请重新登录" });
//未知错误
res.cc(err);
});
const { createServer } = require("http");
const { on } = require("events");
const server = createServer(app);
const io = require("socket.io")(server, {
//服务端配置cors
cors: {
origin: "https://coloey.github.io",
// origin: "http://localhost:8081",
},
});
let onlineUsers = {}; //存储在线用户的对象
//let onlineCount = 0;
let user = "";
io.on("connection", function (socket) {
socket.emit("open");
let toUser = "";
let fromUser = "";
let date = "";
socket.on("addUser", function (username) {
// eslint-disable-next-line no-prototype-builtins
if (!onlineUsers[username]) {
//onlineCount += 1;
onlineUsers[username] = socket;
}
console.log(onlineUsers[username].id);
console.log("onlineCount", Object.keys(onlineUsers).length);
});
socket.on("sendMsg", (obj) => {
//console.log(obj);
(toUser = obj.toUser), (fromUser = obj.fromUser);
date = obj.date;
if (toUser in onlineUsers) {
//接收方
onlineUsers[toUser].emit(toUser, obj); //两边都显示信息
onlineUsers[fromUser].emit(fromUser, obj);
} else {
onlineUsers[fromUser].emit(fromUser, obj);
console.log(toUser + "不在线");
}
});
socket.on("sendPost", (obj) => {
// console.log(obj);
for (let username in onlineUsers) {
// console.log(username);
onlineUsers[username].emit("post" + username, obj);
}
});
socket.on("disconnect", () => {
console.log(`${socket.id}断开连接`);
delete onlineUsers[fromUser];
});
});

server.listen(3007, () => {
console.log("run in http://127.0.0.1:3007");
});

client.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import { io } from "socket.io-client";
import store from "../src/store";

let socket = io();
const CHAT = {
username: null,
date: null,
init: function (username, flush = false) {
socket = io("http://127.0.0.1:3007", {
"force new connection": true,
});
socket.on("open", function () {
console.log("已连接");
//console.log(`client:${socket.connected}`);
if (flush) {
socket.on(username, function (obj) {
console.log("CHAT.msgArr", obj);
store.commit("ChangeMsg", obj);
});
flush = false;
}
});
socket.emit("addUser", username);
},

logout: function () {
if (socket) {
socket.disconnect();
socket.close();
}
},
scrollToBottom: function () {
window.scrollTo(0, 900000);
},
sendPost: function (obj) {
socket.emit("sendPost", obj); //触发发送朋友圈事件
},
submit: function (obj) {
console.log("submit");
socket.emit("sendMsg", obj); //触发事件
},
message: function (username) {
socket.on(username, function (obj) {
console.log("CHAT.msgArr", obj);
store.commit("ChangeMsg", obj);
});
},
post: function (username) {
socket.on("post" + username, function (obj) {
console.log("UploadPost:", obj);
store.commit("UploadPost", obj);
});
},
};
export default CHAT;

消息列表展示:

将当前登录用户信息和聊天记录信息,动态消息存储到vuex,因为组件共享该信息,通过判断toUser即为当前页面的用户并且fromUser是登录用户信息item.toUser==$route.query.username(发给页面用户信息)&&item.fromUser==$store.state.userInfo.username(来自于当前用户)得出是登录用户发送的信息,v-show=”item.toUser==$store.state.userInfo.username&&item.fromUser==$route.query.username” 得出是接收用户的发送信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div v-for="(item, index) in msg"
:key="index" >
<li
class="self clearfix"
v-show="item.toUser===$route.query.username&&item.fromUser===$store.state.userInfo.username"
>
<p class="text" v-more>
{{ item.text }}
<img :src=item.imageUrl alt="" v-show="item.imageUrl"/>
</p>
<div class="header"><img :src="$store.state.userInfo.user_pic" /></div>
</li>
<li
class="other clearfix"
v-show="item.toUser===$store.state.userInfo.username&&item.fromUser===$route.query.username"
>
<!-- 获取当前聊天的人的头像 -->
<div class="header"><img :src="otherpic.headerUrl" /></div>
<p class="text" v-more>
{{ item.text }}
<img :src=item.imageUrl alt="" v-show="item.imageUrl"/>
</p>
</li>
</div>

断线重连和持久存储

1.用vuex-persistedstate插件实现聊天消息持久存储 实现原理就是在Vuex的mutation操作中监听状态变更,并将最新的状态数据保存在本地存储中,以便下次用于程序启动时可以恢复之前的状态,这样可以方便地实现Vuex状态的持久存储,使得应用程序可以在刷新页面或重新打开应用时保持之前的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
export default createStore({
state: {
...
},
mutations,
actions,
getters,
plugins: [
persistedState({
key: "state",
render(state) {
return state.msg;
},
}),
], //默认使用localStorage存储数据
});

实现刷新页面断线重连

因为socket.io会在页面刷新时自动断开socket连接,因此会删除onLineUsers[fromUser],在页面挂载时监听页面刷新完成load事件,页面刷新挂载时重新建立socket连接,将对象加入,客户端部分监听连接是异步回调使用nextTick,避免多次刷新页面,多次初始化用户信息,提高性能

监听到页面刷新时 => 执行回调函数,从localStorage中取出用户信息重新建立socket连接,执行message方法,再拉一次聊天信息 => 在执行message方法时发现socket还是undefined,因为重新建立连接是异步方法,而onMounted中直接执行message是同步方法,此时socket还未建立。因此,刷新时拉取消息要单独执行,可以在重新建立连接init方法中监听事件open事件(服务端连接成功触发open事件)并且用flush标识是否是刷新建立的连接,是的话再监听服务端发过来的信息,才会成功建立新的socket连接

app.js:

1
2
3
4
socket.on("disconnect", () => {
console.log(`${socket.id}断开连接`);
delete onlineUsers[fromUser];
});

contact-dialogue.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const handler = () => {      
const username = JSON.parse(window.localStorage.getItem("userInfo")).username
console.log('刷新',username)
//刷新页面时,socket.io主动断开连接,因此如果localStorage里面有用户信息,则可以直接再为用户初始化一个socket对象,加入onlineUsers,
if(username){
nextTick(()=>{
CHAT.init(username,true)
})
}
}
onMounted(()=>{
//监听刷新事件
window.addEventListener('load',handler)
CHAT.message(store.state.userInfo.username)
})
onUnmounted(()=>{
//卸载刷新事件
window.removeEventListener('load',handler)
})

client.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
init: function (username, flush = false) {
socket = io("http://127.0.0.1:3007", {
"force new connection": true,
});
socket.on("open", function () {
//如果
console.log("已连接");
//console.log(`client:${socket.connected}`);
//刷新成功后,重新建立socket连接,但是message方法是同步的,这时候socket还是undefined,
//因此要在刷新后成功建立连接后再监听服务端发过来的信息,才会成功建立新的socket连接
if (flush) {
socket.on(username, function (obj) {
console.log("CHAT.msgArr", obj);
store.commit("ChangeMsg", obj);
});
flush = false;
}
});
socket.emit("addUser", username);
},
...
message: function (username) {
socket.on(username, function (obj) {
console.log("CHAT.msgArr", obj);
store.commit("ChangeMsg", obj);
});
},
...

mutations.js:

1
2
3
4
//改变聊天信息
ChangeMsg(state,obj){
state.msg.push(obj);
},

性能优化

vue.config.js:

UglifyPlugin Webpack Plugin 插件用来缩小js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
const UglifyJsPlugin=require('uglifyjs-webpack-plugin')
module.exports = {
publicPath: process.env.NODE_ENV === 'production'
? '/Vue-chat/'
: '/' ,
// 输出文件目录
outputDir: 'dist',
// eslint-loader 是否在保存的时候检查
lintOnSave: true,
// use the full build with in-browser compiler?
// https://vuejs.org/v2/guide/installation.html#Runtime-Compiler-vs-Runtime-only
// compiler: false,
// webpack配置
// see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md
chainWebpack: () => {},
//代码压缩
configureWebpack:{
plugins:[
new UglifyJsPlugin({
uglifyOptions:{
compress:{
drop_debugger:true,
drop_console:true,
pure_funcs:['console.log']//删除console.log语句
}
},
sourceMap:false,
parallel:true
})
],
},
// vue-loader 配置项
// https://vue-loader.vuejs.org/en/options.html
// vueLoader: {},
// 生产环境是否生成 sourceMap 文件
productionSourceMap: true,
// css相关配置
// css: {
// // // 是否使用css分离插件 ExtractTextPlugin
// extract: true,
// // 开启 CSS source maps?
// sourceMap: false,
// // // css预设器配置项
// loaderOptions: {
// css:{},//这里的选项会传递给css-loader
// postcss:{}//这里的选项会传递给postcss-loader
// },
// // // 启用 CSS modules for all css / pre-processor files.
// requireModuleExtension: false
// },
// use thread-loader for babel & TS in production build
// enabled by default if the machine has more than 1 cores
parallel: require('os').cpus().length > 1,// 是否为 Babel 或 TypeScript 使用 thread-loader。该选项在系统的 CPU 有多于一个内核时自动启用,仅作用于生产构建。
// 是否启用dll
// See https://github.com/vuejs/vue-cli/blob/dev/docs/cli-service.md#dll-mode
// dll: false,
// PWA 插件相关配置
// see https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa
pwa: {},
// webpack-dev-server 相关配置
devServer: {
open: process.platform === 'darwin',
disableHostCheck: true,
port:8081,
https: false,
hotOnly: false,

before: app => {}
},

// 第三方插件配置
pluginOptions: {
// ...
}
}

2 路由懒加载:

把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加,以函数的形式加载路由,这样就可以把各自的路由文件分别打包,只有在解析给定的路由时,才会加载路由组件

1
2
3
4
5
6
routes:[{
path:'/',
name:"通讯录",
component:()=>Promise.resolve(import("../components/contact/contact.vue"))

},

3 UI框架按需加载

在日常使用的UI框架,例如element-plus,我们经常直接使用整个UI库

1
import ElemtPlus from 'element-plus'

但实际上我用到的组件只有按钮,分页,表格,输入与警告,所以我们需要按需引用

1
2
import {Button,Input,Table,TableColumn,MessageBox} from 'element-plus'

element-plus官网:

配置vue.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const AutoImport = require("unplugin-auto-import/webpack");
const Components = require("unplugin-vue-components/webpack");
const { ElementPlusResolver } = require("unplugin-vue-components/resolvers");
module.exports = {
publicPath: process.env.NODE_ENV === "production" ? "/Vue-chat/" : "/",
// 输出文件目录
outputDir: "dist",
// eslint-loader 是否在保存的时候检查
lintOnSave: true,
configureWepeack:{
plugins:[
AutoImport({
resolvers:[ElementPlusResolver()],
}),
Components({
resolvers:[ElementPlusResolver()]
})
]
},

productionSourceMap: true,
};

4 静态资源本地缓存

后端返回资源问题:

  • 采用HTTP缓存,设置Cache-ControlLast-ModifiedEtag等响应头
  • 采用Service Worker离线缓存

前端合理利用localStorage

5 图片资源的压缩

图片资源虽然不在编码过程中,但它却是对页面性能影响最大的因素

对于所有的图片资源,我们可以进行适当的压缩

对页面上使用到的icon,可以使用在线字体图标,或者雪碧图,将众多小图标合并到同一张图上,用以减轻http请求压力,打包过程中,如果图片尺寸小于等于1KB,会被转成base64编码直接嵌入到bundle.js文件中,它会被保存到output目录中,并在bundle.js中引用图片文件的路径,注意的是,使用base64编码可以减少http请求,但也会增加bundle.js文件的大小,因此需要根据实际情况权衡是否使用base64编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 1024, // 设置图片大小限制为1KB
name: 'img/[name].[hash:8].[ext]' // 文件名和路径配置
}
},
// ...
]
},
// ...
}

开启GZip压缩

拆完包之后,我们再用gzip做一下压缩 安装compression-webpack-plugin

1
npm i compression-webpack-plugin -D

vue.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const CompressionPlugin = require('compression-webpack-plugin')

configureWebpack: (config) => {
if (process.env.NODE_ENV === 'production') {
// 为生产环境修改配置...
config.mode = 'production'
return {
plugins: [new CompressionPlugin({
test: /\.js$|\.html$|\.css/, //匹配文件名
threshold: 10240, //对超过10k的数据进行压缩
deleteOriginalAssets: false //是否删除原文件
})]
}
}

6 使用SSR

SSR(Server side ),也就是服务端渲染,组件或页面通过服务器生成html字符串,再发送到浏览器

从头搭建一个服务端渲染是很复杂的,vue应用建议使用Nuxt.js实现服务端渲染