上文搭建了組件庫 cli 的基礎架子,實作了創建組件時的用戶互動,但遺留了 cli/src/command/create-component.ts 中的 createNewComponent 函式,該函式要實作的功能就是勺ò釜篇提到的 —— 創建一個組件的完整步驟,本文咱們就依次實作那些步驟,(友情提示:本文內容較多,如果你能耐心看完、寫完,一定會有提升)
1 創建工具類
在實作 cli 的程序中會涉及到組件名稱命名方式的轉換、執行cmd命令等操作,所以在開始實作創建組件前,先準備一些工具類,
在 cli/src/util/ 目錄上一篇文章中已經創建了一個 log-utils.ts 檔案,現繼續創建下列四個檔案:cmd-utils.ts、loading-utils.ts、name-utils.ts、template-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 組件新組件要做四件事:
- 創建新組件模塊;
- 創建樣式 scss 檔案并匯入;
- 在組件庫入口模塊安裝新組件模塊為依賴,并引入新組件;
- 創建組件庫檔案和 demo,
本文剩下的部分分享第一點,其余三點下一篇文章分享,
在 src 下創建 service 目錄,上面四個內容拆分在不同的 service 檔案中,并統一由 cli/src/command/create-component.ts 呼叫,這樣層次結構清晰,也便于維護,
首先在 src/service 目錄下創建 init-component.ts 檔案,該檔案用于創建新組件模塊,在該檔案中要完成如下幾件事:
- 創建新組件的目錄;
- 使用 pnpm init 初始化 package.json 檔案;
- 修改 package.json 的 name 屬性;
- 安裝通用工具包 @yyg-demo-ui/utils 到依賴中;
- 創建 src 目錄;
- 在 src 目錄中創建組件本體檔案 xxx.tsx 或 xxx.vue;
- 在 src 目錄中創建 types.ts 檔案;
- 創建組件入口檔案 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 的四個函式:indexTemplate、sfcTemplate、tsxTemplate、typesTemplate,實作如下:
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 函式中呼叫,
感謝閱讀本文,如果本文給了你一點點幫助或者啟發,還請三連支持一下,了解更多內容工薇號“程式員優雅哥”,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/543323.html
標籤:其他
上一篇:Axios
下一篇:JavaScript進階
