6. 封装一个 Icon 组件

React 知命境」第 6 篇

理论结合实践,是非常有效的学习方式。也是本书一直倡导并推行的法则。 在学习了 props 属性之后,结合一个实践案例,我们就能够扎实的掌握它。

在实践应用中,图标的使用无处不在。小到编辑器的功能按钮,大到 chrome 浏览器的任务栏,都有大量的图标需要处理。每个稍微大一点点的项目都必然需要一个图标组件。

在使用时,我们可以控制图标具体类型、颜色、大小。在 React 哲学之封装思想的指导下,这些控制项为组件的差异项,需要通过 props 传入。封装好之后,使用方式大概如下:

js 复制代码
<Icon color="red" size="small" type="skip" />
<Icon color="#FFF" size="small" type="loading" />

那么我们应该如何实现呢?

字体图标

最初见到字体图标的应用,是在淘宝网站上。当时大家都还在使用雪碧图,而淘宝页面的图标居然可以像字体一样,随意的给它设置颜色大小等属性。到了现在,字体图标早已经不是什么黑科技了,它几乎被普及到了所有网站。

在 CSS3 中,有一个语法可以自定义字体 @font-face。如果字体库是有图标组成,那么我们就可以创建字体图标了。字体图标与文字具有相同的特性,我们可以把图标当成字体一样处理。例如修改它的font-size,color等。对应的css语法如下:

css 复制代码
@font-face {
  font-family: 'custom name',   /* 自定义字体名字 */
  src: url('./fonts/custom.eot')  /* 下载到本地的字体库 */
}

通常情况下,字体库中,每一个图标都会对应一个唯一的标识码。现在我们要通过字体图标网站 iconfont 收集一个自己项目中会涉及到的图标。然后组成一个图标库。

可以使用先上图标库。点击查看在线链接并生成代码即可。

css 复制代码
@font-face {
  font-family: 'iconfont';  /* Project id 3219669 */
  src: url('//at.alicdn.com/t/font_3219669_n8051p8dggi.woff2?t=1646884122827') format('woff2'),
       url('//at.alicdn.com/t/font_3219669_n8051p8dggi.woff?t=1646884122827') format('woff'),
       url('//at.alicdn.com/t/font_3219669_n8051p8dggi.ttf?t=1646884122827') format('truetype');
}

将这段代码贴到我们的css文件中,就已经自定义了一个 font-family 为iconfont的字体图标。我们也可以将字体图标库下载下来,把url中的路径都修改为对应的字体库文件就行。

可以看到,每一个图标除了有一个对应的名字之外,还有一个唯一的unicode码。&#x表示他们后面跟的是16进制数字。假设我们期望在 HTML 中放入一个代表图标的标签

css 复制代码
<i class="icon-warn" />

那么,只要它对应的 CSS 这样写,就可以在页面上显示出字体库中的图标了

css 复制代码
.icon-warn {
  font-family: "iconfont";
  color: red;
  font-size: 20px;
}
.icon-warn:before {
  content: '\e6cc';
}

content的值是一个斜杠加上图标对应的十六进制数字。运行之后我们就能在页面中看到一个红色的 warn 图标。

对应的 scss 写法为:

css 复制代码
/* 对应的scss写法为 */
.icon-warn {
  font-family: "iconfont";
  color: red;
  font-size: 20px;
  &:before {
    content: '\e6cc';
  }
}

目前新版本的create-react-app创建的项目,只需新安装node-sass就可以支持 SCSS 语法,具体情况根据你使用的版本来定。

字体图标组件

很显然图标组件的封装不会涉及到太过于复杂的 JS 逻辑处理,更多的是对外部状态 props 的判断与处理。基础元素可以指定一个 i 标签。图标通过 before/after 伪类中的 content 显示。实现方法我们将每一个图标都对应写一个class,然后根据传入的 type 类型,修改对应的 class 即可。

例如:

css 复制代码
.icon {
  &-react::before {
    content: '\e712';
  }
  &-ercode::before {
    content: '\e600';
  }
}

JS 的逻辑的处理主要在于样式合并,例如判断有哪些 class 名应该存在。

