likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 48

前言

由于工作原因,断更时间较久,最近终于能喘口气了 😭,废话不多说,书接上文《手把手带你开发一个脚手架(上)》,我们已经获取到了从命令行输入的参数,接下来只需要按照一定的思路配合 nodejs 的文件操作模块组织代码就可以完成一个脚手架从 01 的落地了。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 中已实现,道友们可以通过下面链接进行参考,有任何问题可以在评论区留言。

DevUI CLI 源码地址

转载自:https://juejin.cn/post/7088999650874621988
评论
请登录