脚手架是为了保证各施工过程顺利进行而搭设的工作平台。按搭设的位置分为外脚手架、里脚手架;按材料不同可分为木脚手架、竹脚手架、钢管脚手架;按构造形式分为立杆式脚手架、桥式脚手架、门式脚手架、悬吊式脚手架、挂式脚手架、挑式脚手架、爬式脚手架。 ——百度百科
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,搞定!下面可以舒服的开发组件了,再也不用操心这些琐碎的事情了!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/79694.html