Vue3 企業級優雅實戰 - 組件庫框架 - 9 實現組件庫 cli - 上

来源:https://www.cnblogs.com/youyacoder/archive/2022/12/27/17008383.html
-Advertisement-
Play Games

上文搭建了組件庫 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 函數中調用。

感謝你閱讀本文,如果本文給了你一點點幫助或者啟發,還請三連支持一下,點贊、關註、收藏,公\/同號 程式員優雅哥更多分享。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 🧭 系列導航 Blazor Server 從頭開始:01 創建項目 📖 閱讀說明 此部分內容旨在說明各種Blazor的基本概念與使用方法,並沒有提供實際的教程式代碼,所以讀者沒必要把代碼敲一遍。沒得意義。 🔧 Razor 組件 Razor組件是Blazor應用的基本單元,每一個頁面或頁面上的元 ...
  • 最近手癢,正好陽性在家,就打算把代碼再撿起來看看,學習下這些年來都有什麼新東西出現。就看到了微軟出的.Net 7 Blazor相關的內容。 走了一遍微軟官方的教程,發現用處不大,(可能是年紀已高,也可能是斷代太久),所以給這段時間立個小目標,從頭開始學習一下這個Blazor。 有關Blazor Se ...
  • 註意:本文旨在解決電腦問題,請勿將本文所述技巧用於非法用途 實際上,如果目的只是為了進入windows桌面,那麼想要破解密碼還不如更改方便。不過同樣有解出賬戶密碼的方法。 下麵我就介紹幾個更改密碼的方法。對於幾個方法在操作當中可能發生的某些問題,也會提出解決方案。破解的方法放在最後。這裡面沒準有各位 ...
  • # In[1]magicians = ['alice', 'david', 'carolina']for i in magicians: print(i)'''4.1.1 深入地研究迴圈'''a = list(range(1, 10, 2))print(a)print(sum(a))''' 4.3. ...
  • 「如果港口是國民經濟的晴雨表,那麼智慧港口就是窺探港口未來發展的視窗。」 12月16日,2022智慧港口大會在浙江嘉興舉行,會議以“加快數字化轉型 賦能高質量發展”為主題,由中國港口協會、中國交通通信信息中心、浙江省海港集團聯合主辦,來自行業主管部門及港航管理部門、各大港口集團、科研院校、解決方案供 ...
  • “某中心受病毒攻擊,導致服務中斷,線上業務被迫暫停” “某公司員工誤操作刪庫,核心業務數據部分丟失,無法完全找回” “由於伺服器斷線,某醫院信息系統癱瘓近4小時,期間病人無法使用醫保卡掛號和結算” …… 數據丟失風險防不勝防,企業構建數據備份方案迫在眉睫! 此次騰訊雲資料庫備份服務DBS攜手富途證券 ...
  • 摘要:解決數據問題的本質,還要從數據層面入手,資料庫的價值就十分關鍵。 過去很長一段時間,不動產行業的數字化程度都是比較低的,特別在業務層面,存在大量碎片化和多主體的問題,導致在數據層面的標準化和數據結構統一化不足;而且在不動產行業全生命周期中,每個階段都頻繁涉及到數據流轉問題,對數據一致性和安全性 ...
  • 1.在聯盟創建伺服器應用 參考文檔:開發準備 2.獲取用戶級Access Token 2.1 獲取code 參考文檔:接入華為帳號獲取憑證 2.1.1 先按照跳轉鏈接進行配置url https://oauth-login.cloud.huawei.com/oauth2/v3/authorize? r ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...