从0搭建vue3组件库: 如何完整搭建一个前端脚手架?

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

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

为什么需要

以下为组件库协同开发时可能会遇到的问题:

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

TODO

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

技术选型

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

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

初始化 cli

Step1 创建 cli 目录

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

在这里插入图片描述

Step2 安装所需依赖

安装前面提到的依赖库:

yarn add commander@8.2.0 inquirer@8.2.0 fs-extra kolorist
yarn add -D inquirer@8.2.0

在这里插入图片描述

Step3 创建入口文件

创建入口文件cli/src/index.ts

console.log('hello sheep-ui cli!')

运行脚本

以下是ts-node-dev方式,会有卡顿的问题,所以后面替换为了tsc编译后再执行的方式。


请注意为了方便以ts形式开发,需要提前安装ts-node-dev

npm i -g ts-node-dev

执行一下试试:

ts-node-dev ./src/index.ts

tsnd ./src/index.ts

没有问题,配个脚本方便后续执行和打包, package.json

"scripts": {
  "dev": "tsnd ./src/index.ts",
  "build": "tsc"
},

这里推荐另一个方式:使用esbuild构建ts再执行。esbuild构建速度相当快,vite底层就是用它执行各种预编译任务。

安装esbuild

yarn add esbuild -D

添加脚本如下,cli/package.json:

  "scripts": {
    "dev": "esbuild --bundle ./src/index.ts --format=cjs --platform=node --outdir=./lib --watch",
    "build": "esbuild --bundle ./src/index.ts --format=cjs --platform=node --outdir=./lib",
    "cli": "node ./lib/index.js create"
  },

开发时先执行yarn dev,运行代码执行yarn cli

开发命令脚本

准备工作结束,接下来开始正式的 cli 脚本编写。

先注册下我们需要执行的一些命令以及一些命令参数, src/index.ts

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()

命令逻辑,src/commands/create.js

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

测试脚本命令

如果你选择了前面的esbuild方案可以跳过。


执行一下脚本

tsnd ./src/index.ts create

报错,这里是因为模块参数没设置,我们需要最终支持cjs
在这里插入图片描述

设置module为commonjs,tsconfig.json

{
  "compilerOptions": {
    "outDir": "./build",
    "module": "commonjs"
  }
}

再次执行问题解决!

在这里插入图片描述

再试试传递参数:

yarn dev create -t component // -t 是 --type 的别名

效果如下:

在这里插入图片描述


下面是我们使用esbuild方式,如果采用的tsnd或tsc可以跳过,执行脚本:

yarn cli

可以直接显示结果:

在这里插入图片描述

传个参数试试:

yarn cli create -t component

在这里插入图片描述

后面执行方式都是这样,不再赘述

完善 create 命令

接下来就是进一步完善我们的命令交互了,以 component 为例,我们需要询问用户想要创建的组件的中英文名称、分类等信息,代码如下:

import * as inquirer from 'inquirer' // 如果你使用的是tsnd方式需要这样导入
// import inquirer from 'inquirer' // 如果使用的esbuild方式可以这样导入
import { red } from 'kolorist'

// create type 支持项
const CREATE_TYPES = ['component', 'lib-entry']
// 文档分类
const DOCS_CATEGORIES = ['通用', '导航', '反馈', '数据录入', '数据展示', '布局']

