上文搭建了组件库 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. css自定义会话框

    效果图图下: HTML代码: <div style="background-color: transparent; border: 1px #DDDDDD solid; width: ...

  2. JuiceFS 元数据引擎选型指南

    文件系统是我们常见的存储形式,内部主要由数据和元数据两部分组成.其中数据是文件的具体内容,通常会直接展现给用户:而元数据是描述数据的数据,用来记录文件属性.目录结构.数据存储位置等.一般来说,元数据有 ...

  3. IDEA 调试起来太费劲?你需要了解这几招!

    各位好啊,我是会编程的蜗牛,我们在使用IDEA开发java项目时,经常需要用到IDEA的调试功能,不过平时我们用的调试方法可能过于简单了,其实IDEA还给我们提供了非常强大的调试功能,下面让我来看一看 ...

  4. 强制更改windows电脑密码

    强制更改windows电脑密码 1.重启电脑,连续按F8,在出现的高级选项中 2.选择administrator用户,打开管理员命令窗口 3.输入命令 etuserAA123456/add 密码就会被 ...

  5. ByPass

    WebShell-ByPass php一句话木马 <?php eval($_REQUEST['a']]);?> 拦截进行替换 替换eval() assert() 替换$_REQUEST[' ...

  6. Go素数筛选分析

    Go素数筛选分析 1. 素数筛选介绍 学习Go语言的过程中,遇到素数筛选的问题.这是一个经典的并发编程问题,是某大佬的代码,短短几行代码就实现了素数筛选.但是自己看完原理和代码后一脸懵逼(仅此几行能实 ...

  7. python学习笔记----必备知识

    一.必备知识 二.流程控制 https://blog.csdn.net/weixin_43304253/article/details/120778228 1.1语法特点: 1.1.1 代码注释 单行 ...

  8. json文本数据

    本文主要针对三个问题:json格式数据,text数据与json数据之间的关系,json和python字典的区别 1.什么是json数据? json是文本数据,可以在网络中传输的通用数据,它是具有特定格 ...

  9. 在CentOS7下安装Oracle11教程

    前言 安装oracle时,发现网上的文章总是缺少一些信息,导致安装不顺利,因为我对一些文章进行了整合,用以备忘. Oracle安装 首先下载linux版本的oracle安装文件,然后通过XFTP上传到 ...

  10. Python基础部分:5、 python语法之变量与常量

    目录 python语法之变量与常量 一.什么是变量与常量 1.什么是变量 2.什么是常量 二.变量的基本使用 1.代码中如何记录事物状态 2.变量使用的语法结构与底层原理 3.变量名的命名规范 4.变 ...