列举一些在日常开发中常用的辅助函数,以便于快速开发

资源加载相关

加载一个远程脚本

从给定的地址链接加载 js 脚本,并在加载完成后执行后续的一些操作

const needRunCallbacks: Record<string, any[]> = {};

function onScriptLoaded($script: HTMLScriptElement) {
  const onEnd = 'onload' in $script ? stdOnEnd.bind($script) : ieOnEnd.bind($script);
  onEnd($script);

  function stdOnEnd(this: any, script: HTMLScriptElement) {
    const _that = this;
    const handler = (isError: boolean) => {
      const error = isError ? new Error(`Failed to load ${src}`) : null;

      // 移除事件监听,避免重复执行
      _that.onerror = _that.onload = null;
      needRunCallbacks[script.src]?.forEach((callback) => callback(error, script));

      delete needRunCallbacks[script.src];
    };

    script.onload = () => handler(false);
    script.onerror = () => handler(true);
  }

  function ieOnEnd(this: any, script: HTMLScriptElement) {
    const _that = this;

    script.onreadystatechange = () => {
      if (_that.readyState !== 'complete' && _that.readyState !== 'loaded') return;

      _that.onreadystatechange = null;
      needRunCallbacks[script.src]?.forEach((callback) => callback(null, script));

      delete needRunCallbacks[script.src];
    };
  }
}

/**
 * 加载一个远程脚本
 * @param {String} src 一个远程脚本
 * @param {Function} callback 回调
 */
export function loadScript(src: string, callback?: (err?: Error, script?: any) => void) {
  const existScript = document.getElementById(src);
  const cb = callback || (() => {});

  if (existScript) {
    // 如果当前脚本资源还未结束加载,则只需要往回调数组里追加,否则(表示已加载完成)直接执行即可
    return needRunCallbacks[src] ? needRunCallbacks[src].push(cb) : cb();
  } else {
    const $script = document.createElement('script');

    needRunCallbacks[src] = [cb];
    $script.src = src;
    $script.id = src;
    $script.async = true;
    document.body.appendChild($script);

    onScriptLoaded($script);
  }
}

同上实现,如果要卸载脚本资源:

export function unloadScript(src: string) {
  document.getElementById(src)?.remove();
}

加载一组远程脚本

在上面的基础上,添加实现:

import { cloneDeep } from 'lodash';

/**
 * 顺序加载一组远程脚本
 * @param {Array} list 一组远程脚本
 * @param {Function} cb 回调
 */
export function loadScripts(srcs: string[], callback?: () => void) {
  const cloneSrcs = cloneDeep(srcs);
  const firstSrc = cloneSrcs.shift() as string;

  firstSrc ? loadScript(firstSrc, () => loadScripts(cloneSrcs, callback)) : callback?.();
}

moment/dayjs相关

时间格式化

通常在开发中,存在类似这样的需求:将时间字符串转为 今天..昨天..具体时间,下面通过 moment/dayjs(包体积更小,两者 api 大致相同) 进行实现:

/**
 * 将时间字符串转为 今天..昨天..具体时间
 * @param {String} timeStr 目标时间
 * @param {String} defaultFormat 默认输出格式
 * @return {String} 转换后的时间
 */
export function timeFormat(timeStr: string, defaultFormat = 'YYYY-MM-DD HH:mm') {
  const dataTime = moment(timeStr);
  const timeSuffix = dataTime.format('HH:mm');

  const isToday = dataTime.isSame(moment().startOf('day'), 'd');
  const yd = moment().subtract(1, 'days').startOf('day');
  const isYesterday = dataTime.isSame(yd, 'd');

  // 或者你可以再分的细一点,当年的不显示年份...
  if (isToday) return '今天 ' + timeSuffix;
  else if (isYesterday) return '昨天 ' + timeSuffix;
  return dataTime.format(defaultFormat);
}

复制/粘贴相关

文本选中

/**
 * 选中元素中的文本
 * @param {Element} textEl 目标元素
 * @param {Number} startIndex 起始索引
 * @param {Number} stopIndex 结束索引
 */
export function selectText(textEl: any, startIndex: number, stopIndex: number) {
  if (textEl.createTextRange) {
    // ie
    const range = textEl.createTextRange();
    range.collapse(true);
    range.moveStart('character', startIndex); // 起始光标
    range.moveEnd('character', stopIndex - startIndex); // 结束光标
    range.select(); // 不兼容苹果
  } else {
    // firefox/chrome
    textEl.setSelectionRange(startIndex, stopIndex);
    textEl.focus();
  }
}

文本复制到剪贴板(丢弃)

考虑到复制的文本中存在换行符,所以使用的 textarea元素:

/**
 * 将一段文本复制到剪贴板
 * @param {String} value 文本内容
 */
export function setClipboard(value: string) {
  const tempInput = document.createElement('textarea');
  (tempInput as any).style = 'position: absolute; left: -1000px; top: -1000px';
  tempInput.value = value;
  document.body.appendChild(tempInput);
  tempInput.select();
  document.execCommand('copy');
  document.body.removeChild(tempInput);
}

文本复制到系统剪贴板(新)

async function copyToClipboard(text: string) {
  if (navigator.clipboard) {
    try {
      await navigator.clipboard.writeText(text);
      return true;
    } catch (error) {
      console.error('无法将文本复制到剪贴板:', error);
      return false;
    }
  } else {
    // 备用方案:在不支持 navigator.clipboard 的浏览器中创建临时输入框并复制文本
    const tempInput = document.createElement('textarea');
    tempInput.value = text;
    document.body.appendChild(tempInput);
    tempInput.select();
    document.execCommand('copy');
    document.body.removeChild(tempInput);
    return true;
  }
}

