从零搭建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个属性:
- 输入框初始默认值(defaultValue)
- 占位提示信息(placeholder)
- 输入改变事件(onChange)
对于UI结构来说,我们可以使用一个div作为整体包裹,然后左侧是图标的区域(使用一个div),右侧是输入框(input)。
综上,我们的初始代码如下:
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样式文件,当然,对于该文件我们不赘述实现。
@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;
}
}
修改组件代码,改动如下:
- 以模块化的方式引入less文件
import * as React from "react";
+import styles from './index.module.less';
- 针对每个元素配置其less样式,采用
styles.xxx
方式使用
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需要进行简单的配置:
{
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:
{
"compilerOptions": {
"noEmit": true,
"esModuleInterop": true,
"jsx": "react"
},
"include": [
"src",
"./src/external.d.ts"
]
}
其中,"noEmit": true
表明由ts进行类型检查,但是不编译文件。include中的./src/external.d.ts
中的内容如下:
// less模块声明
declare module '*.module.less' {
const content: { [className: string]: string };
export = content;
}
也就是说,希望IDEA的内置ts读取tsconfig.json,并添加关于import*.module.less
时候得到的模块的类型定义。这样,IDEA的ts类型检查在这句话的时候就不会报错了:
import styles from './index.module.less';
总结一下,想要在ts+babel-loader项目中使用样式模块化。
在类型检查阶段,需要:
- 单独配置tsconfig.json
- 编写d.ts,并被tsconfig.json配置包含在类型定义查找的范围(inlcude)
在编译阶段,需要只需要配置css-loader的module为true即可。
这一块我会再写一篇文章来单独讲解webpack+ts+babel的方案。
效果演示
编写样例html:
<!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后打开样例界面,可以看到如下效果:
svg引入配置
实际上,react中想要使用svg有这很多种方式,像是直接编写react组件,并返回svg代码:
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来进行处理。
那么,是否存在这样一种方式:
import IconSearch from 'path/to/search.svg'
// IconSearch是一个React组件,可以在其他组件中使用
个人最常使用的方案是svgr/webpack
(Webpack - SVGR (react-svgr.com))
只需要三个步骤的配置:
- 引入@svgr/webpack
yarn add -D @svgr/webpack
- 配置webpack,处理svg
{
test: /\.svg$/,
use: ['@svgr/webpack'],
}
- external.d.ts配置(配置理由和上述less配置一样,为了达到类型检查)
// svg类型
declare module '*.svg' {
const content: React.FunctionComponent<React.SVGAttributes<React.ReactSVGElement>>
export default content
}
完成配置以后,我们就可以通过import XXX from 'path/to/xxx.svg',来使用SVG组件了:
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>
{/* 输入框*/}