开发自己的脚手架工具
尤雨溪觉得 webpack 不好用,于是自己写了个 vite,那么本篇文章就来说说,如果我们也来自己写个脚手架玩,要怎么一步步去实现。
初体验
自定义命令
创建项目 qycli,执行 npm init -y
生成 package.json。然后新建 lib\index.js,我们想实现在命令行输入 qycli
,就能执行 ib\index.js 的测试代码跟 jym 打个招呼,输出 'Hello jym',为了告诉 npm 到底如何执行 ./lib/index.js,我们在第一行添加了 #!/usr/bin/env node
,来说明是去用户的环境变量中的 Path 里找到 node 来执行:
#!/usr/bin/env node
// lib\index.js
console.log('Hello jym')
在 package.json 添加 bin
属性,告诉 npm 命令 qycli
要执行的文件是 ./lib/index.js:
{
"bin": {
"qycli": "./lib/index.js"
}
}
然后在命令行执行 npm link
:
目的在于模拟我们 npm install 了 qycli 这个包,使得在全局(在我的电脑里就是 C:\Program Files\nodejs\)的 node_modules 目录下创建了一个指向我们项目的软连接:
之后就可以在任意目录下执行 qycli
命令了:
查看版本号
如果我们想通过命令 qycli -V
或 qycli --version
来查看当前脚手架的版本号,可以安装 tj 编写的 Commander.js 来实现。
npm install commander
在 lib\index.js 使用从 commander 中导出的 program
对象:
#!/usr/bin/env node
// lib\index.js
const { program } = require('commander')
const version = require('../package.json').version
program.version(version)
program.parse(process.argv)
- 使用
program.version()
来设置版本,版本号version
从 package.json 内获取; - 使用
program.parse()
来解析命令,传入process.argv
表明按照 node 约定来解析。直接打印console.log(process.argv)
得到的结果如下,是个字符串数组,第一个值是应用,第二个值是要跑的脚本文件,之后就是我们在命令后面添加的参数了:
现在,只要在命令行输入 qycli -V
(注意是大写的 V) 或 qycli --version
即可得到版本号,如果想自定义参数,比如使用 -v
(小写的 v),则可以在 program.version
传入第二个参数:
program.version(version, '-v --version')
但是注意命令只能有两个,如此一来大写的 V,即 -V
就失效了。
增加选项
截至目前,我们可以在命令 qycli
后面跟上 -v
和 --version
选项参数,其实还有 commander 默认帮我们设置的 -h
和 --help
,用于查看有哪些可用选项。如果我们想增加其它选项,可以通过 program.option()
:
// lib\index.js 省略其它代码
program.option('-b --blg', '打个lck闹麻了')
第一个参数就是选项名称,第二个参数执行 -h
或 --help
时呈现的对选项的描述。现在执行 qycli -h
结果如下:
如果想让 --help
输出更多内容,可以使用 program.on()
监听命令和选项来执行自定义内容:
program.on('--help', () => {
console.log('') // 空行
console.log('友情小提示')
console.log(' balabala') // 空两格更好看
})
现在执行 qycli -h
的结果如下:
我们还可以在执行命令时,给选项后面添加参数,那么就在 program.option()
的第一个参数中,选项后添加 <team>
(team 是随便写的),然后在执行完 program.parse()
后,就可以通过 program.opts().blg
获取参数了:
program.option('-b --blg <team>', '打个lck闹麻了')
program.parse(process.argv)
console.log(program.opts().blg)
现在执行 qycli -b
时就需要添加参数了,执行结果如下:
使用脚手架创建项目
新增 create 命令
接下来,我们要实现在命令行输入 qycli create 项目名
,就会自动帮我们创建一个 vue 项目。
首先是通过 program.command()
来定义 create
命令,后面跟上 <project>
用以接收项目名称,然后还可以使用 [...args]
接收更多的参数,比如指定 vue 的版本:
program
.command('create <project> [...args]')
.description('用于创建 vue 项目')
.action((project, version) => {
console.log(project, version)
createVue(project) // 下文实现
})
之后可以链式调用 description()
来给 create 命令添加描述,这样当输入 qycli -h
的时候就能看到描述信息了:
create
命令要完成的操作可以在传给 action()
的回调内实现,该回调还能获取执行 create
时跟上的参数:
我们将创建项目的操作封装到 createVue
这么一个函数内,使用 download-git-repo 这个库,直接将远程 git 仓库的 vue3 项目模板下载到本地:
const { promisify } = require('util')
const download = promisify(require('download-git-repo'))
async function createVue(project) {
try {
await download('direct:https://gitee.com/chaimhl/vue3-ts-todolist-exercise.git', project, {
clone: true
})
} catch (error) {
console.log(error)
}
}
module.exports = {
createVue
}
download
第一个参数为仓库地址(我使用了之前一个 todo 的小练习作为例子),前面加上 direct:
不然 clone 会报错。如果是 github 仓库,地址后面还得跟上 #main
,因为 github 上的主分支已经不是 master 了;第二个参数就是项目创建的目标目录,直接传 project
表示在执行命令的当前目录下直接创建项目;第三个参数传入配置项。
download
原本是在项目 clone 结束后,会调用最后一个回调函数参数,如果失败则 err
有值:
download(
'direct:https://gitee.com/layui/layui-vue.git',
project,
{ clone: true },
err => {}
)
通过使用 node 内置的 util
模块提供的 promisify,将 download
这种错误优先的回调风格的函数转换成了一个返回 promise 的方法。所以可以使用 async/await 来简化我们的代码。
现在,我们可以在需要创建项目的目录下,执行命令:qycli create <项目名称>
,来创建项目了。
使用 node 子进程执行命令
创建完项目我们还可以利用 node 的 child_process 模块来开启一个子进程,在子进程中执行安装项目依赖 npm install
和启动项目(用的是 npm run serve
)这两个命令。
我们把相关代码封装成 execCommand
函数,通过 spawn
方法创建子进程,传入的参数使用剩余参数接收,然后使用 childProcess.stdout.pipe(process.stdout)
将子进程的 stdout
,也就是标准输出流 (Standard Output Stream),通过管道输出给主进程的 stdout
,以实现将内容打印到终端;使用 childProcess.stderr.pipe(process.stderr)
则是将子进程的标准错误输出流 stderr
(Standard Error Stream) 通过管道输出给主进程的 stderr
,最后监听子进程的 close
事件,在回调中调用 promise 的 resolve()
,以通知外界子进程已经关闭 :
const { spawn } = require('child_process')
function execCommand(...args) {
return new Promise(resolve => {
// 创建子进程
const childProcess = spawn(...args)
// 获取子进程的输出
childProcess.stdout.pipe(process.stdout)
// 获取子进程的错误
childProcess.stderr.pipe(process.stderr)
childProcess.on('close', () => {
resolve()
})
})
}
module.exports = execCommand
在 createVue
中,执行完 download
调用 execCommand
来执行相应的命令,调用时传入的三个参数最终会传给 spawn
方法。第一个参数是执行的命令的名称;第二个参数为命令后跟的参数组成的数组;第三个参数为配置对象,cwd
用于指明子进程的当前工作目录:
async function createVue(project) {
try {
await download(
// ...
)
const commandName = process.platform === 'win32' ? 'npm.cmd' : 'npm'
await execCommand(commandName, ['install'], { cwd: `./${project}` })
await execCommand(commandName, ['run', 'serve'], { cwd: `./${project}` })
} catch (error) {
console.log(error)
}
}
我们使用 process.platform
区分了下平台,如果是 win32
,即Windows 平台,npm 命令需要使用 npm.cmd
。
现在,当我们在命令行输入 qycli create 项目名称
后,就会直接从远程仓库 clone 下来项目并自动安装依赖,然后启动项目了。
使用命令创建组件
前面我们定义了 create
命令来创建项目,现在我们再定义个 addComponent
命令用于创建组件,name
为组件名:
// lib\index.js
program
.command('addComponent <name> [...args]')
.description('用于创建组件')
.action(name => {
addComponent(name)
})
我们先得准备个组件模板,它是个 .ejs 文件,以方便我们将组件名通过诸如 <%= name %>
的形式插入到生成的组件中:
<!-- lib\template\component.vue.ejs -->
<script lang="ts" setup></script>
<template>
<div class="<%= name %>"><%= name %></div>
</template>
<style scoped lang="scss"></style>
在之前使用 vue-cli 生成的 vue2 的项目里,public\index.html 中也能看到使用了 ejs:
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= webpackConfig.name %></title>
要想实现将 .ejs 模板生成组件然后写入到指定目录下,首先需要安装 ejs:
npm install ejs
然后将相关代码封装成 compileEjs
函数。使用 ejs
的 renderFile
方法,传入模板的绝对路径,以及要插入的数据 data
,当编译成功后会调用回调,将结果传给 promise 的 resolve()
:
// lib\utils\compile-ejs.js
const path = require('path')
const ejs = require('ejs')
function compileEjs(fileName, data) {
return new Promise((resolve, reject) => {
// 获取模板路径(绝对路径)
const templatePath = path.resolve(__dirname, `../template/${fileName}`)
// 使用 ejs 引擎编译模板
ejs.renderFile(templatePath, data, (err, result) => {
if (err) {
reject(err)
return
}
// console.log(result)
resolve(result)
})
})
}
module.exports = compileEjs
如果打印编译的结果 console.log(result)
,得到的内容如下图(假设执行的命令为 qycli addComponent banner
):
如此,传给 addComponent 命令的 action
的回调中,执行的 addComponent
函数里,就可以调用 compileEjs
,传入模板文件名称以及组件的名称,将得到的结果通过 fs 模块的 writeFile
方法写入到指定的目录下了:
// lib\core\actions.js
const fs = require('fs')
const { program } = require('commander')
const compileEjs = require('../utils/compile-ejs')
async function addComponent(name) {
try {
const result = await compileEjs('component.vue.ejs', { name })
// 将组件写入指定目录 dest 下
const dest = program.opts().dest || 'src/components'
await fs.promises.writeFile(`${dest}/${name}.vue`, result)
console.log('创建成功')
} catch (error) {
console.log(err)
}
}
dest
是通过选项 -d
或 --dest
传入的,所以还需要定义增加选项的代码:program.option('-d --dest <dest>', '指定目录,例如 -d src')
。执行创建组件命令时,就可以自定义创建目录了,比如 qycli addComponent banner --dest src
。
转载自:https://juejin.cn/post/7249172199960363045