一些公共方法和组件

220 阅读3分钟

vue-helper (vue2)

merge-data

合并 vue 数据对象 VNodeData (点此处学习深入 vue 数据对象)

import { dashToHump } from "../string-helper";
import { wrapInArray } from "../array-helper";

const pattern = {
  styleList: /;(?![^(]*\))/g,
  styleProp: /:(.*)/,
};

function parseStyle(style) {
  const styleMap = {};
  for (const s of style.split(pattern.styleList)) {
    let [key, val] = s.split(pattern.styleProp);
    key = key.trim();
    if (!key) continue;
    if (typeof val === "string") val = val.trim();
    styleMap[dashToHump(key)] = val;
  }
  return styleMap;
}

export function mergeStyles(target, source) {
  if (!target) return source;
  if (!source) return target;
  target = wrapInArray(typeof target === "string" ? parseStyle(target) : target);
  return target.concat(typeof source === "string" ? parseStyle(source) : source);
}

export function mergeClasses(target, source) {
  if (!source) return target;
  if (!target) return source;
  return target ? wrapInArray(target).concat(source) : source;
}

export function mergeListeners(...args) {
  const dest = {};
  let i = args.length;
  while (i--) {
    const arg = args[i];
    if (!arg) continue;
    for (const event of Object.keys(arg)) {
      if (!arg[event]) continue;
      if (dest[event]) {
        dest[event] = wrapInArray(dest[event]).concat(arg[event]);
      } else {
        dest[event] = arg[event];
      }
    }
  }
  return dest;
}

export function mergeData(...args) {
  const mergeTarget = {};
  let i = args.length;
  while (i--) {
    const arg = args[i];
    if (!arg) continue;
    for (const prop of Object.keys(arg)) {
      switch (prop) {
        case "class":
        case "directives":
          if (!arg[prop]) break;
          mergeTarget[prop] = mergeClasses(mergeTarget[prop], arg[prop]);
          break;
        case "style":
          if (!arg[prop]) break;
          mergeTarget[prop] = mergeStyles(mergeTarget[prop], arg[prop]);
          break;
        case "staticClass":
          if (!arg[prop]) break;
          if (mergeTarget[prop] === undefined) mergeTarget[prop] = "";
          else if (mergeTarget[prop]) mergeTarget[prop] += " ";
          mergeTarget[prop] += arg[prop].trim();
          break;
        case "on":
        case "nativeOn":
          if (!arg[prop]) break;
          mergeTarget[prop] = mergeListeners(mergeTarget[prop], arg[prop]);
          break;
        case "attrs":
        case "props":
        case "domProps":
        case "scopedSlots":
        case "staticStyle":
        case "hook":
        case "transition":
          if (!arg[prop]) break;
          if (!mergeTarget[prop]) mergeTarget[prop] = {};
          mergeTarget[prop] = { ...arg[prop], ...mergeTarget[prop] };
          break;
        default:
          if (arg[prop] === undefined || mergeTarget[prop] !== undefined) break;
          mergeTarget[prop] = arg[prop];
      }
    }
  }
  return mergeTarget;
}

用法

在你想二次封装一个组件,想给内部标签一些默认的 HTML attribute 或者 props, 但又不想丢失组件原有的功能或者特性。你可以用它

函数式组件中尤其好用

const VVBtn = {
  functional: true,
  render(h, { data }) {
    const $data = mergeData(data, {
      staticClass: "vv-btn",
      style: "",
      props: {},
      directives: [],
    });
    return <Btn {...$data} />;
  },
};

composition-api-wrap

这个是用来在 vue2 的项目中引入了 composition-api,但是在 setup 中不能访问$router,$route,$store

import get from "lodash.get";
import { computed, getCurrentInstance as getVM } from "@vue/composition-api";

export function getCurrentInstance() {
  const vm = getVM();
  if (!vm) return;
  return vm.proxy;
}

export const wrapProperty =
  (property, makeComputed = true) =>
    () => {
      const vm = getCurrentInstance();
      if (!vm) throw new Error("This must be called within a setup function.");
      return makeComputed ? computed(() => get(vm, property)) : get(vm, property);
    };

export const useRouter = wrapProperty("$router", false);

export const useI18n = wrapProperty("$i18n", false);

export const t = (...args) => {
  const i18n = useI18n() ?? I18n;
  return i18n.t.call(i18n, ...args);
};

export const useRoute = wrapProperty("$route");

export const useStore = wrapProperty("$store");

