提升元件庫通用能力 - NutUI 線上主題定製功能探索

京東設計中心JDC發表於2022-03-25

開發背景

NutUI 作為京東風格的元件庫,已具備 H5 和多端小程式開發能力。隨著業務的不斷髮展,元件庫的應用場景越來越廣。在公司內外面臨諸如科技、金融、物流等各多個大型團隊使用時,單一的京東 APP 視覺雖可以一鍵進行換膚操作,但是對於更個性化的定製需求(元件級樣式、規範、尺寸等)近千行的主題樣式變數對開發者來說工作量是非常大的。為提升開發體驗,提高開發者效率,加強換膚功能以及實現「元件級式定製」功能迫在眉睫。

設計目標

允許使用者在開發階段切換不同主題風格的皮膚,也允許開發者對指定的元件直接進行樣式修改,以滿足不同設計風格的移動端業務場景。

效率提升

官網會提供多套主題供開發者選擇,同時開發者也可以在多套主題基礎上進行實時編輯修改,完成後下載配置變數,應用在專案中即可,非常易上手。完成一個全域性樣式配置僅需1分鐘。
相對這種場景下的需求開發是比較快的,能夠降低開發成本。

元件粒度

主題定製配置層分為全域性基本變數、元件基本變數,開發者可以修改全域性,比如元件庫的全域性主題顏色,字型等樣式。元件層的配置可以更細緻,比如 Button 按鈕成功型別的圓角邊框尺寸

通用變數

元件變數

通用擴充套件能力

現階段官方會提供一些優質主題整合到官網的,對於社群開發者、開發團隊、如果您的團隊定製的樣式主題檔案受眾非常之廣,可以聯絡我們,將您的主題內建到官方 npm 包中,造福更多的開發者

官方主題

開發者如何使用

視訊教程

NutUI 一分鐘快速線上主題定製 https://www.bilibili.com/video/BV1fi4y1D7qb

1、開啟線上配置網站,按照下方圖片進行修改預覽下載

效果預覽

2、本地專案配置

修改本地專案 webpack 或者 vite 的配置檔案將下載後的 custom_theme.sass 檔案,整合到專案中比如assets/styles/custom_theme.sass

  • vite 構建工具使用示例 vite.config
// https://vitejs.dev/config/
export default defineConfig({
  //...
  css: {
    preprocessorOptions: {
      scss: {
        // 預設京東 APP 10.0主題 > @import "@nutui/nutui/dist/styles/variables.scss";
        // 京東科技主題 > @import "@nutui/nutui/dist/styles/variables-jdt.scss";
        additionalData: `@import "./assets/styles/custom_theme.scss";@import "@nutui/nutui/dist/styles/variables.scss";`
      }
    }
  }
})
  • webpack 構建工具使用示例
{
    test: /\.(sa|sc)ss$/,
    use: [
        {
            loader: 'sass-loader',
            options: {
                // 預設京東 APP 10.0主題 > @import "@nutui/nutui/dist/styles/variables.scss";
                // 京東科技主題 > @import "@nutui/nutui/dist/styles/variables-jdt.scss";
                data: `@import "./assets/styles/custom_theme.scss";@import "@nutui/nutui/dist/styles/variables.scss";`,
            }
        }
    ]
}
  • taro 小程式使用示例

修改 config/index.js 檔案中配置 scss 檔案全域性覆蓋如:

