npm-modules

configstore

轻松加载和持久化配置,无需考虑位置和方式

目标

之前有写过一个 @deepjs/storage 用于封装 localStorage, sessionStorage, 大体结构和这个有点类似。

两者定位不同

下面我们学习下流行库是怎么设计的,先看下 npm 上的大体情况

作用

轻松加载和保留配置,而无需考虑存储在哪里以及是怎么配置的

配置存储在位于环境变量 $XDG_CONFIG_HOME 如无,默认取值为 ~/.config

# configstore 路径
~/.config/configstore/some-id.json

用法

还是要实操下,体会才更好,涉及到文件操作就无法右键 RUN CODE 来运行了

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

源码分析

configsotre 的源码也不多,不过引用了几个三方包,可以先了解下做什么的,然后再来看源码分析

整体结构

export default class Configstore {
  constructor(id, defaults, options = {}) {}
  get all() {}
  set all(value) {}
  get size() {}
  get(key) {}
  set(key, value) {}
  has(key) {}
  delete(key) {}
  clear() {}
  get path() {}
}

能看明白干什么的,也得写出来到底是干什么的。

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';

/**
 * 1. configDirectory: 存储当前操作系统的临时文件路径。Linux 通过 xdgConfig 包获取。
 *    其他操作系统通过 os.tmpdir() 获取, 后面再拼接一个 32 位的字符串。
 *        eg: 'C:\\Users\\userName\\AppData\\Local\\Tempb4de2a49c8ffa3fbee04446f045483b2'
 * 2. permissionError: 文件权限提示语
 * 3. mkdirOptions: 文件夹创建配置, mode 参数代表的是 linux 系统的目录权限, recursive 表示递归创建
 *        mode: 0o0600 只有拥有者有读写权限
 *        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};

export default class Configstore {
  constructor(id, defaults, options = {}) {
    const pathPrefix = options.globalConfigPath ?
      path.join(id, 'config.json') :
      path.join('configstore', `${id}.json`);

    // 保存配置路径 configPath > configDirectory + pathPrefix
    this._path = options.configPath || path.join(configDirectory, pathPrefix);

    if (defaults) {
      this.all = {
        ...defaults,
        ...this.all
      };
    }
  }

  // 存取有异常的场景,就要做兼容处理
  get all() {
    try {
      // 同步以 utf-8 格式读取配置文件
      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;
  }
}

xdg-basedir, 源码如下

Get XDG Base Directory paths

import os from 'os';
import path from 'path';

const homeDirectory = os.homedir();
const {env} = process;

export const xdgData = env.XDG_DATA_HOME ||
  (homeDirectory ? path.join(homeDirectory, '.local', 'share') : undefined);

export const xdgConfig = env.XDG_CONFIG_HOME ||
  (homeDirectory ? path.join(homeDirectory, '.config') : undefined);

export const xdgState = env.XDG_STATE_HOME ||
  (homeDirectory ? path.join(homeDirectory, '.local', 'state') : undefined);

export const xdgCache = env.XDG_CACHE_HOME || (homeDirectory ? path.join(homeDirectory, '.cache') : undefined);

export const xdgRuntime = env.XDG_RUNTIME_DIR || undefined;

export const xdgDataDirectories = (env.XDG_DATA_DIRS || '/usr/local/share/:/usr/share/').split(':');

if (xdgData) {
  xdgDataDirectories.unshift(xdgData);
}

export const xdgConfigDirectories = (env.XDG_CONFIG_DIRS || '/etc/xdg').split(':');

if (xdgConfig) {
  xdgConfigDirectories.unshift(xdgConfig);
}

graceful-fs

graceful-fs 作为 fs 模块的替代品,旨在规范跨不同平台和环境的行为,并使文件系统访问对错误更具弹性。 fs-extrafs 的一个扩展,提供了非常多的便利API,并且继承了fs所有方法并提供 Promise 风格的 API。fs-extra 底层依赖了 graceful-fs 用于防止EMFILE错误

对比

dot-prop

unique-string

生成一个 32 位长度的随机字符串。

import uniqueString from 'unique-string';

uniqueString();
//=> 'b4de2a49c8ffa3fbee04446f045483b2'

// unique-string 源码如下:
// 其底层,引入的是 [`crypto-random-string`](https://github.com/sindresorhus/crypto-random-string#readme)
import cryptoRandomString from 'crypto-random-string';

export default function uniqueString() {
  return cryptoRandomString({length: 32});
}

// 再向里了解
// in 'crypto-random-string',默认使用 hex 方式
const generateRandomBytes = (byteLength, type, length) => crypto.randomBytes(byteLength).toString(type).slice(0, length);
const cryptoRandomString = ({length}) => generateRandomBytes(Math.ceil(length * 0.5), 'hex', length);
// 调用内置模块 [`crypto.randomBytes`](http://nodejs.cn/api/crypto.html#cryptorandombytessize-callback)
// crypto 源码 [`lib/crypto.js`](https://github.com/nodejs/node/blob/v16.13.2/lib/crypto.js)

返回一个 32 个字符的唯一字符串。匹配 MD5 的长度,这对于非加密目的来说足够独特

write-file-atomic

This is an extension for node’s fs.writeFile that makes its operation atomic and allows you set ownership (uid/gid of the file).

基于 fs.writeFile 的扩展模块,用于文件写入,并支持设置文件的 uid/gid

uid 代表用户对文件的操作权限,gid 代表用户所属组对文件的操作权限。

var writeFileAtomic = require('write-file-atomic');

// 同步版本
writeFileAtomic.sync('message.txt', 'Hello Node', {chown:{uid:100,gid:50}});

知识点

扩展知识

值得注意的一点是, 0O 前缀的可读性太差了(0和大写的O长的太像了, 很难区分), 我在esdiscuss上提出了这个问题, 希望能禁用掉大写的0O前缀, 不过TC39目前的决定还是认为一致性应该大于可读性(一致性指的是要和0X以及0B等一致). 我认为这个决定是值得商榷的, 我推荐你永远不要使用大写的0O.

参考:

在开发 js 库的时候,涉及文件操作的部分需要考虑到不同系统及平台等环境的情况。configstore 的做法就是一个很好的思路,值得借鉴。

关于权限

十位权限表示 T rwx rwx rwx, 权限用 8 进制数字表示。

rwx 分别表示owner,group,other的(读,写,执行)权限,每个数可以转换为三位二进制数,分别表示rwx(读,写,执行)权限,为1表示有权限,0无权限

-rw------- (600)    只有拥有者有读写权限。
-rw-r--r-- (644)    只有拥有者有读写权限;而属组用户和其他用户只有读权限。
-rwx------ (700)    只有拥有者有读、写、执行权限。
-rwxr-xr-x (755)    拥有者有读、写、执行权限;而属组用户和其他用户只有读、执行权限。
-rwx--x--x (711)    拥有者有读、写、执行权限;而属组用户和其他用户只有执行权限。
-rw-rw-rw- (666)    所有用户都有文件读、写权限。
-rwxrwxrwx (777)    所有用户都有读、写、执行权限。

第一位表示文件的类型

d 代表的是目录(directroy)
- 代表的是文件(regular file)
s 代表的是套字文件(socket)
p 代表的管道文件(pipe)或命名管道文件(named pipe)
l 代表的是符号链接文件(symbolic link)
b 代表的是该文件是面向块的设备文件(block-oriented device file)
c 代表的是该文件是面向字符的设备文件(charcter-oriented device file)

总结

刚一看到这个包,感觉也没什么,不就是存个配置文件嘛,前些日子写的 @deepjs/cdn-cli 包也有存储配置啊

等这一通源码看下来,又收益匪浅了,知识点里列的都是加深熟悉的点,关于文件操作的部分,我之前也没考虑过跨平台系统的兼容支持,这个库就考虑的很周全。

而且通过扩展学习,也了解到了更多的知识。