经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » Vue.js » 查看文章
Vue3 企业级优雅实战 - 组件库框架 - 9 实现组件库 cli - 上
来源:cnblogs  作者:程序员优雅哥(公\/同)  时间:2023/2/10 9:23:02  对本文有异议

上文搭建了组件库 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

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

  1. /**
  2. * 将首字母转为大写
  3. */
  4. export const convertFirstUpper = (str: string): string => {
  5. return `${str.substring(0, 1).toUpperCase()}${str.substring(1)}`
  6. }
  7. /**
  8. * 将首字母转为小写
  9. */
  10. export const convertFirstLower = (str: string): string => {
  11. return `${str.substring(0, 1).toLowerCase()}${str.substring(1)}`
  12. }
  13. /**
  14. * 转为中划线命名
  15. */
  16. export const convertToLine = (str: string): string => {
  17. return convertFirstLower(str).replace(/([A-Z])/g, '-$1').toLowerCase()
  18. }
  19. /**
  20. * 转为驼峰命名(首字母大写)
  21. */
  22. export const convertToUpCamelName = (str: string): string => {
  23. let ret = ''
  24. const list = str.split('-')
  25. list.forEach(item => {
  26. ret += convertFirstUpper(item)
  27. })
  28. return convertFirstUpper(ret)
  29. }
  30. /**
  31. * 转为驼峰命名(首字母小写)
  32. */
  33. export const convertToLowCamelName = (componentName: string): string => {
  34. return convertFirstLower(convertToUpCamelName(componentName))
  35. }

1.2 loading-utils.ts

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

  1. import ora from 'ora'
  2. let spinner: ora.Ora | null = null
  3. export const showLoading = (msg: string) => {
  4. spinner = ora(msg).start()
  5. }
  6. export const closeLoading = () => {
  7. if (spinner != null) {
  8. spinner.stop()
  9. }
  10. }

1.3 cmd-utils.ts

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

  1. import shelljs from 'shelljs'
  2. import { closeLoading } from './loading-utils'
  3. export const execCmd = (cmd: string) => new Promise((resolve, reject) => {
  4. shelljs.exec(cmd, (err, stdout, stderr) => {
  5. if (err) {
  6. closeLoading()
  7. reject(new Error(stderr))
  8. }
  9. return resolve('')
  10. })
  11. })

1.4 template-utils.ts

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

2 参数实体类

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

2.1 component-info.ts

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

  1. import * as path from 'path'
  2. import { convertToLine, convertToLowCamelName, convertToUpCamelName } from '../util/name-utils'
  3. import { Config } from '../config'
  4. export class ComponentInfo {
  5. /** 中划线分隔的名称,如:nav-bar */
  6. lineName: string
  7. /** 中划线分隔的名称(带组件前缀) 如:yyg-nav-bar */
  8. lineNameWithPrefix: string
  9. /** 首字母小写的驼峰名 如:navBar */
  10. lowCamelName: string
  11. /** 首字母大写的驼峰名 如:NavBar */
  12. upCamelName: string
  13. /** 组件中文名 如:左侧导航 */
  14. zhName: string
  15. /** 组件类型 如:tsx */
  16. type: 'tsx' | 'vue'
  17. /** packages 目录所在的路径 */
  18. parentPath: string
  19. /** 组件所在的路径 */
  20. fullPath: string
  21. /** 组件的前缀 如:yyg */
  22. prefix: string
  23. /** 组件全名 如:@yyg-demo-ui/xxx */
  24. nameWithLib: string
  25. constructor (componentName: string, description: string, componentType: string) {
  26. this.prefix = Config.COMPONENT_PREFIX
  27. this.lineName = convertToLine(componentName)
  28. this.lineNameWithPrefix = `${this.prefix}-${this.lineName}`
  29. this.upCamelName = convertToUpCamelName(this.lineName)
  30. this.lowCamelName = convertToLowCamelName(this.upCamelName)
  31. this.zhName = description
  32. this.type = componentType === 'vue' ? 'vue' : 'tsx'
  33. this.parentPath = path.resolve(__dirname, '../../../packages')
  34. this.fullPath = path.resolve(this.parentPath, this.lineName)
  35. this.nameWithLib = `@${Config.COMPONENT_LIB_NAME}/${this.lineName}`
  36. }
  37. }

