最近要寫一些腳本工具,爲了方便作一些簡單的交互,忽然想起 Vue-Cli
裏的那種交互不錯,而後看了一下,是用了inquirer.js,那麼就簡單地聊聊諸如Vue-Cli等工具用到的Inquirer.js如何工做的吧。javascript
這裏先把inquirer.js的github連接奉上:
github.com/SBoudrias/I…html
而後開始分析分析之旅vue
這裏先貼一些 Vue-Cli
的截圖吧:
java
list.js
'use strict'; let inquirer = require('inquirer'); inquirer .prompt([ { type: 'list', name: 'action', message: '你如今想幹嗎', choices: [ '打代碼', new inquirer.Separator(), { name: '叫個小姐姐上門', disabled: '不能夠!' }, '上廁所', ] }, { type: 'list', name: 'os', message: '你打代碼的系統是啥', choices: ['macOS', 'Windows', 'Centos', 'Ubuntu', 'FreeBSD', 'Others'], filter: function(val) { return val.toLowerCase(); } } ]) .then(answers => { console.log(JSON.stringify(answers, null, ' ')); }); 複製代碼
先看看項目中的 package.json
看看它有沒有依賴什麼庫。node
{ "private": true, "devDependencies": { "@babel/core": "^7.4.5", "@babel/preset-env": "^7.4.5", "babel-jest": "^24.8.0", "codecov": "^3.5.0", "eslint": "^5.16.0", "eslint-config-prettier": "^5.0.0", "eslint-config-xo": "^0.26.0", "eslint-plugin-prettier": "^3.1.0", "husky": "^2.4.1", "jest": "^24.8.0", "lerna": "^3.15.0", "lint-staged": "^8.2.1", "prettier": "^1.18.2" }, "workspaces": [ "packages/*" ], "scripts": { "bootstrap": "lerna bootstrap --no-ci", "pretest": "eslint .", "test": "jest --coverage && lerna exec npm test --scope inquirer" }, "husky": { "hooks": { "pre-commit": "lint-staged" } }, "lint-staged": { "*.json": [ "prettier --write", "git add" ], "*.js": [ "eslint --fix", "git add" ] }, "jest": { "coverageDirectory": "./coverage/", "collectCoverage": true }, "dependencies": {} } 複製代碼
看了以後發現這裏的 package.json
並非咱們npm安裝時的json,直接進 packages/inquirer/
裏看到的纔是真的。git
{ "name": "inquirer", "version": "6.4.1", "description": "A collection of common interactive command line user interfaces.", "author": "Simon Boudrias <admin@simonboudrias.com>", "files": [ "lib", "README.md" ], "main": "lib/inquirer.js", "keywords": [ "command", "prompt", "stdin", "cli", "tty", "menu" ], "engines": { "node": ">=6.0.0" }, "devDependencies": { "chai": "^4.2.0", "chalk-pipe": "^2.0.0", "cmdify": "^0.0.4", "mocha": "^5.0.0", "mockery": "^2.1.0", "nyc": "^13.1.0", "sinon": "^7.1.1" }, "scripts": { "test": "nyc mocha test/**/* -r ./test/before", "posttest": "nyc report --reporter=text-lcov > ../../coverage/nyc-report.lcov", "prepublishOnly": "cp ../../README.md .", "postpublish": "rm -f README.md" }, "repository": "SBoudrias/Inquirer.js", "license": "MIT", "dependencies": { "ansi-escapes": "^3.2.0", "chalk": "^2.4.2", "cli-cursor": "^2.1.0", "cli-width": "^2.0.0", "external-editor": "^3.0.3", "figures": "^2.0.0", "lodash": "^4.17.11", "mute-stream": "0.0.7", "run-async": "^2.2.0", "rxjs": "^6.4.0", "string-width": "^2.1.0", "strip-ansi": "^5.1.0", "through": "^2.3.6" } } 複製代碼
接着看到 "main": "lib/inquirer.js"
,那麼咱們就從這裏看起。github
// lib/inquirer.js 'use strict'; /** * Inquirer.js * A collection of common interactive command line user interfaces. */ var inquirer = module.exports; /** * Client interfaces */ inquirer.prompts = {}; inquirer.Separator = require('./objects/separator'); inquirer.ui = { BottomBar: require('./ui/bottom-bar'), Prompt: require('./ui/prompt') }; /** * Create a new self-contained prompt module. */ inquirer.createPromptModule = function(opt) { var promptModule = function(questions) { var ui = new inquirer.ui.Prompt(promptModule.prompts, opt); var promise = ui.run(questions); // Monkey patch the UI on the promise object so // that it remains publicly accessible. promise.ui = ui; return promise; }; promptModule.prompts = {}; /** * Register a prompt type * @param {String} name Prompt type name * @param {Function} prompt Prompt constructor * @return {inquirer} */ promptModule.registerPrompt = function(name, prompt) { promptModule.prompts[name] = prompt; return this; }; /** * Register the defaults provider prompts */ promptModule.restoreDefaultPrompts = function() { this.registerPrompt('list', require('./prompts/list')); this.registerPrompt('input', require('./prompts/input')); this.registerPrompt('number', require('./prompts/number')); this.registerPrompt('confirm', require('./prompts/confirm')); this.registerPrompt('rawlist', require('./prompts/rawlist')); this.registerPrompt('expand', require('./prompts/expand')); this.registerPrompt('checkbox', require('./prompts/checkbox')); this.registerPrompt('password', require('./prompts/password')); this.registerPrompt('editor', require('./prompts/editor')); }; promptModule.restoreDefaultPrompts(); return promptModule; }; /** * Public CLI helper interface * @param {Array|Object|Rx.Observable} questions - Questions settings array * @param {Function} cb - Callback being passed the user answers * @return {inquirer.ui.Prompt} */ inquirer.prompt = inquirer.createPromptModule(); // Expose helper functions on the top level for easiest usage by common users inquirer.registerPrompt = function(name, prompt) { inquirer.prompt.registerPrompt(name, prompt); }; inquirer.restoreDefaultPrompts = function() { inquirer.prompt.restoreDefaultPrompts(); }; 複製代碼
而後咱們從用法上 inquirer.prompt(questions)
去看,一步步往下,找到 inquirer.prompt = inquirer.createPromptModule();
npm
往裏面看 promptModule
知道實際調用等於 promptModule(questions)
。編程
接着應該往下研究 inquirer.ui.Prompt
即 ./ui/prompt.js
了。json
// lib/ui/prompt.js 'use strict'; var _ = require('lodash'); var { defer, empty, from, of } = require('rxjs'); var { concatMap, filter, publish, reduce } = require('rxjs/operators'); var runAsync = require('run-async'); var utils = require('../utils/utils'); var Base = require('./baseUI'); /** * Base interface class other can inherits from */ class PromptUI extends Base { constructor(prompts, opt) { super(opt); this.prompts = prompts; } run(questions) { // Keep global reference to the answers this.answers = {}; // Make sure questions is an array. if (_.isPlainObject(questions)) { questions = [questions]; } // Create an observable, unless we received one as parameter. // Note: As this is a public interface, we cannot do an instanceof check as we won't // be using the exact same object in memory. var obs = _.isArray(questions) ? from(questions) : questions; this.process = obs.pipe( concatMap(this.processQuestion.bind(this)), publish() // Creates a hot Observable. It prevents duplicating prompts. ); this.process.connect(); return this.process .pipe( reduce((answers, answer) => { _.set(this.answers, answer.name, answer.answer); return this.answers; }, {}) ) .toPromise(Promise) .then(this.onCompletion.bind(this)); } /** * Once all prompt are over */ onCompletion() { this.close(); return this.answers; } processQuestion(question) { question = _.clone(question); return defer(() => { var obs = of(question); return obs.pipe( concatMap(this.setDefaultType.bind(this)), concatMap(this.filterIfRunnable.bind(this)), concatMap(() => utils.fetchAsyncQuestionProperty(question, 'message', this.answers) ), concatMap(() => utils.fetchAsyncQuestionProperty(question, 'default', this.answers) ), concatMap(() => utils.fetchAsyncQuestionProperty(question, 'choices', this.answers) ), concatMap(this.fetchAnswer.bind(this)) ); }); } fetchAnswer(question) { var Prompt = this.prompts[question.type]; this.activePrompt = new Prompt(question, this.rl, this.answers); return defer(() => from( this.activePrompt.run().then(answer => ({ name: question.name, answer: answer })) ) ); } setDefaultType(question) { // Default type to input if (!this.prompts[question.type]) { question.type = 'input'; } return defer(() => of(question)); } filterIfRunnable(question) { if (question.when === false) { return empty(); } if (!_.isFunction(question.when)) { return of(question); } var answers = this.answers; return defer(() => from( runAsync(question.when)(answers).then(shouldRun => { if (shouldRun) { return question; } }) ).pipe(filter(val => val != null)) ); } } module.exports = PromptUI; 複製代碼
沒看出來有操做標準輸入輸出的代碼,大膽猜想都是在父類 Base
中完成基本 stdin
和 stdout
的操做(通常來講確實要這樣😉)。
// lib/ui/baseUI.js 'use strict'; var _ = require('lodash'); var MuteStream = require('mute-stream'); var readline = require('readline'); /** * Base interface class other can inherits from */ class UI { constructor(opt) { // Instantiate the Readline interface // @Note: Don't reassign if already present (allow test to override the Stream) if (!this.rl) { this.rl = readline.createInterface(setupReadlineOptions(opt)); } this.rl.resume(); this.onForceClose = this.onForceClose.bind(this); // Make sure new prompt start on a newline when closing process.on('exit', this.onForceClose); // Terminate process on SIGINT (which will call process.on('exit') in return) this.rl.on('SIGINT', this.onForceClose); } /** * Handle the ^C exit * @return {null} */ onForceClose() { this.close(); process.kill(process.pid, 'SIGINT'); console.log(''); } /** * Close the interface and cleanup listeners */ close() { // Remove events listeners this.rl.removeListener('SIGINT', this.onForceClose); process.removeListener('exit', this.onForceClose); this.rl.output.unmute(); if (this.activePrompt && typeof this.activePrompt.close === 'function') { this.activePrompt.close(); } // Close the readline this.rl.output.end(); this.rl.pause(); this.rl.close(); } } function setupReadlineOptions(opt) { opt = opt || {}; // Default `input` to stdin var input = opt.input || process.stdin; // Add mute capabilities to the output var ms = new MuteStream(); ms.pipe(opt.output || process.stdout); var output = ms; return _.extend( { terminal: true, input: input, output: output }, _.omit(opt, ['input', 'output']) ); } module.exports = UI; 複製代碼
這裏能夠看到用了兩個庫,一個是第三方的 mute-stream
,一個是 node
官方的 readline
。readline
文檔安排一下先:英文文檔 中文文檔
簡單來講, readline
就是作可讀流可寫流的操做庫, mute-stream
就是作可寫流的靜默寫操做庫。
從 baseUI.js
代碼來看,它主要作了如下幾件事情:
constructor
建立 readline
的實例,恢復 input
流,監聽進程退出事件;onForceClose
時,先調用了 close
函數,最終關閉進程關閉;close
函數,先把事件的監聽移除(防止內存泄漏等),再把 output
流解除靜默輸出狀態;而後對活躍的 prompt
進行關閉,再對 readline
結束並關閉;咱們這裏先無論控制檯中的交互如何作,先把上面的 baseUI.js
作的事情有個大概的認識先。
回過頭來,看看 inquirer.js
裏關於 prompt
的關鍵代碼 var promise = ui.run(questions);
,整個問答交互的核心應該就是在這個 run
函數裏面,咱們來看看。
可是這裏又涉及到另外一個庫 rxjs
。
這裏引用一句話描述 rxjs
:
RxJS 是一套處理異步編程的 API 庫。
因爲篇幅有限,這裏就先不展開說了。有興趣的能夠先查查這個庫的一些資料。
注意下這行代碼: var obs = _.isArray(questions) ? from(questions) : questions;
這裏的意思實際上是,若是 questions
是一個數組的話,就把它們轉換成 observable
。
run
函數處理過程以下流程圖:
fetchAnswer
這個函數,發現有點意思的是:對於每種不一樣詢問類型,都是直接
new
出不一樣的插件,而後所有所有交給插件內部完成,而後只接受返回結果。
這樣作的好處就是可以很是容易地擴展不一樣的類型,甚至是作定製化等,都很是的方便。
接下來咱們要繼續分析的話就要到插件內部去看了。
接下來咱們來分析裏面的插件,就以一個舉例便可。這裏以 list.js
作例子。
老規矩,上代碼。
// lib/prompts/list.js 'use strict'; /** * `list` type prompt */ var _ = require('lodash'); var chalk = require('chalk'); var figures = require('figures'); var cliCursor = require('cli-cursor'); var runAsync = require('run-async'); var { flatMap, map, take, takeUntil } = require('rxjs/operators'); var Base = require('./base'); var observe = require('../utils/events'); var Paginator = require('../utils/paginator'); class ListPrompt extends Base { constructor(questions, rl, answers) { super(questions, rl, answers); if (!this.opt.choices) { this.throwParamError('choices'); } this.firstRender = true; this.selected = 0; var def = this.opt.default; // If def is a Number, then use as index. Otherwise, check for value. if (_.isNumber(def) && def >= 0 && def < this.opt.choices.realLength) { this.selected = def; } else if (!_.isNumber(def) && def != null) { let index = _.findIndex(this.opt.choices.realChoices, ({ value }) => value === def); this.selected = Math.max(index, 0); } // Make sure no default is set (so it won't be printed) this.opt.default = null; this.paginator = new Paginator(this.screen); } /** * Start the Inquiry session * @param {Function} cb Callback when prompt is done * @return {this} */ _run(cb) { this.done = cb; var self = this; var events = observe(this.rl); events.normalizedUpKey.pipe(takeUntil(events.line)).forEach(this.onUpKey.bind(this)); events.normalizedDownKey .pipe(takeUntil(events.line)) .forEach(this.onDownKey.bind(this)); events.numberKey.pipe(takeUntil(events.line)).forEach(this.onNumberKey.bind(this)); events.line .pipe( take(1), map(this.getCurrentValue.bind(this)), flatMap(value => runAsync(self.opt.filter)(value).catch(err => err)) ) .forEach(this.onSubmit.bind(this)); // Init the prompt cliCursor.hide(); this.render(); return this; } /** * Render the prompt to screen * @return {ListPrompt} self */ render() { // Render question var message = this.getQuestion(); if (this.firstRender) { message += chalk.dim('(Use arrow keys)'); } // Render choices or answer depending on the state if (this.status === 'answered') { message += chalk.cyan(this.opt.choices.getChoice(this.selected).short); } else { var choicesStr = listRender(this.opt.choices, this.selected); var indexPosition = this.opt.choices.indexOf( this.opt.choices.getChoice(this.selected) ); message += '\n' + this.paginator.paginate(choicesStr, indexPosition, this.opt.pageSize); } this.firstRender = false; this.screen.render(message); } /** * When user press `enter` key */ onSubmit(value) { this.status = 'answered'; // Rerender prompt this.render(); this.screen.done(); cliCursor.show(); this.done(value); } getCurrentValue() { return this.opt.choices.getChoice(this.selected).value; } /** * When user press a key */ onUpKey() { var len = this.opt.choices.realLength; this.selected = this.selected > 0 ? this.selected - 1 : len - 1; this.render(); } onDownKey() { var len = this.opt.choices.realLength; this.selected = this.selected < len - 1 ? this.selected + 1 : 0; this.render(); } onNumberKey(input) { if (input <= this.opt.choices.realLength) { this.selected = input - 1; } this.render(); } } /** * Function for rendering list choices * @param {Number} pointer Position of the pointer * @return {String} Rendered content */ function listRender(choices, pointer) { var output = ''; var separatorOffset = 0; choices.forEach((choice, i) => { if (choice.type === 'separator') { separatorOffset++; output += ' ' + choice + '\n'; return; } if (choice.disabled) { separatorOffset++; output += ' - ' + choice.name; output += ' (' + (_.isString(choice.disabled) ? choice.disabled : 'Disabled') + ')'; output += '\n'; return; } var isSelected = i - separatorOffset === pointer; var line = (isSelected ? figures.pointer + ' ' : ' ') + choice.name; if (isSelected) { line = chalk.cyan(line); } output += line + ' \n'; }); return output.replace(/\n$/, ''); } module.exports = ListPrompt; 複製代碼
留意一下 lib/ui/prompt.js
裏的 this.activePrompt.run()
,在 ListPrompt
並無聲明,應該是來自父類方法,上代碼看看。
// lib/prompts/base.js 'use strict'; /** * Base prompt implementation * Should be extended by prompt types. */ var _ = require('lodash'); var chalk = require('chalk'); var runAsync = require('run-async'); var { filter, flatMap, share, take, takeUntil } = require('rxjs/operators'); var Choices = require('../objects/choices'); var ScreenManager = require('../utils/screen-manager'); class Prompt { constructor(question, rl, answers) { // Setup instance defaults property _.assign(this, { answers: answers, status: 'pending' }); // Set defaults prompt options this.opt = _.defaults(_.clone(question), { validate: () => true, filter: val => val, when: () => true, suffix: '', prefix: chalk.green('?') }); // Make sure name is present if (!this.opt.name) { this.throwParamError('name'); } // Set default message if no message defined if (!this.opt.message) { this.opt.message = this.opt.name + ':'; } // Normalize choices if (Array.isArray(this.opt.choices)) { this.opt.choices = new Choices(this.opt.choices, answers); } this.rl = rl; this.screen = new ScreenManager(this.rl); } /** * Start the Inquiry session and manage output value filtering * @return {Promise} */ run() { return new Promise(resolve => { this._run(value => resolve(value)); }); } // Default noop (this one should be overwritten in prompts) _run(cb) { cb(); } /** * Throw an error telling a required parameter is missing * @param {String} name Name of the missing param * @return {Throw Error} */ throwParamError(name) { throw new Error('You must provide a `' + name + '` parameter'); } /** * Called when the UI closes. Override to do any specific cleanup necessary */ close() { this.screen.releaseCursor(); } /** * Run the provided validation method each time a submit event occur. * @param {Rx.Observable} submit - submit event flow * @return {Object} Object containing two observables: `success` and `error` */ handleSubmitEvents(submit) { var self = this; var validate = runAsync(this.opt.validate); var asyncFilter = runAsync(this.opt.filter); var validation = submit.pipe( flatMap(value => asyncFilter(value, self.answers).then( filteredValue => validate(filteredValue, self.answers).then( isValid => ({ isValid: isValid, value: filteredValue }), err => ({ isValid: err }) ), err => ({ isValid: err }) ) ), share() ); var success = validation.pipe( filter(state => state.isValid === true), take(1) ); var error = validation.pipe( filter(state => state.isValid !== true), takeUntil(success) ); return { success: success, error: error }; } /** * Generate the prompt question string * @return {String} prompt question string */ getQuestion() { var message = this.opt.prefix + ' ' + chalk.bold(this.opt.message) + this.opt.suffix + chalk.reset(' '); // Append the default if available, and if question isn't answered if (this.opt.default != null && this.status !== 'answered') { // If default password is supplied, hide it if (this.opt.type === 'password') { message += chalk.italic.dim('[hidden] '); } else { message += chalk.dim('(' + this.opt.default + ') '); } } return message; } } module.exports = Prompt; 複製代碼
原來 run()
是調用了子類方法 _run()
,那麼咱們接着分析吧。
run()
函數
劃重點!!! render()
函數
當咱們選擇好答案按回車以後,會自動把選項收起來,進行下一個問題,它是如何實現重繪的呢?難道它還能把已經輸出的流數據收回去?🤔
我想,你們都看出來了,奧妙確定在 this.screen.render(message);
的 screen
裏面。
list.js
裏的 screen
並無在該類中進行初始化,那麼必然是在其父類當中。
父類 base
代碼已經在上面貼過了,其中裏面大量用到的 chalk
,簡單地說就是控制檯輸出帶顏色的庫,貼個圖大家立刻就懂了。
父類的構造函數中,找到關鍵代碼 this.screen = new ScreenManager(this.rl);
而 ScreenManager
來自 ../utils/screen-manager
。老規矩走起。
// lib/utils/screen-manage.js 'use strict'; var _ = require('lodash'); var util = require('./readline'); var cliWidth = require('cli-width'); var stripAnsi = require('strip-ansi'); var stringWidth = require('string-width'); function height(content) { return content.split('\n').length; } function lastLine(content) { return _.last(content.split('\n')); } class ScreenManager { constructor(rl) { // These variables are keeping information to allow correct prompt re-rendering this.height = 0; this.extraLinesUnderPrompt = 0; this.rl = rl; } render(content, bottomContent) { this.rl.output.unmute(); this.clean(this.extraLinesUnderPrompt); /** * Write message to screen and setPrompt to control backspace */ var promptLine = lastLine(content); var rawPromptLine = stripAnsi(promptLine); // Remove the rl.line from our prompt. We can't rely on the content of // rl.line (mainly because of the password prompt), so just rely on it's // length. var prompt = rawPromptLine; if (this.rl.line.length) { prompt = prompt.slice(0, -this.rl.line.length); } this.rl.setPrompt(prompt); // SetPrompt will change cursor position, now we can get correct value var cursorPos = this.rl._getCursorPos(); var width = this.normalizedCliWidth(); content = this.forceLineReturn(content, width); if (bottomContent) { bottomContent = this.forceLineReturn(bottomContent, width); } // Manually insert an extra line if we're at the end of the line. // This prevent the cursor from appearing at the beginning of the // current line. if (rawPromptLine.length % width === 0) { content += '\n'; } var fullContent = content + (bottomContent ? '\n' + bottomContent : ''); this.rl.output.write(fullContent); /** * Re-adjust the cursor at the correct position. */ // We need to consider parts of the prompt under the cursor as part of the bottom // content in order to correctly cleanup and re-render. var promptLineUpDiff = Math.floor(rawPromptLine.length / width) - cursorPos.rows; var bottomContentHeight = promptLineUpDiff + (bottomContent ? height(bottomContent) : 0); if (bottomContentHeight > 0) { util.up(this.rl, bottomContentHeight); } // Reset cursor at the beginning of the line util.left(this.rl, stringWidth(lastLine(fullContent))); // Adjust cursor on the right if (cursorPos.cols > 0) { util.right(this.rl, cursorPos.cols); } /** * Set up state for next re-rendering */ this.extraLinesUnderPrompt = bottomContentHeight; this.height = height(fullContent); this.rl.output.mute(); } clean(extraLines) { if (extraLines > 0) { util.down(this.rl, extraLines); } util.clearLine(this.rl, this.height); } done() { this.rl.setPrompt(''); this.rl.output.unmute(); this.rl.output.write('\n'); } releaseCursor() { if (this.extraLinesUnderPrompt > 0) { util.down(this.rl, this.extraLinesUnderPrompt); } } normalizedCliWidth() { var width = cliWidth({ defaultWidth: 80, output: this.rl.output }); return width; } breakLines(lines, width) { // Break lines who're longer than the cli width so we can normalize the natural line // returns behavior across terminals. width = width || this.normalizedCliWidth(); var regex = new RegExp('(?:(?:\\033[[0-9;]*m)*.?){1,' + width + '}', 'g'); return lines.map(line => { var chunk = line.match(regex); // Last match is always empty chunk.pop(); return chunk || ''; }); } forceLineReturn(content, width) { width = width || this.normalizedCliWidth(); return _.flatten(this.breakLines(content.split('\n'), width)).join('\n'); } } module.exports = ScreenManager; 複製代碼
render
流程圖走一個。
clean()
函數。追到
lib/utils/readline.js
發現這行代碼:
rl.output.write(ansiEscapes.eraseLines(len));
使用了
ansi-escapes
庫。
發現這個庫的代碼好短,感受要解開了!
'use strict'; const x = module.exports; const ESC = '\u001B['; const OSC = '\u001B]'; const BEL = '\u0007'; const SEP = ';'; const isTerminalApp = process.env.TERM_PROGRAM === 'Apple_Terminal'; const fs = require('fs'); x.cursorTo = (x, y) => { if (typeof x !== 'number') { throw new TypeError('The `x` argument is required'); } if (typeof y !== 'number') { return ESC + (x + 1) + 'G'; } return ESC + (y + 1) + ';' + (x + 1) + 'H'; }; x.cursorMove = (x, y) => { if (typeof x !== 'number') { throw new TypeError('The `x` argument is required'); } let ret = ''; if (x < 0) { ret += ESC + (-x) + 'D'; } else if (x > 0) { ret += ESC + x + 'C'; } if (y < 0) { ret += ESC + (-y) + 'A'; } else if (y > 0) { ret += ESC + y + 'B'; } return ret; }; x.cursorUp = count => ESC + (typeof count === 'number' ? count : 1) + 'A'; x.cursorDown = count => ESC + (typeof count === 'number' ? count : 1) + 'B'; x.cursorForward = count => ESC + (typeof count === 'number' ? count : 1) + 'C'; x.cursorBackward = count => ESC + (typeof count === 'number' ? count : 1) + 'D'; x.cursorLeft = ESC + 'G'; x.cursorSavePosition = ESC + (isTerminalApp ? '7' : 's'); x.cursorRestorePosition = ESC + (isTerminalApp ? '8' : 'u'); x.cursorGetPosition = ESC + '6n'; x.cursorNextLine = ESC + 'E'; x.cursorPrevLine = ESC + 'F'; x.cursorHide = ESC + '?25l'; x.cursorShow = ESC + '?25h'; x.eraseLines = count => { let clear = ''; for (let i = 0; i < count; i++) { clear += x.eraseLine + (i < count - 1 ? x.cursorUp() : ''); } if (count) { clear += x.cursorLeft; } // fs.writeFileSync('log.txt', JSON.stringify(clear), {flag: 'a'}); return clear; }; x.eraseEndLine = ESC + 'K'; x.eraseStartLine = ESC + '1K'; x.eraseLine = ESC + '2K'; x.eraseDown = ESC + 'J'; x.eraseUp = ESC + '1J'; x.eraseScreen = ESC + '2J'; x.scrollUp = ESC + 'S'; x.scrollDown = ESC + 'T'; x.clearScreen = '\u001Bc'; x.clearTerminal = process.platform === 'win32' ? `${x.eraseScreen}${ESC}0f` : // 1. Erases the screen (Only done in case `2` is not supported) // 2. Erases the whole screen including scrollback buffer // 3. Moves cursor to the top-left position // More info: https://www.real-world-systems.com/docs/ANSIcode.html `${x.eraseScreen}${ESC}3J${ESC}H`; x.beep = BEL; x.link = (text, url) => { return [ OSC, '8', SEP, SEP, url, BEL, text, OSC, '8', SEP, SEP, BEL ].join(''); }; x.image = (buf, opts) => { opts = opts || {}; let ret = OSC + '1337;File=inline=1'; if (opts.width) { ret += `;width=${opts.width}`; } if (opts.height) { ret += `;height=${opts.height}`; } if (opts.preserveAspectRatio === false) { ret += ';preserveAspectRatio=0'; } return ret + ':' + buf.toString('base64') + BEL; }; x.iTerm = {}; x.iTerm.setCwd = cwd => OSC + '50;CurrentDir=' + (cwd || process.cwd()) + BEL; 複製代碼
其實有經驗的同窗一眼就看出來是怎麼回事了:
就是利用ANSI控制碼控制終端操做。能夠看看經常使用ANSI控制碼。
好比說:
我先輸出內容: abc\n123\n666\nXXX
而後我在一秒後再輸出:\u001B[2K\u001B[1A\u001B[2K\u001B[1A\u001B[2K\u001B[1A\u001B[2Kcontent
最終輸出結果就是:
(一開始)
abc
123
666
XXX
(一秒後)
abc
content
簡單地描述這兩個控制碼:\u001B[2K
:清除整行,光標不動;\u001B[nA
:光標上移n行;
通過控制碼的組合和邏輯的控制,便可實現 inquirer.js
的交互功能。
草草結束,文章太長寫起來有點卡了。
Github 倉庫地址:github.com/scott-leung…
原本想本身實現一個簡單的 inquirer.js
的,可是時間緣由,後面有時間再說吧☺️。
參考文章:
[1] RxJS v6 學習指南
[2] 經常使用ANSI控制碼表
歡迎轉載,轉載時請標註來源出處。Scott Leung(響螢)