手把手带你开发一个脚手架(上)

3,643 阅读8分钟

脚手架是为了保证各施工过程顺利进行而搭设的工作平台。按搭设的位置分为外脚手架、里脚手架;按材料不同可分为木脚手架、竹脚手架、钢管脚手架;按构造形式分为立杆式脚手架、桥式脚手架、门式脚手架、悬吊式脚手架、挂式脚手架、挑式脚手架、爬式脚手架。 --百度百科

本质上就是一个便利工具,为一些比较特殊或繁琐的工作提供辅助,我们这里需要开发的是一个基于命令行的工具,后文以 cli 代替。

为什么需要开发一个脚手架?

以下为初期组件库协同开发时遇到的问题:

  • 组件目录结构不一致
    • 扁平化目录
    • src 型目录
  • 组件产出命名不一致
    • 前缀 D 命名
    • 无前缀 D 命名
    • 小写驼峰命名
    • 大写驼峰命名
    • xxxService 命名
    • useXxxService 命名
  • 组件入口文件经常冲突

为了解决上述问题,本着为开源社区做贡献,发光发热的时候到了,和项目组织人 kagol 沟通了脚手架方案顺利通过,Prefect

TODO

  • 创建统一组件结构
  • 创建组件库入口文件

技术选型

脚手架 = 命令 + 交互 + 逻辑处理

  • 命令
    • 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.png

image.png

测试脚本命令

我们可以先在 onCreate 里面打印一下我们接受到的参数。

export function onCreate (cmd) {
  console.log(cmd)
}

执行一下脚本。

node src/index.js

image.png

报错了!!!我们才刚开始就报错了,是否已经开始崩溃?

稳住别慌,一切在我们的意料之中。这是因为我们编写的是 node 程序,本应该使用 commonjs 简称 CJS 格式,也就是用 requireexports 等语法才能正常使用 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.png

image.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

按照提示信息一步一步完成输入最终获取到了我们需要的数据,接下来就是模板的生成了。