export const useRouteQuery = () => {
  const route = useRoute();
  return computed(() => route.value.query);
};
export const useRouteParams = () => {
  const route = useRoute();

  return computed(() => route.value.params);
};

export const useContext = () => {
  const vm = getCurrentInstance();
  if (!vm) throw new Error("This must be called within a setup function.");

  return {
    app: vm,
    store: vm.$store,
    router: vm.$router,
    route: computed(() => vm.$route),
    query: computed(() => vm.$route.query),
    params: computed(() => vm.$route.params),
  };
};

用法

const routeParams = useRouteParams();
const id = computed(() => routeParams.value.id);
// useI18n 因为vue-i18n实现问题, 不能使用像const {t} = useI18n()
// 但你可以这样使用
const i18n = useI18n();
i18n.t("");
// 或者
t("");

getSlot

因为 vue2 作用域插槽 scopedSlots 与具名插槽 slots 是不同的实现 有了这个你在封装组件时就不需要区分是作用域插槽还是具名插槽

export function getSlot(vm, name = "default", data, optional = false) {
  if (vm.$scopedSlots[name]) {
    return vm.$scopedSlots[name](isFunction(data) ? data() : data);
  } else if (vm.$slots[name] && (!data || optional)) {
    return vm.$slots[name];
  }
  return undefined;
}

用法

const Demo = {
  name: "Demo",
  render() {
    return getSlot(this, "default", { a: 5 });
  },
};
<template>
  <demo>test 01</demo>
  <demo>
    <template #default="{ a }"> test 02 a: {{ a }}</template>
  </demo>
</template>

RenderToComp

众所周知, render 函数是可以通过混入(mixin)或者扩展(extends,Vue.extend)来组合渲染的内容 例如

const DemoA = Vue.extends({
  render() {
    return [this.renderBtnOne(), this.renderBtnTwo(), this.renderAppendBtns()]
  },
  methods: {
    renderBtnOne() {
      return <button>功能1</button>
    },
    renderBtnTwo() {
      return <button>功能2</button>
    }
    ,
    renderAppendBtns() {
    }
  }
})
const DemoB = DemoA.extends({
  methods: {
    renderBtnTwo() {
      return null
    },
    renderAppendBtns() {
      return <button>功能3</button>
    }
  }
})

但很显然模板语法是做不到上面这样的, 只能写 v-if, 我看到那 v-if 我就来气 既然在模板里不行, 那在模板里塞个 render 不就行了吗 然后就有了

export const RenderToComp = {
  props: {
    render: Function,
    slim: Boolean,
    tag: { type: String, default: "span" },
  },
  functional: true,
  render(h, { props }) {
    const { render, slim, tag } = props;
    const vNode = render?.call({ $createElement: h }, h);
    if (!vNode) return null;
    if (slim) return vNode;
    return <tag>{vNode}</tag>;
  },
};

多提一嘴:setup 里没法用 jsx, 你可以写 h()

用法

上面那个例子

  • DemoA
<template>
  <div>
    <RenderToComp :render="renderBtns" />
  </div>
</template>

<script>
export default Vue.extends({
  methods: {
    renderBtns() {
      return [this.renderBtnOne(), this.renderBtnTwo(), this.renderAppendBtns()]
    },
    renderBtnOne() {
      return <button>功能1</button>
    },
    renderBtnTwo() {
      return <button>功能2</button>
    }
    ,
    renderAppendBtns() {
    }
  }
})
</script>
// DemoB
const DemoB = DemoA.extends({
  methods: {
    renderBtnTwo() {
      return null;
    },
    renderAppendBtns() {
      return <button>功能3</button>;
    },
  },
});

WatchItem

这个还没有具体实践过,慎用! 现在想 watch 数组里的各项, 但又不想直接 watch 这个数组。 且数组增加时要能给加上的数组项加上监听

这个组件可以用来监听数组的各项(太 hack 了哈哈)

这个是利用了 vue 是已组件为细粒度的特性(说的不对的话,欢迎指正)

const WatchItem = {
  props: {
    item: null,
    watch: Function,
  },
  setup(props, { emit }) {
    const stop = watch(() => props.item, props.watch);
    emit("stop-init", { stop, item: props.item });
  },
};

用法


<template>
  <WatchItem v-for="item of items" :key="item.id" :watch="watchItem" @stop-init="" />