const path = require('path');
const config = {
  deviceRatio: {
    640: 2.34 / 2,
    750: 1,
    828: 1.81 / 2,
    375: 2 / 1
  },
  sass: {
		resource: [
			path.resolve(__dirname, '..', 'src/assets/styles/custom_theme.scss')
		],
    // 預設京東 APP 10.0主題 > @import "@nutui/nutui-taro/dist/styles/variables.scss";
    // 京東科技主題 > @import "@nutui/nutui-taro/dist/styles/variables-jdt.scss";
    data: `@import "@nutui/nutui-taro/dist/styles/variables.scss";`
	},
  // ...

實現原理解析

整個元件庫主題定製模組,實現可以分為兩個方向,一個是內部的元件庫設計(供開發者使用配置每個樣式變數),另一個是線上配置官網(供開發者便捷的修改),接下來依次按照設計圖來闡述。

設計圖

元件庫內部設計

首先原始碼內部style資料夾下,分別存在variables.scssvariables-jdt.scss多個檔案對應的不同的官方主題,每個主題的全域性的variables.scss檔案,內部其實按標準的規則存放存放通用樣式變數和每個元件的樣式變數,像下面一樣

// --------base begin-------
// 主色調
$primary-color: #fa2c19 !default;
$primary-color-end: #fa6419 !default;
// 輔助色
$help-color: #f5f5f5 !default;
// 標題常規文字
$title-color: #1a1a1a !default;
// 副標題
$title-color2: #666666 !default;
// 次內容
$text-color: #808080 !default;

//...

// Font
$font-size-0: 10px !default;
$font-size-1: 12px !default;
$font-size-2: 14px !default;
$font-size-3: 16px !default;
$font-size-4: 18px !default;
$font-weight-bold: 400 !default;

$font-size-small: $font-size-1 !default;
$font-size-base: $font-size-2 !default;
$font-size-large: $font-size-3 !default;
$line-height-base: 1.5 !default;
// --------base end-------

// button
$button-border-radius: 25px !default;
$button-border-width: 1px !default;
$button-default-bg-color: $white !default;
$button-default-border-color: rgba(204, 204, 204, 1) !default;
$button-default-color: rgba(102, 102, 102, 1) !default;
//...

// icon 
// ...

這裡囉嗦一句,可以看到每一行後面都有一個 !default,這個是必不可少的,如果不加,開發者本地專案是無法覆蓋這個變數的

https://www.sass.hk/docs/#t6-9 Tips: 可以在變數的結尾新增 !default 給一個未通過 !default 宣告賦值的變數賦值,此時,如果變數已經被賦值,不會再被重新賦值,但是如果變數還沒有被賦值,則會被賦予新的值。

對於每一個元件的內部,例如button/index.scss下是這樣引用height: $button-default-height;

.nut-button {
  position: relative;
  display: inline-block;
  flex-shrink: 0;
  height: $button-default-height;
  // ...
}

其實最終元件庫構建成 npm 包時,將主題的全域性的variables.scss等主題檔案暴露給開發者,然後開發者根據需求替換其中的樣式變數,至此元件庫內部實現主題定製就實現了

視覺化配置官網

原始碼搶先看:https://github.com/jdf2e/nutui/tree/theme/src/sites/doc/components/ThemeSetting

整體實現流程如下,接下來依次闡述

  • variables.scss 原始檔,通過元件配置資料 + 正則匹配拆分,得到這樣的資料結構
// 主色調
$primary-color: #fa2c19 !default;
$primary-color-end: #fa6419 !default;
//...

// button
$button-border-radius: 25px !default;
$button-border-width: 1px !default;
//...

// icon 
// ...
[
  {name: 'Base', lowerCaseName: 'base', key: '$primary-color', rawValue: '#fa2c19', computedRawValue: ''}
  {name: 'Base', lowerCaseName: 'base', key: '$primary-color-end', rawValue: '#fa6419', computedRawValue: ''}
  // ...
  {name: 'Button', lowerCaseName: 'button', key: '$button-border-width', rawValue: '1px', computedRawValue: ''}
  {name: 'Button', lowerCaseName: 'button', key: '$button-border-radius', rawValue: '25px', computedRawValue: ''}
  //{name: 'components1', lowerCaseName: 'components1', key: '$components1-border-radius', rawValue: 'xx', computedRawValue: ''}
  //...
]
const findStyle = (componentName: string) => {
  // https://raw.githubusercontent.com/jdf2e/nutui/next/src/packages/styles/variables.scss
  // var pattern = /\$button.*;/g;
  var p = new RegExp(`\\$${componentName}.*;`, 'g');
  let parray: any[] = varcss.match(p) || [];
  // 需要包含換行
  let commponetns = parray.map((item) => {
    let cArray = item.split(':');
    let name = cArray[0],
      value: string = cArray[1].replace(' !default;', '').trim();
      return {
        name: componentName, 
        key: name, 
        rawValue:value,
        computedRawValue: ''
      }
  });
}
components.map(item=>{ findStyle(item.name) });
  • 接下來根據元件不同展示該元件下所有變數,監聽元件切換切換或者編輯,進行實時編譯
const cssText = computed(() => {
  const variablesText = store.variables.map(({ key, value }) => `${key}:${value}`).join(';');
  cachedStyles = cachedStyles || extractStyle(store.rawStyles);
  return `${variablesText};${cachedStyles}`;
});
const formItems = computed(() => {
  const name = route.path.substring(1);
  return store.variables.filter(({ lowerCaseName }) => lowerCaseName === name);
});
watch(
    () => cssText.value,
    (css) => {
      clearTimeout(timer);
      timer = setTimeout(() => {
        const Sass = (window as any).Sass;
        let beginTime = new Date().getTime();
        console.log('sass編譯開始', beginTime);
        Sass &&
          Sass.compile(css, async (res: Obj) => {
            await awaitIframe();
            const iframe = window.frames[0] as any;
            if (res.text && iframe) {
              console.log('sass編譯成功', new Date().getTime() - beginTime);
              if (!iframe.__styleEl) {
                const style = iframe.document.createElement('style');
                style.id = 'theme';
                iframe.__styleEl = style;
              }
              iframe.__styleEl.innerHTML = res.text;
              iframe.document.head.appendChild(iframe.__styleEl);
            } else {
              console.log('sass編譯失敗', new Date().getTime() - beginTime);
              console.error(res);
            }

            if (res.status !== 0 && res.message) {
              console.log(res.message);
            }
          });
      }, 300);
    },
    { immediate: true }
  );
  • 下載配置變數操作

由於變數檔案近千行,以後可能還會更大,直接採用Blob檔案流進行生成下載。

downloadScssVariables() {
  if (!store.variables.length) {
    return;
  }

  let temp = '';
  const variablesText = store.variables
    .map(({ name, key, value }) => {
      let comment = '';
      if (temp !== name) {
        temp = name;
        comment = `\n// ${name}\n`;
      }
      return comment + `${key}: ${value};`;
    })
    .join('\n');
  download(`// NutUI主題定製\n${variablesText}`, 'custom_theme.scss');
}
function download(content: string, filename: string) {
  const eleLink = document.createElement('a');
  eleLink.download = filename;
  eleLink.style.display = 'none';

  const blob = new Blob([content]);
  eleLink.href = URL.createObjectURL(blob);

  document.body.appendChild(eleLink);
  eleLink.click();
  document.body.removeChild(eleLink);
}

總結

文章詳細介紹了 NutUI 的「主題定製」和「元件級樣式定製」功能實現機制。「主題定製」能實現簡單的顏色切換,「元件級樣式定製」功能更強大,通過將元件的樣式變數暴露出來開發者幾乎可以任意修改自己想要的設計風格(元件尺寸、字型、邊距)。通過強大的主題定製可以讓元件庫的使用不侷限於原設計者的設計範疇,可靈活擴充套件元件,讓元件庫的應用範圍更廣,能滿足更廣泛的業務場景。

期待您的使用與反饋 ❤️~

相關文章