前端常用功能集合

2025-05-06 04:10:12

以下功能主要是以移动端为主

使用到的ES6在移动端中没有不兼容情况,这里基本应用在微信端,手机浏览器的话也不用担心

所有功能均由原生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|

* @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}

*/

let nodeItems = [];

/**

* 圆点容器

* @type {HTMLElement}

*/

let nodePagination = null;

/**

* 圆点节点列表

* @type {Array}

*/

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 += `

`;

}

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 可加载``、`

* @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上传图片

* @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),百度上搜到的复制功能全部都少了这个操作,所以搜到的复制文本代码在 ios和 IE 等一些浏览器上复制不了。

/**

* 复制文本

* @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

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

作者:黄景圣

转载于此,膜拜大佬