前言
我在上篇文章的评论中提到,我会带着大家,把每个组件的开发流程分享给大家
但是需要说的是,我并不会完全给大家一比一复刻出来,当然我这里说的是项目,比如,文档
我并不会带着大家写,因为手搓文档需要单开一个工程,过于耗时,但是组件的所有效果,思路,我都会带着大家写
关于文档,我其实是推荐大家用vitepress的,因为这样不仅开发方便,后面配置Algolia
的时候,网上教程也足够用,我们文档是手搓的,基于markdown-it
,这方面是大飞
来负责的,辛苦的很,所以如果大家没有特殊需求,推荐大家去使用vitepress
来创建文档
同时还需要说的是我重心还是会放在我们项目的开发工作上,所以文章更新的频率并不会很高,希望大家多多支持~
并且我也会带着大家通过pnpm
、monorepo
的形式带着大家来开发,那么下面咱们就开始吧~
开发前的准备
vscode
:IDE,不多说pnpm
:你有两种方式来下载pnpm
,一个是npm -g pnpm
,这里需要你的node
在16
以上,或者是卸载掉你电脑上原有的node
,并通过pnpm
官网给的形式来下载,这种方式的好处是可以类似nvm
等node版本管理工具来管理node版本
,坏处是你可能需要下载很多遍才可以生效,我个人就捣鼓了半天才成功。volar
插件:vue3官方插件
monorepo项目的搭建
首先,我们来手动创建一个monorepo
的项目
这里我新建了一个yk-design的文件夹,并且执行pnpm init
来进行初始化
然后通过vscode
打开我们的项目
建立工作空间
我们通过pnpm
来搭建monorepo
的项目需要建立一个workspace
的工作空间
新建pnpm-workspace.yaml
文件
yaml
packages:
- 'packages/**'
这样我们在packages
下创建的项目即是我们的子包
同时我现在通过pnpm create vite
新建一个demo
的vue3项目
,注意它的位置
这个demo
我打算用来进行组件的效果查看,然后我们将demo
也放在我们monorepo
的工作区下
yaml
packages:
- 'packages/**'
- 'demo'
这样,我们的demo工程
最终通过monorepo的形式
会和别的子工程进行串联
限制使用pnpm
当然,如果你需要的话,你可以禁用掉除了pnpm
以外的依赖下载的工具,那么你可以在根目录下新建一个scripts
文件夹,并新建一个preinstall.js
,然后书写下面这段代码
js
if (!/pnpm/.test(process.env.npm_execpath || '')) {
console.log('只能使用pnpm')
console.warn(
`\u001b[33mThis repository requires using pnpm as the package manager ` +
` for scripts to work properly.\u001b[39m\n`
)
process.exit(1)
}
然后你需要在package.json
中添加一段脚本
json
"scripts": { "preinstall": "node ./scripts/preinstall.js" }
这样你去使用npm i
下载的话就会出现下面的效果,用pnpm i
则是正常
还有一种方式,你可以直接在package.json
中添加脚本
json
"preinstall": "npx only-allow pnpm",
具体的方式你可以根据想要的进行选择
创建yk-design项目
我们现在packages
下通过pnpm init
初始化一个yk-design
的项目
然后我们通过-w
在根目录下安装一下我们需要用到的库
bash
pnpm add vue@next typescript less -D -w
vue@next
:安装 Vue 的下一个版本,即Vue 3。typescript
:安装 TypeScript,用于在 Vue 3 中编写TS的代码。less
:安装 Less,一种CSS预处理器,用于在Vue组件中使用Less样式。
然后我们根据目录规范,建一个test
组件
这里简单介绍一下,每个文件都代表什么
src
:组件源代码style
:组件样式文件index.ts
:组件入口文件
utils文件夹
我们会在components
同级创建一个utils
文件夹,用来存放一些公共的方法
,当然,你也可以新建一个工程,专门存放这些函数。
我现在往里面放了几个个文件bem.ts
,constant.ts
,index.ts
,tools.ts
好的,那我逐一为大家讲解这些函数的意义和作用
bem.ts
typescript
const isObject = (val: unknown): val is object => {
return val !== null && typeof val === 'object';
};
const isArray = (val: unknown): val is string[] => {
return Array.isArray(val);
};
const isString = (val: unknown): val is string => {
return typeof val === 'string';
};
type BEMElement = string;
type BEMModifier =
| (string | undefined)[]
| Record<string, boolean | string | undefined>;
const createModifier = (prefixClass: string, modifierObject?: BEMModifier) => {
let modifiers: string[] = [];
if (isArray(modifierObject)) {
modifiers = modifierObject.map((modifier) => {
return `${prefixClass}--${modifier}`;
});
} else if (isObject(modifierObject)) {
modifiers = Object.entries(modifierObject).map(([modifier, value]) => {
if (!value) return '';
return `${prefixClass}--${modifier}`;
});
}
return [prefixClass, ...modifiers].join(' ');
};
/**
* CSS BEM
* @example
* const bem = createCssScope('button')
* bem() // button
* bem('label') // button__label
* bem({ disabled }) // button button--disabled
* bem('label', { disabled }) // button__label button__label--disabled
* bem(['disabled', 'primary']) // button button--disabled button--primary
*/
export const createCssScope = (prefix: string, identity = 'yk') => {
const prefixClass = `${identity}-${prefix.replace(identity, '')}`;
return (
elementOrModifier?: BEMElement | BEMModifier,
modifier?: BEMModifier,
) => {
if (!elementOrModifier) return prefixClass;
if (isString(elementOrModifier)) {
const element = `${prefixClass}__${elementOrModifier}`;
if (!modifier) return element;
return createModifier(element, modifier);
}
return createModifier(prefixClass, elementOrModifier);
};
};
这段代码是一个用于创建CSS BEM风格
的辅助函数的实现。这段代码其实比较常见,大家在别的组件库项目中应该也见过这段代码。
至于组件样式不采用原子化css书写
,主要是考虑到不便于后期维护,而是想让template
中的结构清晰一些。为此想方便管理,采用BEM命名规范,对于不知道或者只是简单了解过的小伙伴,我这边简单介绍一下。具体可以查阅知乎文章如何看待CSS中的BEM?。
我们再回头看看这段代码
首先,定义了
isObject
、isArray
和isString
三个辅助函数。这些函数用于判断传入的值是否为对象、数组或字符串类型。它们返回的是类型谓词,即在编译时能够确定传入值的类型。
接下来,定义了两个类型别名BEMElement
和BEMModifier
。BEMElement
表示BEM风格中的元素名称,是一个字符串类型。BEMModifier
表示BEM风格中的修饰符,可以是一个字符串数组或一个包含布尔值、字符串或undefined
的键值对。
然后,定义了createModifier
函数,用于生成带有修饰符的CSS类名。该函数接受一个前缀类名和一个可选的修饰符对象作为参数。根据修饰符对象的类型,生成相应的修饰符类名数组,并将其与前缀类名拼接返回。
最后,定义了createCssScope
函数,它是对createModifier
的进一步封装。它接受一个前缀字符串和一个可选的命名空间标识符作为参数。通过调用createModifier
生成带有前缀的类名,并根据传入的元素名称或修饰符参数生成相应的类名。
通过使用createCssScope
函数,你可以方便地创建符合BEM命名规范的CSS类名。具体使用方法可以参考代码中给出的示例注释。
关于具体怎么用,在组件开发的时候会教给大家,因为封装好了,只要进行调用即可,所以比较简单
constant.ts
ts
export const TYPES = ['primary', 'secondary', 'outline'] as const;
export type Type = (typeof TYPES)[number];
export const SIZES = ['s', 'm', 'l', 'xl'] as const;
export type Size = (typeof SIZES)[number];
export const SHAPES = ['default', 'round', 'circle', 'square'] as const;
export type Shape = (typeof SHAPES)[number];
export const STATUS = ['success', 'warning', 'danger', 'primary'] as const;
export type Status = (typeof STATUS)[number];
export const MESSAGETYPE = [
'success',
'warning',
'error',
'primary',
'loading',
] as const;
export type MessageType = (typeof MESSAGETYPE)[number];
export const NOTIFICATIONTYPE = [
'primary',
'success',
'warning',
'error',
] as const;
export type NotificationType = (typeof NOTIFICATIONTYPE)[number];
export const SKIN = ['auto', 'light', 'dark'] as const;
export type Skin = (typeof SKIN)[number];
export const TITLETYPE = [...STATUS, 'secondary', 'default'] as const;
export type TitleType = (typeof TITLETYPE)[number];
export const TEXTTYPE = [
...STATUS,
'secondary',
'default',
'third',
'disabled',
] as const;
export type TextType = (typeof TEXTTYPE)[number];
export const DIRECTION = ['vertical', 'horizontal'] as const;
export type Direction = (typeof DIRECTION)[number];
export type AnimationType = 'upward' | 'fade';
这个文件理解起来较为轻松,其实就是因为一些类型,比如primary
,success
,warning
,error
等,在多处都会进行使用,那干脆直接封装起来,在别的地方直接引用即可。
通过这些常量数组和类型别名,你可以限制某些属性或变量的取值范围
,确保它们只能取特定的预定义值
,而不是任意字符串
。
index.ts
ts
import type { Plugin } from 'vue';
export type SFCWithInstall<T> = T & Plugin;
export const withInstall = <T, E extends Record<string, any>>(
main: T,
extra?: E,
) => {
(main as SFCWithInstall<T>).install = (app): void => {
for (const comp of [main, ...Object.values(extra ?? {})]) {
app.component(comp.name, comp);
}
};
if (extra) {
for (const [key, comp] of Object.entries(extra)) {
(main as any)[key] = comp;
}
}
return main as SFCWithInstall<T> & E;
};
这段代码其实也是我们在巨人的肩膀上参考的插件的注册
方式
这段代码实现了一个辅助函数 withInstall
,它用于为 Vue 组件添加 install
方法,以便能够通过 Vue 插件方式进行安装和使用。
通过使用 withInstall
函数,可以方便地扩展组件对象,使其具备 Vue 插件的安装特性,并能够一次性注册多个组件到 Vue 实例中。
styles文件夹
我们建立了如下的一些样式文件,其实很简单,就是为了降低样式代码的耦合度,相信大家在开发中也会进行一定程度的抽离工作
如图所示的,我们对很多公共的样式进行了抽离。这里我就不一一放代码了,也只会增加没必要的字数而已,用法也是less
的变量以及一些用法,相信大家可以看得懂
想拿到这些代码可以直接去我们的仓库中拿
[styles文件](yike-design-dev/packages/yike-design-ui/src/styles at monorepo-dev · ecaps1038/yike-design-dev (github.com))
当然我们还在不断地进行样式的丰富,一切都以我们的仓库为准即可。
index.ts
index.ts
是一个用于全局注册 Vue 组件
的模块
通过这段代码,可以实现全局注册 Vue 组件的功能。大家可以在
components
对象中添加组件,然后在应用程序的入口处调用app.use()
方法安装这些组件。
好的,现在我们回过头来看看我们的test组件
吧
test组件进行测试
我们下test
组件的src下会有个test.ts
,通常用来做props
的类型定义,当然也可以做emit
的类型定义,来做到一个很好的类型提示。
当然,因为这个test仅用来测试,所以先不写什么了,就是告诉大家一下,这个文件存在的意义
html
<template>
<div>
<button>Test</button>
</div>
</template>
<script setup lang="ts">
import { TestProps } from './test';
defineOptions({
name: 'YkTest'
})
withDefaults(defineProps<TestProps>(), {
})
</script>
withDefaults
是用来做props的默认值
defineOptions
函数时传入一个选项对象 { name: 'YkTest' }
,用于指定当前组件的名称为 'YkTest'
然后我们在test
的index.ts
中进行导出
ts
import Test from './src/test.vue';
import { withInstall } from '../../utils/index';
export const YkTest = withInstall(Test);
export default YkTest;
export * from './src/test';
最后我们在yk-design
目录下的index.ts
进行注册
ts
import { YkTest } from './components/test/index';
import type { Component, App } from 'vue';
import './styles/index.less';
const components: {
[propName: string]: Component;
} = {
YkTest
};
export {
YkTest
};
// 全局注册
export default {
install: (app: App) => {
for (const c in components) {
app.component(c, components[c]);
}
},
};
然后呢,我们在demo下引用一下试试
首先。我们需要在demo下引入我们的组件库
ts
import { createApp } from 'vue'
import './style.css'
import YkDesign from '../../packages/yk-design/src/index'
import App from './App.vue'
const app = createApp(App);
app.use(YkDesign);
app.mount("#app")
然后我可以在App.vue中进行使用
html
<template>
<div class="">
<YkTest></YkTest>
</div>
</template>
<script setup lang='ts'>
</script>
<style scoped></style>
这样,基础工作就可以啦~
当然,我没有加任何的样式,在下面我就带着大家开发组件,并书写样式。
后续
可能大家也想要一些规范化的内容:比如eslint
,stylelint
,这种配置相关的,我下面会单开一节分享给大家
当然,大家有想了解的也可以私信我或者评论,我会给大家补上
还有一点是,我们的这个基建工作
也会进行调整,后面我会考虑是把更改的内容,在这篇文章进行更改,还是说单开文章进行更改,这也欢迎大家的建议~