React进阶(十)实战演练之Icon组件

在实际应用中,图标的使用无处不在,小到简书的编译页面,大到chrome浏览器的任务栏等,都有大量的图标需要处理,那如果我们自己的应用里也需要使用代表各种含义的图标时,我们应该怎么处理呢?是每一个图标都引入一张图片来做吗?

当然不是 。

可爱的小图标们

利用react,我将试图创建一个Icon组件,我们可以在使用时,控制图标的颜色,大小,旋转,图标类型等。大概如下:

<Icon color="red" size="small" type="skip" />
<Icon color="#FFF" size="small" type="loading" />

那么我们应该怎么实现呢?

一、构建工具集成sass

我通常更喜欢使用预编译工具sass来代替css,如果读者朋友们还没有了解过它,可以花10分钟时间搜索学习,非常的简单。

首先下载sass-loader, node-sass

> yarn add sass-loader node-sass

然后修改webpack的配置如下:

{
  test: /\.s[ac]ss$/,
  include: paths.appSrc,
  loaders: ["style-loader", "css-loader", "sass-loader"]
},

注意,随着版本的更新,方式可能有所调整,以create react app 官方文档为准

重启项目即可生效。

二、字体图标

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

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

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

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

点击第一个购物车图标,即可将图标收集。按需收集一些图标,统一添加到一个项目中。

我们可以看到,图标安静的躺在我们的项目中。

可以使用线上图标库。点击查看在线链接并且生成代码即可。我的项目生成的在线代码如下:

@font-face {
  font-family: 'iconfont';  /* project id 496908 */
  src: url('//at.alicdn.com/t/font_496908_9n2jkt8rov1xxbt9.eot');
  src: url('//at.alicdn.com/t/font_496908_9n2jkt8rov1xxbt9.eot?#iefix') format('embedded-opentype'),
  url('//at.alicdn.com/t/font_496908_9n2jkt8rov1xxbt9.woff') format('woff'),
  url('//at.alicdn.com/t/font_496908_9n2jkt8rov1xxbt9.ttf') format('truetype'),
  url('//at.alicdn.com/t/font_496908_9n2jkt8rov1xxbt9.svg#iconfont') format('svg');
}

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

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

<i class="icon-loading" />

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

.icon-loading {
  font-family: "iconfont";
  color: red;
  font-size: 20px;
}
.icon-loading:before {
  content: "\e602";
}

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

对应的scss写法为:

/* 对应的scss写法为 */
.icon-loading {
  font-family: "iconfont";
  color: red;
  font-size: 20px;
  &:before {
    content: "\e602";
  }
}

三、字体图标组件

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

例如

/* loading */
.icon-loading {
    /* ... */
    &:before { content: '\e602' }
}

.icon-refresh {
    /* ... */
    &:before { content: '\e6aa' }
}

js的逻辑的处理主要是根据传入的参数,判断有哪些class名应该存在。

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

第一个基本情况,就是简单的传入type,得到对应的图标显示。

<Icon type="close" />

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

<Icon className="close" type="close" />

第三种情况,则是要直接修改图标的样式,例如设置颜色,字体大小等。

// 第一种可以直接传入对应的属性
<Icon type="close" color="red" />

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

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

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

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

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

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

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

+ Icon
  + fonts
  - index.jsx
  - style.scss

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

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

> yarn add classnames

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

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.jsx中的代码编写。

首先引入必要的模块。

import React from 'react';
import classnames from 'classnames';
import './style.scss';

然后给可能会接收的props设定一个默认值。

const defaultProps = {
    type: '',
    spin: false
}

定义组件,因为仅仅只是一个UI展示,所以该组件是一个无状态组件,我们用function的方式来定义即可。

上一章主要介绍的是有状态的创建方式,没有涉及到这种方式,不过可以通过该例子直接掌握,不再特别描述

const Icon = (props = defaultProps) => {
  // 依次从props中取出可能会出现的值,此处的other表示其余所有剩余的属性,这是ES6的语法
  const { type, className, spin, color, style, ...other } = props;

  // 利用classnames方法计算出最终的classname字符串。
  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} /> 
  )
}

完整代码为

import React from 'react';
import classnames from 'classnames';
import './style.scss';

const defaultProps = {
  type: '',
  /**
   * 是否旋转
   */
  spin: false
}

const Icon = (props = defaultProps) => {
  // 依次从props中取出可能会出现的值,此处的other表示其余所有剩余的属性,这是ES6的语法
  const { type, className, spin, color, style, ...other } = props;

  // 利用classnames方法计算出最终的classname字符串。
  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} /> 
  )
}

export default Icon;

