用node,手写code实现持久化数据
开场
在node学习的过程中,开发项目需要用到持久化数据,来保存一些客户端的访问信息。一般对于持久化数据,我们第一时间想到的是用数据库,SQL或者Sqlite等等。但实际情况是我们只需要练习一个小功能,比如session的实现,这时引入持久化的数据存储的需要的,但引入数据库却是麻烦的。
能不能自己写一个数据的增删改查的功能,而不使用数据库呢?答案是可以的。主要的API是node的fs。
我们开始
数据的查询与存储
第一个需求
假设一个需求,对用户的信息进行增删改查,用户的信息有name,password
基本思路:我们将数据以json格式方式放在项目文件夹中,查询方式用node环境支持的
require
语法
首先我们新建一个文件来储存用户信息
//user.json
{
"userInfo": [
{
"name": "zenos",
"password": "123"
}
]
}
现在json文件中只有一条信息然后对该用户信息进行查询。在项目目录中新建一个文件:model.js
// model.js
const fs = require("fs");
const path = require("path");
const getUserInfo = (userParam) => {
const userData = require('./user.json');
for (let index = 0; index < userData.userInfo.length; index++) {
if (userData.userInfo[index].name === userParam.name) {
return userData.userInfo[index];
}
}
return null;
};
定义了一个getUserInfo
函数,接收一个参数。主要功能是按照提供的用户名称来查询用户信息。如果查到了就返回信息,如果没有查到就返回null。
这样就实现了基本的用户信息查询,如果想根据用户的其他信息来查询,或者查询所有用户(select * from user),都是可以的
下面实现对用户信息修改之后,进行储存
// model.js
//...
const setUserInfo = (userParam) => {
const userData = require('./user.json');
for (let index = 0; index < userData.userInfo.length; index++) {
if (userData.userInfo[index].name === userParam.name) {
userData.userInfo[index].password = userParam.password;
break;
}
}
const path = path.resolve(__dirname, "./user.json")
fs.writeFileSync(path, JSON.stringify(userData), { encoding: "utf-8" });
};
定义了一个函数setUserInfo,接收用户信息作为参数。主要功能是对某个用户的密码进行修改,然后对修改后的信息进行持久化存储。
持久化的功能也基本实现了
测试
// 测试 getUserInfo 函数
console.log("测试 getUserInfo 函数");
// 测试一个存在的用户
let user1 = getUserInfo({name: "zenos"});
console.log(user1); // 应该输出 {name: "zenos", password: "123"}
// 测试一个不存在的用户
let user2 = getUserInfo({name: "alice"});
console.log(user2); // 应该输出 null
// 测试 setUserInfo 函数
console.log("测试 setUserInfo 函数");
// 修改一个存在的用户的密码
setUserInfo({name: "zenos", password: "456"});
let user3 = getUserInfo({name: "zenos"});
console.log(user3); // 应该输出 {name: "zenos", password: "456"}
// 修改一个不存在的用户的密码
setUserInfo({name: "bob", password: "789"});
let user4 = getUserInfo({name: "bob"});
console.log(user4); // 应该输出 null
该测试可以直接运行
第二个需求
假设第二个需求:我们需要对客户端的session信息进行存储。
session的基本逻辑是,客户端的cookie中提供session key,然后拿这个key去查找对应的session。因为一个session可以存放很多信息,不止用户信息(也可能是暂时的聊天信息,或者是页面的访问信息等等),基于此我们还需要一个session name。所以查找一个session要两个信息:session key,session name。session的第二个逻辑是,具有时效性。也就是session会过期。基于此,经常用session的过期与否来判断是否让用户重新登录。做法是,每次用户登录,或者是访问其他的API,我们都会更新储存的session的过期时间。如果没有查到储存的session,就新增session(用户第一次登录的时候,会出现这种情况)
OK,了解完了session的逻辑之后,就可以实现session信息的🍵询和存储了
首先在相同目录中新建一个文件session.json,来储存session的信息
//session.json
{
"Session": {
"key1": {
"name1": {
"key": "key1",
"name": "name1",
"value": { "name": "name" },
"createDate": 1683102632209,
"expires": 1683106232209
},
"name2": {
"key": "key1",
"name": "name2",
"value": {},
"createDate": 1682649621033,
"expires": 1682653221033
}
},
"key2": {
"name1": {
"key": "key2",
"name": "name1",
"value": "value1",
"createDate": 0,
"expires": 0
}
}
}
}
这里说明下数据的存储格式。最原始的存储格式是单纯的数组,查询的时候就需要遍历每一条数据了。但这样做的效率比较低。我们可以搭建一个Map格式的数据结构,然后将常用的查询value
做为Map的key
,就像上面的session key的value来作为Map的key。
这样处理了之后,查询就很方便了。
下面来看查询的代码(依旧是在model.js添加代码):
// model.js
//...
const getDataTable = (path) => {
return JSON.parse(fs.readFileSync(path, { encoding: "utf-8" }));
};
const setDataTable = (path, data) => {
fs.writeFileSync(path, JSON.stringify(data), { encoding: "utf-8" });
};
const getSession = (key, name) => {
const sessionData = getDataTable(path.resolve(__dirname, "./session.json")).Session;
return (sessionData[key] && sessionData[key][name]) || null;
};
定义了一个函数getSession,接受session key和session name作为参数。功能是根据session key和session name查询对应的session对象,如果查到了就返回该对象,如果没有查到,就返回null。除此之外,还做了些代码的优化:
- 将数据的读取和数据的存储提取成一个单独的函数,只需要传入必要的参数,就可以完成操作。将两个需求之间的重复代码做了简化
- 数据的读取也用了fs的API来处理,这样可以不用将数据的存储格式局限于json文件格式,也可以有其他的文件格式
下面来看session的添加和更新代码
//model.js
//...
// 定义一个函数,用于更新session数据
const setSession = (key, name, data) => {
// 获取session.json文件的路径
const sessionPath = path.resolve(__dirname, "./session.json");
// 读取session.json文件中的Session对象
const sessionData = getDataTable(sessionPath).Session;
// 调用getSession函数,根据key和name获取对应的session对象
const session = getSession(key, name);
if (session) {
// 如果存在该session对象,就更新它的value,createDate和expires属性
sessionData[key][name] = {
...sessionData[key][name],
value: data,
createDate: Date.now(),
expires: Date.now() + 3600 * 1000,
};
} else {
// 如果不存在该session对象,就创建一个新的session对象
if (sessionData[key]) {
// 如果存在该key对应的对象,就在它下面添加一个name属性
sessionData[key][name] = {
key,
name,
value: data,
createDate: Date.now(),
expires: Date.now() + 3600 * 1000,
};
} else {
// 如果不存在该key对应的对象,就创建一个新的对象,并添加一个name属性
sessionData[key] = {
[name]: {
key,
name,
value: data,
createDate: Date.now(),
expires: Date.now() + 3600 * 1000,
},
};
}
}
// 把更新后的Session对象写入到session.json文件中
setDataTable(sessionPath, { Session: sessionData });
};
定义了一个setSession的函数,接收三个参数,主要功能是更新储存中的session,如果没有找到要更新的session,就创建新的session并储存。
储存session的功能也做好了,是不是觉得很简单呢
测试
// 测试 getSession 函数
console.log("测试 getSession 函数");
// 测试一个存在的session对象
let session1 = getSession("key1", "name1");
console.log(session1); // 应该输出 {key: "key1", name: "name1", value: {name: "name"}, createDate: 1683102632209, expires: 1683106232209}
// 测试一个不存在的session对象
let session2 = getSession("key3", "name3");
console.log(session2); // 应该输出 null
// 测试 setSession 函数
console.log("测试 setSession 函数");
// 设置一个新的session对象
setSession("key3", "name3", {});
let session3 = getSession("key3", "name3");
console.log(session3); // 应该输出 {key: "key3", name: "name3", value: {}, createDate: ..., expires: ...}
// 设置一个已存在的session对象
setSession("key1", "name1", "value4");
let session4 = getSession("key1", "name1");
console.log(session4); // 应该输出 {key: "key1", name: "name1", value: "value4", createDate: ..., expires: ...}
优化代码
上面我们完成了两个需求的代码编写,其中有很多重复的相似的逻辑,我们可以将其做些优化,并且让它的调用更像一个封装的数据库API。新建一个文件Table.js
//Table.js
const fs = require("fs");
// 定义一个 Table 类
class Table {
// 构造函数接受一个参数,表示 JSON 文件的路径
constructor(path) {
this.path = path;
}
// _getData 方法用于从 JSON 文件中读取数据
_getData() {
// 使用 fs.readFileSync 方法同步读取文件内容,然后将文件内容(JSON 格式)解析为 JavaScript 对象
return JSON.parse(fs.readFileSync(this.path, { encoding: "utf-8" }));
}
// _setData 方法用于将数据写入 JSON 文件
_setData(data) {
// 将数据转换为 JSON 格式,然后使用 fs.writeFileSync 方法同步写入文件
fs.writeFileSync(this.path, JSON.stringify(data), { encoding: "utf-8" });
}
}
module.exports = Table;
在这个 Table 类中,_getData 方法用于读取 JSON 文件中的数据,setData 方法用于将数据写入 JSON 文件。这两个方法都被标记为“私有”(在 JavaScript 中,约定以“”开头的方法和属性是私有的,但实际上它们仍然可以被外部访问和修改),这意味着它们主要用于 Table 类的内部操作,而不是被外部直接调用
然后再新建一个文件UserTable.js
//UserTable.js
const path = require("path");
const Table = require("./Table.js");
class UserTable extends Table {
constructor(userParam) {
super(path.resolve(__dirname, "./user.json"));
this.userParam = userParam;
}
get userData() {
if (!this._userData) this._userData = this._getData();
return this._userData;
}
setUserParam = (userParam) => {
this.userParam = userParam;
};
getUserInfo = () => {
const userInfo = this.userData.userInfo;
for (let index = 0; index < userInfo.length; index++) {
if (userInfo[index].name === this.userParam.name) {
return userInfo[index];
}
}
return null;
};
setUserInfo = () => {
const userInfo = this.userData.userInfo;
for (let index = 0; index < userInfo.length; index++) {
if (userInfo[index].name === this.userParam.name) {
userInfo[index].password = this.userParam.password;
break;
}
}
this._setData(this.userData);
};
}
定义了一个 UserTable 类,它继承自 Table 类,用于操作一个包含用户信息的 JSON 文件。这个 UserTable 类中,getUserInfo 方法用于获取指定用户的信息,setUserInfo 方法用于设置指定用户的信息。userData 是一个访问器属性,用于获取 JSON 文件中的数据。如果还没有读取过数据,它将调用父类的 _getData 方法来读取数据
再新建一个文件SessionTable.js
//SessionTable.js
const path = require("path");
const Table = require("./Table.js");
class SessionTable extends Table {
constructor(key, name) {
super(path.resolve(__dirname, "./session.json"));
this.key = key;
this.name = name;
}
get sessionData() {
if (!this._sessionData) this._sessionData = this._getData();
return this._sessionData.Session;
}
getSession = () => {
const key = this.key,
name = this.name,
sessionData = this.sessionData;
return (sessionData[key] && sessionData[key][name]) || null;
};
setSession = (data) => {
const key = this.key,
name = this.name,
sessionData = this.sessionData;
const session = this.getSession();
if (session) {
sessionData[key][name] = {
...sessionData[key][name],
value: data,
createDate: Date.now(),
expires: Date.now() + 3600 * 1000,
};
} else {
if (sessionData[key]) {
sessionData[key][name] = {
key,
name,
value: data,
createDate: Date.now(),
expires: Date.now() + 3600 * 1000,
};
} else {
sessionData[key] = {
[name]: {
key,
name,
value: data,
createDate: Date.now(),
expires: Date.now() + 3600 * 1000,
},
};
}
}
this._setData(sessionData);
};
_setData = (data) => {
const sessionData = { Session: data };
super._setData(sessionData);
};
}
在 SessionTable 类中,getSession 方法用于获取指定的会话信息,如果不存在对应的信息则返回 null。
setSession 方法用于设置指定的会话信息。它首先尝试获取指定的会话信息,如果信息已存在,则更新它。如果不存在,则创建一个新的会话信息。在设置会话信息时,它还会设置会话的创建时间和过期时间。最后,它将更新后的数据写入 JSON 文件。
_setData 方法重写了父类的同名方法。在写入数据时,它将数据放入一个名为 Session 的属性下,以匹配 JSON 文件的结构。
这个 SessionTable 类就是一个会话管理器,它提供了获取和设置会话信息的方法,以及读取和写入会话数据的方法,使得对会话数据的操作变得非常简单
测试
// 引入 UserTable 类
const UserTable = require('./UserTable.js');
// 创建一个 UserTable 对象
let userTable = new UserTable({name: 'zenos'});
// 测试 getUserInfo 方法,应输出:{ name: 'zenos', password: '123' }
console.log(userTable.getUserInfo());
// 更新用户的密码
userTable.setUserParam({name: 'zenos', password: '1234'});
userTable.setUserInfo();
// 再次测试 getUserInfo 方法,应输出:{ name: 'zenos', password: '1234' }
console.log(userTable.getUserInfo());
// 引入 SessionTable 类
const SessionTable = require('./SessionTable.js');
// 创建一个 SessionTable 对象
let sessionTable = new SessionTable('key1', 'name1');
// 测试 getSession 方法,应输出:{
// key: 'key1',
// name: 'name1',
// value: { name: 'name' },
// createDate: 1683102632209,
// expires: 1683106232209
// }
console.log(sessionTable.getSession());
// 更新会话的值
sessionTable.setSession({newName: 'newName'});
// 再次测试 getSession 方法,应输出:{
// key: 'key1',
// name: 'name1',
// value: { newName: 'newName' },
// createDate: <当前时间的时间戳>,
// expires: <当前时间的时间戳 + 3600 * 1000>
// }
console.log(sessionTable.getSession());
总结
这篇文章中,我们在node环境中,用fs的API来做简单的文件操作,实现了数据的持久化,并且实现数据的查询和存储。之后又对代码进行了面向对象的代码优化,让数据的持久化操作变得更像封装之后的数据库API。
转载自:https://juejin.cn/post/7228886005070807101