描述
1.以下功能主要是以移动端为主
2.使用到的ES6在移动端中没有不兼容情况,这里我基本应用在微信端,手机浏览器的话也不用担心
3.所有功能均由原生JavaScript实现,没有任何依赖,我一贯的做法是用最少的代码,造最高效的事情
我在做一些H5单页(活动页)的时候,像我这种最求极致加载速度,且不喜欢用第三方库的人,所以决定自己动手做一些无依赖、精简高效的东西,然后按需应用在实际项目中,同时为了比百度上搜到更好用的代码分享给大家。
这里推荐前端使用vs code这个代码编辑器,理由是在声明的时候写好标准的JSDoc注释,在调用时会有很全面的代码提示,让弱类型的javascript也有类型提示
1. http请求 前端必备技能,也是使用最多的功能。我个人不喜欢用axios这个东西(懒得去看文档,而且觉得很鸡肋),几乎所有的web项目都是用的这个轮子。
第一种:fetch
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 /** * 基于`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请求时通常用得最多,因为代码极少,就像下面这样
1 2 3 fetch("http://xxx.com/api/get").then(response => response.text()).then(res => { console.log("请求成功", res); })
第二种:XMLHttpRequest,需要Promise用法在外面包多一层function做二次封装即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 /** * `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轮播图组件 这是我写的第一个web功能组件,拖拽、回弹物理效果是参照开源项目Swiper.js做的,效果功能保持一致,代码实现均由自己完成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 /** * 轮播组件 * @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. 图片懒加载 非传统实现方式,性能最优
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 /** * 懒加载 * @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中使用指令去使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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. 上传图片 这个超简单,没啥好说的
1 2 <!-- 先准备好一个input标签,然后设置type="file",最后挂载一个onchange事件 --> <input class="upload-input" type="file" name="picture" onchange="upLoadImage(this)">
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 /** * 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 请求超时 !!!"); } }); }
5. 下拉刷新组件 拖拽效果参考上面swiper的实现方式,下拉中的效果是可以自己定义的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 // 这里我做的不是用 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. 监听滚动到底部 就几行代码的一个方法,另外监听元素滚动到底部可以参考代码笔记
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 /** * 监听滚动到底部 * @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官网,剩下的就是一些基于这个API的JavaScript库,但是我需要用到的功能比较简单,就是点击播放无延迟。所以自己去实现一个基于new AudioContext常用的音频组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 /** * `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. 全局监听图片错误并替换到默认图片 1 2 3 4 5 6 7 8 9 10 11 12 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),百度上搜到的复制功能全部都少了这个操作,所以搜到的复制文本代码在 ios 和 IE 等一些浏览器上复制不了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 /** * 复制文本 * @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. 检测类型 可检测所有类型
1 2 3 4 5 6 7 8 9 10 11 /** * 检测类型 * @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. 生成时间戳(代码极少版) 1 2 3 4 5 6 7 8 /** * 获取指定日期时间戳 * @param {number} time 毫秒数 */ function getDateFormat(time = Date.now()) { const date = new Date(time); return `${date.toLocaleDateString()} ${date.toTimeString().slice(0, 8)}`; }
12. JavaScript小数精度计算 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 /** * 数字运算(主要用于小数点精度问题) * @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
1 html{ font-size: calc(100vw / 750); }
14. 好用的格式化日期方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 /** * 格式化日期 * @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. 网页定位 这里使用百度定位,无论代码封装、调用方式还是位置准确性都比微信sdk那个好用太多了,包括在任何网页端;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 /** * 插入脚本 * @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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 /** * 输入只能是数字 * @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; }