上节最后,我提出一个疑问:
我们这次使用文件或localStorage持久化了数据,但上面的代码有什么问题?还能优化吗?
问题
其实早在1.3节,有段高亮的内容,不知道大家有没有注意到:
注意:增加、修改、删除都是在它的基础上,修改之后,再覆盖原文件。
仔细看user.service.ts代码:
async addUser(user: Omit<User, "id">) {
const users = await this.getAll();
const id = users.length + 1;
const newUser = {
...user,
id,
};
users.push(newUser);
await this.saveToFile(users);
return newUser;
}
async removeUser(id: number) {
const users = await this.getAll();
const newUsers = users.filter((user) => user.id !== id);
await this.saveToFile(newUsers);
}
async updateUser(id: number, user: Omit<User, "id">) {
const users = await this.getAll();
const oldUser = users.find((u) => u.id === id);
if (!oldUser) {
throw new Error(`user not found`);
}
Object.assign(oldUser, user);
await this.saveToFile(users);
}
无论增加、修改还是删除,都需要先读取一遍所有数据,然后在它的基础上进行操作。
假设我们先增加一条数据,未等它结束,就更新另一条数据,会是什么结果呢?
在浏览器打开http://localhost:8000/user,F12里执行:
fetch("/user", {
method: "post",
headers: { "content-type": "application/json" },
body: JSON.stringify({ author: "王五", age: 15 }),
});
fetch("/user/1", {
method: "put",
headers: { "content-type": "application/json" },
body: JSON.stringify({ age: 10 }),
});
呃,你发现好像没什么问题。
从我们代码上看,要复现这个问题,时机很难把握。
所以我们在代码中添加以下:
userService.addUser({
author: "张三",
age: 20,
});
userService.updateUser(1, {
author: "李四",
age: 11,
});
刷新http://localhost:8000/user,我们预期的是会有个新用户张三,但事实只是李四年龄修改了:
所以,现有代码只维护了一个数据文件,多人同时操作时就有可能出现bug。
而且,当这个文件体量越来越大时,每次读取、处理的性能开销也会越来越大。
优化
针对这种情况,怎么办呢?
当然是分而治之。大了就往小的拆呗。
现在所有数据都在localStorage的users下面。我们需要有几个空间呢?
- 一条数据一个文件,比如id是1,就有一条users_1的数据。
- 一个维护自增长的id:users_id
- 存储当前用户的ids:users_ids
这样一来,根据id查找、修改都较之前要快,而且修改不会与其它操作冲突。比原来要慢的是获取全量数据,而在实际业务中,如果数据量大的话,一般都要使用分页。
修改后的代码如下:
class UserService {
async getAll(): Promise<User[]> {
const userIdsStr = localStorage.getItem("users_ids");
if (userIdsStr) {
const ids: string[] = JSON.parse(userIdsStr);
const users = await Promise.all(
ids.map((id) => this.getUserById(parseInt(id))),
);
return users.filter(Boolean) as User[];
}
return [];
}
async getUserById(id: number): Promise<User | null> {
const userStr = localStorage.getItem(`users_${id}`);
if (userStr) {
return JSON.parse(userStr) as User;
}
return null;
}
generateId(): number {
const idStr = localStorage.getItem("users_id");
if (idStr) {
const id = parseInt(idStr, 10) + 1;
localStorage.setItem("users_id", id.toString()); // 取一次就得改一次
return id;
} else {
localStorage.setItem("users_id", "1");
return 1;
}
}
async addUser(user: Omit<User, "id">) {
const id = this.generateId();
const newUser = {
...user,
id,
};
const userIdsStr = localStorage.getItem("users_ids");
const userIds = userIdsStr ? JSON.parse(userIdsStr) : [];
userIds.push(id);
localStorage.setItem(`users_${id}`, JSON.stringify(newUser));
localStorage.setItem("users_ids", JSON.stringify(userIds));
return newUser;
}
async removeUser(id: number) {
const userIdsStr = localStorage.getItem("users_ids");
if (!userIdsStr) {
return;
}
const userIds = JSON.parse(userIdsStr);
const index = userIds.indexOf(id);
if (index > -1) {
userIds.splice(index, 1);
localStorage.setItem("users_ids", JSON.stringify(userIds));
}
}
async updateUser(id: number, user: Omit<User, "id">) {
const oldUser = await this.getUserById(id);
if (!oldUser) {
return;
}
Object.assign(oldUser, user);
localStorage.setItem(`users_${id}`, JSON.stringify(oldUser));
}
}
看起来有些复杂,再封装2个函数:
function getData<T = string>(key: string) {
const str = localStorage.getItem(key);
if (str) {
return JSON.parse(str) as T;
}
return null;
}
function setData(key: string, val: unknown) {
localStorage.setItem(key, JSON.stringify(val));
}
这样代码能清减一点:
class UserService {
async getAll(): Promise<User[]> {
const ids = getData<string[]>("users_ids");
if (ids) {
const users = await Promise.all(
ids.map((id) => this.getUserById(parseInt(id))),
);
return users.filter(Boolean) as User[];
}
return [];
}
async getUserById(id: number): Promise<User | null> {
return getData<User>(`users_${id}`);
}
generateId(): number {
const idStr = getData("users_id");
if (idStr) {
const id = parseInt(idStr, 10) + 1;
setData("users_id", id); // 取一次就得改一次
return id;
} else {
setData("users_id", 1);
return 1;
}
}
async addUser(user: Omit<User, "id">) {
const id = this.generateId();
const newUser = {
...user,
id,
};
const userIds = getData<number[]>("users_ids") || [];
userIds.push(id);
setData(`users_${id}`, newUser);
setData("users_ids", userIds);
return newUser;
}
async removeUser(id: number) {
const userIds = getData<number[]>("users_ids");
if (!userIds) {
return;
}
const index = userIds.indexOf(id);
if (index > -1) {
userIds.splice(index, 1);
setData("users_ids", userIds);
}
}
async updateUser(id: number, user: Omit<User, "id">) {
const oldUser = await this.getUserById(id);
if (!oldUser) {
return;
}
Object.assign(oldUser, user);
setData(`users_${id}`, oldUser);
}
}
验证
再用这段代码验证下:
userService.addUser({
author: "张三",
age: 20,
});
userService.updateUser(1, {
author: "李四",
age: 11,
});
可以看到张三添加成功了,李四的年龄也修改成功了。
作业
再思考下,上面的代码还有什么问题?还有什么能优化的吗?