本輪子是經過 React + TypeScript + Webpack 搭建的,至於環境的搭建這邊就不在細說了,本身動手谷歌吧。固然能夠參考個人源碼。css
這裏我也是經過別人學的,主要作些總結及說明造各個輪子的一種思路,方便從此使用別人的的輪子時本身腦中有造輪子的思想,能經過修改源碼及時修改 bug,按時上線。html
本文的 Icon 組件主要是參考 Framework7 中的 Icon React Component 寫的。前端
想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等着你!node
1.爲了避免求人react
假設你使用某個UI框架發現有一個 bug,因而你反饋給開發者,開發者說兩週後修復,而你的項目一週後就要上線,你怎麼辦?webpack
爲何不少大公司都不使用其餘公司的輪子,要本身造?爲了把控本身的業務,不被別人牽着走。git
2.爲了避免流於平庸es6
3.爲了創造github
4.爲何是 UI 輪子,不是其餘方面的輪子web
本輪子使用 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)
複製代碼
對於上面的定義方式,後面的輪子會常用,因此沒必要擔憂看不懂。
在上面咱們指定了 Icon 的name
爲wechat
,那怎麼讓它顯示微信的圖標呢,首先在阿里的 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'))
複製代碼
運行效果:
你們有沒有注意到,我須要使用哪一個 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'
當咱們須要給 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'))
複製代碼
上述咱們只監聽了 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('')
})
})
複製代碼
這裏測試 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()
})
})
複製代碼
解決辦法:
這是由於 describe 和 it 的定於位於 jest 的類型聲明文件中,不信你能夠按住 ctrl 並點擊 jest 查看。
若是還不行,你須要在 WebStorm 裏設置對 jest 的引用:
這是由於 typescript 默認排除了 node_modules
裏的類型聲明。
以上主要是在學習造輪子過程總結的,環境搭建就沒有細說了,主要記錄實現 Icon 輪子的一些思路及注意事項等,想看源碼,跑跑看的,能夠點擊這裏查看。
方應杭老師的React造輪子課程
乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。
我是小智,公衆號「大遷世界」做者,對前端技術保持學習愛好者。我會常常分享本身所學所看的乾貨,在進階的路上,共勉!
關注公衆號,後臺回覆福利,便可看到福利,你懂的。