提升组件库通用能力 - NutUI 在线主题定制功能探索

开发背景
NutUI 作为京东风格的组件库,已具备 H5 和多端小程序开发能力。随着业务的不断发展,组件库的应用场景越来越广。在公司内外面临诸如科技、金融、物流等各多个大型团队使用时,单一的京东 APP 视觉虽可以一键进行换肤操作,但是对于更个性化的定制需求(组件级样式、规范、尺寸等)近千行的主题样式变量对开发者来说工作量是非常大的。为提升开发体验,提高开发者效率,加强换肤功能以及实现「组件级式定制」功能迫在眉睫。
设计目标
【提升组件库通用能力 - NutUI 在线主题定制功能探索】允许用户在开发阶段切换不同主题风格的皮肤,也允许开发者对指定的组件直接进行样式修改,以满足不同设计风格的移动端业务场景。
效率提升 官网会提供多套主题供开发者选择,同时开发者也可以在多套主题基础上进行实时编辑修改,完成后下载配置变量,应用在项目中即可,非常易上手。完成一个全局样式配置仅需1分钟。
相对这种场景下的需求开发是比较快的,能够降低开发成本。
组件粒度 主题定制配置层分为全局基本变量、组件基本变量,开发者可以修改全局,比如组件库的全局主题颜色,字体等样式。组件层的配置可以更细致,比如 Button 按钮成功类型的圆角边框尺寸
提升组件库通用能力 - NutUI 在线主题定制功能探索
文章图片

提升组件库通用能力 - NutUI 在线主题定制功能探索
文章图片

通用扩展能力 现阶段官方会提供一些优质主题集成到官网的,对于社区开发者、开发团队、如果您的团队定制的样式主题文件受众非常之广,可以联系我们,将您的主题内置到官方 npm 包中,造福更多的开发者
提升组件库通用能力 - NutUI 在线主题定制功能探索
文章图片

开发者如何使用
视频教程 NutUI 一分钟快速在线主题定制 https://www.bilibili.com/video/BV1fi4y1D7qb
1、打开在线配置网站,按照下方图片进行修改预览下载 提升组件库通用能力 - NutUI 在线主题定制功能探索
文章图片

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"; ` }, // ...

实现原理解析
整个组件库主题定制模块,实现可以分为两个方向,一个是内部的组件库设计(供开发者使用配置每个样式变量),另一个是在线配置官网(供开发者便捷的修改),接下来依次按照设计图来阐述。
提升组件库通用能力 - NutUI 在线主题定制功能探索
文章图片

组件库内部设计 首先源码内部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 = https://www.it610.com/article/URL.createObjectURL(blob); document.body.appendChild(eleLink); eleLink.click(); document.body.removeChild(eleLink); }

总结
文章详细介绍了 NutUI 的「主题定制」和「组件级样式定制」功能实现机制。「主题定制」能实现简单的颜色切换,「组件级样式定制」功能更强大,通过将组件的样式变量暴露出来开发者几乎可以任意修改自己想要的设计风格(组件尺寸、字体、边距)。通过强大的主题定制可以让组件库的使用不局限于原设计者的设计范畴,可灵活扩展组件,让组件库的应用范围更广,能满足更广泛的业务场景。
期待您的使用与反馈 ??~

    推荐阅读