创建一个icon.tsx接受一个name属性,也就是你的图标的名字
- icon.tsx
import React from 'react';
interface IconProps {
name: string
}
const Icon: React.FunctionComponent<IconProps> = (props) => {
return (
<span>{props.name}</span>
)
}
export default Icon;
- index.tsx
import Icon from './icon'
ReactDom.render(
<div>
<Icon name="wechat"/>
</div>
, document.querySelector('#root'));
通过iconfont引入一个svg
- 在webpack中添加svg的loader
{
test: /\.svg$/,
loader: 'svg-sprite-loader'
}
- 在我们的ts的声明中声明一下svg的类型
创建lib/types/custom.d.ts
declare module '*.svg' {
const content: any;
export default content;
}
- 在tsconfig里添加types
- tsconfig.json
"include": [
"lib/**/*"
],
- 使用svg标签,里面是一个use它的属性是xlinkHref定义的id就是我们svg的文件名
import './icons/wechat.svg'
const Icon: React.FunctionComponent<IconProps> = (props) => {
return (
<span>
<svg>
<use xlinkHref="#wechat"></use>
</svg>
</span>
)
}
- 通过props动态接受多个name
import './icons/wechat.svg'
import './icons/alipay.svg'
import './icons/qq.svg'
interface IconProps {
name: string
}
const Icon: React.FunctionComponent<IconProps> = (props) => {
return (
<span>
<svg>
<use xlinkHref={`#${props.name}`}/>
</svg>
</span>
)
}
export default Icon;
问题:如果我们有几十个icon的话,那么我们一个个import就会很麻烦,我们如何直接import一个目录,让它直接自动引入下面的所有文件那。
解决方法:新建一个lib/importicons.js
let importAll = (requireContext) => requireContext.keys().forEach(requireContext)
try {
importAll(require.context('./icons/', true, /\.svg$/))
} catch {
}
tree-shaking
- 静态引入
对于目录或者库里的某一模块或某一文件引入,比如:
import A from './a
- 非静态引入
直接引入一个完整的库或者完整的目录
importAll './all'
tree-shaking: 比喻我们项目的依赖把那些没有依赖的从打包里删掉,只留下我们真正用到的依赖,tree-shaking的基础是静态引入
配置sass
- 配置scss loader
- webpack.config.js
rules: [
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
}
]
loader的解析执行顺序从右往左
sass-loader
: 将.scss
文件以字符串的形式把语法翻译成css
的语法;
css-loader
: 将翻译的css
语法文件变成一个对象;
style-loader
: 把对象变成一个style
标签
yarn add --dev style-loader css-loader sass-loader
yarn add node-sass
这里为了不影响其他人的类名和被其他人影响,我们最后在我们的类名前面加一个前缀
- icon.scss
.ireact-icon {
width: 1.4rem;
height: 1.4rem;
}
- icon.tsx
<svg className="ireact-icon">
<use xlinkHref={`#${props.name}`}/>
</svg>
接受一个onClick事件
-i ndex.tsx
let fn = (e: React.MouseEvent) => {
console.log((e.target as HTMLDivElement).style)
}
ReactDom.render(
<div>
<Icon name="qq" onClick={fn}/>
</div>
, document.querySelector('#root'));
- icon.tsx
interface IconProps {
name: string;
onClick: React.MouseEventHandler //onClick类型是一个React的鼠标回调事件
}
const Icon: React.FunctionComponent<IconProps> = (props) => {
return (
<svg className="ireact-icon" onClick={props.onClick}>
<use xlinkHref={`#${props.name}`}/>
</svg>
)
}
问题:如果我们有多个事件的话,那我们就得每个都在我们我们的iconProps里定义,就会很复杂
解决办法:让IconProps继承React里的SVGAttributes它里面有所有的事件和属性
- icon.tsx
interface IconProps extends React.SVGAttributes<SVGElement>{
name: string;
}
const Icon: React.FunctionComponent<IconProps> = (props) => {
return (
<svg className="ireact-icon" onClick={props.onClick} onMouseEnter={props.onMouseEnter}>
<use xlinkHref={`#${props.name}`}/>
</svg>
)
}
- index.tsx
onClick={fn}
onMouseEnter={() => console.log('mousenter')}
onMouseLeave={() => console.log('mouseleave')}
/>
上面我们在子组件中调用父组件的事件的时候,因为我们也是不确定有哪些事件,所以我们也可以通过...props把所有的事件都解构到icon组件上
<svg className="ireact-icon" {...props}>
<use xlinkHref={`#${props.name}`}/>
</svg>
问题1:因为我们是直接通过props把所有的属性放到了svg上,这时如果我们使用icon的时候父组件也传入一个className,这个className也会在我们解构的props里,所以我们自己的className就会被覆盖
比如:
<Icon name="qq" className="qqq">
const Icon: React.FunctionComponent<IconProps> = (props) => {
const { className, ...restProps } = props
return (
<svg className={`ireact-icon ${className}`} {...restProps}>
<use xlinkHref={`#${props.name}`}/>
</svg>
)
}
问题2:如果用户没有传className
那么就会在页面显示一个undefined
解决方法:
1). 通过三元运算符判断是否有className
,如果有就用否则就是空
<svg className={`ireact-icon ${className ? className : ''}`} {...restProps}>
2). 引入一个classNames
库,可以把我们多个class
合起来,并且不会出现undefined,手写一个classNames
- classes.tsx
// 将数组的所有参数解构出来,里面的每一项是string或undefined
function classes(...names: (string | undefined)[]) {
// 将每一项的值通过Boolean值返回,undefined会被过滤
return names.filter(Boolean).join(' ')
}
export default classes
- icon.tsx
import classNames from './helpers/classes'
<svg className={classNames('ireact-icon', 'qq', className)} {...restProps}>
进一步通过解构来简化我们的参数
const Icon: React.FunctionComponent<IconProps> = ({
className,
name,
...restProps
}) => {
return (
<svg className={classNames('ireact-icon', 'qq', className)} {...restProps}>
<use xlinkHref={`#${name}`}/>
</svg>
);
};
单元测试
最简单的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', 'b', undefined)
expect(result).toEqual('a b')
})
it('接受 0 个参数', () => {
const result = classes()
expect(result).toEqual('')
})
})
Snapshot(快照)
当我们运行测试的时候会得到第一个快照,这时候我们需要判断这个快照是对的还是错的,如果是对的就通过test -u 保存下来,如果是错的那么我们就继续运行测试,每次就是跟上一次运行的结果作对比。
- icon.unit.jsx
import React from 'react'
import renderer from 'react-test-renderer'
import Icon from '../icon'
describe('Icon', () => {
it('是个svg', () => {
//渲染一个Button,因为Button是一个对象所以我们可以把它转成json
const json = renderer.create(<Icon/>).toJSON()
//期待它去匹配Snapshot
expect(json).toMatchSnapshot()
})
})
运行yarn test
报错 Cannot find module 'babel-preset-react-app'
安装babel-preset-react-app发现错误又变了
解决方法:
1). 在test下面新建一个mocks目录,里面存放我们默认mock的文件数据和对象数据
- mock/file-mock.js
// 导出的字符串可以随便写
module.exports = 'test-file-stub'
- mock/object-mock.js
module.exports = {}
2). 配置jest.config.js
里的moduleNameMapper
让css less sass使用object-mock随便导出一个对象其他的使用file-mock随便导出一个字符串
moduleNameMapper: {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/test/__mocks__/file-mock.js",
"\\.(css|less|sass|scss)$": "<rootDir>/test/__mocks__/object-mock.js",
},
这时候我们运行yarn test在我们的快照中会生成
如果我们对我们的测试进行的更改,比如传入一个name,那么就需要运行yarn test -u 来更新我们的快照为最新的
测试点击事件
使用enzyme
库来测试点击事件
yarn add --dev enzyme
- 将要测试的元素通过
mount
挂载到页面
import { mount } from 'enzyme'
const component = mount(<Icon name="alipay" onClick={fn}/>)
- 通过
simulate
来触发对应事件
component.find('svg').simulate('click')
- 声明触发事件的函数为
jest.fn()
const fn = jest.fn()
- 期待我们的函数被调用
expect(fn).toBeCalled
- 在test/setupTests.js里添加enzyme配置
const enzyme = require('enzyme')
const Adapter = require('enzyme-adapter-react-16')
enzyme.configure({adapter: new Adapter()})
yarn add --dev enzyme-adapter-react-16
完整点击事件测试代码
it('onClick', () => {
const fn = jest.fn()
const component = mount(<Icon name="alipay" onClick={fn}/>)
component.find('svg').simulate('click')
expect(fn).toBeCalled
})
网友评论