因为涉及到比较多的ES6语法的使用,如果还有ES6语法掌握不够熟练的同学,可以花半个小时熟悉一下ES6的的基本语法。前几个例子,我会尽量注释,等多几个例子之后,我就可能不会注释太多,相信到时候大家对ES6的掌握也比较熟练了。

接来下是css的实现。

/**
 * 图标项目地址
 * http://iconfont.cn/manage/index?spm=a313x.7781069.1998910419.db775f1f3&manage_type=myprojects&projectId=597416&keyword=
 */

@font-face {
  font-family: "iconfont";
  src: url('./fonts/iconfont.eot?t=1543309990807'); /* IE9*/
  src: url('./fonts/iconfont.eot?t=1543309990807#iefix') format('embedded-opentype'), /* IE6-IE8 */
  url('./fonts/iconfont.ttf?t=1543309990807') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
  url('./fonts/iconfont.svg?t=1543309990807#iconfont') format('svg'); /* iOS 4.1- */
}

.icon {
  font-family: "iconfont" !important;
  font-size: 16px;
  font-style: normal;
  display: block;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.icon-add::before { content: '\e65b' }
.icon-step:before { content: '\e65a'; }
.icon-complete:before { content: '\e658'; }
.icon-clock:before { content: '\e67d'; }
.icon-selected:before { content: '\e674'; }
.icon-phone:before { content: '\e67c'; }
.icon-share:before { content: '\e67b'; }
.icon-advisory:before { content: '\e67a'; }
.icon-car:before { content: '\e679'; }
.icon-star:before { content: '\e678'; }
.icon-starno:before { content: '\e677'; }
.icon-count:before { content: '\e676'; }
.icon-add2:before { content: '\e675'; }
.icon-check:before { content: '\e673'; }
.icon-gift:before { content: '\e672'; }
.icon-programe:before { content: '\e671'; }
.icon-order:before { content: '\e670'; }
.icon-adviser:before { content: '\e66f'; }
.icon-activity:before { content: '\e66e'; }
.icon-coupon:before { content: '\e66d'; }
.icon-aboutus:before { content: '\e66c'; }
.icon-envelope:before { content: '\e66b'; }
.icon-eyehide:before { content: '\e66a'; }
.icon-category:before { content: '\e669'; }
.icon-volume:before { content: '\e668'; }
.icon-pk:before { content: '\e667'; }
.icon-slider:before { content: '\e664'; }
.icon-scenes:before { content: '\e661'; }
.icon-full:before { content: '\e660'; }
.icon-eyeshow:before { content: '\e65f'; }
.icon-forward:before { content: '\e65e'; }

.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,一个简单却非常nice的Icon组件就这样完成了。

我相信对于刚接触React的朋友来说,特别是对ES6运用不太熟练的朋友,这个例子虽然简单,却有许多值得总结的经验,建议大家动手实践,细细回味。淡化对React的紧张感与重视感,从大局去体会React的魅力,这比React本身的知识点更为重要。

最后写一个例子来简单瞄一眼我们的图标组件

import React from 'react';
import ReactDOM from 'react-dom';
import Icon from 'components/Icon';
import { icons } from 'components/Icon/config';

const container = {
  display: 'flex',
  maxWidth: '300px',
  margin: 'auto',
  justifyContent: 'space-around',
  flexWrap: 'wrap'
}

const itemContainer = {
  width: '40px',
  height: '40px',
  display: 'flex',
  justifyContent: 'center',
  alignItems: 'center'
}

class App extends React.Component {
  render() {
    return (
      <div style={container}>
        <Icon type="add" spin />
        {icons.map((item, i) => (
          <div key={i} style={itemContainer}>
            <Icon type={item} />
          </div>
        ))}
      </div>
    )
  }
}

ReactDOM.render(<App />, document.getElementById('root'));
可爱的小图标们
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容

  • 第一部分 HTML&CSS整理答案 1. 什么是HTML5? 答:HTML5是最新的HTML标准。 注意:讲述HT...
    kismetajun阅读 27,474评论 1 45
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,062评论 25 707
  • 作为一个合格的开发者,不要只满足于编写了可以运行的代码。而要了解代码背后的工作原理;不要只满足于自己的程序...
    六个周阅读 8,441评论 1 33
  • “非暴力沟通”的创始人马歇尔·卢森堡博士说因为愤怒而杀人太肤浅了。 他认为杀人、打人、骂人都无法真正传达我们的心声...
    岁月莲上写诗阅读 584评论 0 0
  • 生活和工作中经常遇到意外和突发事件,那么碰到的时候如何才能提高自己的应变能力,根据总结积累给出三点建议给大家参考:...
    Gwenduly_W2016阅读 363评论 0 0