草庐IT

Vue3 企业级优雅实战 - 组件库框架 - 9 实现组件库 cli - 上

程序员优雅哥 (youyacoder) 2023-03-28 原文

上文搭建了组件库 cli 的基础架子,实现了创建组件时的用户交互,但遗留了 cli/src/command/create-component.ts 中的 createNewComponent 函数,该函数要实现的功能就是上文开篇提到的 —— 创建一个组件的完整步骤。本文咱们就依次实现那些步骤。(友情提示:本文内容较多,如果你能耐心看完、写完,一定会有提升)

1 创建工具类

在实现 cli 的过程中会涉及到组件名称命名方式的转换、执行cmd命令等操作,所以在开始实现创建组件前,先准备一些工具类。

cli/src/util/ 目录上一篇文章中已经创建了一个 log-utils.ts 文件,现继续创建下列四个文件:cmd-utils.tsloading-utils.tsname-utils.tstemplate-utils.ts

1.1 name-utils.ts

该文件提供一些名称组件转换的函数,如转换为首字母大写或小写的驼峰命名、转换为中划线分隔的命名等:

/**
 * 将首字母转为大写
 */
export const convertFirstUpper = (str: string): string => {
  return `${str.substring(0, 1).toUpperCase()}${str.substring(1)}`
}
/**
 * 将首字母转为小写
 */
export const convertFirstLower = (str: string): string => {
  return `${str.substring(0, 1).toLowerCase()}${str.substring(1)}`
}
/**
 * 转为中划线命名
 */
export const convertToLine = (str: string): string => {
  return convertFirstLower(str).replace(/([A-Z])/g, '-$1').toLowerCase()
}
/**
 * 转为驼峰命名(首字母大写)
 */
export const convertToUpCamelName = (str: string): string => {
  let ret = ''
  const list = str.split('-')
  list.forEach(item => {
    ret += convertFirstUpper(item)
  })
  return convertFirstUpper(ret)
}
/**
 * 转为驼峰命名(首字母小写)
 */
export const convertToLowCamelName = (componentName: string): string => {
  return convertFirstLower(convertToUpCamelName(componentName))
}

1.2 loading-utils.ts

在命令行中创建组件时需要有 loading 效果,该文件使用 ora 库,提供显示 loading 和关闭 loading 的函数:

import ora from 'ora'

let spinner: ora.Ora | null = null

export const showLoading = (msg: string) => {
  spinner = ora(msg).start()
}

export const closeLoading = () => {
  if (spinner != null) {
    spinner.stop()
  }
}

1.3 cmd-utils.ts

该文件封装 shelljs 库的 execCmd 函数,用于执行 cmd 命令:

import shelljs from 'shelljs'
import { closeLoading } from './loading-utils'

export const execCmd = (cmd: string) => new Promise((resolve, reject) => {
  shelljs.exec(cmd, (err, stdout, stderr) => {
    if (err) {
      closeLoading()
      reject(new Error(stderr))
    }
    return resolve('')
  })
})

1.4 template-utils.ts

由于自动创建组件需要生成一些文件,template-utils.ts 为这些文件提供函数获取模板。由于内容较多,这些函数在使用到的时候再讨论。

2 参数实体类

执行 gen 命令时,会提示开发人员输入组件名、中文名、类型,此外还有一些组件名的转换,故可以将新组件的这些信息封装为一个实体类,后面在各种操作中,传递该对象即可,从而避免传递一大堆参数。

2.1 component-info.ts

src 目录下创建 domain 目录,并在该目录中创建 component-info.ts ,该类封装了组件的这些基础信息:

import * as path from 'path'
import { convertToLine, convertToLowCamelName, convertToUpCamelName } from '../util/name-utils'
import { Config } from '../config'

export class ComponentInfo {
  /** 中划线分隔的名称,如:nav-bar */
  lineName: string
  /** 中划线分隔的名称(带组件前缀) 如:yyg-nav-bar */
  lineNameWithPrefix: string
  /** 首字母小写的驼峰名 如:navBar */
  lowCamelName: string
  /** 首字母大写的驼峰名 如:NavBar */
  upCamelName: string
  /** 组件中文名 如:左侧导航 */
  zhName: string
  /** 组件类型 如:tsx */
  type: 'tsx' | 'vue'

