Node.jsによるスキャフォールディングの開発とプロジェクト生成

四、スキャフォールディングのコマンド登録と実行プロセスの開発

1、npminstall

const path = require("path")
// npm i npminstall
const npminstall = require("npminstall")
// npm i user-home
const userHome = require("user-home")

npminstall({
  root: path.resolve(userHome, ".my-cli-dev"),
  storeDir: path.resolve(userHome, ".my-cli-dev", "node_modules"),
  registry: "https://registry.npmjs.org",
  pkgs: [
    {name: "sample-package", version: "~1.0.0"}
  ]
})

2、path-existsとfs-extra

// npm i fs-extra
const fse = require("fs-extra")
// npm i path-exists
const pathExists = require("path-exists").sync
const pathStr = "/home/user/projects/new-dir"

// パスの存在を確認
if (!pathExists(pathStr)) {
  // 存在しないディレクトリをすべて作成
  fse.mkdirpSync(pathStr)
}

3、WebStormでのスキャフォールディングのデバッグ

* Edit Configurations -> Add New Configuration -> Node.js
* コマンド: <node interpreter> <working directory> + <node parameters>

4、プロセスの検索

* Linux:ps -ef|grep キーワードまたはプロセスID
    - pid:プロセスID
    - ppid:親プロセスID
* Windows:tasklist|findstr キーワードまたはプロセスID

5、child_processの非同期メソッドの使用

const cp = require("child_process")
const path = require("path")
// npm i iconv-lite
const iconv = require("iconv-lite")

const encoding = 'cp936';
const binaryEncoding = 'binary';

// Windowsでテスト
cp.exec(path.resolve(__dirname, "test.bat param1 param2"), {
  timeout: 0, // タイムアウト、0はタイムアウトなし
  cwd: path.resolve(__dirname), // 実行パスを変更
  encoding: binaryEncoding // Windowsコンソールの文字化けを処理
}, function (error, stdout, stderr) {
  console.log(error) // エラー情報
  console.log(iconv.decode(new Buffer.from(stdout, binaryEncoding), encoding)) // 正常な実行出力
  console.log(iconv.decode(new Buffer.from(stderr, binaryEncoding), encoding)) // エラー出力
})
/*
// DOS/cmdスクリプト
dir

echo %1
echo %*
*/

// Linuxでテスト
cp.execFile(path.resolve(__dirname, "test.shell"), ["-al", "-bl"], function (error, stdout, stderr) {
  console.log(error) // エラー情報
  console.log(stdout) // 正常な実行出力
  console.log(stderr) // エラー出力
})
/*
// シェルスクリプト
ls -al|grep node_modules

echo $1
echo $2
*/

6、child_processのspawnの使い方

const cp = require("child_process")
const iconv = require("iconv-lite")

const encoding = 'cp936';
const binaryEncoding = 'binary';

const child = cp.spawn('npm.cmd', ['i'], {
  cwd: 'E:\\projects\\web-architect\\work-space-01\\test-lib',
  encoding: binaryEncoding,
  // stdio: 'inherit' // 相応するstdioストリームを親プロセスに渡す
})
// child.pid:子プロセス;process.pid:親プロセス。
// console.log(child.pid, process.pid)
child.stdout.on('data', function (chunk) {
  console.log('stdout', iconv.decode(new Buffer.from(chunk, binaryEncoding), encoding))
})
child.stderr.on('data', function (chunk) {
  console.log('stderr', iconv.decode(new Buffer.from(chunk, binaryEncoding), encoding))
})
// spawn:時間のかかるタスク(例:npm install)で、継続的なログが必要
// exec/execFile:オーバーヘッドが小さいタスク

7、forkの使い方と親子プロセス間の通信メカニズム

非同期

const cp = require("child_process")
const path = require("path");

