一、关于 Webpack 的部分配置
Webpack 的主要作用是当我运行 yarn start、yarn build 时可以对 ts、tsx、scss、svg 文件进行打包压缩或者转译,本项目的配置主要还是参考开源项目,并根据个人理解进行配置。
1、package.json
"scripts": {
"start": "cross-env NODE_ENV=development webpack-dev-server --config webpack.config.dev.js",
"build": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js",
"docs": "cross-env NODE_ENV=production webpack --config webpack.config.docs.js",
},
2、配置文件的入口和出口
// 导入 path 模块
const path = require('path')
// 打包成 npm 发布文件
entry: {
index: './lib/index.tsx'
},
output: {
path: path.resolve(__dirname, 'dist/lib'),
library: 'wu',
libraryTarget: 'umd',
},
// 打包成网站部署文件
entry: {
example: './example.tsx',
},
output: {
path: path.resolve(__dirname, 'docs'),
},
导出模块中用了 path.resolve()
方法,它的作用是将 __dirname
(当前项目的根目录)和 dist/lib
连成一个绝对路径,输出的绝对路径会根据系统的不同而使用不同的斜杠( Linux 系统:\ , window 系统:/ )。
3、配置文件的解析顺序
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx'], // 先解析 .ts 和 .tsx 开头的文件(同名)
},
4、配置模块打包、转译规则
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'awesome-typescript-loader' // 将 ts 文件编译成 js 文件
},
{
test: /\.svg$/,
loader: 'svg-sprite-loader' // 把 svg 文件变成成雪碧图
},
{
test: /\.scss$/,
/** sass-loader 把 scss 文件变成 css 文件
css-loader 将 css 文件变成 js 字符串
style-loader 将 js 字符串变成 style 标签 **/
use: ['style-loader', 'css-loader', 'sass-loader']
}
]
},
5、配置生产环境时不打包的第三方依赖
// 目的主要是减小打包、压缩后文件的体积
mode: 'production',
externals: {
react: {
commonjs: 'react',
commonjs2: 'react',
amd: 'react',
root: 'React',
},
'react-dom': {
commonjs: 'react-dom',
commonjs2: 'react-dom',
amd: 'react-dom',
root: 'ReactDOM',
},
}
6、配置 HTMLWebpackPlugin 插件
// webpack.config.prod.js
const base = require('./webpack.config')
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = Object.assign({}, base, {
mode: 'production',
plugins: [
new HtmlWebpackPlugin({
template: 'example.html',
filename:'index.html' // 若不配置,默认生成的也是 index.html
})
],
// ...some other config
})
HTMLWebpack 插件的主要作用还是在指定的目录生成一个 html 文件,并自动添加一个链接到 js 文件的脚本,如下:
// 原 html 文件
<body>
<div id="root"></div>
</body>
// 生成后的 html 文件
<body>
<div id="root"></div>
<script type="text/javascript" src="example.js"></script>
</body>
二、Icon 组件
每个项目中都会用到很多 svg 图标,如果能把引入 svg 图表的方法封装成一个组件,那么开发效率会高很多,代码也会写起来更舒服。下面是实现代码:
// webpack 上述已配置
// 目录 icon.tsx。
import React from "react";
import "./importAllIcons"; // icons 目录文件一次性导入
interface IconProps extends React.SVGAttributes<SVGElement> {
name: string;
}
const Icon: React.FunctionComponent<IconProps> = ({
name,
}) => {
return (
<svg>
<use xlinkHref={`#${name}`}/>
</svg>
);
};
export default Icon;
// 目录 importAllIcon.tsx
// require.context() 会一次性导入所有 Icon 文件
// importAll 函数会把将所有 Icon 文件变成模块导出
let importAll = (requireContext) => requireContext.keys().forEach(requireContext)
try {
importAll(require.context('../icons/', true, /\.svg$/))
} catch (error) {
}
// 目录 test.tsx
import Icon from "**/icon.tsx"
<Icon name="svgName" /> // 只要将代码下载到 Icons 目录,给我文件名,即可直接使用。
了解更多:实现原理和细节
三、Form 组件
在做用户界面的时候,无论是登录界面、还是输入框等交互页面,form 表单都是必不可少的,而且它涉及到的知识点也很多,因此,在这里将它的一些常用功能在加以理解后封装成一个组件,现在对在这个过程中用到的一些知识点进行讲解。
1、使用 table 元素做对 form 表单的样式
<table>
<tbody>
<tr>
<td>Username</td><td><input /></td>
</tr>
<tr>
<td>Password</td><td><input /></td>
</tr>
</tbody>
</table>
2、区分 react 中的受控组件和非受控组件
(1)受控组件,表单中的内容受 react 控制(推荐)
const [name, setName] = useState('frank')
// 无 onChange 属性:input 中的内容不能改变
<input value={name} />
// 无 onChange 属性:input 中的内容可以改变
// 这种情况与 React 的函数式设计思想有关,
// 即 UI 中的参数即 state 中的 name 不改变时,不可以直接输入文字。
// 而 name 必须通过函数的方法 setName(xxx) 来改变,而不能在页面直接改变。
<input value={name} onChange={e=>setName(e.target.value) />
(2)非受控组件,表单中的内容不受 react 控制,可通过 useRef 获取当前值。
const refInput = useRef<HTMLInputElement>(null)
const x = () => {
console.log(refInput.current!.value)
}
// 输入完且鼠标离开后打印当前值
<input defaultValue="frank" ref={refInput} type="text" onBlur={x} />
3、配置校验器规则,对输入的内容进行检验
检验器是一个函数,并设置了检验规则,当 input 中的内容被提交时,它会对比提交的值是否符合要求,若不符合,则返回 error。
const onSubmit = () => {
// 检验规则
const rules = [
{ key: "username", required: true },
{ key: "username", validator },
{ key: "username", minLength: 6, maxLength: 16 },
{ key: "password", required: true },
{ key: "password", pattern: /^[A-Za-z0-9]+$/ },
];
// 检验函数
Validator(formData, rules, (errors) => {
setErrors(errors);
if (noError(errors)) {
console.log("输入正确,noError。");
}
});
};
另外,上述检验过程是被假设成同步的,当检验完后,所有的错误会以一个数组的形式返回。但是,当出现异步操作时,比如说向服务器发送请示,看用户名是否重复时,返回的错误数组集就会出现问题,因为返回同步 error 的时候,异步操作很大可能还未结束,那么,应该怎么解决这个问题?答案是等异步操作结束后再一起返回错误集!
// 假设同步返回的错误是一个字符串,而异步返回的是一个 Promise 对象。
// 如果可以把字符串都变 Promise 对象,并将状态都变成 fulfilled。然后,根据 Promise.all() 方法的原理并调用它,就可以解决这个问题。
// Promise.all() 方法会等所有 Promise 对象都执行完后(或者第一个失败)才会往下执行,且参数以数组的形式传递出去。 **/
const newPromises = flattenErrors.map(([key, promiseOrString]) => {
// 把字符串变成 Promise 实例
const promise =
promiseOrString instanceof Promise
? promiseOrString
: Promise.reject(promiseOrString)
// 参数加上 key,它是对 input 的标记,以确定抛出的是哪一个 input 的 error。
// 由于 then() 中的回调函数的返回值是一个数组,所以,newPromises 对象的状态是 fulfilled。
return promise.then<[string, undefined], [string, string]>(
() => [key, undefined],
(reason) => [key, reason]
)
})
// results 是一个数组,由所有 error 组成。
Promise.all(newPromises).then((results) => {
callback(results)
})
四、Dialog 组件
alert() 在平时会经常用到,它属于 Dialog 三种样式的一种,它还包括 confirm 和 modal 两种样式,下面,来说说做这个组件时用到的知识点。
1、利用 react 传送门将对话框放到最顶层
在该组件中,对话框是被设计放到最顶层的,但这时候,如果只是用定位和 z-index 的方式将对话框浮起来,可能就会存在层叠上下文的问题,导致样式错误。因此,点击时,直接将它放到最顶层是一个比较好的方法。
const Dialog: React.FC<Props> = (pros) => {
// ...some code
const result = visible && (
<Fragment>
// Dialog 组件
</Fragment>
);
return ReactDOM.createPortal(result, document.body); // React 传送门
};
export default Dialog
2、将 Dialog 组件封装成一个函数并导出
// 每次调用函数,会先创建一个 div,然后把 Dialog 组件放到该 div 上,并渲染到页面。
// 点击关闭时,设置组件的 visible 为 false 并重新渲染,然后把 div 删掉。
const modal = (
title: string,
content: string | ReactNode,
maskClosable?: Boolean,
buttons?: ReactElement[]
) => {
const close = () => {
// React.cloneElement(component, { visible: false }) :克隆该组件,并更改 visible 属性。
ReactDOM.render(React.cloneElement(component, { visible: false }), div)
ReactDOM.unmountComponentAtNode(div);
div.remove();
};
const component = (
<Dialog
title={"Declarative"}
visible={true}
onClose={() => close()}
buttons={buttons}
maskClosable={maskClosable ? maskClosable : false}
>
{content}
</Dialog>
);
const div = document.createElement("div");
document.body.append(div);
ReactDOM.render(component, div);
return close;
}
const alert = (...)=>{...}
const confirm = (...)=>{...}
export { alert, confirm, modal };
五、Scroll 组件
由于系统自带的滚动条都比较“丑”,所以,在做项目时,有时候需要隐藏原生滚动条,并制作自定义滚动条。而获取屏幕数据、对鼠标点击或者触屏等事件进行监听是制作该组件的重点和难点。
1、隐藏原生滚动条
// HTML
<div class="wrapper">
<div class="item">
<div>1</div>
<div>2</div>
......
<div>19</div>
<div>20</div>
</div>
</div>
// css
.wrapper{
border:1px solid red;
position:relative;
height:40vh;
width:300px;
overflow:hidden; // 溢出隐藏
}
.item{
border:3px solid blue;
position:absolute;
left:0;
top:0;
bottom:0;
right:-width(); // 计算后的原生滚动条宽度,滚动条通过在父组件设置 overflow:hidden 隐藏
height:40vh;
overflow:auto;
}
// JS
let width= function scrollbarWidth() {
const div = document.createElement('div');
div.style.position = 'absolute';
div.style.top = div.style.left = '-9999px'; // 把 div 放到屏幕之外,防止影响用户
div.style.width = div.style.height = '100px';
div.style.overflow = 'scroll';
document.body.appendChild(div);
const width = div.offsetWidth - div.clientWidth;
document.body.removeChild(div);
return width;
}
2、获取当前元素的 scrollHigth、topHigth,并计算自定义滚动条的 barHigth、barTop。
// 获取当前元素的 div 元素
const containerRef = useRef<HTMLDivElement>(null)
<div ref={containerRef} onScroll={onScroll}><div>
// 获取当前元素的滚动高度 scrollHeight | 滚动条顶部区域高度 scrollTop
const height = () => {
const scrollHeight = containerRef.current!.scrollHeight;
const scrollTop = containerRef.current!.scrollTop;
const viewHeight = containerRef.current!.getBoundingClientRect().height; // 视口高度
return { scrollHeight, scrollTop, viewHeight };
};
// 按比例计算自定义滚动条 barHeight、barTop 的动态高度。
const [barHeight, setBarHeight] = useState(0);
const [barTop, setBarTop] = useState(0);
useEffect(() => { // 组件渲染到屏幕后执行
setBarHeight((height().viewHeight * height().viewHeight) / height().scrollHeight)
});
const onScroll = () => { // 滚动时执行
setBarTop((height().scrollTop * height().viewHeight) / height().scrollHeight)
};
3、拉动自定义滚动条时,内容自动更新。
// 监听鼠标放到滚动条上、拉动滚动条、离开滚动条三个事件,并更新相关数据。
const onMouseDownBar = (e: React.MouseEvent) => {
...
};
const onMouseMoveBar = (e: MouseEvent) => {
...
};
const onMouseUpBar = (e: React.MouseEvent) => {
...
};
4、在移动端,下拉时,更新数据。
// 监听被触屏前、中、后三个事件,并更新数据。
const onTouchStart = (e: React.TouchEvent) => {
...
};
const onTouchMove = (e: React.TouchEvent) => {
...
};
const onTouchEnd = () => {
...
};
六、总结
该项目做了 6 个 ui 组件,用到的技术栈包括 React、TypeScript、React-dom、React-Router-dom 等。在做项目的过程中,通过配置部分 webpack,使得自己对 webpack 的基本运行、打包原理有了更进一步的认知和理解。另外,项目坚持使用 TypeScript,也减少了很多因为类型错误而导致的 bug,让代码结构变得更加严谨。当然,做各个组件的时候,也遇到了很多问题,比如一次性引入所有 svg 文件的方法、如何解决依赖包冲突问题、应该使用哪些事件类型、ts 类型声明出错、react 路由、form 组件数据更新检验等等,虽然最终都解决了,但这个过程所带给我的经验和知识却是宝贵的。话不多说,项目完成了,后期我也会继续完善和更新,一起期待吧!
项目源码链接:wgbcode/wu-ui-react-2 (github.com)
项目 gitee 网址:Webpack App (gitee.io)