图标的设计是个技术活,需要设计出自己专属的风格,就像上一期一开始讲的那样,Style风格设计是三要素之首。基础组件设计,比如按钮是扁平还是立体,输入框是方角还是圆角,加不加阴影等这些,受项目经理、产品和交互的影响会多一点,但是大体来说,应与公司同类产品保持一致,至于如何设计,那就是另外的课题了,不在本文的讨论之列。
本文中的Icon图标使用字体库来完成,通过CSS无侵入式的在一个元素上加入before
或者after
伪类来实现图标显示,这里不是浏览器的字体,也不是客户电脑里安装的字体,也不是图片或其他方式,而且是以文字的方式显示,这样做相对比较简洁,方便修改,更重要的是利于SEO优化。浏览器兼容性比较好的字体库有WOFF
、WOFF2
等。字体库兼容性见官方解释。字体库有专门的自定义生成工具,例如fonteditor,可以试用30天;至于字体库,你也可以使用第三方开源的字体库,例如Font Awesome。这里作者就使用Font Awesome的woff字体库,我们打开图形创建工具fonteditor来查看这个文件:
可以看到,每一个Icon都有对应的Code-points
,这样我们就能通过CSS来配置字体图标了:
// 上图中玻璃杯
.glass:before {
content: '\F000';
}
设计思路有了,接下来就开始动工!
在src/components
文件夹下,新建文件夹Icon
,在该文件夹下新建Icon.tsx
:
...
// 定义接收参数
export interface IconProps {
/** 图标类型 */
type: string;
/** 是否旋转 */
spin?: boolean;
/** 自定义 icon 类名前缀,使用自定义图标库时使用,默认为 icon\_\_ */
prefix?: string;
}
// 图标控件
const Icon = ({
type,
spin,
prefix,
className,
...rest
} => {
const configContext = useContext(ConfigContext);
const finalPrefix = useMemo(() => prefix || configContext.iconDefaultPrefix || 'icon__', [
configContext.iconDefaultPrefix,
prefix
]);
return (
<IconWrap
className={classnames(prefixCls, `${finalPrefix}${type}`, spin && `${prefixCls}-spin`, className)}
spin={spin}
{...rest}
/>
);
};
export default React.memo(Icon);
这里可以看到,我在全局configContext
定义了默认的图标类名前缀,默认为icon__
,你也可以自定义,只要和CSS样式对应即可。最后返回的是一个IconWrap
组件,我们在Icon
文件夹下新建style
文件夹来放置样式包裹类,style
下设置font
文件夹来安置我们下载的WOFF文件,并在style
同目录下新建icon.css
和index.ts
:
src/components/Icon/style/icon.css:
/* 自定义专属的字体类型 */
@font-face {
font-family: duiicon;
/* src: url(./fonts/duiicon.eot?v=1552285261926); */
src: url(./fonts/fontawesome-webfont.woff) format('woff');
font-weight: 400;
font-style: normal;
}
...
/* 设置字体对应的类 */
[class*=' icon__']:before,
[class^='icon__']:before {
display: inline-block;
}
.icon__glass:before {
content: '\F000';
}
...
src/components/Icon/style/index.ts:
import styled from '@emotion/styled';
import { css } from '@emotion/core';
// spinMixin是公共的旋转样式,详见全部代码
const iconSpinMixin = css`
${spinMixin};
line-height: normal;
`;
export const IconWrap = styleWrap<{ spin?: boolean }>({})(
styled('i')(props => {
const { spin } = props;
return css`
vertical-align: baseline;
&&& {
${spin && iconSpinMixin};
}
`;
})
);
从代码中看到,虽然这里面没有用到主题样式的变量,但为了风格统一,这里用styleWrap
包裹一下,不给输入参数即可,其返回的仍然是一个回调函数,接受一个函数式组件作为参数,这里传递一个i标签:styled('i')
,参数props
是组件IconWrap
接受的参数,如果有spin旋转,那就加上旋转的样式。
关于前缀的拼接,我这里说一下:
classnames(prefixCls, `${finalPrefix}${type}`, spin && `${prefixCls}-spin`, className)
第一段是公共样式,这里为dux-ui-icon
,可以理解为来自组件库的标识,也方便用户在使用时批量添加样式;第二段就是我们代码中拼接的icon__glass
等,用于实际图标显示;第三段是旋转标识;第四段提供了用户自定义class。
现在,代码目录结构如下:
当然别忘了在src/index
导出:
export { default as Icon } from './components/Icon';
我们在同目录下的index.md
中写上demo用例:
import React from 'react';
// dumi-dux-ui要与你package.json中的name一致
import { Icon } from 'dumi-dux-ui';
import Copy from 'copy-to-clipboard';
// demo start
const layout = {
style: {
marginRight: 10
}
};
const Icons = [
'glass',
];
const TypeDemo = () => (
<div style={{ display: "flex" }}>
{Icons.map(item => (
<div key={item} style={{ width: '50px', height: '50px', cursor: 'pointer' }} onClick={() => Copy(item)}>
<Icon type={item} {...layout} />
</div>
))}
</div>
);
修改.umirc.ts
:
...
menus: {
// 需要自定义侧边菜单的路径,没有配置的路径还是会使用自动生成的配置
'/components': [
{
title: '组件',
path: '/components',
children: [
// 菜单子项(可选)
'components/Icon/index.md',
],
},
],
},
然后在根目录下执行:npm run docs
,打开浏览器,进入localhost:8000/components/icon即可看到:
我们的玻璃杯图标加载出来了!F12查看元素,确实是我们想要的加载方式:
关于 @emotion中的styled和css方法,可以避免使用外挂css文件,同时组件传递参数更加方便,当然也可以完全不用styled,如下面代码所示,其效果是等价的。详细使用可以查看emotion官网
function Content(props: any) {
// props的属性需要特殊处理,可传递className属性,通过外挂css实现相同的样式
...
return <i {...props}></i>
}
export const IconWrap = styleWrap<{ spin?: boolean }>({})(Content);
按钮一般会分类别,不同的类别有不同的颜色,我们分为实心,边框空心和禁用三种模式。下面列一个表格说明所有的button样式:
类别名称 | 种类名集合 |
---|---|
StyleTypes 种类 | ['primary', 'warning', 'success', 'error', 'border', 'border-gray'] |
Sizes 大小 | ['sm', 'md', 'lg'] |
Shapes 形状 | ['circle', 'square'] |
Shadowed 阴影 | ['true', 'false'] |
Loading 加载 | ['true', 'false'] |
Disabled 禁用 | ['true', 'false'] |
Block 块级显示 | ['true', 'false'] |
我们根据罗列的类型,开始搭建组件!
我们在src/components
文件夹下新建文件夹Button
,在该目录下新建文件index.tsx
:
// 定义接受参数,为表格中罗列属性
export interface ButtonProps {
/** 按钮类型 */
styleType?: 'primary' | 'warning' | 'success' | 'error' | 'border' | 'border-gray';
/** 按钮尺寸 */
size?: 'sm' | 'md' | 'lg';
/** 形状 */
shape?: 'circle' | 'square';
/** 阴影 */
shadowed?: boolean;
/** 主题 */
// theme?: 'dark';
/** 是否加载中 */
loading?: boolean;
/** 图标 */
icon?: string | ReactNode;
/** 设置原生的 button 上 type 属性 */
type?: string;
/** 展示设置为块元素 */
block?: boolean;
}
同样的,我们使用一个样式类包裹一下原生的button(还是在这个文件):
...
render() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { loading, icon, children, ...rest } = this.props;
return (
<StyleButton loading={loading} {...rest}>
// renderIcon为挂载按钮内图标的函数
{this.renderIcon()}
{children}
</StyleButton>
);
}
接下来去创建StyleButton
,与Icon创建一样地,我们在src/components/Button
文件夹下新建style
文件夹,在该文件夹下新建index.tsx
,核心代码在这里:
...
const Button = ({
loading,
styleType,
disabled,
onClick,
block,
shadowed,
...rest
}) => (
<button
disabled={disabled}
onClick={!disabled ? onClick : undefined}
{...rest}
/>
);
export const StyleButton = styleWrap<SButtonProps, HTMLButtonElement>({
className: classNameMixin,
})(styled(Button)(buttonStyleMixin));
定义一个叫Button的函数式组件,其返回的就是一个原生的button,所以rest参数里你可以传递原生的属性,最后用styleWrap包裹后导出。与Icon不同的是,Button的样式更多,而且还要适配主题。所以这里多了一个类名声明classNameMixin
和一个样式函数buttonStyleMixin
.
classNameMixin:
// classNameMixin负责添加各种类名,用于唯一识别和开发者进行定制
const classNameMixin = ({
size,
styleType,
shape,
loading,
disabled,
checked,
}) =>
classnames(
prefixCls,
`${prefixCls}-size-${size}`,
`${prefixCls}-styletype-${styleType}`,
shape && `${prefixCls}-${shape}`,
loading && `${prefixCls}-loading`,
disabled && `${prefixCls}-disabled`,
checked && `${prefixCls}-checked`,
);
buttonStyleMixin是一个总开关,用于添加各种样式:
// buttonStyleMixin
const buttonStyleMixin = (props) => {
const { theme, loading, shape, checked, block } = props;
const { designTokens: DT } = theme;
return css`
margin: 0;
box-sizing: border-box;
border-radius: ${DT.T_CORNER_LG};
text-align: center;
text-decoration: none;
cursor: pointer;
outline: none;
font-size: ${DT.T_TYPO_FONT_SIZE_1};
white-space: nowrap;
${inlineBlockWithVerticalMixin}; // 块级
${sizeMixin(props)}; // 大小
${styleTypeMixin(props)}; // styleType
${shape && shapeMixin(props)}; // 形状
${loading && loadingMixin(props)}; // 加载中
${block &&
css`
width: 100%;
`};
`;
};
接下来分别记录上述各个样式变量:
sizeMixin:
// 通过getHeightBySize拿到主题配置文件中的各个大小
...
return css`
height: ${getHeightBySize(DT, size)};
line-height: ${getHeightBySize(DT, size)};
padding: 0 ${getPaddingBySize(DT, size)};
`;
...
shapeMixin:
...
// 目前支持圆形和方形
switch (shape) {
case 'circle':
return css`
border-radius: 50% !important;
padding: 0;
overflow: hidden;
width: ${getHeightBySize(DT, size)};
`;
case 'square':
return css`
padding: 0;
overflow: hidden;
width: ${getHeightBySize(DT, size)};
`;
default:
return css``;
}
loadingMixin的设置也一样,使得鼠标不可点击,暗灰色显示即可。
styleTypeMixin用来通过styleType的不同,对应的设置不同的主题配色, 以primary为例:
const {
// 接受ThemeProvider传递的theme参数
theme: { designTokens: DT },
styleType,
disabled,
size,
shadowed,
} = props;
...
primary: {
background: DT.T_BUTTON_PRIMARY_COLOR_BG_DEFAULT,
':hover,:active': {
background: DT.T_BUTTON_PRIMARY_COLOR_BG_HOVER,
boxShadow: shadowed ? DT.T_SHADOW_BUTTON_PRIMARY_HOVER : 'none',
},
color: DT.T_BUTTON_PRIMARY_COLOR_TEXT_DEFAULT,
fill: DT.T_BUTTON_PRIMARY_COLOR_TEXT_DEFAULT,
border: 'none',
boxShadow: shadowed ? DT.T_SHADOW_BUTTON_PRIMARY : 'none',
transition: `${transitionProperty} ${transitionFlat}`,
':link,:visited': {
color: DT.T_BUTTON_PRIMARY_COLOR_TEXT_DEFAULT,
},
},
...
可以看到,当styleType='primary'时
,设置了其背景色、文字颜色、填充色、渐变、过渡、激活时的样式以及被访问后的样式等。
如此,一个简单的Button组件封装装好了,我们只是对样式进行了改动,其本质还是返回了一个原生按钮,原来的事件不影响使用。
我们写个demo测试一下。在src/components/Button
下新建index.md
:
import React from 'react';
import { Button } from 'dumi-dux-ui';
// demo start
const { StyleTypes } = Button;
const ColorDemo = () => {
return (
<div>
{StyleTypes.map((type) => (
<Button styleType={type} key={type} onClick={() => console.log('clicked')}>
Button
</Button>
))}
</div>
);
};
// demo end
export default ColorDemo;
在.umirc.ts
中加入:
...
menus: {
// 需要自定义侧边菜单的路径,没有配置的路径还是会使用自动生成的配置
'/components': [
{
title: '组件',
path: '/components',
children: [
// 菜单子项(可选)
'components/Icon/index.md',
'components/Button/index.md',
],
},
],
},
根目录下执行npm run docs
, 不出意外的话,在localhost:8000/components/button下可以看到:
F12测试点击事件:
至此,基础组件Icon和Button的记录就到此结束了。下一期会记录布局组件的搭建,敬请期待~
阅读量:2017
点赞量:0
收藏量:0