美文网首页
icon组件

icon组件

作者: sweetBoy_9126 | 来源:发表于2019-10-28 16:33 被阅读0次

创建一个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

  1. 在webpack中添加svg的loader
{
  test: /\.svg$/,
  loader: 'svg-sprite-loader'
}
  1. 在我们的ts的声明中声明一下svg的类型
    创建lib/types/custom.d.ts
declare module '*.svg' {
    const content: any;
    export default content;
}
  1. 在tsconfig里添加types
  • tsconfig.json
"include": [
  "lib/**/*"
],
  1. 使用svg标签,里面是一个use它的属性是xlinkHref定义的id就是我们svg的文件名
import './icons/wechat.svg'
const Icon: React.FunctionComponent<IconProps> = (props) => {
    return (
        <span>
            <svg>
                <use xlinkHref="#wechat"></use>
            </svg>
        </span>
    )
}
  1. 通过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

  1. 配置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库来测试点击事件

  1. yarn add --dev enzyme
  2. 将要测试的元素通过mount挂载到页面
import { mount } from 'enzyme'
const component = mount(<Icon name="alipay" onClick={fn}/>)
  1. 通过simulate来触发对应事件
component.find('svg').simulate('click')
  1. 声明触发事件的函数为jest.fn()
const fn = jest.fn()
  1. 期待我们的函数被调用
expect(fn).toBeCalled
  1. 在test/setupTests.js里添加enzyme配置
const enzyme = require('enzyme')
const Adapter = require('enzyme-adapter-react-16')

enzyme.configure({adapter: new Adapter()})
  1. 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
})

相关文章

网友评论

      本文标题:icon组件

      本文链接:https://www.haomeiwen.com/subject/qfkbvctx.html