分享一些前端常用功能集合

16,987 阅读12分钟

描述

  • 所有功能均由原生javascript实现,用最少的代码,造最高效的事情。

  • 先前在做一些H5单页(活动页)的时候,像我这种最求极致加载速度,且不喜欢用第三方库的人,选择自己动手造一些无依赖精简高效的轮子,然后按需应用在实际项目中;同时为了比百度上搜到更好用的代码分享给有需要的老铁。

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

1. http请求

前端必备技能,也是使用最多的功能。代码中的checkType方法看第10个功能介绍

第一种fetch

XMLHttpRequest的功能区别在fetch没有请求进度监听功能,而XMLHttpRequest有;当前功能封装可以直接在项目中使用,headers的配置也根据传入参数类型做了对应设置,无需再手动设置

/**
 * 基于`fetch`请求 [MDN文档](https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API)
 * @param {"GET"|"POST"|"PUT"|"DELETE"} method 请求方法
 * @param {string} url 请求路径
 * @param {object|FormData|string=} data 传参对象,json、formdata、普通表单字符串
 * @param {RequestInit & { timeout: number }} option 其他配置
 */
function fetchRequest(method, url, data = {}, option = {}) {
  /** 非`GET`请求传参 */
  let body = undefined;
  /** `GET`请求传参 */
  let query = "";
  /** 默认请求头 */
  const headers = {};
  /** 超时毫秒 */
  const timeout = option.timeout || 8000;
  /** 传参数据类型 */
  const dataType = checkType(data);
  // 传参处理
  if (method === "GET") {
    // 解析对象传参
    if (dataType === "object") {
      for (const key in data) {
        query += "&" + key + "=" + data[key];
      }
    } else {
      console.warn("fetch 传参处理 GET 传参有误,需要的请求参数应为 object 类型");
    }
    if (query) {
      query = "?" + query.slice(1);
      url += query;
    }
  } else {
    body = dataType === "object" ? JSON.stringify(data) : data;
  }
  // 设置对应的传参请求头,GET 方法不需要
  if (method !== "GET") {
    switch (dataType) {
      case "object":
        headers["Content-Type"] = "application/json";
        break;

      case "string":
        headers["Content-Type"] = "application/x-www-form-urlencoded"; // 表单请求,`id=1&type=2` 非`new FormData()`
        break;

      default:
        break;
    }
  }
  const controller = new AbortController();
  let timer;
  return new Promise(function(resolve, reject) {
    fetch(url, {
      method,
      body,
      headers,
      signal: controller.signal,
      // credentials: "include",  // 携带cookie配合后台用
      // mode: "cors",            // 配合后台设置用的跨域模式
      ...option,
    }).then(response => {
      // 把响应的信息转为`json`
      return response.json();
    }).then(res => {
      clearTimeout(timer);
      resolve(res);
    }).catch(error => {
      clearTimeout(timer);
      reject(error);
    });
    timer = setTimeout(function() {
      reject("fetch is timeout");
      controller.abort();
    }, timeout);
  });
}

特殊应用场景:在一些H5单页的简单GET请求时,通常用得最多;因为代码极少,就像下面这样

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

第二种XMLHttpRequest的功能区别在fetch没有请求进度监听功能,而,需要Promise用法在外面包多一层function做二次封装即可

使用场景比较广的一种,axios也是基于XMLHttpRequest去封装的

/**
 * `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|FormData|string} params.data 传参对象,json、formdata、普通表单字符串
 * @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 进度回调(暂时没用到)
 * @param {"arraybuffer"|"blob"|"document"|"json"|"text"} params.responseType 响应结果类型,默认`json`
 */
function ajax(params) {
  if (checkType(params) !== "object") return console.error("ajax 请求参数类型有误");
  if (!params.method) return console.error("ajax 缺少请求方法");
  if (!params.url) return console.error("ajax 缺少请求 url");

  const XHR = new XMLHttpRequest();
  /** 请求方法 */
  const method = params.method;
  /** 超时检测 */
  const overtime = checkType(params.overtime) === "number" ? params.overtime : 0;
  /** 请求链接 */
  let url = params.url;
  /** 非`GET`请求传参 */
  let body = "";
  /** `GET`请求传参 */
  let query = "";
  /** 传参数据类型 */
  const dataType = checkType(params.data);

  // 传参处理
  if (method === "GET") {
    // 解析对象传参
    if (dataType === "object") {
      for (const key in params.data) {
        query += "&" + key + "=" + params.data[key];
      }
    } else {
      console.warn("ajax 传参处理 GET 传参有误,需要的请求参数应为 object 类型");
    }
    if (query) {
      query = "?" + query.slice(1);
      url += query;
    }
  } else {
    body = dataType === "object" ? JSON.stringify(params.data) : params.data;
  }

  // 监听请求变化;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(XHR.response, XHR);
    } else {
      typeof params.fail === "function" && params.fail(XHR);
    }
  }

  // 判断请求进度
  if (params.progress) {
    XHR.addEventListener("progress", params.progress);
  }
  
  XHR.responseType = params.responseType || "json"; // TODO: 设置响应结果为`json`这个一般由后台返回指定格式,前端无配置
  // XHR.withCredentials = true;	// 是否Access-Control应使用cookie或授权标头等凭据进行跨站点请求。
  XHR.open(method, url, true);

  // 设置对应的传参请求头,GET 方法不需要
  if (params.method !== "GET") {
    switch (dataType) {
      case "object":
        XHR.setRequestHeader("Content-Type", "application/json"); // `json`请求
        break;

      case "string":
        XHR.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); // 表单请求,非`new FormData`
        break;

      default:
        break;
    }
  }

  // 判断设置配置头信息
  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轮播图组件

