Node.js util 模組解讀

Randal發表於2018-05-21

Node模組解讀第四篇util模組,之前已經介紹過vm模組Buffer模組Event模組

util模組最初的目的是為內部API提供一些工具支援,然而很多工具函式對於普通的開發者來說也十分有用,因此util模組將一些方法實現了對外暴露。本文主要探討以下三方面的工具函式:

  • 風格轉換
  • 除錯輸出
  • 廢棄API

風格轉換

callback轉換promise

針對傳入error-first回撥作為函式的最後一個引數的函式(比如fs.readFile('./filename', (err, data) => {})), util提供了promisify(original)方法用來將這種型別的函式轉換成返回promise的形式。

以fs.readFile為例

const fs = require('fs');

fs.readFile('./h.js', (err, data) => {
  if (err) {
    console.error(err);
    return;
  }
  console.log(data.toString());
})

// 使用util.promisify轉換後
const fs = require('fs');
const util = require('util');
const readFilePromise = util.promisify(fs.readFile);
readFilePromise('./h.js')
  .then((data) => {
    console.log(data.toString());
  }, (err) => {
    console.error(err);
  });
複製程式碼
具體實現

promisify執行完後返回的是一個新的函式,新的函式的執行結果是一個promise,新函式內部會呼叫original原有的方法並且會自動追加error-first型別的callback,根據original的執行結果判斷是resolve還是reject,簡易版本的程式碼如下:

function promisify(original) {
  function fn(...args) {
    const promise = createPromise();
    try {
      original.call(this, ...args, (err, ...values) => {
        if (err) {
          promiseReject(promise, err);
        } else {
          promiseResolve(promise, values[0]);
        }
      });
    } catch (err) {
      promiseReject(promise, err);
    }
    return promise;
  }
  return fn
}
複製程式碼

util模組還提供了promisify的自定義轉換方式(original函式上定義util.promisify.custom屬性),比如通過下面的方式可以實現禁用檔案讀取,util.promisify.custom必須定義在util.promisify呼叫之前

const fs = require('fs');
const util = require('util');
fs.readFile[util.promisify.custom] = (fileName) => {
  return Promise.reject('not allowed');
}
const readFilePromise = util.promisify(fs.readFile);
readFilePromise('./h.js')
  .then((data) => {
    console.log(data.toString());
  }, (err) => {
    console.error(err);    // not allowed
  });
複製程式碼

promise轉callback

util的callbackify方法與promisify剛好相反,callbackify用於把async(或者返回promise)的函式轉換成遵從error-first回撥風格的型別

const util = require('util');
const fn = () => {
  return 'fn executed'
};
function delay(second, fn) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(fn());
    }, second);
  });
}
delay(1000, fn)
.then((data) => {
  console.log(data); // fn executed
});

// 使用util.callbackify轉換後

const delayFn = util.callbackify(delay);
delayFn(1000, fn, (err, data) => {
  if (err) {
    console.error(err);
    return;
  }
  console.log(data);  // fn executed
});
複製程式碼

有一種情況需要關注,假如promise是reject(null || 0 || false)的話,那麼callback的err判斷為非,程式會繼續執行console.log(data),這其實是不正確的。因此callbackify對於這種情況做了特殊的處理(建立一個error,將原始的資訊放在error的reason屬性上,把error傳遞給最終的回撥函式)

function callbackifyOnRejected(reason, cb) {
  if (!reason) {
    const newReason = new ERR_FALSY_VALUE_REJECTION();
    newReason.reason = reason;
    reason = newReason;
    Error.captureStackTrace(reason, callbackifyOnRejected);
  }
  return cb(reason);
}
複製程式碼
具體實現

實現的邏輯是呼叫原始函式original通過then來呼叫callback方法

function callbackify(original) {
  function callbackified(...args) {
    const maybeCb = args.pop();
    const cb = (...args) => { Reflect.apply(maybeCb, this, args); };
    Reflect.apply(original, this, args)
      .then((ret) => process.nextTick(cb, null, ret),
            (rej) => process.nextTick(callbackifyOnRejected, rej, cb));
  }
  return callbackified;
}
複製程式碼

