美文网首页前端
前端常用功能集合

前端常用功能集合

作者: MiSiTeWang | 来源:发表于2021-05-06 10:36 被阅读0次
    1. 以下功能主要是以移动端为主
    2. 使用到的ES6在移动端中没有不兼容情况,这里基本应用在微信端,手机浏览器的话也不用担心
    3. 所有功能均由原生JavaScript实现,没有任何依赖,做法是用最少的代码,造最高效的事情,在做一些H5单页(活动页)的时候,像这种最求极致加载速度,且不喜欢用第三方库的人,所以决定自己动手做一些无依赖精简高效的东西,然后按需应用在实际项目中。

    转载于此,膜拜大佬
    作者:黄景圣

    这里推荐前端使用vs code这个代码编辑器,理由是在声明的时候写好标准的JSDoc注释,在调用时会有很全面的代码提示,让弱类型的javascript也有类型提示

    1. http请求

    前端必备技能,也是使用最多的功能。个人不喜欢用axios这个东西的或懒得去看文档,而且觉得很鸡肋的,这是一个很好的web项目用的轮子。

    第一种:fetch

    /**
     * 基于`fetch`请求 [MDN文档](https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API)
     * @param {"GET"|"POST"|"PUT"|"DELETE"} method 请求方法
     * @param {string} url 请求路径
     * @param {object} data 请求参数对象
     * @param {number} timeout 超时毫秒
     */
    function fetchRequest(method, url, data = {}, timeout = 5000) {
        let body = null;
        let query = "";
        if (method === "GET") {
            // 解析对象传参
            for (const key in data) {
                query += `&${key}=${data[key]}`;
            }
            if (query) {
                query = "?" + query.slice(1);
            }
        } else {
            // 若后台没设置接收 JSON 则不行 需要跟 GET 一样的解析对象传参
            body = JSON.stringify(data);
        }
        return new Promise((resolve, reject) => {
            fetch(url + query, {
                // credentials: "include",  // 携带cookie配合后台用
                // mode: "cors",            // 貌似也是配合后台设置用的跨域模式
                method: method,
                headers: {
                    // "Content-Type": "application/json"
                    "Content-Type": "application/x-www-form-urlencoded" 
                },
                body: body
            }).then(response => {
                // 把响应的信息转为`json`
                return response.json();
            }).then(res => {
                resolve(res);
            }).catch(error => {
                reject(error);
            });
            setTimeout(reject.bind(this, "fetch is timeout"), timeout);
        });
    }
    

    特别说明一下:H5单页的一些简单GET请求时通常用得最多,因为代码极少,就像下面这样

    fetch("http://xxx.com/api/get").then(response => response.text()).then(res => {
        console.log("请求成功", res);
    })
    

    第二种:XMLHttpRequest,需要Promise用法在外面包多一层function做二次封装即可

    /**
     * `XMLHttpRequest`请求 [MDN文档](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest)
     * @param {object} params 传参对象
     * @param {string} params.url 请求路径
     * @param {"GET"|"POST"|"PUT"|"DELETE"} params.method 请求方法
     * @param {object} params.data 传参对象(json)
     * @param {FormData|string} params.formData `form`表单式传参:上传图片就是使用这种传参方式;使用`formData`时将覆盖`data`
     * @param {{ [key: string]: string }} params.headers `XMLHttpRequest.header`设置对象
     * @param {number?} params.overtime 超时检测毫秒数
     * @param {(result?: any, response: XMLHttpRequest) => void} params.success 成功回调 
     * @param {(error?: XMLHttpRequest) => void} params.fail 失败回调 
     * @param {(info?: XMLHttpRequest) => void} params.timeout 超时回调
     * @param {(res?: ProgressEvent<XMLHttpRequestEventTarget>) => void} params.progress 进度回调(暂时没用到)
     */
    function ajax(params) {
        if (typeof params !== "object") return console.error("ajax 缺少请求传参");
        if (!params.method) return console.error("ajax 缺少请求类型 GET 或者 POST");
        if (!params.url) return console.error("ajax 缺少请求 url");
        if (typeof params.data !== "object") return console.error("请求参数类型必须为 object");
    
        const XHR = new XMLHttpRequest();
        /** 请求方法 */
        const method = params.method;
        /** 超时检测 */
        const overtime = typeof params.overtime === "number" ? params.overtime : 0;
        /** 请求链接 */
        let url = params.url;
        /** 非`GET`请求传参 */
        let body = null;
        /** `GET`请求传参 */
        let query = "";
    
        // 传参处理
        if (method === "GET") {
            // 解析对象传参
            for (const key in params.data) {
                query += "&" + key + "=" + params.data[key];
            }
            if (query) {
                query = "?" + query.slice(1);
                url += query;
            }
        } else {
            body = JSON.stringify(params.data); // 若后台没设置接收 JSON 则不行,需要使用`params.formData`方式传参
        }
    
        // 监听请求变化;XHR.status learn: http://tool.oschina.net/commons?type=5
        XHR.onreadystatechange = function () {
            if (XHR.readyState !== 4) return;
            if (XHR.status === 200 || XHR.status === 304) {
                typeof params.success === "function" && params.success(JSON.parse(XHR.response), XHR);
            } else {
                typeof params.fail === "function" && params.fail(XHR);
            }
        }
    
        // 判断请求进度
        if (params.progress) {
            XHR.addEventListener("progress", params.progress);
        }
    
        // XHR.responseType = "json"; // 设置响应结果为`json`这个一般由后台返回指定格式,前端无配置
        // XHR.withCredentials = true;  // 是否Access-Control应使用cookie或授权标头等凭据进行跨站点请求。
        XHR.open(method, url, true);
    
        // 判断传参类型,`json`或者`form`表单
        if (params.formData) {
            body = params.formData;
            XHR.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); // 默认就是这个,设置不设置都可以
        } else {
            XHR.setRequestHeader("Content-Type", "application/json");
        }
    
        // 判断设置配置头信息
        if (params.headers) {
            for (const key in params.headers) {
                const value = params.headers[key];
                XHR.setRequestHeader(key, value);
            }
        }
    
        // 在IE中,超时属性只能在调用 open() 方法之后且在调用 send() 方法之前设置。
        if (overtime > 0) {
            XHR.timeout = overtime;
            XHR.ontimeout = function () {
                console.warn("XMLHttpRequest 请求超时 !!!");
                XHR.abort();
                typeof params.timeout === "function" && params.timeout(XHR);
            }
        }
    
        XHR.send(body);
    }
    

    源码地址
    实际项目使用展示

    2. swiper轮播图组件

    拖拽回弹物理效果是参照开源项目Swiper.js做的,效果功能保持一致

    /**
     * 轮播组件
     * @param {object} params 配置传参
     * @param {string} params.el 组件节点 class|id|<label>
     * @param {number} params.moveTime 过渡时间(毫秒)默认 300
     * @param {number} params.interval 自动播放间隔(毫秒)默认 3000
     * @param {boolean} params.loop 是否需要回路
     * @param {boolean} params.vertical 是否垂直滚动
     * @param {boolean} params.autoPaly 是否需要自动播放
     * @param {boolean} params.pagination 是否需要底部圆点
     * @param {(index: number) => void} params.slideCallback 滑动/切换结束回调
     * @author https://github.com/Hansen-hjs
     * @description 
     * 移动端`swiper`组件,如果需要兼容`pc`自行修改对应的`touch`到`mouse`事件即可。现成效果预览:https://huangjingsheng.gitee.io/hjs/cv/demo/face/
     */
    function swiper(params) {
        /**
         * css class 命名列表
         * @dec ["滑动列表","滑动item","圆点容器","底部圆点","圆点高亮"]
         */
        const classNames = [".swiper_list", ".swiper_item", ".swiper_pagination", ".swiper_dot", ".swiper_dot_active"];
        /** 滑动结束函数 */
        const slideEnd = params.slideCallback || function() {};
        /**
         * 组件节点
         * @type {HTMLElement}
         */
        let node = null;
        /**
         * item列表容器
         * @type {HTMLElement}
         */
        let nodeItem = null;
        /**
         * item节点列表
         * @type {Array<HTMLElement>}
         */
        let nodeItems = [];
        /**
         * 圆点容器
         * @type {HTMLElement}
         */
        let nodePagination = null;
        /**
         * 圆点节点列表
         * @type {Array<HTMLElement>}
         */
        let nodePaginationItems = [];
        /** 是否需要底部圆点 */
        let pagination = false;
        /** 是否需要回路 */
        let isLoop = false;
        /** 方向 `X => true` | `Y => false` */
        let direction = false;
        /** 是否需要自动播放 */
        let autoPaly = false;
        /** 自动播放间隔(毫秒)默认 3000 */
        let interval = 3000;
        /** 过渡时间(毫秒)默认 300 */
        let moveTime = 300;
    
        /** 设置动画 */
        function startAnimation() {
            nodeItem.style.transition = `${moveTime / 1000}s all`; 
        }
    
        /** 关闭动画 */
        function stopAnimation() {
            nodeItem.style.transition = "0s all";
        }
    
        /**
         * 属性样式滑动
         * @param {number} n 移动的距离
         */
        function slideStyle(n) {
            let x = 0, y = 0;
            if (direction) {
                y = n;
            } else {
                x = n;
            }
            nodeItem.style.transform = `translate3d(${x}px, ${y}px, 0px)`;
        }
    
        /**
         * 事件开始
         * @param {number} width 滚动容器的宽度
         * @param {number} height 滚动容器的高度
         */
        function main(width, height) {
            /**
             * 动画帧
             * @type {requestAnimationFrame}
             */
            const animation = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
            /** 触摸开始时间 */
            let startTime = 0;
            /** 触摸结束时间 */
            let endTime = 0;
            /** 开始的距离 */
            let startDistance = 0;
            /** 结束的距离 */
            let endDistance = 0;
            /** 结束距离状态 */
            let endState = 0;
            /** 移动的距离 */
            let moveDistance = 0;
            /** 圆点位置 && 当前 item 索引 */
            let index = 0;
            /** 动画帧计数 */
            let count = 0;
            /** loop 帧计数 */
            let loopCount = 0;
            /** 移动范围 */
            let range = direction ? height : width;
    
            /** 获取拖动距离 */
            function getDragDistance() {
                /** 拖动距离 */
                let dragDistance = 0;
                // 默认这个公式
                dragDistance = moveDistance + (endDistance - startDistance);
                // 判断最大正负值
                if ((endDistance - startDistance) >= range) {
                    dragDistance = moveDistance + range;
                } else if ((endDistance - startDistance) <= -range) {
                    dragDistance = moveDistance - range;
                }
                // 没有 loop 的时候惯性拖拽
                if (!isLoop) {
                    if ((endDistance - startDistance) > 0 && index === 0) {
                        // console.log("到达最初");
                        dragDistance = moveDistance + ((endDistance - startDistance) - ((endDistance - startDistance) * 0.6));
                    } else if ((endDistance - startDistance) < 0 && index === nodeItems.length - 1) {
                        // console.log("到达最后");
                        dragDistance = moveDistance + ((endDistance - startDistance) - ((endDistance - startDistance) * 0.6));
                    }
                }
                return dragDistance;
            }
    
            /**
             * 判断触摸处理函数 
             * @param {number} slideDistance 滑动的距离
             */
            function judgeTouch(slideDistance) {
                //  这里我设置了200毫秒的有效拖拽间隔
                if ((endTime - startTime) < 200) return true;
                // 这里判断方向(正值和负值)
                if (slideDistance < 0) {
                    if ((endDistance - startDistance) < (slideDistance / 2)) return true;
                    return false;
                } else {
                    if ((endDistance - startDistance) > (slideDistance / 2)) return true;
                    return false;
                }
            }
    
            /** 返回原来位置 */
            function backLocation() {
                startAnimation();
                slideStyle(moveDistance);
            }
    
            /**
             * 滑动
             * @param {number} slideDistance 滑动的距离
             */
            function slideMove(slideDistance) {
                startAnimation();
                slideStyle(slideDistance);
                loopCount = 0;
                // 判断 loop 时回到第一张或最后一张
                if (isLoop && index < 0) {
                    // 我这里是想让滑块过渡完之后再重置位置所以加的延迟 (之前用setTimeout,快速滑动有问题,然后换成 requestAnimationFrame解决了这类问题)
                    function loopMoveMin() {
                        loopCount += 1;
                        if (loopCount < moveTime / 1000 * 60) return animation(loopMoveMin);
                        stopAnimation();
                        slideStyle(range * -(nodeItems.length - 3));
                        // 重置一下位置
                        moveDistance = range * -(nodeItems.length - 3);
                    }
                    loopMoveMin();
                    index = nodeItems.length - 3;
                } else if (isLoop && index > nodeItems.length - 3) {
                    function loopMoveMax() {
                        loopCount += 1;
                        if (loopCount < moveTime / 1000 * 60) return animation(loopMoveMax);
                        stopAnimation();
                        slideStyle(0);
                        moveDistance = 0;
                    }
                    loopMoveMax();
                    index = 0;
                }
                // console.log(`第${ index+1 }张`);   // 这里可以做滑动结束回调
                if (pagination) {
                    nodePagination.querySelector(classNames[4]).className = classNames[3].slice(1);
                    nodePaginationItems[index].classList.add(classNames[4].slice(1));
                }
            }
    
            /** 判断移动 */
            function judgeMove() {
                // 判断是否需要执行过渡
                if (endDistance < startDistance) {
                    // 往上滑动 or 向左滑动
                    if (judgeTouch(-range)) {
                        // 判断有loop的时候不需要执行下面的事件
                        if (!isLoop && moveDistance === (-(nodeItems.length - 1) * range)) return backLocation();
                        index += 1;
                        slideMove(moveDistance - range);
                        moveDistance -= range;
                        slideEnd(index);
                    } else {
                        backLocation();
                    }
                } else {
                    // 往下滑动 or 向右滑动
                    if (judgeTouch(range)) {
                        if (!isLoop && moveDistance === 0) return backLocation();
                        index -= 1;
                        slideMove(moveDistance + range);
                        moveDistance += range;
                        slideEnd(index)
                    } else {
                        backLocation();
                    }
                }
            }
    
            /** 自动播放移动 */
            function autoMove() {
                // 这里判断 loop 的自动播放
                if (isLoop) {
                    index += 1;
                    slideMove(moveDistance - range);
                    moveDistance -= range;
                } else {
                    if (index >= nodeItems.length - 1) {
                        index = 0;
                        slideMove(0);
                        moveDistance = 0;
                    } else {
                        index += 1;
                        slideMove(moveDistance - range);
                        moveDistance -= range;
                    }
                }
                slideEnd(index);
            }
    
            /** 开始自动播放 */
            function startAuto() {
                count += 1;
                if (count < interval / 1000 * 60) return animation(startAuto);
                count = 0;
                autoMove();
                startAuto();
            }
    
            // 判断是否需要开启自动播放
            if (autoPaly && nodeItems.length > 1) startAuto();
    
            // 开始触摸
            nodeItem.addEventListener("touchstart", ev => {
                startTime = Date.now();
                count = 0;
                loopCount = moveTime / 1000 * 60;
                stopAnimation();
                startDistance = direction ? ev.touches[0].clientY : ev.touches[0].clientX;
            });
    
            // 触摸移动
            nodeItem.addEventListener("touchmove", ev => {
                ev.preventDefault();
                count = 0;
                endDistance = direction ? ev.touches[0].clientY : ev.touches[0].clientX;
                slideStyle(getDragDistance());
            });
    
            // 触摸离开
            nodeItem.addEventListener("touchend", () => {
                endTime = Date.now();
                // 判断是否点击
                if (endState !== endDistance) {
                    judgeMove();
                } else {
                    backLocation();
                }
                // 更新位置 
                endState = endDistance;
                // 重新打开自动播
                count = 0;
            });
        }
    
        /**
         * 输出回路:如果要回路的话前后增加元素
         * @param {number} width 滚动容器的宽度
         * @param {number} height 滚动容器的高度
         */
        function outputLoop(width, height) {
            const first = nodeItems[0].cloneNode(true), last = nodeItems[nodeItems.length - 1].cloneNode(true);
            nodeItem.insertBefore(last, nodeItems[0]);
            nodeItem.appendChild(first);
            nodeItems.unshift(last);
            nodeItems.push(first);
            if (direction) {
                nodeItem.style.top = `${-height}px`;
            } else {
                nodeItem.style.left = `${-width}px`;
            }
        }
    
        /**
         * 输出动态布局
         * @param {number} width 滚动容器的宽度
         * @param {number} height 滚动容器的高度
         */
        function outputLayout(width, height) {
            if (direction) {
                for (let i = 0; i < nodeItems.length; i++) {
                    nodeItems[i].style.height = `${height}px`;
                }
            } else {
                nodeItem.style.width = `${width * nodeItems.length}px`;
                for (let i = 0; i < nodeItems.length; i++) {
                    nodeItems[i].style.width = `${width}px`;
                }
            }
        }
    
        /** 输出底部圆点 */
        function outputPagination() {
            let paginations = "";
            nodePagination = node.querySelector(classNames[2]);
            // 如果没有找到对应节点则创建一个
            if (!nodePagination) {
                nodePagination = document.createElement("div");
                nodePagination.className = classNames[2].slice(1);
                node.appendChild(nodePagination);
            }
            for (let i = 0; i < nodeItems.length; i++) {
                paginations += `<div class="${classNames[3].slice(1)}"></div>`;
            }
            nodePagination.innerHTML = paginations;
            nodePaginationItems = [...nodePagination.querySelectorAll(classNames[3])];
            nodePagination.querySelector(classNames[3]).classList.add(classNames[4].slice(1));
        }
    
        /** 初始化动态布局 */
        function initLayout() {
            node = document.querySelector(params.el);
            if (!node) return console.warn("没有可执行的节点!");
            nodeItem = node.querySelector(classNames[0]);
            if (!nodeItem) return console.warn(`缺少"${classNames[0]}"节点!`);
            nodeItems = [...node.querySelectorAll(classNames[1])];
            if (nodeItems.length == 0) return console.warn("滑动节点个数必须大于0!");
            const moveWidth = node.offsetWidth, moveHeight = node.offsetHeight;
            if (pagination) outputPagination();
            if (isLoop) outputLoop(moveWidth, moveHeight);
            outputLayout(moveWidth, moveHeight);
            main(moveWidth, moveHeight);
        }
    
        /** 初始化参数 */
        function initParams() {
            if (typeof params !== "object") return console.warn("传参有误");
            pagination = params.pagination || false;
            direction = params.vertical || false;
            autoPaly = params.autoPaly || false;
            isLoop = params.loop || false;
            moveTime = params.moveTime || 300;
            interval = params.interval || 3000;
            initLayout();
        }
        initParams();
    }
    

    源码地址及使用展示

    3. 图片懒加载

    非传统实现方式,性能最优

    /**
     * 懒加载
     * @description 可加载`<img>`、`<video>`、`<audio>`等一些引用资源路径的标签
     * @param {object} params 传参对象
     * @param {string?} params.lazyAttr 自定义加载的属性(可选)
     * @param {"src"|"background"} params.loadType 加载的类型(默认为`src`)
     * @param {string?} params.errorPath 加载失败时显示的资源路径,仅在`loadType`设置为`src`中可用(可选)
     */
    function lazyLoad(params) {
        const attr = params.lazyAttr || "lazy";
        const type = params.loadType || "src";
    
        /** 更新整个文档的懒加载节点 */
        function update() {
            const els = document.querySelectorAll(`[${attr}]`);
            for (let i = 0; i < els.length; i++) {
                const el = els[i];
                observer.observe(el);
            }
        }
    
        /**
         * 加载图片
         * @param {HTMLImageElement} el 图片节点
         */
        function loadImage(el) {
            const cache = el.src; // 缓存当前`src`加载失败时候用
            el.src = el.getAttribute(attr);
            el.onerror = function () {
                el.src = params.errorPath || cache;
            }
        }
    
        /**
         * 加载单个节点
         * @param {HTMLElement} el 
         */
        function loadElement(el) {
            switch (type) {
                case "src":
                    loadImage(el);
                    break;
                case "background":
                    el.style.backgroundImage = `url(${el.getAttribute(attr)})`;
                    break;
            }
            el.removeAttribute(attr);
            observer.unobserve(el);
        }
    
        /** 
         * 监听器 
         * [MDN说明](https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver)
        */
        const observer = new IntersectionObserver(function(entries) {
            for (let i = 0; i < entries.length; i++) {
                const item = entries[i];
                if (item.isIntersecting) {
                    loadElement(item.target);
                }
            }
        })
    
        update();
    
        return {
            observer,
            update
        }
    }
    

    vue中使用指令去使用

    import Vue from "vue";
    
    /** 添加一个加载`src`的指令 */
    const lazySrc = lazyLoad({
        lazyAttr: "vlazy",
        errorPath: "./img/error.jpg"
    })
    
    Vue.directive("v-lazy", {
        inserted(el, binding) {
            el.setAttribute("vlazy", binding.value); // 跟上面的对应
            lazySrc.observer.observe(el);
        }
    })
    
    /** 添加一个加载`background`的指令 */
    const lazyBg = lazyLoad({
        lazyAttr: "vlazybg",
        loadType: "background"
    })
    
    Vue.directive("v-lazybg", {
        inserted(el, binding) {
            el.setAttribute("vlazybg", binding.value); // 跟上面的对应
            lazyBg.observer.observe(el);
        }
    })
    

    源码地址及使用展示

    4. 上传图片
    <!-- 先准备好一个input标签,然后设置type="file",最后挂载一个onchange事件 -->
    <input class="upload-input" type="file" name="picture" onchange="upLoadImage(this)">
    
    /**
     * input上传图片
     * @param {HTMLInputElement} el 
     */
    function upLoadImage(el) {
        /** 上传文件 */
        const file = el.files[0];
        /** 上传类型数组 */
        const types = ["image/jpg", "image/png", "image/jpeg", "image/gif"];
        // 判断文件类型
        if (types.indexOf(file.type) < 0) {
            file.value = null; // 这里一定要清空当前错误的内容
            return alert("文件格式只支持:jpg 和 png");
        }
        // 判断大小
        if (file.size > 2 * 1024 * 1024) {
            file.value = null;
            return alert("上传的文件不能大于2M");
        }
        
        const formData = new FormData();    // 这个是传给后台的数据
        formData.append("img", file);       // 这里`img`是跟后台约定好的`key`字段
        console.log(formData, file);
        // 最后POST给后台,这里我用上面的方法
        ajax({
            url: "http://xxx.com/uploadImg",
            method: "POST",
            data: {},
            formData: formData,
            overtime: 5000,
            success(res) {
                console.log("上传成功", res);
            },
            fail(err) {
                console.log("上传失败", err);
            },
            timeout() {
                console.warn("XMLHttpRequest 请求超时 !!!");
            }
        });
    }
    

    base64转换和静态预览
    配合接口上传到后台 这个可能要安装环境,因为是serve项目

    5. 下拉刷新组件

    拖拽效果参考上面swiper的实现方式,下拉中的效果是可以自己定义的

    // 这里我做的不是用 window 的滚动事件,而是用最外层的绑定触摸下拉事件去实现
    // 好处是我用在Vue这类单页应用的时候,组件销毁时不用去解绑 window 的 scroll 事件
    // 但是滑动到底部事件就必须要用 window 的 scroll 事件,这点需要注意
    
    /**
     * 下拉刷新组件
     * @param {object} option 配置
     * @param {HTMLElement} option.el 下拉元素(必选)
     * @param {number} option.distance 下拉距离[px](可选)
     * @param {number} option.deviation 顶部往下偏移量[px](可选)
     * @param {string} option.loadIcon 下拉中的 icon html(可选)
     */
    function dropDownRefresh(option) {
        const doc = document;
        /** 整体节点 */
        const page = option.el;
        /** 下拉距离 */
        const distance = option.distance || 88;
        /** 顶部往下偏移量 */
        const deviation = option.deviation || 0;
        /** 顶层节点 */
        const topNode = doc.createElement("div");
        /** 下拉时遮罩 */
        const maskNode = doc.createElement("div");
    
        topNode.innerHTML = `<div refresh-icon style="transition: .2s all;"><svg style="transform: rotate(90deg); display: block;" t="1570593064555" viewBox="0 0 1575 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="26089" width="48" height="48"><path d="M1013.76 0v339.968H484.115692V679.778462h529.644308v339.968l529.644308-485.612308v-48.600616L1013.76 0zM243.396923 679.857231h144.462769V339.968H243.396923V679.778462z m-240.797538 0h144.462769V339.968H2.599385V679.778462z" fill="#000000" fill-opacity=".203" p-id="26090"></path></svg></div><div refresh-loading style="display: none; animation: refresh-loading 1s linear infinite;">${option.loadIcon || '<p style="font-size: 15px; color: #666;">loading...</p>'}</div>`;
        topNode.style.cssText = `width: 100%; height: ${distance}px; position: fixed; top: ${-distance + deviation}px; left: 0; z-index: 10; display: flex; flex-wrap: wrap; align-items: center; justify-content: center; box-sizing: border-box; margin: 0; padding: 0;`;
        maskNode.style.cssText = "position: fixed; top: 0; left: 0; width: 100%; height: 100vh; box-sizing: border-box; margin: 0; padding: 0; background-color: rgba(0,0,0,0); z-index: 999;";
        page.parentNode.insertBefore(topNode, page);
    
        /**
         * 设置动画时间
         * @param {number} n 秒数 
         */
        function setAnimation(n) {
            page.style.transition = topNode.style.transition = n + "s all";
        }
    
        /**
         * 设置滑动距离
         * @param {number} n 滑动的距离(像素)
         */
        function setSlide(n) {
            page.style.transform = topNode.style.transform = `translate3d(0px, ${n}px, 0px)`;
        }
        
        /** 下拉提示 icon */
        const icon = topNode.querySelector("[refresh-icon]");
        /** 下拉 loading 动画 */
        const loading = topNode.querySelector("[refresh-loading]");
    
        return {
            /**
             * 监听开始刷新
             * @param {Function} callback 下拉结束回调
             * @param {(n: number) => void} rangeCallback 下拉状态回调
             */
            onRefresh(callback, rangeCallback = null) {
                /** 顶部距离 */
                let scrollTop = 0;
                /** 开始距离 */
                let startDistance = 0;
                /** 结束距离 */
                let endDistance = 0;
                /** 最后移动的距离 */
                let range = 0;
    
                // 触摸开始
                page.addEventListener("touchstart", function (e) {
                    startDistance = e.touches[0].pageY;
                    scrollTop = 1;
                    setAnimation(0);
                });
    
                // 触摸移动
                page.addEventListener("touchmove", function (e) {
                    scrollTop = doc.documentElement.scrollTop === 0 ? doc.body.scrollTop : doc.documentElement.scrollTop;
                    // 没到达顶部就停止
                    if (scrollTop != 0) return;
                    endDistance = e.touches[0].pageY;
                    range = Math.floor(endDistance - startDistance);
                    // 判断如果是下滑才执行
                    if (range > 0) {
                        // 阻止浏览自带的下拉效果
                        e.preventDefault();
                        // 物理回弹公式计算距离
                        range = range - (range * 0.5);
                        // 下拉时icon旋转
                        if (range > distance) {
                            icon.style.transform = "rotate(180deg)";
                        } else {
                            icon.style.transform = "rotate(0deg)";
                        }
                        setSlide(range);
                        // 回调距离函数 如果有需要
                        if (typeof rangeCallback === "function") rangeCallback(range);
                    }
                });
    
                // 触摸结束
                page.addEventListener("touchend", function () {
                    setAnimation(0.3);
                    // console.log(`移动的距离:${range}, 最大距离:${distance}`);
                    if (range > distance && range > 1 && scrollTop === 0) {
                        setSlide(distance);
                        doc.body.appendChild(maskNode);
                        // 阻止往上滑动
                        maskNode.ontouchmove = e => e.preventDefault();
                        // 回调成功下拉到最大距离并松开函数
                        if (typeof callback === "function") callback();
                        icon.style.display = "none";
                        loading.style.display = "block";
                    } else {
                        setSlide(0);
                    }
                });
    
            },
            /** 结束下拉 */
            end() {
                maskNode.parentNode.removeChild(maskNode);
                setAnimation(0.3);
                setSlide(0);
                icon.style.display = "block";
                loading.style.display = "none";
            }
        }
    }
    

    源码地址及使用展示

    6. 监听滚动到底部

    就几行代码的一个方法,另外监听元素滚动到底部可以参考代码笔记

    /**
     * 监听滚动到底部
     * @param {object} options 传参对象
     * @param {number} options.distance 距离底部多少像素触发(px)
     * @param {boolean} options.once 是否为一次性(防止重复用)
     * @param {() => void} options.callback 到达底部回调函数
     */
    function onScrollToBottom(options) {
        const { distance = 0, once = false, callback = null } = options;
        const doc = document;
        /** 滚动事件 */
        function onScroll() {
            /** 滚动的高度 */
            let scrollTop = doc.documentElement.scrollTop === 0 ? doc.body.scrollTop : doc.documentElement.scrollTop;
            /** 滚动条高度 */
            let scrollHeight = doc.documentElement.scrollTop === 0 ? doc.body.scrollHeight : doc.documentElement.scrollHeight;
            if (scrollHeight - scrollTop - distance <= window.innerHeight) {
                if (typeof callback === "function") callback();
                if (once) window.removeEventListener("scroll", onScroll);
            }
        }
        window.addEventListener("scroll", onScroll);
        // 必要时先执行一次
        // onScroll(); 
    }
    

    源码地址及使用展示

    7. 音频播放组件
    /**
     * `AudioContext`音频组件 
     * [资料参考](https://www.cnblogs.com/Wayou/p/html5_audio_api_visualizer.html)
     * @description 解决在移动端网页上标签播放音频延迟的方案 貌似`H5`游戏引擎也是使用这个实现
     */
    function audioComponent() {
        /**
         * 音频上下文
         * @type {AudioContext}
         */
        const context = new (window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.msAudioContext)();
        /** 
         * @type {AnalyserNode} 
         */
        const analyser = context.createAnalyser();;
        /**
         * @type {AudioBufferSourceNode}
         */
        let bufferNode = null;
        /**
         * @type {AudioBuffer}
         */
        let buffer = null;
        /** 是否加载完成 */
        let loaded = false;
    
        analyser.fftSize = 256;
    
        return {
            /**
             * 加载路径音频文件
             * @param {string} url 音频路径
             * @param {(res: AnalyserNode) => void} callback 加载完成回调
             */
            loadPath(url, callback) {
                const XHR = new XMLHttpRequest(); 
                XHR.open("GET", url, true); 
                XHR.responseType = "arraybuffer"; 
                // 先加载音频文件
                XHR.onload = () => {
                    context.decodeAudioData(XHR.response, audioBuffer => {
                        // 最后缓存音频资源
                        buffer = audioBuffer;
                        loaded = true;
                        typeof callback === "function" && callback(analyser);
                    });
                }
                XHR.send(null);
            },
    
            /** 
             * 加载 input 音频文件
             * @param {File} file 音频文件
             * @param {(res: AnalyserNode) => void} callback 加载完成回调
             */
            loadFile(file, callback) {
                const FR = new FileReader();
                // 先加载音频文件
                FR.onload = e => {
                    const res = e.target.result;
                    // 然后解码
                    context.decodeAudioData(res, audioBuffer => {
                        // 最后缓存音频资源
                        buffer = audioBuffer;
                        loaded = true;
                        typeof callback === "function" && callback(analyser);
                    });
                }
                FR.readAsArrayBuffer(file);
            },
    
            /** 播放音频 */
            play() {
                if (!loaded) return console.warn("音频未加载完成 !!!");
                // 这里有个问题,就是创建的音频对象不能缓存下来然后多次执行 start , 所以每次都要创建然后 start()
                bufferNode = context.createBufferSource();
                bufferNode.connect(analyser);
                analyser.connect(context.destination);
                bufferNode.buffer = buffer;
                bufferNode.start(0);
            },
    
            /** 停止播放 */
            stop() {
                if (!bufferNode) return console.warn("音频未播放 !!!");
                bufferNode.stop();
            }
        }
    }
    

    源码地址及使用展示

    8. 全局监听图片错误并替换到默认图片
    window.addEventListener("error", e => {
        const defaultImg = ''; //默认图片地址
        /**
         * @type {HTMLImageElement}
         */
        const node = e.target;
        if (node.nodeName && node.nodeName.toLocaleLowerCase() === "img") {     
            node.style.objectFit = "cover";
            node.src = defaultImg;
        }
    }, true);
    
    9. 复制功能

    Clipboard.js 这个插件库源码的时候找到核心代码 setSelectionRange(start: number, end: number),百度上搜到的复制功能全部都少了这个操作,所以搜到的复制文本代码在 iosIE 等一些浏览器上复制不了。

    /**
     * 复制文本
     * @param {string} text 复制的内容
     * @param {() => void} success 成功回调
     * @param {(tip: string) => void} fail 出错回调
     */
    function copyText(text, success = null, fail = null) {
        text = text.replace(/(^\s*)|(\s*$)/g, "");
        if (!text) {
            typeof fail === "function" && fail("复制的内容不能为空!");
            return;
        }
        const id = "the-clipboard";
        /**
         * 粘贴板节点
         * @type {HTMLTextAreaElement}
         */
        let clipboard = document.getElementById(id);
        if (!clipboard) {
            clipboard = document.createElement("textarea");
            clipboard.id = id;
            clipboard.readOnly = true
            clipboard.style.cssText = "font-size: 15px; position: fixed; top: -1000%; left: -1000%;";
            document.body.appendChild(clipboard);
        }
        clipboard.value = text;
        clipboard.select();
        clipboard.setSelectionRange(0, text.length);
        const state = document.execCommand("copy");
        if (state) {
            typeof success === "function" && success();
        } else {
            typeof fail === "function" && fail("复制失败");
        }
    }
    
    10. 检测类型

    可检测所有类型

    /**
     * 检测类型
     * @param {any} target 检测的目标
     * @returns {"string"|"number"|"array"|"object"|"function"|"null"|"undefined"} 只枚举一些常用的类型
     */
    function checkType(target) {
        /** @type {string} */
        const value = Object.prototype.toString.call(target);
        const result = value.match(/\[object (\S*)\]/)[1];
        return result.toLocaleLowerCase();
    }
    
    11. 格式化日期(代码极少版)
    /**
     * 获取指定日期时间戳
     * @param {number} time 毫秒数
     */
    function getDateFormat(time = Date.now()) {
        const date = new Date(time);
        return `${date.toLocaleDateString()} ${date.toTimeString().slice(0, 8)}`;
    }
    
    12. JavaScript小数精度计算
    /**
     * 数字运算(主要用于小数点精度问题)
     * @param {number} a 前面的值
     * @param {"+"|"-"|"*"|"/"} type 计算方式
     * @param {number} b 后面的值
     * @example 
     * ```js
     * // 可链式调用
     * const res = computeNumber(1.3, "-", 1.2).next("+", 1.5).next("*", 2.3).next("/", 0.2).result;
     * console.log(res);
     * ```
     */
    function computeNumber(a, type, b) {
        /**
         * 获取数字小数点的长度
         * @param {number} n 数字
         */
        function getDecimalLength(n) {
            const decimal = n.toString().split(".")[1];
            return decimal ? decimal.length : 0;
        }
        /**
         * 修正小数点
         * @description 防止出现 `33.33333*100000 = 3333332.9999999995` && `33.33*10 = 333.29999999999995` 这类情况做的处理
         * @param {number} n
         */
        const amend = (n, precision = 15) => parseFloat(Number(n).toPrecision(precision));
        const power = Math.pow(10, Math.max(getDecimalLength(a), getDecimalLength(b)));
        let result = 0;
    
        a = amend(a * power);
        b = amend(b * power);
    
        switch (type) {
            case "+":
                result = (a + b) / power;
                break;
            case "-":
                result = (a - b) / power;
                break;
            case "*":
                result = (a * b) / (power * power);
                break;
            case "/":
                result = a / b;
                break;
        }
    
        result = amend(result);
    
        return {
            /** 计算结果 */
            result,
            /**
             * 继续计算
             * @param {"+"|"-"|"*"|"/"} nextType 继续计算方式
             * @param {number} nextValue 继续计算的值
             */
            next(nextType, nextValue) {
                return computeNumber(result, nextType, nextValue);
            }
        };
    }
    
    13. 一行css适配rem

    750是设计稿的宽度:之后的单位直接1:1使用设计稿的大小,单位是rem

    html{ font-size: calc(100vw / 750); }
    
    14. 好用的格式化日期方法
    /**
     * 格式化日期
     * @param {string | number | Date} value 指定日期
     * @param {string} format 格式化的规则
     * @example
     * ```js
     * formatDate();
     * formatDate(1603264465956);
     * formatDate(1603264465956, "h:m:s");
     * formatDate(1603264465956, "Y年M月D日");
     * ```
     */
    function formatDate(value = Date.now(), format = "Y-M-D h:m:s") {
        const formatNumber = n => `0${n}`.slice(-2);
        const date = new Date(value);
        const formatList = ["Y", "M", "D", "h", "m", "s"];
        const resultList = [];
        resultList.push(date.getFullYear().toString());
        resultList.push(formatNumber(date.getMonth() + 1));
        resultList.push(formatNumber(date.getDate()));
        resultList.push(formatNumber(date.getHours()));
        resultList.push(formatNumber(date.getMinutes()));
        resultList.push(formatNumber(date.getSeconds()));
        for (let i = 0; i < resultList.length; i++) {
            format = format.replace(formatList[i], resultList[i]);
        }
        return format;
    }
    
    15. 网页定位

    文档说明
    获取百度地图key

    /**
     * 插入脚本
     * @param {string} link 脚本路径
     * @param {Function} callback 脚本加载完成回调
     */
    function insertScript(link, callback) {
        const label = document.createElement("script");
        label.src = link;
        label.onload = function () {
            if (label.parentNode) label.parentNode.removeChild(label);
            if (typeof callback === "function") callback();
        }
        document.body.appendChild(label);
    }
    
    /**
     * 获取定位信息 
     * @returns {Promise<{ city: string, districtName: string, province: string, longitude: number, latitude: number }>}
    */
    function getLocationInfo() {
        /**
         * 使用百度定位
         * @param {(value: any) => void} callback
         */
        function useBaiduLocation(callback) {
            const geolocation = new BMap.Geolocation({
                maximumAge: 10
            })
            geolocation.getCurrentPosition(function(res) {
                console.log("%c 使用百度定位 >>", "background-color: #4e6ef2; padding: 2px 6px; color: #fff; border-radius: 2px", res);
                callback({
                    city: res.address.city,
                    districtName: res.address.district,
                    province: res.address.province,
                    longitude: Number(res.longitude),
                    latitude: Number(res.latitude)
                })
            })
        }
    
        return new Promise(function (resolve, reject) {
            if (!window._baiduLocation) {
                window._baiduLocation = function () {
                    useBaiduLocation(resolve);
                }
                // ak=你自己的key
                insertScript("https://api.map.baidu.com/api?v=2.0&ak=66vCKv7PtNlOprFEe9kneTHEHl8DY1mR&callback=_baiduLocation");
            } else {
                useBaiduLocation(resolve);
            }
        })
    }
    
    16. 输入保留数字 <input type="text">

    使用场景:用户在输入框输入内容时,实时过滤保持数字值显示;
    tips:在Firefox中设置<input type="number">会有样式 bug

    /**
     * 输入只能是数字
     * @param {string | number} value 输入的值
     * @param {boolean} decimal 是否要保留小数
     * @param {boolean} negative 是否可以为负数
     */
    function inputOnlyNumber(value, decimal, negative) {
        let result = value.toString().trim();
        if (result.length === 0) return "";
        const minus = (negative && result[0] == "-") ? "-" : "";
        if (decimal) {
            result = result.replace(/[^0-9.]+/ig, "");
            let array = result.split(".");
            if (array.length > 1) {
                result = array[0] + "." + array[1];
            }
        } else {
            result = result.replace(/[^0-9]+/ig, "");
        }
        return minus + result;
    }
    
    END

    以上就是就是一些常用到的功能分享,后续有也会更新 另外还有一些其他功能我觉得不重要所以不贴出来了,有兴趣可以看看 仓库地址

    作者:黄景圣
    转载于此,膜拜大佬

    相关文章

      网友评论

        本文标题:前端常用功能集合

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