vue3+ts+vite组件库搭建

记录一次UI组件库搭建过程,涉及到的技术很多,也遇到很多问题,大致工程参考Element-plus仓库搭建。其中关键技术点和遇到的问题,大量借鉴各社区大佬文章及解决方案,最终得以实现,站在巨人肩膀上,致敬,学习。

下面内容,你可以跳过直接去github查看源码,如果对你有帮助,希望start一下 谢谢!github:github地址

搭建组件库-环境包管理

我们使用pnpm当做包管理工具,用pnpm workspace来实现monorepo

当使用 npm 或 Yarn 时,如果你有 100 个项目使用了某个依赖(dependency),就会有 100 份该依赖的副本保存在硬盘上。  而在使用 pnpm 时,依赖会被存储在内容可寻址的存储中,所以:

  1. 如果你用到了某依赖项的不同版本,只会将不同版本间有差异的文件添加到仓库。 例如,如果某个包有100个文件,而它的新版本只改变了其中1个文件。那么 pnpm update 时只会向存储中心额外添加1个新文件,而不会因为仅仅一个文件的改变复制整新版本包的内容。
  2. 所有文件都会存储在硬盘上的某一位置。 当软件包被被安装时,包里的文件会硬链接到这一位置,而不会占用额外的磁盘空间。 这允许你跨项目地共享同一版本的依赖。

因此,您在磁盘上节省了大量空间,这与项目和依赖项的数量成正比,并且安装速度要快得多!详细了解点击这里查看

不多bobo开整

首先需要全局安装pnpm

npm install pnpm -g // 全局安装pnpm

在你的桌面新增一个文件夹手动或者复制代码

mkdir xlz-ui  //创建项目文件cd xlz-ui  //进入目录pnpm init  //初始化package.json配置⽂件 私有库

修改package.json删除掉无用配置

{  "private": true,  "scripts": {    "test": "echo \"Error: no test specified\" && exit 1"  },  "keywords": [],  "author": "",  "license": "ISC",  "devDependencies": {    "typescript": "^4.8.4",    "vue": "^3.2.41"  }}

安装vue3typescript依赖

pnpm install vue@next typescript -D // 全局下添加依赖

创建 .npmrc

touch .npmrc

.npmrc内容添加 .npmrc配置更多详情

shamefully-hoist = true  // 作用依赖包都扁平化的安装在node_modules下面

创建tsconfig.json文件

touch tsconfig.json //创建tsconfig.jsonnpx tsc --init // 初始化ts配置文件

配置如下 如果需要了解全部配置请看这里

