yeoman/configstrore 源码解析

前言

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