學習 nodemon 的實現原理

引子

相信你們都知道 nodemon 吧 ,就是那個能夠幫助咱們在修改了代碼以後重啓服務的 cli 工具,可是它的實現原理是什麼樣子的呢?接下來咱們就來造個小輪子來探索探索html

經過造輪子的方式來學習,能夠更深層次的理解原理node

b站配套視頻git

原理

想想,若是不用 ndoemon 的話,咱們本身手動是如何更新的?github

// main.js
const Koa = require("koa");
const app = new Koa();
app.use((ctx) => {
  ctx.body = "hi my name is cuixiaorui";
});
app.listen(3000);
複製代碼

手動更新步驟:npm

  1. 修改 main.js 文件內的 ctx.body 的值api

  2. 關閉當前的服務,而後從新執行 node main.jsmarkdown

而ndoemon 就是幫助咱們自動化了上面的兩個步驟,讓咱們只關注於修改文件便可,像從新啓動服務這件事實際上是個重複的勞動。而程序最適合解決這種重複性的問題app

那也就是說,只有咱們把上面兩個步驟用代碼來實現就能夠了,而這個就是 nodemon 的原理所在。koa

那怎麼把上面的兩個步驟翻譯成具體的代碼呢?ide

仔細分析的話,你會發現修改 main.js 文件其實能夠翻譯成觀察文件的改變,修改了代碼的話,那麼文件確定是發生改變了

而關閉當前的服務這個就更具體了, 就是關閉進程而後再執行 node main.js 這個命令便可

觀察文件的變化

那在 nodejs 中如何觀察文件的變化呢?

若是你熟悉 nodejs 的 api 的話,你會發現 fs 模塊下面有一個 watch 的方法,它的功能就是觀察文件的變動,可是這個 api 不是太友好,在 Mac 平臺下有不少的問題

而社區裏面有一個更好用的工具 chokidar, 能夠看看他的 README,

image.png

專門解決了 fs.watch 的一系列問題

ok,工具咱們選擇好了,那麼接下來看看如何去使用呢?

const chokidar = require("chokidar");
chokidar.watch(["main.js"]).on("all", (event, path) => {
  console.log(event, path);
});

複製代碼

經過 chokidar.watch 來觀察具體的文件,咱們這裏是 main.js 而後經過監聽 all 事件。這樣當文件變動的時候咱們就能夠收到消息啦

好 ,這個問題就算搞定了。咱們接着往下看

執行 node main.js 命令

那下一步就須要探索如何才能夠執行 node main.js 命令了,在nodejs 中有兩種方式能夠執行命令

  1. exec

  2. spawn

這兩個函數都是在 child_process 模塊下。用起一個子進程的方式來幫助你執行命令

而對於子進程這個概念能夠理解爲:咱們當前執行的主進程是爸爸,爸爸正在幹活,可是他如今想喝水(執行 node main.js)若是他本身去的話,那麼就會影響到他手頭上的活,那怎麼能不影響手頭的活還能夠喝到水呢?叫兒子去不就行了。 而子進程就至關因而兒子。

那 exec 和spawn 的區別是什麼呢?

咱們來實驗一下,同時執行 test.js 腳本

//test.js
console.log("test.js");

setTimeout(() => {
  console.log("set timeout");
}, 500);


複製代碼

exec 是同步執行,他會等待腳本都執行完成以後在進行回調

exec("node test.js", (err,stdout)=>{
  console.log(stdout);
 }); 
複製代碼

而 spawn 是基於流的形式,執行完就經過流的方式把數據發過來

spawn("node", ["test.js"], {
stdio:[process.stdin,process.stdout,process.stderr]
}) 
複製代碼

具體的區別差別能夠看視頻

接着咱們就能夠把觀察文件的代碼和執行命令的代碼合併到一塊兒咯

chokidar.watch(["main.js"]).on("all", (event, path) => {
  spawn("node", ["main.js"], {
  stdio:[process.stdin,process.stdout,process.stderr]
  }) 
});
複製代碼

可是,這樣的話,當咱們改變了 main.js 代碼後,會給咱們報錯

image_1.png

告訴咱們,端口被佔用啦!爲何會被佔用呢?

其實就是由於咱們沒有把以前打開的服務給關掉嘛,因此在打開一個的話,確定就會出現這個問題啦

那問題又來了,如何關閉以前的進程呢?

關閉進程

若是咱們仔細的看 spawn api 的話,你會發現它會返回 chidlProcess 對象。而這個對象裏面有個方法是 kill

咱們就能夠經過這個方法來關閉進程

childProcess && childProcess.kill();

  childProcess = spawn("node", ["main.js"], {
    stdio: [process.stdin, process.stdout, process.stderr],
  });
複製代碼

用childProcess 變量來存儲,而後再執行的時候檢測一下,若是存在的話,那麼就執行 kill 來關閉以前的進程

到這裏的時候,其實咱們就已經實現完了 nodemon 的最核心的部分,可是仍是有太多能夠優化的點了。

使用防抖來優化

若是咱們在 main.js 裏面一直保存的話,你會發現它會一直觸發 all 事件,而這樣的話,就會頻繁的執行 spawn 了,而其實咱們只須要在最後一次調用 all 事件的時候執行一次 spawn 就能夠了。而這個場景就是使用防抖的最佳場景

function debounce(fn, delay) {
  let id;
  return () => {
    clearTimeout(id);

    id = setTimeout(() => {
      fn();
    }, delay);
  };
}
複製代碼

接着咱們把執行 spawn 的邏輯封裝成一個函數

function restart() {
  console.log("restart");
  childProcess && childProcess.kill();

  childProcess = spawn("node", ["main.js"], {
    stdio: [process.stdin, process.stdout, process.stderr],
  });
}

let debounceRestart = debounce(restart, 500)

複製代碼

最後在 watch 裏面執行debounceRestart

chokidar.watch(["main.js"]).on("all", (event, path) => {
  console.log(event, path);

  debounceRestart();
});
複製代碼

這樣你在怎麼改動 main.js 你會發現它都只會調用一次了

總結

看看咱們經過這個小小的輪子都學到了什麼吧!

  • 觀察文件的改變

    • fs.watch

    • chokidar

  • 在 nodejs 中使用 exec 和 spawn 執行命令

  • 防抖

若是你仔細閱讀的話,你會發現咱們整個都是從問題出發,把實際的問題轉換成具體的代碼,從而慢慢的完成了簡版的 nodemon 的實現。

其實寫任何程序都是這樣的,發現問題,解決問題。迭代式的完善程序。

而對於問題的拆分這個技能就是 Tasking 任務拆分了。

掌握了 Tasking 至少能夠提升你寫程序的百分之 50 的能力

而 Tasking 就須要一直不斷的刻意練習

而最好的練習方式就是本身造輪子,這個我也稱之爲造輪子學習法。

關注我,後面分享更多的輪子

文章會首發到個人公衆號:阿崔cxr

相關連接

代碼

b站視頻

chokidar

exec和spawn

相關文章
相關標籤/搜索