从系统剪贴板读取内容

async function readFromClipboard() {
  let text = '';
  if (navigator.clipboard) {
    try {
      text = await navigator.clipboard.readText();
    } catch (error) {
      console.error('无法从剪贴板读取文本:', error);
    }
  } else {
    // 备用方案:在不支持 navigator.clipboard 的浏览器中使用 document.execCommand('paste')
    const tempInput = document.createElement('textarea');
    document.body.appendChild(tempInput);
    tempInput.focus();
    document.execCommand('paste');
    text = tempInput.value;
    document.body.removeChild(tempInput);
  }
  return text;
}

注意:基于安全性和隐私保护限制,document.execCommand 不会直接从系统剪贴板获取内容。所以更多时候请优先使用 navigator.clipboard,因为它提供了更安全的方法来执行这些任务,并且更具兼容性。

HTML 复制到剪贴板

在需要将元素带样式复制到 word/excel 中的场景使用(例如表格复制?):

/**
 * 将 html 带格式复制到剪贴板
 * @param {Element} elToBeCopied 需要粘贴的元素节点 eg. document.querySelector('.table')
 */
export function copyEl(elToBeCopied: Element) {
  let sel;
  // Ensure that range and selection are supported by the browsers
  if (document.createRange && window.getSelection) {
    const range = document.createRange();
    sel = window.getSelection();
    // unselect any element in the page
    sel?.removeAllRanges();

    try {
      range.selectNodeContents(elToBeCopied);
    } catch (e) {
      range.selectNode(elToBeCopied);
    } finally {
      sel?.addRange(range);
    }

    document.execCommand('copy');
  }
  sel?.removeAllRanges();
}

文字头像相关

根据名称随机设置背景

/**
 * 根据名称随机设置背景
 *
 * @param {String} name 名称
 * @returns {String} 背景色
 */
export function randomColorByName(name: string) {
  let str = '';
  // 避免名称过短导致不能生成正常长度的十六进制数
  let tempName = name.padEnd(2, 'a');
  0;
  for (let i = 0; i < tempName.length; i++) {
    str += parseInt(name[i].charCodeAt(0), 10).toString(16);
  }
  return '#' + str.slice(1, 4);
}

通过 canvas 绘制文字头像

/**
 * 根据传入的尺寸和名字生成文字头像
 *
 * @param {[Number, Number]} size 头像尺寸
 * @param {String} name 名字
 * @param {String} bgc 背景色
 * @returns 返回生成的图片
 * @example
 * import { genTextImg } from '@/utils/genTextImg';
 * genTextImg([100, 100], '张三');
 */
export function genTextImg(size: [number, number], name: string, bgc?: string) {
  const showName = name?.charAt(0);
  const cvs = document.createElement('canvas');
  cvs.setAttribute('width', size[0]);
  cvs.setAttribute('height', size[1]);

  const ctx = cvs.getContext('2d')!;

  ctx.fillStyle = bgc ?? randomColorByName(name);
  ctx.fillRect(0, 0, size[0], size[1]);
  ctx.fillStyle = '#fff';

  ctx.font = size[0] * 0.6 + 'px Arial';
  ctx.textBaseline = 'middle';
  ctx.textAlign = 'center';
  ctx.fillText(showName, size[0] / 2, size[1] / 2);

  return cvs.toDataURL('image/jpeg', 1);
}

window.location 相关

链接追加 query 参数

import querystring from 'querystring';

/**
 * 在链接末尾追加上指定的query参数
 *
 * @param {String} link 链接
 * @param {String} queryId 参数名
 * @param {String} queryVal 参数值
 *
 * @returns 返回新的链接
 */
export const appendLinkQuery = (link: string, queryId: string, queryVal: any) => {
  const queryObj = querystring.parse(link.split('?')[1]);
  const query = querystring.stringify({ ...queryObj, [queryId]: queryVal });
  return `${link.split('?')[0]}?${query}`;
};

获取 hash 路径中需要的 key

形如:http://www.xxx.com/#/xxx?a=1&b=true&c=name

/**
 * 获取 hash 路径中需要的 queryKey 的值
 * @param {String} [key] 可选,需要的 queryKey
 * @returns {String | Object} 返回 queryKey 的值,未传 queryKey 时返回整个 query 对象
 */
export function getHashParams(queryKey?: string) {
  const params: Record<string, string> = {};
  const query = window.location.hash.split('?')[1];

  if (query) {
    const keyValuePairs = queryString.split('&');

    keyValuePairs.forEach((keyValue) => {
      const [key, value] = keyValue.split('=');
      if (key && value) {
        const decodedKey = decodeURIComponent(key);
        const decodedValue = decodeURIComponent(value);
        params[decodedKey] = decodedValue;
      }
    });
  }

  return queryKey ? params[queryKey] : params;
}

获取 hash 内部的 query 参数值

形如:http://www.xxx.com/?d=1&e=2#/xxx?a=1&b=true&c=name 从中获取 d 的值

import qs from 'querystring';

/**
 * 获取 hash 内部的 query 参数值
 * @param {String} [queryKey] 可选,需要的 queryKey
 * @returns {String | Object} 返回 queryKey 的值,未传 queryKey 时返回整个 query 对象
 */
function getHashInnerParams(queryKey?: string) {
  const search = window.location.search;
  const filterSearch = search.split(encodeURIComponent('#/'))[0];

  const hashQuery = qs.parse(filterSearch?.slice(1));

  return queryKey ? hashQuery[queryKey] : hashQuery;
}

持续更新中…