前端常用功能集合

  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) => 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|

源码地址及使用展示

3. 图片懒加载

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

/**
 * 懒加载
 * @description 可加载``、`

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上传图片
 * @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 = `
${option.loadIcon || '

loading...

'}
`; 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. 输入保留数字

使用场景:用户在输入框输入内容时,实时过滤保持数字值显示;
tips:在Firefox中设置会有样式 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

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

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

你可能感兴趣的:(前端常用功能集合)