// fork:Node(親) -> Node(子)
const child = cp.fork(path.resolve(__dirname, 'child.js'))
child.send('子プロセスへこんにちは!', () => {
  // child.disconnect()
})
child.on('message', (msg) => {
  console.log(msg)
})
console.log('親プロセスPID:' + process.pid)

/*
// child.jsファイル
console.log('子プロセス')

console.log('子プロセスPID:' + process.pid)

process.on('message', (msg) => {
  console.log(msg)
})
process.send('親プロセスへこんにちは!')
*/

同期

const cp = require("child_process")

const ret = cp.execSync('ls -al|grep node_modules')
console.log(ret.toString())

const ret2 = cp.execFileSync('ls', ['-al'])
console.log(ret2.toString())

const ret3 = cp.spawnSync('ls', ['-al'])
console.log(ret3.stdout.toString())

8、Node.jsのマルチプロセスchild_processライブラリのソースコード分析

補足知識

* シェルの使用
    - シェルファイルを直接実行:/bin/sh test.shell
    - シェルコマンドを直接実行:/bin/sh -c "ls -al|grep node_modules"
* exec/execFile/spawn/forkの違い
    - exec:原理は/bin/sh -cを呼び出して渡されたシェルスクリプトを実行し、内部でexecFileを呼び出す
    - execFile:原理は渡されたfileとargsを直接実行し、内部でspawnを呼び出して子プロセスを作成・実行し、コールバックを設定し、stdoutとstderrの結果を一度に返す
    - spawn:原理はinternal/child_processを呼び出し、ChildProcess子プロセスオブジェクトをインスタンス化し、child.spawnを呼び出して子プロセスを作成・実行し、内部でchild._handle.spawnを呼び出してprocess_wrapのspawnメソッドを実行する。実行プロセスは非同期で、完了後はPIPEを通じて一方向のデータ通信を行い、通信終了後は子プロセスがonexitコールバックを発行し、同時にSocketがcloseコールバックを実行する
    - fork:原理はspawnを通じて子プロセスを作成・実行し、nodeコマンドを実行し、setupchannelを通じてIPCを使用して子プロセスと親プロセス間の双方向通信を行う
* data/error/exit/closeコールバックの違い
    - data:親プロセスがデータを読み取る際にonStreamReadを通じて発行されるコールバック
    - error:コマンド実行失敗後に発行されるコールバック
    - exit:子プロセスが終了した後に発行されるコールバック
    - close:子プロセスのすべてのSocket通信ポートが閉じられた後に発行されるコールバック
    - stdout close/stderr close:特定のPIPEが読み取り完了後、onReadableStreamEndを通じてSocketを閉じる際に発行されるコールバック

child_processイベントの応用方法の詳細

const cp = require("child_process")

const child = cp.exec(`dir E:\\projects\\web-architect\\work-space-01\\test-lib | findstr node_modules`, function (error, stdout, stderr) {
  console.log("コールバック開始-----------------")
  console.log(error)
  console.log(stdout)
  console.log(stderr)
  console.log("コールバック終了-----------------")
})

// cp.execをcp.execFileに変更してテスト可能
child.on("error", err => {
  console.log("エラー発生!", err)
})

child.stdout.on("data", chunk => {
  console.log("stdoutデータ", chunk)
})
child.stderr.on("data", chunk => {
  console.log("stderrデータ", chunk)
})

child.stdout.on("close", () => {
  console.log("stdoutクローズ")
})
child.stderr.on("close", () => {
  console.log("stderrクローズ")
})

child.on("exit", (exitCode) => {
  console.log("終了!", exitCode)
})
child.on("close", () => {
  console.log("クローズ!")
})

五、スキャフォールディングによるプロジェクト作成フローの設計と開発

1、概念

* アーキテクチャの背後にある思考
    - 拡張性:異なるチームに迅速に適用でき、チーム間の差異に適応できる
    - 低コスト:スキャフォールディングのソースコードを変更せずに、テンプレートを追加でき、かつ追加コストが低い
    - 高性能:ストレージスペースを制御し、インストール時にNodeのマルチプロセスを最大限に活用してインストールパフォーマンスを向上させる

