Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React 造轮子系列:Icon 组件思路 #35

Open
husky-dot opened this issue May 22, 2019 · 0 comments
Open

React 造轮子系列:Icon 组件思路 #35

husky-dot opened this issue May 22, 2019 · 0 comments

Comments

@husky-dot
Copy link
Owner

husky-dot commented May 22, 2019

简介

本轮子是通过 React + TypeScript + Webpack 搭建的,至于环境的搭建这边就不在细说了,自己动手谷歌吧。当然可以参考我的源码

这里我也是通过别人学的,主要做些总结及说明造各个轮子的一种思路,方便今后使用别人的的轮子时自己脑中有造轮子的思想,能通过修改源码及时修改 bug,按时上线。

本文的 Icon 组件主要是参考 Framework7 中的 Icon React Component 写的。

为什么要造轮子

1.为了不求人

  • 假设你使用某个UI框架发现有一个 bug,于是你反馈给开发者,开发者说两周后修复,而你的项目一周后就要上线,你怎么办?

  • 为什么很多大公司都不使用其他公司的轮子,要自己造?为了把控自己的业务,不被别人牵着走。

2.为了不流于平庸

  • 大家都是写增删改查,你跟别人比有什么优势?你如果能说一局【我公司的人都在用我写的UI框架】是不是就很牛逼?造 UI 轮子会遇到很多技术层面而非业务层面的知识?比如一些算法。

3.为了创造

  • 你为别人做了这么久的事情,有没有自己做什么?自驱动力。

4.为什么是 UI 轮子,不是其他方面的轮子

  • 比如,为什么不自己写一个 React 框架,要写 React UI 框架呢?

React.FunctionComponent 与 IconPropps

本轮子使用 React + TypeScript 来写的,那么在 ts 中如何声明函数组件及级 Icon 组件传递参数呢,答案是使用React提供的静态方法 React.FunctionComponent 及 TypeScript 提供的接口定义。

// lib/icon.tsx

import React from 'react'

interface IconProps {
  name: string
}

const Icon: React.FunctionComponent<IconProps> = () => {
  return (
    <span>icon</span>
  )
}

export default Icon

在 index.txt 中调用:

import React from "react";
import ReactDOM from "react-dom";
import Icon from './icon'
  
ReactDOM.render(<div>
  <Icon name='wechat'/>
</div>, document.body)

对于上面的定义方式,后面的轮子会经常使用,所以不必担心看不懂。

使用 svg-sprite-loader 加载 SVG

在上面我们指定了 Iconnamewechat,那怎么让它显示微信的图标呢,首先在阿里的 Iconfont 下载对应的 SVG

接着如何显示 svg? 这里我们使用一个 svg-sprite-loader 库,然后在对应的 webpack下的 rules 中添加:

{
  test: /\.svg$/,
  loader: 'svg-sprite-loader'
}

在 Icon 中引用,当然对应 tsconfig.json 也要配置(这不是本文的重点):

import React from 'react'
import wechat from './icons/wechat.svg'

console.log(wechat)
interface IconProps {
  name: string
}

const Icon: React.FunctionComponent<IconProps> = () => {
  return (
    <span>
      <svg>
        <use xlinkHref="#wechat"></use>
      </svg>
    </span>
  )
}

export default Icon

运行效果:

当然 svg 里面不能直接写死,我们需要根据外部传入的 name 来指定对应的图像:

// 部分代码
import  './icons/wechat.svg'
import './icons/alipay.svg'

const Icon: React.FunctionComponent<IconProps> = (props) => {
  return (
    <span>
      <svg>
        <use xlinkHref={`#${props.name}`}></use>
      </svg>
    </span>
  )
}

外部调用:

ReactDOM.render(<div>
  <Icon name='wechat'/>
  <Icon name='alipay'/>
</div>, document.getElementById('root'))

运行效果:

importAll

