從零到一,元件庫的進化

EvontNg發表於2019-12-23

前言

今年早些時候用preact 做了個新專案,並自己開發了所有元件,於是萌生了做一個元件庫的想法,在閒暇時終於慢慢地磨出來一個元件庫。當是記錄和總結一下開發的二三事吧。

如果感興趣的話,可以看看 元件庫文件 ,原始碼在 專案地址


起手式

工欲善其事,必先利其器。個人建議不著急先寫元件,一開始我就是太著急了後面就一直在大改,以經驗來說,可以先設計好我們需要的。可以從如下幾點入手進行設計:

  1. 除錯和預覽元件:

    我們需要建立demo 去預覽元件,一般是用腳手架或自己搭建一個實際開發環境去引入元件。這個時候我們就可以代入使用者的實際使用情況來考慮我們的各個方面,比如:如何做到按需載入、怎麼相容 CommonJsES Module 的元件引入等這一類情況

  2. 文件編寫:

    當我們寫完元件,需要編寫使用文件。我們可以考慮是不是可以做到程式碼即文件,怎麼高效地去寫一份好的元件文件,還有多語言支援。

  3. 指令碼設計:

    當我們需要建立和刪除元件,人工去總是可能存在遺漏,也不夠優雅,這個時候由指令碼去幫我們把一系列操作整合是更好的選擇。包括到後續的構建、自動生成文件及推送更新文件等操作,我們都可以用指令碼來輔助我們開發。

帶著這些預設,我們就可以開始從大局出發進行元件庫的設計了。

目錄架構

  1. 首先我們可以建立一個demo,大多數情況下,cli 腳手架都相較成熟完善,也是很多開發者的寫專案的預設選擇,我們可以直接用cli 去建立自己的demo,再去定製或整合一些開發工具。

    同時,最終呈現給開發者的文件大多都含有demo 演示,因此,我們可以將demo 和doc 合二為一,在開發文件時除錯demo。以筆者的專案為例,便是用preact-cli 建立的專案,然後只需要定義路由及一些loader等配置即可。

    建立完demo,我們就可以開始準備引入我們的元件庫來開發了。

  2. 我們通常需要以 import { Comp } from 元件庫名 的方式引入元件,那就需要建立整個元件庫的入口檔案,以 export { default as Comp } from './comp'; 的方式來匯出單個元件。此時目錄如下:

     |-- src/ // 原始碼
         |-- comp/ // 元件目錄
             index.tsx // 元件入口檔案
         index.ts // 整個元件庫的入口
    複製程式碼
  3. 有了入口檔案,大多陣列件都需要樣式檔案。對於樣式檔案的位置,有的人會選擇獨立於元件庫目錄,在使用時再去以如 import '元件庫名/style/comp.scss' 的方式載入樣式。

    但如果希望相容 babel-plugin-import 的方式,根據下圖可知,實際上該外掛是通過引入元件目錄下的 style 目錄,假定你設定 styletrue, 則會查詢comp/style/index.js,為css時則是comp/style/css.js, 這些js 的作用就是去以 import ../index.scss的方式去 引入元件目錄下的樣式檔案。因此,我們在編寫元件時,並不需要預先在元件中引入樣式檔案,一旦引入則無法實現按需引入了。

    另外,我們也需要在整個元件庫的目錄下再多一個樣式入口檔案,引入基類樣式和各個元件樣式,這樣就支援一次性將所有樣式引入了。

    從零到一,元件庫的進化

    此時我們的目錄又變成如下樣子了:

    |-- comp/ // 元件目錄
        |-- style/ // 元件樣式
            index.ts
            css.ts
        index.scss // 元件樣式
    複製程式碼
  4. 我們寫完元件,通常需要進行測試,同樣式檔案一樣,你可以考慮將其獨立在元件目錄外,但你查詢時就需要在兩個目錄中查詢對應的檔案,因此我會建議將測試檔案置於元件目錄中,方便管理。

  5. 元件完成了,該編寫文件了。如果你覺得需要為元件增加篇幅較多的文件說明,在元件目錄中新增一個檔案,在文件中根據實際情況自行引入,如果需要多語言支援,可以考慮在元件目錄下同樣建立文件目錄,放上不同語言的文件檔案,如:zh-CN.mden-US.md等。

    至此,我們的元件目錄已經清晰很多了,該有的都有了

     |-- comp/ // 元件目錄
         |-- style/ // 元件樣式
             index.ts
             css.ts
         |-- test/ // 測試檔案
             index.test.js
         |-- doc // 文件目錄
            index.tsx // 元件入口檔案
            index.scss // 元件樣式|-- comp/ // 元件目錄
        index.scss // 元件樣式
    複製程式碼
  6. 元件的架構完成了,接下來可以考慮設計指令碼來管理元件了。

    • 當我們每次想建立或刪除元件時,需要建立對應檔案,並在入口檔案中進行引入。 如果每次都手動操作,可能會出現遺漏,也可能會不符合預期規範,很麻煩,也不夠優雅。當我們設計好了架構之後,我們就可以根據架構寫好指令碼,每次只需呼叫指令碼,傳入元件名,即可生成符合預期的元件目錄及檔案,並在入口檔案新增依賴。刪除時只需刪除元件目錄並刪除依賴即可。

    • 當開發者想要引入我們的元件庫時,一般是引入我們構建過的程式碼,如果我們使用了typescript編寫或開發者希望使用CommonJs 的方式引入,我們都需要進行編譯。這部分可以根據情況,使用webpackgulp 進行編譯,我的專案複雜度不高,所以直接建立指令碼進行編譯。