先思考一下组件封装好后,我们会遇到哪些情况。

第一种情况:简单传入 type,得到对应的图标显示。

html 复制代码
<Icon type="close" />

第二种情况:组件本身需要设置一些样式,因此可能会有通过添加 class 的方式自定义 CSS 样式。

html 复制代码
<Icon className="close" type="close" />

第三种情况:直接修改图标的样式

html 复制代码
<!--第一种可以直接传入对应的属性-->
<Icon type="close" color="red" />

<!--另外一种是利用jsx支持的style语法,传入css样式-->
const style = {
  color: 'red',
  fontSize: '20px'
}
<Icon type="close" style={style} />

第四种情况:我们要考虑特殊的类型,例如 loading 图标需要一直旋转。例如 refresh 刷新图标,点击时才旋转,刷新完成就停止旋转。因此我们要专门针对这种情况做特殊处理。添加一个是否旋转的控制属性。

html 复制代码
<!--通过对spin的修改,来控制图标是否旋转-->
<Icon type="refresh" spin={true} />

其余的我们可能在实践中还会添加新的需求,到时候再根据需求做改进即可。

OK,带着这些基础知识和需求,我们开始动手来完成我们的第一个正式的 React 组件。

在 src 目录下,创建一个专门用来存放组件的文件夹:components。然后在 components 目录下创建 Icon 目录。并分别创建 index.tsx 与 index.scss。我们将字体图标下载下来,存放于Icon目录的fonts目录中。

最终的文件结构大致如下:

bash 复制代码
+ Icon
  + fonts
  - index.jsx
  - style.scss

图标库也可以使用在线地址

通过上面的分析我们知道,基础元素的 class 可能会涉及到很多个,如果通过 if/else 来判断的话,可能我们的代码可读性会非常的低。因此这里我们借助一个专门处理 class 名的工具方法来完成逻辑的判断。这个工具库叫做 classnames。

我们先安装这个库,然后重启项目

bash 复制代码
> yarn add classnames

该工具方法的使用比较简单,它的目的在于拼接 class 名

js 复制代码
import classnames from 'classnames';

// 拼接所有参数
classnames('foo', 'bar');  // 'foo bar'

// 拼接值为true的参数
classnames({
  foo: true,
  bar: false
})  //  'foo'

// 也可以比较随意的混合使用
classnames('foo', {
  bar: true,
  tag: true,
  mm: false
}) // 'foo bar tag'

更具体的用法可以查看其 npm 上的文档

现在我们先来实现index.tsx中的代码编写

首先引入必要的模块

js 复制代码
import classnames from 'classnames'
import './index.scss'

然后定义相关的 TS 类型声明

ts 复制代码
export type IconType = 'react' | 'ercode' | 'search' | 'warn' |
  'warn2' | 'back' | 'setting' | 'html' | 'html2' | 'like' |
  'home' | 'profile' | 'project' | 'css' | 'css2' | 'css3' | 'css4' | 'wxgzhao' |
  'wxgzhao2' | 'hot' | 'js' | 'menu' | 'wxs' | 'arrow-right' | 'arrow-left' |
  'arrow-up' | 'arrow-down' | 'webpack' | 'vue' | 'redux' | 'event' |
  'antd' | 'loading' | 'fed'

type IconProps = {
  type: IconType,
  spin?: boolean,
  className?: string,
  color?: string,
  style?: Object,
  [key: string]: any
}

然后给 props 设定一个默认值

js 复制代码
const defaultProps: IconProps = {
  type: 'loading',
  spin: false
}

创建函数组件,主要逻辑是根据外部传入的 props 整合最终的样式

js 复制代码
export default function Icon(props = defaultProps) {
  const {type, className, spin, color, style, ...other} = props

  const cls = classnames({
    'icon': true,
    'icon-spin': !!spin || type === 'loading',
    [`icon-${type}`]: true
  }, className)

  const _style = {...style, color}

  return (
    <i className={cls} {...other} style={_style} />
  )
}

CSS 的样式