2、inquirerの基本的な使い方

// インストール:npm i -S inquirer@8
const inquirer = require('inquirer')

inquirer
  .prompt([
    {
      name: "userName",
      type: "input",
      message: "あなたの名前は?",
      default: "匿名",
      validate: function (v) {
        return typeof v === "string"
      },
      transformer: function (v) {
        return v + ":名前"
      },
      filter: function (v) {
        return "名前:" + v
      }
    },
    {
      name: "age",
      type: "number",
      message: "あなたの年齢は?",
      default: 0
    },
    {
      name: "agree",
      type: "confirm",
      message: "同意しますか?",
      default: false
    },
    {
      name: "favorite",
      type: "list",
      message: "お気に入りのスポーツ選手は?",
      default: 0,
      choices: [
        {value: 1, name: "メッシ"},
        {value: 2, name: "クリスティアーノ・ロナウド"},
        {value: 3, name: "ネイマール"}
      ]
    },
    {
      name: "favoriteRaw",
      type: "rawlist",
      message: "お気に入りのスポーツ選手(生リスト)",
      default: 0,
      choices: [
        {value: 1, name: "メッシ"},
        {value: 2, name: "クリスティアーノ・ロナウド"},
        {value: 3, name: "ネイマール"}
      ]
    },
    {
      name: "colorChoice",
      type: "expand",
      message: "お気に入りの色は?",
      default: "red",
      choices: [
        {key: "R", value: "赤"},
        {key: "G", value: "緑"},
        {key: "B", value: "青"}
      ]
    },
    {
      name: "hobbies",
      type: "checkbox",
      message: "趣味は?",
      default: 0,
      choices: [
        {value: 1, name: "読書"},
        {value: 2, name: "映画"},
        {value: 3, name: "旅行"}
      ]
    },
    {
      name: "userPassword",
      type: "password",
      message: "パスワードを入力"
    },
    {
      name: "userEditor",
      type: "editor",
      message: "エディタを開く"
    }
  ])
  .then(answers => {
    console.log(answers)
  })
  .catch(error => {
    if (error.isTtyError) {

    } else {

    }
  })

3、Egg.jsのクイックスタート

# npm >= 6.1.0
# npx create-react-app my-projectの実行プロセス:
#     - ローカルにcreate-react-appコマンドがあるか確認し、あればローカルを実行。ローカルにない場合はグローバルを実行。
#       グローバルにもない場合は、コマンドを一時ディレクトリにダウンロードして実行し、完了後にコマンドを削除。
# npm init react-app my-projectは以下と同等:
#     - npx create-react-app my-project
mkdir egg-example && cd egg-example
npm init egg --type=simple
npm i
# プロジェクトを起動
npm run dev
open http://localhost:7001

4、Egg.jsフレームワークへの新しいAPIの追加

app/router.js

'use strict';

/**
 * @param {Egg.Application} app - egg application
 */
module.exports = app => {
  const { router, controller } = app;
  router.get('/project/template', controller.project.getTemplate);
};

app/controller/project.js

'use strict';

const { Controller } = require('egg');

class ProjectController extends Controller {
  // プロジェクト/コンポーネントのコードテンプレートを取得
  async getTemplate() {
    const { ctx } = this;
    ctx.body = 'テンプレートを取得';
  }
}

module.exports = ProjectController;

ローカルドメイン名のマッピング設定

* 手動でhostファイルを編集
    - C:\Windows\System32\drivers\etc\hosts
* SwitchHostsツールを使用
    - https://github.com/oldj/SwitchHosts

5、Egg.jsでのMongoDBの接続

config/db.js

'use strict';

