如何快速学会脚手架开发?
先回答以下两个问题
问题1:为什么要用Node.js开发脚手架?
- JavaScript和TypeScript强大的语法特性
- Node.js具有强大的生态能够支持CLI的快速开发 内置库,如:fs、path、os、child_process……
- Node.js具有强大的npm和yarn等包管理系统,可以快速完成CLI的发布和更新 创建,npm init 开发,npm scripts 发布,npm publish 应用,npm instal
问题2:如何快速学会脚手架开发?
- 找到脚手架的使用场景,树立应用脚手架的信心
- 学习基础知识:学习Shell、Bash、CLI等操作系统的基础知识,牢固地掌握脚手架开发的底层原理
- 掌握Node开发:根据脚手架开发需求,学习Node.js内置库、常用库、脚手架框架及各种命令行特有能力的开发方法(键盘输入、键盘监听、文本颜色、命令行交互等) 开发提效工具:将学到的知识进行综合应用,开发脚手架工具,解决实际项目开发过程中的具体问题
什么是Bash和CLI
Bash和Shell是什么?它们有什么用?
什么是Shell?
Shell是计算机提供给用户与其他程序进行交互的接口
Shell是一个命令解释器,当你输入命令后,由Shell进行解释后交给操作系统内核(OSKernel进行处理)
图形操作系统是Shell吗?是的!图形操作系统属于CUI Shell
什么是Bash?
简单地说:Bash是一种程序,它的职责是用来进行人机交互的
Bash和其他程序最大的区别在于,它不是用来完成特定任务(如计算器、文件管理等),我们通过bash shell来执行程序
Bash有什么用?
绝大部分人都习惯使用可视化的图形界面操作系统,但是Bash使用了一种与图形界面完全相反的方案:通过纯文本的控制台进行控制,它的主要交互方式通过键盘输入文本,文字反馈来实现人机交互
那么有一个关键的问题:在GUI(图形界面系统)发展如火如荼的今天,Bash过时了吗?不!恰恰相反Bash在开发领域应用越来越广泛
Bash最大的优势就是简单易用,虽然它的显示效果不如GUI(图形界面系统),但一旦熟练后其操作效率远远大于GUI(图形界面系统)!
什么是CLI?
命令行界面(CLI)是一种基于文本界面(类似:MacOS终端、Windows cmd.exe),用于运行程序
CLI接受键盘输入,在命令符号提示处输入命令,然后由计算机执行并返回结果
今天大部分操作系统都会以GUI作为基础,但是基于Unix的系统都会同时提供CLI和GUI
总结
Shell是操作系统提供的接口程序,用于接受用户输入的命令,交给操作系统内核执行并接受响应结果
Bash是Shell的一个实现,用于执行用户输入的命令
CLI是Bash的运行环境,CLI接收用户键盘输入,交给Bash执行,并将程序处理结果以文本形式进行显示
从使用角度理解什么是脚手架?
脚手架简介
脚手架本质是一个操作系统的客户端,它通过命令行执行比如:
vue create vue-test-app
上面这条命令由3个部分组成:
主命令:vue
command:create
command的param:vue-test-app
它表示创建一个vue项目,项目的名称为vue-test-app,以上是一个较为简单的脚手架命令,但实际场景往往更加复杂,比如:
当前目录已经有了文件了,我们需要覆盖当前目录下的文件,强制进行安装vue项目,此时我们就可以输入:
vue create vue-test-app --force
这里的--force 叫做option(选项的意思),用来辅助脚手架确认在特定场景下用户的选择(可以理解为配置)
还有一种场景:
通过“vue create”创建项目时,会自动执行npm install 帮用户安装依赖,如果我们希望使用淘宝源来安装,可以输入命令:
vue create vue-test-app --force -r https://registry.npm.taobao.org
这里的-r也叫做option,它与--force不同的是它使用-,并且使用简写,这里的-r也可以更换成--registry,有的人可能要问了,为什么我会知道这个命令,其实我们输入下面的命令就可以看到vue create支持所有的options:
vue create --help
-r https://registry.npm.taobao.org 后面的 https://registry.npm.taobao.org 成为option的param,其实--force可以理解为:--force true ,简写为:--force或-f
脚手的执行原理
- 1、在终端输入命令 vue create vue-test-app
- 2、终端解析出vue命令
- 3、终端在环境变量中找到vue命令
- 4、终端根据vue命令链接到实际文件vue.js
- 5、终端利用node执行vue.js
- 6、vue.js解析command/options
- 7、vue.js执行command
- 8、执行完毕,退出执行
从应用的角度看如何开发一个脚手架
这里以vue-cli为例
- 1、开发npm项目,该项目中应包含一个bin/vue.js文件,并将这个项目发布到npm
- 2、将npm项目安装到node的lib/node_modules
- 3、在node的bin目录下配置vue软连接指向lib/node_modules/@vue/cli/bin/vue.js 这样我们在执行vue命令的时候就可以找到vue.js进行执
还有很多疑问需要解答
1、为什么全局安装@vue/cli后会添加的命令为vue? npm install -g @vue/cli
2、全局安装@vue/cli时发生了什么?
3、为什么vue指向一个js文件,我们却可以直接通过vue命令直接去执行它?
4、扩展一下,两种写法的区别 #!/usr/bin/env node #!/usr/bin/node
第一种是在环境变量中查找node (环境变量中找寻node在任何机器都可以使用)
第二种是直接执行/usr/bin/目录下的node (缺点:固定路径,换机器极大可能无法使用)
以上问题在下面都可以得到解答,请耐心看下去
脚手架执行全过程
- 脚手架创建
- npm init
- 脚手架开发
- 分包
- 参数解析
- 脚手架调试
- npm link
- 脚手架发布
- npm publish
框架搭建脚手架
为什么需要脚手架框架?
提升脚手架开发效率、大幅度提升脚手架命令创建、修改的速度
简化脚手架开发过程,大幅度提升代码的可读性和可维护性
常用脚手架框架
yargs 周下载量6000w+ 案例:gulp-cli
commander周下载量8000w+ 案例: vue-cli webpack-cli create-react-app
oclif 脚手架生成器
脚手架开发流程详解
- 开发流程
- 创建npm项目
- 创建脚手架入口文件,最上方添加:
- #!/usr/bin/env node 意思是从当前电脑的环境变量中寻找node
- 配置package.json,添加bin属性
- 编写脚手架代码
- 将脚手架发布到npm
- 使用流程
- 安装脚手架
- npm install -g your-own-cli
- 使用脚手架
- your-own-cli
- 脚手架开发难点解析
- 分包:如何将将复杂的系统拆分为若干个模块?
- 命令注册:
- vue create --name VueTest
- vue add
- vue invoke
- 参数解析:
- vue command [options] <params> //命令
- vue对应的是vue命令
- command对应的是create
- [options]对应的是--name
- <params>对应的是VueTest
- options全称:--version、--help
- options简写:-v、-h
- 带params的options:--path/Users/sam/Desktop/vue-test
- 帮助文档:
- global help
- Usage
- Options
- Commands
- 示例:vue的帮助信息:
- Usage: vue <command> [options]
- Options:
- -V, --version output the version number
- -h, --help output usage information
- Commands:
- init generate a new project from a template
- list list available official templates
- build prototype a new project
- create (for v3 warning only)
- help [cmd] display help for [cmd]
- 还有很多,比如:
- 命令行交互
- 日志打印
- 命令行文字变色
- 网络通信:HTTP/WebSocket
- 文件处理
- 等等……
开始第一个脚手架
1、创建一个文件夹
2、npm init -y 进行初始化
3、在创建的文件夹中新增一个index.js文件
4、创建一个文件夹bin,将index.js文件拖入bin文件夹中
5、在index.js中最上方添加#!/usr/bin/env node 找到node所在文件路径
6、在packag.json中配置bin
{
"name": "bzw-cli",
"version": "1.0.0",
"description": "",
"main": "index.js",
"bin": {
"bzw-cli": "bin/index.js" //这里是最重要的
//表示当前根路径下bin文件夹中的index.js文件
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
7、发布到npm上,npm login 进行登录
npm install -g nrm安装
nrm use npm 切换到官方源
nrm use taobao发布完成后切换到淘宝源
注意如果遇到以下错误,执行npm config set registry https://registry.npmjs.org
npm ERR! code E301
npm ERR! Registry returned 301 for POST on https://registry.npm.taobao.org/-/v1/login
npm ERR! A complete log of this run can be found in:
npm ERR! C:\Users\yangbin\AppData\Roaming\npm-cache\_logs\2023-03-04T08_50_44_889Z-debug.log
然后重新npm login 就会让你输入username 和password了
在输入password的时候鼠标指针会被隐藏,不用害怕,直接输入完然后回车就行了
输入完邮箱之后会让你输入验证码:
Enter one-time password from your authenticator app:
去你邮箱找验证码输入进去就行了
8、输入发布命令 npm publish
当出现了你的脚手架name 和版本号代表成功+ bzw-cli@1.0.0
注意:如果你更改了东西,那么一定要到package.json文件中更改版本号要不然再次更新会报错
安装和使用你创建的脚手架
1、npm i -g 脚手架名称
2、windows系统通过where 脚手架名称 查找脚手架安装路径
mac系统通过which 脚手架名称 查找脚手架安装路径
终端增删文件
md 文件目录名 新增文件夹
rd 文件目录或文件夹名 删除文件夹或目录
cd . > 文件名.js 新增.js文件
分包功能,在本地调试
1、创建两个文件夹
2、两个文件夹中都初始化 npm init -y
3、本地库文件包导出方法或者属性
4、脚手架文件需要在index.js中添加#!/usr/bin/env node
5、到本地库文件的包终端输入npm link添加软链接到本地node上
6、回到脚手架文件的终端输入npm link 被调试包的名称
7、将node_modules包下面的本地库文件的package.json文件中的main改为正确的文件路径
8、在脚手架文件的index.js文件中通过require引入被调试包
9、在脚手架文件夹下的package.json文件中需要添加
"dependencies": {
"bzw-cli":"^1.0.0" 脚手架的名称跟版本号,否则上传到npm无法正常使用分包
}
例如:const lib=require("被调试包名称")之后就可以获取被调试包的所有导出的属性好方法了
通过node执行调试包的index.js文件即可
问题报错:
Error: Cannot find module 'D:\CLI\bzw-cli\node_modules\bzw-cli-lib\index.js'. Please verify that the package.json has a valid "main" entry
解决方案:将node_modules文件夹下面的package.json文件中的main入口文件路径设置为文件路径
例如:lib文件夹下面的index.js文件作为入口文件,那么main:"lib/index.js"就不会报错了
脚手架本地link标准流程
your-cli-dir:代表脚手架文件
your-lib:代表本地库文件
cd your-cli-dir
npm link
链接本地库文件:
cd your-cli-dir
npm link
cd your-cli-dir
npm link your-lib
取消链接本地库文件:
cd your-cli-dir
npm unlink your-lib
cd your-cli-dir
npm unlink your-lib
取消链接本地库报错
npm ERR! code ENOVERSIONS
npm ERR! No valid versions available for bzw-cli
找不到该文件
理解npm link:
npm link your-lib:将当前项目中的node_modules下指定的库文件链接到node全局node_modules下的库文件
npm link:将当前项目链接到node全局node_modules中作为一个库文件,并解析bin配置创建可执行文件
理解npm unlink:
npm unlink:将当前项目从node全局node_modules中移除
npm unlink your-lib:将当前项目中的库文件依赖移除
cli命令注册参数解析
1、首先在分包中导出init方法
module.exports = {
init({ option, param }) {
console.log("执行init流程", option, param);
},
};
2、分包中npm link创建软链接
3、在脚手架中npm link 分包名称 创建链接
4、注册一个命令 bzw-cli init 初始化
#!/usr/bin/env node
const lib = require("bzw-cli-lib");
//注册一个bzw-cli init命令
const argv = require("process").argv;
const command = argv[2];
//解析options参数
const options = argv.slice(3); //获取第三个元素和之后的元素
let [option, param] = options; //解构
if (option.length) {
option = option.replace("--", ""); //将--转化为""
if (command) {
if (lib[command]) {
lib[command]({ option, param }); //将options参数传递到方法中处理
} else {
console.log("无效的命令");
}
} else {
console.log("请输入命令");
}
}
执行node .\index.js init --name vue-test //执行init流程 name vue-test
执行node .\index.js publish //无效的命令
执行node .\index.js //请输入命令
5、//实现参数解析 --version 和init --name
if (command.startsWith("--") || command.startsWith("-")) {
const globalOption = command.replace(/--|-/g, "");
if (globalOption === "version" || globalOption === "V") {
console.log("1.0.0");
}
} else {
console.log("无效命令");
}
执行node .\index.js --version //1.0.0
执行node .\index.js -V //1.0.0
执行node .\index.js V//无效命令
分包发布脚手架
1、cd到脚手架包目录下
2、通过npm unlink 分包名称 解除分包链接
3、cd到分包目录下 通过 npm unlink 解除node全局链接
4、在分包目录下 通过 npm publish 发布到npm上
5、cd到脚手架目录下更改package.json文件中的
"dependencies": {
"bzw-cli-lib":"^1.0.0" //版本号需要和分包版本号同步
}
6、将脚手架node_modules文件夹删除
7、在脚手架目录下执行 npm unlink
8、在脚手架目录的package.json文件中添加
"bin":{
"bzw-cli":"bin/index.js"
},
9、发布脚手架npm publish
yargs脚手架框架
#!/usr/bin/env node
//安装yargs
npm i yargs
//安装dedent
npm i -S dedent
//引入yargs
const yargs = require("yargs/yargs");
//引入解析器
const { hideBin } = require("yargs/helpers");
//引入dedent
const dedent = require("dedent");
//执行node .\bin\index.js --help 打印['--help']
const argv = hideBin(process.argv);
//定义yargs
const cli = yargs(argv);
//调用yargs
cli
//alias代表别名,第一个参数是简称,第二个是全称
.alias("h", "help")
.alias("v", "version")
//wrap可以控制脚手架宽度变化,cli.terminalWidth()最大宽度
.wrap(cli.terminalWidth())
//demandCommand中第一个参数表示最少输入命令个数,第二个参数是提示语
.demandCommand(
1,
"A command is required. Pass --help to see all available commands and options."
)
//usage用来规定用法
.usage("Usage: bzw-cli [command] <options>")
//strict代表严格模式
.strict()
//recommendCommands作用:当输入命令错误,那么会在最后一行提示像似命令
.recommendCommands()
//自定义错误信息
.fail((err, msg) => {
//err表示错误内容
console.log("err", err);
//msg表示提示
console.log("msg", msg);
})
//epilogue表示可以在结尾添加一句话
.epilogue(
dedent` 111
111`
)
//options可以增加全局的多个选项
.options({
debug: {
//选项类型
type: "boolean",
//选项描述
describe: "日志断点",
//选项别名
alias: "d",
hidden: false,
},
})
//option代表单个选项
.option("registry", {
//选项类型
type: "string",
//选项是否隐藏true/false(适用于内部开发)
hidden: false,
//选项描述
describe: "日志记录",
alias: "r",
})
//group可以将不同的command分组
.group(["debug"], "Dev Options")
.group(["registry"], "Serve Options")
//command自定义命令
.command(
"init [name]",
"初始化项目",
(yargs) => {
//init [name] 代表命令格式
//初始化项目 表示命令提示
//(yargs)函数定义执行方法
yargs.option("name", {
//选项类型
type: "string",
//选项描述
describe: "项目名称",
//选项别名
alias: "n",
});
},
(argv) => {
//argv函数代表解析的参数
console.log(argv);
}
)
.command({
//自定义list命令
command: "list",
//命令别名,可以使用ls,la,ll
aliases: ["ls", "la", "ll"],
//选项描述
describe: "List local packages",
//处理方法
builder: (yargs) => {},
//解析参数
handler: (argv) => {
console.log(argv);
},
}).argv;
//argv是Options参数
commander脚手架框架
//安装commander
npm i commander
代码:
#!/usr/bin/env node
//引入commander
const commander = require("commander");
//引入package文件
const pkg = require("../package.json");
//引入脚手架,获取commander单例
// const { program } = commander;
//手动实例化Commander实例
const program = new commander.Command();
//定义version方法
program.version(pkg.version).parse(process.argv);