内容简介:本轮子是通过 React + TypeScript + Webpack 搭建的,至于环境的搭建这边就不在细说了,自己动手谷歌吧。当然可以参考我的这里我也是通过别人学的,主要做些总结及说明造各个轮子的一种思路,方便今后使用别人的的轮子时自己脑中有造轮子的思想,能通过修改源码及时修改 bug,按时上线。本文的 Icon 组件主要是参考
简介
本轮子是通过 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
在上面我们指定了 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'))
运行效果:
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 怎么办?
解决办法:
- yarn add -D @types/jest
- 在文件开头加一句 import 'jest'
这是因为 describe 和 it 的定于位于 jest 的类型声明文件中,不信你可以按住 ctrl 并点击 jest 查看。
如果还不行,你需要在 WebStorm 里设置对 jest 的引用:
这是因为 typescript 默认排除了 node_modules
里的类型声明。
总结
以上主要是在学习造轮子过程总结的,环境搭建就没有细说了,主要记录实现 Icon 轮子的一些思路及注意事项等,想看源码,跑跑看的,可以点击 这里 查看。
参考
方应杭老师的React造轮子课程
欢迎加入前端大家庭,里面会经常分享一些技术资源。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 造轮子之图片预览组件(preview)
- 动手造轮子,用DownLoadManage封装一个App的更新组件(兼容android 6、7、8)
- 请继续重复发明轮子
- 造一个「轮子」musionUI
- 造轮子-golang日志系统
- 造轮子 | golang | 单元测试
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Web信息架构(第3版)
[美] Peter Morville、Louis Rosenfeld / 陈建勋 / 电子工业出版社 / 2013-10 / 99.00元
本书内容涵盖了信息架构基本原理和实践应用的方方面面。全书共7个部分,包括信息架构概述、信息架构的基本原理、信息架构的开发流程和方法论、信息架构实践、信息架构与组织、两个案例研究,以及参考资料清单。 本书兼具较高的理论价值和实用价值,曾被Web设计领域多本书籍重点推荐,是信息架构领域公认的经典书籍,不论新手还是专家都能各取所需。本书可供Web设计与开发者、Web架构师、网站管理者及信息管理相关......一起来看看 《Web信息架构(第3版)》 这本书的介绍吧!