開發背景—
NutUI 作爲京東風格的組件庫,已具備 H5 和多端小程序開發能力。隨着業務的不斷髮展,組件庫的應用場景越來越廣。在公司內外面臨諸如科技、金融、物流等各多個大型團隊使用時,單一的京東 APP 視覺雖可以一鍵進行換膚操作,但是對於更個性化的定製需求(組件級樣式、規範、尺寸等)近千行的主題樣式變量[1]對開發者來說工作量是非常大的。爲提升開發體驗,提高開發者效率,加強換膚功能以及實現「組件級式定製」功能迫在眉睫。
設計目標—
允許用戶在開發階段切換不同主題風格的皮膚,也允許開發者對指定的組件直接進行樣式修改,以滿足不同設計風格的移動端業務場景。
效率提升
官網會提供多套主題供開發者選擇,同時開發者也可以在多套主題基礎上進行實時編輯修改,完成後下載配置變量,應用在項目中即可,非常易上手。完成一個全局樣式配置僅需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.scss
、variables-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 的「主題定製」和「組件級樣式定製」功能實現機制。「主題定製」能實現簡單的顏色切換,「組件級樣式定製」功能更強大,通過將組件的樣式變量暴露出來開發者幾乎可以任意修改自己想要的設計風格(組件尺寸、字體、邊距)。通過強大的主題定製可以讓組件庫的使用不侷限於原設計者的設計範疇,可靈活擴展組件,讓組件庫的應用範圍更廣,能滿足更廣泛的業務場景。
期待您的使用與反饋[2] ❤️~
相關鏈接
主題樣式變量: https://github.com/jdf2e/nutui/blob/next/src/packages/styles/variables.scss
[2]反饋: https://github.com/jdf2e/nutui/issues
本文分享自微信公衆號 - 凹凸實驗室(AOTULabs)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。