React 造輪子系列:Icon 組件思路

簡介

本輪子是經過 React + TypeScript + Webpack 搭建的,至於環境的搭建這邊就不在細說了,本身動手谷歌吧。固然能夠參考個人源碼css

這裏我也是經過別人學的,主要作些總結及說明造各個輪子的一種思路,方便從此使用別人的的輪子時本身腦中有造輪子的思想,能經過修改源碼及時修改 bug,按時上線。html

本文的 Icon 組件主要是參考 Framework7 中的 Icon React Component 寫的。前端

想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等着你!node

爲何要造輪子

1.爲了避免求人react

  • 假設你使用某個UI框架發現有一個 bug,因而你反饋給開發者,開發者說兩週後修復,而你的項目一週後就要上線,你怎麼辦?webpack

  • 爲何不少大公司都不使用其餘公司的輪子,要本身造?爲了把控本身的業務,不被別人牽着走。git

2.爲了避免流於平庸es6

  • 你們都是寫增刪改查,你跟別人比有什麼優點?你若是能說一局【我公司的人都在用我寫的UI框架】是否是就很牛逼?造 UI 輪子會遇到不少技術層面而非業務層面的知識?好比一些算法。

3.爲了創造github

  • 你爲別人作了這麼久的事情,有沒有本身作什麼?自驅動力。

4.爲何是 UI 輪子,不是其餘方面的輪子web

  • 好比,爲何不本身寫一個 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造輪子課程

交流

乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。

github.com/qq449245884…

我是小智,公衆號「大遷世界」做者,對前端技術保持學習愛好者。我會常常分享本身所學所看的乾貨,在進階的路上,共勉!

關注公衆號,後臺回覆福利,便可看到福利,你懂的。

相關文章
相關標籤/搜索