目的
如果你使用 Vue 开发项目,那么你一定用过或听过大名鼎鼎的 Element-UI,在 Element-UI 众多好用的组件中,有一个组件叫 Loading 组件,这个组件使用起来特别的灵活,支持:
- 指令方式和服务方式(服务方式还带单例模式)
- 灵活的配置项,特别是 target 可以随意指定渲染节点。
可惜的是,强大无比的 Antd ,它的 Spin 组件竟然就只支持指令方式,而且配置选项还无法支持我们指定 DOM 进行渲染,尤其是在项目中使用非常的不方便。
所以就用 Antd UI 框架的 Spin 组件和 Icon 组件来实现 Element-UI 的 Loading 组件的服务方式 功能。
先感受下 Antd-spin 综合案例演示效果:
环境准备
- 一个空的 React 项目
这个之前写过见文章 React 起步实现 hello world,这里快速搞一下,新建一个目录 app-react-demo,先看下文件结构:
├── main.js
├── main.less
├── package-lock.json
├── package.json
├── public
│ └── index.html
└── webpack.config.js
然后在文件夹里面执行命令:
# 生成 package.json 依赖文件
$ npm init -y
# 安装项目依赖,wepack-cli 要指定版本3 ,less-loader 要指定版本5
$ npm i -D webpack webpack-cli@3 webpack-dev-server @babel/core @babel/preset-env babel-loader @babel/preset-react antd react react-dom style-loader css-loader less-loader@5 less webpackbar friendly-errors-webpack-plugin webpack-bundle-analyzer address
# webpackbar 打包进度条
新建 webpack.config.js 文件内容为:
const path = require("path");
const WebpackBar = require("webpackbar");
const FriendlyErrorsWebpackPlugin = require("friendly-errors-webpack-plugin");
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
const address = require("address");
// address.ip 的实现思路是使用 os.platform 辨别平台,在去读取 os.networkInterfaces 里面的 IP
const IP = address.ip();
const PORT = 6666;
module.exports = {
mode: "development",
entry: "./main.js",
output: {
// webpack要求的输出路径
// path: path.resolve(__dirname,"dist"),
// webpack-dev-server的虚拟输出路径
publicPath: "virtual",
filename: "all.js"
},
module: {
rules: [
{
// 以less结尾的文件
test: /\.less$/,
use: [
{
loader: "style-loader" // creates style nodes from JS strings
},
{
loader: "css-loader" // translates CSS into CommonJS
},
{
loader: "less-loader",
options: {
javascriptEnabled: true
} // compiles Less to CSS
}
]
},
{
test: /\.m?js$/,//匹配.mjs和.js结束的文件
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react']
}
}
}
]
},
plugins: [
new WebpackBar(),
new FriendlyErrorsWebpackPlugin({
compilationSuccessInfo: {
messages: [ `You can now view liuguoci in the browser.\n Local: http://localhost:${PORT}\n On Your Network: http://${IP}:${PORT}` ],
notes: [ `Note that the development build is not optimized \n To create a production build, use npm run build.` ]
},
onErrors: (severity, errors) => {
console.log(severity, errors);
},
// default is true
clearConsole: true,
}),
new BundleAnalyzerPlugin({
// module 依赖关系
generateStatsFile: false,
// 默认 8888
analyzerPort: 8888,
// 打包完成默认打开分析页面
openAnalyzer: false
})
],
resolve: {
//自动解析确定的扩展。默认值为:
extensions: [".js", ".json", ".jsx", ".css"],
//解析目录时要使用的文件名。默认:
mainFiles: ["index", "Index"]
},
devServer: {
/* webpack-dev-server 结合 friendly-errors-webpack-plugin 的设置 */
quiet: true,
contentBase: path.join(__dirname, "public"), // public目录开启服务器
hot: true, // 开启热更新
compress: true, // 是否使用gzip压缩
// port: PORT, // 端口号
// open : true // 自动打开网页
// https: true,
// proxy: {
// "/api": "http://localhost:9999"
// }
},
}
新建 main.js 文件内容为:
import React from "react";
import ReactDOM from "react-dom";
import "./main.less";
import 'antd/dist/antd.less'; // or 'antd/dist/antd.less';
ReactDOM.render(hello world!
, document.getElementById("app"));
public 文件夹新建 index.html 文件内容为:
写一个 Antd-spin 组件
package.json 修改 scripts 字段为:
"scripts": {
"dev": "webpack-dev-server"
},
项目终端执行命令 npm run dev
,浏览器打开 http://localhost:6666
链接即可看到项目启动完成。
- rollupjs 打包环境
本来打包工具我是想用 webpack 的,因为只对 webpack 比较熟,结果大意了,结果配了一天的环境,愣是没配好,越来越感觉前端在工具化这方便还有很长的路要走,一些周边工具官方不维护也不指定,导致各种方案层出不穷,容易选择困难症,而且各自迭代也是非常的快,往往就会出现兼容问题。webpack 也是其代表之一,顺便了解下:
webpack 为什么这么难用?
文章发布于 webpack4 发版前期,现在 webpack5 都出来了,发现没问题,核心问题还是没解决。不过不要怕我们的战神尤大神,已经在解决这个问题了,Vite 横空出世,等它稳定了,绝对吊打一切 JS 打包工具,而且绝对贼其简单,文档绝对简单易读。期待中...
在 app-react-demo 同级目录下,在新建一个项目 antd-spin 目录。进入目录执行命令:
# 生成 package.json 依赖文件
$ npm init -y
# 安装项目依赖
npm i -D @babel/cli @babel/core @babel/plugin-proposal-class-properties @babel/preset-env @babel/preset-react @rollup/plugin-babel antd babel-eslint eslint eslint-config-prettier eslint-plugin-prettier eslint-plugin-react husky lint-staged prettier react react-dom rollup rollup-plugin-postcss
#
配置文件过多,请到 github antd-spin 获取,顺便可以学学项目工程化配置的内容,这部分我参考了三个特别有用的资源。
- rollupjs 官网快速入门
- rollup 打包基于 Antd 的 React 组件库 与我经历何其相似,作者很良心代码都给出来了,还有注释 。
- 视频版【React插件开发】发布自制React组件到NPM全流程 (使用rollup打包工具)
设计思路:
1. 组件如何渲染上树?
使用 React 的核心 API,ReactDOM.render()
方法来实现。
2. 组件如何实现 target?
简单,target 没有赋,也就是值默认情况下,ReactDOM.render()
渲染到 body 元素下,就实现了全局 loading,当 target 有值值分三种情况:
- JS 原生 DOM
- React 的 createRef/uesRef 创建的 DOM
- 传入字符串时,通过 docuemnt.querySelector 来查找 DOM
有值的三种情况,都是取 DOM,有了DOM 把 ReactDOM.render()
之后的内容渲染到 DOM 中。就实现了局部 loading。
需要注意的是全局 loading 使用的 fixed 定位,局部 loading 使用的是 absolute 定位。因为局部 loading 使用 absolute 定位,会造成脱标,所以通过 getComputedStyle 和 getPropertyValue 拿到父元素的定位值,当 position 不是 inherit 或 static 添加 relative,来避免脱标的影响。
3. 组件如何实现多次创建只有一个 loading?
loading 的单例模式,这个很简单通过一个信号量变量 requestFlag 来控制,请求的时候为 true,请求结束为 false,分别加条件判断就行了,防抖思想的运用。
4. 支持 Antd 的 Icon 组件的所有属性
记住一句话:
Only Call Hooks from React Functions
React 的 hooks 只能在函数组件里面调用
我们封装的组件 antdSpin,其实就是一个普通函数(虽然它是一个类,本质还是函数),所以无法直接调用 hooks 的,只能使用 JSX 语法来定义 hook,而 Icon 组件很多属性都是都通过 React Functions 来控制的,这点不像 Element-UI,是通过类名来控制的。所以在实现的时候:
- hook 尽量写在 antdSpin 里面。
- 比较遗憾的是,Icon 组件本身就是通过函数组件来使用的,所以只能委屈的用动态
import("@ant-design/icons")
来实现,使用的时候传 Icon 名字的字符串就行了。(PS:这个卡我半天,最难受的地方)
注意,如果 loading/Spin 的图标是自定义的,我们就要用到自己图标了,那怎么办呢?当当当,当然是让 UI 小姐姐给我们图了,记住不要 PNG、不要 JPG就要 SVG 的。SVG 是支持代码编辑的,我们就利用 SVG 的这个特性,把 SVG 封装一个函数组件,然后配合 component 属性来使用。
不要急就快写完了,还差一个功能,使用 iconfont.cn 在线图标。
千万记住是 JS 文件,我第一次就弄错了点击的 font class 模块复制的 CSS 文件。
就这四个难点,没别的了,代码剩下的就是大量逻辑判断,用来处理些边边角角的东西的。组件的核心代码在这 antd-spin 核心代码,注释我写的非常清楚就不继续一行一行的解释了。组件的用法也不写了,在这 antd-spin README。
使用演示
给一个综合使用案例,案例演示动图在文章开头处,下面是源代码:
一点样式:
html, body, #root {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}
section {
width: 200px;
height: 200px;
margin: 10px;
border: 1px solid skyblue;
}
main {
width: 200px;
height: 200px;
margin: 10px;
border: 1px solid rgb(27, 218, 110);
}
.global-text {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: #b03fc3;
font-size: 50px;
line-height: 100px;
border-radius: 5px;
}
核心 JS 代码:
import './App.css';
// 引入 Spin 服务:
import antdSpin from "antd-spin";
import { useEffect, useRef, useState } from "react";
const delay = (instance, ms) => new Promise((resolve, reject) => setTimeout(() => {
instance && instance.close();
resolve();
}, ms * 1000));
const HeartSvg = (props) => (
);
function App() {
const ref = useRef();
const [ text, setText ] = useState();
useEffect(() => {
(async function () {
await delay(null, 1);
await delay(setText("Are u ready?"), 1);
await delay(setText("please count of three!"), 1);
await delay(setText(3), 1);
await delay(setText(2), 1);
await delay(setText(1), 1);
await delay(setText("ready go!"), 1);
await delay(setText(""), 0);
// options 参数支持的配置对象
let options = {
target: ref.current,
lock: false,
text: "传入 ReactDOM 演示",
background: "rgba(0, 0, 0, .1)",
customClass: "antd-spin"
};
// 在需要调用时,以服务的方式调用的 antdSpin 且是单例的
let antdSpinInstance = antdSpin.service(options);
// 关闭
await delay(antdSpinInstance, 3);
const mainId = document.getElementById("main-id");
options = {
target: mainId,
lock: false,
indicator: "PlusSquareTwoTone",
loadingConfig: {
spin: true
},
text: "双色图标和JS传入DOM演示...",
background: "rgba(0, 0, 0, .2)",
customClass: "antd-spin",
twoToneColor: "#73c41d"
};
antdSpinInstance = antdSpin.service(options);
await delay(antdSpinInstance, 3);
options = {
target: "aside",
lock: false,
text: "自动搜索 DOM 演示",
indicator: "LoadingOutlined",
background: "rgba(0, 0, 0, .3)",
customClass: "antd-spin"
};
antdSpinInstance = antdSpin.service(options);
await delay(antdSpinInstance, 3);
options = {
target: "sys-icon",
text: "图标动画配置演示",
indicator: "ReloadOutlined",
loadingConfig: {
spinner: "icon-class",
/* 图标旋转角度(IE9 无效) */
rotate: 180,
/* 是否有旋转动画 */
spin: true
}
};
antdSpinInstance = antdSpin.service(options);
await delay(antdSpinInstance, 3);
options = {
target: "sys-icon",
text: "在线 icon 演示",
IconFont: {
type: "icon-tuichu",
scriptUrl: "//at.alicdn.com/t/font_8d5l8fzk5b87iudi.js"
},
loadingConfig: {
rotate: 90,
spin: true,
style: { fontSize: 40, color: "red" }
}
};
antdSpinInstance = antdSpin.service(options);
await delay(antdSpinInstance, 3);
options = {
target: "sys-svg",
text: "自定义组件演示",
component: HeartSvg,
loadingConfig: {
rotate: 10,
spin: true,
style: { fontSize: 40, color: "red" }
}
};
antdSpinInstance = antdSpin.service(options);
await delay(antdSpinInstance, 3);
options = {
background: "rgba(0, 0, 0, .75)",
text: "加载中..."
};
antdSpinInstance = antdSpin.service(options);
await delay(antdSpinInstance, 3);
await delay(setText("game over!"), 2);
})();
}, []);
return (
<>
{text}
>
);
}
export default App;
Ajax/fetch
封装的一些思考
- 页面开始加载,并发十个请求,也就是需要同时请求十个接口 needRequestCount = 10
- loading 组件无单例模式时,创建了十个 loading 图,造成 loading 闪动问题。
- loading 组件有单例模式时,第一个请求创建了 loading 图,因为是并发十个请求,所以之后的九个请求不再创建 loading,第一个请求结束拿到数据 loading 就会立刻关闭,问题在于其他九个接口可能返回数据慢,但是 loading 已经结束,loading 图的作用没有完全发挥出来。
并发请求我们很明显要选择单例模式的,接下来的问题在于找到开始第一个请求开始和最后一个请求结束,用它们之间的时间,用作 loading 图的时间,核心实现代码如下。
// 页面开始加载,并发十个请求, needRequestCount = 10:
let needRequestCount = 0;
// 1. interceptors.request ++, 请求之前
if (needRequestCount === 0) {
startLoading();
};
needRequestCount++;
// 2. interceptors.response -- 返回数据之后
if( needRequestCount <= 0 ) return
needRequestCount--;
needRequestCount = Math.max(needRequestCount, 0);
if(needRequestCount === 0){
endLoading()
};
/*
fetch backward
1. 不能获取进度
2. 不能设置超时
*/
- 接口之间有依赖性,比如我要联动十个接口
并发请求用的是 减法逻辑,本来联动请求我想用加法逻辑发现行不通,封装完不好用。坐地铁回家的时候想来了,联动十个请求可不就是隐藏 loading 图的功能吗。前九个隐藏 loading 图,只有第十个请求是能创建 loading 图的。你还可以视接口返回时间长短,或减少用户等待时间等,把创建 loading 图这个动作,放在第五个接口。不过一般联动接口也就两三个,就放在最后一个接口上面创建 loading 就行了。也算完美解决了。
最后
不得不讲,那些封装库给我们用的人是在是太厉害了,我就写了这么个小东西把我给累的半死,现在版本都发到 v1.0.6 了,更新了六个版本才算稳定,编程经验不够,但是需要考虑的东西还要求多,还是有点顾此失彼的感觉,前路漫漫,还是猥琐发育,别浪。
周五了,刚入职的时候做的那个项目请客吃饭,晚上又能省一顿饭钱,哈哈哈就是这么没出息。
当前时间 Friday, November 27, 2020 14:27:12