除錯輸出

util.debuglog(section)

debuglog方法用於根據NODE_DEBUG環境變數來選擇性的輸出debug資訊,例如下面的例子

//index.js 
const util = require('util');
const debuglog = util.debuglog('foo-bar');

debuglog('hello from foo [%d]', 123);

// 執行index檔案
node index.js // 沒有輸出
NODE_DEBUG=foo-bar node index.js //FOO-BAR 18470: hi there, it's foo-bar [2333]
複製程式碼

NODE_DEBUG如果希望輸出多個section可以用逗號做分隔,同時NODE_DEBUG也支援萬用字元形式(node版本需要10)

NODE_DEBUG=fs,net,tls // 多個section
NODE_DEBUG=foo*  // 萬用字元
複製程式碼

上面的debuglog函式執行的時候支援佔位符,其實底層使用的是util.format方法。

util.format

util.format用於佔位符替換,不同型別的資料採用不同的佔位符表示

%s 字串
%d 整數或浮點數
%i 整數
%f 浮點數
%j JSON
%o Object(包括不可列舉的屬性)
%O Object(不包括不可列舉的屬性)
%% 輸出%

對Object格式化輸出字串的時候用到的其實是util.inspect方法,%o與%O的區別僅在於呼叫inspect方法時傳入配置項有別

%o 傳入util.inspect的配置項是 { showHidden: true, showProxy: true }
複製程式碼

util.inspect

util.inspect用於對object做格式化字串操作,並提供個性化配置項

const util = require('util');
var child = {
  "name": "child",
  "age": 18,
  "friends": [
    {
      "name": "randal",
      "age" : 19,
      "friends": [
        {
          "name": "Kate",
          "age": 18
        }
      ]
    }
  ],
  "motto": "Now this is not the end. It is not even the beginning of the end. But it is, perhaps, the end of the beginning."
}
console.log(util.inspect(child, { compact: false, depth: null, breakLength: 80}));

複製程式碼

compact 用於各屬性獨佔一行顯示

depth 用於控制顯示層級,預設是2

breakLength用於一行文字的最大個數,超出換行

更詳細的引數及釋義參見官網api

廢棄API

使用util.deprecate方法可以針對廢棄的api在終端輸出廢棄的提示資訊

const util = require('util');
const oldFn = () => {
  console.log('old fn');
};
class oldClass {
  constructor() {
    console.log('old class');
  }
}
const fn1 = util.deprecate(oldFn, 'deprecated fn');
const fn2 = util.deprecate(oldClass, 'deprecated class');
fn1();
new fn2();

// 輸出
old fn
old class
(node:18001) DeprecationWarning: deprecated fn
(node:18001) DeprecationWarning: deprecated class
複製程式碼
具體實現
function deprecate(fn, msg, code) {
  let warned = false;
  function deprecated(...args) {
    if (!warned) {
      warned = true;
      if (code !== undefined) {
        if (!codesWarned[code]) {
          process.emitWarning(msg, 'DeprecationWarning', code, deprecated); // emit警告資訊
          codesWarned[code] = true; // 避免同一errorCode重複提示
        }
      } else {
        process.emitWarning(msg, 'DeprecationWarning', deprecated);
      }
    }
    if (new.target) { // class型別
      return Reflect.construct(fn, args, new.target);
    }
    return fn.apply(this, args); // 函式型別
  }
  return deprecated;
}
複製程式碼
與此相關的命令列配置項
命令列選項 process屬性
不輸出警告資訊 --no-deprecation
--no-warnings
noDeprecation
輸出詳細的堆疊資訊 --trace-deprecation
--trace-warnings
traceDeprecation
丟擲錯誤異常 --throw-deperecation throwDeprecation

其他

除了前面提到的三方面的工具外,util.types下還提供了非常豐富的型別識別的方法(isGeneratorFunction、isWeakSet、isPromise、isArrayBuffer)等,以後在需要型別判斷的時候可以考慮使用util模板提供的這些方法。

參考資料

nodejs.org/dist/latest…

相關文章