移动端H5开发-辅助函数篇

166 阅读3分钟

引言

移动端 H5 页面开发过程中会用到非常多的辅助函数,合理使用这些函数能大大提高研发效率和程序的健壮性。这里总结了一些出现频次较高的函数,包括 JS 类型判断、浏览器环境判断、获取 URL 参数、DOM处理等等。俗话说好记性不如烂笔头,把常用的代码片段记下来也可以做到温故而知新。

基础函数

创建一个纯函数的缓存版本
export function cached(fn: Function): Function {
  const cache = Object.create(null);
  return function(str: string) {
    const hit = cache[str];
    // eslint-disable-next-line no-return-assign
    return hit || (cache[str] = fn(str));
  };
}
新对象覆盖旧对象
export function copy(dest: Object, source: Object) {
  Object.keys(source).forEach(key => {
    dest[key] = source[key];
  });
}
是否为空对象
export function isEmptyObject(obj: Object) {
  try {
    return Object.getOwnPropertyNames(obj).length === 0;
  } catch (error) {
    // 在安卓 5 上此处报错,导致页面白屏
    // 此处直接抛出异常可以重现问题,但安卓 4 真机,安卓 5 真机,chrome 老版本,IE 均无法重现此问题
    // 修复方式:捕获异常后,当做参数非空返回,保障后续业务逻辑可以继续
    return false;
  }
}
是否空值
export function isEmpty(val: any) {
  return val === null || val === undefined || val === '' || isEmptyObject(val);
}
限制函数的执行频率
export function throttle({ func = () => {}, wait = 200, frequency = 200 }) {
  let timeout = null;
  let lastExecuteTime = Date.now();

  return function(...rest: any) {
    const self = this;
    const currentTime = Date.now();

    if (currentTime - lastExecuteTime > frequency) {
      lastExecuteTime = Date.now();
      func.apply(self, rest);
    } else {
      timeout = setTimeout(function() {
        lastExecuteTime = Date.now();
        clearTimeout(timeout);
        timeout = null;
      }, wait);
    }
  };
}
获取随机数(包含首尾临界值)
export function getRandomInt(min: number, max: number) {
  return Math.floor(Math.random() * (max - min + 1) + min);
}

浏览器环境判断

是否 iOS 终端
export function isiOS() {
  const ua = navigator.userAgent.toLowerCase();
  return /iphone|ipad|ipod|ios/.test(ua);
}
是否 Safari 浏览器
export function isSafari() {
  const ua = navigator.userAgent.toLowerCase();
  const result = ua.match(/version\/([\d.]+).*safari/gi) || ua.match(/(ipad).*os\s([\d_]+)/) || ua.match(/(iphone\sos)\s([\d_]+)/);
  // qq3.7 和海豚浏览器等部分浏览器 UA 中包含 safari,需排除
  if (result && ua.match(/.\*android/gi)) {
    return false;
  }
  return result;
}
是否 Android 浏览器
export function isAndroid() {
  const u = navigator.userAgent;
  return u.includes('Android') || u.includes('Adr');
}
是否微信环境
export function isWeChat() {
  return !!navigator.userAgent.match(/MicroMessenger\/([\d.]+)/i);
}
是否PC端微信环境
export function isWeChatPc() {
  return !!navigator.userAgent.match(/WindowsWechat/i);
}
是否微信小程序环境
export function isWechatMp() {
  return (
    isWeChat() &&
    (navigator.userAgent.match(/miniprogram/i) ||
      window.__wxjs_environment === 'miniprogram')
  );
};
是否QQ环境
export function isQQ() {
  return !!navigator.userAgent.match(/QQ\/([\d.]+)/);
}
是否微博环境
export function isWeibo() {
  return !!navigator.userAgent.match(/WeiBo/i);
}
是否支持webp格式
export function isSupportWebp() {
  try {
    return (
      document
        .createElement('canvas')
        .toDataURL('image/webp', 0.5)
        .indexOf('data:image/webp') === 0
    );
  } catch (err) {
    return false;
  }
}

URL 解析