</template>
<script>
export default {
  data: () => ({ stopMap: {} }),
  methods: {
    watchItem(val, oldVal) {
      console.log(val, oldVal);
    },
    initStopMap({ stop, item }) {
      this.stopMap[item.id] = stop
    },
  },
};
</script>

array-helper

文件路径: src/common/utils/helper/array-helper.js

map

在有些时候, 我们对一个数组进行处理的时候, 不仅要过滤,还得生成一个新数组。

大家可能会这么写:.filter(...).map(...), 我就看这个不爽,明明我一遍循环就能解决的问题,非得搞两次 然后就有了这个

const mapSkip = Symbol("skip");

function map(iterable, mapper) {
  const result = [];
  for (let i = 0; i < iterable.length; i++) {
    const item = iterable[i];
    const element = mapper(item, i, iterable);
    if (element === mapSkip) continue;
    result.push(element);
  }
  return result;
}

用法

这里也是利用的 symbol 唯一值的特性, 你可以这么用

const arr = Array.from({ length: 10 }, (_, index) => index + 1);

// before
const arr1 = arr.filter((i) => i > 5).map((i) => i * 10);
//after
const arr2 = map(arr, (i) =>
    i > 5 ? mapSkip : (i * 10)
  )
;

listToMap

function listToMap<T = any, K = any, R = any>(
  list: T[],
  getKey: K | ((T) => K) = "id",
  getValue: (T) => T | R = (item) => item,
  map: Map<K, T | R> | any = {}
): { [K]: T | R } | Map<K, T | R> {
  return list.reduce((acc, cur) => {
    const key = isFunction(getKey) ? getKey(cur) : cur[getKey];
    const value = getValue(cur);
    Reflect.set(acc, key, value);
    return acc;
  }, map);
}

看名字就知道, 这是个可以把数组转换为映射的方法

用法

const attributes = [
  { id: "id1", value: 5 },
  { id: "id2", value: 3 },
];
const items = ["id1", "id2"];

// before 有些同学会这么写
const newItems1 = items.map((item) => {
  const findItem = attributes.find((attribute) => attribute.id === item);
  if (!findItem) return;
  return findItem.value;
});

// after
// 有了`listToMap`, 时间复杂度O(n^2)=>O(n)
const attributesMap = listToMap(attributes, "id");
const newItems2 = items.map((item) => {
  const attribute = attributesMap[item];
  if (!attribute) return;
  return attribute.value;
});

这种利用空间换时间的思路也可以用在其他地方

此外, 这个映射的格式可以是Object,也可以是MapWeakMap(键名,键值都得是对象),甚至是Object.create(null)

promise-helper

文件路径: src/common/utils/helper/promise-helper.js

pMap

github 地址: github.com/sindresorhu… p-map 适用于使用不同的输入多次运行 promise-returning 或 async 函数的场景。

它与 Promise.all 方法的区别是,你可以控制并发,也可以决定是否在出现错误时停止迭代。

const pMapSkip = Symbol("skip");

async function pMap<T = Promise | any, R = any>(
  iterable: Iterable<T>,
  mapper: (item, index) => R,
  {
    concurrency = Number.POSITIVE_INFINITY,
    stopOnError = true,
  }: {
    concurrency?: number; //—— 并发数,默认值 Infinity,最小值为 1;
    stopOnError?: boolean; //出现异常时,是否终止,默认值为 true。
  } = {}
): Promise<R> {
  return new Promise((resolve, reject) => {
    const result = [];
    const errors = [];
    const skippedIndexes = [];
    const iterator = iterable[Symbol.iterator]();
    let isRejected = false;
    let isIterableDone = false;
    let resolvingCount = 0;
    let currentIndex = 0;

    const next = () => {
      if (isRejected) return;
      const nextItem = iterator.next();
      const index = currentIndex;
      currentIndex++;
      if (nextItem.done) {
        isIterableDone = true;
        if (resolvingCount === 0) {
          if (stopOnError && errors.length > 0) {
            reject(errors);
          } else {
            skippedIndexes.sort((a, b) => b - a);
            for (const skippedIndex of skippedIndexes) {
              result.splice(skippedIndex, 1);
            }
            resolve(result);
          }
        }
        return;
      }
      resolvingCount++;
      (async () => {
        try {
          const element = await nextItem.value;
          if (isRejected) return;
          const value = await mapper(element, index);
          if (value === pMapSkip) {
            skippedIndexes.push(index);
          } else {
            result[index] = value;
          }

          resolvingCount--;
          next();
        } catch (e) {
          if (stopOnError) {
            isRejected = true;
            reject(e);
          } else {
            errors.push(e);
            resolvingCount--;
            next();
          }
        }
      })();
    };
    for (let index = 0; index < concurrency; index++) {
      next();
      if (isIterableDone) break;
    }
  });
}

