手把手带你开发一个脚手架(下)
前言
由于工作原因,断更时间较久,最近终于能喘口气了 😭,废话不多说,书接上文《手把手带你开发一个脚手架(上)》,我们已经获取到了从命令行输入的参数,接下来只需要按照一定的思路配合
nodejs
的文件操作模块组织代码就可以完成一个脚手架从0
到1
的落地了。Let's go!
工作流梳理
通过上节内容我们已经拿到了即将要创建的组件配置项,接下来我们的一个工作流大概如下:
Step1 获取组件目录
获取组件目录是我们需要做的第一步,它决定我们生成的文件位置在哪儿。
注意:因为我们所做的
cli
工具最终可能不是从脚本所在目录下运行,所以这里不会使用__dirname(当前脚本所在目录)
,需要通过process.cwd()
来获取命令执行地址,然后再通过配置项决定在哪里生成。
// index.js
// 输出一下更直观的看到差异
console.log('__dirname: ', __dirname)
console.log('cwd: ', process.cwd())
我们这里为了教学就在根目录下建立 devui/src
文件夹,通过执行命令获得的路径就是当前工作区根目录,所以生成的组件也将存放在 src
下。
接下来修改下 create
脚本代码输出下看看。
// 顶部导入 resolve 方法
import { resolve } from 'path'
// ...省略其他内容
// 修改 createComponent 方法
function createComponent(info) {
// 输出收集到的组件信息
console.log(info)
// 组件存放的目录,相对于 cwd 来说应该存在于哪个目录下
const COMPONENT_ROOT_DIR = 'vue-devui/src'
// 组件存放完整目录地址
const COMPONENT_ROOT_FULL_DIR = resolve(process.cwd(), COMPONENT_ROOT_DIR)
console.log('组件存放目录:', COMPONENT_ROOT_FULL_DIR)
}
至此,第一步完成!
Step2 添加组件模板
现在我们将为我们的组件库添加标准组件模板,在 src
下创建 templates
文件夹,里面需要包含一个组件的入口文件、样式文件、核心逻辑文件、元信息文件。
将要生成的组件目录结构如下:
xxx 组件根目录
├─src
│ ├─core.ts 核心逻辑文件
│ ├─xxx.scss 组件样式文件
├─index.ts 组件入口文件
├─meta.json 组件元信息
meta.json
组件元信息文件,可以用来存储一些与组件相关的配置项,包括但不限于:组件基础信息(名称、英文名、类别等)、组件目录地址、组件完成状态等。收集这些放到meta.json
中可以用于组件库文档的自动化生成,且与组件自身功能相分离(不会在导出组件时携带),毕竟这些信息只是作用于组件库文档介绍中,项目使用时并不会关心这些信息。
模板代码
部分字符串格式转换这里用 lodash-es
库进行处理,所以先安装该库。
npm i -D lodash-es
index.ts
import { camelCase, kebabCase } from 'lodash-es'
export default function genIndexTemplate(name) {
// 组件名用大驼峰形式
const coreName = upperFirst(camelCase(name))
// 文件名用短横线连接形式
const fileName = kebabCase(name)
return `\
import type { App } from 'vue'
import ${coreName} from './src/${fileName}'
// 为组件增加独立安装方式
${coreName}.install = function (app: App) {
app.component(${coreName}.name, ${coreName})
}
export { ${coreName} }
export default {
install(app: App) {
app.use(${coreName} as any)
}
}
`
}
core.ts
import { camelCase, kebabCase, upperFirst } from 'lodash-es'
export default function genCoreTemplate(name) {
// 组件名用大驼峰形式
const coreName = upperFirst(camelCase(name))
// 组件类名用短横线连接形式
const coreClassName = kebabCase(name)
// 组件属性名用小写驼峰形式
const propsName = camelCase(name) + 'Props'
// 组件属性类型名用大写驼峰形式
const propsTypesName = coreName + 'Props'
return `\
import { defineComponent, PropType, ExtractPropTypes } from 'vue'
export const ${propsName} = {
\/\* test: {
type: Object as PropType<{ xxx: xxx }>
} \*\/
} as const
export type ${propsTypesName} = ExtractPropTypes<typeof ${propsName}>
export default defineComponent({
name: '${coreName}',
props: ${propsName},
emits: [],
setup(props: ${propsTypesName}, ctx) {
return () => {
return (<div class="${coreClassName}"></div>)
}
}
})
`
}
style.js
import { kebabCase } from 'lodash-es'
export default function genStyleTemplate(name) {
// 组件类名用短横线连接形式
const coreClassName = kebabCase(name)
return `\
.${coreClassName} {
/* your style */
}`
}
meta.js
import { isPlainObject, upperFirst, camelCase } from 'lodash-es'
// 验证是否为有效组件元信息
export function isValidComponentMeta(obj) {
return isPlainObject(obj) && !!obj.name
}
export function genMetaObj(meta = {}) {
// 组件名用大驼峰形式
const coreName = upperFirst(camelCase(meta.name))
// 普通 json 文件无法添加注释,这里我们使用 $xxx 约定进行属性说明
return {
$name: '组件英文名称',
name: coreName,
$title: '组件中文名称',
title: meta.title ?? '',
$fullTitle: '完整的组件标题,用于文档组件列表树使用',
fullTitle: meta.fullTitle ?? `${coreName} ${meta.title ?? ''}`,
$category: '组件分类',
category: meta.category ?? '',
$status: '组件开发进度:可设置百分比进度(10%、80%)或文字状态(待开发、开发中、已完成)',
status: meta.status ?? '0%',
$dir: '组件目录,以 $CWD 开头,需在获取后替换 $CWD 为 process.cwd() 以保证不同环境下获取到正确的文件地址',
dir: meta.dir ?? ''
}
}
export default function genMetaTemplate(meta = {}) {
// 转为 json 格式
return JSON.stringify(genMetaObj(meta), null, 2)
}
Step3 生成组件模板(目录生成 + 模板写入)
该步骤将目录生成和模板写入流程融为一体,将是 create component
命令的最后一步,我们将通过获取到的组件配置项按照模板文件添加到指定的地址下!
// 顶部导入文件操作方法
import { mkdirSync, writeFileSync } from 'fs-extra'
import { red, lightGreen, lightBlue } from 'kolorist'
import { camelCase, kebabCase, upperFirst } from 'lodash-es'
import { join, resolve } from 'path'
import genMetaTemplate, { genMetaObj } from '../templates/meta'
import genIndexTemplate from '../templates/index'
import genCoreTemplate from '../templates/core'
import genStyleTemplate from '../templates/style'
// ...省略其他内容
// 修改 createComponent 方法
function createComponent(info) {
// 输出收集到的组件信息
console.log(info)
// 组件存放的目录,相对于 cwd 来说应该存在于哪个目录下
const COMPONENT_ROOT_DIR = 'vue-devui/src'
// 组件存放完整目录地址
const COMPONENT_ROOT_FULL_DIR = resolve(process.cwd(), COMPONENT_ROOT_DIR)
console.log('组件存放目录:', COMPONENT_ROOT_FULL_DIR)
// 待创建组件目录
const componentDir = resolve(COMPONENT_ROOT_FULL_DIR, kebabCase(info.name))
// 待创建组件源码目录
const componentSrcDir = resolve(componentDir, 'src')
// 同步且递归创建目标目录,会连带组件根目录一并创建完成
mkdirSync(componentSrcDir, { recursive: true })
// 生成组件元信息
const meta = genMetaObj(Object.assign({}, info, { dir: join('$CWD', COMPONENT_ROOT_DIR, kebabCase(info.name) ) }))
console.log('组件元信息: ', meta)
// 组件相关文件地址
const indexFilePath = resolve(componentDir, 'index.ts')
const metaFilePath = resolve(componentDir, 'meta.json')
const coreFilePath = resolve(componentSrcDir, `${kebabCase(meta.name)}.tsx`)
const styleFilePath = resolve(componentSrcDir, `${kebabCase(meta.name)}.scss`)
// fs 写文件配置项
const WRITE_FILE_OPTIONS = { encoding: 'utf-8' }
// 开始为组件写入文件
writeFileSync(indexFilePath, genIndexTemplate(meta.name), WRITE_FILE_OPTIONS)
writeFileSync(metaFilePath, genMetaTemplate(meta), WRITE_FILE_OPTIONS)
writeFileSync(coreFilePath, genCoreTemplate(meta.name), WRITE_FILE_OPTIONS)
writeFileSync(styleFilePath, genStyleTemplate(meta.name), WRITE_FILE_OPTIONS)
const CLI_PREFIX = '[devui-cli]'
console.log(lightGreen(`✔ ${CLI_PREFIX} - 组件 "${upperFirst(camelCase(meta.name))}" 目录生成成功!`));
console.log(lightBlue(`✈ ${CLI_PREFIX} - 目标地址:${componentDir}`));
}
尝试执行命令: npm run cli
or yarn cli
or pnpm cli
结束
我们已经成功通过开发的脚手架创建出了标准组件模板,至此大功告成✌️!
后需大家可以根据个人需求额外扩展或优化部分功能,例如:
- 组件库入口文件的生成
- 动态脚手架配置(通过类似
vite.config.js
的文件定制化我们所需要执行的操作)
扩展的功能我们在 VueDevUI Cli
中已实现,道友们可以通过下面链接进行参考,有任何问题可以在评论区留言。
转载自:https://juejin.cn/post/7088999650874621988