最小可行的 Vue Component Library

2020-12-26#front-end
文章目錄

最近工作上開始規劃 Design System 啦(撒花)但人人都沒有 Design System 的經驗 TAT,決定從巨人的肩膀上開始學習,所以先從分析各大公司 Design System 的 Vue Component Library 跟 Front End Master 的新課程 Desgin System with React。

什麼是 Design System ?

不知道大家在使用一些大公司的產品的時候會有一種感覺?例如:Google, Facebook ... 我們無論在他們的任何子產品、或是任何頁面,基本上不用看到 Logo 也能猜到:「啊,這是 Google 的產品!」

沒錯,這是因為這些產品是基於同一套 Design System 所打造出來的產品,以最有名的 Google Material Design 來說,扁平式的設計風格、明亮的顏色、App 右下角圓圓的按鈕 ... 等等我們習以為常的特徵,其實都是被定義過的並且實作在 Google 的每一個產品之中。

而這一切從功能、設計到工程的定義跟實踐,就是 Design System。

為什麼要做 Design System

  • Design System 可以節約工程 & 設計成本
  • Design System 可以創造出公司一致性的視覺呈現
  • Design System 可以減少工程跟設計的溝通成本
  • ...

真的嗎?答案很顯然的是:看情況。

如果公司的頁面需要不同呈現來呈現豐富內容、有許多獨特互動,實際上 Design System 的應用層面可能很窄、且維護成本還高於他原本設計的初衷、違背公司哲學,所以通常公司會在有一定規模、且穩定的設計產出情況下,推動 Design System。

正因為推動當下狀況不同,更應該根據推動的時空背景來決定要建設什麼,並要可以動態的建設這個系統。Design System 是延伸自公司的文化與設計詞彙,甚至從公司哲學出發、過程中有嚴謹的研究、工作坊來溝通。大致分為下面三大內容:

  1. Design Language: 設計部門對於設計樣式、風格的規範
  2. Component Library: 工程部門對於設計元件落實後的 Library
  3. Style Guide: 以上兩個規範的文件系統

而這三大內容有也不同的成熟度,2015 CSS conference 上 Marcelo Somers 就分享了 Design System Maturity Model,將 Design System 的完成度分為五個階段:

要實現多少端看推動 Design System 過程中的時間、資源,並且根據是 Top Down 還是 Bottom Up 推動來決定推動的方式,通常市面上常見的知名 Design System 多是大公司 Top Down 來推動,也就是獨立成立一個團隊來管理、維護這個 Design System。

但現實是...... 可能只有一個人、跟另一個也感興趣但很忙的人,不過很熱血的是你不想放棄,希望可以增加工作上的效率,所以開始著手整理 Design System。

所以作為小小前端工程師第一步可以做的事情是:先做一個最小可行的小玩意給大家看看,這東西可以有什麼好處!

一次 Bottom Up & Top Down 來回的嘗試

由於公司要漸漸的從 PHP 前後端混合的系統走向前後端分離的開發方式,故趁著這個機會在導入前端框架的同時,一邊 Bottom Up 的整理新頁面 Component 打造 Component Library 的雛形,也同時揪起設計部門來 Top Down 整理一些元件。

從工程的角度來看,過去公司網站前端上有幾個開發問題:

  • 因為切版外包的關係,有各式各樣的 CSS 命名規則與殘存的套件
  • 進一步導致過長的 CSS Selector 與 CSS 肥大等問題
  • 不同時期殘留不同的設計風格

... 由於以上種種原因,這次的建置方針就是:「輕便彈性!」除此之外,在溝通的時候發現大家對 Design System 的想像都不大一樣,於是有了這篇文章的誕生:先做一個最小可行的 Component Library 來試試看。

Style Structure

基於輕便彈性的方針,在建置 Component Library 選擇使用 Functional CSS 在 Vue 中實現 CSS 管理,雖然上手難度高一點,但搭配前處理器與 Vue 使用,可以透過組合來簡化上手門檻。

所以首先透過 vue-cli 創建一個 vue 專案,如果對 Component Library 怎麼架構沒有想法,可以參考 Atomic Design 的架構來整理整理元件:

.
├── tachyons.min.css
├── index.css
├── api.js
├── App.vue
├── main.js
├── assets
│   └── ...
├── components
│   ├── atoms
│   ├── molecules
│   ├── organisms
│   ├── templates
│   ├── pages

但由於這次以輕便為主調整成以下的形式:

