較完善的 Ant-Design 動態換膚方案

前言

預覽地址: Ant Design Themecss

const App = () => {
  return (
    <ThemeProvider
        theme={{
            name: 'dark',
            variables: { 'primary-color': '#00ff00' },
        }}
    >
      <Button type="primary">Primary Button</Button>
    </ThemeProvider>
  );
};

開箱即用 antd-theme 歡迎 Star ❤git

現有方案缺陷

  • CSS 變量github

    • 沒法支持複雜表達式 mix(var(--primary-color), #fff, 20%)
    • 瀏覽器兼容性有限
  • 多套 CSS 主題瀏覽器

    • 主題樣式固定,沒法實時切換主題色
    • CSS 代碼分離很差處理
  • less 動態切換antd

    • 須要引入less runtime 體積較大
    • 切換主題時會對整個樣式表進行 parse -> eval -> genCSS 處理,致使切換速度很慢

想要實現的目標

  • 提供接口支持實時修改 @primary-color, @border-radius-base 之類的參數
  • 瀏覽器頁面刷新後默認就是切換後的樣式,沒有多餘的動畫、明顯的加載過程
  • 打包體積小,無需打包多個樣式文件
  • 代碼分離友好,不一樣頁面的樣式能夠分別加載

實現過程

編譯時把不一樣皮膚須要修改的less變量、表達式留空, 在運行時填充。app

好比less

.btn {
    background: @primary-color;
    &:active {
        background: mix(@primary-color, white, 10%);
    }
}

會編譯成異步

/* less-loader!./style.less */
.btn {
    background: "[theme:primaryColor,default:#1890ff]";
}
.btn:active {
    background: "[theme:e8efafb1,default:#40a9ff]";
}

這種樣式是不能直接加載到頁面中的的, 因此會進一步處理成ide

/* themed-style-loader!./style.css */

var loadStyle = require('./load-themed-style').loadStyle;

var css = `
.btn {
    background: "[theme:primaryColor,default:#1890ff]";
}
.btn:active {
    background: "[theme:e8efafb1,default:#40a9ff]";
}
`;

loadStyle(css);

同時也會編譯出不一樣皮膚的替換變量,並注入到特定的文件 themes.js函數

/* themes.js */
module.exports = {
    default: {
        primaryColor: '#1890ff',
        e8efafb1: '#40a9ff'
    },
    dark: {
        primaryColor: '#1890ee',
        e8efafb1: '#40a9ee'
    },
    compact: {
        primaryColor: '#1890dd',
        e8efafb1: '#40a9dd'
    }
}

皮膚加載器大概長這樣

/* load-themed-style.js */
var themes = require('./themes.js')
var styles = [];

var loadStyle = function(css) {
    styles.push(css);
    applyStyles();
}

var loadTheme = function(name) {
    applyStyles(themes[name]);
}

var applyStyles = function(variables) {
    var css = styles.join('').replace(
        /"\[theme:([\w]+),default:(\S+)\]"/,
        function(_, themeSlot, defaultValue){ 
            return varialbes && varialbes[themeSlot] || defaultValue;
        }
    );
    // 生成好的css插入到頁面中
}

module.exports = {
    loadStyle,
    loadTheme
}

最後調用 loadTheme('xx') 就能夠切換到對應的皮膚

變量實時修改

插件若是分析到某個表達式依賴了須要實時修改的變量,就會把該表達式對應的 AST 注入到 themes.js 裏面

/* themes.js */

// @primary-color
var expr1 = {
    type: 'Variable',
    name: '@primary-color'
};

// mix(@primary-color, white, 10%)
var expr2 = {
    type: 'Call',
    name: 'mix',
    args: [
        {
            type: 'Variable',
            name: '@primary-color'
        },
        {
            type: 'Color',
            rgb: [255, 255, 255],
            alpha: 255,
        },
        {
            type: 'Dimension',
            value: 10,
            unit: '%'
        }
    ]
};

module.exports = {
    default: {
        background: 'white',
        primaryColor: { expr: expr1, default: '#1890ff' },
        e8efafb1: { expr: expr2, default: '#40a9ff' }
    },
    dark: {
        background: 'black',
        primaryColor: { expr: expr1, default: '#1890ff' },
        e8efafb1: { expr: expr2, default: '#1890ff' }
    },
    ...
}

loadTheme 會根據傳入的實時變量和皮膚裏面的 AST 計算出填充值,填充留空並應用修改

var loadTheme = function(name, runtimeVariables) {
    // 根據傳入實時變量計算出本次的皮膚變量
    var themeVariables = compute(
        themes[name],
        runtimeVariables
    );
    // 應用樣式
    applyStyles(themeVariables);
}

如今調用 loadTheme('xx', { 'primary-color': '#xxxxxx' }) 就能夠實時的修改頁面主色調

遇到的問題

  • colorPalette 函數

ant-design 內部使用了 ~`colorPalette('@{background}', 7)` 內聯 Javascript 塊, 致使沒法跟蹤表達式的變量依賴, 因此在 less 解析以前對樣式代碼進行預處理,所有替換成 colorPalette(@background, 7) 並提供對應的 colorPalette 函數實現。

  • Mixins 展開 & CSS Guards 轉換

換膚方案是基於CSS屬性替換, 在全部皮膚下同一個組件生成出來的樣式須要行數一致且表達式hash一致。

// Mixin
.button-color(@color) {
    color: @color;
}

.btn-primary {
    &:active {
        // CSS Guard 1
        & when (@theme = dark) {
            .button-color(@primary-7);
        }
        
        // CSS Guard 2
        & when not (@theme = dark) {
            .button-color(~`colorPalette('@{btn-primary-bg}', 7)`);
        }
    }
}

上面的按鈕樣式在默認皮膚下會生成

.btn-primary:active {
    color: "[theme:primary7]";
}

暗黑模式下生成的倒是

/* sha1(colorPalette(@btn-primary-bg, 7))= 9ebde6df87d1def7be1e8e5c80144b793cb1e2c2 */
.btn-primary:active {
    color: "[theme:9ebde6df]";
}

這樣就會出現運行時變量填充錯誤,所以須要修改樣式解析後的AST。先對 Mixin 調用進行展開, 而後對 CSS Guards 進行轉換,在 AST 執行以前上面的按鈕樣式會被轉換成:

.btn-primary {
    &:active {
        color: if(@theme = darak, @primary-7);
        color: if(not @theme = dark, colorPalette(@btn-primary-bg, 7));
    }
}

轉換後的代碼就能夠愉快的用上文的方式進行處理了, 這裏產生的多個color定義會在運行時拿到皮膚變量後進行刪除處理。

限制

  • 遞歸 Mixin Call 的循環變量不能做爲皮膚變量, 好比 ant-design 柵格系統相關代碼中的 @grid-columns

    .loop-grid-columns(@index, @class) when (@index > 0) {
      // ...
      .@{ant-prefix}-col@{class}-order-@{index} {
        order: @index;
      }
      .loop-grid-columns((@index - 1), @class);
    }
    
    .loop-grid-columns(@grid-columns, @class);
  • postcss-position 不兼容

    postcss-position 會直接對 css 中的 position 屬性值進行 value.match(/^static|absolute|fixed|relative.../).toString(),

    '"[theme:position,default:relative]"'.match(...) === null

    所以會出如今編譯過程當中的報錯,具體的 postcss-position 代碼在 這裏

What's Next

  • 支持 CSS Variable Backend 配置

    開啓 CSS Variable Backend 後樣式文件會編譯成

    /* less-loader!./style.less */
    .btn {
        background: var(--primaryColor, #1890ff);
    }
    .btn:active {
        background: var(--e8efafb1, #40a9ff);
    }

    applyStyle 的內部實現調整爲 style.setProperty(--primaryColor, '#xxxxxx')

  • requestIdleCallback

    採用 React Fiber 相似的方案對樣式的渲染過程進行異步處理,避免過多樣式同步渲染致使的頁面卡頓

相關文章
相關標籤/搜索