preface

Nodemon is my favorite source monitoring tool for Node.

background

Previously explored Node-Watch, Chokidar, read their source code, roughly understand the implementation of the train of thought.

Now there is another problem. Every time you change a file, you need to restart it for the service to take effect. This makes our development less efficient. The above two plug-ins do not solve this problem. With the appearance of Nodemon, we can monitor the changes of files at any time and automatically restart the service. We only need to pay attention to the code during development, and there is no need to manually restart the service.

Let’s explore how Nodemon can automatically restart the server.

The project structure

|____index.js  // Develop file entry
|____README.md 
|____yarn-lock.json
|____package.json
Copy the code

Install nodemon

yarn add nodemon -D
Copy the code

Start the project

First you need to configure the packge.json to start the project using Nodemon instead of Node

{
  "scripts": {
    "start": "nodemon index.js".// Start with nodemon instead}}Copy the code

After the configuration, run the command on the terminal

yarn start
Copy the code

experience

You can change the contents of the index.js file, write something, and save it. You’ll see that the terminal displays the project restart.

Search for the source code

According to the command nodemon index.js, it can be seen that the bin command under Nodemon must be executed. According to this direction, find the bin object under Nodemon /pacage.json

"bin": {
  "nodemon": "./bin/nodemon.js"
},
Copy the code

/bin/nodemon.js according to the preceding address

#! /usr/bin/env node

const nodemon = require('.. /lib/');

nodemon(options); // Start the project

Copy the code

Nodemon is a method wrapped in lib/index.js.

The first step

The first step resets all configuration information, resets the listening file queue, and kills the child process.

function nodemon(settings) {
  / / reset
  nodemon.reset();
  / /...
}
Copy the code

The reset function looks like this:

bus.on('reset'.function (done) {
  debug('reset');
  nodemon.removeAllListeners(); // Clear all listeners
  monitor.run.kill(true.function () {
    utils.reset(); // Reset the utility function
    config.reset(); // Reset the configuration information
    config.run = false; // Turn off the running state, the next restart through this state whether to start
    if(done) { done(); }}); });Copy the code

The utils utility function has the following configuration information:

const utils = {
  semver: semver,
  satisfies: test= > semver.satisfies(process.versions.node, test),
  version: {/* Version control */},
  clone: require('./clone'), / / clone
  merge: require('./merge'),  / / merge
  bus: require('./bus'), / / subscribe
  isWindows: process.platform === 'win32'.isMac: process.platform === 'darwin'.isLinux: process.platform === 'linux'.isRequired: (function () {/* Check whether */ can be executed normally}) (),home: process.env.HOME || process.env.HOMEPATH,
  quiet: function () {/* Reset the log function */},
  reset: function () {/* Reset the log function */},
  regexpToText: function (t) {/* Matches the special character */ },
  stringify: function (exec, args) {/* to string */}};Copy the code

As a global object, config is configured as follows. It interacts with multiple files and records the monitored and filtered files and directories.

const config = {
  run: false.system: {
    cwd: process.cwd(),
  },
  required: false.dirs: [].timeout: 1000.options: {},}function reset() {
  config.dirs = []; // Listen to the directory
  config.options = { ignore: [].watch: [].monitor: []};// Listen options, including filter files, listen files, files already in observer
  config.lastStarted = 0;
  config.loaded = [];
}
Copy the code

Then the nodemon command will be converted into node command. Node implements the operation of the project. The Nodemon index.js executed at the beginning will be converted into Node index.js

// allow the cli string as the argument to nodemon, and allow for
// `node nodemon -V app.js` or just `-V app.js` 
if (typeof settings === 'string') {
    settings = settings.trim();
    if (settings.indexOf('node')! = =0) {
      if (settings.indexOf('nodemon')! = =0) {
        settings = 'nodemon ' + settings;
      }
      settings = 'node ' + settings; // Execute the command, such as node index.js
    }
    settings = cli.parse(settings);
  }
Copy the code

The second step

Read all the listening files in the root directory, fill in the information of config configuration items, and listen to the user’s keystrokes, such as CTRL + D, CTRL + L, and so on

Listening for keyboard events

config.load(settings, function (config) {
   if (config.options.stdin && config.options.restartable) {
     		// If you press CTRL + L, clear the information printed on the console
        if (str === config.options.restartable) {
          bus.emit('restart');
        } else if (data.charCodeAt(0) = = =12) { // ctrl+l
          console.clear(); }}else if (config.options.stdin) {
      if (chr === 3) {
          if (ctrlC) {
            process.exit(0);
          }

          ctrlC = true;
          return;
        } else if (buffer === '.exit' || chr === 4) { // ctrl+d
          process.exit();
        } else if (chr === 13 || chr === 10) { // enter / carriage return
          buffer = ' ';
        } else if (chr === 12) { // ctrl+l
          console.clear();
          buffer = ' '; }}}Copy the code

Start the

config.load(settings, function (config) {
   config.run = true;
  
   monitor.run(config.options); // Pass the configuration information to monitor to start the listener
}
Copy the code

After config is filled, all the information is as follows:

{
  run: false.system: { cwd: '/Users/zhoujianpiao/Desktop/node/rollup' },
  required: false.dirs: [].timeout: 1000.options: { ignore: [].watch: [].monitor: []},load: [Function (anonymous)],
  reset: [Function: reset],
  lastStarted: 0.loaded: []}Copy the code

The third step

This step is responsible for listening for files.

As soon as you run run, restart is immediately started.

  restart = run.bind(this, options);
  run.restart = restart;
Copy the code

The start signal is then notified in the form of publish subscriptions

bus.emit('start');
Copy the code

The actual file listening is done by watch.js. All monitoring files are saved in the Watchers queue. If no monitoring file is found, watch is not executed.

function watch() {
  // Check whether there are listening files
  if (watchers.length) {
    debug('early exit on watch, still watching (%s)', watchers.length);
    return; }}Copy the code

Chokidar is a high-performance, stable file listening tool that uses different listening systems according to different operating environments. Nodemon’s core listening source code is here.

 const promise = new Promise(function (resolve) {
    // Configuration information
    var watchOptions = {
      ignorePermissionErrors: true.ignored: ignored, // File ignored
      persistent: true.// Keep the process running after it is ready
      usePolling: config.options.legacyWatch || false.interval: config.options.pollingInterval,
    };

   	// Create a listener
    var watcher = chokidar.watch(
      dirs,
      Object.assign({}, watchOptions, config.options.watchOptions || {})
    );

    watcher.ready = false;

    var total = 0;

    watcher.on('change', filterAndRestart); // The callback function is notified of file changes
    watcher.on('add'.function (file) {
      if (watcher.ready) {
        return filterAndRestart(file);
      }

      watchedFiles.push(file);
      bus.emit('watching', file);
    });
    watcher.on('ready'.function () {
      watchedFiles = Array.from(new Set(watchedFiles)); // ensure no dupes
      total = watchedFiles.length;
      watcher.ready = true; // Ready
      resolve(total);
      debugRoot('watch is complete');
    });

    watchers.push(watcher);
  });

Copy the code

FilterAndRestart is responsible for filtering files, looking for matching files, and getting those files that are actually listening.

function filterAndRestart(files) {
    // Matches a file that can be listened on
    if (matched.result.length) {
      if (config.options.delay > 0) {
        utils.log.detail('delaying restart for ' + config.options.delay + 'ms');
        if (debouncedBus === undefined) {
          debouncedBus = debounce(restartBus, config.options.delay);
        }
        debouncedBus(matched); // If delay is set, the anti-shake function is called
      } else {
        return restartBus(matched); // Otherwise, restart the server}}}}Copy the code

RestartBus is responsible for restarting the server

// Restart the server
function restartBus(matched) {
  utils.log.status('restarting due to changes... '); // This print prompt can be seen on the console
  matched.result.map(file= > {
    utils.log.detail(path.relative(process.cwd(), file));
  });

  bus.emit('restart', matched.result); // Issue a restart notification
}
Copy the code

The fourth step

To update the file, the run.kill() function executes, sends a SIGINT flag to the child process, kills the child process, and then restarts the child process using the exit event handler.

bus.on('restart'.function () {
  // run.kill will send a SIGINT to the child process, which will cause it
  // to terminate, which in turn uses the 'exit' event handler to restart
  run.kill();
});
Copy the code

summary

The above four episodes are the core part of nodemon’s entire process. What I find most interesting is the use of a publish-subscribe model.

conclusion

  • When Nodemon runs, it will first reset all configuration information, such as clearing the queue of the previously monitored subprocess, clearing the subscribed events, and resetting all tool methodsnodemon index.jsTerminal commands are converted tonode index.js
  • Once you’ve done the basics, scan the entire root directory for all executable files and process user parameter information, such asignoringWhich files to filter, all fill toconfig.optionsIn the.
  • Next, start the server, create the child process, use the Chokidar tool, start the file to listen for events.
  • When the file changes,watcherThe instance will executefilterAndRestartCallback function. If the restart of the service needs to be delayed, the system uses the anti-shake function to restart the service according to the time specified by the user. Otherwise, the system restarts the service immediatelybus.emit('restart', matched.result)Release a restart notification.
  • If a restart message is received, the system will execute the commandrun.kill()Function to send one to the child processSIGINTFlag, kill the child process, and then the child process to useexitThe event handler is restarted.