Vue3 企業級優雅實戰

2023-02-09 18:06:49

上文建立了一堆 utils、component-info,並實現了新元件模組相關目錄和檔案的建立。本文繼續實現後面的內容。

1 元件樣式檔案並匯入

src/service 目錄中建立 init-scss.ts 檔案,該檔案匯出 initScss 函數。

由於 .vue 型別的元件的樣式就直接寫在了 style 中,故首先判斷元件型別是否是 tsx,tsx 型別的元件才進行這一步的操作:

  1. scss/components/ 目錄下建立元件的 scss 檔案 _xxx.module.scss
  2. scss/components/index.scss 中匯入 _xxx.module.scss

1.1 init-scss.ts

程式碼實現如下:

import { ComponentInfo } from '../domain/component-info'
import path from 'path'
import { scssTemplate } from '../util/template-utils'
import fs from 'fs'
import { g } from '../util/log-utils'

const updateComponentScssIndex = (scssRootPath: string, lineName: string) => {
  const indexScssPath = path.resolve(scssRootPath, 'components/index.scss')

  const content = fs.readFileSync(indexScssPath).toString()
  const newContent = content.substring(0, content.length) + `@use "${lineName}.module";\n`
  fs.writeFileSync(indexScssPath, newContent)
}

/**
 * 建立元件庫 scss 檔案,並在 scss/components/index.scss 中引入該檔案
 */
export const initScss = (componentInfo: ComponentInfo) => new Promise((resolve, reject) => {
  // tsx 型別需要建立scss檔案
  if (componentInfo.type === 'tsx') {
    const { parentPath, lineName, lineNameWithPrefix } = componentInfo
    
    // scss 根目錄(packages/scss)
    const scssRootPath = path.resolve(parentPath, 'scss')

    // 1. 建立元件的 scss 檔案
    const componentScssPath = path.resolve(scssRootPath, `components/_${lineName}.module.scss`)
    fs.writeFileSync(componentScssPath, scssTemplate(lineNameWithPrefix))

    // 2. 在元件庫 scss 入口檔案 (packages/components/index.scss)引入上面建立的檔案
    updateComponentScssIndex(scssRootPath, lineName)

    g('component scss init success')
  }
  resolve(componentInfo)
})

1.2 template-utils.ts

上面的 init-scss.ts 在建立 scss 檔案時呼叫了 template-utils.ts 中的 scssTemplate 函數獲取模板。故需要在 util/template-utils.ts 中新增該函數:

/**
 * scss 檔案模板
 */
export const scssTemplate = (lineNameWithPrefix: string): string => {
  return `@import "../tools";
@import "../acss/mp";
@import "../base/var.module";

@include b('${lineNameWithPrefix}') {
}
`
}

2 新增到元件庫入口模組

新元件和樣式建立完成,接下來便是將新元件模組安裝到元件庫入口模組的依賴中。在 src/service 目錄中建立 update-component-lib.ts 檔案,該檔案匯出函數 updateComponentLib。該函數需要完成兩件事:

  1. 在元件庫入口模組中安裝新元件為依賴;
  2. 更新元件庫入口模組的 index.ts 檔案,引入新元件。

程式碼實現如下:

import { ComponentInfo } from '../domain/component-info'
import { execCmd } from '../util/cmd-utils'
import path from 'path'
import { Config } from '../config'
import fs from 'fs'
import { g } from '../util/log-utils'

const updateComponentLibIndex = (libPath: string, componentInfo: ComponentInfo) => {
  const indexPath = path.join(libPath, 'index.ts')
  const content = fs.readFileSync(indexPath).toString()

  const index1 = content.indexOf('// import component end')
  const index2 = content.indexOf('] // components')

  const result = `${content.substring(0, index1)}` +
    `import ${componentInfo.upCamelName} from '${componentInfo.nameWithLib}'\n` +
    content.substring(index1, index2 - 1) +
    `,\n  ${componentInfo.upCamelName}\n` +
    content.substring(index2)

  fs.writeFileSync(indexPath, result)
}

/**
 * 更新元件庫入口
 */
export const updateComponentLib = async (componentInfo: ComponentInfo) => {
  // 元件庫入口的路徑
  const libPath = path.resolve(componentInfo.parentPath, Config.COMPONENT_LIB_NAME)

  // 1. 新增新建立的元件到依賴中
  await execCmd(`cd ${libPath} && pnpm install ${componentInfo.nameWithLib}`)

  // 2. 更新入口 index.ts
  updateComponentLibIndex(libPath, componentInfo)

  g('component library update success')
}

3 元件庫檔案相關檔案

3.1 init-doc.ts

src/service 目錄中建立 init-doc.ts 檔案,該檔案匯出函數 initDoc。該函數需要完成三件事:

  1. 建立元件的 MarkDown 檔案;
  2. 建立元件 MD 檔案中的 demo;
  3. 更新元件庫檔案選單。

程式碼實現如下:

