ReactWheels04-03Icon组件下

让Icon支持sass

icon.scss

1
2
3
4
5
.fui-icon {
width: 1.4em;
height: 1.4em;
display: inline-block;
}

在 icon.tsx 里 import ‘./icon.scss’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import './importIcons';
import './icon.scss'

interface IconProps {
name: string;
}

const Icon:React.FunctionComponent<IconProps> = (props)=> {
return (
<svg className="fui-icon">
<use xlinkHref={`#${props.name}`}/>
</svg>
)
}

export default Icon;

运行 yarn start ,报错了

1
2
3
4
5
6
7
8
9

ERROR in ./lib/icon/icon.scss 1:0
Module parse failed: Unexpected token (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
> .fui-icon {
| width: 1.4em;
| height: 1.4em;
@ ./lib/icon/icon.tsx 4:0-21
@ ./lib/index.tsx

安装loader,修改webpack配置

1
2
3
4
5
6
7
8
# 安装
yarn add style-loader css-loader sass-loader node-sass --dev

# use 里的数组意思是 从右往左
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
}
  1. sass-loader 将你的 icon.scss读取到内存(字符串) 把 sass语法变成 icon.css
  2. css-loader 将 icon.css 变成一个对象
  3. style-loader 将 对象 变成 一个 style标签 把对象里的内容放到 style.innerHTML里

让Icon支持 click

于是你去 index.tsx 里

1
2
3
4
ReactDOM.render(<div>
<Icon name="qq" onClick={() => console.log(1)}
/>
</div>, document.querySelector('#root'));

竟然直接报错了

1
2
3
client:159 [at-loader] ./lib/index.tsx:24:6 
TS2322: Type '{ name: string; onClick: () => void; }' is not assignable to type 'IntrinsicAttributes & IconProps & { children?: ReactNode; }'.
Property 'onClick' does not exist on type 'IntrinsicAttributes & IconProps & { children?: ReactNode; }'.

意思就是 IconProps 接口里没有onClick 属性

添加 onClick ,但是它的类型是什么

此时就体现你是js程序员和ts程序员的区别了

  • js只管它叫什么从来不管它是什么
  • ts一定要问你它到底是什么
1
2
3
4
5
6
// 最终答案
interface IconProps {
name: string;
// svg元素的鼠标处理函数
onClick:React.MouseEventHandler<SVGAElement>
}

以推敲的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// icon.tsx
interface IconProps {
name: string;
onClick:()=>undefined
}

// index.tsx
const fn = ()=>{
console.log(1);
}
ReactDOM.render(<div>
<Icon name="qq" onClick={fn}
/>
</div>, document.querySelector('#root'));


// 又报错了
TS2322: Type '() => void' is not assignable to type '() => undefined'.
Type 'void' is not assignable to type 'undefined'.

意思是你声明的函数 fn返回的是 void,而你要的函数返回的是undefined

所以你不能写 undefined,所以应该改写 void

interface IconProps {
name: string;
onClick:()=> void
}

const Icon:React.FunctionComponent<IconProps> = (props)=> {
return (
<svg className="fui-icon" onClick={props.onClick}>
<use xlinkHref={`#${props.name}`}/>
</svg>
)
}

我想把点击的元素打印出来怎么办?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//index.tsx
const fn = (e)=>{
console.log(e);
}

此时 ide 提示 不知道这个e是什么 你需要在IconProps里定义函数的参数的类型

// 修改 index.tsx
const fn = (e:React.MouseEvent)=>{
console.log(e);
console.log(e.target);
}

// icon.tsx
interface IconProps {
name: string;
onClick:(e:React.MouseEvent) => void
}

此时我想打印 e.target.style

  • style被标红了,意思是 我怎么知道 target里面有没有 style属性呢?
  • 因为这样是存在潜在bug的,因为你不知道target到底是什么 可能是div也可能是 img 你没法保证它一定有 width属性
    1
    2
    3
    4
    5
    6
    7
    // 修改index.tsx,此时 

    const fn = (e:React.MouseEvent)=>{
    console.log(e);
    console.log(e.target);
    console.log(e.target.style);
    }

答案是 React.MouseEvent 是可以接受参数的

1
2
3
4
5
6
7
8
9
// 如果你要明确 target的类型 就要在 MouseEvent 上加参数
const fn = (e:React.MouseEvent<SVGElement | SVGUseElement>)=>{
console.log(e);
console.log(e.target);
console.log((e.target as SVGUseElement).href);
}

// target as xxxElement 意思把这个target当作 xxx元素用
// 它是一种断言,有可能是错的

标准写法(清爽写法)

1
2
3
4
5
6
7
8
9
10
// icon.tsx
interface IconProps {
name: string;
onClick:React.MouseEventHandler<SVGElement>
}

// index.tsx
const fn:React.MouseEventHandler = (e)=>{
console.log(e.target);
}

如何接受所有事件, mouseover/doubleclick/mouseleave 这样写那么多事件要猴年马月啊

  • IconProps 里一个一个写(太笨了)
  • 是否记得 打印 target的时候有很多属性 如name/width/href 是从那里来的 是继承来的
  • 所以我们的IconProps 应该继承 一个东西
  • 编辑器里点击 import React from 'react' 搜索 onClick 看到 DOMAttributes
1
2
3
4
5
6
7
8
9
10
interface IconProps extends React.SVGAttributes{
name: string;
}
// 这样写会报错,然后提示你还需要一个参数,意思就是它里面的内容如 onClick 要接受一个 target是什么类型


// 最终答案
interface IconProps extends React.SVGAttributes<SVGElement>{
name: string;
}

修改 index.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
import ReactDOM from 'react-dom';
import React from 'react';
import Icon from './icon/icon';
const fn:React.MouseEventHandler = (e)=>{
console.log(e.target);
}
ReactDOM.render(<div>
<Icon name="qq"
onClick={fn}
onMouseEnter={()=>{console.log('enter')}}
onMouseLeave={()=>{console.log('leave')}}
/>
</div>, document.querySelector('#root'));

修改 icon.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface IconProps extends React.SVGAttributes<SVGElement>{
name: string;
}

const Icon:React.FunctionComponent<IconProps> = (props)=> {
return (
<svg className="fui-icon"
onClick={props.onClick}
onMouseEnter={props.onMouseEnter}
onMouseLeave={props.onMouseLeave}
>
<use xlinkHref={`#${props.name}`}/>
</svg>
)
}

虽然完成了我们的需求,但是如果N多个事件难道要在组件里一个一个的写吗?

优化

修改 icon.tsx

1
2
3
4
5
6
7
8
9
10
11
12
//{...props}  react 里规定 写js要用 "{}" 包起来
//所以 {...props} 别误以为是对象

const Icon:React.FunctionComponent<IconProps> = (props)=> {
return (
<svg className="fui-icon"
{...props}
>
<use xlinkHref={`#${props.name}`}/>
</svg>
)
}

这样写没问题吗?此时有bug

  • bug就是如果外面的人也传递了 className呢?
1
2
3
4
5
6
7
8
<Icon name="qq" 
className="qq"
onClick={fn}
onMouseEnter={()=>{console.log('enter')}}
onMouseLeave={()=>{console.log('leave')}}
/>

这样导致 我们的Icon组件 class="qq" 而不是 "fui-icon qq"

这就是 react和vue的理念冲突,如果你用vue不用操心它帮你处理,而react则需要你自己处理

再一次优化

1
2
3
4
5
6
7
8
9
10
const Icon:React.FunctionComponent<IconProps> = (props)=> {
const {className,...restProps} = props;
return (
<svg className={`fui-icon ${className}`}
{...restProps}
>
<use xlinkHref={`#${props.name}`}/>
</svg>
)
}

此时还有bug 如果用户不传递 className呢? 就导致 class=”fui-icon undefined”

classnames库,它就是把多个 class合并的库,但是很明显我们不会额外引入其他的库

新建 lib/helpers/classes.tsx

1
2
3
4
5
6
// 将所有参数放到names数组里,然后声明names是一个字符串数组
function classes(...names: string[]) {
return names.join(' ');
}

export default classes;

修改 icon.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from 'react';
import './importIcons';
import './icon.scss'
import classes from '../helpers/classes';

interface IconProps extends React.SVGAttributes<SVGElement>{
name: string;
}

const Icon:React.FunctionComponent<IconProps> = (props)=> {
const {className,...restProps} = props;
return (
<svg className={classes('fui-icon',className)}
{...restProps}
>
<use xlinkHref={`#${props.name}`}/>
</svg>
)
}

export default Icon;
// 此时className={classes('fui-icon',className)} 标红了 意思这里的className可能不是 string

再次修改 classnames.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function classes(...names: (string|undefined)[]) {
return names.join(' ');
}

export default classes;

// 但是这样 ["1",undefined] ===> "1 " 多了一个空格,强迫症导致必须改了

function classes(...names: (string | undefined)[]) {
return names.filter(Boolean).join(' ');
}

export default classes;


// Boolean(xxx) 的好处就是接受一个值返回 true/false

再次优化我们的 Icon(解构赋值)

  • svg里面有name属性 ,所以name也要单独提取出来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

import React from 'react';
import './importIcons';
import './icon.scss'
import classes from '../helpers/classes';

interface IconProps extends React.SVGAttributes<SVGElement>{
name: string;
}

const Icon:React.FunctionComponent<IconProps> = (props)=> {
const {className,name,...restProps} = props;
return (
<svg className={classes('fui-icon',className)}
{...restProps}
>
<use xlinkHref={`#${name}`}/>
</svg>
)
}

export default Icon;

我们可以直接把解构赋值放在参数里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from 'react';
import './importIcons';
import './icon.scss';
import classes from '../helpers/classes';

interface IconProps extends React.SVGAttributes<SVGElement> {
name: string;
onClick: React.MouseEventHandler<SVGElement>
}

const Icon: React.FunctionComponent<IconProps> =
({ className, name, ...restProps }) => {
return (
<svg className={classes('fui-icon', className)}
{...restProps}
>
<use xlinkHref={`#${name}`} />
</svg>
);
};

export default Icon;

代码链接