大家有没有注意到,我需要使用哪个 svg, 需要在对应的 icon 组件导入对应的 svg,这样要是我需要100个 svg ,我就要导入100次,这样做太傻,文件也会变得冗长。

因此我们需要一个动态导入全部 SVG 的方法:

 // lib/importIcons.js
let importAll = (requireContext) => requireContext.keys().forEach(requireContext)
try {
  importAll(require.context('./icons/', true, /\.svg$/))
} catch (error) {
  console.log(error)
}

要想看懂上诉的代码,可能需要一点 node.js 的基础,这边建议你直接收藏好啦,下次有用到,直接拷贝过来用就行了。

接着在 Icon 组件里面导入就行了: import './importIcons'

React.MouseEventHandler 的使用

当我们需要给 Icon 注册事件的时候,如果直接在组件上写 onClick 事件是会报错的,因为它没有声明接收 onClick 事件类型,所以需要声明,如下所示:

/lib/icon.tsx

import React from 'react'
import './importIcons'
import './icon.scss';
interface IconProps {
  name: string,
  onClick: React.MouseEventHandler<SVGElement>
}

const Icon: React.FunctionComponent<IconProps> = (props) => {
  return (
    <span>
      <svg onClick={ props.onClick}>
        <use xlinkHref={`#${props.name}`} />
      </svg>
    </span>
  )
}

export default Icon

调用方式如下:

import React from "react";
import ReactDOM from "react-dom";
import Icon from './icon'

const fn: React.MouseEventHandler = (e) => {
  console.log(e.target);
};


ReactDOM.render(<div>
  <Icon name='wechat' onClick={fn}/>
</div>, document.getElementById('root'))

让Icon响应所有事件

上述我们只监听了 onClick 事件 ,但对于其它事件是不支持了,所以我们需要进一步完善。这里我们不能一个一个添加对应的事件类型,需要一个统一的事件类型,那这个是什么呢?

通过 react 我们会找到一个 SVGAttributes 类,这里我们需要继承它:

/lib/icon.tsx
import React from 'react'
import './importIcons'
import './icon.scss';
interface IconProps extends React.SVGAttributes<SVGElement> {
  name: string;
}

const Icon: React.FunctionComponent<IconProps> = (props) => {
  return (
    <span>
      <svg 
        onClick={ props.onClick}
        onMouseEnter = {props.onMouseEnter}
        onMouseLeave = {props.onMouseLeave}
      >
        <use xlinkHref={`#${props.name}`} />
      </svg>
    </span>
  )
}

export default Icon

调用方式:

import React from "react";
import ReactDOM from "react-dom";
import Icon from './icon'

const fn: React.MouseEventHandler = (e) => {
  console.log(e.target);
};


ReactDOM.render(<div>
  <Icon name='wechat' 
    onClick={fn}
    onMouseEnter = { () => console.log('enter')}
    onMouseLeave = { () => console.log('leave')}
  />
</div>, document.getElementById('root'))

上述还是会有问题,我们还有 onFocus, onBlur, onChange 等等事件,也不可能一个一个传递进来,那还有什么方法呢。

icon.tsx 中我们会发现我们用的都是通过 props 传递进来的。聪明的朋友的可能立马想到了使用展开运算符的形式 {...props},改写如下:

...
const Icon: React.FunctionComponent<IconProps> = (props) => {
  return (
    <span>
      <svg className="fui-icon" {...props}>
        <use xlinkHref={`#${props.name}`} />
      </svg>
    </span>
  )
}
...

上述还是会有问题,如果使用的人也传入 className 呢,用过 Vue 就知道 Vue 是真的好,它会把传入和里面的合并起来,但 React 就不一样了,传入的会覆盖里面的,所以需要自己手动处理:

...
const Icon: React.FunctionComponent<IconProps> = (props) => {
  const { className, ...restProps} = props
  return (
    <span>
      <svg className={`fui-icon ${className}`} {...restProps}>
        <use xlinkHref={`#${props.name}`} />
      </svg>
    </span>
  )
}
...

