本輪子是經過 React + TypeScript + Webpack 搭建的,至於環境的搭建這邊就不在細說了,本身動手谷歌吧。固然能夠參考個人源碼。css
這裏我也是經過別人學的,主要作些總結及說明造各個輪子的一種思路,方便從此使用別人的的輪子時本身腦中有造輪子的思想,能經過修改源碼及時修改 bug,按時上線。html
本文的 Icon 組件主要是參考 Framework7 中的 Icon React Component 寫的。前端
想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等着你!node
1.爲了避免求人react
2.爲了避免流於平庸webpack
3.爲了創造git
4.爲何是 UI 輪子,不是其餘方面的輪子es6
本輪子使用 React + TypeScript
來寫的,那麼在 ts 中如何聲明函數組件及級 Icon 組件傳遞參數呢,答案是使用React提供的靜態方法 React.FunctionComponent
及 TypeScript 提供的接口定義。github
// 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 中調用:web
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造輪子課程
你的點贊是我持續分享好東西的動力,歡迎點贊!