四、脚手架命令注册和执行过程开发
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, ".linding-cli-dev"),
storeDir: path.resolve(userHome, ".linding-cli-dev", "node_modules"),
registry: "https://registry.npmjs.org",
pkgs: [
{name: "foo", version: "~1.0.0"}
]
})
// npm i fs-extra
const fse = require("fs-extra")
// npm i path-exists
const pathExists = require("path-exists").sync
const pathStr = "E:\\study\\aaa\\bbb\\ccc"
// 判断路径是否存在
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 关键字或进程号
- pid:进程号
- ppid:父进程号
* windows:tasklist|findstr 关键字或进程号
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) // 异常执行输出结果
})
/*
// shell脚本
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:\\study\\web-architect\\work-space-01\\imooc-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(main) -> Node(child)
const child = cp.fork(path.resolve(__dirname, 'child.js'))
child.send('hello child process!', () => {
// child.disconnect()
})
child.on('message', (msg) => {
console.log(msg)
})
console.log('main pid:' + process.pid)
/*
// child.js文件
console.log('child process')
console.log('child pid:' + process.pid)
process.on('message', (msg) => {
console.log(msg)
})
process.send('hello main process')*/
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多进程child_process库源码分析
* shell的使用
- 直接执行shell文件:/bin/sh test.shell
- 直接执行shell语句:/bin/sh -c "ls -al|grep node_modules"
* exec/execFile/spawn/fork的区别
- exec:原理是调用/bin/sh -c执行我们传入的shell脚本,底层调用了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时发起的
回调
const cp = require("child_process")
const child = cp.exec(`dir E:\\study\\web-architect\\work-space-01\\imooc-test | findstr node_modules`, function (error, stdout, stderr) {
console.log("callback start-----------------")
console.log(error)
console.log(stdout)
console.log(stderr)
console.log("callback end-----------------")
})
// cp.exec改成cp.execFile可测试此事件
child.on("error", err => {
console.log("error!", err)
})
child.stdout.on("data", chunk => {
console.log("stdout data", chunk)
})
child.stderr.on("data", chunk => {
console.log("stderr data", chunk)
})
child.stdout.on("close", () => {
console.log("stdout close")
})
child.stderr.on("close", () => {
console.log("stderr close")
})
child.on("exit", (exitCode) => {
console.log("exit!", exitCode)
})
child.on("close", () => {
console.log("close!")
})
五、脚手架创建项目流程设计和开发
1、概念
* 架构背后的思考
- 可扩展:能够快速复用到不同团队,适应不同团队之间的差异
- 低成本:在不改动脚手架源码的情况下,能够新增模板,且新增模板的成本很低
- 高性能:控制存储空间,安装时充分利用node多进程提升安装性能
2、inquirer基本用法
// 安装:npm i -S inquirer@8
const inquirer = require('inquirer')
inquirer
.prompt([
{
name: "yourName",
type: "input",
message: "your name",
default: "noname",
validate: function (v) {
return typeof v === "string"
},
transformer: function (v) {
return v + ":name"
},
filter: function (v) {
return "name:" + v
}
},
{
name: "num",
type: "number",
message: "your number",
default: 0
},
{
name: "choice",
type: "confirm",
message: "your choice",
default: false
},
{
name: "choiceList",
type: "list",
message: "your choice list",
default: 0,
choices: [
{value: 1, name: "科比"},
{value: 2, name: "乔丹"},
{value: 3, name: "约基奇"}
]
},
{
name: "choiceRawList",
type: "rawlist",
message: "your choice raw list",
default: 0,
choices: [
{value: 1, name: "科比"},
{value: 2, name: "乔丹"},
{value: 3, name: "约基奇"}
]
},
{
name: "choiceExpand",
type: "expand",
message: "your choice expand",
default: "red",
choices: [
{key: "R", value: "red"},
{key: "G", value: "green"},
{key: "B", value: "blue"}
]
},
{
name: "choiceCheckbox",
type: "checkbox",
message: "your choice checkbox",
default: 0,
choices: [
{value: 1, name: "科比"},
{value: 2, name: "乔丹"},
{value: 3, name: "约基奇"}
]
},
{
name: "yourPassword",
type: "password",
message: "your password"
},
{
name: "yourEditor",
type: "editor",
message: "your editor"
}
])
.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
- linding-cli-dev-server/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);
};
- linding-cli-dev-server/app/controller/project.js
'use strict';
const { Controller } = require('egg');
class ProjectController extends Controller {
// 获取项目/组件的代码模板
async getTemplate() {
const { ctx } = this;
ctx.body = 'get template';
}
}
module.exports = ProjectController;
* 手动修改host文件
- C:\Windows\System32\drivers\etc\hosts
* 使用SwitchHosts工具
- https://github.com/oldj/SwitchHosts
5、egg.js接入mongodb
- linding-cli-dev-server/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://linding:123456@localhost:27017/linding-cli-dev';
const mongodbDbName = 'linding-cli-dev';
module.exports = {
mongodbUrl,
mongodbDbName,
};
- linding-cli-dev-server/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;
- linding-cli-dev-server/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、通过spinner实现命令行loading效果
(async function () {
// 安装:npm i cli-spinner
const Spinner = require('cli-spinner').Spinner
const spinner = new Spinner("loading.. %s")
spinner.setSpinnerString("|/-\\")
spinner.start()
await new Promise(resolve => setTimeout(resolve, 1000))
spinner.stop(true)
})()
7、readline的使用方法和实现原理
const readline = require("readline")
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
rl.question('your name: ', answer => {
console.log(answer)
rl.close()
})
function Interface(input, output, completer, terminal) {
// 强制将函数转换为构造函数
if (!(this instanceof Interface)) {
return new Interface(input, output, completer, terminal);
}
}
* nodejs的三大特性
- 单线程
- 非阻塞IO
- 事件驱动
// 生成器复习
function* g() {
console.log('read')
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('answer:' + s)
})
8、命令行样式修改的核心原理:ansi转义序列
/**
* 1、ANSI-escape-code查阅文档:https://handwiki.org/wiki/ANSI_escape_code
*/
console.log('\x1B[41m\x1B[4m%s\x1B[0m', 'your name:')
console.log('\x1B[2B%s', 'your name2:')
9、响应式库rxjs快速入门
// 安装:npm i rxjs
const {range} = require('rxjs')
const {map, filter} = require("rxjs/operators")
range(1, 200).pipe(
filter(x => x % 2 === 1),
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: "name",
message: "select your name:",
choices: [{
name: "sam", value: "sam"
}, {
name: "shuangyue", value: "sy"
}, {
name: "zhangxuan", value: "zx"
}]
}
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(Use arrow keys)\x1B[22m\n"
this.choices.forEach((choice, index) => {
if (index === this.selected) {
// 判断是否为最后一个元素,如果是,则不加\n
if (index === this.choices.length - 1) {
title += '\x1B[36m> ' + choice.name + '\x1B[39m '
} else {
title += '\x1B[36m> ' + choice.name + '\x1B[39m \n'
}
} else {
if (index === this.choices.length - 1) {
title += ' ' + choice.name
} else {
title += ' ' + choice.name + '\n'
}
}
})
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 \n"
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:', answers)
})