上达写法还存在问题的,如果外面没有写 className ,那么内部会多出一个 undefined

聪明你的可能就想到了使用三目运算符来做判断,如:

className={`fui-icon ${className ? className : ''}`}

但这种情况如果有多个参数要怎么办呢?

所以有人就非常聪明专门写了一个库存 classnames,这个库有多火呢,每周有300多万的下载量,它的作用就是处理 className 的情况。

当然我们这边只做简单的处理,如下所示

// helpers/classes
function classes(...names:(string | undefined )[]) {
  return names.join(' ')
}

export default classes

使用方式:

...
const Icon: React.FunctionComponent<IconProps> = (props) => {
  const { className, name,...restProps} = props
  return (
    <span>
      <svg className={classes('fui-icon', className)} {...restProps}>
        <use xlinkHref={`#${name}`} />
      </svg>
    </span>
  )
}
...

这样最终渲染出来的 className还是会多出一个空格,作为完美者,并不希望有空格的出现的,所以需要进一步处理空格,这里使用 es6 中数组的 filters 方法。

// helpers/classes
function classes(...names:(string | undefined )[]) {
  return names.filter(Boolean).join(' ')
}

export default classes

单元测试

首先我们对我们的 classes 方法时行单元测试,这里使用 Jest 时行测试,也是 React 官网推荐的。

classes 测试用例如下:

import classes from '../classes'
describe('classes', () => {
  it('接受 1 个 className', () => {
    const result = classes('a')
    expect(result).toEqual('a')
  })
  it('接受 2 个 className', ()=>{
    const result = classes('a', 'b')
    expect(result).toEqual('a b')
  })
  it('接受 undefined 结果不会出现 undefined', ()=>{
    const result = classes('a', undefined)
    expect(result).toEqual('a')
  })
  it('接受各种奇怪值', ()=>{
    const result = classes(
      'a', undefined, '中文', false, null
    )
    expect(result).toEqual('a 中文')
  })
  it('接受 0 个参数', ()=>{
    const result = classes()
    expect(result).toEqual('')
  })
})

使用Snapshot测试UI

这里测试 UI 相关还需要使用一个库 Enzyme , Enzyme 来自 airbnb 公司,是一个用于 React 的 JavaScript 测试工具,方便你判断、操纵和历遍 React Components 输出。Enzyme 的 API 通过模仿 jQuery 的 API ,使得 DOM 操作和历遍很灵活、直观。Enzyme 兼容所有的主要测试运行器和判断库。

icon 的测试用例

import * as renderer from 'react-test-renderer'
import React from 'react'
import Icon from '../icon'
import {mount} from 'enzyme'

describe('icon', () => {
  it('render successfully', () => {
    const json = renderer.create(<Icon name="alipay"/>).toJSON()
    expect(json).toMatchSnapshot()
  })
  it('onClick', () => {
    const fn = jest.fn()
    const component = mount(<Icon name="alipay" onClick={fn}/>)
    component.find('svg').simulate('click')
    expect(fn).toBeCalled()
  })
})

IDE 提示找不到 describe 和 it 怎么办?

解决办法:

  1. yarn add -D @types/jest
  2. 在文件开头加一句 import 'jest'

这是因为 describe 和 it 的定于位于 jest 的类型声明文件中,不信你可以按住 ctrl 并点击 jest 查看。

如果还不行,你需要在 WebStorm 里设置对 jest 的引用:

这是因为 typescript 默认排除了 node_modules 里的类型声明。

总结

以上主要是在学习造轮子过程总结的,环境搭建就没有细说了,主要记录实现 Icon 轮子的一些思路及注意事项等,想看源码,跑跑看的,可以点击这里查看。

参考

方应杭老师的React造轮子课程

欢迎加入前端大家庭,里面会经常分享一些技术资源。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant