快上車!搭建一個屬於自己的組件庫!

語言: CN / TW / HK

組件在前端開發中越來越重要了,開發者更細分、聚焦於組件層面的開發,然後像搭積木一樣完成應用功能。組件庫可以統一管理組件,輸出文檔,能提升組件複用性、避免重複造輪子。趕快搭建自己的組件庫吧,這瓜保甜!

需求背景

為什麼要搭建組件庫?

雖然業界已經有很多成熟優秀的ui庫可以供我們使用,也為我們解決了很多問題。但是基礎的東西總是不能滿足所有業務場景,更多時候我們需要 擴展功能 來滿足業務的需求,好比 table 需要自定列這樣的~相信這也是很多小夥伴開發時候的場景。

  1. 跨項目複用 。很多時候為了方便,只是基於當前項目對組件進行二次封裝(反正我是這樣乾的哈哈),然後做其他項目遇到同樣場景時,要麼copy(經常忘記之前封裝在哪個項目裏了:new_moon_with_face:)、要麼重新干一個...總是缺少一個統籌的地方,複用很不方便。
  2. 組件使用文檔 。文檔產出對於一線開發來説可能相對比較欠缺,因為大家都忙於擼業務,文檔這種奢侈品能省一點是一點。這樣導致一個問題就是自己封裝的組件別人不會用、不知道在哪裏用,甚至不知道有這麼個東西。
  3. 跨團隊共建發展 。大多B端系統都是以 elementantd 等ui框架為主,基於各種業務場景,基本都會有自己團隊的二次封裝。其實類似的功能擴展肯定會有的,如果有組件庫把組件都集中起來,就能減少很多重複造輪子的勞動力了!

筆者之前就經常有這樣的痛點,在某個項目裏二次封裝了 el-select ,實現 filterable 的時搜索輸入框移到下拉列表中,避免多選時多個 tag 擠壓了搜索框的空間。當時是寫在一個項目裏,然後其他項目也遇到了這樣的需求...我在十幾個項目裏面尋找、回憶,找回當年封裝的組件,人都麻了......

正好最近在搞 雲產品 ,需要提供給各中後台統一的樣式、佈局規範以接入,還需要 統一擴展基礎組件的能力 。於是組件庫的需求的就這麼出來了!基本想做成的就是對 element-ui/plusantd 一些組件進行二次封裝、擴展,並集成到組件庫中,筆者當仁不讓把需求從大佬手上搶過來做。

目前初步搭建起來了一個簡易的組件庫了,可對 element-pluselement-ui 的組件進行開發調試,且目前已經實現幾個組件的擴展了~當然,第一版還是有很多工作沒做完、做好,不過沒關係,畢竟不能一下吃成一個胖子。更多實現、優化、還會慢慢迭代做,到時候有空會繼續分享相關的乾貨~

本文會從組件庫的 工程架構文檔組件開發環境準備打包、發佈 進行分享,組件開發環境主要是 element-ui/plus 的 ,因為本期需求只要滿足 vue2vue3 的中台,所以 antd 的還沒有投入,只好等下期了。與其説分享,其實更是做一個記錄沉澱一下,也是回顧總結~事不宜遲!開始進入主題吧, 從0-1搭建一個組件庫

一、項目架構

第一次搞組件庫,彷彿走進一個新的空白領域了。作為一個沒經驗的小白,當然是得抄作業啦,不不不,應該是“借鑑”:stuck_out_tongue_winking_eye:。這時候搞個開源的項目來參照參照還是挺香的,於是筆者就去“學習了”element-plus項目的架構、代碼組織方式,再結合自己的需求場景就開始幹了。

1. Monorepo

整個工程的代碼組織採用 Monorepo 的組織方式,使用工具 pnpm + workspace 來實現。所以全部項目都是放在一個倉庫裏的,包括文檔、組件。

工程具體分為以下幾塊,以 文檔組件庫 為兩大類進行分塊:

  1. 文檔工程(docs)
    • 安裝指引
    • 組件使用文檔( elmelpantd
    • 組件開發文檔
  2. 組件庫(packages)
    element-plus
    element-ui
    ant-design
    voice-components
    

其中 voice-components 筆者是打算用來做 adapt層 用的,因為文檔工具用了 VitePress (後面會講),它只能支持 vue3 的組件,所以 vue2react 的組件需要做一層適配,這一塊是預留的,暫時可以不關注。

第一版比較簡單,後續如果沉澱出一些工具、打包腳本等,也會再擴展幾個項目放進去 workspace 裏。所以目前就先這樣吧,用着先~

2. 文檔項目結構

抄作業抄作業,這部分跟 element-plus 基本是一致的。

  • index.md 。!!顧名思義,文檔首頁~
  • .vitepress 目錄:文檔站點工具配置相關,這個後面再展開~
  • zh-CN目錄:文檔md文件
    • components: 組件使用文檔.md 。組件的 使用demo案例代碼 ,相關 配置説明
    • guide: 組件庫指引文檔.md 。包括組件的安裝指南、開發指南
  • public目錄:相關靜態資源目錄。 cssimage
  • build目錄:放點自己實現的構建腳本、vite插件啥的

3. 組件庫結構

這部分跟 element-plus 也是基本一致的,具體大家可以參照他們的實現,這裏就記錄個大概,粗略帶過吧。

每一個ui框架的結構都一樣,以其中一個為例記錄:

  1. 組件項目入口—— 根index 。導出當前項目需要導出的所有模塊。
    export * from './components'
    ...
    複製代碼
  2. components:
    • 入口文件:index。導出所有組件。
      export * from '...'
      export * from '...'
      export * from '...'
      複製代碼
    • 存放全部組件,以組件名作為文件夾名。
  3. 組件文件夾(以button為例):
    • 入口文件:index。導出當前組件,幷包裝 install 方法(主要用於 Vue.use 調用時進行全局註冊)。
    • 組件文件。實現組件擴展的二次封裝。(這裏建議擴展組件時 保留組件的原來用法 ,這樣可以降低使用時候的學習成本)

二、組件庫工具

這裏不會面面俱到,只記錄一些用到的核心工具以及核心的用法~就算不是特別細粒度,相信大家要自己動手搞的時候也難不倒你們的!!筆者這麼菜都一樣搞,你們肯定都行!

1. 文檔站點工具—— VitePress

對於組件庫來説,文檔可以説是最關鍵的一環了,沒有文檔的組件庫不是真的組件庫~這裏筆者用了幾分鐘去調研(根本就沒怎麼調研),最終決定使用VitePress 作為文檔站點工具,目前用的版本是 1.0.0-alpha.4 。(哈哈哈大家不要害怕alpha版,用着沒啥毛病)

使用下來 基本配置用法 在官方文檔中都能找到,已經滿足當前的使用場景了~大家也要採用的話,花點時間去搓一搓就好,整個文檔站點搭建不算難,畢竟只要能跑起來就可以慢慢調整慢慢搞。

核心配置(都放在 .vitepress 目錄下):

  1. 配置文件: .vitepress 根目錄的 config 文件。其實沒有特別多的配置,主要就是 導航欄菜單欄 而已。

    export default defineConfig({
      title: 'voice-ui',
      description: '',
      base,
      head: [
        [
          'link',
          {
            rel: 'icon',
            href: '/images/favicon.ico'
          }
        ]
      ],
      themeConfig: {
        logo: '/images/favicon.ico',
        nav, // 配置導航欄
        sidebar, // 配置側邊菜單欄
        footer // 配置頁腳
      }
    })
    複製代碼
  2. nav 配置導航欄配置(文檔鏈接)

    export default [
      { text: '指南', link: '', activeMatch: '' },
      { text: 'element-plus', link: '', activeMatch: '' },
      { text: 'element-ui', link: '', activeMatch: '' },
      { text: 'ant-design', link: '', activeMatch: '' }
    ]
    複製代碼
  3. sidebar 配置側邊菜單欄(文檔鏈接)。具體配置太多就不全貼出來了,這裏的配置在文檔中都能找到。如下這樣配置就是一個 nav 路由對應一個 sidebar 菜單。

    export default {
      '/zh-CN/guide/': [
        {
          text: '安裝',
          items: [
            { text: 'element-plus', link: '' },
            ...
          ]
        },
        {
          text: '開發者指南',
          items: ...
        }
      ]
    }
    複製代碼

    大概的效果如下,不同nav對應各自的側邊欄菜單:

  4. /theme/index 中自定義主題 & 全局註冊 vue3 組件

  • 具體配置參照文檔。這裏的僅是筆者的基本配置~
    import { App } from 'vue'
    import Theme from 'vitepress/theme'
    import '../../public/css/customStyle.css' // 自定義的主題色文件
    import 'element-plus/dist/index.css'
    import 'element-plus/theme-chalk/dark/css-vars.css'
    import VcComponent from '@voice-ui/voice-components' // 上文提到的adapt層,導出vue3的組件
    
    export default {
      ...Theme,
      enhanceApp ({ app }: {app: App}) {
        app.use(VcComponent) // 進行組件註冊,這樣我們可以直接在 markdown 中使用組件啦!
      }
    }
    複製代碼
  • customStyle.css 文件其實就是對 VitePress 的一些 css變量 進行自定義重寫:point_down:
    :root {
        --vc-primary-color: #295dfa;
        ...
      }
    
      :root {
        --vp-c-brand: var(--vc-primary-color); /* 自定義 VitePress 的主題色 */
        ...
      }
    複製代碼

2. 打包工具—— Vite

提到這個必須提一嘴:開發真絲滑!是的,包括各項目的 devbuild 都是使用 Vite 完成。其實這個沒什麼好説的,大家可能用得比我都熟~所以這裏只簡單帶一帶用了什麼功能~

  1. 用到的 vue2、 vue3 插件(官方文檔戳):
    underfin/vite-plugin-vue2
    @vitejs/plugin-vue
    
  2. 打包配置——庫模式。基本的都有了。 escjsumdiife 。(官方文檔戳,具體下文會講)
  3. 配置 alias 。各模塊在 dev、prod環境 中相互引用(官方文檔戳,具體下文會講)
  4. 配置 external 。( vite配置rollup配置

第一版差不多就這些了,配置上還是比較簡單的。基本可以滿足 dev開發、build打包 需求。

三、開發環境

因為使用的 VitePress 支持在 markdown 直接使用 vue3 組件 ,所以 vue2vue3react 相關的開發環境有所不同。基於此, element-plus 的開發環境就沒有單獨搞了,直接在 docs 項目中進行組件開發。

1. vue3 + element-plus 開發環境

這裏也是直接抄作業的,模仿 element-plus 的實現。核心做法:

  • 包裝 element-plus組件 一層 install 方法
  • VitePress 中進行 全局註冊
  • md文件 中直接使用註冊好的組件,可以直接在文檔中進行開發調試

大概的代碼思路:

  1. 給組件對象添加 install 方法

    import { withInstall } from '../../utils'
    
    import Button from './button.vue'
    
    export const VcButton = withInstall(Button)
    
    export default VcButton
    
    export * from './'
    複製代碼
  2. install方法:接收一個 Vue3 對象,用 Vue.component 進行組件註冊

    export const withInstall = comp => {
      comp.install = app => {
        app.component(comp.name, comp)
      }
      return comp
    }
    複製代碼
  3. 文檔項目中,在 .vitepress/theme/index.ts 中進行全局註冊(上文也有提到)

    export default {
      enhanceApp ({ app }: {app: App}) {
        // 這裏能拿到 app ,也就是Vue3的app
        // VcButton在這裏進行全局註冊
        app.use(VcButton) // app.use 就會調用 VcButton的install方法
      }
    }
    複製代碼
  4. md中直接使用註冊好的組件

    # Button 按鈕
    
    這是一個按鈕
    
    # Element-plus
    
    ##  Button
    
    <vc-button />
    複製代碼

然後就能在頁面上看到了,並且是有熱更新的!這樣我們直接開發調試即可了。

2. vue2 + element-ui 開發環境

vue2react 的開發環境 實現思路大致相同 (react的這次還沒搞,以vue2為例就好),就是在當前項目中用vite啟動一個devServer進行開發,就跟普通的項目開發是一樣的。

  1. 根目錄建一個 index.html ,指定入口

  2. 搞個demo目錄,其實就是Vue項目,newVue完後掛載到dom上。如下: demo-xxx 的vue文件中導入開發的組件進行試用、調試。

    頁面效果如下:

react的雖然還沒做,但是具體思路也是跟vue2一樣的,在react自己的項目中起服務進行組件的開發調試~後續做了的話會補充進這裏~

四、組件打包、發佈

目前的打包、發佈實現得比較簡單。大概是統一打包,然後進到每個目錄中去進去npm發佈,目前也是隻發佈在內部的npm中。

1. 統一打包

為了打包方便,且契合當前發佈平台的特性,在整個項目的根目錄中 package.json 的 scripts 中進行了 命令整合 。這裏後續可能會用腳本的方式去實現,因為可能在打包的時候要處理一些其他的邏輯。目前第一版大概如下:

{
    "scripts": {
      "build": "pnpm run build:elp && pnpm run build:elu && pnpm run build:shared",
      "build:elp": "pnpm run -C packages/element-plus build",
      "build:elu": "pnpm run -C packages/element-ui build",
      "build:shared": "pnpm run -C packages/shared build",
      "release": "node scripts/release.ts"
    }
}
複製代碼

2. 組件庫的打包配置

上文打包工具哪裏有提到過,目前是最簡單版的,打出 es、cjs、umd、iife 格式的包,而且要external掉第三方庫。大概配置:

export default defineConfig(async ({ command, mode }) => {
  return {
    plugins: [ createVuePlugin() ],
    build: {
      rollupOptions: {
        external: ['element-ui', 'vue']
      },
      lib: {
        entry: path.resolve(__dirname, './components/index.js'),
        name: 'voiceUi',
        formats: ['es', 'cjs', 'umd', 'iife']
      }
    },
    resolve: {
      alias: await alias()
    }
  }
})
複製代碼

3. npm發佈

其實 npm包 安裝只是其中一種方式,該組件庫後續還會新增 模塊聯邦——MF 的接入方式,這個會在後續進行擴展,到時候做了的話再補充一下或者再寫一篇文章吧~

目前的npm發包時候用了個腳本,基本就是進到每個目錄下去執行以下更新版本好,然後執行 npm publish 。(這裏的腳本會結合自己發佈平台的一些能力去寫的,所以就不貼出來了,大致思路就是這樣)

五、開發時一些注意點

1. Vue版本衝突導致啟動服務、打包失敗

因為是使用 Monorepo 代碼組織方式,所以整個項目難免會出現依賴包重合(版本不同)的問題。就好比這整個項目中既裝了 Vue3 、也裝了 Vue2 ,可能起項目時會報錯:

Vue packages version mismatch:

- [email protected] 
- [email protected]
複製代碼

但是仔細檢查發現當前項目的 node_modules 中的 vuevue-compiler 是同版本的,而且是在當前工程中執行的啟動、打包。查了個 issus ,在pnpm文檔中找到了相關的解決方案:

具體文檔地址: pnpm—shared-workspace-lockfile

配置了這個文件後問題就解決了~

2. 配置alias解決入口問題

組件庫 打包後的入口開發時的入口 其實是有點 不一致 的,所以如果我們開發中直接 import xxx from 組件庫名稱 這樣導入組件是會有問題的,畢竟一般情況下我們的入口是配置打包完之後的產物的入口的(一般是dist、lib目錄下的index)。

出於這點,配置個alias就很好解決問題了,因為很多地方用到,筆者直接就封成了個函數:

export async function alias (): Promise<Array<Alias>> {
  const projectPath = packagesPath()
  const dirArr = await fsPromises.readdir(projectPath)

  return dirArr.map(packagePath => {
    return {
      find:  new RegExp(`^@voice-ui\/${packagePath}(\/(dist))?$`),
      replacement: path.join(projectPath, `/${packagePath}/index`)
    }
  })
}
複製代碼

大概作用就是把入口從 dist 下面換到當前工程的對應項目下的index入口。

3. 樣式隔離

docs項目中用的 VitePress ,他會有一些自己的樣式控制,可能會影響到我們需要在文檔中展示的組件。剛好筆者就遇到了這麼一個情況:

如圖所示,table的樣式變得很奇怪。筆者並沒有對樣式有做什麼處理,就是element-plus的table。用審查元素看了下,主要是 VitePress 也有自己的table的樣式,影響到 el-table 的表現了。

我們可以通過重寫樣式解決,但這樣在其他的如 antdelement-ui 的組件放進來的時候也會有問題。所以最好還是把樣式進行隔離。哈哈哈,其實寫到這裏的時候,筆者還沒有實現樣式隔離, 畢竟給大夥寫文章更重要嘛 !!

筆者有個大概想法是用 webComponent 去隔離,這隻好做完了再寫出來了:stuck_out_tongue_closed_eyes:,讓大家留個念想。

寫在最後

其實組件庫這個東西真是早有早好,如果當前團隊還沒有的話,趕緊搞一個吧。筆者以前也沒意識去搞這個東西(還是太菜了),所以做了很多重複的勞動。每做一些新項目,或者參與別人的項目,經常想用一些自己之前封裝的組件,都很麻煩,有時候為了不想切來切去,直接就動手寫了。現在想想,要是那時候就做了個組件庫該多好啊~其實做個簡單版的也不是很難,如果是隻需要關注一種前端框架的那就更簡單了,要考慮的東西更少。把整個組件庫搭出來之後,還能找其他小夥伴一起共建,一起維護,不斷強大自己團隊的組件庫,大家一起受益~

真的!還沒有的趕緊搭!這瓜保甜。