vue组件开发的原理(Vue3 企业级优雅实战 – 组件库框架 – 9 实现组件库 cli – 上)
上文搭建了组件库 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 ../config3.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 函数中调用 。
感谢你阅读本文 ,如果本文给了你一点点帮助或者启发 ,还请三连支持一下,点赞 、关注、收藏 ,公\/同号 程序员优雅哥更多分享 。
创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!