使用之前我們
react + typescript
搭建的腳手架來(lái)寫(xiě)我們第一個(gè)也是最簡(jiǎn)單的Icon
組件
開(kāi)發(fā)UI組件庫(kù)-從0開(kāi)始搭建 React + TypeScript(一)
-
首先我們先分析一下 一個(gè)
Icon
組件需要哪些屬性- 應(yīng)該有名字
name
, 知道是哪一個(gè)icon
- 應(yīng)該可以附加顏色
color
...
// TODO: <Icon name="iconName" color="#fff" { ...restProps } />
- 應(yīng)該有名字
-
我們分析了
Icon
應(yīng)該有哪些屬性, 我們第一步先創(chuàng)建文件lib/Icon/icon.tsx
import React from 'react' const Icon = (props) => { return <div>icon</div> } export default Icon
-
我們?cè)贋?
Icon
添加第一個(gè)屬性,name
import React from 'react' interface IconProps { name: string } const Icon: React.FunctionComponent<IconProps> = (props) => { return ( <div>{ props.name }</div> ) }
-
在寫(xiě)下面的 Icon 代碼之前, 我們先思考一下, 我們從哪里獲取 Icon?
4.1 我主要從 阿里巴巴圖標(biāo)庫(kù) , 來(lái)下載svg
圖片,豐富我的Icon
圖標(biāo), 統(tǒng)一放到一個(gè)文件夾下, 例如:assert/icon
4.2webpack
里面使用svg
需要做一些配置
svg-sprite.png
4.3svg-sprite-loader
作用:svg-sprite-loade
r會(huì)把你的icon
塞到一個(gè)個(gè)symbol
中,symbol
的id
如果不特別指定悟衩,就是你的文件名
多望。它最終會(huì)在你的html
中嵌入這樣一個(gè)svg
,根據(jù)導(dǎo)入的 svg 文件自動(dòng)生成 symbol 標(biāo)簽并插入 html
4.4 可以參考 未來(lái)必?zé)幔篠VG Sprite技術(shù)介紹<svg class="icon" aria-hidden="true"> <use xlink:href="#icon-xxx"></use> </svg>
- 看了上面的分析, 我們來(lái)簡(jiǎn)單寫(xiě)一下
Icon
組件
5.1 我們引入import React from 'react' import './assert/icon/alipay.svg' // 我們有一個(gè) alipay 的圖標(biāo) import './assert/icon/qq.svg' // 我們有一個(gè) qq 的圖標(biāo) interface IconProps { name: string } const Icon: React.FunctionComponent<IconProps> = (props) => { return ( <svg> <use xlinkHref="#alipay"></use> </svg> <svg> <use xlinkHref="#qq"></use> </svg> ) }
svg
時(shí), 如果是通過(guò)import qq from './icon/qq.svg'
, 引入, 會(huì)被ts
報(bào)一個(gè)找不到模塊的錯(cuò)誤, 因?yàn)槲覀?import
一個(gè)東西, 對(duì)應(yīng)的應(yīng)該有 導(dǎo)出, 但沒(méi)有, 所以我們可以添加一個(gè)聲明
export-svg.png
- 我們每次
import
一個(gè)svg
, 就可以顯示一個(gè)圖標(biāo), 我們希望用戶傳進(jìn)來(lái)name
, 就可以使用對(duì)應(yīng)的icon
, 組件化
icon的name屬性
使用Icon組件
html會(huì)有svg的元素, 可以看到icon已經(jīng)到頁(yè)面了
-
但是隨著
icon
越來(lái)越多, 有幾百個(gè), 我們程序員當(dāng)然不會(huì)import
幾百遍, 那該怎么解決呢?- 利用
webpack
的 `require.contextrequire.context會(huì)返回一個(gè)函數(shù)扼倘,并且該函數(shù)有keys()按价,id, resolve() 屬性 keys()方法返回的該模塊可以處理的所有可能請(qǐng)求的模塊的數(shù)組瓢湃,簡(jiǎn)單一點(diǎn)就是滿足該參數(shù)的模塊矢炼; resolve()返回的是請(qǐng)求的module的id; id是該context module的id;
- 我們?cè)?
lib/Icon/
創(chuàng)建一個(gè)importAllIcon.js
打印里面的每一項(xiàng)可以查看對(duì)應(yīng)值
打印require.context會(huì)把目錄下的icons 全部映射 - 利用
- 這樣我們就不用一個(gè)一個(gè)引入
icon
, 可以使用有name
的icon
了
icon 初步完成
-
初步完成了
Icon
組件, 我們來(lái)思考一個(gè)問(wèn)題, 是應(yīng)該一個(gè)一個(gè)import
, 還是importAll
- 使用一個(gè)一個(gè)
import
, 好處是tree-shaking
: 把沒(méi)有依賴的刪除掉, 使用依賴的,tree-shaking
基礎(chǔ)是靜態(tài)引入 -
importAll
, 主要是方便, 但是沒(méi)辦法tree-shaking
- 使用一個(gè)一個(gè)
現(xiàn)在我們來(lái)為我們的
Icon
來(lái)寫(xiě)一些簡(jiǎn)單地樣式, 可以使用less scss stylus
等css
擴(kuò)展語(yǔ)言, 這里我使用的是scss
, 但webpack
需要一些配置
// webpack.config.js
// 我們需要安裝這些依賴
module: {
rules: [
// 配置 ts tsx
{
test: /\.tsx?$/,
loader: "awesome-typescript-loader"
},
// 配置svg
{
test: /\.svg$/,
loader: "svg-sprite-loader"
},
// 配置 scss-loader
// npm i css-laoder style-loader sass-loader
{
test: /\.s[ac]ss$/,
use: ["style-loader", "css-loader", "sass-loader"]
},
]
}
這幾個(gè)loader作用, 先使用 sass-loader, 遇見(jiàn) icon.scss 文件, 把這個(gè)文件解析為 類似 icon.css, css-loader 會(huì)把 icon.css變?yōu)?對(duì)象或者字符串, style-loader 會(huì)把這些對(duì)象變?yōu)橐粋€(gè) style標(biāo)簽, 里面是 css
document.createElement('style')
style.innerHTML = '對(duì)象css'
documnet.head.appendChild('style')
一個(gè) loader 只做一件事情
- 我們可以使用了
icon
, 通過(guò)不同的name
展示 不同的icon
, 現(xiàn)在還有什么我可以做的呢? 我們可以給icon
加一個(gè)click
事件
// icon.tsx
import React from "react";
import "./importAllIcons.js";
interface IconProps {
name: string;
// ? 可選擇的屬性 類型是 svg元素的鼠標(biāo)事件處理函數(shù)
onClick?: React.MouseEventHandler<SVGElement>;
}
const Icon: React.FunctionComponent<IconProps> = (props: any) => {
return (
// 接收這個(gè) click 事件
<svg className="yym-icon" onClick={props.onClick}>
<use xlinkHref={`#${props.name}`}></use>
</svg>
);
};
export default Icon;
// 使用 click 事件
import React from "react";
import Icon from "./icon.tsx";
const IconDemo = () => {
// 函數(shù)類型, 鼠標(biāo)事件
const fn: React.MouseEventHandler = e => {
console.log("我被點(diǎn)擊了", e);
console.log("我被點(diǎn)擊了", e.target);
};
return (
<div>
<Icon name="zhihu-circle" onClick={fn} />
</div>
);
};
export default IconDemo;
- 我們僅僅做了
click
事件還是不夠, 如果我們希望icon
有其他的事件, 比如onMouseMove
,onMouseLeave
等事件, 我們?nèi)绻粋€(gè)一個(gè)把我們需要的事件加上去, 就太麻煩了, 那我們應(yīng)該怎么做呢?
- 我們知道我們使用的是
svg
標(biāo)簽, 所以我們的IconProps
也屬于React.SVGAttributes
的屬性
// IconProps 繼承 SVG 的 所有屬性
interface IconProps extends React.SVGAttributes<SVGElement> {
name: string;
}
const Icon: React.FunctionComponent<IconProps> = (props: any) => {
return (
<svg
className="yym-icon"
onClick={props.onClick}
onMouseEnter={props.onMouseEnter}
onMouseLeave={props.onMouseLeave}
>
<use xlinkHref={`#${props.name}`}></use>
</svg>
);
};
- 但是上面也有優(yōu)化空間, 我們不需要一個(gè)一個(gè)的接收所有事件,
// IconProps 繼承 SVG 的 所有屬性
interface IconProps extends React.SVGAttributes<SVGElement> {
name: string;
}
const Icon: React.FunctionComponent<IconProps> = (props: any) => {
return (
// 使用展開(kāi)運(yùn)算符
<svg className="yym-icon" {...props}>
<use xlinkHref={`#${props.name}`}></use>
</svg>
);
};
- 繼續(xù)上一步,完成后, 我們思考一下, 上面我們有自己寫(xiě)的
class
, 如果使用的人也寫(xiě)了自己定義的 class
, 那會(huì)覆蓋我們的className
, 所一我們需要改造一下我們的className
- 我們可以使用已經(jīng)有的
classnames
庫(kù)
- 我們可以使用已經(jīng)有的
- 我們也可以使用
const { className, ...restProps } = props
- 我們也可以使用
const Icon: React.FunctionComponent<IconProps> = (props: any) => {
const { className, ...restProps } = props;
return (
<svg className={`yym-icon ${className}`} {...restProps}>
<use xlinkHref={`#${props.name}`}></use>
</svg>
);
};
- 上面后又一點(diǎn)點(diǎn)問(wèn)題, 如果用戶不傳
className
,class
會(huì)有undefined
, 或者多一個(gè)空格
所以我們自己來(lái)簡(jiǎn)單封裝一個(gè)類classnames
的庫(kù)
// helpers/classes.tsx
// filter Boolean 你給我什么值, 返回什么值 Boolean(1) true Boolean(0) false
// 需要傳一個(gè) string 或 undefined 數(shù)組的值, 簡(jiǎn)單實(shí)現(xiàn)
const classes = (...names: (string | undefined)[]) => {
return names.filter(Boolean).join(" ");
};
export default classes;
// Icon.tsx
import classes from "../helpers/classes";
interface IconProps extends React.SVGAttributes<SVGElement> {
name: string;
}
const Icon: React.FunctionComponent<IconProps> = (props: any) => {
const { className, ...restProps } = props;
return (
<svg className={classes("yym-icon", className)} {...restProps}>
<use xlinkHref={`#${props.name}`}></use>
</svg>
);
};
export default Icon;
- 現(xiàn)在我們的
Icon
, 已經(jīng)可以滿足 通過(guò)傳name
顯示不同的icon
, 可以為icon
定義事件, 我們當(dāng)然也可以通過(guò)傳class 修改我們的icon
的樣式, 那我們有需要為icon
單獨(dú)的color
屬性, 來(lái)只修改icon的顏色嗎?
, 我們可以做也可以不做, 不過(guò)為了方便, 我們可以做的更完美一點(diǎn)
-
svg
更改顏色是通過(guò)fill
屬性來(lái)更改的, 我們動(dòng)態(tài)修改fill
的值, 就可以直接修改icon
的顏色了
const Icon: React.FunctionComponent<IconProps> = (props: any) => {
const { className, color, ...restProps } = props;
return (
<svg
style={{ fill: color }}
className={classes("yym-icon", className)}
{...restProps}
>
<use xlinkHref={`#${props.name}`}></use>
</svg>
);
};
- 我們寫(xiě)到這里, 先看一下我們的代碼都滿足哪些東西, 優(yōu)化一下我們的代碼.
import React from "react";
import "./importAllIcons.js";
import classes from "../helpers/classes";
interface IconProps extends React.SVGAttributes<SVGElement> {
name: string;
}
const Icon: React.FunctionComponent<IconProps> = (props: any) => {
// 這里可以解構(gòu)
const { className, color, name, ...restProps } = props;
return (
<svg
style={{ fill: color }}
className={classes("yym-icon", className)}
{...restProps}
>
<use xlinkHref={`#${props.name}`}></use>
</svg>
);
};
export default Icon;
// 優(yōu)化后的完整代碼
import React from "react";
import "./importAllIcons.js";
import classes from "../helpers/classes";
interface IconProps extends React.SVGAttributes<SVGElement> {
name: string;
}
const Icon: React.FunctionComponent<IconProps> = ({
className,
color,
name,
...restProps
}) => {
return (
<svg
className={classes("yym-icon", className)}
style={{ fill: color }}
{...restProps}
>
<use xlinkHref={`#${name}`}></use>
</svg>
);
};
export default Icon;
- 最后, 上面是我們的
Icon
組件的源代碼, 我們?cè)趺词褂媚?
// 使用 Icon
import React from "react";
import Icon from "./Icon/icon";
const Demo = () => {
const fn: React.MouseEventHandler = e => {
console.log("我被點(diǎn)擊了", e);
console.log("我被點(diǎn)擊了", e.target);
};
return (
<div>
<Icon name="alert" />
<Icon name="alert" className="icon" />
<Icon name="alert" className="icon" color="red" />
<Icon name="alert" className="icon" color="red" onClick={fn} />
</div>
);
};
export default Demo;
Icon 組件完成了, 我們繼續(xù)使用 ts 寫(xiě)下一個(gè)組件