.
├── tachyons.min.css
├── index.css
├── api.js
├── App.vue
├── main.js
├── assets
│   └── ...
├── components
│   ├── button
│   │   ├── index.js
│   │   ├── button.vue
│   │   ├── buttonMixin.js
│   │   ├── Readme.md // for styleguidist
│   │   └── tests // for jest
│   ├── icon
│   ├── text
│   ├── box
│   └── ...

規劃好了檔案架構跟決定了 CSS 系統後,就可以來開發第一個元件按鈕。

一 Button 一宇宙

如果不知道要怎麼開始一個 Design System 會推薦先從 Button 開始一個最小可行的雛形就對了!Button 會涉及到其他元件、也會有行為狀態可以測試、帶入的參數也需要文件來紀錄,是一個很棒的起點。

樣式設定

在正式進到 Button 之前,Button 上其實也可以分成其他元件,分別是 icon, text 跟基本的 CSS 變數,像是:顏色、字級可以設定。這次嘗試導入 Functional CSS 系統,在 Tachyons 跟 Tailwind 之間選了用過、相對容易上手的、前處理簡易 Tachyons。

故將 Tachyons Library 載下來,他會是獨立的 src 跟打包好了 minify css,將 src css 拉出來放入專案檔案之中,設定公司品牌色、字級、media query 的斷點更新到他的設定檔之中:

:root {
  --black: #DEE9F3;
  --near-black: #C9CACA;
  --dark-gray:#333;
  --mid-gray:#555;
    ...
}

並且更新原本 tachyons build css 的語法到 package.json 方便專案更新設定:

"scripts": {
    ...
    "build:css": "tachyons css/tachyons.css --minify > dist/tachyons.min.css"
}

接著就可以開始準備按鈕所需的元件了。

Icon Sprites

在 Icon 這邊,可以根據現階段公司 Icon 的多寡與客製化程度來決定要使用什麼要的匯入方法,以我自己的狀況為例,因為 Icon 數量不多且有換色需求,選擇使用 SVG Sprites 來處理 Icon 引入跟節約 request 數。

module.exports = {
  chainWebpack: config => {
    config.module
      .rule('svg')
      .uses.clear()
    config.module
      .rule('svg1')
      .test(/\\.svg$/)
      .use('svg-sprite')
        .loader('svg-sprite-loader')
        .options({
          symbolId: '[name]'
        })
        .end()
      .include
        .add(resolve('./src/assets/icons'))
        .end()
  }
}

記得,如果不想要每到一個頁面就引入一個 svg,可以在 main.js 先引入所有 SVG:

const requireAll = requireContext => requireContext.keys().map(requireContext);
const req = require.context("../../assets/icons", true, /\\.svg$/);

requireAll(req);

除此之外,也記得要將 SVG icon 的 fill 跟 stroke 設定為 none,並在 Tachyons 裡面設定 fill-color 的 class,這樣才能透過 css 更改 Icon 顏色方便後續變色使用:

