前言
yeoman/configstore 是 yeoman 中用于配置存储的一个工具,非常实用和强大。核心思想就是通过既定文件命名规则,在当前系统中创建文件,用于存储用户的配置数据。再通过文件读写,实现配置的修改和保存。
使用
新建一个 Node
项目,安装依赖。
npm install configstore
设置 package.json
中的 type
为 module
。
新建 test_configstore.js
并执行。test_configstore.js
内容如下:
import Configstore from "configstore";
import fs from "fs";
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"));
运行 test_configstore.js
文件即可。
依赖分析
先了解下 configstore
所依赖的 npm
包的主要作用。
- path
Node 内置模块,用于操作文件和目录。
- os
Node 内置模块,提供操作系统相关的方法和属性。
- graceful-fs
一个升级版的 fs 模块,兼容不同平台和环境。在文件读写这一块,优于原生 fs 模块。
- xdg-basedir
在 Linux 平台下,获取用户的配置文件路径。例如: '/usr/share' - 用于存放共享数据的文件目录等等。
- write-file-atomic
基于 fs.writeFile
的扩展模块,用于文件写入,并支持设置文件的 uid/gid。
uid 代表用户对文件的操作权限,gid 代表用户所属组对文件的操作权限。
- dotProp
支持多层级嵌套对象,通过 '.' 方式访问及操作对象属性。
- unique-string
随机生成一个 32 位长度字符串。
前置变量
/**
* 1. configDirectory: 存储当前操作系统的临时文件路径。Linux 通过 xdgConfig 包获取。
* 其他操作系统通过 os.tmpdir() 获取,后面再拼接一个 32 位的字符串。
* eg: 'C:\\Users\\userName\\AppData\\Local\\Tempb4de2a49c8ffa3fbee04446f045483b2'
* 2. permissionError: 文件权限提示语
* 3. mkdirOptions:文件夹创建配置,mode 参数代表的是 linux 系统的目录权限, recursive 表示递归创建
* mode: 0o0700:代表所有者具有读、写及可执行权限
* 4. writeFileOptions: 写文件配置。
*/
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 };
核心代码
/**
* 5. Configstore 类声明,使用 ES modules 语法进行导出
*/
export default class Configstore {
/**
* 6. 构造函数,用于创建 Configstore 实例
* @param {*} id 用于 configstore 存储文件命名
* @param {*} defaults 默认存储数据,比如一个对象
* @param {*} options 存储配置
*/
constructor(id, defaults, options = {}) {
/**
* 7. pathPrefix: 存储文件路径名称。
* 若用户设置了 globalConfigPath,则名称为: id/config.json。
* 否则为:configstore/id.json
*/
const pathPrefix = options.globalConfigPath
? path.join(id, "config.json")
: path.join("configstore", `${id}.json`);
/**
* 8. _path:存储文件在操作系统中的完整路径。
* options.configPath:用户自定义的完整路径。
* 或为 系统临时存储路径/存储文件路径。
*/
this._path = options.configPath || path.join(configDirectory, pathPrefix);
/**
* 9. _all: 存储当前 configstore 实例的存储数据,是一个对象格式。
*/
if (defaults) {
this.all = {
...defaults,
...this.all,
};
}
}
/**
* 10. 以 utf-8 编码格式读取存储对象的文件
*/
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;
}
}
/**
* 11. 存储数据写入,等同于覆盖写入
*/
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;
}
}
/**
* 12. 获取存储数据(JSON 对象)属性数量
*/
get size() {
return Object.keys(this.all || {}).length;
}
/**
* 13. 按 key 名称去获取存储数据(本质上就是对象属性访问,额外支持了嵌套对象的属性访问)
* @param {*} key
* @returns
*/
get(key) {
return dotProp.get(this.all, key);
}
/**
* 14. 按照 key-value 形式给数据对象属性赋值
* @param {*} key
* @param {*} value
*/
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;
}
/**
* 15. 判断数据中是否存在某个属性
* @param {*} key
* @returns
*/
has(key) {
return dotProp.has(this.all, key);
}
/**
* 16. 删除指定 key 的属性值
* @param {*} key
*/
delete(key) {
const config = this.all;
dotProp.delete(config, key);
this.all = config;
}
/**
* 17. 清空当前存储数据
*/
clear() {
this.all = {};
}
/**
* 18. 获取当前数据存储文件的完整路径
*/
get path() {
return this._path;
}
总结
在开发 js 库的时候,涉及文件操作的部分需要考虑到不同系统及平台等环境的情况。configstore 的做法就是一个很好的思路,值得借鉴。