  /** packages 目录所在的路径 */
  parentPath: string
  /** 组件所在的路径 */
  fullPath: string

  /** 组件的前缀 如:yyg */
  prefix: string
  /** 组件全名 如:@yyg-demo-ui/xxx */
  nameWithLib: string

  constructor (componentName: string, description: string, componentType: string) {
    this.prefix = Config.COMPONENT_PREFIX
    this.lineName = convertToLine(componentName)
    this.lineNameWithPrefix = `${this.prefix}-${this.lineName}`
    this.upCamelName = convertToUpCamelName(this.lineName)
    this.lowCamelName = convertToLowCamelName(this.upCamelName)
    this.zhName = description
    this.type = componentType === 'vue' ? 'vue' : 'tsx'
    this.parentPath = path.resolve(__dirname, '../../../packages')
    this.fullPath = path.resolve(this.parentPath, this.lineName)
    this.nameWithLib = `@${Config.COMPONENT_LIB_NAME}/${this.lineName}`
  }
}

2.2 config.ts

上面的实体中引用了 config.ts 文件,该文件用于设置组件的前缀和组件库的名称。在 src 目录下创建 config.ts

export const Config = {
  /** 组件名的前缀 */
  COMPONENT_PREFIX: 'yyg',
  /** 组件库名称 */
  COMPONENT_LIB_NAME: 'yyg-demo-ui'
}

3 创建新组件模块

3.1 概述

上一篇开篇讲了,cli 组件新组件要做四件事:

  1. 创建新组件模块;
  2. 创建样式 scss 文件并导入;
  3. 在组件库入口模块安装新组件模块为依赖,并引入新组件;
  4. 创建组件库文档和 demo。

本文剩下的部分分享第一点,其余三点下一篇文章分享。

src 下创建 service 目录,上面四个内容拆分在不同的 service 文件中,并统一由 cli/src/command/create-component.ts 调用,这样层次结构清晰,也便于维护。

首先在 src/service 目录下创建 init-component.ts 文件,该文件用于创建新组件模块,在该文件中要完成如下几件事:

  1. 创建新组件的目录;
  2. 使用 pnpm init 初始化 package.json 文件;
  3. 修改 package.json 的 name 属性;
  4. 安装通用工具包 @yyg-demo-ui/utils 到依赖中;
  5. 创建 src 目录;
  6. 在 src 目录中创建组件本体文件 xxx.tsx 或 xxx.vue;
  7. 在 src 目录中创建 types.ts 文件;
  8. 创建组件入口文件 index.ts。

3.2 init-component.ts

上面的 8 件事需要在 src/service/init-component.ts 中实现,在该文件中导出函数 initComponent 给外部调用:

/**
 * 创建组件目录及文件
 */
export const initComponent = (componentInfo: ComponentInfo) => new Promise((resolve, reject) => {
  if (fs.existsSync(componentInfo.fullPath)) {
    return reject(new Error('组件已存在'))
  }

  // 1. 创建组件根目录
  fs.mkdirSync(componentInfo.fullPath)

  // 2. 初始化 package.json
  execCmd(`cd ${componentInfo.fullPath} && pnpm init`).then(r => {
    // 3. 修改 package.json
    updatePackageJson(componentInfo)

    // 4. 安装 utils 依赖
    execCmd(`cd ${componentInfo.fullPath} && pnpm install @${Config.COMPONENT_LIB_NAME}/utils`)

    // 5. 创建组件 src 目录
    fs.mkdirSync(path.resolve(componentInfo.fullPath, 'src'))

    // 6. 创建 src/xxx.vue 或s src/xxx.tsx
    createSrcIndex(componentInfo)

    // 7. 创建 src/types.ts 文件
    createSrcTypes(componentInfo)

    // 8. 创建 index.ts
    createIndex(componentInfo)

    g('component init success')

    return resolve(componentInfo)
  }).catch(e => {
    return reject(e)
  })
})

上面的方法逻辑比较清晰,相信大家能够看懂。其中 3、6、7、8抽取为函数。

**修改 package.json ** :读取 package.json 文件,由于默认生成的 name 属性为 xxx-xx 的形式,故只需将该字段串替换为 @yyg-demo-ui/xxx-xx 的形式即可,最后将替换后的结果重新写入到 package.json。代码实现如下:

