本轮子是经过 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,欢迎 加群 互相学习。
我是小智,公众号「大迁世界」做者,对前端技术保持学习爱好者。我会常常分享本身所学所看的干货,在进阶的路上,共勉!
关注公众号,后台回复福利,便可看到福利,你懂的。