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

  • Vue3 企业级优雅实战 – 组件库框架 – 9 实现组件库 cli – 上已关闭评论
  • 81 次浏览
  • A+
所属分类:Web前端
摘要

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

上文搭建了组件库 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 函数中调用。

感谢阅读本文,如果本文给了你一点点帮助或者启发,还请三连支持一下,了解更多内容工薇号“程序员优雅哥”。