一路繁花似锦绣前程
失败的越多,成功才越有价值

导航

 

四、脚手架命令注册和执行过程开发

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"}
  ]
})
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 = "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时发起的
      回调
  • child_process事件应用方法详解
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)
})
posted on 2023-04-17 17:09  一路繁花似锦绣前程  阅读(46)  评论(0编辑  收藏  举报