可持久化的本地存储方案的简单实现 - configstore源码解析学习
本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
这是源码共读的第10期 | configstore 存储
前言
configstore 是一个轻量级的 本地持久化的存储实现,其本质就是对于本地JSON文件的键值对读写,我们无需考虑在何处以及如何去加载它
使用
npm install configstore
import fs from 'node:fs';
import Configstore from 'configstore';
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
// Create a Configstore instance.
const config = new Configstore(packageJson.name, {foo: 'bar'});
console.log(config.get('foo'));
//=> 'bar'
config.set('awesome', true);
console.log(config.get('awesome'));
//=> true
// Use dot-notation to access nested properties.
config.set('bar.baz', true);
console.log(config.get('bar'));
//=> {baz: true}
config.delete('awesome');
console.log(config.get('awesome'));
//=> undefined
源码
index.js 整体源码100行出头
初始化和常量
const configDirectory = xdgConfig || path.join(os.tmpdir(), uniqueString());
const permissionError = 'You don\'t have access to this file.';
const mkdirOptions = {mode: 0o0700, recursive: true};
const writeFileOptions = {mode: 0o0600};
xdgConfig 通过 xdg-basedir 获取用户目录下的配置文件路径, path.join(os.tmpdir(), uniqueString())
获取的是当前用户目录 + 随机字符串的路径
constructor(id, defaults, options = {}) {
const pathPrefix = options.globalConfigPath ?
path.join(id, 'config.json') :
path.join('configstore', `${id}.json`);
// 如果没有传入options则取 xdgConfig + pathPrefix的拼接目录
// 比如我本地的在这里 C:\Users\Harexs\.config\configstore
this._path = options.configPath || path.join(configDirectory, pathPrefix);
// 默认对象 解构覆盖值
if (defaults) {
this.all = {
...defaults,
...this.all
};
}
}
all - get/set
在get 和 set 中会对 all
进行访问,所以这里先看 它对应的 getter/setter
, 它也是核心的读写属性
try {
return JSON.parse(fs.readFileSync(this._path, 'utf8'));
} catch (error) {
// Create directory if it doesn't exist
if (error.code === 'ENOENT') {
return {};
}
// Improve the message of permission errors
if (error.code === 'EACCES') {
error.message = `${error.message}\n${permissionError}\n`;
}
// Empty the file if it encounters invalid JSON
if (error.name === 'SyntaxError') {
writeFileAtomic.sync(this._path, '', writeFileOptions);
return {};
}
throw error;
}
读取时,使用graceful-fs
对 本地文件进行访问,将其取出后JSON化, 如果不存在文件或报错根据情况进行处理
try {
// Make sure the folder exists as it could have been deleted in the meantime
fs.mkdirSync(path.dirname(this._path), mkdirOptions);
writeFileAtomic.sync(this._path, JSON.stringify(value, undefined, '\t'), writeFileOptions);
} catch (error) {
// Improve the message of permission errors
if (error.code === 'EACCES') {
error.message = `${error.message}\n${permissionError}\n`;
}
throw error;
}
触发setter
时,创建对应的文件和目录,将对象重新进行覆写
增删改查
dotProp 是一个对象属性操作的包,它允许我们可以用点的形式去操作和访问对象,例如 xxx.xxx
// 从返回的key获取值
get(key) {
return dotProp.get(this.all, key);
}
//先得到完整的对象,再根据传递参数的数量进行不同的处理
set(key, value) {
const config = this.all;
if (arguments.length === 1) {
for (const k of Object.keys(key)) {
dotProp.set(config, k, key[k]);
}
} else {
dotProp.set(config, key, value);
}
this.all = config;
}
has(key) {
return dotProp.has(this.all, key);
}
delete(key) {
const config = this.all;
dotProp.delete(config, key);
this.all = config;
}
clear() {
this.all = {};
}
- 如果访问了
all
属性 getter操作符,那么就会去读取文件返回整个对象,再通过dotProp 对整个属性进行读取,返回给用户 - 如果是setter 操作符, 那么就通过dotProp 去修改属性的值,再将 all整个对象进行覆盖
总结
其最核心就是对于all
的属性访问操作符,通过dotProp 对 属性的夹子(get/set)进行操作, 而其他增删改查则是最基本的对象属性操作,我们可以依靠这个思路 学习写一个不依赖包的轮子, 并尝试使用Proxy来处理, 和使用split 分割的思路来支持xx.xx.xx
的操作方式:
harexs-store
//处理分割
export function splitSet(obj: Record<string, any>, key: string, val: any) {
let allKey = key.split(".");
for (let i = 0; i < allKey.length; i++) {
let key = allKey[i];
let keyVal = obj[key];
// 没到最后一项遍历就不存在则初始化这个对象
if (!isObject(keyVal)) obj[key] = {};
if (i === allKey.length - 1) obj[key] = val;
obj = obj[key];
}
}
//通过对象代理来对 对象的操作
let allProxy = new Proxy(_all, {
get(target: Record<string, any>, key: string) {
try {
let obj = JSON.parse(fs.readFileSync(Path, "utf8"));
//分割判断返回
return splitGet(obj, key);
} catch (err: any) {
//文件不存在
if (err.code === "ENOENT") {
return {};
}
if (err.name === "SyntaxError") {
fs.writeFileSync(Path, "", "utf-8");
return {};
}
err.message = getFileErrorMessage;
throw err;
}
},
set(target: Record<string, any>, key: string, val: any, receiver: any) {
try {
//如果是目录不存在 创建对应目录
if (!fs.existsSync(path.dirname(Path))) {
fs.mkdirSync(path.dirname(Path), { mode: 0o0700, recursive: true });
}
//如果文件不存在 创建对应文件并写入内容
if (!fs.existsSync(Path)) {
fs.writeFileSync(Path, "{}", "utf-8");
}
//读出整个对象
let obj = JSON.parse(fs.readFileSync(Path, "utf8"));
//分割处理
splitSet(obj, key, val);
//重新存储整个对象
fs.writeFileSync(Path, JSON.stringify(obj, null, "\t"));
return true;
} catch (err: any) {
err.message = getFileErrorMessage;
throw err;
}
},
});
转载自:https://juejin.cn/post/7252172628470644791