从零搭建react与ts组件库(二)less模块化与svg引入配置

在上一篇《从零搭建react+ts组件库(一)项目搭建与封装antd组件》介绍了使用webpack来搭建一个基于antd的组件库的基本框架,但是作为一个组件库,实际上还有很多的都还未引入,本篇将会补充less模块化以及svg引入的基本方式。

本文所有修改的代码分支为chapter02位于w4ngzhen/r-ui (github.com)仓库的chapter02_less_and_svg分支,该分支基于上一篇文章的chapter01_init分支而来(main分支总是显示最新的内容)。

为了讲解如何进行less模块化配置以及如何引入svg作为组件库的一部分,我们设想这样一个需求:一个搜索输入框,左侧是一个svg的icon搜索图标,右侧是输入框。

组件规划

首先考虑组件具备的属性,作为一个简单的搜索框,我们至少有3个属性:

  1. 输入框初始默认值(defaultValue)
  2. 占位提示信息(placeholder)
  3. 输入改变事件(onChange)

对于UI结构来说,我们可以使用一个div作为整体包裹,然后左侧是图标的区域(使用一个div),右侧是输入框(input)。

综上,我们的初始代码如下:

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
import * as React from "react";

interface SearchInputProps {
defaultValue?: string;
placeholder?: string;
onChange?: (value: string, e: React.ChangeEvent<HTMLInputElement>) => void;
}

const SearchInput: React.FC<SearchInputProps> = (props) => {
const {
defaultValue,
placeholder,
onChange,
} = props;

const inputOnChange: React.ChangeEventHandler<HTMLInputElement>
= (e: React.ChangeEvent<HTMLInputElement>) => {
if (onChange) {
onChange(e.target.value, e);
}
}

return (
// 包裹
<div>
{/*存放icon*/}
<div></div>
{/* 输入框*/}
<input defaultValue={defaultValue}
placeholder={placeholder}
onChange={inputOnChange}></input>
</div>
);
}

less样式模块化配置

首先我们编写less样式文件,当然,对于该文件我们不赘述实现。

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
37
38
39
40
41
@input-size: 32px;

.centerAll() {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}

.searchInputWrap {

width: 100%;
height: @input-size;

background-color: #f4f4f4;
border-radius: 5px;

.centerAll();

.searchInputIconBox {
width: @input-size;
height: @input-size;

background-color: transparent;
.centerAll();
}

.searchInput {
width: calc(100% - @input-size);
height: 100%;
border: none;
outline: none;
background-color: transparent;
}

.searchInput:focus {
border: none;
outline: none;
}
}

修改组件代码,改动如下:

  1. 以模块化的方式引入less文件
1
2
 import * as React from "react";
+import styles from './index.module.less';
  1. 针对每个元素配置其less样式,采用styles.xxx方式使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
     return (
// 包裹
- <div>
+ <div className={styles.searchInputWrap}>
{/*存放icon*/}
- <div></div>
+ <div className={styles.searchInputIconBox}></div>
{/* 输入框*/}
- <input defaultValue={defaultValue}
+ <input className={styles.searchInput}
+ defaultValue={defaultValue}
placeholder={placeholder}
onChange={inputOnChange}></input>
</div>
)

webpack对于css-loader需要进行简单的配置:

1
2
3
4
5
6
7
8
9
10
11
12
           {
loader: MiniCssExtractPlugin.loader,
},
- 'css-loader',
+ {
+ loader: "css-loader",
+ options: {
+ modules: true
+ }
+ },
'less-loader'

对于该处的配置,详细可以查看关于css-loader的文档webpack-contrib/css-loader: CSS Loader (github.com)

此时,如果有同学在使用IDEA会发现有编译报错。有同学会发现,我们的项目里面没有直接安装typescript,那么为什么IDEA能够检测到我们代码呢?实际上这是IDEA自带的ts在进行类型检测,仅仅是类型检查,实际上编译过程我们是调用的babel-loader+preset/typescript这条链路来完成的,所以并不影响编译后的内容。当然,为了能够进行正确的类型检查,我们在项目根目录下添加tsconfig.json:

1
2
3
4
5
6
7
8
9
10
11
{
"compilerOptions": {
"noEmit": true,
"esModuleInterop": true,
"jsx": "react"
},
"include": [
"src",
"./src/external.d.ts"
]
}

其中,"noEmit": true表明由ts进行类型检查,但是不编译文件。include中的./src/external.d.ts中的内容如下:

1
2
3
4
5
// less模块声明
declare module '*.module.less' {
const content: { [className: string]: string };
export = content;
}

也就是说,希望IDEA的内置ts读取tsconfig.json,并添加关于import*.module.less时候得到的模块的类型定义。这样,IDEA的ts类型检查在这句话的时候就不会报错了:

1
import styles from './index.module.less';

总结一下,想要在ts+babel-loader项目中使用样式模块化。

在类型检查阶段,需要:

  1. 单独配置tsconfig.json
  2. 编写d.ts,并被tsconfig.json配置包含在类型定义查找的范围(inlcude)

在编译阶段,需要只需要配置css-loader的module为true即可。

这一块我会再写一篇文章来单独讲解webpack+ts+babel的方案。

效果演示

编写样例html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>r-ui example</title>
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="r-ui.umd.js"></script>
<link href="r-ui.umd.css" rel="stylesheet"/>
</head>
<body>
<div id="example"></div>
<script>
// window上存在rui,是因为我们将组件包导出为了umd包,取名为rui
const button = React.createElement(rui.SearchInput, {
placeholder: '占位符',
defaultValue: 'hello, world',
onChange: (value, e) => console.log(value, e)
});
ReactDOM.render(button, document.getElementById('example'));
</script>
</body>
</html>

编译r-ui后打开样例界面,可以看到如下效果:

010-less-module

svg引入配置

实际上,react中想要使用svg有这很多种方式,像是直接编写react组件,并返回svg代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
import * as React from "react";

const IconSearch = () => {
return (
<svg className="icon"
viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="4136" width="25" height="25">
<path
d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6c3.2 3.2 8.4 3.2 11.6 0l43.6-43.5c3.2-3.2 3.2-8.4 0-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z"
p-id="4137"></path>
</svg>
);
}

这种方式弊端在于,每次svg有修改的时候,都需要重新复制svg代码对代码文件进行修改,且很多svg的数据较为复杂,容易出错。

将svg作为react组件来使用

我们知道,对于webpack来说,可以将一切的东西都是为模块,对于任何import进来的,webpack都可以通过匹配的规则(rules)调用对应的loader来进行处理。

那么,是否存在这样一种方式:

1
2
import IconSearch from 'path/to/search.svg'
// IconSearch是一个React组件,可以在其他组件中使用

个人最常使用的方案是svgr/webpackWebpack - SVGR (react-svgr.com)

只需要三个步骤的配置:

  1. 引入@svgr/webpack
1
yarn add -D @svgr/webpack
  1. 配置webpack,处理svg
1
2
3
4
{
test: /\.svg$/,
use: ['@svgr/webpack'],
}
  1. external.d.ts配置(配置理由和上述less配置一样,为了达到类型检查)
1
2
3
4
5
// svg类型
declare module '*.svg' {
const content: React.FunctionComponent<React.SVGAttributes<React.ReactSVGElement>>
export default content
}

完成配置以后,我们就可以通过import XXX from ‘path/to/xxx.svg’,来使用SVG组件了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 import * as React from "react";
import styles from './index.module.less';
+import IconSearch from "../../assets/svg/search.svg";

interface SearchInputProps {
defaultValue?: string;
// ... ...
// 包裹
<div className={styles.searchInputWrap}>
{/*存放icon*/}
- <div className={styles.searchInputIconBox}></div>
+ <div className={styles.searchInputIconBox}>
+ <IconSearch width={18} height={18}/>
+ </div>
{/* 输入框*/}

效果演示

020-use-svg