上文搭建了组件库 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. Vue3 企业级优雅实战 - 组件库框架 - 1 搭建 pnpm monorepo

    前两篇文章分享了基于 vite3 vue3 的组件库基础工程 vue3-component-library-archetype 和用于快速创建该工程的工具 yyg-cli,但在中大型的企业级项目中,通 ...

  2. Vue3 企业级优雅实战 - 组件库框架 - 4 组件库的 CSS 架构

    在前一篇文章中分享了搭建组件库的基本开发环境.创建了 foo 组件模块和组件库入口模块,本文分享组件库的样式架构设计. 1 常见的 CSS 架构模式 常见的 CSS 架构模式有很多:OOCSS.ACS ...

  3. Vue3 企业级优雅实战 - 组件库框架 - 3 搭建组件库开发环境

    前文已经初始化了 workspace-root,从本文开始就需要依次搭建组件库.example.文档.cli.本文内容是搭建 组件库的开发环境. 1 packages 目录 前面在项目根目录下创建了 ...

  4. Vue3 企业级优雅实战 - 组件库框架 - 2 初始化 workspace-root

    上文已经搭建了 pnpm + monorepo 的基础环境,本文对 workspace-root 进行初始化配置,包括:通用配置文件.公共依赖.ESLint. 1 通用配置文件 在项目 根目录 下添加 ...

  5. Vue企业级优雅实战04-组件开发01-SVG图标组件

    (后续的文章 公众号会提前一周更新,欢迎关注文末的微信公众号:程序员搞艺术) 预览本文的实现效果: # gitee git clone git@gitee.com:cloudyly/dscloudy- ...

  6. Vue企业级优雅实战-00-开篇

    从2018.1.开始参与了多个企业的中台建设,这些中台的技术选型几乎都是基于 Spring Cloud 微服务架构 + 基于 Vue 全家桶的前端.我前后端架构及开发我几乎各占一半的精力,在企业级前端 ...

  7. 小程序组件化框架 WePY 在性能调优上做出的探究

    作者:龚澄 导语 性能调优是一个亘古不变的话题,无论是在传统H5上还是小程序中.因为实现机制不同,可能导致传统H5中的某些优化方式在小程序上并不适用.因此必须另开辟蹊径找出适合小程序的调估方式. 本文 ...

  8. Vue企业级优雅实战05-框架开发01-登录界面

    预览本文的实现效果: # gitee git clone git@gitee.com:cloudyly/dscloudy-admin-single.git # github git clone git ...

  9. Vue企业级优雅实战03-准备工作04-全局设置

    本文包括如下几个部分: 初始化环境变量文件 JS 配置文件初始化:如是否开启 Mock 数据.加载本地菜单.URL 请求路径等: 国际化文件初始化:初始化国际化文件的结构: 整合 Element UI ...

  10. Vue企业级优雅实战02-准备工作03-提交 GIT 平台

    代码管理.版本管理是件老大难的事情,尤其多人开发中的代码冲突.突击功能时面临的 hotfix 等.本文只是简单说说如何将一套代码提交到两个 Git 平台(GitHub.GitEE)上.其他的 Git ...

随机推荐

  1. jquery修改图片src

    jquery修改图片src,会重新请求此图片url,重新加载图片.方法如下: $("#<img的id>").attr("src", src_valu ...

  2. Docker 部署 Kibana

    Docker 部署 Kibana 本篇主要介绍 使用 Docker 部署 kibana 用于操作 Elasticsearch 使用. 1. 前置准备 1.1 Elasticsearch 准备 可以先准 ...

  3. 18.drf request及源码分析

    REST framework的 Request 类扩展了Django标准的 HttpRequest ,添加了对REST framework请求解析和身份验证的支持. 源代码片段: class Requ ...

  4. webgl(three.js)3D光伏,3D太阳能能源,3D智慧光伏、光伏发电、清洁能源三维可视化解决方案——第十六课

    序: 能源是文明和发展的重要保障,人类命运不可避开的话题,无论是战争还是发展,都有它存在的身影.从石器时代到现代文明,人类的能源应用在进步,也在面临能源枯竭的危机与恐惧,而开发与应用可再生能源才是解决 ...

  5. vue3+element-plus+登录逻辑token+环境搭建

    vue3+element-plus+登录逻辑token环境搭建 安装脚手架工具 1 npm i @vue/cli@4.5.13 -g 验证是否安装成功 1 vue -V # 输出 @vue/cli 4 ...

  6. 四、Pod 介绍

    一.什么是 Pod ​Pod 是 kubernetes 集群中最小的部署和管理的基本单元,协同寻址,协同调度. ​Pod 是一个或多个容器的集合,是一个或一组服务(进程)的抽象集合. ​Pod 中可以 ...

  7. JIRA操作之 基本说明

    官方说明:https://docs.atlassian.com/software/jira/docs/api/7.6.1/ 项目(Project) Project是一组问题单(Issue)的集合,每个 ...

  8. 常用Python库整理

    记录工作和学习中遇到和使用过的Python库. Target 四个Level 整理 Collect 学习 Learn 练习 Practice 掌握 Master 1. Python原生和功能增强 1. ...

  9. elasticsearch聚合之bucket terms聚合

    目录 1. 背景 2. 前置条件 2.1 创建索引 2.2 准备数据 3. 各种聚合 3.1 统计人数最多的2个省 3.1.1 dsl 3.1.2 运行结果 3.2 统计人数最少的2个省 3.2.1 ...

  10. C#11之原始字符串

    最近.NET7.0和C#11相继发布,笔者也是第一时间就用上了C#11,其中C#11的有一个更新能解决困扰我多年的问题,也就是文章的标题原始字符串. 在使用C#11的原始字符串时,发现的一些有意思的东 ...