前言
- 本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
- 这是源码共读的第10期,链接:第10期 | configstore 存储
1 学习目标
configstore是如何轻松地加载和写入配置,而不必考虑在哪里和如何进行- 学习功能封装以及暴露接口
源码地址:configstor
2 源码分析
源码只有109行:configstore/index.js
2.1 加载依赖
import path from 'path';
import os from 'os';
import fs from 'graceful-fs';
import {xdgConfig} from 'xdg-basedir';
import writeFileAtomic from 'write-file-atomic';
import dotProp from 'dot-prop';
import uniqueString from 'unique-string';
- graceful-fs npm是node模块fs的增强版
- xdg-basedir npm用于获取XDG Base Directory路径
- write-file-atomic npm这是node的fs.writeFile的一个扩展,它使其操作成为原子性的,并允许您设置所有权(文件的uid/gid)
- dot-prop npm:使用点路径从嵌套对象获取,设置或删除属性
- unique-string npm: 返回一个 32 个字符的唯一字符串。匹配 MD5 的长度,这对于非加密目的来说足够了。
2.2 初始化工作
// 配置路径
const configDirectory = xdgConfig || path.join(os.tmpdir(), uniqueString());
const permissionError = 'You don't have access to this file.';
// mode:创建文件/文件夹给的权限,recursive表示是否递归,指权限是否给子文件和文件夹同步
const mkdirOptions = {mode: 0o0700, recursive: true};
const writeFileOptions = {mode: 0o0600};
2.3 主体 Configstore 类
export default class Configstore {
constructor(id, defaults, options = {}) {
const pathPrefix = options.globalConfigPath ? path.join(id, "config.json") : path.join("configstore", `${id}.json`);
this._path = options.configPath || path.join(configDirectory, pathPrefix);
if (defaults) {
this.all = {
...defaults,
...this.all,
};
}
}
get all() {
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;
}
}
set all(value) {
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;
}
}
get size() {
return Object.keys(this.all || {}).length;
}
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 = {};
}
get path() {
return this._path;
}
}
实现步骤:
- 构造函数根据传入的id,设置文件存储路径_path;
- 如果有默认的存取内容defaults,就去创建对应文件,写入默认内容;不存在默认内容时,文件暂时不会创建
- 对应的实例方法set、get、delete、has内部都是调用dot-prop这个包的相关方法,但在set时可以传入单个对象参数,设置多个key/value值
Configstore类的核心主要是围绕实例属性all的处理
- 设置了all的存值函数和取值函数
- get all取值函数主要做的是读取对应_path的内容,如果路径不存在或者文件内容不符合json格式,抛出对应的错误
- set all存值函数主要是创建文件,写入内容,如果无写入权限,抛出对应的错误
总结
Configstore这个库代码不多,但是其封装文件操作实现,并暴露操作文件内容的接口(函数)的思想还是非常经典的;和目前比较火的hooks相比其实有异曲同工之妙,hooks使用闭包和作用域来屏蔽了实现细节,而返回操作对象(或者变量)和方法(也就是接口)给外部使用;两种都是让外部不需要关系实现细节,只需要功能符合用户需要直接调用即可。