使用vue3+vite開發一個仿element ui框架
theme: channing-cyan highlight: an-old-hope
看完這篇文章,你會有以下新的認識: 1. 如何使用vue3+vite封裝外掛併發布到npm 2. 如何構建一個ui框架文件網站 3. 外掛開發中的技巧
前言
在平日的開發中,我們經常使用不同的ui框架,不知道大家有沒有想法自己開發一個自己的ui框架,或許很多人感覺,沒有必要重複造輪子,但是現在前端工程師的要求越來越高,需要的技術棧也越來越多,學習一下這個開發流程和一些解決方案還是很有必要的。而且我覺得,最重要的是,在平時的專案開發中,會有許多ui框架無法覆蓋的元件,這是和這個業務比較繫結的,獨屬於這個業務需求的元件,當這個業務比較大的時候,這個元件就需要有更高的靈活性和易用性,有時候使用現有的ui框架進行二次封裝也具有一定的成本,甚至高過從頭開發,所以在這種情況下,我們就可以把常用的元件,封裝成ui外掛,配合上完整的元件文件,無論是方便以後專案迭代的時候檢視,還是分享給其他人,都是極好的。
下面,仿照element plus官網的樣子,來仿一個ui框架,以此講述開發流程和用到的技術與方案。成品展示:
倉庫地址: http://gitee.com/biluo_x/biluo-ui
npm地址:biluo-ui - npm (npmjs.com)
技術棧
- vue3 前端主流框架之一,這裡我們使用3.2版本
- vite 代替vue-cli的新腳手架
- typescript js的超集,提供型別系統
- vite-plugin-md vite的md外掛,提供把md檔案當做vue匯入的能力,最厲害的是,也可以在md檔案中使用vue元件
- tailwindcss 為了快速得到效果,使用原子類提供樣式
- prismjs 在程式碼展示的時候,提供程式碼高亮
如果沒有使用過tailwindcss 可以看看這篇文章:受夠了重複繁瑣的css?來試試原子類吧 - 掘金 (juejin.cn)
需求分析
我們是仿照element plus來寫的所以,我們可以觀察一下element 的展示情況。
拋開那些其他的功能,主要部分分為三個,左邊根據元件分類的導航欄,中間的展示文件,以及右邊的文件目錄。
先看左側導航
一個元件對應了一個目錄,而我們需要把同種的目錄分組,比如基礎元件放一項,表單元件放一項等。
再看主體文件
1. 主體文件應該使用markdown編寫,一個元件對應一個md檔案,所以我們需要有在vue中匯入md的功能。
2. 元件有不同的功能,需要提供一個演示框,這個演示框裡面會放不同的元件功能展示,以及固定的檢視程式碼,貼上程式碼,前往倉庫的固定功能。我可以發現這個演示框應該是一個vue元件,所以需要有在md檔案中匯入vue元件的功能
最後看右側的目錄
1. 目錄需要自動提取md檔案中的標題
2. 目錄需要跟著文件滾動而滾動
3. 點選目錄可以跳轉到對應的標題
目錄介紹
專案使用vite初始化,選擇vue3+ts模板,然後包管理器使用的是yarn。具體初始化就不獻醜了。
除此之外,我這裡加入了eslint+prettier
為程式碼格式化,[email protected]/test-utils來提供測試支援(寫了兩三個元件測試就懶得寫了...),這些沒有也不影響開發,這裡提一嘴。
目錄規劃如下:
1. src 和平時的頁面開發一致,這裡存放展示在外的文件頁面,打包成文件網站使用
2. packages 這裡存放我們ui元件相關的程式碼。主要結構如下:
在components資料夾下編寫ui元件,一個資料夾表示一個元件,元件中,src存放元件檔案,__tests__存放測試程式碼,index.ts 提供預設匯出。當然components資料夾下還有一個index.ts提供統一入口,匯出所有的元件。
元件開發
這裡我們用button元件的開發來展示基礎開發流程,用input元件的開發來講述vue3更好的開發方式。
button元件
button元件的資料夾結構
components
├── button
│ ├── __tests__
│ │ ├── button.test.ts // bl-button.vue 測試
│ │ └── buttonGroup.test.ts // bl-button-group.vue 測試
│ └── src
│ └── bl-button.vue // button 元件
|__bl-button-group.vue // button 組
├── index.ts // 模組匯出檔案
|── index.ts // 元件庫匯出檔案
在button資料夾下的index.ts中我們將src下的兩個元件暴露出去:
packages/components/button/index.ts
```js
import BlButton from './src/bl-button.vue'
import BlButtonGroup from './src/bl-button-group.vue'
import { App } from 'vue'
export default {
install(app: App) {
app.component('BlButton', BlButton)
app.component('BlButtonGroup', BlButtonGroup)
}
}
export { BlButtonGroup, BlButton }
這裡選擇了兩種匯出,主要是為了能直接全域性註冊的同時,也支援單獨引用。
然後在總的index.ts中全部匯出:
`packages/components/index.ts`
js
import { App } from 'vue'
export * from './button'
import button from './button'
const components = [button]
export default {
install(app: App) {
components.map((item) => item.install(app))
}
}
```
後續如果需要新增新的元件,按這個流程匯入即可。下面讓我們來看一下button元件的具體開發:
```js
這個程式碼看起來不少,實際上很簡單,最多的就是,prop和根據prop對類名進行處理。button的所有樣式都是使用css來控制的。js只在原生屬性上面稍微處理了一下。這個程式碼其實寫的不好,在類名處理哪裡寫了一堆的三元表示式,後來發現element原始碼裡面寫弄了一個hook專門搞這個,我也去整了一個,程式碼很簡單,大概就是根據bool改變類名之類的:
ts
type namespaceStyle = 'backgroundColor' | 'color' | 'width' | 'height'
export const DEFAULT_NAMESPACE = 'bl'
export const STATE_PREFIX = 'is'
export const useNamespace = (namespace: string) => {
return {
b() {
return ${DEFAULT_NAMESPACE}-${namespace}
},
is(state: boolean, name: string) {
return name && state ? ${STATE_PREFIX}-${name}
: ''
},
m(suffix: string) {
if (suffix) {
return ${DEFAULT_NAMESPACE}-${namespace}-${suffix}
}
return ''
},
sy(data: string, label: namespaceStyle) {
return {
[label]: data
} as CSSProperties
},
is_sy(is: Boolean, one: CSSProperties, two?: CSSProperties) {
if (!two) {
if (is) return one
return {} as CSSProperties
}
if (is) {
return one
} else {
return two
}
}
}
}
有了這個後,後來的類名處理就寫了這樣
ts
``` 開發方面都很簡單,就不過多贅述了.
input 元件
這裡為什麼把input元件單獨拿出來說一下呢,因為大家也看到了上面button的程式碼,功能不多,但是程式碼量特別大,而且繁瑣。實際上,vue3的開發方式並不是這樣的,上面的開發把全部都合併到一起了,有點像以前vue2的感覺,我們來看一下input元件。用過element的朋友應該知道,input元件在開啟清除按鈕後,滑鼠滑入按鈕才會顯示,滑出後又會隱藏。這個功能我們要怎麼實現呢,其實很簡單,用一個bool變數,然後監聽滑鼠的滑入和滑出事件嘛。在這裡我們選擇封裝成hook的寫法,其實就是利用閉包
ts
export const useMouseEnterLeave = () => {
const mouse_is = ref(false)
return {
mouse_is,
enter: () => (mouse_is.value = true),
leave: () => (mouse_is.value = false)
}
}
然後在vue中引用
ts
const { mouse_is, enter, leave } = useMouseEnterLeave()
因為vue3把響應式的功能封裝成了ref和reactive這兩個函式,不像以前vue2必須寫在data函式返回值裡面才具備相應監聽,這樣就讓我們開發與封裝更加靈活多變。
路由設計
根據上面的對元件導航欄的分析,我們可以發現,這是由多個型別元件的集合組成的大路由。簡而言之,就是一個一級標題代表的就是該分類下的所有元件。
原本我是打算把它設計成陣列的,但是考慮到對不同模組的顯示隱藏的控制,最終把它設計為了一個物件,各位可以根據自己的實際情況自行處理。
元件路由的型別如下
ts
export interface routerType {
title: string
routerData: RouteRecordRaw[]
}
這是具體設計
/src/router/routerConfig/index.ts
ts
export const routerDocsComponentConfig = {
index: {
title: '前言',
routerData: beforeComponent
},
baseComponents: {
title: 'Basic 基礎元件',
routerData: baseComponent
},
dataShowComponents: {
title: 'Data 資料展示',
routerData: dataShowComponent
},
...
}
基礎路由就是正常vue-router配置的型別
/src/router/routerConfig/base.component.ts
ts
// 基礎元件路由
export const baseComponent: RouteRecordRaw[] = [
{
path: 'button',
meta: { title: 'Button 按鈕' },
component: () => import('../../docs/button/README.md')
},
{
path: 'layout',
meta: { title: 'Layout 佈局' },
component: () => import('../../docs/layout/README.md')
},
{
path: 'container',
meta: { title: 'Container 佈局容器' },
component: () => import('../../docs/container/README.md')
},
{
path: 'icon',
meta: { title: 'Icon 圖示' },
component: () => import('../../docs/icon/README.md')
}
]
以基礎元件路由舉例,我們把基礎路由相關的文件全部放在這裡。可以看到這裡引用的元件是一個md檔案,具體操作我們等下會講到。
具體的使用就是在通用路由中配置需要顯示的模組的key.
/src/components/doc-component-pag.vue
```ts
``` asideKeys裡面配置了需要顯示的路由模組,可以通過引數的順序和增傷進一步控制導航的顯示。
文件主體
上面我們說到每一個元件路由其實是一個md檔案。要想在vue中正常解析md.我們需要下載一個vite外掛。
js
yarn add [email protected]
為什麼使用這個固定版本,因為當時我下載的最新版,有一個bug,就是無法在md文件中匯入vue元件,通過它gitHub上提的issues說這個問題已經被解決,但是npm沒有更新,現在不曉得更新了沒得,但是我們不需要太多功能,這個版本夠用了
接下來我們在vite的配置檔案裡面配置它
ts
plugins: [
vue({ include: [/.vue$/, /.md$/] }),
vueJsx(),
Markdown({
markdownItSetup(md) {
// add anchor links to your H[x] tags
md.use(require('markdown-it-anchor'))
}
})
]
這裡用到了markdown-it-anchor這個外掛,這個外掛的作用是在上面那個外掛生成vue元件時候,把h標籤的內容作為它的id,這樣我們就可以通過id跳轉的方式從目錄跳轉到指定內容了。
如果你使用的是ts,請在環境中提供md支援,將其檔案型別定義為vue元件
ts
declare module '*.md' {
const Component: ComponentOptions
export default Component
}
接下來我們就可以愉快的使用vue和md雙向匯入功能了。
vue匯入md就不多說了,直接匯入作為元件就是,在md中使用vue元件的方法,這裡簡單說一下,md中可以用兩種元件.
1. 全域性元件 直接當html標籤使用,可以直接解析
2. 區域性元件,在md檔案中匯入使用,使用方式如下:
以上,我們就完成了md引入vue元件的操作,接下來我們來開發程式碼展示元件。 一共三個區域。 1. 展示區:通過slot,展示外部元件。 2. 控制元件去:前往倉庫,一鍵複製,程式碼展示,三個控制元件 3. 程式碼區:獲取展示區傳入的外部元件的程式碼,加上程式碼高亮展示 這個元件本身很簡單,因為使用頻繁,所以我們直接註冊為全域性元件,這樣就可以直接在md檔案中引入,而展示區的程式碼,則通過區域性引入的方式,匯入進行展示。檔案結構如下:
每一個展示區,對應一個vue檔案,這樣控制粒度更加精細。
程式碼展示
下面我們來看看程式碼展示功能是如何實現的,vite可以通過如這種形式import xx from 'xx?raw'
把一個檔案標記為資原始檔,從而獲取檔案的內容,我們可以通過這種形式,獲取展示區的程式碼。但是這種方式只能在開發環境得到支援,所以生產環境需要換成網路請求的方式,具體程式碼如下:
/src/components/common/show-code.vue
ts
onMounted(async () => {
const isDev = import.meta.env.MODE === 'development'
if (isDev) {
/* @vite-ignore */
const data: any = await import(/* @vite-ignore */ `../../docs/${props.showPath}.vue?raw`)
sourceCode.value = data.default
} else {
sourceCode.value = await fetch(`/docs/${props.showPath}.vue`).then((res) => res.text())
}
await nextTick(() => {
Prism.highlightAll()
})
})
判斷是否是開發環境,選擇靜態資源載入或者網路請求。這裡也可以看到,在開發環境下,我們需要把docs資料夾複製一份到打包後的根路徑。開發到後期經常打包,這樣手動cv實在是太惱火了,這裡寫了一個指令碼,在打包後自己複製過去,用到了copy-dir這個包,需要自行下載
js
let copydir = require('copy-dir')
copydir.sync(
process.cwd() + '/src/docs',
process.cwd() + '/BiLuoUiDoc/docs',
{
utimes: true,
mode: true,
cover: true
},
function (err) {
if (err) throw err
console.log('done')
}
)
使用方式只需要在原本的打包命令後加上,就會自動在打包後執行這個程式碼,node後面是程式碼所在相對路徑。
js
&& node ./config/copyDocs.js
一鍵複製
一鍵複製功能就比較簡單了,就是把程式碼的內容複製給一個input,進入選擇狀態後控制鍵盤執行copy指令
/src/components/common/show-code.vue
ts
// 複製程式碼
const copyCode = () => {
const input = document.createElement('input')
document.body.appendChild(input)
input.setAttribute('value', sourceCode.value as string)
input.setAttribute('readonly', 'readonly')
input.select()
input.setSelectionRange(0, 9999) // 如果select 沒有選擇到
if (document.execCommand('copy')) {
// console.log('報文已複製到剪下板')
BlMessageFn.success!({
message: '成功複製',
duration: 2000
})
}
document.body.removeChild(input)
}
md檔案使用方式
當我們把show-code元件全域性註冊後,就可以在md檔案中使用它了
html
<show-code showPath="button/baseButton">
<baseButton></baseButton>
</show-code>
showPath是展示元件的路徑,以便在展示程式碼的時候,獲取對應的資料。
具體細節請檢視
文件
打包上傳npm
編寫元件打包配置:
/config/prod.com.config.ts
```ts
import { defineConfig } from 'vite'
// import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import baseConfig from './base.config' // 主要用於alias檔案路徑別名
export default defineConfig({
...baseConfig,
// 打包配置
build: {
sourcemap: false, //不開啟映象
outDir: 'BiLuoUI',
assetsInlineLimit: 8192, // 小於 8kb 的匯入或引用資源將內聯為 base64 編碼
terserOptions: {
// 生產環境移除console
compress: {
drop_console: true,
drop_debugger: true
}
},
lib: {
entry: resolve(process.cwd(), './packages/components/index.ts'), // 設定入口檔案
name: 'biluo-ui', // 起個名字,安裝、引入用
fileName: (format) => biluo-ui.${format}.js
// 打包後的檔名
},
rollupOptions: {
// 確保外部化處理那些你不想打包進庫的依賴
external: ['vue', 'tailwindcss', '@element-plus/icons-vue'],
output: {
// 在 UMD 構建模式下為這些外部化的依賴提供一個全域性變數
globals: {
vue: 'Vue',
tailwindcss: 'tailwindcss',
'@element-plus/icons-vue': '@element-plus/icons-vue'
}
}
}
}
})
配置package.json
json
{
"name": "biluo-ui",
"auther": "biluo. Email: [email protected]",
"private": false,
"version": "1.0.6",
"description": "這是一個模仿element ui寫的ui元件。用以練手和學習。",
"keyword": "typescript tailwindcss ui element",
"files": ["BiLuoUI"],
"main": "./BiLuoUI/biluo-ui.es.js",
"module": "./BiLuoUI/biluo-ui.es.js",
"repository": "http://gitee.com/biluo_x/biluo-ui",
...
}
這裡最重要的是這三個欄位,files,main,module
- files: 設定你要上傳的目錄,寫上我們打包輸出的目錄
- main: 專案主入口 這裡主要是require引用的入口
- module: 同樣的主入口,這裡是import引入的入口,比如我使用
ts
import BlUi from 'biluo-ui'
``
預設就是匯入:
./BiLuoUI/biluo-ui.es.js`。
因為這是一個ui框架,用不上require匯入,所以我們都寫的一樣的入口檔案。
打包生成BiLuoUI:
上傳npm: - 登陸 執行npm login命令,系統會提示輸入賬戶和密碼。如果沒有npm賬戶,請註冊 → npm官網 - 釋出 若賬戶登入成功後,就可以再次執行 npm publish 進行釋出 - 注意 1. 每次釋出,都需要更新版本號,否則無法成功上傳 2. 上傳到npm上時,要將package.json中的private屬性值改為false
最後
這裡大概是梳理了一下開發一個開源元件網站的方案和基本流程,希望對有此想法的朋友提供一定的幫助。文章並沒有太過詳細的簡述ui元件的開發,相信這對大家來說都不是什麼問題。如果有什麼其他需要的可以自行檢視本專案倉庫。