前端数据一致性
# 前端用户登录后如何保持数据一致性
在前端开发中,用户登录后,用户信息的管理是一个非常重要的环节。我们需要确保用户信息在整个应用中保持一致,并且在页面刷新后不丢失。这通常涉及到以下几个步骤:
- 用户信息的存储:在用户登录成功后,我们需要将用户信息存储在浏览器的
localStorage
或sessionStorage
中。 - Vuex 中的状态管理:为了方便在应用的不同组件之间共享用户信息,我们通常会使用
Vuex
来管理这些信息。 - 数据的同步与持久化:为了避免页面刷新导致
Vuex
中的数据丢失,我们需要在Vuex
初始化时从localStorage
或sessionStorage
中加载用户信息,并在用户信息更新时同步到本地存储中。
# 1. 理论背景
# 1.1 为什么需要使用 Vuex 和本地存储?
- 数据共享:在大型应用中,用户信息可能会被多个组件使用。通过
Vuex
,我们可以轻松实现数据的共享,并确保数据的一致性。 - 数据持久化:
Vuex
中的数据是保存在内存中的,这意味着一旦页面刷新,数据将会丢失。因此,我们需要将重要的数据(如用户信息)存储在localStorage
或sessionStorage
中,以便在页面刷新时重新加载这些数据。
# 1.2 localStorage
vs sessionStorage
localStorage
:数据会一直保存在浏览器中,直到用户手动删除或清除缓存。适用于需要长期保存的数据。sessionStorage
:数据只在当前会话中有效,关闭浏览器窗口后数据会被清除。适用于需要临时保存的数据。
# 2. 实现步骤
- 用户登录成功后:我们首先将用户信息提交到
Vuex
中,这样可以方便在应用的不同组件之间共享这些信息。 - Vuex 中的数据同步到本地存储:在用户信息提交到
Vuex
后,我们会自动将这些信息同步到本地浏览器的存储(localStorage
或sessionStorage
)中。
# 2.1 用户信息存储在 localStorage
的方案
在这个方案中,用户信息在登录成功后会被存储到 Vuex
中。我们会将这些信息同步到 localStorage
,并在用户信息变更时同时更新 localStorage
。
代码实现:
创建 Vuex Store
首先,我们创建一个 Vuex Store,用于管理应用中的用户信息和其他状态数据。
// 引入 Vue 核心库 import Vue from 'vue'; // 引入 Vuex import Vuex from 'vuex'; // 应用 Vuex 插件 Vue.use(Vuex); // 同步用户信息到 localStorage 的辅助函数 function syncUserToLocalStorage(user) { // 将用户信息序列化后存储到 localStorage 中 localStorage.setItem('xm-user', JSON.stringify(user)); } // 准备 state 对象——保存具体的数据 const state = { activeIndex: '/home', // 当前激活的菜单索引 user: { id: '', // 用户ID username: '', // 用户名 avatar: '', // 用户头像 email: '', // 用户邮箱 token: '', // 用户令牌 callbackUrl: '' // 回调地址 } }; // 准备 mutations 对象——修改 state 中的数据 const mutations = { EditActiveIndex(state, val) { state.activeIndex = val; // 修改激活的菜单索引 }, EditUser(state, val) { state.user = { ...state.user, ...val }; // 合并更新用户信息,保留未修改的数据 syncUserToLocalStorage(state.user); // 同步用户信息到 localStorage } }; // 创建并导出 store const store = new Vuex.Store({ state, mutations }); // 在应用初始化时,从 localStorage 加载用户信息到 Vuex const savedUser = JSON.parse(localStorage.getItem('xm-user') || '{}'); // 检查存储数据的完整性,避免无效数据覆盖 Vuex 状态 if (savedUser.id) { // 判断 id 是否存在,确保数据有效 store.commit('EditUser', savedUser); // 将保存的用户信息同步到 Vuex } export default store;
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代码详解:
syncUserToLocalStorage
函数:用于将用户信息同步到localStorage
中。每次用户信息发生变动时,都会调用这个函数。state
对象:state
中保存了当前的用户信息和菜单索引等数据。mutations
对象:mutations
用于修改state
中的数据,并在用户信息修改后同步到localStorage
。- 应用初始化:在应用初始化时,我们从
localStorage
中加载用户信息到Vuex
,确保刷新页面后数据不丢失。
用户登录后的处理
当用户登录成功后,我们需要将用户信息存储到
Vuex
并同步到localStorage
。methods: { handleLogin() { this.$refs.loginForm.validate((valid) => { if (valid) { request.post('/web/login', this.loginForm) .then(response => { if(response && response.code === 200) { this.$message.success('登录成功'); const user = response.data; // 将用户信息同步到 Vuex 和 localStorage this.$store.commit("EditUser", user); this.$router.push('/main'); // 登录成功后跳转到主页面 } else { this.$message.error(response.message); } }); } }); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22代码详解:
handleLogin
方法:在用户登录成功后,调用EditUser
mutation 更新Vuex
中的用户信息,同时会自动同步到localStorage
中。
# 2.2 支持 localStorage
和 sessionStorage
的方案
在某些情况下,我们可能需要根据用户选择的“记住我”选项,动态选择将用户信息存储到 localStorage
或 sessionStorage
中。为此,我们扩展了之前的方案,支持在登录时动态选择存储方式。
代码实现:
创建 Vuex Store
在这个方案中,我们在
Vuex
的state
中新增一个storageType
字段,用于记录当前用户信息存储在哪个存储中。// 引入 Vue 核心库 import Vue from 'vue'; // 引入 Vuex import Vuex from 'vuex'; // 应用 Vuex 插件 Vue.use(Vuex); // 同步用户信息到存储的辅助函数 function syncUserToStorage(user, storageType) { const storage = storageType === 'local' ? localStorage : sessionStorage; storage.setItem('xm-user', JSON.stringify(user)); } // 准备 state 对象——保存具体的数据 const state = { activeIndex: '/home', // 当前激活的菜单索引 user: { id: '', // 用户ID username: '', // 用户名 avatar: '', // 用户头像 email: '', // 用户邮箱 token: '', // 用户令牌 callbackUrl: '' // 回调地址 }, storageType: 'local' // 默认使用 localStorage }; // 准备 mutations 对象——修改 state 中的数据 const mutations = { EditActiveIndex(state, val) { state.activeIndex = val; // 修改激活的菜单索引 }, EditUser(state, val) { state.user = { ...state.user, ...val }; // 合并更新用户信息,保留未修改的数据 // 根据 storageType 来确定修改后的用户信息同步到local还是session syncUserToStorage(state.user, state.storageType); }, SetStorageType(state, storageType) { state.storageType = storageType; // 设置存储类型为 localStorage 或 sessionStorage } }; // 创建并导出 store const store = new Vuex.Store({ state, mutations }); // 刷新页面的时候,vuex数据丢失,我们会按以下步骤重新加载数据 // 首先从 localStorage 获取中的用户信息,并设置存储类型为local let savedUser = JSON.parse(localStorage.getItem('xm-user') || '{}'); let storageType = 'local'; // 如果localStorage中没有获取到,我们再从sessionStorage中获取,并设置存储类型为session if (Object.keys(savedUser).length === 0) { savedUser = JSON.parse(sessionStorage.getItem('xm-user') || '{}'); storageType = 'session'; } // 如果用户信息存在,更新 Vuex 的状态 if (Object.keys(savedUser).length > 0) { store.commit('SetStorageType', storageType); store.commit('EditUser', savedUser); } export default store;
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代码详解:
syncUserToStorage
函数:根据传入的storageType
参数决定将用户信息保存到localStorage
还是sessionStorage
中。storageType
:用于记录当前用户信息保存在哪种存储中,以确保后续的同步操作能正确进行。初始化逻辑:首先尝试从
localStorage
获取用户信息,如果获取不到,再从sessionStorage
获取,并根据获取到的信息来源设置storageType
。
用户登录后的处理
登录时,根据用户是否选择“记住我”来决定用户信息保存的位置。
methods: { handleLogin() { this.$refs.loginForm.validate((valid) => { if (valid) { request.post('/web/login', this.loginForm) .then(response => { if(response && response.code === 200) { this.$message.success('登录成功'); const user = response.data; if (this.loginForm.rememberMe) { this.$store.commit("SetStorageType", 'local'); this.$store.commit("EditUser", user); } else { this.$store.commit("SetStorageType", 'session'); this.$store.commit("EditUser", user); } this.$router.push('/main'); // 登录成功后跳转到主页面 } else { this.$message.error(response.message); } }); } }); } }
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代码详解:
handleLogin
方法:在用户登录成功后,根据“记住我”选项设置storageType
,然后更新Vuex
和对应的存储(localStorage
或sessionStorage
)。
# 2.3 用户登录成功之后同步信息到vuex
发送登录请求:用户提交登录表单,前端通过
request
(或其他 HTTP 库)将表单数据发送给后端服务器进行验证。接收服务器响应:服务器验证成功后,会返回完整的用户信息(包括
id
、username
、email
等)。将用户信息提交到 Vuex:
- 使用 Vuex 的
commit
方法将完整的用户信息提交到 Vuex 中的user
状态。 - 同时,Vuex 会自动将用户信息同步到本地存储(
localStorage
或sessionStorage
)。
- 使用 Vuex 的
methods: {
handleLogin() {
// 进行表单验证
this.$refs.loginForm.validate((valid) => {
if (valid) {
// 向服务器发送登录请求
request.post('/web/login', this.loginForm)
.then(response => {
if (response && response.code === 200) {
this.$message.success('登录成功');
const user = response.data; // 服务器返回的完整用户信息
// 根据用户是否选择“记住我”,确定存储类型
const storageType = this.loginForm.rememberMe ? 'local' : 'session';
// 将存储类型设置到 Vuex
this.$store.commit('SetStorageType', storageType);
// 将用户信息提交到 Vuex,并同步到指定的存储(localStorage 或 sessionStorage)
this.$store.commit('EditUser', user);
// 登录成功后跳转到主页面
this.$router.push('/main');
} else {
this.$message.error(response.message);
}
})
.catch(error => {
console.error('登录失败:', error);
this.$message.error('登录失败,请检查您的网络连接或联系管理员。');
});
}
});
}
}
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
存储类型选择:
- 使用
this.loginForm.rememberMe
来决定用户信息存储在localStorage
还是sessionStorage
中。如果用户选择了“记住我”,则使用localStorage
,否则使用sessionStorage
。
更新 Vuex 和存储:
SetStorageType
:先通过SetStorageType
mutation 将当前的存储类型(localStorage
或sessionStorage
)保存到 Vuex 中的storageType
字段。EditUser
:然后通过EditUser
mutation 将服务器返回的用户信息合并更新到 Vuex 的user
状态中,并自动同步到相应的存储。
# 2.4 如何在 Vuex 中处理用户信息的部分更新
- 合并更新:
EditUser
mutation 使用state.user = { ...state.user, ...val }
来更新用户信息。这意味着当你传入部分用户信息时,只会更新指定的字段,未传入的字段会保留原来的值。- 例如,如果只传入
{ email: 'newemail@example.com' }
,那么state.user
的其他字段如username
、avatar
等将保持不变。
- 确保同步:
- 每次更新用户信息后,都会调用
syncUserToStorage
函数,将最新的用户信息同步到localStorage
或sessionStorage
中。这保证了即使刷新页面,Vuex 也能从存储中恢复最新的用户信息。
- 每次更新用户信息后,都会调用
假设用户在表单中更新了他们的邮箱地址,并提交表单。我们可以这样处理:
methods: {
updateUserInfo() {
const updatedUserInfo = {
email: this.form.email, // 假设用户只修改了邮箱地址
// 其他可能更新的字段可以在此处添加
};
// 提交更新的用户信息到 Vuex
this.$store.commit('EditUser', updatedUserInfo);
// 提交后可能进行一些后续操作,例如显示成功提示
this.$message.success('用户信息已更新');
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
在这个例子中,即使用户只修改了邮箱地址,Vuex 也能正确地更新用户信息,同时保留其他未修改的字段。
小结
以上方案可以兼容用户只修改部分字段或全部字段的情况。通过使用对象合并({ ...state.user, ...val }
),我们可以确保 Vuex 中的用户信息始终是完整和最新的。同时,每次更新后,我们都会同步信息到本地存储,确保页面刷新后数据不丢失。这种设计能够在不影响用户体验的前提下,保持数据的一致性和持久性。
# 2.5 vuex完整代码
// 引入 Vuex
import { createStore } from 'vuex';
// 同步用户信息到存储的辅助函数
function syncUserToStorage(user, storageType) {
const storage = storageType === 'local' ? localStorage : sessionStorage;
storage.setItem('xm-user', JSON.stringify(user));
}
// 准备 state 对象——保存具体的数据
const state = {
activeIndex: '/home', // 当前激活的菜单索引
tabList: [{ name: "首页", path: "/main/home" }], // 保存当前打开的标签页列表
user: {
// 你之前的用户信息相关字段在这里
},
storageType: 'local', // 默认使用 localStorage
collapse: false // 控制导航栏的折叠状态
};
// 准备 mutations 对象——修改 state 中的数据
const mutations = {
EditActiveIndex(state, val) {
state.activeIndex = val; // 修改激活的菜单索引
},
EditUser(state, val) {
state.user = { ...state.user, ...val }; // 合并更新用户信息,保留未修改的数据
// 根据 storageType 来确定修改后的用户信息同步到local还是session
syncUserToStorage(state.user, state.storageType);
},
SetStorageType(state, storageType) {
state.storageType = storageType; // 设置存储类型为 localStorage 或 sessionStorage
},
saveTab(state, tab) {
// 如果当前标签页不在 tabList 中,则添加到 tabList
if (!state.tabList.find(item => item.path === tab.path)) {
state.tabList.push({ name: tab.meta.name, path: tab.path });
}
},
removeTab(state, tab) {
// 根据标签名找到对应的索引并删除
const index = state.tabList.findIndex(item => item.path === tab.path);
state.tabList.splice(index, 1);
},
resetTab(state) {
// 重置标签页,只保留首页
state.tabList = [{ name: "首页", path: "/main/home" }];
},
trigger(state) {
// 切换导航栏的折叠状态
state.collapse = !state.collapse;
}
};
// 创建并导出 store
const store = createStore({
state,
mutations
});
// 刷新页面的时候,vuex数据丢失,我们会按以下步骤重新加载数据
// 首先从 localStorage 获取中的用户信息,并设置存储类型为local
let savedUser = JSON.parse(localStorage.getItem('xm-user') || '{}');
let storageType = 'local';
// 如果localStorage中没有获取到,我们再从sessionStorage中获取,并设置存储类型为session
if (Object.keys(savedUser).length === 0) {
savedUser = JSON.parse(sessionStorage.getItem('xm-user') || '{}');
storageType = 'session';
}
// 如果用户信息存在,更新 Vuex 的状态
if (Object.keys(savedUser).length > 0) {
store.commit('SetStorageType', storageType);
store.commit('EditUser', savedUser);
}
export default store;
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
# 2.6 前端请求响应拦截器
import axios from 'axios';
import router from "@/router";
import { Message } from 'element-ui'; // 引入 Element UI 的 Message 模块
// 创建一个新的 axios 实例
const request = axios.create({
baseURL: '/api', // 设置基础URL,所有请求会自动加上这个前缀
timeout: 30000 // 设置请求超时时间为30秒
});
// request 拦截器
// 这个拦截器在每个请求发送前都会执行
request.interceptors.request.use(config => {
config.headers['Content-Type'] = 'application/json;charset=utf-8'; // 设置请求头的内容类型为 JSON
// 首先尝试从 localStorage 获取用户信息
let user = JSON.parse(localStorage.getItem("xm-user") || '{}');
// 如果 localStorage 中没有用户信息,再尝试从 sessionStorage 获取
if (Object.keys(user).length === 0) {
user = JSON.parse(sessionStorage.getItem("xm-user") || '{}');
}
// 如果用户信息存在,并且 token 存在,将其加入请求头的 Authorization 中
if (user != null && user.token) {
config.headers['token'] = `Bearer ${user.token}`; // 将 token 以 "Bearer " 前缀形式添加到请求头中
}
return config; // 返回修改后的请求配置
}, error => {
console.error('request error: ' + error); // 如果请求配置出错,打印错误信息
return Promise.reject(error); // 返回拒绝的 Promise,阻止请求发送
});
// response 拦截器
// 这个拦截器在每个响应返回后都会执行
request.interceptors.response.use(
response => {
let res = response.data; // 获取响应数据
console.log(response.data);
// 如果服务器返回的是字符串数据,尝试将其解析为 JSON 对象
if (typeof res === 'string') {
try {
res = JSON.parse(res); // 安全解析为 JSON 对象
} catch (error) {
console.error('JSON 解析错误:', error);
Message.error('响应数据格式错误'); // 添加错误提示
return Promise.reject(new Error('响应数据格式错误')); // 如果解析失败,返回拒绝的 Promise
}
}
// 如果服务器返回 401 错误,表示用户未授权或登录已过期
if (res.code === 401 || res.code === '401') { // 检查响应状态码是否为 401(未授权)
Message.error('登录已过期,请重新登录'); // 添加未授权提示
router.push('/login'); // 重定向到登录页面
}
return res; // 返回处理后的响应数据
},
error => {
// 检查 error.response 是否存在
if (error.response) {
const status = error.response.status;
if (status === 401) {
Message.error('登录已过期,请重新登录'); // 添加未授权提示
router.push('/login'); // 如果是 401 错误,跳转到登录页面
} else if (status === 403) {
Message.error('权限不足,访问被拒绝'); // 添加 403 错误提示
} else if (status === 404) {
Message.error('请求资源未找到'); // 添加 404 错误提示
} else {
// 处理其他状态码错误,比如 500 等
Message.error(`请求错误,状态码: ${status}`); // 添加其他错误提示
console.error(`HTTP 错误: ${status}`);
}
} else {
// 处理网络错误或其他无法预料的错误
Message.error('网络错误或未知错误,请稍后重试'); // 添加网络错误提示
console.error('网络错误或未知错误:', error);
}
return Promise.reject(error); // 返回拒绝的 Promise
}
);
export default request; // 导出 axios 实例
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
关键点说明
创建 Axios 实例:
- 通过
axios.create()
创建一个具有基础 URL 和超时时间设置的实例,以便所有的请求都遵循相同的配置。
- 通过
请求拦截器 (
request.interceptors.request.use
):- 在请求发送前统一设置请求头的
Content-Type
为application/json
。 - 从
localStorage
或sessionStorage
中读取用户信息。如果用户已登录,并且有token
,则将token
添加到请求头的Authorization
中。
- 在请求发送前统一设置请求头的
响应拦截器 (
request.interceptors.response.use
):- 在接收到响应后首先检查返回的数据是否为字符串,如果是字符串则尝试将其解析为 JSON。
- 处理可能的
401
未授权错误,并提示用户重新登录。 - 处理其他常见的 HTTP 状态码错误,如
403
、404
和服务器错误(如500
)。 - 如果发生网络错误或未知错误,则给出相应的提示,并将错误信息打印到控制台。
导出 Axios 实例:
- 最后,将配置好的
request
实例导出,以便在应用的其他部分使用。
- 最后,将配置好的
# 3. 总结与补充说明
Vuex 与存储的结合:通过结合
Vuex
和localStorage
或sessionStorage
,我们可以确保应用中的用户信息在页面刷新后不会丢失,并且能够在不同的组件之间共享这些信息。双向同步:通过在
mutations
中处理数据的同时更新存储,我们确保了每次数据变动后,存储中的数据都能及时同步,从而保持数据一致性。安全性考虑:在实际生产环境中,敏感信息(如
token
)不建议直接存储在localStorage
或sessionStorage
中,建议通过更安全的方式(如HttpOnly
的Cookie
)来管理这些数据。