2.2 config.ts

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

  1. export const Config = {
  2. /** 组件名的前缀 */
  3. COMPONENT_PREFIX: 'yyg',
  4. /** 组件库名称 */
  5. COMPONENT_LIB_NAME: 'yyg-demo-ui'
  6. }

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 给外部调用:

  1. /**
  2. * 创建组件目录及文件
  3. */
  4. export const initComponent = (componentInfo: ComponentInfo) => new Promise((resolve, reject) => {
  5. if (fs.existsSync(componentInfo.fullPath)) {
  6. return reject(new Error('组件已存在'))
  7. }
  8. // 1. 创建组件根目录
  9. fs.mkdirSync(componentInfo.fullPath)
  10. // 2. 初始化 package.json
  11. execCmd(`cd ${componentInfo.fullPath} && pnpm init`).then(r => {
  12. // 3. 修改 package.json
  13. updatePackageJson(componentInfo)
  14. // 4. 安装 utils 依赖
  15. execCmd(`cd ${componentInfo.fullPath} && pnpm install @${Config.COMPONENT_LIB_NAME}/utils`)
  16. // 5. 创建组件 src 目录
  17. fs.mkdirSync(path.resolve(componentInfo.fullPath, 'src'))
  18. // 6. 创建 src/xxx.vue 或s src/xxx.tsx
  19. createSrcIndex(componentInfo)
  20. // 7. 创建 src/types.ts 文件
  21. createSrcTypes(componentInfo)
  22. // 8. 创建 index.ts
  23. createIndex(componentInfo)
  24. g('component init success')
  25. return resolve(componentInfo)
  26. }).catch(e => {
  27. return reject(e)
  28. })
  29. })

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

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

  1. const updatePackageJson = (componentInfo: ComponentInfo) => {
  2. const { lineName, fullPath, nameWithLib } = componentInfo
  3. const packageJsonPath = `${fullPath}/package.json`
  4. if (fs.existsSync(packageJsonPath)) {
  5. let content = fs.readFileSync(packageJsonPath).toString()
  6. content = content.replace(lineName, nameWithLib)
  7. fs.writeFileSync(packageJsonPath, content)
  8. }
  9. }

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

  1. const createSrcIndex = (componentInfo: ComponentInfo) => {
  2. let content = ''
  3. if (componentInfo.type === 'vue') {
  4. content = sfcTemplate(componentInfo.lineNameWithPrefix, componentInfo.lowCamelName)
  5. } else {
  6. content = tsxTemplate(componentInfo.lineNameWithPrefix, componentInfo.lowCamelName)
  7. }
  8. const fileFullName = `${componentInfo.fullPath}/src/${componentInfo.lineName}.${componentInfo.type}`
  9. fs.writeFileSync(fileFullName, content)
  10. }

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

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

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

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

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

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

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

3.3 template-utils.ts

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

  1. import { ComponentInfo } from '../domain/component-info'
  2. /**
  3. * .vue 文件模板
  4. */
  5. export const sfcTemplate = (lineNameWithPrefix: string, lowCamelName: string): string => {
  6. return `<template>
  7. <div>
  8. ${lineNameWithPrefix}
  9. </div>
  10. </template>
  11. <script lang="ts" setup name="${lineNameWithPrefix}">
  12. import { defineProps } from 'vue'
  13. import { ${lowCamelName}Props } from './types'
  14. defineProps(${lowCamelName}Props)
  15. </script>
  16. <style scoped lang="scss">
  17. .${lineNameWithPrefix} {
  18. }
  19. </style>
  20. `
  21. }
  22. /**
  23. * .tsx 文件模板
  24. */
  25. export const tsxTemplate = (lineNameWithPrefix: string, lowCamelName: string): string => {
  26. return `import { defineComponent } from 'vue'
  27. import { ${lowCamelName}Props } from './types'
  28. const NAME = '${lineNameWithPrefix}'
  29. export default defineComponent({
  30. name: NAME,
  31. props: ${lowCamelName}Props,
  32. setup (props, context) {
  33. console.log(props, context)
  34. return () => (
  35. <div class={NAME}>
  36. <div>
  37. ${lineNameWithPrefix}
  38. </div>
  39. </div>
  40. )
  41. }
  42. })
  43. `
  44. }
  45. /**
  46. * types.ts 文件模板
  47. */
  48. export const typesTemplate = (lowCamelName: string, upCamelName: string): string => {
  49. return `import { ExtractPropTypes } from 'vue'
  50. export const ${lowCamelName}Props = {
  51. } as const
  52. export type ${upCamelName}Props = ExtractPropTypes<typeof ${lowCamelName}Props>
  53. `
  54. }
  55. /**
  56. * 组件入口 index.ts 文件模板
  57. */
  58. export const indexTemplate = (componentInfo: ComponentInfo): string => {
  59. const { upCamelName, lineName, lineNameWithPrefix, type } = componentInfo
  60. return `import ${upCamelName} from './src/${type === 'tsx' ? lineName : lineName + '.' + type}'
  61. import { App } from 'vue'
  62. ${type === 'vue' ? `\n${upCamelName}.name = '${lineNameWithPrefix}'\n` : ''}
  63. ${upCamelName}.install = (app: App): void => {
  64. // 注册组件
  65. app.component(${upCamelName}.name, ${upCamelName})
  66. }
  67. export default ${upCamelName}
  68. `
  69. }

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

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

原文链接:https://www.cnblogs.com/youyacoder/p/17102201.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号