远古时期写的一个web功能组件,拖拽回弹物理效果是参照开源项目Swiper.js做的,效果功能保持一致,代码实现思路由自己完成

源码地址及使用展示

/**
 * 轮播组件
 * @author https://github.com/Travis-hjs
 * @description 
 * 移动端`swiper`组件,如果需要兼容`pc`自行修改对应的`touch`到`mouse`事件即可。现成效果预览:https://huangjingsheng.gitee.io/hjs/face/
 * @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 滑动/切换结束回调
 */
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: "lazy-src",
  errorPath: "./img/error.jpg"
})

Vue.directive("lazy", {
  inserted(el, binding) {
    el.setAttribute("lazy-src", binding.value); // 跟上面的对应
    lazySrc.observer.observe(el);
  }
})

/** 添加一个加载`background`的指令 */
const lazyBg = lazyLoad({
  lazyAttr: "lazy-bg",
  loadType: "background"
})

Vue.directive("lazy-bg", {
  inserted(el, binding) {
    el.setAttribute("lazy-bg", binding.value); // 跟上面的对应
    lazyBg.observer.observe(el);
  }
})

4. 上传图片

这个超简单,没啥好说的

<!-- 先准备好一个input标签,然后设置type="file",最后挂载一个onchange事件 -->
<input class="upload-input" type="file" accept="image/*" 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"];
  // 使用完一定要清空
  el.value = "";
  // 判断大小
  if (file.size > 5 * 1024 * 1024) {
    return alert("上传的文件不能大于5MB");
  }

  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,
    overtime: 8000,
    success(res) {
      console.log("上传成功", res);
    },
    fail(err) {
      console.log("上传失败", err);
    },
    timeout() {
      console.warn("XMLHttpRequest 请求超时 !!!");
    }
  });
}

base64转换和静态预览

配合接口上传到后台 这个可能要安装环境,因为是服务端项目

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. 音频播放组件

这里需要说明一下应用场景:我先前做H5活动页(红包雨)的时候遇到一个问题,就是在移动端快速点击节点并播放音频的时候,aduio标签播放的速度会有很严重的延迟。后来搜了下相关资料发现一个音频API:new AudioContext,和我之前做小游戏时用到的引擎(cocos creator)音频API是一样的。然后找了挺久发现这个API的使用资料、教程还是挺少的可能是除了做H5游戏引擎的人会用到吧,比较详细的也只有MDN官网,剩下的就是一些基于这个APIJavaScript库,但是我需要用到的功能比较简单,就是点击播放无延迟。所以自己去实现一个基于new AudioContext常用的音频组件。

源码地址及使用展示

/**
 * `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 => {
  /** 默认`base64`图片 */
  const defaultImg = 'data:image/png;base64,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';
  /**
   * @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 {(error: 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");
  // clipboard.blur(); // 设置readOnly就不需要这行了
  if (state) {
    typeof success === "function" && success();
  } else {
    typeof fail === "function" && fail("复制失败");
  }
}

10. 检测类型

可检测所有类型

/**
 * 检测类型
 * @param {any} target 检测的目标
 * @returns {"string"|"number"|"array"|"object"|"function"|"null"|"undefined"|"regexp"} 只枚举一些常用的类型
 */
function checkType(target) {
  /** @type {string} */
  const value = Object.prototype.toString.call(target);
  const result = value.match(/\[object (\S*)\]/)[1];
  return result.toLocaleLowerCase();
}

11. 好用的格式化日期方法

