likes
comments
collection
share

可持久化的本地存储方案的简单实现 - configstore源码解析学习

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

本文参加了由公众号@若川视野 发起的每周源码共读活动,      点击了解详情一起参与。

这是源码共读的第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

源码

github.com/yeoman/conf…

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

getset 中会对 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 = {};
	}
  1. 如果访问了all属性 getter操作符,那么就会去读取文件返回整个对象,再通过dotProp 对整个属性进行读取,返回给用户
  2. 如果是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;
      }
    },
  });