脚手架是为了保证各施工过程顺利进行而搭设的工作平台。按搭设的位置分为外脚手架、里脚手架;按材料不同可分为木脚手架、竹脚手架、钢管脚手架;按构造形式分为立杆式脚手架、桥式脚手架、门式脚手架、悬吊式脚手架、挂式脚手架、挑式脚手架、爬式脚手架。 ——百度百科
本质上就是一个便利工具,为一些比较特殊或繁琐的工作提供辅助,我们这里需要开发的是一个基于命令行的工具,后文以 cli
代替。
前言
本文作者是vue devui核心代码贡献者之一的小雷同学,感谢他的无私分享!
视频教程
本期配套视频,欢迎观看+三连+分享!
为什么需要开发一个脚手架?
以下为初期组件库协同开发时遇到的问题:
- 组件目录结构不一致
- 扁平化目录
-
src
类型目录
- 组件产出命名不一致
前缀
D
命名无前缀
D
命名小写驼峰命名
大写驼峰命名
xxxService
命名
useXxxService
命名组件入口文件经常冲突
为了解决上述问题,本着为开源社区做贡献,发光发热的时候到了,和项目组织人 kagol
沟通了脚手架方案顺利通过,Prefect
!
TODO
- [x] 创建统一组件结构
- [x] 创建组件库入口文件
技术选型
脚手架 = 命令 + 交互 + 逻辑处理
- 命令
-
commander
插件提供命令注册、参数解析、执行回调
- 交互
inquirer
插件用于命令行的交互(问答)逻辑处理
fs-extra
插件是对 nodejs
文件 Api
的进一步封装,便于使用
kolorist
插件用于输出颜色信息进行友好提示
初始化 cli
step1 创建 cli 目录
mkdir devui-cli // 创建脚手架目录
cd devui-cli // 进入脚手架目录
// 初始化一个 node 项目
npm init
// or
yarn init
第一步先创建一个目录来存放我们即将开发的脚手架,作为一个 nodejs
包,需要我们通过 npm
或者 yarn
初始化包的信息,一律回车即可通过,生成后的目录结构如下图。
image.png
step2 创建入口文件
mkdir src
echo 'console.log("hello devui-cli")' > src/index.js
step3 安装所需依赖
npm i -D commander inquirer fs-extra kolorist
// or
yarn add -D commander inquirer fs-extra kolorist
image.png
开发命令脚本
这里先给大家梳理下 cli
的执行流程:命令行输入 devui-cli
--> 命令行交互 --> 根据不同参数进行不同操作。
这里大家可能要问了,命令行如何识别 devui-cli
的?又是如何执行交互操作的?
这里简单给大家解答一下,命令行里面输入
devui-cli
本质上是执行某一个可执行脚本,那么对应我们 node
包来说就是入口文件 src/index.js
了,所以可以看成是 node src/index.js
,效果是一样的,只不过第一种更为方便与友好一点。那么我们直接执行是否就可以了呢?答案肯定不是的,需要在 package.json
里面配置 bin
属性来标明脚本的一个入口。
准备工作结束,接下来开始正式的 cli
脚本编写。
配置环境解释器
#!/usr/bin/env node
image.png
部分看官可能会疑惑这句话有什么用呢?
答案在这里,若是有使用过
Linux
或者 Unix
的小伙伴们,对于 Shebang
应该不陌生,它是一个符号的名称 #!
。这个符号通常在 Unix
系统的基本中第一行开头中出现,用于指明这个脚本文件的解释程序, #!/usr/bin/env node
目的就是告诉操作系统执行这个脚本的时候,在 /usr/bin
的环配置里找到 node
解释器并执行。
注册命令
配置好环境解释器之后就可以编写我们的命令逻辑了。
首先,先注册下我们需要执行的一些命令以及一些命令参数。
#!/usr/bin/env node
import { Command } from 'commander'
import { onCreate } from './commands/create'
// 创建命令对象
const program = new Command()
// 注册命令、参数、回调
program
// 注册 create 命令
.command('create')
// 添加命令描述
.description('创建一个组件模板或配置文件')
// 添加命令参数 -t | --type <type> ,<type> 表示该参数必填,[type] 表示选填
.option('-t --type <type>', `创建类型,可选值:component, lib-entry`)
// 注册命令回调
.action(onCreate)
// 执行命令行参数解析
program.parse()
创建具体命令目录,方便统一管理。
mkdir src/commands // 命令存放目录
echo 'export function onCreate() { }' > src/commands/create.js // 创建 create 命令文件并导出回调函数
image.pngimage.png
测试脚本命令
我们可以先在 onCreate
里面打印一下我们接受到的参数。
export function onCreate (cmd) {
console.log(cmd)
}
执行一下脚本。
node src/index.js
image.png
报错了!!!我们才刚开始就报错了,是否已经开始崩溃?
稳住别慌,一切在我们的意料之中。这是因为我们编写的是 node
程序,本应该使用 commonjs
简称 CJS
格式,也就是用 require
和 exports
等语法才能正常使用 node xxx.js
进行启动,但是我们使用了新一代的 esmodule
简称 ESM
格式,所以 node
脸盲了!那么有什么办法呢?
解决办法一:将 .js
改成 .mjs
。why? 很明显因为 ESM
和 CJS 的加载方式不同,为了更好区分这两种不同的加载方式,所以创建了 .mjs
的文件类型,旨在 Module javascript
。.mjs
就是表示当前文件用 ESM
的方式进行加载,如果是普通的 .js
文件,则采用 CJS
的方式加载。
解决办法二:通过一些模块打包器进行转换为 node
熟悉的 cjs
格式,然后再进行开发。
这里选择第二种方式,原因是采用打包器我们可以对代码进行其他操作,例如:压缩、转换等。
模块打包器的话这里采用 esbuild
,理由就是:快捷、方便。
npm i -D esbuild
// or
yarn add -D esbuild
安装好后先看下命令行帮助文档。
npx esbuild -h
// or
yarn esbuild -h
执行完后会看到以下帮助信息。
image.png
看过帮助信息后我们加入如下命令:
{
// --bundle 标识打包的入口文件
// --format 转换为目标格式代码
// --platform 目标平台,默认 browser
// --outdir 输出目录
// 开发时实时编译
"dev": "esbuild --bundle ./src/index.js --format=cjs --platform=node --outdir=./lib --watch",
// 打包命令
"build": "esbuild --bundle ./src/index.js --format=cjs --platform=node --outdir=./lib",
// 执行 create 命令,如果有多个命令,可以去掉 create ,使用时再传入
"cli": "node ./lib/index.js create"
}
image.png
执行下 dev
命令,然后重新开一个 shell
再执行 cli
命令。
image.png
yarn cli
// or
npm run cli
image.png
输出了一个 {}
,这是我们打印的 cmd
入参,我们并没有填入任何参数,所以解析后是一个空对象,接下来传入 type
参数再看看。
yarn cli -t component // -t 是 --type 的别名
// or
npm run cli -- -t component // -- 是 npm run 脚本传参时需要加的,类似于参数透传给脚本
image.pngimage.png
现在已经能够正常获取到命令参数了,证明命令注册成功,后面可以继续实现我们的交互逻辑。
完善 create 命令
接下来就是进一步完善我们的命令交互了,以 component
为例,代码如下:
import inquirer from 'inquirer'
import { red } from 'kolorist'
// create type 支持项
const CREATE_TYPES = ['component', 'lib-entry']
// 文档分类
const DOCS_CATEGORIES = ['通用', '导航', '反馈', '数据录入', '数据展示', '布局']
export async function onCreate(cmd = {}) {
let { type } = cmd
// 如果没有在命令参数里带入 type 那么就询问一次
if (!type) {
const result = await inquirer.prompt([
{
// 用于获取后的属性名
name: 'type',
// 交互方式为列表单选
type: 'list',
// 提示信息
message: '(必填)请选择创建类型:',
// 选项列表
choices: CREATE_TYPES,
// 默认值,这里是索引下标
default: 0
}
])
// 赋值 type
type = result.type
}
// 如果获取的类型不在我们支持范围内,那么输出错误提示并重新选择
if (CREATE_TYPES.every((t) => type !== t)) {
console.log(
red(`当前类型仅支持:${CREATE_TYPES.join(', ')},收到不在支持范围内的 "${type}",请重新选择!`)
)
return onCreate()
}
try {
switch (type) {
case 'component':
// 如果是组件,我们还需要收集一些信息
const info = await inquirer.prompt([
{
name: 'name',
type: 'input',
message: '(必填)请输入组件 name ,将用作目录及文件名:',
validate: (value) => {
if (value.trim() === '') {
return '组件 name 是必填项!'
}
return true
}
},
{
name: 'title',
type: 'input',
message: '(必填)请输入组件中文名称,将用作文档列表显示:',
validate: (value) => {
if (value.trim() === '') {
return '组件名称是必填项!'
}
return true
}
},
{
name: 'category',
type: 'list',
message: '(必填)请选择组件分类,将用作文档列表分类:',
choices: DOCS_CATEGORIES,
default: 0
}
])
createComponent(info)
break
case 'lib-entry':
createLibEntry()
break
default:
break
}
} catch (e) {
console.log(red('✖') + e.toString())
process.exit(1)
}
}
function createComponent(info) {
// 输出收集到的组件信息
console.log(info)
}
function createLibEntry() {
console.log('create lib-entry file.')
}
ok,接下来尝试运行一下我们的脚本。
先尝试错误的类型:
yarn cli -t error
// or
npm run cli -- -t error
image.png
按照我们的预想提示了错误信息并让我们重新选择类型。
接下来尝试正确的类型:
yarn cli -t component
// or
npm run cli -- -t component
image.png
因为指定了类型为组件,所以现在需要收集一下即将创建的组件信息。
image.png
按照提示信息一步一步完成输入最终获取到了我们需要的数据,接下来就是模板的生成了。