.stroke-primary { stroke: #4BBEBE; }
.fill-primary { fill: #4BBEBE; }
...

在 Icon 設定完之後,進到 Framework 裡面跟 Component Library 最有關的一個議題,Component Composition。

Component Composition

Component 就像是積木,很多時候是一個疊著一個組建而成。所以在開始建置 Design System 之前,記得先熟悉框架中的 Component Composition 使用方法。舉例來說:在共用參數上,過去比較熟悉 React 習慣將 Component 放進 props 來傳遞,也可以直接用 React Hook 來共用一些 function。

但 Vue 的做法不太一樣,Vue 使用的是:mixins 與 slot。

mixin 可以先將常見共用的 props 另外打包起來,概念就像是日本的拉麵店,不管什麼麵基本上都可以選麵種、油度、辣度這些共同選項,但有少數的麵可能會多一些選項,像是:叉燒拉麵才有叉燒厚度、海鮮拉麵才有要不要干貝,那那些共用選項,我們就統一影印成一份公版的點菜單就可以了,這就是 mixin 的概念。

所以不管有哪些按鈕,基本上:按鈕連結、顏色等等這些是共同選項,我們可以獨立成一個 Mixin 檔案來整理:

export default {
    props: {
        href: {
          type: String,
        },
        color: {
          type: String,
          default: 'primary'
        }
        ...
    }
};

並讓不同的 component 可以共用這份 props 設定。

<template>
    ...
</template>
<script>
export default {
    name: 'Button',
    mixins: [buttonMixin],
    ...
};
</script>

另外一方面,Slot 可以預先在母元件上開放有這幾個欄可以放入我們自己設計的子元件,藉此來做到部分客製化的元件,就像是小時候玩的彈珠超人、戰鬥陀螺一樣,固定就是中間的金屬片、上蓋殼可以更換,其他的都是公版固定的,頂多改改顏色,這個就是 Slot。

Button 屬於小元件,使用到 slot 的狀況比較罕見,但在大型一點的元件,例如: 有圖有字有 Header 的 Card、可能有 List 有按鈕的 Navigation 等等,就很常會需要使用到 slot 的設計。

但 Button 還是有一個可以使用的地方,由於 Slot 在預設的情況下,就是只 Component 中間帶入的資訊,所以一般會將填入文字的地方使用 Slot 來減少多傳一個 Text Props。

<template>
  <a
    :href="href"
    target="_blank"
    :class="`
      ${buttonStyle}
      ${shapeClass[shape]}
      no-underline dib ph3 pv2`"
    <Label :color="color">
      <slot />
    </Label>
  </a>
</template>

在後續就可以直接在元素中間放入文字:

<template>
  <Button>
      放入想要放入的元素
  </Button>
</template>

Vue Styleguidist

當一個基礎的 Button 逐漸成形,Mixin Props 跟 Slot 都規劃好,接著就可以開始來整理文件。原本打算目前看起來資源最多的 Storybook,但需要額外設定這點不符合希望先打造大家都可以輕便的概念,所以比較了一番選擇了 Vue Styleguidist,只要撰寫 JSDoc Style 的註解在元件旁邊,就可以自動產生 Style Guide。

也就是說,只要在開發 component 的時候舉手之勞的在 props 跟 slot 旁邊寫下註解:

export default {
  props: {
    /**
     * 按鈕連結
     */
    href: {
      type: String,
    },
    /**
     * 按鈕字體顏色,使用 style 裡有定義的 color e.g. primary, white ...
     */
    color: {
      type: String,
      default: 'primary'
    },
  ...
}

但要注意到引入檔案(e.g. SVG sprites)需要額外處理,由於 Stylegudist 的架構類似於 multiple pages 的設計,所以需要另外設定 Style Guide 使用的 main.js 檔案:

// 同 main.js
const requireAll = requireContext => requireContext.keys().map(requireContext);
const req = require.context("./src/assets/icons", true, /\\.svg$/);

requireAll(req);

export default previewComponent => {
  return {
    render(createElement) {
      return createElement(previewComponent)
    }
  }
}

Test & CI

最後,作為基礎建設,測試是讓未來的開發更順利的必要過程,參考了 Ant Design Vue, Carbon Design 等等 Library 的測試規劃,以 Button 為例,它們主要在測試特殊類別的樣式正不正確 e.e. primary, default 顏色符不符合、是否正確 Render 且在一些互動限制上有沒有正確禁止 e.g. Disable ...... 等面向的內容。

故選擇使用了 Jest 撰寫類似的測試:

describe('Button', () => {
  mountTest(Button);
  ...

  it('renders shape class correctly', () => {
    const wrapper1 = mount({
      render() {
        return <Button shape="round">Follow</Button>;
      },
    });
    expect(wrapper1.contains('.br2')).toBe(true);
    const wrapper2 = mount({
      render() {
        return <Button shape="circle">Follow</Button>;
      },
    });
    expect(wrapper2.contains('.br-100')).toBe(true);
  });

  it('renders icon correctly', () => {
    const wrapper = mount({
      render() {
        return <Button icon="article">Follow</Button>;
      },
    });
    expect(wrapper.findAll('svg').length).toBe(1);
  });

  ...

});

加上,這個 Component Library 會在 Github 上維護跟 Deploy Style Guide 頁面,所以直接選用 Travis CI 在 push 的時候自動測試,下面是一個非常簡易的 .travis.yml 的示意:

language: node_js
node_js:
  - 13
script:
  - npm run test:unit

小結

由於大部分的 Component Library React 的資源比較多,加上前兩天 FrontEnd Master 釋出了 Design System 的課程(RRRR 相見恨晚)催生了這篇文章,分享輕量簡便下 Vue 跟不同的工具選擇與自己的嘗試。

推薦 React 生態系的大家可以看看那堂課,也可以試試看用按鈕嘗試開始 Design System。在 Design System 這個主題上可以接觸到不少不同面向議題,是一件很有趣的事情!

其它文章


© 2020 minw Powered by Gatsby