js 常用 utils

430 阅读4分钟

页面加载时间

//在页面头部以同步的方式加载 js 代码
var start_time = new Date();
var end_time = "";

window.onload = function () {
  end_time = new Date();
  // do something
};

页面停留时间

  1. 优先使用 visibilitychange 事件监听

  2. 监听pageshow

let start = Date.now();
function loadHandler(e) {
  start = Date.now();
}

function unloadHandler(e) {
  // 如果用户一直不关闭页面,可能出现超大值,可以根据业务需要处理,例如设置一个上限
  const duration = Date.now() - start;
  // do something
}

if ("onpageshow" in window) {
  addEvent(window, "pageshow", loadHandler);
  addEvent(window, "pagehide", unloadHandler);
} else {
  addEvent(window, "load", loadHandler);
  addEvent(window, "unload", unloadHandler);
}

function addEvent(target, type, listener) {
  if (window.addEventListener) {
    target.addEventListener(type, listener, false);
  } else {
    target.attachEvent("on" + type, listener);
  }
}








图片转 base64

加载图片

html2canvas 图片如果不显示,可以先将图片转成 base64

  • 加载图片
    /**
     * 加载图片
     * @param {String} src 资源地址
     * @return Promise(Image)
     */
    function loadImg(src) {
      return new Promise((resolve, reject) => {
        const img = new Image();
        // listen event
        img.onload = () => {
          // do something
          // example Image to base64
          resolve(img);
        };
        img.onerror = reject;
        // attr
        img.setAttribute("crossOrigin", "anonymous");
        img.src = src;
      });
    }
    
  1. 使用 canvas.toDataURL()

    /**
     * 图片转 base64 格式
     * @param {Image} img
     * @returns Data URLs
     */
    function imgToBase64(img) {
      const canvas = document.createElement('canvas')
      const ctx = canvas.getContext('2d')
      canvas.width = img.width
      canvas.height = img.height
      ctx.drawImage(img, 0, 0, img.width, img.height)
    
      // 获取 dataURL
      const dataURL = canvas.toDataURL()
      // canvas.toBlob() 可以获取 blob 数据
      // 无法直接作为<img>的src属性值呈现的,需要URL.createObjectURL()方法处理下
      return dataURL
    }
    
  2. 通过request

    /**
     * 图片 src 转 base64 格式
     * @param {String} src 资源地址
     * @returns Promise(Data URLs)
     */
    function loadBase64Img(src) {
      const data = await fetch(src);
      const blob = await data.blob();
      return new Promise(resolve => {
        const reader = new FileReader();
        reader.onloadend = function (e) {
          resolve(e.target.result);
        };
        reader.readAsDataURL(blob);
      });
    }
    








下载文件

参考链接


  • 单个文件

    如果图片返回头是 Content-Type: application/octet-stream可以使用window.open()直接下载图片

    /**
     * 下载单个图片
     * @param {dataURL | URL} img
     * @returns
     */
    async function downloadImg(img) {
      //  base64 转 Blob
      const base64ToBlob = code => {
        const parts = code.split(";base64,");
        const contentType = parts[0].split(":")[1];
        const raw = window.atob(parts[1]);
        const rawLength = raw.length;
        const uInt8Array = new Uint8Array(rawLength);
        for (let i = 0; i < rawLength; i++) {
          uInt8Array[i] = raw.charCodeAt(i);
        }
        return new Blob([uInt8Array], { type: contentType });
      };
      // http 获取 Blob
      const httpToBlob = async url => {
        const data = await fetch(url);
        return data.blob();
      };
      // 创建文件流
      let blob = null;
      if (/^https?/.test(img)) {
        blob = await httpToBlob(img);
      } else {
        blob = base64ToBlob(img);
      }
      // create download
      const a = document.createElement("a");
      a.download = "test"; // filename
      a.href = URL.createObjectURL(blob);
      a.click();
    }
    
  • 多个文件 zip 下载

    import JSZip from "jszip";
    import { saveAs } from "file-saver";
    
    /**
     * 下载多个图片 zip
     * @param {Array[dataURL]} imgs base64 图片列表
     */
    function downloadMultipleImg(imgs) {
      const zip = new JSZip();
      const folder = zip.folder("images");
      imgs.forEach(img => {
        // base64 格式
        const base64 = img.split(",")[1];
        folder.file(`filename.png`, base64, {
          base64: true,
        });
      });
      zip.generateAsync({ type: "blob" }).then(content => {
        // see FileSaver.js
        saveAs(content, `folderName.zip`);
      });
    }
    








复制到剪切板

