四、スキャフォールディングのコマンド登録と実行プロセスの開発
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)
})