likes
comments
collection
share

开发自己的脚手架工具

作者站长头像
站长
· 阅读数 67

尤雨溪觉得 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 -Vqycli --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 函数。使用 ejsrenderFile 方法,传入模板的绝对路径,以及要插入的数据 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
评论
请登录