美文网首页基础前端
写一个 Antd-spin 组件

写一个 Antd-spin 组件

作者: CondorHero | 来源:发表于2020-11-27 14:31 被阅读0次

    目的

    如果你使用 Vue 开发项目,那么你一定用过或听过大名鼎鼎的 Element-UI,在 Element-UI 众多好用的组件中,有一个组件叫 Loading 组件,这个组件使用起来特别的灵活,支持:

    • 指令方式和服务方式(服务方式还带单例模式)
    • 灵活的配置项,特别是 target 可以随意指定渲染节点。

    可惜的是,强大无比的 Antd ,它的 Spin 组件竟然就只支持指令方式,而且配置选项还无法支持我们指定 DOM 进行渲染,尤其是在项目中使用非常的不方便。

    所以就用 Antd UI 框架的 Spin 组件和 Icon 组件来实现 Element-UILoading 组件的服务方式 功能。

    先感受下 Antd-spin 综合案例演示效果:

    Antd-spin 案例演示 gif

    环境准备

    • 一个空的 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(<h1>hello world!</h1>, document.getElementById("app"));
    

    public 文件夹新建 index.html 文件内容为:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>写一个 Antd-spin 组件</title>
    </head>
    <body>
        <div id="app"></div>
        <script src="virtual/all.js"></script>
    </body>
    </html>
    

    package.json 修改 scripts 字段为:

    "scripts": {
        "dev": "webpack-dev-server"
    },
    

    项目终端执行命令 npm run dev,浏览器打开 http://localhost:6666 链接即可看到项目启动完成。

    本来打包工具我是想用 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 获取,顺便可以学学项目工程化配置的内容,这部分我参考了三个特别有用的资源。

    设计思路:

    1. 组件如何渲染上树?

    使用 React 的核心 API,ReactDOM.render() 方法来实现。

    2. 组件如何实现 target?

    简单,target 没有赋,也就是值默认情况下,ReactDOM.render() 渲染到 body 元素下,就实现了全局 loading,当 target 有值值分三种情况:

    1. JS 原生 DOM
    2. React 的 createRef/uesRef 创建的 DOM
    3. 传入字符串时,通过 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,是通过类名来控制的。所以在实现的时候:

    1. hook 尽量写在 antdSpin 里面。
    2. 比较遗憾的是,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) => (
        <svg { ...props} width="1em" height="1em" fill="currentColor" viewBox="0 0 1024 1024">
            <path d="M923 283.6c-13.4-31.1-32.6-58.9-56.9-82.8-24.3-23.8-52.5-42.4-84-55.5-32.5-13.5-66.9-20.3-102.4-20.3-49.3 0-97.4 13.5-139.2 39-10 6.1-19.5 12.8-28.5 20.1-9-7.3-18.5-14-28.5-20.1-41.8-25.5-89.9-39-139.2-39-35.5 0-69.9 6.8-102.4 20.3-31.4 13-59.7 31.7-84 55.5-24.4 23.9-43.5 51.7-56.9 82.8-13.9 32.3-21 66.6-21 101.9 0 33.3 6.8 68 20.3 103.3 11.3 29.5 27.5 60.1 48.2 91 32.8 48.9 77.9 99.9 133.9 151.6 92.8 85.7 184.7 144.9 188.6 147.3l23.7 15.2c10.5 6.7 24 6.7 34.5 0l23.7-15.2c3.9-2.5 95.7-61.6 188.6-147.3 56-51.7 101.1-102.7 133.9-151.6 20.7-30.9 37-61.5 48.2-91 13.5-35.3 20.3-70 20.3-103.3 0.1-35.3-7-69.6-20.9-101.9z" />
        </svg>
    );
    
    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 (
            <>
                <span className="global-text">{text}</span>
                <section ref={ref}></section>
                <main id="main-id"></main>
                <main id="aside"></main>
                <main id="sys-icon"></main>
                <main id="sys-svg"></main>
            </>
        );
    }
    
    export default App;
    
    

    Ajax/fetch 封装的一些思考🤔

    1. 页面开始加载,并发十个请求,也就是需要同时请求十个接口 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. 不能设置超时
    */
    
    1. 接口之间有依赖性,比如我要联动十个接口

    并发请求用的是 减法逻辑,本来联动请求我想用加法逻辑发现行不通,封装完不好用👎。坐地铁回家的时候想来了,联动十个请求可不就是隐藏 loading 图的功能吗。前九个隐藏 loading 图,只有第十个请求是能创建 loading 图的。你还可以视接口返回时间长短,或减少用户等待时间等,把创建 loading 图这个动作,放在第五个接口。不过一般联动接口也就两三个,就放在最后一个接口上面创建 loading 就行了。也算完美解决了😂。

    最后

    不得不讲,那些封装库给我们用的人是在是太厉害了,我就写了这么个小东西把我给累的半死,现在版本都发到 v1.0.6 了,更新了六个版本才算稳定,编程经验不够,但是需要考虑的东西还要求多,还是有点顾此失彼的感觉,前路漫漫,还是猥琐发育,别浪。

    周五了,刚入职的时候做的那个项目请客吃饭,晚上又能省一顿饭钱💰,哈哈哈就是这么没出息。

    当前时间 Friday, November 27, 2020 14:27:12

    相关文章

      网友评论

        本文标题:写一个 Antd-spin 组件

        本文链接:https://www.haomeiwen.com/subject/viwdiktx.html