Vue3 enterprise-level elegance in practice - component library framework - 9 Implementing component library cli - Part 1

The above has built the basic framework of the component library cli and implemented user interaction when creating components. However, the createNewComponent function in cli/src/command/create-component.ts has been left behind. The function to be implemented by this function is the function mentioned at the beginning. Here you go - the complete steps for creating a component. In this article, we will implement those steps in sequence. (Friendly reminder: This article contains a lot of content. If you can read it and finish it patiently, you will definitely improve)

1 Create tool class

The process of implementing cli will involve operations such as converting component name naming methods and executing cmd commands. Therefore, before starting to create components, prepare some tool classes.

A log-utils.ts file has been created in the previous article in the cli/src/util/ directory . Now continue to create the following four files: cmd-utils.ts , loading-utils.ts , name-utils.ts , template-utils.ts

1.1 name-utils.ts

This file provides some functions for converting name components, such as converting to camel case naming with the first letter uppercase or lowercase, converting to naming separated by underscores, etc.:

/**
 * 将首字母转为大写
 */
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

When creating a component in the command line, a loading effect is required. This file uses the ora library to provide functions for displaying loading and closing 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

This file encapsulates the execCmd function of the shelljs library , which is used to execute cmd commands:

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

Since automatically creating components requires generating some files, template-utils.ts provides functions to obtain templates for these files. Due to the large amount of content, these functions will be discussed when they are used.

2 parameter entity class

When executing the gen command, the developer will be prompted to enter the component name, Chinese name, type, and some component name conversions. Therefore, this information of the new component can be encapsulated into an entity class, and later passed in various operations. Object can be used to avoid passing a lot of parameters.

2.1 component-info.ts

Create a domain directory in the src directory, and create component-info.ts in this directory . This class encapsulates these basic information of the component:

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

The config.ts file is referenced in the above entity , which is used to set the prefix of the component and the name of the component library. Create config.ts in the src directory :

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

3 Create a new component module

3.1 Overview

As mentioned at the beginning of the previous article, the new cli component must do four things:

  1. Create new component modules;
  2. Create a style scss file and import it;
  3. Install new component modules as dependencies in the component library entry module and introduce new components;
  4. Create component library documents and demos.

The rest of this article will share the first point, and the remaining three points will be shared in the next article.

Create a service directory under src . The above four contents are split into different service files and are uniformly called by cli/src/command/create-component.ts . This makes the hierarchy clear and easy to maintain.

First, create the init-component.ts file in the src/service directory . This file is used to create a new component module . The following things need to be completed in this file:

  1. Create a directory for new components;
  2. Use pnpm init to initialize the package.json file;
  3. Modify the name attribute of package.json;
  4. Install the common tool package @yyg-demo-ui/utils into dependencies;
  5. Create src directory;
  6. Create the component body file xxx.tsx or xxx.vue in the src directory;
  7. Create the types.ts file in the src directory;
  8. Create the component entry file index.ts.

3.2 init-component.ts

The above 8 things need to be implemented in src/service/init-component.ts . In this file, the function initComponent is exported to external calls:

/**
 * 创建组件目录及文件
 */
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)
  })
})

The logic of the above method is relatively clear, I believe everyone can understand it. Among them, 3, 6, 7, and 8 are extracted as functions.

**Modify package.json**: Read the package.json file. Since the name attribute generated by default is in the form of xxx-xx, you only need to replace the field string with the form of @yyg-demo-ui/xxx-xx. That's it, and finally rewrite the replacement result to package.json. The code is implemented as follows:

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)
  }
}

Create the component body xxx.vue / xxx.tsx : read the corresponding template according to the component type (.tsx or .vue), and then write it to the file. Code:

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)
}

Two functions for generating templates in src/util/template-utils.ts are introduced here : sfcTemplate and tsxTemplate, which will be provided later.

Create the src/types.ts file : call the function typesTemplate in template-utils.ts to get the template, and then write it to the file. Code:

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

Create index.ts : Same as above, call the function indexTemplate in template-utils.ts to get the template and then write it to the file. Code:

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

The contents introduced by init-component.ts are as follows:

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

Four functions of template-utils.ts are introduced in init-component.ts: indexTemplate , sfcTemplate , tsxTemplate , typesTemplate , and are implemented as follows:

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}
`
}

This completes the creation of a new component module. The next article will share the remaining three steps and call them in the createNewComponent function.

Thank you for reading this article. If this article has given you a little help or inspiration, please support it three times in a row, like, follow, collect, and share more with the programmer Yaya Ge of the same account.

Guess you like

Origin blog.csdn.net/youyacoder/article/details/128457900