获取查询字符串
export const getQueryString = cached((name: string) => {
  const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`, 'i');
  const r = window.location.search.substr(1).match(reg);
  if (r != null) return decodeURIComponent(r[2]);
  return null;
});
还原查询字符串

例如:parseUrlSearchParams({name: 'xiaowang', age: 25}) 输出 name=xiaowang&age=25

export function parseUrlSearchParams(map: Record<string, string>) {
  try {
    if (typeof URLSearchParams === 'function') {
      return new URLSearchParams(map).toString();
    }
    return Object.entries(map)
      .reduce((initial, [key, value]) => {
        return `${initial}&${key}=${value}`;
      }, '')
      .substring(1);
  } catch (err) {
    console.error(err);
    return '';
  }
}
获取url上的参数params
const channelUrlReg = /channel(?:v2)?\/(\w+)/;
// url约定 /channel/{id}
function getIdByChannel(pattern: RegExp = channelUrlReg) {
  const result = window.location.href.match(pattern);

  if (result) {
    return result[1];
  }
  return null;
}

// 例如url是:https://www.baidu.com/channel/abefd7206249e561
// getIdByChannel()输出:abefd7206249e561
获取url上的多个参数params
const singleRingUrlReg = /\/(?:sr|ring)\/(\w+)\/(\w+)/i;
// url约定 ring/{cid}/{ringid}
export function getCidAndRingIdFromUrl(pattern: RegExp = singleRingUrlReg) {
  const result = window.location.href.match(pattern);

  if (result) {
    return {
      cid: result[1],
      ringId: result[2],
    };
  }
  return null;
}

// 例如url是:https://www.baidu.com/ring/abefd7206249e561/541564145111111
// getCidAndRingIdFromUrl()输出:{cid: 'abefd7206249e561', ringId: '541564145111111'}

字符串处理

将文本中所有的数字两边添加空格
export function addSpaceBetweenNumber(htmlStr: string) {
  if (!htmlStr) {
    return htmlStr;
  }

  const htmlTagReg = /(<\s*\w+[^>]*\s*>|<\s*\/\s*\w+\s*>)/gi;
  const segements = htmlStr.split(htmlTagReg);

  return segements
    .filter(item => item.trim() !== '')
    .map(item => {
      if (htmlTagReg.test(item) || /<.*>/.test(item)) {
        return item;
      }
      return item.replace(/(\d[-\d/\\]*)/g, ' $1 ');
    })
    .join('');
}

DOM 处理

加载脚本文件
export function loadScript(url: string, callback?: () => void) {
  // 兼容服务端渲染模式
  if (!window) return;
  const scriptElem = document.createElement('script');
  if (callback) {
    scriptElem.onload = callback;
  }

  scriptElem.async = true;
  scriptElem.src = url;
  document.head.appendChild(scriptElem);
}
检测某个元素是否在 viewport 中
export function isInViewport(el: Element) {
  if (!el) {
    return true;
  }

  const { top, left, bottom, right, width, height } = el.getBoundingClientRect();
  const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
  const viewportHeight = window.innerHeight || document.documentElement.clientHeight;

  return top + height > 0 && left + width > 0 && bottom - width < viewportWidth && right - height < viewportHeight;
}
图片预加载

一般设计元素重的H5页面,我们经常看到一进页面就是带有进度条的loading页,此时其实就是在加载图片等资源,到真正渲染时能做到拿来就用不卡顿,提升用户体验。

export function preload(resArr: Array<string>, notifyProgress: Function) {
  if (!resArr || !Array.isArray(resArr) || !notifyProgress) {
    return;
  }
  let loaded = 0;
  const resNum = resArr.length;
  let isReplace = false;

  function loadEvents() {
    if (isReplace) {
      return;
    }
    loaded++;
    if (loaded === resNum) {
      isReplace = true;
    }
    // eslint-disable-next-line consistent-return
    return notifyProgress(loaded / resNum);
  }

  resArr.forEach(item => {
    const tempImg = new Image();
    tempImg.src = item;
    tempImg.onload = loadEvents;
    tempImg.onerror = loadEvents;
  });

  // 10s还没加载完时 直接结束
  setTimeout(() => {
    if (loaded < resNum) {
      isReplace = true;
      notifyProgress(1);
    }
  }, 10 * 1000);
}

使用案例

// 使用webpack内置的require.content API获取所有图片url
const req = (require as any).context('../images', true, /\.png$/);
const res = req.keys().map(req);
const resArr = res.filter(item => /images\/first_.*\.png$/.test(item));

// 传入图片url数组,和进度变化函数
preload(resArr, this.progressChange);

// 全部加载完毕进入首页
progressChange(progress: number) {
  this.progress = +(progress * 100).toFixed(0);
  if (progress === 1) {
    this.$router.replace({ name: 'home' });
  }
}

其他

获取网络类型
export function getNetworkType() {
  return ((navigator as any).connection || {}).type || 'unknown';
}
判断是否网络错误
export function isNetworkError(e: Error) {
  return /network error/i.test(e.message);
}
判断是否网络超时
export function isNetworkTimeout(e: Error) {
  return /timeout/i.test(e.message);
}