/**
 * 一、MongoDBにユーザー名とパスワードを設定
 *     - Studio 3TでMongoDBに接続し、adminデータベースにユーザーを追加し、rootロールを割り当てる
 *     - MongoDBのインストールディレクトリ/bin/mongod.cfgの設定:
 *         ~ security:
 *             authorization: enabled
 *     - MongoDBサービスを再起動
 *     - Studio 3Tで再接続するには、authenticationのmodeをbasicに変更し、
 *       追加したユーザー名、パスワード、adminデータベースを入力する必要がある
 * 二、ビジネスデータベースにユーザー名とパスワードを設定
 *     - Studio 3TでMongoDBに接続し、ビジネスデータベースにユーザーを追加し、readWriteロールを割り当てる
 */
const mongodbUrl = 'mongodb://devuser:password@localhost:27017/dev-db';
const mongodbDbName = 'dev-db';

module.exports = {
  mongodbUrl,
  mongodbDbName,
};

app/utils/mongo.js

'use strict';

// インストール:npm i @pick-star/cli-mongodb
const Mongodb = require('@pick-star/cli-mongodb');
const { mongodbUrl, mongodbDbName } = require('../../config/db');

function mongo() {
  return new Mongodb(mongodbUrl, mongodbDbName);
}

module.exports = mongo;

app/controller/project.js

'use strict';

const { Controller } = require('egg');
const mongo = require('../utils/mongo');

class ProjectController extends Controller {
  async getTemplate() {
    const { ctx } = this;
    const data = await mongo().query('project');
    ctx.body = data;
  }
}

module.exports = ProjectController;

6、スピナーを使用してコマンドラインのローディング効果を実現

(async function () {
  // インストール:npm i cli-spinner
  const Spinner = require('cli-spinner').Spinner

  const spinner = new Spinner("読み込み中.. %s")
  spinner.setSpinnerString("|/-\")
  spinner.start()
  await new Promise(resolve => setTimeout(resolve, 1500))
  spinner.stop(true)
})()

7、readlineの使用方法と実装原理

基本的な使用方法

const readline = require("readline")

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
})

rl.question('あなたの名前は? ', answer => {
  console.log(answer)
  rl.close()
})

ソースコードの読み取り

function Interface(input, output, completer, terminal) {
  // 関数を強制的にコンストラクタに変換
  if (!(this instanceof Interface)) {
    return new Interface(input, output, completer, terminal);
  }
}
* Node.jsの3つの主な特性
    - シングルスレッド
    - 非ブロッキングIO
    - イベントドリブン
// ジェネレーターの復習
function* g() {
  console.log('読み取り')
  let ch = yield
  console.log(ch)
  let s = yield
  console.log(s)
}

const f = g()
f.next()
f.next('a')
f.next('b')

手書き実装

function stepRead(callback) {
  function onkeypress(s) {
    output.write(s)
    line += s
    switch (s) {
      case '\r':
        input.pause()
        callback(line)
        break
    }
  }

  const input = process.stdin
  const output = process.stdout
  let line = ''

  emitKeypressEvents(input)
  input.on('keypress', onkeypress)

  input.setRawMode(true)
  input.resume()
}

function emitKeypressEvents(stream) {
  function onData(chunk) {
    g.next(chunk.toString())
  }

  const g = emitKeys(stream)
  g.next()

  stream.on('data', onData)
}

function* emitKeys(stream) {
  while (true) {
    let ch = yield
    stream.emit('keypress', ch)
  }
}

stepRead(function (s) {
  console.log('回答:' + s)
})

8、コマンドラインのスタイル変更の核心原理:ANSIエスケープシーケンス

/**
 * 1、ANSI-escape-codeのドキュメントを参照:https://handwiki.org/wiki/ANSI_escape_code
 */
console.log('\x1B[41m\x1B[4m%s\x1B[0m', 'あなたの名前:')
console.log('\x1B[2B%s', '名前2:')

9、リアクティブライブラリRxJSのクイックスタート

