Skip to content

从0到1搭建Vue组件库06:手把手带你开发一个脚手架

image

本文作者:iel

原文链接:https://juejin.cn/post/7021870182855344142

上一期内容回顾

上一期主要给大家分享了两部分内容:

  • 使用vitepress搭建组件库的文档系统
  • 给文档系统增加demo展开/收起功能

本期邀请了Vue DevUI团队的雷同学给大家分享DevUI CLI工具的实现原理。

雷同学是我们Vue DevUI的早期贡献者,也是Toast组件的田主,早期Vue DevUI组件库还很不完善,组件的文件和组件库的入口文件都是手动添加的,每次新增一个组件,都非常麻烦,需要

  1. 先在devui目录下创建一堆文件index.ts/src/xx.tsx
  2. devui/vue-devui.ts组件库入口文件中添加相应的组件导出
  3. sidebar.ts左侧组件菜单中添加相应的组件配置

而且像vue-devui.ts/sidebar.ts等公共文件,经常多个田主一起修改,经常导致冲突。

雷同学敏锐得发现了这个问题,并主动提出创建一个DevUI CLI工具来解决这个问题,本期直播雷同学就给大家分享了DevUI CLI的实现原理。

这只是DevUI CLI工具系列的第一期,后续还会继续深入如何使用DevUI CLI

  • 为组件库生成入口文件
  • 创建组件目录结构
  • 自动生成左侧组件菜单文件

以下是原文:

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

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

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

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

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

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

TODO

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

技术选型

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

  • 命令
    • commander 插件提供命令注册、参数解析、执行回调
  • 交互
    • inquirer 插件用于命令行的交互(问答)
  • 逻辑处理
    • fs-extra 插件是对 nodejs 文件 Api 的进一步封装,便于使用
    • kolorist 插件用于输出颜色信息进行友好提示

初始化 cli

step1 创建 cli 目录

shell
mkdir devui-cli // 创建脚手架目录
cd devui-cli // 进入脚手架目录
// 初始化一个 node 项目
npm init
// or
yarn init

第一步先创建一个目录来存放我们即将开发的脚手架,作为一个 nodejs 包,需要我们通过 npm 或者 yarn 初始化包的信息,一律回车即可通过,生成后的目录结构如下图。

image.png

step2 创建入口文件

shell
mkdir src
echo 'console.log("hello devui-cli")' > src/index.js

step3 安装所需依赖

shell
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 脚本编写。

配置环境解释器

shell
#!/usr/bin/env node

image.png

部分看官可能会疑惑这句话有什么用呢?

答案在这里,若是有使用过 Linux 或者 Unix 的小伙伴们,对于 Shebang 应该不陌生,它是一个符号的名称 #! 。这个符号通常在 Unix 系统的基本中第一行开头中出现,用于指明这个脚本文件的解释程序, #!/usr/bin/env node 目的就是告诉操作系统执行这个脚本的时候,在 /usr/bin 的环配置里找到 node 解释器并执行。

注册命令

配置好环境解释器之后就可以编写我们的命令逻辑了。

首先,先注册下我们需要执行的一些命令以及一些命令参数。

js
#!/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()

创建具体命令目录,方便统一管理。

shell
mkdir src/commands // 命令存放目录
echo 'export function onCreate() { }' > src/commands/create.js // 创建 create 命令文件并导出回调函数

image.png

image.png

测试脚本命令

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

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

执行一下脚本。

shell
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 ,理由就是:快捷、方便。

shell
npm i -D esbuild
// or
yarn add -D esbuild

安装好后先看下命令行帮助文档。

shell
npx esbuild -h
// or
yarn esbuild -h

执行完后会看到以下帮助信息。

image.png

看过帮助信息后我们加入如下命令:

js
{
    // --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

shell
yarn cli
// or
npm run cli

image.png

输出了一个 {} ,这是我们打印的 cmd 入参,我们并没有填入任何参数,所以解析后是一个空对象,接下来传入 type 参数再看看。

shell
yarn cli -t component // -t --type 的别名
// or
npm run cli -- -t component // -- npm run 脚本传参时需要加的,类似于参数透传给脚本

image.png

image.png

现在已经能够正常获取到命令参数了,证明命令注册成功,后面可以继续实现我们的交互逻辑。

完善 create 命令

接下来就是进一步完善我们的命令交互了,以 component 为例,代码如下:

js
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,接下来尝试运行一下我们的脚本。

先尝试错误的类型:

shell
yarn cli -t error
// or
npm run cli -- -t error

image.png

按照我们的预想提示了错误信息并让我们重新选择类型。

接下来尝试正确的类型:

shell
yarn cli -t component
// or
npm run cli -- -t component

image.png

因为指定了类型为组件,所以现在需要收集一下即将创建的组件信息。

image.png

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

未完待续

尽情期待后续更精彩的分享!

发布时间:

Made with ❤ by