最終目錄如下

|-- assets/ //資原始檔夾
|-- test/ // 測試初始化目錄
|-- src/ // 原始碼
    |-- comp/ // 元件目錄
        |-- style/ // 元件樣式,按需載入所需,後續講解
            index.ts
            css.ts
        |-- test/ // 測試檔案
            index.test.js
        |-- doc // 文件目錄
        index.tsx // 元件入口檔案
        index.scss // 元件樣式
    index.ts // 整個元件庫的入口js
    index.scss // 整個元件庫的樣式入口
|-- scripts/ // 指令碼
|-- doc/ // 文件
複製程式碼

開發

  • 元件

    在編寫元件時,為了更良好地使得可複用性及通用性提高,可以在設計元件時注意以下幾個方面:

    • 字首名稱空間

      通用元件總是不能完全適應所有人的需求的,開發者常常有定製的需求。為你的元件暴露一個字首名稱空間,讓使用者在定製元件上有更高的自由度,僅需修改元件的字首即可在符合約定的情況下去定製元件。

    • 檢查屬性型別

      在編寫React 元件時,定義propTypesinterface,檢查輸入屬性的型別提高了程式碼的健壯性,開發者也可以更清楚自己需要輸入什麼樣的資料。另外,良好的定義屬性及註釋,可以使用如react-docgen 之類的工具提取元件屬性來生成文件,筆者的元件庫就用了這種方式。

    • 受控與非受控

      在Vue 中,我們常常可以通過v-model 這一類語法糖來完成雙向繫結,而在react 系的語法中並沒有原生的此類支援,我們在設計元件的時候就必須清晰元件是否需要受控,儘量避免混用導致最後狀態混亂。

  • 圖示處理

    大多陣列件庫中都存在圖示元件,一般來說,可以通過svgiconfont的方式引入圖示。 兩者各有優劣,查閱之後總結了各自的優缺點,僅供參考:

    • iconfont

      優點: 向量,相容性良好,可以控制顏色及透明度甚至是漸變效果

      缺點: 1. 一些瀏覽器會進行抗鋸齒處理,導致圖示清晰度下降; 2. 無法控制圖示各部分顏色,也就是無法實現彩色圖示,通常是純色圖示;3. 會收到行高,間距等及字型相關css 屬性影響;

    • svg

      優點: 1. 不受抗鋸齒影像,同樣是向量;2. 可以控制圖示各部分顏色;3. 可以直接插入頁面,方便控制,語義化更好;

      缺點: 1. 在PC 端個別瀏覽器相容較差,但移動端相容良好;2. 可能會增加頁面體積;

  • 定製主題

    我們在編寫樣式時,一般是用Sass 或Less 前處理器來編寫,這使得我們得以方便地通過覆蓋變數來定製主題。為了讓樣式檔案支援變數覆蓋,我們可以將一些關鍵的希望可定製的變數抽離出來。

    例如,有一個calendar元件,希望可以定製其圓角,我們就可以將border-radius 設定為$calendar-border-radius,然後將該變數以$calendar-border-radius: 16px !default; 的方式寫在style/_var.scss檔案中,變數後面加!default是代表它是預設變數,可以被覆蓋。然後在元件樣式中引入../style/_var.scss 即可。

  • 構建

    1. 元件構建

      元件開發完成之後,一般會將其進行編譯為ES ModuleCommonJs 的形式,在使用Babel7.x 時,編譯配置項一般是存在babel.config.js中,它匯出了一個可傳遞配置函式 Api 的函式,意味著我們可以在執行時變更配置。

      以我的專案為例,我通過複製原始碼到libes 目錄中,並通過設定process.env 來決定以何種方式編譯。具體程式碼可以參考build-components.js 檔案。

      // scripts/build-component.js
      const fs = require('fs-extra');
      // ...
      fs.copySync(srcDir, esDir);
      compile(esDir);
      
      process.env.BABEL_MODULE = 'commonjs';
      fs.copySync(srcDir, libDir);
      compile(libDir);
      複製程式碼
      // babel.config.js
      // ...
      const { BABEL_MODULE } = process.env;
      const useESModules = BABEL_MODULE !== 'commonjs';
      return {
          presets: [
            [
              '@babel/preset-env',
              {
                loose: true,
                modules: useESModules ? false : 'commonjs'
              }
            ],
            '@babel/preset-typescript'
          ],
      // ...
      複製程式碼
    2. 樣式構建

      實際上,前面程式碼中說到按需引入所需要的style 目錄中的程式碼是不完整的,因為我們常常會在引入元件庫中的其他元件,這時候按需載入是不會幫我們找到依賴元件的樣式的,我們需要在style目錄中的入口檔案中引入依賴元件的樣式。但如果我們手動新增依賴是很麻煩的,這時候,我們可以先通過dependency-tree 去查詢依賴的元件,並檢查該依賴元件是否存在樣式檔案,若有,便新增到style目錄中的入口檔案中。

      完成了依賴元件樣式的引入之後,我們就可以將前處理器樣式編譯為css 了。編譯完成之後,要在style目錄中生成css.js以支援按需載入style 選項設定為'css' 時的查詢。

      假定A元件引入了icon元件,此時A元件style 目錄下的檔案應該如下:

      // index.js
      import '../../icon/index.scss';
      import '../index.scss';
      
      // css.js
      import '../../icon/index.css';
      import '../index.css';
      複製程式碼

      具體的構建程式碼可以在 build-style.js 中找到。

釋出

  • 提交規範

    良好的commit 記錄在什麼時候都是一個好習慣,對於後續的協同review 及管理都很重要,由於篇幅問題,這部分的實際操作可以參考 優雅的提交你的 Git Commit Message

  • npm 釋出

    一般來說,元件庫的開發者在使用我們的元件時,並不需要原始碼及文件程式碼,我們可以通過在package.json 中,增加"files": ["dist", "lib", "es"], 的欄位,限制開發者在下載的安裝包只包含哪些檔案,同時也能減少npm 體積。

    同時,我們也需要新增"main": "./lib/index.js", "module": "./es/index.js",兩個欄位,讓開發者能夠以需要的方式引入我們的程式碼,對其定義如下:

    main : 定義了 npm 包的入口檔案,browser 環境和 node 環境均可使用

    module : 定義 npm 包的 ESM 規範的入口檔案,browser 環境和 node 環境均可使用

    browser : 定義 npm 包在 browser 環境下的入口檔案

文件

元件開發完成後,如果我們一個個在文件目錄中去新增及編寫元件文件,是很麻煩的,如果元件需要刪除,我們還要去刪除對應的文件。這個時候就可以以元件為單位,用指令碼去提取文件。

元件庫在迭代的過程中,偶爾需要更改屬性名或型別,這個時候文件就需要手動更新,如果能夠讓文件及時與程式碼同步,那就更完美了。react 元件可以通過react-docgen,只要寫好 propTypesinterface,註釋完善之後,就可以提取出屬性資訊,再根據實際需求,比如載入不同語言文件的說明,然後生成目標文件。

以我的元件庫為例,在doc 目錄中會有markdown 目錄,按型別劃分文件,比如元件文件在components目錄下,通過docgen 提取資訊,生成markdown 寫入該目錄;將說明文件放在doc 目錄中,手動編寫維護。這樣的好處就是方便查詢,每次執行生成文件命令時只更新必要的文件。然後在webpack 中配置自定義的markdown-loader,通過prismjs 高亮程式碼展示。

比如元件庫中的icon 元件的interface定義如下

從零到一,元件庫的進化
最終生成的屬性列表如下
從零到一,元件庫的進化

你也可以通過諸如styleguidiststorybook 等工具去生成你喜歡的文件。

github page

文件寫完了,我們需要釋出出來讓其他人也能看到,最普遍的方式就是直接用 Github Page 託管我們的文件。 Github Page 的說明可以查閱此幫助文章,簡述就是當我們建立了<使用者名稱>.github.io 倉庫後,只要在我們當前專案倉庫下建立一個gh-pages分支(也可以是master 分支中的 docs 資料夾),就可以通過<使用者名稱>.github.io/專案名/的方式去訪問該分支下的靜態專案了。

作為程式設計師,每次提交都要切分支實在麻煩,因此,我們可以藉助 gh-pages 的力量,指定好倉庫和分支名,執行一下命令即可自動提交文件,至此,我們的文件就釋出完成了。

一些感想

  1. 如果多人開發,最好在拉取程式碼後進行bootstrap,即移除node_modules再安裝依賴,以免有人變更了依賴之後導致開發問題;
  2. 如果希望多人使用,釋出前儘量謹慎測試及檢查,做好規範和自動測試很有必要。

至此,一個元件庫的雛形就已然出現,我們可以參考成熟的元件庫,站在巨人的肩膀上,發展出自己的一套優秀的元件庫。

再次放一下地址,希望大佬們不吝賜教,讓我學習改進,謝謝。

元件庫文件

專案地址

相關文章