{  "compilerOptions": {    "module": "ESNext", // 打包模块类型ESNext    "declaration": false, // 默认不要声明⽂件    "noImplicitAny": false, // ⽀持类型不标注可以默认any    "removeComments": true, // 删除注释    "moduleResolution": "node", // 按照node模块来解析    "esModuleInterop": true, // ⽀持es6,commonjs模块    "jsx": "preserve", // jsx 不转    "noLib": false, // 不处理类库    "target": "es6", // 遵循es6版本    "sourceMap": true,    "lib": [      // 编译时⽤的库      "ESNext",      "DOM"    ],    "allowSyntheticDefaultImports": true, // 允许没有导出的模块中导⼊    "experimentalDecorators": true, // 装饰器语法    "forceConsistentCasingInFileNames": true, // 强制区分⼤⼩写    "resolveJsonModule": true, // 解析json模块    "strict": true, // 是否启动严格模式    "skipLibCheck": true, // 跳过类库检测    "types": ["unplugin-vue-define-options"] // sfc 添加 name属性的包需要的  },  "exclude": [    // 排除掉哪些类库    "node_modules",    "**/__tests__",    "dist/**"  ]}

在项目根目录下面创建pnpm-workspace.yaml配置文件。

touch pnpm-workspace.yaml

配置如下

packages:  - "packages/**" # 存放所有组件  - docs # 文档  - play # 测试组件

pnpm-workspace.yaml 定义了 工作空间 的根目录,并能够使您从工作空间中包含 / 排除目录 。 默认情况下,包含所有子目录。

创建组件测试环境

pnpm create vite play --template vue-tscd play pnpm install

在根目录新建一个typings目录,用来存放项目中通用的自定义的类型,然后把用vite创建的play/src下面的vite-env.d.ts移动到typings下面去。

在根目录下面的package.json下面添加scripts脚本。pnpm -C <path>, --dir <path>在 <path> 中启动 pnpm ,而不是当前的工作目录。

  "scripts": {    "dev": "pnpm -C play dev"   }

这样就可以在根目录执行pnpm dev启动测试服务了

创建组件目录结构

+ packages //跟目录中创建    - components  // 组件代码    - theme-chalk // 样式    - utils  // 公共方法

依次创建并初始化pnpm init修改package.json

# 以components为例子,其它同,修改name即可{  "name": "@xlz-ui/components",  "version": "1.0.0",  "description": "",  "main": "index.js",  "scripts": {    "test": "echo \"Error: no test specified\" && exit 1"  },  "keywords": [],  "author": "",  "license": "ISC"}

在根目录下安装三个子包pnpm install @xlz-ui/components @xlz-ui/theme-chalk @xlz-ui/utils -w,其它两个包同样的操作,-w--workspace代表允许安装到根目录下,不加会报错、执行-w 的命令可以在任意目录下执行都会安装在根目录,然后查看根目录下的package.json已经有了这三个包

//package.json中被添加了三个包"dependencies": {    "@xlz-ui/components": "workspace:^1.0.0",    "@xlz-ui/theme-chalk": "workspace:^1.0.0",    "@xlz-ui/utils": "workspace:^1.0.0"  }

components目录下创建icon目录,来编写一个icon组件,目录如下:

    +components        + icon            + src # 组件源代码                - icon.ts  # 放组件的props及公共方法                - icon.vue # 组件代码            - index.ts # 组件入口        + index.ts //组件整体抛出 后续为了全部导入做准备

icon.ts下来定义props

import { ExtractPropTypes } from 'vue';// 定义props类型声明export const iconProps = {  name: {    type: String,  },  size: {    type: [Number,String],  },  color: {    type: String,  },} as const//as const,会让对象的每个属性变成只读(readonly)export type IconProps = ExtractPropTypes<typeof iconProps>;

icon.vue中写组件代码

<template>  <svg :class="bem.b()" :style="style" aria-hidden="true">    <use :xlink:href="iconName"></use>  </svg></template><script lang="ts" setup>import { computed, CSSProperties } from "vue";import "./font/iconfont.js";  //这里用了阿里适量import { createNamespace } from "@xlz-ui/utils";import { iconProps } from "./icon";const bem = createNamespace("icon");defineOptions({  name: "XIcon",});const props = defineProps(iconProps);const iconName = computed(() => {  return `#xlz-${props?.name}`;});const style = computed<CSSProperties>(() => {  const { size, color } = props;  if (!color && !size) {    return {};  }  return {    ...(size ? { "font-size": size + "px" } : {}),    ...(color ? { color: color } : {}),  };});</script>

在组件入口处导出组件,index.ts

import _Icon from './src/icon.vue';import { withInstall } from '@xlz-ui/utils';const XIcon = withInstall(_Icon); // 生成带有 install 方法的组件export {//提供按需加载  XIcon}export default XIcon; // 导出组件

在icon同级的index.ts导出icon

export  * from './icon'

接下来解决上面用的withInstallcreateNamespace方法

css-BEM命名规范

具体规则这里推荐一篇文章提供参考我也是看的这篇

Js 实现部分utils/src/create.ts中写一几个方法

/** * * @param prefixName 前缀名 * @param blockName 代码块名 * @param elementName 元素名 * @param modifierName 装饰符名 * @returns  说白了 ,就是提供一个函数,用来拼接三个字符串,并用不同的符号进行分隔开来 */ function _bem(prefixName, blockName, elementName, modifierName) {  if (blockName) {    prefixName += `-${blockName}`;  }  if (elementName) {    prefixName += `__${elementName}`;  }  if (modifierName) {    prefixName += `--${modifierName}`;  }  return prefixName;}/** * * @param prefixName 前缀 * @returns */function createBEM(prefixName: string) {  const b = (blockName?) => _bem(prefixName, blockName, "", "");  const e = (elementName) =>    elementName ? _bem(prefixName, "", elementName, "") : "";  const m = (modifierName) =>    modifierName ? _bem(prefixName, "", "", modifierName) : "";  const be = (blockName, elementName) =>    blockName && elementName      ? _bem(prefixName, blockName, elementName, "")      : "";  const bm = (blockName, modifierName) =>    blockName && modifierName      ? _bem(prefixName, blockName, "", modifierName)      : "";  const em = (elementName, modifierName) =>    elementName && modifierName      ? _bem(prefixName, "", elementName, modifierName)      : "";  const bem = (blockName, elementName, modifierName) =>    blockName && elementName && modifierName      ? _bem(prefixName, blockName, elementName, modifierName)      : "";  const is = (name, state?) => (state ? `is-${name}` : "");  return {    b,    e,    m,    be,    bm,    em,    bem,    is,  };}export function createNamespace(name: string) {  const prefixName = `xlz-${name}`;  return createBEM(prefixName);}

Bem scss 部分 根据下方创建文件

theme-chalk├── package.json└── src   ├── icon.scss   ├── index.scss   ├── mixins   │   ├── config.scss   │   └── mixins.scss

config.scss

$namespace: "xlz";$element-separator: "__"; // 元素连接符$modifier-separator: "--"; // 修饰符连接符$state-prefix: "is-"; // 状态连接符* { box-sizing: border-box;}

mixins.scss

@use "config" as *;@forward "config";// xlz-icon@mixin b($block) { $B: $namespace + "-" + $block; .#{$B} {   @content; }}// xlz-icon.is-xxx@mixin when($state) { @at-root {   &.#{$state-prefix + $state} {     @content;   } }}// .xlz-icon--primary@mixin m($modifier) { @at-root {   #{& + $modifier-separator + $modifier} {     @content;   } }}// xlz-icon__header@mixin e($element) { @at-root {   #{& + $element-separator + $element} {     @content;   } }}

index.scss

@use './icon.scss';

icon.scss

@use './mixins/mixins.scss' as *;@keyframes transform { from {   transform: rotate(0deg); } to {   transform: rotate(360deg); }}@include b(icon) { width: 1em; height: 1em; line-height: 1em; display: inline-flex; vertical-align: middle; svg.loading {   animation: transform 1s linear infinite; }}

withInstall方法

utils/src/with-install.ts文件,代码如下:

import type { App, Plugin } from "vue"; // 只是导入类型不是导入App的值/*** 组件外部使用use时执行install,然后将组件注册为全局*/// 类型必须导出否则生成不了.d.ts文件export type SFCWithInstall<T> = T & Plugin;/** * 定义一个withInstall方法处理以下组件类型问题 * @param comp  */export const withInstall = <T>(comp: T) => {  /**   * 直接写comp.install = function(){} 的话会报错,因为comp下没有install方法   * 所以从vue中引入Plugin类型,断言comp的类型为T&Plugin   */  (comp as SFCWithInstall<T>).install = function (app: App) {    app.component((comp as any).name, comp);  };  return comp as SFCWithInstall<T>;};

utils/index.ts中添加

export * from './src/create'export * from './src/with-install'

icon使用阿里适量字体库搭建

登录自己的iconfont账号没有的注册一个

购物车中随便添加几个icon
添加到新建的项目中

下载后之需要将iconfont.js放在icon/src/font中并把icon引入play/src/app.vue中执行pnpm dev起服务

<template>  <XIcon name="anquanchaxun" color="red"></XIcon></template><script lang="ts" setup>import "@xlz-ui/theme-chalk/src/icon.scss";import XIcon from "@xlz-ui/components/icon";</script>

不出意外控制台会抱一个defineOptions is not defined咱们来解决一下咱们在play中安装一下unplugin-vue-define-options

pnpm i unplugin-vue-define-options -D

配置vite.config.ts

import { defineConfig } from 'vite'import DefineOptions from 'unplugin-vue-define-options/vite'import vue from '@vitejs/plugin-vue'export default defineConfig({  plugins: [vue(), DefineOptions()],})

前面忘记了安装sass现在补一下 安装在根目录 然后起服务

pnpm i sass -w -D

不出意外你会看到一个icon

想法

  1. icon 其实还有其他的方式字体的方式引入字体文件 然后用class的方式展示icon 咱们这里是用的svg形式 没有想好最用用那种方式暂时用这个
  2. 组件的按需加载与全部导入方式
  3. 组件的打包方式gulp+rollup
  4. git提交规范的设计
  5. 代码规范的设计

到此结束 欢迎一起沟通交流 欢迎大神指点✌️🫡

本文使用 文章同步助手 同步

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容