// インストール:npm i rxjs
const {range} = require('rxjs')
const {map, filter} = require("rxjs/operators")

range(1, 100).pipe(
  filter(x => x % 3 === 0),
  map(x => x * x)
).subscribe(x => console.log(x))

10、手書きコマンドラインインタラクティブリストコンポーネント

const EventEmitter = require('events')
const readline = require('readline')
// インストール:npm i mute-stream
const MuteStream = require("mute-stream")
// インストール:npm i rxjs
const {fromEvent} = require("rxjs")
// インストール:npm i ansi-escapes@4
const ansiEscapes = require("ansi-escapes")

const option = {
  type: "list",
  name: "selectName",
  message: "名前を選択してください:",
  choices: [{
    name: "Alice", value: "alice"
  }, {
    name: "Bob", value: "bob"
  }, {
    name: "Charlie", value: "charlie"
  }]
}

function Prompt(option) {
  return new Promise((resolve, reject) => {
    try {
      const list = new List(option)
      list.render()
      list.on('exit', function (answers) {
        resolve(answers)
      })
    } catch (e) {
      reject(e)
    }
  })
}

class List extends EventEmitter {
  constructor(option) {
    super();
    this.name = option.name
    this.message = option.message
    this.choices = option.choices
    this.input = process.stdin
    const ms = new MuteStream()
    ms.pipe(process.stdout)
    this.output = ms
    this.rl = readline.createInterface({
      input: this.input,
      output: this.output
    })
    this.selected = 0
    this.height = 0
    this.keypress = fromEvent(this.rl.input, 'keypress')
      .forEach(this.onkeypress);
    this.haveSelected = false; // 選択が完了したかどうか
  }

  onkeypress = (keymap) => {
    const key = keymap[1]
    if (key.name === 'down') {
      this.selected++
      if (this.selected > this.choices.length - 1) {
        this.selected = 0
      }
      this.render()
    } else if (key.name === 'up') {
      this.selected--
      if (this.selected < 0) {
        this.selected = this.choices.length - 1
      }
      this.render()
    } else if (key.name === 'return') {
      this.haveSelected = true
      this.render()
      this.close()
      this.emit('exit', this.choices[this.selected])
    }
  }

  render() {
    this.output.unmute()
    this.clean()
    this.output.write(this.getContent())
    this.output.mute()
  }

  getContent = () => {
    if (!this.haveSelected) {
      let title = '\x1B[32m?\x1B[39m \x1B[1m' + this.message + "\x1B[22m\x1B[0m\x1B[0m\x1B[2m(矢印キーを使用)\x1B[22m
"
      this.choices.forEach((choice, index) => {
        if (index === this.selected) {
          // 最後の要素かどうかを判断し、最後であれば
を追加しない
          if (index === this.choices.length - 1) {
            title += '\x1B[36m> ' + choice.name + '\x1B[39m '
          } else {
            title += '\x1B[36m> ' + choice.name + '\x1B[39m 
'
          }
        } else {
          if (index === this.choices.length - 1) {
            title += '  ' + choice.name
          } else {
            title += '  ' + choice.name + '
'
          }
        }
      })
      this.height = this.choices.length + 1
      return title
    } else {
      // 入力終了後のロジック
      const name = this.choices[this.selected].name
      let title = '\x1B[32m?\x1B[39m \x1B[1m' + this.message + "\x1B[22m\x1B[0m\x1B[36m" + name + "\x1B[39m\x1B[0m 
"
      return title
    }
  }

  clean() {
    const emptyLines = ansiEscapes.eraseLines(this.height)
    this.output.write(emptyLines)
  }

  close() {
    this.output.unmute()
    this.rl.output.end()
    this.rl.pause()
    this.rl.close()
  }
}

Prompt(option).then(answers => {
  console.log('回答:', answers)
})

タグ: Node.js CLI Scaffolding child_process Egg.js

8月11日 09:14 投稿