开发组件库的准备工作~

前言

我在上篇文章的评论中提到,我会带着大家,把每个组件的开发流程分享给大家

但是需要说的是,我并不会完全给大家一比一复刻出来,当然我这里说的是项目,比如,文档我并不会带着大家写,因为手搓文档需要单开一个工程,过于耗时,但是组件的所有效果,思路,我都会带着大家写

关于文档,我其实是推荐大家用vitepress的,因为这样不仅开发方便,后面配置Algolia的时候,网上教程也足够用,我们文档是手搓的,基于markdown-it,这方面是大飞来负责的,辛苦的很,所以如果大家没有特殊需求,推荐大家去使用vitepress来创建文档

同时还需要说的是我重心还是会放在我们项目的开发工作上,所以文章更新的频率并不会很高,希望大家多多支持~

并且我也会带着大家通过pnpmmonorepo的形式带着大家来开发,那么下面咱们就开始吧~

开发前的准备

  • vscode:IDE,不多说
  • pnpm:你有两种方式来下载pnpm,一个是npm -g pnpm,这里需要你的node16以上,或者是卸载掉你电脑上原有的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新建一个demovue3项目,注意它的位置

这个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
  1. vue@next:安装 Vue 的下一个版本,即Vue 3。
  2. typescript:安装 TypeScript,用于在 Vue 3 中编写TS的代码。
  3. less:安装 Less,一种CSS预处理器,用于在Vue组件中使用Less样式。

然后我们根据目录规范,建一个test组件

这里简单介绍一下,每个文件都代表什么

  • src:组件源代码
  • style:组件样式文件
  • index.ts:组件入口文件

utils文件夹

我们会在components同级创建一个utils文件夹,用来存放一些公共的方法,当然,你也可以新建一个工程,专门存放这些函数。

我现在往里面放了几个个文件bem.tsconstant.tsindex.tstools.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?

我们再回头看看这段代码

首先,定义了isObjectisArrayisString三个辅助函数。这些函数用于判断传入的值是否为对象、数组或字符串类型。它们返回的是类型谓词,即在编译时能够确定传入值的类型。
接下来,定义了两个类型别名BEMElementBEMModifierBEMElement表示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';

这个文件理解起来较为轻松,其实就是因为一些类型,比如primarysuccesswarning,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'

然后我们在testindex.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>

这样,基础工作就可以啦~

当然,我没有加任何的样式,在下面我就带着大家开发组件,并书写样式。

后续

可能大家也想要一些规范化的内容:比如eslintstylelint,这种配置相关的,我下面会单开一节分享给大家

当然,大家有想了解的也可以私信我或者评论,我会给大家补上

还有一点是,我们的这个基建工作也会进行调整,后面我会考虑是把更改的内容,在这篇文章进行更改,还是说单开文章进行更改,这也欢迎大家的建议~

相关推荐
秦jh_13 分钟前
【Linux】多线程(概念,控制)
linux·运维·前端
蜗牛快跑21326 分钟前
面向对象编程 vs 函数式编程
前端·函数式编程·面向对象编程
Dread_lxy27 分钟前
vue 依赖注入(Provide、Inject )和混入(mixins)
前端·javascript·vue.js
涔溪1 小时前
Ecmascript(ES)标准
前端·elasticsearch·ecmascript
榴莲千丞1 小时前
第8章利用CSS制作导航菜单
前端·css
奔跑草-1 小时前
【前端】深入浅出 - TypeScript 的详细讲解
前端·javascript·react.js·typescript
羡与2 小时前
echarts-gl 3D柱状图配置
前端·javascript·echarts
guokanglun2 小时前
CSS样式实现3D效果
前端·css·3d
咔咔库奇2 小时前
ES6进阶知识一
前端·ecmascript·es6
渗透测试老鸟-九青2 小时前
通过投毒Bingbot索引挖掘必应中的存储型XSS
服务器·前端·javascript·安全·web安全·缓存·xss