记录一次UI组件库搭建过程,涉及到的技术很多,也遇到很多问题,大致工程参考Element-plus仓库搭建。其中关键技术点和遇到的问题,大量借鉴各社区大佬文章及解决方案,最终得以实现,站在巨人肩膀上,致敬,学习。
下面内容,你可以跳过直接去github查看源码,如果对你有帮助,希望start一下 谢谢!github:github地址
搭建组件库-环境包管理
我们使用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.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'
接下来解决上面用的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使用阿里适量字体库搭建
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提交规范的设计
- 代码规范的设计
到此结束 欢迎一起沟通交流 欢迎大神指点✌️🫡
本文使用 文章同步助手 同步