脚手架开发-Englighten CLI
基本概念
深入理解shell、bash 和 CLI
什么是 Shell? Shell 是一个计算机程序,它作为用户与操作系统之间的接口。简单来说,shell 就是一个接收命令并执行相应操作的程序。你可以把它想象成一个“翻译器”,把用户的命令翻译成操作系统能理解的语言。
Shell 的种类
- bash(Bourne Again SHell)
- sh(Bourne SHell)
- csh(C SHell)
- zsh(Z SHell)
- fish(Friendly Interactive SHell)
不同操作系统上的 Shell
- Windows 在 Windows 系统中,传统的命令行工具是
cmd.exe(简称 cmd)。
- cmd:Windows 的默认命令行工具,提供了基本的命令行功能。
- 例如:
dir(列出目录内容)、cd(切换目录)、del(删除文件)等。
- 例如:
- Linux 在 Linux 系统中,最常见的命令行工具是
bash。
- bash:Linux 的默认 shell,提供了丰富的命令行功能。
- 例如:
ls(列出目录内容)、cd(切换目录)、rm(删除文件)等。
- 例如:
- macOS macOS 早期使用的是
bash,但从macOS Catalina(10.15)开始,默认shell改成了zsh。
命令行界面(CLI)CLI(Command Line Interface)是指命令行界面,即用户通过输入命令来进行操作的界面。CLI 是 shell 的一种表现形式,允许用户通过文本输入命令来与操作系统交互。
总结
Shell是操作系统提供的接口程序,用于接收用户输入的命令,交给操作系统内核执行并接受响应结果,常见的有bash、zsh等。Bash是Shell的一个实现,用于执行用户输入的命令CLI是通过文本输入命令来与操作系统交互的界面。
脚手架的执行原理
脚手架(Scaffold)是一种自动化工具,用于快速生成项目的基本结构和文件。它可以帮助开发者快速搭建一个项目的基础框架,而不需要手动创建每一个文件和目录。 脚手架通常提供一个命令行工具(CLI),用户可以通过命令行来启动脚手架。
当我们执行npm install @vue/cli -g的时候发生了什么?
- 安装过程:
- 执行
npm install @vue/cli -g会全局安装Vue CLI - 全局安装的包会被放置在一个全局的
node_modules目录中,通常是%AppData%\npm\node_modules或者%LocalAppData%\npm\node_modules。
- 执行
- 生成可执行文件:
- Vue CLI 会在安装完成后生成几个可执行文件:
vue.cmd(Windows)vue(Unix-like 系统,如 macOS 和 Linux)vue.ps1(PowerShell 脚本)
- 通常位于
%AppData%\npm或%LocalAppData%\npm
- Vue CLI 会在安装完成后生成几个可执行文件:
当我们执行vue create my-project的时候发生了什么?
vue create my-project --force -r https://registry.npmjs.org上述从左到右依次为:主命令 command command参数 option配置
- 查找可执行文件:
- 当我们执行vue的时候,在macos中相当于执行了
which vue,windows中相当于执行了where vue,用于找到在Node.js的bin目录下的vue(vue.cmd),这个vue是一个软连接 - 这里软连接指向的是
@vue/cli/bin/vue.js
- 当我们执行vue的时候,在macos中相当于执行了
- 执行命令:
vue.cmd文件是一个批处理文件,它会调用全局安装的 Vue CLI,也就是@vue/cli/bin/vue.js。- Vue CLI 会解析命令行参数并执行相应的操作。
- 为什么系统会识别
vue这个命令呢?- 因为在package.json中配置了bin字段,这个字段会告诉npm如何执行这个包。
json{ "name": "@vue/cli", "version": "x.y.z", "bin": { "vue": "bin/vue.js" } } - 为什么
vue.js会被执行呢?- 因为
vue.js的源码第一行是#!/usr/bin/env node,这个是Node.js的shebang,表示这个文件是一个Node.js脚本,然后Node.js会自动执行这个脚本。
- 因为
Node其实就是一个客户端 macOS中,which node我们可以发现他是一个软连接,windows中我们也可以查看node是一个exe文件,我们平时执行node test.js,其实是把test.js中的代码当成一个字符串,传给了node.exe,然后node进行解析,把它当成一个可执行文件进行解析,最后返回结果,我们也可以输入node -e "console.log('hello world')",这样也可以执行
脚手架基础
脚手架开发流程详解
- 初始化项目
npm init -y- 新建
bin目录,并在其中创建一个名为index.js的文件,内容如下:
#!/usr/bin/env node
console.log('welcome my cli- nv----')- 配置package.json
"bin": {
"cli-nv": "bin/index.js"
}- 发布
npm login
npm publish注意事项
- 注意脚手架package.json中的name,name不能重复,否则会报错
- 注意npm源,如果npm源不是官方的,可能会导致发布失败
本地调试方法
- 在你的npm包的上层目录
npm i -g xxx,这样软链会指向你的本地 - 使用
npm link命令,这样软链会指向你的本地 - 分包之后的本地调试
- 假如你的A包依赖B包,首先B包中
npm init -y,然后新建lib/index.js,然后在package.json中添加"main": "lib/index.js",然后npm link,然后A包中npm link B,这样A包中就可以使用B包中的内容了
- 假如你的A包依赖B包,首先B包中
- 在你的npm包的上层目录
脚手架本地link标准流程 链接本地脚手架:
cd you-cli-dir
npm link链接本地库文件:
cd your-lib-dir
npm link
cd your-cli-dir
npm link your-lib取消链接本地库文件:
cd your-lib-dir
npm unlink
cd your-cli-dir
npm unlink your-lib理解npm link:
npm link your-lib:将当前项目中node_modules目录下的your-lib链接到node全局的node_modules目录下的库文件。npm link:将当前项目链接到node全局的node_modules中作为一个库文件,并解析bin配置创建可执行文件
命令注册以及参数解析
可以借助node中的process.argv获取命令行参数,然后进行解析。argv是一个数组,第一个元素是node可执行文件路径,第二个元素是js文件路径,第三个元素开始才是命令行参数。 如果手动去分析后续参数,需要考虑很多组合情况,来区分command和args,我们这里只给出一个简单示例:
#!/usr/bin/env node
const lib = require('liuhuakawaii-cli-lib')
const argv = require('process').argv
console.log(argv, 'argv');
const command = argv[2]
const options = argv.slice(3)
let [option, param] = options
option = option.replace('--', '')
console.log(command, 'command');
console.log(options, 'options');
console.log(param, 'param');
if (lib[command]) {
lib[command](option, param)
} else {
console.log('command not found')
}yargs
基本设置
hideBin:用于移除 Node.js 特定的命令行参数arg:处理后的命令行参数
全局配置
.usage():设置使用说明.demandCommand():要求至少一个命令.strict():严格模式,不允许未知选项.recommendCommands():在输入错误时推荐相似命令.fail():自定义错误处理.alias():为选项设置别名.wrap():设置帮助信息的宽度.epilogue():设置帮助信息的结尾文本
全局选项
.options():定义多个选项.option():定义单个选项.group():将选项分组
代码示例
#!/usr/bin/env node
const yargs = require('yargs')
const { hideBin } = require('yargs/helpers')
const arg = hideBin(process.argv)
const cli =
yargs(arg)
.usage('Usage: liuhuakawaii-cli <command> [options]')
.demandCommand(1, 'A command is required. Pass --help to see all available commands and options.')
.strict()
.recommendCommands()
.fail((msg, err, yargs) => {
console.log(msg)
})
.alias('h', 'help')
.alias('v', 'version')
.wrap(yargs.terminalWidth())
.epilogue('For more information, visit https://github.com/liuhuakawaii/liuhuakawaii-cli')
.options({
debug: {
type: 'boolean',
describe: 'Enable debug mode',
default: false,
alias: 'd',
},
})
.option('registry', {
type: "string",
describe: "Specify npm registry",
default: "https://registry.npmjs.org",
alias: "r"
})
.group(['debug', 'registry'], 'Dev Options:')
.command('init [name]', 'Initialize a new project', (yargs) => {
yargs.option('name', {
type: 'string',
describe: 'Project name',
default: 'my-project',
alias: 'n'
})
}, (argv) => {
console.log(argv);
})
.command({
command: 'list',
describe: 'List all packages',
alias: ['ls', 'l'],
handler: () => {
console.log('List all packages');
}
})
.argvCommander.js
基本设置
new commander.Command():创建 Commander 实例.name():设置程序名称.usage():设置使用说明.version():设置版本信息.option():定义全局选项- 短选项,长选项
- 描述
- 默认值(可选)
命令定义
.command():定义命令.description():设置命令描述.option():定义命令特定选项.action():定义命令执行的回调函数.addCommand():将子命令添加到主程序
独立的可执行命令
executableFile:指定可执行文件isDefault:设置为默认命令.alias():设置命令别名
参数处理
.arguments():定义命令参数.description():设置描述,可以为参数单独设置描述.action():定义参数处理函数.parse():解析命令行参数
代码示例
#!/usr/bin/env node
const commander = require('commander')
const pkg = require('../package.json')
//获取commander的单例
// const { program } = commander
//手动实例化
const program = new commander.Command()
program
.name(Object.keys(pkg.bin)[0])
.usage('<command> [options]')
.version(pkg.version)
.option('-d, --debug', 'output extra debugging', false)
.option('-s, --small', 'small pizza size')
.option('-p, --pizza-type <type>', 'flavour of pizza')
.option('-e, --env <env>', 'environment</env>')
//command 注册命令
const clone = program.command('clone <source> [destination]') //<source>是必填参数,[destination]是可选参数
clone
.description('clone a repository')
.option('-f, --force', '是否强制克隆')
.action((source, destination) => {
console.log('clone from %s to %s', source, destination || 'current directory')
})
//addCommand 注册子命令
const service = new commander.Command('service')
service
.command('start <service-name>')
.description('start service')
.action((serviceName) => {
console.log('start %s service', serviceName)
})
service
.command('stop <service-name>')
.description('stop service')
.action((serviceName) => {
console.log('stop %s service', serviceName)
})
program.addCommand(service)
program
.command('install [name]', 'install package', {
executableFile: 'xxx-cli', //多个脚手架可以串行调用
isDefault: true, //默认执行
})
.alias('i')
program
.arguments('<cmd> [options]')
.description('test command', {
cmd: 'command to run',
options: 'options for command'
})
.action((cmd, options) => {
console.log('cmd: %s, options: %O', cmd, options)
})
program.parse(process.argv)命令行UI显示
chalk
基本语法
chalk 提供了一系列预定义的颜色和样式函数,你可以通过调用这些函数来改变文本的颜色和样式。
const chalk = require('chalk')
console.log(chalk.red('Hello') + ' World' + chalk.blue('!')) //单独调用
console.log(chalk.red('Hello', 'World', '!')) //拼接字符串
console.log(chalk.red.bgYellow.bold('Hello world!')) //链式调用
console.log(chalk.hex('#FF00FF').underline(' rainbow'))
console.log(chalk.rgb(123, 45, 67).inverse('inverse'))
console.log(chalk.keyword('orange')('Yay for orange colored text!')) //通过关键字获取颜色
console.log(chalk.red('hello',chalk.underline('Underlined reddish color'))) //迭代调用源码阅读
自定义import
import ansiStyles from '#ansi-styles'
import supportColors from '#supports-color'
//package.json 配置 指向内置库
"imports":{
"#ansi-styles":"./source/vendor/ansi-styles/index.js",
"#supports-color":{
"node":"./source/vendor/supports-color/index.js",//node
"default":"./source/vendor/supports-color/browser.js" //浏览器
},
}批量生成方法和构造者模式应用 根据ansiStyles配置批量生成构造者方法
for (const [styleName, style] of Object.entries(ansiStyles)) {
styles[styleName] = {
get() {
const builder = createBuilder(this, createStyler(style.open, style.close, this[STYLER]), this[IS_EMPTY])
Obeject.defineProperty(this, styleName, { value: builder })
return builder
}
}
}使用工厂模式生成实例
function createChalk(options){
return chalkFactory(options)
}
const chalk = createChalk()
export default chalk链式调用 简单实现
class Test {
one() {
console.log(1);
return this
}
two() {
console.log(2);
return this
}
}
function createBuilder() {
return new Test()
}
const builder = createBuilder()
builder.one().two() // 1 2ora
基本使用
import ora from 'ora';
// 模拟一个异步任务
function simulateAsyncTask() {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve('任务成功完成');
} else {
reject(new Error('任务失败'));
}
}, 2000);
});
}
// 创建一个加载器实例
const spinner = ora('正在加载...').start();
// 执行异步任务
simulateAsyncTask()
.then((message) => {
spinner.succeed(message);
})
.catch((error) => {
spinner.fail(error.message);
})
.finally(() => {
// 无论成功还是失败,最终都会执行的代码
console.log('任务结束');
});
// 你可以使用其他 API 来定制加载效果
spinner.color = 'yellow'; // 设置加载器的颜色
spinner.text = '正在处理数据...'; // 更新加载器的文本
// 如果你需要暂停和恢复加载器
setTimeout(() => {
spinner.stopAndPersist({
symbol: '🚀', // 自定义符号
text: '任务完成,准备发射!'
});
}, 4000);源码阅读
class私有属性
class Test {
#name = 'liuhuakawaii'
#getName() {
return this.#name
}
getName() {
return this.#getName()
}
}
const test = new Test()
console.log(test.getName()) // liuhuakawaii
console.log(test.#getName()) // 报错
console.log(test.#name) // 报错命令行交互
inquirer
基本使用
创建提示inquirer.prompt 方法用于创建提示。它接受一个数组,数组中的每个对象定义一个提示。
提示类型
input:用于获取用户输入的文本。password:用于获取用户输入的密码,输入内容会被隐藏。list:用于让用户从一组选项中选择一个。checkbox:用于让用户从一组选项中选择多个。confirm:用于获取用户的确认(是/否)。
const inquirer = require('inquirer').default;
//findLastIndex 是 ECMAScript 2022 中引入的一个新方法,如果你的 Node.js 版本不支持这个方法,就会出现这个错误。
Array.prototype.findLastIndex = function (predicate) {
for (let i = this.length - 1; i >= 0; i--) {
if (predicate(this[i], i, this)) {
return i;
}
}
return -1;
};
inquirer
.prompt([
{
type: 'input',
name: 'name',
message: 'What is your name?',
default: 'none',
validate: function (value) {
var pass = value.match(
/^[a-zA-Z0-9_]+$/
);
if (pass) {
return true;
}
return 'Please enter a valid name';
}
},
{
type: 'password',
name: 'password',
message: 'Enter your password',
validate: function (value) {
if (value.length < 8) {
return 'Password should be at least 8 characters';
}
return true;
}
},
{
type: 'checkbox',
name: 'provider',
message: 'Select a provider',
choices: [
{ name: 'Google', value: 'google' },
{ name: 'Facebook', value: 'facebook' },
{ name: 'Github', value: 'github' },
{ name: 'Twitter', value: 'twitter' },
{ name: 'WeChat', value: 'wechat' },
{ name: 'QQ', value: 'qq' },
{ name: 'Weibo', value: 'weibo' },
]
},
{
type: 'confirm',
name: 'confirm',
message: 'Do you want to create a new account?',
default: true
},
]).then(answers => {
console.log(answers);
})多package项目管理
npm workspaces
创建项目
npm init --wworkspaces a
npm init -w a项目创建后,会在顶级目录下创建node_modules并存储所有的package的依赖。
node_modules
|—— @liuhuakawaiitestgroup
| |—— test-a -->../../a
| |__ test-cli -->../../cli安装依赖或者更新依赖
为某个特定的workspace安装依赖
npm install chalk --workspace test-a
npm install @liuhuakawaiitestgroup/test-cli -w cli当修改了package的内容后(如:package.json中的name)
npm install --workspace
npm install --ws发布项目
npm publish --ws脚手架开发技术沉淀
esm不能使用filename和dirname ESM无法像CJS那样使用__filename和__dirname,因为ESM是运行在浏览器的,浏览器没有这个概念。所以需要通过下面的方式获取
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);优先加载本地脚手架版本 使用import-local优先加载本地版本
import importLocal from 'import-local';
if (importLocal(__filename)) {
log.info('use local version');
}else {
entry(process.argv.slice(2))
}脚手架hook注册
import { program } from 'commander';
program
.hook('preAction', () => {
preAction();
})监听option特定选项
program.on('option:debug', () => {
if (program.opts().debug) {
log.verbos('debug', 'launch debug mode');
}
})未注册命令监听
program.on('command:*', (unknownCommand) => {
log.error('未知命令', unknownCommand[0]);
})ESM不支持json文件
import pkg from '../package.json' assert { type: 'json' };镜像源问题
如果使用的是淘宝镜像源(npmmirror.com)。要发布包到 NPM,我们需要使用 NPM 官方源。
# 1. 首先切换到 npm 官方源
npm config set registry https://registry.npmjs.org/
# 2. 验证当前源
npm config get registry
# 应该显示 https://registry.npmjs.org/
# 3. 现在重新登录
npm login
# 4. 这时应该会提示输入用户名、密码和邮箱,而不是打开浏览器完成发布后,如果你想切回淘宝镜像(因为安装包会更快),可以:
npm config set registry https://registry.npmmirror.com/你可以使用 nrm 来管理 npm 源,这样切换起来会更方便:
# 安装 nrm
npm install -g nrm
# 查看可用的源
nrm ls
# 切换到 npm 官方源
nrm use npm
# 切换到淘宝源
nrm use taobao