yeoman/configstrore 源码解析

2023年 9月 29日 41.3k 0

前言

yeoman/configstore 是 yeoman 中用于配置存储的一个工具,非常实用和强大。核心思想就是通过既定文件命名规则,在当前系统中创建文件,用于存储用户的配置数据。再通过文件读写,实现配置的修改和保存。

使用

  • 新建一个 Node 项目,安装依赖。

    npm install configstore
    
  • 设置 package.json 中的 typemodule

  • 新建 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 的做法就是一个很好的思路,值得借鉴。

    相关文章

    服务器端口转发,带你了解服务器端口转发
    服务器开放端口,服务器开放端口的步骤
    产品推荐:7月受欢迎AI容器镜像来了,有Qwen系列大模型镜像
    如何使用 WinGet 下载 Microsoft Store 应用
    百度搜索:蓝易云 – 熟悉ubuntu apt-get命令详解
    百度搜索:蓝易云 – 域名解析成功但ping不通解决方案

    发布评论