const updatePackageJson = (componentInfo: ComponentInfo) => {
  const { lineName, fullPath, nameWithLib } = componentInfo
  const packageJsonPath = `${fullPath}/package.json`
  if (fs.existsSync(packageJsonPath)) {
    let content = fs.readFileSync(packageJsonPath).toString()
    content = content.replace(lineName, nameWithLib)
    fs.writeFileSync(packageJsonPath, content)
  }
}

创建组件的本体 xxx.vue / xxx.tsx:根据组件类型(.tsx 或 .vue)读取对应的模板,然后写入到文件中即可。代码实现:

const createSrcIndex = (componentInfo: ComponentInfo) => {
  let content = ''
  if (componentInfo.type === 'vue') {
    content = sfcTemplate(componentInfo.lineNameWithPrefix, componentInfo.lowCamelName)
  } else {
    content = tsxTemplate(componentInfo.lineNameWithPrefix, componentInfo.lowCamelName)
  }
  const fileFullName = `${componentInfo.fullPath}/src/${componentInfo.lineName}.${componentInfo.type}`
  fs.writeFileSync(fileFullName, content)
}

这里引入了 src/util/template-utils.ts 中的两个生成模板的函数:sfcTemplate 和 tsxTemplate,在后面会提供。

创建 src/types.ts 文件:调用 template-utils.ts 中的函数 typesTemplate 得到模板,再写入文件。代码实现:

const createSrcTypes = (componentInfo: ComponentInfo) => {
  const content = typesTemplate(componentInfo.lowCamelName, componentInfo.upCamelName)
  const fileFullName = `${componentInfo.fullPath}/src/types.ts`
  fs.writeFileSync(fileFullName, content)
}

创建 index.ts:同上,调用 template-utils.ts 中的函数 indexTemplate 得到模板再写入文件。代码实现:

const createIndex = (componentInfo: ComponentInfo) => {
  fs.writeFileSync(`${componentInfo.fullPath}/index.ts`, indexTemplate(componentInfo))
}

init-component.ts 引入的内容如下:

import { ComponentInfo } from '../domain/component-info'
import fs from 'fs'
import * as path from 'path'
import { indexTemplate, sfcTemplate, tsxTemplate, typesTemplate } from '../util/template-utils'
import { g } from '../util/log-utils'
import { execCmd } from '../util/cmd-utils'
import { Config } from '../config'

3.3 template-utils.ts

init-component.ts 中引入了 template-utils.ts 的四个函数:indexTemplatesfcTemplatetsxTemplatetypesTemplate,实现如下:

import { ComponentInfo } from '../domain/component-info'

/**
 * .vue 文件模板
 */
export const sfcTemplate = (lineNameWithPrefix: string, lowCamelName: string): string => {
  return `<template>
  <div>
    ${lineNameWithPrefix}
  </div>
</template>

<script lang="ts" setup name="${lineNameWithPrefix}">
import { defineProps } from 'vue'
import { ${lowCamelName}Props } from './types'

defineProps(${lowCamelName}Props)
</script>

<style scoped lang="scss">
.${lineNameWithPrefix} {
}
</style>
`
}

/**
 * .tsx 文件模板
 */
export const tsxTemplate = (lineNameWithPrefix: string, lowCamelName: string): string => {
  return `import { defineComponent } from 'vue'
import { ${lowCamelName}Props } from './types'

const NAME = '${lineNameWithPrefix}'

export default defineComponent({
  name: NAME,
  props: ${lowCamelName}Props,
  setup (props, context) {
    console.log(props, context)
    return () => (
      <div class={NAME}>
        <div>
          ${lineNameWithPrefix}
        </div>
      </div>
    )
  }
})
`
}

/**
 * types.ts 文件模板
 */
export const typesTemplate = (lowCamelName: string, upCamelName: string): string => {
  return `import { ExtractPropTypes } from 'vue'

export const ${lowCamelName}Props = {
} as const

export type ${upCamelName}Props = ExtractPropTypes<typeof ${lowCamelName}Props>
`
}

/**
 * 组件入口 index.ts 文件模板
 */
