記錄一次UI組件庫搭建過程擦酌,涉及到的技術很多俱诸,也遇到很多問題,大致工程參考Element-plus倉庫搭建仑氛。其中關鍵技術點和遇到的問題乙埃,大量借鑒各社區(qū)大佬文章及解決方案,最終得以實現(xiàn)锯岖,站在巨人肩膀上介袜,致敬,學習出吹。
下面內(nèi)容遇伞,你可以跳過直接去github查看源碼,如果對你有幫助捶牢,希望start一下 謝謝鸠珠!github:github地址
搭建組件庫-環(huán)境包管理
我們使用pnpm
當做包管理工具,用pnpm workspace
來實現(xiàn)monorepo
秋麸。
當使用 npm 或 Yarn 時渐排,如果你有 100 個項目使用了某個依賴(dependency),就會有 100 份該依賴的副本保存在硬盤上灸蟆。 ?而在使用 pnpm 時驯耻,依賴會被存儲在內(nèi)容可尋址的存儲中,所以:
- 如果你用到了某依賴項的不同版本炒考,只會將不同版本間有差異的文件添加到倉庫寥茫。 例如藕赞,如果某個包有100個文件神年,而它的新版本只改變了其中1個文件玄货。那么?
pnpm update
?時只會向存儲中心額外添加1個新文件,而不會因為僅僅一個文件的改變復制整新版本包的內(nèi)容瓤帚。 - 所有文件都會存儲在硬盤上的某一位置描姚。 當軟件包被被安裝時涩赢,包里的文件會硬鏈接到這一位置,而不會占用額外的磁盤空間轩勘。 這允許你跨項目地共享同一版本的依賴谒主。
因此,您在磁盤上節(jié)省了大量空間赃阀,這與項目和依賴項的數(shù)量成正比霎肯,并且安裝速度要快得多!詳細了解點擊這里查看
不多bobo開整
首先需要全局安裝pnpm
npm install pnpm -g // 全局安裝pnpm
在你的桌面新增一個文件夾手動或者復制代碼
mkdir xlz-ui //創(chuàng)建項目文件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 // 全局下添加依賴
創(chuàng)建 .npmrc
touch .npmrc
.npmrc
內(nèi)容添加 .npmrc配置更多詳情
shamefully-hoist = true // 作用依賴包都扁平化的安裝在node_modules下面
創(chuàng)建tsconfig.json
文件
touch tsconfig.json //創(chuàng)建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, // 強制區(qū)分??寫 "resolveJsonModule": true, // 解析json模塊 "strict": true, // 是否啟動嚴格模式 "skipLibCheck": true, // 跳過類庫檢測 "types": ["unplugin-vue-define-options"] // sfc 添加 name屬性的包需要的 }, "exclude": [ // 排除掉哪些類庫 "node_modules", "**/__tests__", "dist/**" ]}
在項目根目錄下面創(chuàng)建pnpm-workspace.yaml
配置文件榛斯。
touch pnpm-workspace.yaml
配置如下
packages: - "packages/**" # 存放所有組件 - docs # 文檔 - play # 測試組件
pnpm-workspace.yaml
?定義了?工作空間?的根目錄观游,并能夠使您從工作空間中包含 / 排除目錄 。 默認情況下驮俗,包含所有子目錄懂缕。
創(chuàng)建組件測試環(huán)境
pnpm create vite play --template vue-tscd play pnpm install
在根目錄新建一個typings
目錄王凑,用來存放項目中通用的自定義的類型搪柑,然后把用vite
創(chuàng)建的play/src
下面的vite-env.d.ts
移動到typings
下面去。
在根目錄下面的package.json
下面添加scripts
腳本索烹。pnpm -C?<path>, --dir?<path>
在?<path>
?中啟動 pnpm 工碾,而不是當前的工作目錄。
"scripts": { "dev": "pnpm -C play dev" }
這樣就可以在根目錄執(zhí)行pnpm dev
啟動測試服務了
創(chuàng)建組件目錄結構
+ packages //跟目錄中創(chuàng)建 - components // 組件代碼 - theme-chalk // 樣式 - utils // 公共方法
依次創(chuàng)建并初始化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
代表允許安裝到根目錄下垒拢,不加會報錯旬迹、執(zhí)行-w 的命令可以在任意目錄下執(zhí)行都會安裝在根目錄,然后查看根目錄下的package.json
已經(jīng)有了這三個包
//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
目錄下創(chuàng)建icon目錄求类,來編寫一個icon組件奔垦,目錄如下:
+components + icon + src # 組件源代碼 - icon.ts # 放組件的props及公共方法 - icon.vue # 組件代碼 - index.ts # 組件入口 + index.ts //組件整體拋出 后續(xù)為了全部導入做準備
在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命名規(guī)范
Js 實現(xiàn)部分utils/src/create.ts
中寫一幾個方法
/** * * @param prefixName 前綴名 * @param blockName 代碼塊名 * @param elementName 元素名 * @param modifierName 裝飾符名 * @returns 說白了 椿猎,就是提供一個函數(shù),用來拼接三個字符串仓技,并用不同的符號進行分隔開來 */ 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 部分 根據(jù)下方創(chuàng)建文件
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-"; // 狀態(tài)連接符* { 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時執(zhí)行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
中執(zhí)行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
現(xiàn)在補一下 安裝在根目錄 然后起服務
pnpm i sass -w -D
不出意外你會看到一個icon
想法
- icon 其實還有其他的方式字體的方式引入字體文件 然后用
class
的方式展示icon 咱們這里是用的svg形式 沒有想好最用用那種方式暫時用這個 - 組件的按需加載與全部導入方式
- 組件的打包方式gulp+rollup
- git提交規(guī)范的設計
- 代碼規(guī)范的設計
到此結束 歡迎一起溝通交流 歡迎大神指點????
本文使用 文章同步助手 同步