记录一次UI组件库搭建过程,涉及到的技术很多,也遇到很多问题,大致工程参考Element-plus仓库搭建。其中关键技术点和遇到的问题,大量借鉴各社区大佬文章及解决方案,最终得以实现,站在巨人肩膀上,致敬,学习。
下面内容,你可以跳过直接去github查看源码,如果对你有帮助,希望start一下 谢谢! github:github地址
搭建组件库-环境包管理
搭建monorepo环境
我们使用pnpm当做包管理工具,用pnpm workspace来实现monorepo。
当使用 npm 或 Yarn 时,如果你有 100 个项目使用了某个依赖(dependency),就会有 100 份该依赖的副本保存在硬盘上。 而在使用 pnpm 时,依赖会被存储在内容可寻址的存储中,所以:
- 如果你用到了某依赖项的不同版本,只会将不同版本间有差异的文件添加到仓库。 例如,如果某个包有100个文件,而它的新版本只改变了其中1个文件。那么
pnpm update时只会向存储中心额外添加1个新文件,而不会因为仅仅一个文件的改变复制整新版本包的内容。 - 所有文件都会存储在硬盘上的某一位置。 当软件包被被安装时,包里的文件会硬链接到这一位置,而不会占用额外的磁盘空间。 这允许你跨项目地共享同一版本的依赖。
因此,您在磁盘上节省了大量空间,这与项目和依赖项的数量成正比,并且安装速度要快得多! 详细了解点击这里查看
不多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"
}
}
安装vue3和typescript依赖
pnpm install vue@next typescript -D // 全局下添加依赖
创建 .npmrc
touch .npmrc
.npmrc内容添加 .npmrc配置更多详情
shamefully-hoist = true // 作用依赖包都扁平化的安装在node_modules下面
创建tsconfig.json文件
touch tsconfig.json //创建tsconfig.json
npx 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-ts
cd 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'
接下来解决上面用的withInstall与createNamespace方法
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使用阿里适量字体库搭建
购物车中随便添加几个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
想法
- icon 其实还有其他的方式字体的方式引入字体文件 然后用
class的方式展示icon 咱们这里是用的svg形式 没有想好最用用那种方式暂时用这个 - 组件的按需加载与全部导入方式
- 组件的打包方式gulp+rollup
- git提交规范的设计
- 代码规范的设计
到此结束 欢迎一起沟通交流 欢迎大神指点✌️🫡