开发组件库的准备工作~

前言

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

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

关于文档,我其实是推荐大家用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,这种配置相关的,我下面会单开一节分享给大家

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

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

相关推荐
xiaofeichaichai41 分钟前
Webpack
前端·webpack·node.js
Thecozzy1 小时前
线上 Bug 排查与修复实录
架构
鹏大师运维1 小时前
为什么信创电脑装软件总提示“软件包架构不匹配”?
linux·运维·架构·国产化·麒麟·deb·统信uos
问心无愧05131 小时前
ctf show web入门111
android·前端·笔记
唐某人丶1 小时前
模型越来越强,我们还需要 Agent 工程吗?—— 从价值重估到 Harness 实践
前端·agent·ai编程
智码看视界1 小时前
现代Web开发基础:全栈工程师的起航点
前端·后端·c5全栈
JS菌2 小时前
手写一个 AI Agent 全栈项目:从沙箱执行到子智能体的完整实现
前端·人工智能·后端
excel3 小时前
HLS TS 文件损坏的元凶:Git 提交与拉取
前端
Aphasia3113 小时前
https连接传输流程
前端·面试