export const indexTemplate = (componentInfo: ComponentInfo): string => {
  const { upCamelName, lineName, lineNameWithPrefix, type } = componentInfo

  return `import ${upCamelName} from './src/${type === 'tsx' ? lineName : lineName + '.' + type}'
import { App } from 'vue'
${type === 'vue' ? `\n${upCamelName}.name = '${lineNameWithPrefix}'\n` : ''}
${upCamelName}.install = (app: App): void => {
  // 注册组件
  app.component(${upCamelName}.name, ${upCamelName})
}

export default ${upCamelName}
`
}

这样便实现了新组件模块的创建,下一篇文章将分享其余的三个步骤,并在 createNewComponent 函数中调用。

感谢你阅读本文,如果本文给了你一点点帮助或者启发,还请三连支持一下,点赞、关注、收藏,公\/同号 程序员优雅哥更多分享。

有关Vue3 企业级优雅实战 - 组件库框架 - 9 实现组件库 cli - 上的更多相关文章

  1. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  2. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  3. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  4. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

  5. 计算机毕业设计ssm+vue基本微信小程序的小学生兴趣延时班预约小程序 - 2

    项目介绍随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱小学生兴趣延时班预约小程序的设计与开发被用户普遍使用,为方便用户能够可以随时进行小学生兴趣延时班预约小程序的设计与开发的数据信息管理,特开发了小程序的设计与开发的管理系统。小学生兴趣延时班预约小程序的设计与开发的开发利用现有的成熟技术参考,以源代码为模板,分析功能调整与小学生兴趣延时班预约小程序的设计与开发的实际需求相结合,讨论了小学生兴趣延时班预约小程序的设计与开发的使用。开发环境开发说明:前端使用微信微信小程序开发工具:后端使用ssm:VU

  6. 微信小程序开发入门与实战(Behaviors使用) - 2

    @作者:SYFStrive @博客首页:HomePage📜:微信小程序📌:个人社区(欢迎大佬们加入)👉:社区链接🔗📌:觉得文章不错可以点点关注👉:专栏连接🔗💃:感谢支持,学累了可以先看小段由小胖给大家带来的街舞👉微信小程序(🔥)目录自定义组件-behaviors    1、什么是behaviors    2、behaviors的工作方式    3、创建behavior    4、导入并使用behavior    5、behavior中所有可用的节点    6、同名字段的覆盖和组合规则总结最后自定义组件-behaviors    1、什么是behaviorsbehaviors是小程序中,用于实现

  7. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

  8. ruby-on-rails - Rails 优雅地处理超时 session ? - 2

    使用rails4,ruby2。我在rails配置中为我的cookiesession设置了30分钟的超时时间。问题是,如果我转到表单,让session超时,然后提交表单,我会收到此ActionController::InvalidAuthenticityToken错误。如何在Rails中优雅地处理这个错误?比如说,重定向到登录屏幕? 最佳答案 在您的ApplicationController:rescue_fromActionController::InvalidAuthenticityTokendoredirect_tosome_p

  9. ruby - 获取数组中的值并最小化某个类属性的最优雅的方法是什么? - 2

    假设我有以下类(class):classPersondefinitialize(name,age)@name=name@age=ageenddefget_agereturn@ageendend我有一组Person对象。是否有一种简洁的、类似于Ruby的方法来获取最小(或最大)年龄的人?如何根据它对它们进行排序? 最佳答案 这样做会:people_array.min_by(&:get_age)people_array.max_by(&:get_age)people_array.sort_by(&:get_age)

  10. ruby - Arrays Sets 和 SortedSets 在 Ruby 中是如何实现的 - 2

    通常,数组被实现为内存块,集合被实现为HashMap,有序集合被实现为跳跃列表。在Ruby中也是如此吗?我正在尝试从性能和内存占用方面评估Ruby中不同容器的使用情况 最佳答案 数组是Ruby核心库的一部分。每个Ruby实现都有自己的数组实现。Ruby语言规范只规定了Ruby数组的行为,并没有规定任何特定的实现策略。它甚至没有指定任何会强制或至少建议特定实现策略的性能约束。然而,大多数Rubyist对数组的性能特征有一些期望,这会迫使不符合它们的实现变得默默无闻,因为实际上没有人会使用它:插入、前置或追加以及删除元素的最坏情况步骤复

随机推荐