/**
 * 格式化日期
 * @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") {
  if (["null", null, "undefined", undefined, ""].includes(value)) return "";
  // ios 和 mac 系统中,带横杆的字符串日期是格式不了的,这里做一下判断处理
  if (typeof value === "string" && new Date(value).toString() === "Invalid Date") {
    value = value.replace(/-/g, "/");
  }
  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;
}

12. JavaScript小数精度计算

/**
 * 数字运算(主要用于小数点精度问题)
 * [see](https://juejin.im/post/6844904066418491406#heading-12)
 * @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. 网页定位

这里使用百度定位,无论代码封装、调用方式还是位置准确性都比微信sdk那个好用太多了,包括在任何网页端;

文档说明

获取百度地图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);
    }
  })
}

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

16. 自定义事件监听、解绑、派发功能

function moduleEvent() {
  /**
   * 事件集合对象
   * @type {{[key: string]: Array<Function>}}
   */
  const eventMap = {};

  return {
    /**
     * 添加事件
     * @param {string} name 事件名
     * @param {Function} fn 事件执行的函数
     */
    on(name, fn) {
      if (!eventMap.hasOwnProperty(name)) {
        eventMap[name] = [];
      }
      // 没有重复函数的时候才添加
      if (!eventMap[name].some(item => item === fn)) {
        eventMap[name].push(fn);
      }
    },

    /**
     * 解绑事件
     * @param {string} name 事件名
     * @param {Function} fn 事件绑定的函数
     */
    off(name, fn) {
      const fns = eventMap[name];
      if (fns && fns.length > 0 && fn) {
        for (let i = 0; i < fns.length; i++) {
          const item = fns[i];
          if (item === fn) {
            fns.splice(i, 1);
            break;
          }
        }
      } else {
        console.log("[moduleEvent] => 没有要解绑的事件");
      }
    },

    /**
     * 调用事件
     * @param {string} name 事件名
     * @param {any} params 事件参数
     */
    dispatch(name, params) {
      const fns = eventMap[name];
      if (fns && fns.length > 0) {
        for (let i = 0; i < fns.length; i++) {
          const fn = fns[i];
          fn(params);
        }
      } else {
        console.log("[moduleEvent] => 没有要执行的事件");
      }
    }
  }
}

16. 数字转中文(价格)

考虑到 String.prototype.substr 已经弃用,所以这里自行实现了一个方法;觉得没必要的可以手动替换为原生的substr()使用。

/**
 * 实现原生废弃的`String.prototype.substr()`方法
 * @param {string} value 
 * @param {number} start 
 * @param {number} length 
 */
function substr(value, start = 0, length = value.length) {
  if (length < 0) return "";
  const _length = value.length;
  if (start <= -_length) {
    start = 0;
  }
  start = start < 0 ? _length + start : start;
  length = start + length > _length ? _length : start + length;
  let result = "";
  for (let i = start; i < length; i++) {
    result += value[i];
  }
  return result;
}

/**
 * 数字转中文
 * @param {number} target 
 */
function numberToChinese(target) {
  if (typeof target !== "number" || isNaN(target)) return "";
  const cnNums = ["零", "一", "二", "三", "四", "五", "六", "七", "八", "九"];
  const cnIntRadice = ["", "十", "百", "千"];
  const cnIntUnits = ["", "万", "亿", "兆"];
  const cnDecUnits = ["角", "分", "毫", "厘"];
  const cnInteger = "整";
  const cnIntLast = "元";
  const maxNum = 999999999999999.9999;
  let integerNum = "";
  let decimalNum = "";
  let result = "";
  /** @type {Array<string>} */
  let parts;
  if (target >= maxNum) {
    console.warn("超出最大处理数字");
    return "";
  }
  if (target == 0) {
    result = cnNums[0] + cnIntLast + cnInteger;
    return result;
  }
  const numStr = target.toString();
  if (numStr.indexOf(".") == -1) {
    integerNum = numStr;
  } else {
    parts = numStr.split(".");
    integerNum = parts[0];
    decimalNum = substr(parts[1], 0, 4);
  }
  // 获取整型部分转换
  if (parseInt(integerNum, 10) > 0) {
    let zeroCount = 0;
    const intLength = integerNum.length;
    for (let i = 0; i < intLength; i++) {
      const n = substr(integerNum, i, 1);
      const p = intLength - i - 1;
      const q = p / 4;
      const m = p % 4;
      if (n == "0") {
        zeroCount++;
      } else {
        if (zeroCount > 0) {
          result += cnNums[0];
        }
        zeroCount = 0; // 归零
        result += cnNums[parseInt(n)] + cnIntRadice[m];
      }
      if (m == 0 && zeroCount < 4) {
        result += cnIntUnits[q];
      }
    }
    result += cnIntLast;
  }
  // 小数部分
  if (decimalNum != "") {
    const decLength = decimalNum.length;
    for (let i = 0; i < decLength; i++) {
      const n = substr(decimalNum, i, 1);
      if (n != "0") {
        result += cnNums[Number(n)] + cnDecUnits[i];
      }
    }
  }
  if (result == "") {
    result += cnNums[0] + cnIntLast + cnInteger;
  } else if (decimalNum == "") {
    result += cnInteger;
  }
  return result;
}

END

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