likes
comments
collection
share

用node,手写code实现持久化数据

作者站长头像
站长
· 阅读数 72

开场

在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。除此之外,还做了些代码的优化:

  1. 将数据的读取和数据的存储提取成一个单独的函数,只需要传入必要的参数,就可以完成操作。将两个需求之间的重复代码做了简化
  2. 数据的读取也用了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
评论
请登录