iframe 嵌入需要添加属性 allow="clipboard-read;clipboard-write"
safari 会报没有权限错误

  • 使用第三方插件 clipboard.js

  • 文字

    1. 游览器全局属性 Navigator.clipboard

      /**
       * 复制文字到剪贴板
       * @param {String} newClipText
       */
      function copyTextToClipboard(newClipText) {
        return navigator.clipboard.writeText(newClipText);
      }
      
    2. 已弃用 document.execCommand

      /**
       * 复制文字到剪贴板
       * @param {String} text
       */
      function copyTextToClipboard(text) {
        const el = document.createElement("textarea");
        el.value = text;
        document.body.appendChild(el);
        el.select();
        try {
          document.execCommand("Copy");
        } catch (error) {
          // do something
        }
        document.body.removeChild(el);
      }
      
  • 图片

    /**
     * 复制图片到剪贴板
     * @param {DOM} element Event.target
     * @returns
     */
    async function copyImgToClipboard(element) {
      // not image dom
      if (element.nodeName.toLowerCase() !== "img" || !element.src) return;
    
      // Browser does not support
      if (!navigator?.clipboard?.write || !window.ClipboardItem) {
        // do something
        return;
      }
    
      const data = await fetch(element.src);
      const blob = await data.blob();
      try {
        await navigator.clipboard.write([
          new window.ClipboardItem({
            [blob.type]: blob,
          }),
        ]);
        // success
        // do something
      } catch (error) {
        // do something
        console.log("copyImgToClipboard error", error);
      }
    }
    








判断文本是否溢出

参考链接


  1. 元素的 clientWidth < scrollWidth 代表文本溢出

    // 某些浏览器会出现,文本溢出了,但 clientWidth 与 scrollWidth 相等
    const isOverflow = el => el.clientWidth < el.scrollWidth
    
    • 元素 display 需要设置为 inline-block
      • inline 元素的 clientWidth & scrollWidth 为 0

      • block 宽度不是内容撑开

    canvas 的 ctx.measureText().width 可以获取文本宽度

  2. element-plus 源码

    // width + leftPadding + rightPadding < offsetWidth
    // scrollWidth > offsetWidth
    








文本溢出(兼容)

重要 css 属性 white-space overflow-wrap word-break

  • 单行文本

    overflow: hidden; 
    text-overflow: ellipsis; 
    white-space: nowrap;
    
  • 多行文本溢出

    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    /* 文本的行数 */
    -webkit-line-clamp: 3;
    /* 排列方式 */
    /* -webkit-box-orient: vertical; */
    
  • 多行文本溢出(纯css兼容)








实现防篡改水印

参考链接:

防君子不防小人
F12 禁用 javascript 将失效

  • canvas实现水印

    import { computed } from 'vue'
    export default function useWatermarkBg(props) {
      return computed(() => {
        const canvas = document.createElement('canvas')
        const devicePixelRatio = window.devicePixelRatio || 1
        // 设置字体大小
        const fontSize = props.fontSize * devicePixelRatio
        const font = fontSize + 'px serif'
        const ctx = canvas.getContext('2d')
        ctx.font = font
        const { width } = ctx.measureText(props.text) // 获取文字宽度
        const canvasSize = Math.max(100, width) + props.gap * devicePixelRatio
        canvas.width = canvasSize
        canvas.height = canvasSize
        ctx.translate(canvas.width / 2, canvas.height / 2)
        // 旋转 45 度让文字变倾斜
        ctx.rotate((Math.PI / 180) * -45)
        ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'
        ctx.font = font
        ctx.textAlign = 'center'
        ctx.textBaseline = 'middle'
        ctx.fillText(props.text, 0, 0)
        return {
          base64: canvas.toDataURL(),
          size: canvasSize,
          styleSize: canvasSize / devicePixelRatio,
        }
      })
    }
    
  • 挂载 dom

    {
        zIndex: props.zIndex,
        position: 'absolute',
        left: 0,
        top: 0,
        width: '100%',
        height: '100%',
        pointerEvents: 'none', // 元素永远不会成为鼠标事件的target
        backgroundRepeat: 'repeat', // 重复绘制图片
      }
    
  • 防篡改

    let ob
    onMounted(() => {
      ob = new MutationObserver(records => {
        // 循环节点的动作
        for (const record of records) {
          // 如果有节点被删除,循环一下判断是否有水印的节点
          for (const dom of record.removedNodes) {
            if (dom === div) {
              console.log('水印被删除')
              // ...
              return
            }
          }
          // 如果有节点被修改,判断一下是否是水印的节点
          if (record.target === div) {
            console.log('属性被修改')
            // ...
            return
          }
        }
      })
      ob.observe(parentRef.value, {
        childList: true,
        attributes: true,
        subtree: true,
      })
    })
    
    // 在组件卸载的时候取消监听
    onUnmounted(() => {
      ob && ob.disconnect() // 取消监听
      div = null // 因为 div 是全局变量在写在的时候值为空
    })