用法:

它与上述的 map 一样,提供了一个 pMapSkip, 可以直接移除对应索引值

const inputs = [200, 100, pMapSkip];
const mapper = (value) => delay(value, { value });

async function main() {
  console.time("start");
  const result = await pMap(inputs, mapper, { concurrency: 1 });
  console.dir(result); // 输出结果:[ 200, 100 ]
  console.timeEnd("start"); //start: 368.708ms
}

main();

而当把 concurrency 属性的值更改为 2 之后,再次执行以上代码。那么命令行将会输出以下信息:

[ 200, 100 ]
start: 210.322ms

pAll

该模块提供的功能,与 Promise.all API 类似,主要的区别是该模块允许你限制任务的并发数。 如果数组里有函数且 runInFunction 为 true, 可以自动帮你执行

export async function pAll(
  iterable,
  { runInFunction = true, concurrency = Number.POSITIVE_INFINITY, stopOnError = true } = {}
) {
  return pMap(
    iterable,
    (element) => {
      if (!runInFunction) return element;
      return isFunction(element) ? element() : element;
    },
    { concurrency, stopOnError }
  );
}

用法

const inputs = [
  () => delay(200, { value: 1 }),
  async () => {
    await delay(100);
    return 2;
  },
  async () => 8,
];

async function main() {
  console.time("start");
  const result = await pAll(inputs, { concurrency: 1 });
  console.dir(result); // 输出结果:[ 1, 2, 8 ]
  console.timeEnd("start");
}

main();

awaitTo

try-catch 包裹器(异步)

function to(
  promise: Promise,
  {
    notifyError = true,
    errorExt = {},
    notifyOptions = {},
    initialValue = undefined,
    loading = false,
  }: ?{
    notifyError?: boolean | string;
    notifyOptions?: any;
    errorExt?: any;
    initialValue?: any;
    loading?: boolean;
  } = {}
): Promise<[Error, any]> {
  loading && $loading.show();
  return promise
    .then((data) => [null, data])
    .catch((err) => {
      if (errorExt) Object.assign(err, errorExt);
      if (notifyError) {
        notifyErrorByHttpCode(isString(notifyError) ? notifyError : err, notifyOptions);
        console.warn(err);
      }
      return [err, initialValue];
    })
    .finally(() => {
      loading && $loading.hide();
    });
}

用法

const [err, { data }] = await awaitTo(request.get(``));

cached

函数返回值缓存是优化一个函数的常用手段。(同步, 异步皆可) 我们可以将函数、输入参数、返回值全部保存起来,当下次以同样的参数调用这个函数时,直接使用存储的结果作为返回(不需要重新计算)。

这种方法是有代价的,我们实际是在用内存空间换取运行时间。此方法中使用了 LRUCache(Least Recently Used, 最近最少使用)来优化存储空间

注意: 如果是请求, 请谨慎使用, 如果前端的数据和后端数据不一致,请求还一直拿缓存的值就玩大发了 例如像类目列表, 品牌列表等很久才会更新, 请求频率高且数据量大的才考虑用(ps: 整个协商缓存多好, 还得前端在这想办法)

const generateKey = (arg) => {
  if (isString(arg)) return arg;
  else if (Array.isArray(arg))
    return arg.reduce((acc, cur) => (acc ? `${acc},${generateKey(cur)}` : generateKey(cur)), "");
  else if (isRealObject(arg)) {
    const keys = Object.keys(arg).sort();
    const res = keys.reduce((acc, cur) => {
      const value = arg[cur];
      return acc ? `${acc},${cur}=${generateKey(value)}` : `${cur}=${generateKey(value)}`;
    }, "");
    return `{${res}}`;
  }
  return String(arg);
};

export function cached<T>(func: T, capacity: number = 100): T {
  const cache = new LRUCache(capacity);
  return async function(...args) {
    const key = generateKey(args);
    const target = cache.get(key);
    if (target) {
      if (!target?.then) return target;
      const [err, data] = await to(target);
      if (!err) return data;
    }
    const result = func(...args);
    cache.put(key, result);
    return result;
  };
}

用法

const getData = () => request.get(``);
const cacheGetData = cached(getData);
cacheGetData();
//cacheGetData 与 detData 行为一致