import { ComponentInfo } from '../domain/component-info'
import { g } from '../util/log-utils'
import path from 'path'
import fs from 'fs'
import { demoTemplate, mdTemplate } from '../util/template-utils'

/**
 * 建立元件檔案、demo及更新選單
 */
export const initDoc = (componentInfo: ComponentInfo) => {
  // 元件庫檔案根路徑
  const docRootPath = path.resolve(componentInfo.parentPath, '../docs')
  const { lineName, lineNameWithPrefix, upCamelName, zhName } = componentInfo

  // 1. 建立元件的 MD 檔案
  fs.writeFileSync(path.resolve(docRootPath, `components/${lineName}.md`), mdTemplate(componentInfo))

  // 2. 建立元件檔案中的 Demo
  fs.mkdirSync(path.resolve(docRootPath, `demos/${lineName}`))
  fs.writeFileSync(path.resolve(docRootPath, `demos/${lineName}/${lineName}-1.vue`), demoTemplate(lineNameWithPrefix))

  // 3. 更新元件庫檔案選單
  const menuPath = path.resolve(docRootPath, 'components.ts')
  const content = fs.readFileSync(menuPath).toString()
  const index = content.indexOf('] // end')
  const result = content.substring(0, index - 1) +
    `,\n  { text: '${upCamelName} ${zhName}', link: '/components/${lineName}' }\n` +
    content.substring(index)
  fs.writeFileSync(menuPath, result)

  g('component document init success')
}

3.2 template-utils.ts

上面的 init-doc.ts 呼叫了 mdTemplatedemoTemplate 兩個函數,在 template-utils.ts 中新增這兩個函數:

export const mdTemplate = (componentInfo: ComponentInfo) => {
  return `
# ${componentInfo.upCamelName} ${componentInfo.zhName}

## 基本使用

<preview path="../demos/${componentInfo.lineName}/${componentInfo.lineName}-1.vue" title="基本使用" description=" "></preview>

## 元件 API

### Attributes 屬性

| 引數 | 說明 | 型別 | 可選值 | 預設值 |
|  ----  | ----  | ----  | ----  | ----  |
|  |  |  |  | |

### Methods 方法

| 方法名 | 說明 | 引數 | 返回值 |
|  ----  | ----  | ----  | ----  |
|  |  |  |  |

### Events 事件

| 事件名 | 說明 | 引數 | 返回值 |
|  ----  | ----  | ----  | ----  |
|  |  |  |  |

### Slots 插槽

| 插槽名 | 說明 | 引數 |
|  ----  | ----  | ----  |
|  |  |  |
`
}

export const demoTemplate = (lineNameWithPrefix: string) => {
  return `<template>
  <${lineNameWithPrefix}></${lineNameWithPrefix}>
</template>

<script lang="ts" setup>
</script>

<style scoped lang="scss">
</style>
`
}

這兩個函數的模板可以自己去定義。

4 create-component.ts

四個步驟都已實現,最後需要在 src/command/create-component.ts 檔案中的 createNewComponent 函數中完成上面四個 service 的呼叫。

4.1 import

匯入四個service及使用到的其他函數:

import { ComponentInfo } from '../domain/component-info'
import { closeLoading, showLoading } from '../util/loading-utils'
import { g, r } from '../util/log-utils'
import { initComponent } from '../service/init-component'
import { initScss } from '../service/init-scss'
import { updateComponentLib } from '../service/update-component-lib'
import { initDoc } from '../service/init-doc'

4.2 createNewComponent

該函數首先根據使用者輸入,構造 ComponentInfo 物件,然後依次呼叫引入的四個 service,完成元件建立的全部流程:

const createNewComponent = async (componentName: string, description: string, componentType: string) => {
  console.log(componentName, description, componentType)
  showLoading('Generating, please wait...')
  try {
    // 1. 構造 ComponentInfo 物件
    const componentInfo = new ComponentInfo(componentName, description, componentType)
    // 2. 建立元件目錄及檔案
    await initComponent(componentInfo)
    // 3. 建立樣式
    await initScss(componentInfo)
    // 4. 更新元件庫入口
    await updateComponentLib(componentInfo)
    // 5. 元件庫檔案
    initDoc(componentInfo)

    closeLoading()
    g(`component [${componentInfo.lineName} ${componentInfo.zhName}] created done!`)
  } catch (e: any) {
    closeLoading()
    r(e.message)
  }
}

元件庫 cli 就這樣完成了。執行 pnpm run gen,依次輸入元件名、元件中文名,選擇元件型別,便自動完成元件的建立、註冊、檔案的建立了。優雅哥花了大量篇幅介紹 cli 的開發,不僅僅可以在這裡使用,通過本案例的實現,希望大家可以將這種方式移植到其他地方,如從 github 拉取程式碼模板、自動化 CI/CD 等。

下一篇文章將介紹元件庫的打包構建和釋出。

感謝閱讀本文,如果本文給了你一點點幫助或者啟發,還請三連支援一下,瞭解更多內容工薇號「程式設計師優雅哥」。