css 复制代码
// 图标库地址
// https://www.iconfont.cn/manage/index?spm=a313x.7781069.1998910419.11&manage_type=myprojects&projectId=3219669&keyword=&project_type=&page=

@font-face {
  font-family: 'iconfont';  /* Project id 3219669 */
  src: url('//at.alicdn.com/t/font_3219669_n8051p8dggi.woff2?t=1646884122827') format('woff2'),
       url('//at.alicdn.com/t/font_3219669_n8051p8dggi.woff?t=1646884122827') format('woff'),
       url('//at.alicdn.com/t/font_3219669_n8051p8dggi.ttf?t=1646884122827') format('truetype');
}

.icon {
  font-family: 'iconfont';
  font-size: 16px;
  font-style: normal;
  display: block;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
.icon {
  &-react::before {
    content: '\e712';
  }
  &-ercode::before {
    content: '\e600';
  }
  &-search::before {
    content: '\e6ac';
  }
  &-warn::before {
    content: '\e6cc';
  }
  &-warn2::before {
    content: '\e66f';
  }
  &-back::before {
    content: '\e60f';
  }
  &-setting::before {
    content: '\e606';
  }
  &-html::before {
    content: '\e6a0';
  }
  &-html2::before {
    content: '\e632';
  }
  &-like::before {
    content: '\e60a';
  }
  &-home::before {
    content: '\e671';
  }
  &-profile::before {
    content: '\e607';
  }
  &-project::before {
    content: '\e714';
  }
  &-css::before {
    content: '\e605';
  }
  &-css2::before {
    content: '\ec3a';
  }
  &-css3::before {
    content: '\e601';
  }
  &-css4::before {
    content: '\e618';
  }
  &-wxgzhao::before {
    content: '\e612';
  }
  &-wxgzhao2::before {
    content: '\e705';
  }
  &-hot::before {
    content: '\e7a5';
  }
  &-js::before {
    content: '\e810';
  }
  &-menu::before {
    content: '\e666';
  }
  &-wxs::before {
    content: '\e608';
  }
  &-arrow-right::before {
    content: '\e743';
  }
  &-arrow-left::before {
    content: '\e744';
  }
  &-arrow-up::before {
    content: '\e745';
  }
  &-arrow-down::before {
    content: '\e7b2';
  }
  &-webpack::before {
    content: '\e6ae';
  }
  &-vue::before {
    content: '\e69a';
  }
  &-redux::before {
    content: '\ec7d';
  }
  &-event::before {
    content: '\e609';
  }
  &-antd::before {
    content: '\eaa1';
  }
  &-fed::before {
    content: '\e60b';
  }
}

.icon-spin {
  animation-name: rotate;
  animation-duration: 1s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

@keyframes rotate {
  from {
    transform: rotate(0)
  }

  to {
    transform: rotate(360deg)
  }
}

OK,一个简单且非常专业的 Icon 组件就这样完成啦。

把我准备的图标库里所有图标遍历渲染到页面上结果如下

「React 知命境」 是一本从知识体系顶层出发,理论结合实践,通俗易懂,覆盖面广的精品小册,欢迎关注我的公众号,我会持续更新

点击关注我 点击添加我
这波能反杀 icanmeetu
相关推荐
yuanyxh4 小时前
Mac 软件推荐
前端·javascript·程序员
万少4 小时前
AtomCode开发微信小程序《谁去呀》 全流程
前端·javascript·后端
某人辛木4 小时前
Web自动化测试
前端·python·pycharm·pytest
Kagol5 小时前
Superpowers GSD gstack AgentSkills深度测评
前端·人工智能
excel5 小时前
JavaScript 字符串与模板字面量:从表象到本质理解
前端
京东云开发者6 小时前
当AI成为导演-如何用AI创作动漫短剧
前端
李白的天不白6 小时前
使用 SmartAdmin 进行前后端开发
java·前端
乘风gg6 小时前
🤡PUA AI Coding 工具 的 10 条终极语录
前端·ai编程·claude
学Linux的语莫7 小时前
Vue 3 入门教程
前端·javascript·vue.js
怕浪猫7 小时前
第一章、Chrome DevTools Protocol (CDP) 详解
前端·javascript·chrome