export async function onCreate(cmd = { type: '' }) {
  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: string) => {
              if (value.trim() === '') {
                return '组件 name 是必填项!'
              }
              return true
            }
          },
          {
            name: 'title',
            type: 'input',
            message: '(必填)请输入组件中文名称,将用作文档列表显示:',
            validate: (value: string) => {
              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.')
}

可以测试一下,先尝试错误类型

yarn dev create -t error

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

在这里插入图片描述

接下来尝试正确的类型:

yarn dev create -t component

在这里插入图片描述

练习

到这里基本完成控制流程,下面需要根据收集到的信息创建组件目录和相应文件,以及文档菜单内容。

大家先尝试自己写一下试试!

创建组件

创建目录

首先根据用户传入组件名称创建目录,创建cli/src/shared/create-component.ts

import { ensureDirSync } from 'fs-extra'
import { resolve } from 'path'
import { lightBlue, lightGreen } from 'kolorist'

export type ComponentMeta = {
  name: string
  title: string
  category: string
}

export default function createComponent(meta: ComponentMeta) {
  // 拼接组件目录
  const componentDir = resolve('../src', meta.name)

  // 其他核心文件:组件源文件、类型文件、样式文件
  const compSrcDir = resolve(componentDir, 'src')
  const styleDir = resolve(componentDir, 'style')
  const testDir = resolve(componentDir, 'test')

  ensureDirSync(compSrcDir)
  ensureDirSync(styleDir)
  ensureDirSync(testDir)

  console.log(
    lightGreen(
      `✔ The component "${meta.name}" directory has been generated successfully.`
    )
  )
  console.log(lightBlue(`✈ Target directory: ${componentDir}`))
}

create.ts导入并使用

import createComponent from '../shared/create-component'

创建模板文件

现在将组件相关的模板文件创建一下,思路是获取配置的组件名,确定文件地址,文件内容可以以“模板字符串”的形式在工厂函数中设置好,比如下面这样:

在这里插入图片描述

下面是具体实现,template/core.ts

/* eslint-disable prettier/prettier */
import { upperFirst } from './utils'

export default function genCoreTemplate(name: string) {
  const compName = 'S' + upperFirst(name)
  const propsTypeName = upperFirst(name) + 'Props'
  const propsName = name + 'Props'
  const propsFileName = name + '-type'
  const className = 's-' + name
  return `\
import { defineComponent } from 'vue'
import { ${propsTypeName}, ${propsName} } from './${propsFileName}'

export default defineComponent({
  name: '${compName}',
  props: ${propsName},
  emits: [],
  setup(props: ${propsTypeName}, ctx) {
    return () => {
      return (<div class="${className}"></div>)
    }
  }
})
`
}

这里需要将属性首字母大写,template/utils.ts

export function upperFirst(str: string) {
  return str.charAt(0).toUpperCase() + str.slice(1)
}

然后创建并写入这个文件即可,下面编写这部分逻辑,create-component.ts

import { WriteFileOptions } from 'fs'
import { writeFileSync } from 'fs-extra'
import genCoreTemplate from '../template/core'

const WRITE_FILE_OPTIONS: WriteFileOptions = { encoding: 'utf-8' }

export default function createComponent(meta: ComponentMeta) {
  // ...
  ensureDirSync(testDir)

  // 创建组件核心文件:组件文件,类型文件,样式文件
  // 组件文件
  const coreFilePath = resolve(compSrcDir, meta.name) + '.tsx'
  writeFileSync(coreFilePath, genCoreTemplate(meta.name), WRITE_FILE_OPTIONS)
}

下面测试一下

在这里插入图片描述

在这里插入图片描述

生成的内容很理想,但缺少类型声明文件:

在这里插入图片描述

下面生成类型声明文件,创建cli/src/template/types.ts

import { upperFirst } from './utils'

export default function genTypesTemplate(name: string) {
  const propsTypeName = upperFirst(name) + 'Props'
  const propsName = name + 'Props'
  return `\
import type { PropType, ExtractPropTypes } from 'vue'

export const ${propsName} = {} as const

export type ${propsTypeName} = ExtractPropTypes<typeof ${propsName}>
`
}

调用genTypesTemplate(), create-component.ts

// 组件类型文件
const typesFilePath = resolve(compSrcDir, meta.name + '-type.ts')
writeFileSync(typesFilePath, genTypesTemplate(meta.name), WRITE_FILE_OPTIONS)

下面生成样式文件,cli/src/template/style.ts

export function genStyleTemplate(name) {
  return `\
.s-${name} {
  /* your component style */

}
`
}

调用genStyleTemplate(), create-component.ts

// 样式文件
const styleFilePath = styleDir + `/${meta.name}.scss`
writeFileSync(styleFilePath, genStyleTemplate(meta.name), WRITE_FILE_OPTIONS)

下面生成测试文件,cli/src/template/test.ts

import { upperFirst } from './utils'

export default function genTestTemplate(name) {
  return `\
import { render } from '@testing-library/vue'
import ${upperFirst(name)} from '../src/${name}'

describe('${name} test', () => {
  test('${name} init render', async () => {
    const { getByRole } = render(${upperFirst(name)})
    getByRole('${name}')
  })
})
`
}

调用genTestTemplate(), create-component.ts

// 测试文件
const testFilePath = testDir + `/${meta.name}.test.ts`
writeFileSync(testFilePath, genTestTemplate(meta.name), WRITE_FILE_OPTIONS)

最后,还需要生成组件索引文件,导出组件和插件,cli/src/template/index.ts

import { upperFirst } from './utils'

export default function genIndexTemplate(name) {
  const compName = upperFirst(name)
  return `\
import { App } from 'vue'
import ${compName} from './src/${name}'
import { installComponent } from '../install'
import type { SheepUIOptions } from '../_utils/global-config'

// 具名导出
export { ${compName} }

// 导出插件
export default {
  install(app: App, options?: SheepUIOptions) {
    installComponent(app, ${compName}, options)
  }
}  
`
}


调用genIndexTemplate(), create-component.ts

// 索引文件
const indexFilePath = componentDir + `/index.ts`
writeFileSync(indexFilePath, genIndexTemplate(meta.name), WRITE_FILE_OPTIONS)

验证一下效果:yarn dev create -t component

在这里插入图片描述

看看生成的目录和文件

在这里插入图片描述

ok,搞定!下面可以舒服的开发组件了,再也不用操心这些琐碎的事情了!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林多多@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值