Code Monkey home page Code Monkey logo

blogfn's People

Contributors

roger-hiro avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

blogfn's Issues

JavaScript 工具函数大全(ES6版)

前言

一线大厂笔试题灵感来源

目录:

  • 第一部分:数组
  • 第二部分:函数,
  • 第三部分:字符串
  • 第四部分:对象
  • 第五部分:数字
  • 第六部分:浏览器操作及其它

筛选自以下两篇文章:

原本只想筛选下上面的那篇文章,在精简掉了部分多余且无用的代码片段后,感觉不够。于是顺藤摸瓜,找到了原地址:

30 seconds of code

然后将所有代码段都看了遍,筛选了以下一百多段代码片段,并加入了部分自己的理解。

另外,本文工具函数的命名非常值得借鉴。

1. 第一部分:数组

1. all:布尔全等判断

const all = (arr, fn = Boolean) => arr.every(fn);

all([4, 2, 3], x => x > 1); // true
all([1, 2, 3]); // true

2. allEqual:检查数组各项相等

const allEqual = arr => arr.every(val => val === arr[0]);

allEqual([1, 2, 3, 4, 5, 6]); // false
allEqual([1, 1, 1, 1]); // true

3.approximatelyEqual:约等于

const approximatelyEqual = (v1, v2, epsilon = 0.001) => Math.abs(v1 - v2) < epsilon;

approximatelyEqual(Math.PI / 2.0, 1.5708); // true

4.arrayToCSV:数组转CSV格式(带空格的字符串)


const arrayToCSV = (arr, delimiter = ',') =>
  arr.map(v => v.map(x => `"${x}"`).join(delimiter)).join('\n');
  
arrayToCSV([['a', 'b'], ['c', 'd']]); // '"a","b"\n"c","d"'
arrayToCSV([['a', 'b'], ['c', 'd']], ';'); // '"a";"b"\n"c";"d"'

5.arrayToHtmlList:数组转li列表

此代码段将数组的元素转换为<li>标签,并将其附加到给定ID的列表中。

const arrayToHtmlList = (arr, listID) =>
  (el => (
    (el = document.querySelector('#' + listID)),
    (el.innerHTML += arr.map(item => `<li>${item}</li>`).join(''))
  ))();
  
arrayToHtmlList(['item 1', 'item 2'], 'myListID');

6. average:平均数

const average = (...nums) => nums.reduce((acc, val) => acc + val, 0) / nums.length;
average(...[1, 2, 3]); // 2
average(1, 2, 3); // 2

7. averageBy:数组对象属性平均数

此代码段将获取数组对象属性的平均值

const averageBy = (arr, fn) =>
  arr.map(typeof fn === 'function' ? fn : val => val[fn]).reduce((acc, val) => acc + val, 0) /
  arr.length;
  
averageBy([{ n: 4 }, { n: 2 }, { n: 8 }, { n: 6 }], o => o.n); // 5
averageBy([{ n: 4 }, { n: 2 }, { n: 8 }, { n: 6 }], 'n'); // 5

8.bifurcate:拆分断言后的数组

可以根据每个元素返回的值,使用reduce() push() 将元素添加到第二次参数fn中 。

const bifurcate = (arr, filter) =>
  arr.reduce((acc, val, i) => (acc[filter[i] ? 0 : 1].push(val), acc), [[], []]);
bifurcate(['beep', 'boop', 'foo', 'bar'], [true, true, false, true]); 
// [ ['beep', 'boop', 'bar'], ['foo'] ]

9. castArray:其它类型转数组

const castArray = val => (Array.isArray(val) ? val : [val]);

castArray('foo'); // ['foo']
castArray([1]); // [1]
castArray(1); // [1]

10. compact:去除数组中的无效/无用值

const compact = arr => arr.filter(Boolean);

compact([0, 1, false, 2, '', 3, 'a', 'e' * 23, NaN, 's', 34]); 
// [ 1, 2, 3, 'a', 's', 34 ]

11. countOccurrences:检测数值出现次数

const countOccurrences = (arr, val) => arr.reduce((a, v) => (v === val ? a + 1 : a), 0);
countOccurrences([1, 1, 2, 1, 2, 3], 1); // 3

12. deepFlatten:递归扁平化数组

const deepFlatten = arr => [].concat(...arr.map(v => (Array.isArray(v) ? deepFlatten(v) : v)));

deepFlatten([1, [2], [[3], 4], 5]); // [1,2,3,4,5]

13. difference:寻找差异

此代码段查找两个数组之间的差异。


const difference = (a, b) => {
  const s = new Set(b);
  return a.filter(x => !s.has(x));
};

difference([1, 2, 3], [1, 2, 4]); // [3]

14. differenceBy:先执行再寻找差异

在将给定函数应用于两个列表的每个元素之后,此方法返回两个数组之间的差异。

const differenceBy = (a, b, fn) => {
  const s = new Set(b.map(fn));
  return a.filter(x => !s.has(fn(x)));
};

differenceBy([2.1, 1.2], [2.3, 3.4], Math.floor); // [1.2]
differenceBy([{ x: 2 }, { x: 1 }], [{ x: 1 }], v => v.x); // [ { x: 2 } ]

15. dropWhile:删除不符合条件的值

此代码段从数组顶部开始删除元素,直到传递的函数返回为true

const dropWhile = (arr, func) => {
  while (arr.length > 0 && !func(arr[0])) arr = arr.slice(1);
  return arr;
};

dropWhile([1, 2, 3, 4], n => n >= 3); // [3,4]

16. flatten:指定深度扁平化数组

此代码段第二参数可指定深度。

const flatten = (arr, depth = 1) =>
  arr.reduce((a, v) => a.concat(depth > 1 && Array.isArray(v) ? flatten(v, depth - 1) : v), []);

flatten([1, [2], 3, 4]); // [1, 2, 3, 4]
flatten([1, [2, [3, [4, 5], 6], 7], 8], 2); // [1, 2, 3, [4, 5], 6, 7, 8]

17. indexOfAll:返回数组中某值的所有索引

此代码段可用于获取数组中某个值的所有索引,如果此值中未包含该值,则返回一个空数组。

const indexOfAll = (arr, val) => arr.reduce((acc, el, i) => (el === val ? [...acc, i] : acc), []);

indexOfAll([1, 2, 3, 1, 2, 3], 1); // [0,3]
indexOfAll([1, 2, 3], 4); // []

18. intersection:两数组的交集


const intersection = (a, b) => {
  const s = new Set(b);
  return a.filter(x => s.has(x));
};

intersection([1, 2, 3], [4, 3, 2]); // [2, 3]

19. intersectionWith:两数组都符合条件的交集

此片段可用于在对两个数组的每个元素执行了函数之后,返回两个数组中存在的元素列表。


const intersectionBy = (a, b, fn) => {
  const s = new Set(b.map(fn));
  return a.filter(x => s.has(fn(x)));
};

intersectionBy([2.1, 1.2], [2.3, 3.4], Math.floor); // [2.1]

20. intersectionWith:先比较后返回交集

const intersectionWith = (a, b, comp) => a.filter(x => b.findIndex(y => comp(x, y)) !== -1);

intersectionWith([1, 1.2, 1.5, 3, 0], [1.9, 3, 0, 3.9], (a, b) => Math.round(a) === Math.round(b)); // [1.5, 3, 0]

21. minN:返回指定长度的升序数组

const minN = (arr, n = 1) => [...arr].sort((a, b) => a - b).slice(0, n);

minN([1, 2, 3]); // [1]
minN([1, 2, 3], 2); // [1,2]

22. negate:根据条件反向筛选


const negate = func => (...args) => !func(...args);

[1, 2, 3, 4, 5, 6].filter(negate(n => n % 2 === 0)); // [ 1, 3, 5 ]

23. randomIntArrayInRange:生成两数之间指定长度的随机数组

const randomIntArrayInRange = (min, max, n = 1) =>
  Array.from({ length: n }, () => Math.floor(Math.random() * (max - min + 1)) + min);
  
randomIntArrayInRange(12, 35, 10); // [ 34, 14, 27, 17, 30, 27, 20, 26, 21, 14 ]

24. sample:在指定数组中获取随机数

const sample = arr => arr[Math.floor(Math.random() * arr.length)];

sample([3, 7, 9, 11]); // 9

25. sampleSize:在指定数组中获取指定长度的随机数

此代码段可用于从数组中获取指定长度的随机数,直至穷尽数组。
使用Fisher-Yates算法对数组中的元素进行随机选择。

const sampleSize = ([...arr], n = 1) => {
  let m = arr.length;
  while (m) {
    const i = Math.floor(Math.random() * m--);
    [arr[m], arr[i]] = [arr[i], arr[m]];
  }
  return arr.slice(0, n);
};

sampleSize([1, 2, 3], 2); // [3,1]
sampleSize([1, 2, 3], 4); // [2,3,1]

26. shuffle:“洗牌” 数组

此代码段使用Fisher-Yates算法随机排序数组的元素。


const shuffle = ([...arr]) => {
  let m = arr.length;
  while (m) {
    const i = Math.floor(Math.random() * m--);
    [arr[m], arr[i]] = [arr[i], arr[m]];
  }
  return arr;
};

const foo = [1, 2, 3];
shuffle(foo); // [2, 3, 1], foo = [1, 2, 3]

27. nest:根据parent_id生成树结构(阿里一面真题)

根据每项的parent_id,生成具体树形结构的对象。

const nest = (items, id = null, link = 'parent_id') =>
  items
    .filter(item => item[link] === id)
    .map(item => ({ ...item, children: nest(items, item.id) }));

用法:

const comments = [
  { id: 1, parent_id: null },
  { id: 2, parent_id: 1 },
  { id: 3, parent_id: 1 },
  { id: 4, parent_id: 2 },
  { id: 5, parent_id: 4 }
];
const nestedComments = nest(comments); // [{ id: 1, parent_id: null, children: [...] }]


强烈建议去理解这个的实现,因为这是我亲身遇到的阿里一面真题:

2. 第二部分:函数

1.attempt:捕获函数运行异常

该代码段执行一个函数,返回结果或捕获的错误对象。

onst attempt = (fn, ...args) => {
  try {
    return fn(...args);
  } catch (e) {
    return e instanceof Error ? e : new Error(e);
  }
};
var elements = attempt(function(selector) {
  return document.querySelectorAll(selector);
}, '>_>');
if (elements instanceof Error) elements = []; // elements = []

2. defer:推迟执行

此代码段延迟了函数的执行,直到清除了当前调用堆栈。

const defer = (fn, ...args) => setTimeout(fn, 1, ...args);

defer(console.log, 'a'), console.log('b'); // logs 'b' then 'a'

3. runPromisesInSeries:运行多个Promises

const runPromisesInSeries = ps => ps.reduce((p, next) => p.then(next), Promise.resolve());
const delay = d => new Promise(r => setTimeout(r, d));

runPromisesInSeries([() => delay(1000), () => delay(2000)]);
//依次执行每个Promises ,总共需要3秒钟才能完成

4. timeTaken:计算函数执行时间


const timeTaken = callback => {
  console.time('timeTaken');
  const r = callback();
  console.timeEnd('timeTaken');
  return r;
};

timeTaken(() => Math.pow(2, 10)); // 1024, (logged): timeTaken: 0.02099609375ms

5. createEventHub:简单的发布/订阅模式

创建一个发布/订阅(发布-订阅)事件集线,有emitonoff方法。

  1. 使用Object.create(null)创建一个空的hub对象。
  2. emit,根据event参数解析处理程序数组,然后.forEach()通过传入数据作为参数来运行每个处理程序。
  3. on,为事件创建一个数组(若不存在则为空数组),然后.push()将处理程序添加到该数组。
  4. off,用.findIndex()在事件数组中查找处理程序的索引,并使用.splice()删除。
const createEventHub = () => ({
  hub: Object.create(null),
  emit(event, data) {
    (this.hub[event] || []).forEach(handler => handler(data));
  },
  on(event, handler) {
    if (!this.hub[event]) this.hub[event] = [];
    this.hub[event].push(handler);
  },
  off(event, handler) {
    const i = (this.hub[event] || []).findIndex(h => h === handler);
    if (i > -1) this.hub[event].splice(i, 1);
    if (this.hub[event].length === 0) delete this.hub[event];
  }
});

用法:

const handler = data => console.log(data);
const hub = createEventHub();
let increment = 0;

// 订阅,监听不同事件
hub.on('message', handler);
hub.on('message', () => console.log('Message event fired'));
hub.on('increment', () => increment++);

// 发布:发出事件以调用所有订阅给它们的处理程序,并将数据作为参数传递给它们
hub.emit('message', 'hello world'); // 打印 'hello world' 和 'Message event fired'
hub.emit('message', { hello: 'world' }); // 打印 对象 和 'Message event fired'
hub.emit('increment'); // increment = 1

// 停止订阅
hub.off('message', handler);

6.memoize:缓存函数

通过实例化一个Map对象来创建一个空的缓存。

通过检查输入值的函数输出是否已缓存,返回存储一个参数的函数,该参数将被提供给已记忆的函数;如果没有,则存储并返回它。

const memoize = fn => {
  const cache = new Map();
  const cached = function(val) {
    return cache.has(val) ? cache.get(val) : cache.set(val, fn.call(this, val)) && cache.get(val);
  };
  cached.cache = cache;
  return cached;
};

Ps: 这个版本可能不是很清晰,还有Vue源码版的:

/**
 * Create a cached version of a pure function.
 */
export function cached<F: Function> (fn: F): F {
  const cache = Object.create(null)
  return (function cachedFn (str: string) {
    const hit = cache[str]
    return hit || (cache[str] = fn(str))
  }: any)
}

7. once:只调用一次的函数

const once = fn => {
  let called = false
  return function () {
    if (!called) {
      called = true
      fn.apply(this, arguments)
    }
  }
};

8.flattenObject:以键的路径扁平化对象

使用递归。

  1. 利用Object.keys(obj)联合Array.prototype.reduce(),以每片叶子节点转换为扁平的路径节点。
  2. 如果键的值是一个对象,则函数使用调用适当的自身prefix以创建路径Object.assign()
  3. 否则,它将适当的前缀键值对添加到累加器对象。
  4. prefix除非你希望每个键都有一个前缀,否则应始终省略第二个参数。
const flattenObject = (obj, prefix = '') =>
  Object.keys(obj).reduce((acc, k) => {
    const pre = prefix.length ? prefix + '.' : '';
    if (typeof obj[k] === 'object') Object.assign(acc, flattenObject(obj[k], pre + k));
    else acc[pre + k] = obj[k];
    return acc;
  }, {});
  
flattenObject({ a: { b: { c: 1 } }, d: 1 }); // { 'a.b.c': 1, d: 1 }

9. unflattenObject:以键的路径展开对象

与上面的相反,展开对象。

const unflattenObject = obj =>
  Object.keys(obj).reduce((acc, k) => {
    if (k.indexOf('.') !== -1) {
      const keys = k.split('.');
      Object.assign(
        acc,
        JSON.parse(
          '{' +
            keys.map((v, i) => (i !== keys.length - 1 ? `"${v}":{` : `"${v}":`)).join('') +
            obj[k] +
            '}'.repeat(keys.length)
        )
      );
    } else acc[k] = obj[k];
    return acc;
  }, {});
  
unflattenObject({ 'a.b.c': 1, d: 1 }); // { a: { b: { c: 1 } }, d: 1 }

这个的用途,在做Tree组件或复杂表单时取值非常舒服。

3. 第三部分:字符串

1.byteSize:返回字符串的字节长度

const byteSize = str => new Blob([str]).size;

byteSize('😀'); // 4
byteSize('Hello World'); // 11

2. capitalize:首字母大写

const capitalize = ([first, ...rest]) =>
  first.toUpperCase() + rest.join('');
  
capitalize('fooBar'); // 'FooBar'
capitalize('fooBar', true); // 'Foobar'

3. capitalizeEveryWord:每个单词首字母大写

const capitalizeEveryWord = str => str.replace(/\b[a-z]/g, char => char.toUpperCase());

capitalizeEveryWord('hello world!'); // 'Hello World!'

4. decapitalize:首字母小写

const decapitalize = ([first, ...rest]) =>
  first.toLowerCase() + rest.join('')

decapitalize('FooBar'); // 'fooBar'
decapitalize('FooBar'); // 'fooBar'

5. luhnCheck:银行卡号码校验(luhn算法)

Luhn算法的实现,用于验证各种标识号,例如信用卡号,IMEI号,国家提供商标识号等。

String.prototype.split('')结合使用,以获取数字数组。获得最后一个数字。实施luhn算法。如果被整除,则返回,否则返回。

const luhnCheck = num => {
  let arr = (num + '')
    .split('')
    .reverse()
    .map(x => parseInt(x));
  let lastDigit = arr.splice(0, 1)[0];
  let sum = arr.reduce((acc, val, i) => (i % 2 !== 0 ? acc + val : acc + ((val * 2) % 9) || 9), 0);
  sum += lastDigit;
  return sum % 10 === 0;
};

用例:

luhnCheck('4485275742308327'); // true
luhnCheck(6011329933655299); //  false
luhnCheck(123456789); // false

补充:银行卡号码的校验规则

关于luhn算法,可以参考以下文章:

银行卡号码校验算法(Luhn算法,又叫模10算法)

银行卡号码的校验采用Luhn算法,校验过程大致如下:

  1. 从右到左给卡号字符串编号,最右边第一位是1,最右边第二位是2,最右边第三位是3….

  2. 从右向左遍历,对每一位字符t执行第三个步骤,并将每一位的计算结果相加得到一个数s。

  3. 对每一位的计算规则:如果这一位是奇数位,则返回t本身,如果是偶数位,则先将t乘以2得到一个数n,如果n是一位数(小于10),直接返回n,否则将n的个位数和十位数相加返回。

  4. 如果s能够整除10,则此号码有效,否则号码无效。

因为最终的结果会对10取余来判断是否能够整除10,所以又叫做模10算法。

当然,还是库比较香: bankcardinfo

6. splitLines:将多行字符串拆分为行数组。

使用String.prototype.split()和正则表达式匹配换行符并创建一个数组。

const splitLines = str => str.split(/\r?\n/);

splitLines('This\nis a\nmultiline\nstring.\n'); // ['This', 'is a', 'multiline', 'string.' , '']

7. stripHTMLTags:删除字符串中的HTMl标签

从字符串中删除HTML / XML标签。

使用正则表达式从字符串中删除HTML / XML 标记。

const stripHTMLTags = str => str.replace(/<[^>]*>/g, '');

stripHTMLTags('<p><em>lorem</em> <strong>ipsum</strong></p>'); // 'lorem ipsum'

4. 第四部分:对象

1. dayOfYear:当前日期天数

const dayOfYear = date =>
  Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24);

dayOfYear(new Date()); // 285

2. forOwn:迭代属性并执行回调

const forOwn = (obj, fn) => Object.keys(obj).forEach(key => fn(obj[key], key, obj));
forOwn({ foo: 'bar', a: 1 }, v => console.log(v)); // 'bar', 1

3. Get Time From Date:返回当前24小时制时间的字符串

const getColonTimeFromDate = date => date.toTimeString().slice(0, 8);

getColonTimeFromDate(new Date()); // "08:38:00"

4. Get Days Between Dates:返回日期间的天数

const getDaysDiffBetweenDates = (dateInitial, dateFinal) =>
  (dateFinal - dateInitial) / (1000 * 3600 * 24);
  
getDaysDiffBetweenDates(new Date('2019-01-01'), new Date('2019-10-14')); // 286

5. is:检查值是否为特定类型。

const is = (type, val) => ![, null].includes(val) && val.constructor === type;

is(Array, [1]); // true
is(ArrayBuffer, new ArrayBuffer()); // true
is(Map, new Map()); // true
is(RegExp, /./g); // true
is(Set, new Set()); // true
is(WeakMap, new WeakMap()); // true
is(WeakSet, new WeakSet()); // true
is(String, ''); // true
is(String, new String('')); // true
is(Number, 1); // true
is(Number, new Number(1)); // true
is(Boolean, true); // true
is(Boolean, new Boolean(true)); // true

6. isAfterDate:检查是否在某日期后

const isAfterDate = (dateA, dateB) => dateA > dateB;

isAfterDate(new Date(2010, 10, 21), new Date(2010, 10, 20)); // true

7. isBeforeDate:检查是否在某日期前

const isBeforeDate = (dateA, dateB) => dateA < dateB;

isBeforeDate(new Date(2010, 10, 20), new Date(2010, 10, 21)); // true

8 tomorrow:获取明天的字符串格式时间


const tomorrow = () => {
  let t = new Date();
  t.setDate(t.getDate() + 1);
  return t.toISOString().split('T')[0];
};

tomorrow(); // 2019-10-15 (如果明天是2019-10-15)

9. equals:全等判断

在两个变量之间进行深度比较以确定它们是否全等。

此代码段精简的核心在于Array.prototype.every()的使用。

const equals = (a, b) => {
  if (a === b) return true;
  if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
  if (!a || !b || (typeof a !== 'object' && typeof b !== 'object')) return a === b;
  if (a.prototype !== b.prototype) return false;
  let keys = Object.keys(a);
  if (keys.length !== Object.keys(b).length) return false;
  return keys.every(k => equals(a[k], b[k]));
};

用法:

equals({ a: [2, { e: 3 }], b: [4], c: 'foo' }, { a: [2, { e: 3 }], b: [4], c: 'foo' }); // true

5. 第五部分:数字

1. randomIntegerInRange:生成指定范围的随机整数

const randomIntegerInRange = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;

randomIntegerInRange(0, 5); // 3

2. randomNumberInRange:生成指定范围的随机小数

const randomNumberInRange = (min, max) => Math.random() * (max - min) + min;

randomNumberInRange(2, 10); // 6.0211363285087005

3. round:四舍五入到指定位数

const round = (n, decimals = 0) => Number(`${Math.round(`${n}e${decimals}`)}e-${decimals}`);

round(1.005, 2); // 1.01

4. sum:计算数组或多个数字的总和


const sum = (...arr) => [...arr].reduce((acc, val) => acc + val, 0);

sum(1, 2, 3, 4); // 10
sum(...[1, 2, 3, 4]); // 10

5. toCurrency:简单的货币单位转换

const toCurrency = (n, curr, LanguageFormat = undefined) =>
  Intl.NumberFormat(LanguageFormat, { style: 'currency', currency: curr }).format(n);
  
toCurrency(123456.789, 'EUR'); // €123,456.79
toCurrency(123456.789, 'USD', 'en-us'); // $123,456.79  
toCurrency(123456.789, 'USD', 'fa'); // ۱۲۳٬۴۵۶٫۷۹
toCurrency(322342436423.2435, 'JPY'); // ¥322,342,436,423 

6. 第六部分:浏览器操作及其它

1. bottomVisible:检查页面底部是否可见

const bottomVisible = () =>
  document.documentElement.clientHeight + window.scrollY >=
  (document.documentElement.scrollHeight || document.documentElement.clientHeight);

bottomVisible(); // true

2. Create Directory:检查创建目录

此代码段调用fs模块的existsSync()检查目录是否存在,如果不存在,则mkdirSync()创建该目录。

const fs = require('fs');
const createDirIfNotExists = dir => (!fs.existsSync(dir) ? fs.mkdirSync(dir) : undefined);
createDirIfNotExists('test'); 

3. currentURL:返回当前链接url

const currentURL = () => window.location.href;

currentURL(); // 'https://juejin.im'

4. distance:返回两点间的距离

该代码段通过计算欧几里得距离来返回两点之间的距离。

const distance = (x0, y0, x1, y1) => Math.hypot(x1 - x0, y1 - y0);

distance(1, 1, 2, 3); // 2.23606797749979

5. elementContains:检查是否包含子元素

此代码段检查父元素是否包含子元素。

const elementContains = (parent, child) => parent !== child && parent.contains(child);

elementContains(document.querySelector('head'), document.querySelector('title')); // true
elementContains(document.querySelector('body'), document.querySelector('body')); // false

6. getStyle:返回指定元素的生效样式

const getStyle = (el, ruleName) => getComputedStyle(el)[ruleName];

getStyle(document.querySelector('p'), 'font-size'); // '16px'

7. getType:返回值或变量的类型名

const getType = v =>
  v === undefined ? 'undefined' : v === null ? 'null' : v.constructor.name.toLowerCase();
  
getType(new Set([1, 2, 3])); // 'set'
getType([1, 2, 3]); // 'array'

8. hasClass:校验指定元素的类名

const hasClass = (el, className) => el.classList.contains(className);
hasClass(document.querySelector('p.special'), 'special'); // true

9. hide:隐藏所有的指定标签

const hide = (...el) => [...el].forEach(e => (e.style.display = 'none'));

hide(document.querySelectorAll('img')); // 隐藏所有<img>标签

10. httpsRedirectHTTP 跳转 HTTPS

const httpsRedirect = () => {
  if (location.protocol !== 'https:') location.replace('https://' + location.href.split('//')[1]);
};

httpsRedirect(); // 若在`http://www.baidu.com`, 则跳转到`https://www.baidu.com`

11.insertAfter:在指定元素之后插入新元素

const insertAfter = (el, htmlString) => el.insertAdjacentHTML('afterend', htmlString);

// <div id="myId">...</div> <p>after</p>
insertAfter(document.getElementById('myId'), '<p>after</p>'); 

12.insertBefore:在指定元素之前插入新元素

const insertBefore = (el, htmlString) => el.insertAdjacentHTML('beforebegin', htmlString);

insertBefore(document.getElementById('myId'), '<p>before</p>'); // <p>before</p> <div id="myId">...</div>

13. isBrowser:检查是否为浏览器环境

此代码段可用于确定当前运行时环境是否为浏览器。这有助于避免在服务器(节点)上运行前端模块时出错。

const isBrowser = () => ![typeof window, typeof document].includes('undefined');

isBrowser(); // true (browser)
isBrowser(); // false (Node)

14. isBrowserTab:检查当前标签页是否活动

const isBrowserTabFocused = () => !document.hidden;

isBrowserTabFocused(); // true

15. nodeListToArray:转换nodeList为数组

const nodeListToArray = nodeList => [...nodeList];

nodeListToArray(document.childNodes); // [ <!DOCTYPE html>, html ]

16. Random Hexadecimal Color Code:随机十六进制颜色


const randomHexColorCode = () => {
  let n = (Math.random() * 0xfffff * 1000000).toString(16);
  return '#' + n.slice(0, 6);
};

randomHexColorCode(); // "#e34155"

17. scrollToTop:平滑滚动至顶部

该代码段可用于平滑滚动到当前页面的顶部。

const scrollToTop = () => {
  const c = document.documentElement.scrollTop || document.body.scrollTop;
  if (c > 0) {
    window.requestAnimationFrame(scrollToTop);
    window.scrollTo(0, c - c / 8);
  }
};

scrollToTop();

18. smoothScroll:滚动到指定元素区域

该代码段可将指定元素平滑滚动到浏览器窗口的可见区域。

const smoothScroll = element =>
  document.querySelector(element).scrollIntoView({
    behavior: 'smooth'
  });
  
smoothScroll('#fooBar'); 
smoothScroll('.fooBar'); 

19. detectDeviceType:检测移动/PC设备

const detectDeviceType = () =>
  /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
    ? 'Mobile'
    : 'Desktop';

20. getScrollPosition:返回当前的滚动位置

默认参数为windowpageXOffset(pageYOffset)为第一选择,没有则用scrollLeft(scrollTop)

const getScrollPosition = (el = window) => ({
  x: el.pageXOffset !== undefined ? el.pageXOffset : el.scrollLeft,
  y: el.pageYOffset !== undefined ? el.pageYOffset : el.scrollTop
});

getScrollPosition(); // {x: 0, y: 200}

21. size:获取不同类型变量的长度

这个的实现非常巧妙,利用Blob类文件对象的特性,获取对象的长度。

另外,多重三元运算符,是真香。

const size = val =>
  Array.isArray(val)
    ? val.length
    : val && typeof val === 'object'
    ? val.size || val.length || Object.keys(val).length
    : typeof val === 'string'
    ? new Blob([val]).size
    : 0;

size([1, 2, 3, 4, 5]); // 5
size('size'); // 4
size({ one: 1, two: 2, three: 3 }); // 3

22. escapeHTML:转义HTML

当然是用来防XSS攻击啦。

const escapeHTML = str =>
  str.replace(
    /[&<>'"]/g,
    tag =>
      ({
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        "'": '&#39;',
        '"': '&quot;'
      }[tag] || tag)
  );

escapeHTML('<a href="#">Me & you</a>'); // '&lt;a href=&quot;#&quot;&gt;Me &amp; you&lt;/a&gt;'

❤️ 看完三件事

如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

  1. 点赞,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)
  2. 关注公众号「前端劝退师」,不定期分享原创知识。
  3. 也看看其它文章

## 前言
一线大厂笔试题灵感来源

目录:

  • 第一部分:数组,27段
  • 第二部分:函数,9段
  • 第三部分:字符串,7段
  • 第四部分:对象,9段
  • 第五部分:数字,5段
  • 第六部分:浏览器操作及其它,22段

筛选自以下两篇文章:

原本只想筛选下上面的那篇文章,在精简掉了部分多余且无用的代码片段后,感觉不够。于是顺藤摸瓜,找到了原地址:

30 seconds of code

然后将所有代码段都看了遍,筛选了以下一百多段代码片段,并加入了部分自己的理解。

另外,本文工具函数的命名非常值得借鉴。

1. 第一部分:数组

1. all:布尔全等判断

const all = (arr, fn = Boolean) => arr.every(fn);

all([4, 2, 3], x => x > 1); // true
all([1, 2, 3]); // true

2. allEqual:检查数组各项相等

const allEqual = arr => arr.every(val => val === arr[0]);

allEqual([1, 2, 3, 4, 5, 6]); // false
allEqual([1, 1, 1, 1]); // true

3.approximatelyEqual:约等于

const approximatelyEqual = (v1, v2, epsilon = 0.001) => Math.abs(v1 - v2) < epsilon;

approximatelyEqual(Math.PI / 2.0, 1.5708); // true

4.arrayToCSV:数组转CSV格式(带空格的字符串)


const arrayToCSV = (arr, delimiter = ',') =>
  arr.map(v => v.map(x => `"${x}"`).join(delimiter)).join('\n');
  
arrayToCSV([['a', 'b'], ['c', 'd']]); // '"a","b"\n"c","d"'
arrayToCSV([['a', 'b'], ['c', 'd']], ';'); // '"a";"b"\n"c";"d"'

5.arrayToHtmlList:数组转li列表

此代码段将数组的元素转换为<li>标签,并将其附加到给定ID的列表中。

const arrayToHtmlList = (arr, listID) =>
  (el => (
    (el = document.querySelector('#' + listID)),
    (el.innerHTML += arr.map(item => `<li>${item}</li>`).join(''))
  ))();
  
arrayToHtmlList(['item 1', 'item 2'], 'myListID');

6. average:平均数

const average = (...nums) => nums.reduce((acc, val) => acc + val, 0) / nums.length;
average(...[1, 2, 3]); // 2
average(1, 2, 3); // 2

7. averageBy:数组对象属性平均数

此代码段将获取数组对象属性的平均值

const averageBy = (arr, fn) =>
  arr.map(typeof fn === 'function' ? fn : val => val[fn]).reduce((acc, val) => acc + val, 0) /
  arr.length;
  
averageBy([{ n: 4 }, { n: 2 }, { n: 8 }, { n: 6 }], o => o.n); // 5
averageBy([{ n: 4 }, { n: 2 }, { n: 8 }, { n: 6 }], 'n'); // 5

8.bifurcate:拆分断言后的数组

可以根据每个元素返回的值,使用reduce() push() 将元素添加到第二次参数fn中 。

const bifurcate = (arr, filter) =>
  arr.reduce((acc, val, i) => (acc[filter[i] ? 0 : 1].push(val), acc), [[], []]);
bifurcate(['beep', 'boop', 'foo', 'bar'], [true, true, false, true]); 
// [ ['beep', 'boop', 'bar'], ['foo'] ]

9. castArray:其它类型转数组

const castArray = val => (Array.isArray(val) ? val : [val]);

castArray('foo'); // ['foo']
castArray([1]); // [1]
castArray(1); // [1]

10. compact:去除数组中的无效/无用值

const compact = arr => arr.filter(Boolean);

compact([0, 1, false, 2, '', 3, 'a', 'e' * 23, NaN, 's', 34]); 
// [ 1, 2, 3, 'a', 's', 34 ]

11. countOccurrences:检测数值出现次数

const countOccurrences = (arr, val) => arr.reduce((a, v) => (v === val ? a + 1 : a), 0);
countOccurrences([1, 1, 2, 1, 2, 3], 1); // 3

12. deepFlatten:递归扁平化数组

const deepFlatten = arr => [].concat(...arr.map(v => (Array.isArray(v) ? deepFlatten(v) : v)));

deepFlatten([1, [2], [[3], 4], 5]); // [1,2,3,4,5]

13. difference:寻找差异

此代码段查找两个数组之间的差异。


const difference = (a, b) => {
  const s = new Set(b);
  return a.filter(x => !s.has(x));
};

difference([1, 2, 3], [1, 2, 4]); // [3]

14. differenceBy:先执行再寻找差异

在将给定函数应用于两个列表的每个元素之后,此方法返回两个数组之间的差异。

const differenceBy = (a, b, fn) => {
  const s = new Set(b.map(fn));
  return a.filter(x => !s.has(fn(x)));
};

differenceBy([2.1, 1.2], [2.3, 3.4], Math.floor); // [1.2]
differenceBy([{ x: 2 }, { x: 1 }], [{ x: 1 }], v => v.x); // [ { x: 2 } ]

15. dropWhile:删除不符合条件的值

此代码段从数组顶部开始删除元素,直到传递的函数返回为true

const dropWhile = (arr, func) => {
  while (arr.length > 0 && !func(arr[0])) arr = arr.slice(1);
  return arr;
};

dropWhile([1, 2, 3, 4], n => n >= 3); // [3,4]

16. flatten:指定深度扁平化数组

此代码段第二参数可指定深度。

const flatten = (arr, depth = 1) =>
  arr.reduce((a, v) => a.concat(depth > 1 && Array.isArray(v) ? flatten(v, depth - 1) : v), []);

flatten([1, [2], 3, 4]); // [1, 2, 3, 4]
flatten([1, [2, [3, [4, 5], 6], 7], 8], 2); // [1, 2, 3, [4, 5], 6, 7, 8]

17. indexOfAll:返回数组中某值的所有索引

此代码段可用于获取数组中某个值的所有索引,如果此值中未包含该值,则返回一个空数组。

const indexOfAll = (arr, val) => arr.reduce((acc, el, i) => (el === val ? [...acc, i] : acc), []);

indexOfAll([1, 2, 3, 1, 2, 3], 1); // [0,3]
indexOfAll([1, 2, 3], 4); // []

18. intersection:两数组的交集


const intersection = (a, b) => {
  const s = new Set(b);
  return a.filter(x => s.has(x));
};

intersection([1, 2, 3], [4, 3, 2]); // [2, 3]

19. intersectionWith:两数组都符合条件的交集

此片段可用于在对两个数组的每个元素执行了函数之后,返回两个数组中存在的元素列表。


const intersectionBy = (a, b, fn) => {
  const s = new Set(b.map(fn));
  return a.filter(x => s.has(fn(x)));
};

intersectionBy([2.1, 1.2], [2.3, 3.4], Math.floor); // [2.1]

20. intersectionWith:先比较后返回交集

const intersectionWith = (a, b, comp) => a.filter(x => b.findIndex(y => comp(x, y)) !== -1);

intersectionWith([1, 1.2, 1.5, 3, 0], [1.9, 3, 0, 3.9], (a, b) => Math.round(a) === Math.round(b)); // [1.5, 3, 0]

21. minN:返回指定长度的升序数组

const minN = (arr, n = 1) => [...arr].sort((a, b) => a - b).slice(0, n);

minN([1, 2, 3]); // [1]
minN([1, 2, 3], 2); // [1,2]

22. negate:根据条件反向筛选


const negate = func => (...args) => !func(...args);

[1, 2, 3, 4, 5, 6].filter(negate(n => n % 2 === 0)); // [ 1, 3, 5 ]

23. randomIntArrayInRange:生成两数之间指定长度的随机数组

const randomIntArrayInRange = (min, max, n = 1) =>
  Array.from({ length: n }, () => Math.floor(Math.random() * (max - min + 1)) + min);
  
randomIntArrayInRange(12, 35, 10); // [ 34, 14, 27, 17, 30, 27, 20, 26, 21, 14 ]

24. sample:在指定数组中获取随机数

const sample = arr => arr[Math.floor(Math.random() * arr.length)];

sample([3, 7, 9, 11]); // 9

25. sampleSize:在指定数组中获取指定长度的随机数

此代码段可用于从数组中获取指定长度的随机数,直至穷尽数组。
使用Fisher-Yates算法对数组中的元素进行随机选择。

const sampleSize = ([...arr], n = 1) => {
  let m = arr.length;
  while (m) {
    const i = Math.floor(Math.random() * m--);
    [arr[m], arr[i]] = [arr[i], arr[m]];
  }
  return arr.slice(0, n);
};

sampleSize([1, 2, 3], 2); // [3,1]
sampleSize([1, 2, 3], 4); // [2,3,1]

26. shuffle:“洗牌” 数组

此代码段使用Fisher-Yates算法随机排序数组的元素。


const shuffle = ([...arr]) => {
  let m = arr.length;
  while (m) {
    const i = Math.floor(Math.random() * m--);
    [arr[m], arr[i]] = [arr[i], arr[m]];
  }
  return arr;
};

const foo = [1, 2, 3];
shuffle(foo); // [2, 3, 1], foo = [1, 2, 3]

27. nest:根据parent_id生成树结构(阿里一面真题)

根据每项的parent_id,生成具体树形结构的对象。

const nest = (items, id = null, link = 'parent_id') =>
  items
    .filter(item => item[link] === id)
    .map(item => ({ ...item, children: nest(items, item.id) }));

用法:

const comments = [
  { id: 1, parent_id: null },
  { id: 2, parent_id: 1 },
  { id: 3, parent_id: 1 },
  { id: 4, parent_id: 2 },
  { id: 5, parent_id: 4 }
];
const nestedComments = nest(comments); // [{ id: 1, parent_id: null, children: [...] }]


强烈建议去理解这个的实现,因为这是我亲身遇到的阿里一面真题:

2. 第二部分:函数

1.attempt:捕获函数运行异常

该代码段执行一个函数,返回结果或捕获的错误对象。

onst attempt = (fn, ...args) => {
  try {
    return fn(...args);
  } catch (e) {
    return e instanceof Error ? e : new Error(e);
  }
};
var elements = attempt(function(selector) {
  return document.querySelectorAll(selector);
}, '>_>');
if (elements instanceof Error) elements = []; // elements = []

2. defer:推迟执行

此代码段延迟了函数的执行,直到清除了当前调用堆栈。

const defer = (fn, ...args) => setTimeout(fn, 1, ...args);

defer(console.log, 'a'), console.log('b'); // logs 'b' then 'a'

3. runPromisesInSeries:运行多个Promises

const runPromisesInSeries = ps => ps.reduce((p, next) => p.then(next), Promise.resolve());
const delay = d => new Promise(r => setTimeout(r, d));

runPromisesInSeries([() => delay(1000), () => delay(2000)]);
//依次执行每个Promises ,总共需要3秒钟才能完成

4. timeTaken:计算函数执行时间


const timeTaken = callback => {
  console.time('timeTaken');
  const r = callback();
  console.timeEnd('timeTaken');
  return r;
};

timeTaken(() => Math.pow(2, 10)); // 1024, (logged): timeTaken: 0.02099609375ms

5. createEventHub:简单的发布/订阅模式

创建一个发布/订阅(发布-订阅)事件集线,有emitonoff方法。

  1. 使用Object.create(null)创建一个空的hub对象。
  2. emit,根据event参数解析处理程序数组,然后.forEach()通过传入数据作为参数来运行每个处理程序。
  3. on,为事件创建一个数组(若不存在则为空数组),然后.push()将处理程序添加到该数组。
  4. off,用.findIndex()在事件数组中查找处理程序的索引,并使用.splice()删除。
const createEventHub = () => ({
  hub: Object.create(null),
  emit(event, data) {
    (this.hub[event] || []).forEach(handler => handler(data));
  },
  on(event, handler) {
    if (!this.hub[event]) this.hub[event] = [];
    this.hub[event].push(handler);
  },
  off(event, handler) {
    const i = (this.hub[event] || []).findIndex(h => h === handler);
    if (i > -1) this.hub[event].splice(i, 1);
    if (this.hub[event].length === 0) delete this.hub[event];
  }
});

用法:

const handler = data => console.log(data);
const hub = createEventHub();
let increment = 0;

// 订阅,监听不同事件
hub.on('message', handler);
hub.on('message', () => console.log('Message event fired'));
hub.on('increment', () => increment++);

// 发布:发出事件以调用所有订阅给它们的处理程序,并将数据作为参数传递给它们
hub.emit('message', 'hello world'); // 打印 'hello world' 和 'Message event fired'
hub.emit('message', { hello: 'world' }); // 打印 对象 和 'Message event fired'
hub.emit('increment'); // increment = 1

// 停止订阅
hub.off('message', handler);

6.memoize:缓存函数

通过实例化一个Map对象来创建一个空的缓存。

通过检查输入值的函数输出是否已缓存,返回存储一个参数的函数,该参数将被提供给已记忆的函数;如果没有,则存储并返回它。

const memoize = fn => {
  const cache = new Map();
  const cached = function(val) {
    return cache.has(val) ? cache.get(val) : cache.set(val, fn.call(this, val)) && cache.get(val);
  };
  cached.cache = cache;
  return cached;
};

Ps: 这个版本可能不是很清晰,还有Vue源码版的:

/**
 * Create a cached version of a pure function.
 */
export function cached<F: Function> (fn: F): F {
  const cache = Object.create(null)
  return (function cachedFn (str: string) {
    const hit = cache[str]
    return hit || (cache[str] = fn(str))
  }: any)
}

7. once:只调用一次的函数

const once = fn => {
  let called = false
  return function () {
    if (!called) {
      called = true
      fn.apply(this, arguments)
    }
  }
};

8.flattenObject:以键的路径扁平化对象

使用递归。

  1. 利用Object.keys(obj)联合Array.prototype.reduce(),以每片叶子节点转换为扁平的路径节点。
  2. 如果键的值是一个对象,则函数使用调用适当的自身prefix以创建路径Object.assign()
  3. 否则,它将适当的前缀键值对添加到累加器对象。
  4. prefix除非你希望每个键都有一个前缀,否则应始终省略第二个参数。
const flattenObject = (obj, prefix = '') =>
  Object.keys(obj).reduce((acc, k) => {
    const pre = prefix.length ? prefix + '.' : '';
    if (typeof obj[k] === 'object') Object.assign(acc, flattenObject(obj[k], pre + k));
    else acc[pre + k] = obj[k];
    return acc;
  }, {});
  
flattenObject({ a: { b: { c: 1 } }, d: 1 }); // { 'a.b.c': 1, d: 1 }

9. unflattenObject:以键的路径展开对象

与上面的相反,展开对象。

const unflattenObject = obj =>
  Object.keys(obj).reduce((acc, k) => {
    if (k.indexOf('.') !== -1) {
      const keys = k.split('.');
      Object.assign(
        acc,
        JSON.parse(
          '{' +
            keys.map((v, i) => (i !== keys.length - 1 ? `"${v}":{` : `"${v}":`)).join('') +
            obj[k] +
            '}'.repeat(keys.length)
        )
      );
    } else acc[k] = obj[k];
    return acc;
  }, {});
  
unflattenObject({ 'a.b.c': 1, d: 1 }); // { a: { b: { c: 1 } }, d: 1 }

这个的用途,在做Tree组件或复杂表单时取值非常舒服。

3. 第三部分:字符串

1.byteSize:返回字符串的字节长度

const byteSize = str => new Blob([str]).size;

byteSize('😀'); // 4
byteSize('Hello World'); // 11

2. capitalize:首字母大写

const capitalize = ([first, ...rest]) =>
  first.toUpperCase() + rest.join('');
  
capitalize('fooBar'); // 'FooBar'
capitalize('fooBar', true); // 'Foobar'

3. capitalizeEveryWord:每个单词首字母大写

const capitalizeEveryWord = str => str.replace(/\b[a-z]/g, char => char.toUpperCase());

capitalizeEveryWord('hello world!'); // 'Hello World!'

4. decapitalize:首字母小写

const decapitalize = ([first, ...rest]) =>
  first.toLowerCase() + rest.join('')

decapitalize('FooBar'); // 'fooBar'
decapitalize('FooBar'); // 'fooBar'

5. luhnCheck:银行卡号码校验(luhn算法)

Luhn算法的实现,用于验证各种标识号,例如信用卡号,IMEI号,国家提供商标识号等。

String.prototype.split('')结合使用,以获取数字数组。获得最后一个数字。实施luhn算法。如果被整除,则返回,否则返回。

const luhnCheck = num => {
  let arr = (num + '')
    .split('')
    .reverse()
    .map(x => parseInt(x));
  let lastDigit = arr.splice(0, 1)[0];
  let sum = arr.reduce((acc, val, i) => (i % 2 !== 0 ? acc + val : acc + ((val * 2) % 9) || 9), 0);
  sum += lastDigit;
  return sum % 10 === 0;
};

用例:

luhnCheck('4485275742308327'); // true
luhnCheck(6011329933655299); //  false
luhnCheck(123456789); // false

补充:银行卡号码的校验规则

关于luhn算法,可以参考以下文章:

银行卡号码校验算法(Luhn算法,又叫模10算法)

银行卡号码的校验采用Luhn算法,校验过程大致如下:

  1. 从右到左给卡号字符串编号,最右边第一位是1,最右边第二位是2,最右边第三位是3….

  2. 从右向左遍历,对每一位字符t执行第三个步骤,并将每一位的计算结果相加得到一个数s。

  3. 对每一位的计算规则:如果这一位是奇数位,则返回t本身,如果是偶数位,则先将t乘以2得到一个数n,如果n是一位数(小于10),直接返回n,否则将n的个位数和十位数相加返回。

  4. 如果s能够整除10,则此号码有效,否则号码无效。

因为最终的结果会对10取余来判断是否能够整除10,所以又叫做模10算法。

当然,还是库比较香: bankcardinfo

6. splitLines:将多行字符串拆分为行数组。

使用String.prototype.split()和正则表达式匹配换行符并创建一个数组。

const splitLines = str => str.split(/\r?\n/);

splitLines('This\nis a\nmultiline\nstring.\n'); // ['This', 'is a', 'multiline', 'string.' , '']

7. stripHTMLTags:删除字符串中的HTMl标签

从字符串中删除HTML / XML标签。

使用正则表达式从字符串中删除HTML / XML 标记。

const stripHTMLTags = str => str.replace(/<[^>]*>/g, '');

stripHTMLTags('<p><em>lorem</em> <strong>ipsum</strong></p>'); // 'lorem ipsum'

4. 第四部分:对象

1. dayOfYear:当前日期天数

const dayOfYear = date =>
  Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24);

dayOfYear(new Date()); // 285

2. forOwn:迭代属性并执行回调

const forOwn = (obj, fn) => Object.keys(obj).forEach(key => fn(obj[key], key, obj));
forOwn({ foo: 'bar', a: 1 }, v => console.log(v)); // 'bar', 1

3. Get Time From Date:返回当前24小时制时间的字符串

const getColonTimeFromDate = date => date.toTimeString().slice(0, 8);

getColonTimeFromDate(new Date()); // "08:38:00"

4. Get Days Between Dates:返回日期间的天数

const getDaysDiffBetweenDates = (dateInitial, dateFinal) =>
  (dateFinal - dateInitial) / (1000 * 3600 * 24);
  
getDaysDiffBetweenDates(new Date('2019-01-01'), new Date('2019-10-14')); // 286

5. is:检查值是否为特定类型。

const is = (type, val) => ![, null].includes(val) && val.constructor === type;

is(Array, [1]); // true
is(ArrayBuffer, new ArrayBuffer()); // true
is(Map, new Map()); // true
is(RegExp, /./g); // true
is(Set, new Set()); // true
is(WeakMap, new WeakMap()); // true
is(WeakSet, new WeakSet()); // true
is(String, ''); // true
is(String, new String('')); // true
is(Number, 1); // true
is(Number, new Number(1)); // true
is(Boolean, true); // true
is(Boolean, new Boolean(true)); // true

6. isAfterDate:检查是否在某日期后

const isAfterDate = (dateA, dateB) => dateA > dateB;

isAfterDate(new Date(2010, 10, 21), new Date(2010, 10, 20)); // true

7. isBeforeDate:检查是否在某日期前

const isBeforeDate = (dateA, dateB) => dateA < dateB;

isBeforeDate(new Date(2010, 10, 20), new Date(2010, 10, 21)); // true

8 tomorrow:获取明天的字符串格式时间


const tomorrow = () => {
  let t = new Date();
  t.setDate(t.getDate() + 1);
  return t.toISOString().split('T')[0];
};

tomorrow(); // 2019-10-15 (如果明天是2019-10-15)

9. equals:全等判断

在两个变量之间进行深度比较以确定它们是否全等。

此代码段精简的核心在于Array.prototype.every()的使用。

const equals = (a, b) => {
  if (a === b) return true;
  if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
  if (!a || !b || (typeof a !== 'object' && typeof b !== 'object')) return a === b;
  if (a.prototype !== b.prototype) return false;
  let keys = Object.keys(a);
  if (keys.length !== Object.keys(b).length) return false;
  return keys.every(k => equals(a[k], b[k]));
};

用法:

equals({ a: [2, { e: 3 }], b: [4], c: 'foo' }, { a: [2, { e: 3 }], b: [4], c: 'foo' }); // true

5. 第五部分:数字

1. randomIntegerInRange:生成指定范围的随机整数

const randomIntegerInRange = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;

randomIntegerInRange(0, 5); // 3

2. randomNumberInRange:生成指定范围的随机小数

const randomNumberInRange = (min, max) => Math.random() * (max - min) + min;

randomNumberInRange(2, 10); // 6.0211363285087005

3. round:四舍五入到指定位数

const round = (n, decimals = 0) => Number(`${Math.round(`${n}e${decimals}`)}e-${decimals}`);

round(1.005, 2); // 1.01

4. sum:计算数组或多个数字的总和


const sum = (...arr) => [...arr].reduce((acc, val) => acc + val, 0);

sum(1, 2, 3, 4); // 10
sum(...[1, 2, 3, 4]); // 10

5. toCurrency:简单的货币单位转换

const toCurrency = (n, curr, LanguageFormat = undefined) =>
  Intl.NumberFormat(LanguageFormat, { style: 'currency', currency: curr }).format(n);
  
toCurrency(123456.789, 'EUR'); // €123,456.79
toCurrency(123456.789, 'USD', 'en-us'); // $123,456.79  
toCurrency(123456.789, 'USD', 'fa'); // ۱۲۳٬۴۵۶٫۷۹
toCurrency(322342436423.2435, 'JPY'); // ¥322,342,436,423 

6. 第六部分:浏览器操作及其它

1. bottomVisible:检查页面底部是否可见

const bottomVisible = () =>
  document.documentElement.clientHeight + window.scrollY >=
  (document.documentElement.scrollHeight || document.documentElement.clientHeight);

bottomVisible(); // true

2. Create Directory:检查创建目录

此代码段调用fs模块的existsSync()检查目录是否存在,如果不存在,则mkdirSync()创建该目录。

const fs = require('fs');
const createDirIfNotExists = dir => (!fs.existsSync(dir) ? fs.mkdirSync(dir) : undefined);
createDirIfNotExists('test'); 

3. currentURL:返回当前链接url

const currentURL = () => window.location.href;

currentURL(); // 'https://juejin.im'

4. distance:返回两点间的距离

该代码段通过计算欧几里得距离来返回两点之间的距离。

const distance = (x0, y0, x1, y1) => Math.hypot(x1 - x0, y1 - y0);

distance(1, 1, 2, 3); // 2.23606797749979

5. elementContains:检查是否包含子元素

此代码段检查父元素是否包含子元素。

const elementContains = (parent, child) => parent !== child && parent.contains(child);

elementContains(document.querySelector('head'), document.querySelector('title')); // true
elementContains(document.querySelector('body'), document.querySelector('body')); // false

6. getStyle:返回指定元素的生效样式

const getStyle = (el, ruleName) => getComputedStyle(el)[ruleName];

getStyle(document.querySelector('p'), 'font-size'); // '16px'

7. getType:返回值或变量的类型名

const getType = v =>
  v === undefined ? 'undefined' : v === null ? 'null' : v.constructor.name.toLowerCase();
  
getType(new Set([1, 2, 3])); // 'set'
getType([1, 2, 3]); // 'array'

8. hasClass:校验指定元素的类名

const hasClass = (el, className) => el.classList.contains(className);
hasClass(document.querySelector('p.special'), 'special'); // true

9. hide:隐藏所有的指定标签

const hide = (...el) => [...el].forEach(e => (e.style.display = 'none'));

hide(document.querySelectorAll('img')); // 隐藏所有<img>标签

10. httpsRedirectHTTP 跳转 HTTPS

const httpsRedirect = () => {
  if (location.protocol !== 'https:') location.replace('https://' + location.href.split('//')[1]);
};

httpsRedirect(); // 若在`http://www.baidu.com`, 则跳转到`https://www.baidu.com`

11.insertAfter:在指定元素之后插入新元素

const insertAfter = (el, htmlString) => el.insertAdjacentHTML('afterend', htmlString);

// <div id="myId">...</div> <p>after</p>
insertAfter(document.getElementById('myId'), '<p>after</p>'); 

12.insertBefore:在指定元素之前插入新元素

const insertBefore = (el, htmlString) => el.insertAdjacentHTML('beforebegin', htmlString);

insertBefore(document.getElementById('myId'), '<p>before</p>'); // <p>before</p> <div id="myId">...</div>

13. isBrowser:检查是否为浏览器环境

此代码段可用于确定当前运行时环境是否为浏览器。这有助于避免在服务器(节点)上运行前端模块时出错。

const isBrowser = () => ![typeof window, typeof document].includes('undefined');

isBrowser(); // true (browser)
isBrowser(); // false (Node)

14. isBrowserTab:检查当前标签页是否活动

const isBrowserTabFocused = () => !document.hidden;

isBrowserTabFocused(); // true

15. nodeListToArray:转换nodeList为数组

const nodeListToArray = nodeList => [...nodeList];

nodeListToArray(document.childNodes); // [ <!DOCTYPE html>, html ]

16. Random Hexadecimal Color Code:随机十六进制颜色


const randomHexColorCode = () => {
  let n = (Math.random() * 0xfffff * 1000000).toString(16);
  return '#' + n.slice(0, 6);
};

randomHexColorCode(); // "#e34155"

17. scrollToTop:平滑滚动至顶部

该代码段可用于平滑滚动到当前页面的顶部。

const scrollToTop = () => {
  const c = document.documentElement.scrollTop || document.body.scrollTop;
  if (c > 0) {
    window.requestAnimationFrame(scrollToTop);
    window.scrollTo(0, c - c / 8);
  }
};

scrollToTop();

18. smoothScroll:滚动到指定元素区域

该代码段可将指定元素平滑滚动到浏览器窗口的可见区域。

const smoothScroll = element =>
  document.querySelector(element).scrollIntoView({
    behavior: 'smooth'
  });
  
smoothScroll('#fooBar'); 
smoothScroll('.fooBar'); 

19. detectDeviceType:检测移动/PC设备

const detectDeviceType = () =>
  /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
    ? 'Mobile'
    : 'Desktop';

20. getScrollPosition:返回当前的滚动位置

默认参数为windowpageXOffset(pageYOffset)为第一选择,没有则用scrollLeft(scrollTop)

const getScrollPosition = (el = window) => ({
  x: el.pageXOffset !== undefined ? el.pageXOffset : el.scrollLeft,
  y: el.pageYOffset !== undefined ? el.pageYOffset : el.scrollTop
});

getScrollPosition(); // {x: 0, y: 200}

21. size:获取不同类型变量的长度

这个的实现非常巧妙,利用Blob类文件对象的特性,获取对象的长度。

另外,多重三元运算符,是真香。

const size = val =>
  Array.isArray(val)
    ? val.length
    : val && typeof val === 'object'
    ? val.size || val.length || Object.keys(val).length
    : typeof val === 'string'
    ? new Blob([val]).size
    : 0;

size([1, 2, 3, 4, 5]); // 5
size('size'); // 4
size({ one: 1, two: 2, three: 3 }); // 3

22. escapeHTML:转义HTML

当然是用来防XSS攻击啦。

const escapeHTML = str =>
  str.replace(
    /[&<>'"]/g,
    tag =>
      ({
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        "'": '&#39;',
        '"': '&quot;'
      }[tag] || tag)
  );

escapeHTML('<a href="#">Me & you</a>'); // '&lt;a href=&quot;#&quot;&gt;Me &amp; you&lt;/a&gt;'

❤️ 看完三件事

如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

  1. 点赞,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)
  2. 关注公众号「前端劝退师」,不定期分享原创知识。
  3. 也看看其它文章

最惨前端面经 | 前22年的Loser,后4年和自己赛跑的人.md

跳槽原因

前东家部门是做旅游的,在这次疫情打击下,基本玩完。

于是我半休半远程三个月后,在4月底领了裁员便当。至今,差不多找了两个月的工作。

本篇不是标准的面经,想从中获取大厂跳槽经验的可以歇一歇。


更多的是想讲一下绝大多数如你如我,学历渣技术差,没大厂经验的前端如何走。

1. Offer情况

个人比较懒,一周可能就面2~3家,只约下午。部分星期没有面试邀约。

囿于学历+公司,两招聘软件都被我用成“Boss直拒”和“拉钩上吊”

粗略算了下,面了约12家大中小型公司,仅4家Offer,情况分别为:

  • 某游戏公司
  • 某小公司
  • 风变编程
  • 金山系某司

作为一个社交孤儿,在本次跳槽历程中也是发现自己不少的问题,且听我慢慢道来。

本篇虽然有点丧,但你们可以从中找到对应的问题(我几乎犯了所有面试的低级错误)

部分的公司有:360奇舞团,某上市游戏公司,风变编程,金山系某司,阿里。

2. 高频面试题汇总

面过的公司有点多,一并说了吧。

1. 从“在浏览器输入域名”到“页面静态资源完全加载”的整个流程

见于:某游戏公司、小鹅通、阿里一面、另外三家小公司

这问题的答案,我结合了yck《前端面试之道》和 浏览器原理专栏:

整个过程可以分为几步:

  1. 用户输入

    当用户输入关键字并键入回车之后,这意味着当前页面即将要被替换成新的页面,不过在这个流程继续之前,浏览器还给了当前页面一次执行 beforeunload 事件的机会,beforeunload 事件允许页面在退出之前执行一些数据清理操作,还可以询问用户是否要离开当前页面。

  2. URL 请求过程

    首先,网络进程会查找本地缓存是否缓存了该资源。

    如果有缓存资源,那么直接返回资源给浏览器进程;如果在缓存中没有查找到资源,那么直接进入网络请求流程。这请求前的第一步是要进行 DNS 解析,以获取请求域名的服务器 IP 地址。如果请求协议是 HTTPS,那么还需要建立 TLS 连接。

    • 其中,DNS也有几步缓存:浏览器缓存,hosts文件,
    • 如果本地域名解析服务器也没有该域名的记录,则开始递归+迭代解析
    • TCP三次握手,HTTPTLS握手,HTTPS

    接下来就是利用 IP 地址和服务器建立 TCP 连接。连接建立之后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的 Cookie 等数据附加到请求头中,然后向服务器发送构建的请求信息。

    数据在进入服务端之前,可能还会先经过负责负载均衡的服务器,它的作用就是将请求合理的分发到多台服务器上,这时假设服务端会响应一个 HTML 文件。

    首先浏览器会判断状态码是什么,如果是 200 那就继续解析,如果 400500 的话就会报错,如果 300 的话会进行重定向,这里会有个重定向计数器,避免过多次的重定向,超过次数也会报错。

    浏览器开始解析文件,如果是 gzip 格式的话会先解压一下,然后通过文件的编码格式知道该如何去解码文件。

  3. 准备渲染进程

    默认情况下,Chrome 会为每个页面分配一个渲染进程,也就是说,每打开一个新页面就会配套创建一个新的渲染进程。

  4. 渲染阶段

    文件解码成功后会正式开始渲染流程,先会根据 HTML 构建 DOM 树,有CSS的话会去构建 CSSOM 树。如果遇到 script 标签的话,会判断是否存在 async 或者 defer ,前者会并行进行下载并执行 JS,后者会先下载文件,然后等待 HTML 解析完成后顺序执行。

    如果以上都没有,就会阻塞住渲染流程直到 JS 执行完毕。

CSSOM 树和 DOM 树构建完成后会开始生成 Render 树,这一步就是确定页面元素的布局、样式等等诸多方面的东西

在生成 Render 树的过程中,浏览器就开始调用GPU 绘制,合成图层,将内容显示在屏幕上了。

2. eventloop机制,promise的实现和静态方法、async实现。


这题聊起来可就大了,进程,线程,协程。部分还会配以那道最经典的eventloop题目。

见于:阿里一面、小鹅通、头条一面、360一面、风变编程、以及其它四家公司,必考。

Event Loop 是什么?

JavaScript的事件分两种,宏任务(macro-task)和微任务(micro-task)

  • 宏任务:包括整体代码script,setTimeout,setInterval
  • 微任务Promise.then(非new Promise)process.nextTick(node中)
  • 事件的执行顺序,是先执行宏任务,然后执行微任务,这个是基础,任务可以有同步任务和异步任务,同步的进入主线程,异步的进入Event Table并注册函数,异步事件完成后,会将回调函数放入Event Queue中(宏任务和微任务是不同的Event Queue),同步任务执行完成后,会从Event Queue中读取事件放入主线程执行,回调函数中可能还会包含不同的任务,因此会循环执行上述操作。

Promise 的含义

Promise是一个异步编程的解决方案,简单来讲,Promise类似一个盒子,里面保存着在未来某个时间点才会结束的事件。

三种状态:

  • pending:进行中
  • fulfilled :已经成功
  • rejected :已经失败
    状态改变,只能从 pending 变成 fulfilled 或者 rejected,状态不可逆。

async实现和常用方法

async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。

Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。

协程是一种用户态的轻量级线程,
协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

3. VueReact 之间的区别

摘自yck《前端面试之道》

  1. Vue 的表单可以使用 v-model 支持双向绑定,相比于 React 来说开发上更加方便,当然了 v-model 其实就是个语法糖,本质上和 React 写表单的方式没什么区别。

  2. 改变数据方式不同,Vue 修改状态相比来说要简单许多,React 需要使用 setState 来改变状态,并且使用这个 API 也有一些坑点。并且 Vue 的底层使用了依赖追踪,页面更新渲染已经是最优的了,但是 React 还是需要用户手动去优化这方面的问题。

  3. React 16以后,有些钩子函数会执行多次,这是因为引入 Fiber 的原因,这在后续的章节中会讲到。

  4. React 需要使用 JSX,有一定的上手成本,并且需要一整套的工具链支持,但是完全可以通过 JS 来控制页面,更加的灵活。Vue 使用了模板语法,相比于 JSX 来说没有那么灵活,但是完全可以脱离工具链,通过直接编写 render 函数就能在浏览器中运行。

  5. 在生态上来说,两者其实没多大的差距,当然 React 的用户是远远高于 Vue 的。

  6. 在上手成本上来说,Vue 一开始的定位就是尽可能的降低前端开发的门槛,然而 React 更多的是去改变用户去接受它的概念和**,相较于 Vue 来说上手成本略高。

4. Vue 3.0面试题

见于:360一面、风变编程,预计下半年必考。

  1. Vue3.0都有哪些重要新特性?
  • 建议往Composition API和Tree-shaking方面答,对应比较React HookswebpackTree-shaking
  1. Vue3.0 对比Vue2.0的优势在哪?
  1. Vue3.0React 16.X 都有哪些区别和相似处?
  • 可以更新下你的横比答案了,重点突出两者开始相互借鉴,互有优点。记得夸夸Vue3.0抄过来,却做得更好的部分。
  1. Vue3.0是如何实现代码逻辑复用的?
  • 可以先对比Composition APImixin的差异,并凸显出Vue2.0那种代码上下反复横跳的缺点。

以上答案基本可以在下面两篇博客里找到:

《抄笔记:尤雨溪在Vue3.0 Beta直播里聊到了这些…》

《Vue3 究竟好在哪里?(和 React Hook 的详细对比)》

5. React高频面试题

  1. React 16.XFiber原理
  1. setState原理,什么时候是同步的?
  1. React Hooks相对高阶组件和Class组件有什么优势/缺点?
  1. React 16.X的生命周期,以及为何要替换掉以前的?
  1. React跨平台的实现原理。
  1. 说一说redux,以及比flux先进的原因。

平心而论,如果面试前未要求技术栈,建议往Vue方向引。React的面试题要高一两个难度....

6. HTTP高频面试题

见于:阿里一面、头条一面、360一面、风变编程、以及其它四家公司,必考。

  1. 讲一讲强缓存和协议缓存?
  1. HTTP/2.0 都有哪些特性?头部压缩的原理?
  1. TCP三次握手和四次挥手?以其存在意义。
  1. 状态码。302.304.301.401.403的区别?
  1. 状态码。204304分别有什么作用?
  1. HTTPHTTPS握手差异?
  1. CSRF 跨站请求伪造和XSS 跨站脚本攻击是什么?
  1. 你是如何解决跨域的?都有几种?
  1. nginx 了解吗?你都用来做什么?
  1. 有了【Last-Modified,If-Modified-Since】为何还要有【ETag、If-None-Match

我总结过一张xmind图,欢迎到我公众号里自取。

7. JS/CSS高频基础问题

见于:阿里一面、头条一面、风变编程、以及其它多家公司,非常高频。

  1. 弹性盒子中 flex: 0 1 auto 表示什么意思?
  1. 箭头函数可以用new实例化吗?聊聊this的指向问题。
  1. 聊一聊原型链。
  1. 垃圾回收中的堆和栈的区别。
  1. 0.1 + 0.2 != 0.3背后的原理?
  1. TypeScript用过吗?聊聊你对TypeScript的理解?
  1. 图片懒加载的原理。
  1. call、applybind方法的用法以及区别
  1. Webpack原理,以及常用插件

8. 项目及优化相关

  1. 项目中遇到的难点,以及解决思路。
  1. 你是如何做Web性能优化的?首屏渲染如何处理?

这个问题很大,我有个简略版,回答思路引自专栏《
浏览器工作原理与实践》:

主要围绕着两个阶段:加载阶段 和 交互阶段

加载阶段:

  1. 减少关键资源的个数和大小(Webpack拆/合包,懒加载等)
  2. 减少关键资源RTT的时间(Gzip压缩,边缘节点CDN

交互阶段:

  1. JS代码不可占用主线程太久,与首屏无关的脚本加上延后处理(aysnc/defer)属性,与DOM无关的交给Web Worker
  2. CSS用对选择器(尽可能绑定ClassId),否则会遍历多次。
  3. 首屏渲染中如果有动画,加上will-change属性,浏览器会开辟新的层处理(触发合成机制)
  4. 避免强制同步布局,避免布局抖动。
  5. 图片懒加载(有四种方式)
  1. 埋点数据上报的方案做过吗?
  1. 前端架构思考,你是如何考量部门的技术栈的?
  1. 前端重构思考,老项目在新业务紧急与重构技术债务间如何衡量轻重?

9. 全链路以及DevOps认知

由于我第二家公司部门是做DevOps平台,有些与前端无关的面试题。

  1. 单元测试做过吗?你是怎么做的?
  1. docker 准备流程?
  1. DevOps平台关键功能点?
  1. 自动化测试,CI/CD 的关键核心都有哪些?
  1. 如何保障DevOps推动?
  1. 接口如何做优化?Mock平台搭建方案?

3. 面试感受

今年,太难太难了。
@木易杨 大佬的这段话很对:

先来说下大环境,感觉非常不好,就一二线互联网来说招人的没几家公司,裁员的、内部调整的、锁 HC 的确是一大堆,所以大家在换工作的时候一定不要裸辞,风险太大。

今年面试和往年感受有些不同,对于项目的重难点、亮点、个人在团队中做的贡献、对项目的 Owner 意识等比较关注,还有就是编程能力的考察会更多一些。

简单讲,就是学历是第一竞争力,你靠APP投简历几乎没反馈,内推最靠谱


其次,如果你仅有大专,能力非高得不可替代,不建议在今年频繁投大厂,我就经历过各种各样简历面挂和HR面挂,部分原因:

  1. 跳槽频繁(我四年三家,其中两家是部门解散/倒闭)
  2. 博客写太多,专注力不够,英语不够好。
  3. 过往项目没亮点,直接否定。

平心而论,过往跳槽都不是很顺利,但今年是真感受到了天花板:

  1. 大公司投简历投不进。
  2. 小公司薪资满足不了。


充分体会到一句话:

你往后的日子里,都在为高中不努力买单。

4. 面试技巧

严格说,本面渣不配给一些面试技巧,但有些是我没做好却很不错的东西。建议你们看一看:

1. 付费找人包装简历

如果你一没学历,二没大厂简历,简历写得稀烂。

相信我,投递之前,付个几十块找业内大佬帮你改改简历,你一定会有更多的投递反馈。

2. 优秀的自我介绍模版

面试官您好!

  1. 我叫***,很开心今天来应聘** 岗位,我有****岗位工作经验,工作内容包括**、*** **等,曾参与***项目/工作,完成**业绩,这些经验锻炼了我***能力。除了日常业务开发外,我还在***方面.....

  2. 面试之前,我了解到咱们公司主要从事***业务、***类产
    品,属于行业排名***的企业,我对这个行业非常看好,也想在这行
    长期发展!这个岗位要求的** ***能力和经验,与我的工作经历很
    匹配,相信我能够胜任这个岗位,谢谢!

3. 项目问题提前备好草稿

在我面试一个月左右,发现经常挂在二/三面后,开始审视自己的问题:

  1. 回答项目难点,解决方案时太草率,只描述做了什么,并没体现方案给项目带来的进步。
  2. 缺乏断点,一个劲儿说不停,缺乏对语句的掌控。

说实话,挺难述说的,特别是我这种没啥大项目经验,前三年都在做中后台系统居多,只能从一些细节入手,以下是我的草稿(虽然很烂),供你们参考一下。:

Vue后台细颗粒权限控制与防多人操作:

  1. 一般简单的权限控制会以角色区别,但开发/运维们想自己设成员,需求1

  2. 给他们做了一个权限管理的模块,但因为DevOps的功能模块太多,记不住,开发/运维们想要直观的修改权限,需求2

  3. 考虑到这个痛点,我设计了一个“授权模式”,高权限的拥有切换功能。

    切换后,该模块下所有的按钮都会先拦截左键点击,并拦截默认右键,多了一个自定义属性。以及授权/冻结 菜单。授权功能,直接拥有选择成员的树控件弹窗。

    使用后会上传 成员数组 + 自定义属性。下一次访问后台便返回 该自定义属性 对象 ,以确定新的权限。

    权限问题解决后,开发/运维们又发现,一个部署/发布 点击模块存在同时操作的人很多。会冲突。需求4

  4. 在多人频繁触发模块,加入了WebSocket管理,实现类似在线文档的显示功能,显示当前操作人,并设屏蔽(同时操作会有显示姓名,并锁住)。

    • 简单版本:当前按钮 点击后,后台设个时间阀值,该段时间内其他人点击了就弹出提醒当前有人操作。

    • 复杂版本:多人WebSocket维护一个值。

对应场景:DevOps平台

5. 和自己赛跑的人

去年写过一篇年度总结:

我叫“笑妄”,16年地理信息系统专业专科毕业,自学的前端。 目前三年半的经验,前后工作过几家中小公司,做过Python爬虫,也曾在运维开发部混过。前两年的工作,都在为生计挣扎,做码农仅因自身一无所长,看这行工资高,就挤进来了。--- 《前端废材的自我劝退之路 》

y1s1,我底子很差。1年时才搞懂布局,2年不会函数return,3年才熟悉React

入这行以来,一路虽有伯乐,但于前端领域仍单打独斗居多。

却得益于这行,让我知“井外方觉天地大”。认识优秀的人越多,越想努力,精进。

所以这段时间也没闲着,看了一些还不错的书/专栏:

  1. 《网络是怎样连接的》,难,建议笔记。
  2. DevOps实战笔记》,有趣,但需有一定的基础。
  3. 《透视HTTP协议》,还不错,值二刷。
  4. 《图解Google V8》,相对第一本《浏览器原理》,水了不少,部分值二刷。
  5. Nginx核心知识100讲》,目前正在看。

如何提高自己的技术竞争力(或收入)?

  1. 写技术博客。“从某种意义上说,博客是我最好的学习笔记和个人名片。在IT行业内,技术博客是了解一个开发者最好的方式之一,特别是当你没有一张足够分量的文凭或者一段出彩的工作经历时,你就应该沉下心来好好打磨自己技术,打造自己的博客。往者不可谏,来者犹可追。” ---@浪里行舟 《写技术博客那点事》
  2. 立足于前端,放眼全链路。今年我原以为的弱势DevOps,帮助我搞掂了不少2/3面技术面。
  3. 跟对人,这可说是收入涨幅最大的依靠(也是最可遇不可求)。我曾见过某友,从17年9K,到如今50k+的飞跃。不说了,我真的酸。。。

这一路上,我的能力,学历,背景样样不如人,也曾懒惰曾迷茫,但一直都在和自己赛跑,我不服输:

虽然在你离开学校的时候

所有的人都认为你不会有出息

你却没有因此怨天尤人自暴自弃

....

在那时侯我们身边都有一卡车的难题

不知道成功的意义就在超越自己

我们都是和自己赛跑的人

为了更好的未来拼命努力

争取一种意义非凡的胜利

为了更好的明天拼命努力

前方没有终点

我们永不停息

❤️ 看完三件事

如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

  1. 点赞,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)
  2. 关注公众号「前端劝退师」,不定期分享原创知识。
  3. 也看看其它文章

闲暇之余也建了一个“前端人才库”群


共同维护了一个面试文档,近20次面经。

不少人也是如愿获得了满意的Offer:360、腾讯、大搜车、金山等等。想进来讨论面试/蹭蹭最新面试题的可以加我微信。

劝退师个人微信:huab119

也可以来我的GitHub博客里拿所有文章的源文件:

前端劝退指南https://github.com/roger-hiro/BlogFN
一起玩耍呀。~

颜值即正义!这几个库颠覆你对数据交互的想象

1. 手绘风图表库:roughViz.js

基于D3(v5), roughjs, 和handy

1.1 衡量方式

有三种衡量方式:

粗糙度:

线条种类:

线条粗细:

1.2 多种搭配

简答CDN:

<script src="https://unpkg.com/[email protected]"></script>

npm:

npm install rough-viz

react/vue:

npm install react-roughviz
npm install vue-roughviz

甚至在Python中也可以:

pip install roughviz

具体用法请参照官方文档:https://github.com/jwilber/roughViz

2. 抖音字体爆炸特效:react-three-fiber

Webreact-native都可用的高性能Threejs for react库。

可以在React外部驱动渲染循环,而不会产生额外开销。

最新版本采用了Hooks的写法,不像以往强行兼容的Threejs,写起来更加友好。

不止抖音字体爆炸特效,它能实现什么,源于你的技术和想象力。

以下一部分特效:

如果有人学会了...大佬带带?

抖音爆炸特效的实现:

其中用到一个库:react-spring,这是react最优秀的动画库,没有之一。

3. 播放器里的颜值担当:Mini Music Player - VueJS

国外友人写的一个Vue.js音乐播放器,好看的不得了。

其中的交互和逻辑,也是非常精炼。

源码:https://codepen.io/JavaScriptJunkie/pen/qBWrRyg

4. UI都夸好的卡片验证库:interactive-paycard

这个11月Vue新库一发布,就狂揽3k+star,过于优秀。

完整库名vue-interactive-paycard

React版的作者表示也即将发布了。

源码:https://github.com/muhammederdem/vue-interactive-paycard/issues

5. 真*动态可视化数据:SandDance

微软出品,必属精品

SandDance是使用Vega进行图表布局,使用Deck.gl进行WebGL渲染。

能在如此密集的数据量上保持动画流畅和美观的,也就微软爸爸能做到了。

我先跪了,你们随意。

此外,该库还有多种使用方式:

  1. Power BI软件内使用:
    • PowerBI是微软发布的一款数据可视化软件,可以在较短时间内生成各种报表。
  2. VSCode插件形式:
  3. 网页版和React:

官网:https://sanddance.js.org/

体验:https://sanddance.js.org/app/

「Vue实践」武装你的前端项目.md

本文目录

本文项目基于Vue-Cli3,想知道如何正确搭建请看我之前的文章:

「Vue实践」项目升级vue-cli3的正确姿势

1. 接口模块处理

1.1 axios二次封装

这里封装的依据是后台传的JWT,已封装好的请跳过。

import axios from 'axios'
import router from '../router'
import {MessageBox, Message} from 'element-ui'

let loginUrl = '/login'
// 根据环境切换接口地址
axios.defaults.baseURL = process.env.VUE_APP_API
axios.defaults.headers = {'X-Requested-With': 'XMLHttpRequest'}
axios.defaults.timeout = 60000

// 请求拦截器
axios.interceptors.request.use(
  config => {
    if (router.history.current.path !== loginUrl) {
      let token = window.sessionStorage.getItem('token')
      if (token == null) {
        router.replace({path: loginUrl, query: {redirect: router.currentRoute.fullPath}})
        return false
      } else {
        config.headers['Authorization'] = 'JWT ' + token
      }
    }
    return config
  }, error => {
    Message.warning(error)
    return Promise.reject(error)
  })

紧接着的是响应拦截器(即异常处理)

axios.interceptors.response.use(
  response => {
    return response.data
  }, error => {
    if (error.response !== undefined) {
      switch (error.response.status) {
        case 400:
          MessageBox.alert(error.response.data)
          break
        case 401:
          if (window.sessionStorage.getItem('out') === null) {
            window.sessionStorage.setItem('out', 1)
            MessageBox.confirm('会话已失效! 请重新登录', '提示', {confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning'}).then(() => {
              router.replace({path: loginUrl, query: {redirect: router.currentRoute.fullPath}})
            }).catch(action => {
              window.sessionStorage.clear()
              window.localStorage.clear()
            })
          }
          break
        case 402:
          MessageBox.confirm('登陆超时 !', '提示', {confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning'}).then(() => {
            router.replace({path: loginUrl, query: {redirect: router.currentRoute.fullPath}})
          })
          break
        case 403:
          MessageBox.alert('没有权限!')
          break
        // ...忽略
        default:
          MessageBox.alert(`连接错误${error.response.status}`)
    }
    return Promise.resolve(error.response)
  }
  return Promise.resolve(error)
})

这里做的处理分别是会话已失效和登陆超时,具体的需要根据业务来作变更。

最后是导出基础请求类型封装。

export default {
  get (url, param) {
    if (param !== undefined) {
      Object.assign(param, {_t: (new Date()).getTime()})
    } else {
      param = {_t: (new Date()).getTime()}
    }
    return axios({method: 'get', url, params: param})
  },
  // 不常更新的数据用这个
  getData (url, param) {
    return axios({method: 'get', url, params: param})
  },
  post (url, param, config) {
    return axios.post(url, param, config)
  },
  put: axios.put,
  _delete: axios.delete
}

其中给get请求加上时间戳参数,避免从缓存中拿数据。
除了基础请求类型,还有很多类似下载、上传这种,需要特殊的的请求头,此时可以根据自身需求进行封装。

浏览器缓存是基于url进行缓存的,如果页面允许缓存,则在一定时间内(缓存时效时间前)再次访问相同的URL,浏览器就不会再次发送请求到服务器端,而是直接从缓存中获取指定资源。

1.2 请求按模块合并


模块的请求:

import http from '@/utils/request'
export default {
  A (param) { return http.get('/api/', param) },
  B (param) { return http.post('/api/', param) }
  C (param) { return http.put('/api/', param) },
  D (param) { return http._delete('/api/', {data: param}) },
}

utils/api/index.js:

import http from '@/utils/request'
import account from './account'
// 忽略...
const api = Object.assign({}, http, account, \*...其它模块*\)
export default api

1.3 global.js中的处理

global.js中引入:

import Vue from 'vue'
import api from './api/index'
// 略...

const errorHandler = (error, vm) => {
  console.error(vm)
  console.error(error)
}

Vue.config.errorHandler = errorHandler
export default {
  install (Vue) {
    // 添加组件
    // 添加过滤器
    })
    // 全局报错处理
    Vue.prototype.$throw = (error) => errorHandler(error, this)
    Vue.prototype.$http = api
    // 其它配置
  }
}

写接口的时候就可以简化为:

async getData () {
    const params = {/*...key : value...*/}
    let res = await this.$http.A(params)
    res.code === 4000 ? (this.aSata = res.data) : this.$message.warning(res.msg)
}

2. 基础组件自动化全局注册

来自
@SHERlocked93:Vue 使用中的小技巧

官方文档:基础组件的自动化全局注册

我们写组件的时候通常需要引入另外的组件:

<template>
    <BaseInput  v-model="searchText"  @keydown.enter="search"/>
    <BaseButton @click="search">
        <BaseIcon name="search"/>
    </BaseButton>
</template>
<script>
    import BaseButton from './baseButton'
    import BaseIcon from './baseIcon'
    import BaseInput from './baseInput'
    export default {
      components: { BaseButton, BaseIcon, BaseInput }
    }
</script>

写小项目这么引入还好,但等项目一臃肿起来...啧啧。
这里是借助webpack,使用 require.context() 方法来创建自己的模块上下文,从而实现自动动态require组件。

这个方法需要3个参数:

  • 要搜索的文件夹目录
  • 是否还应该搜索它的子目录
  • 一个匹配文件的正则表达式。

在你放基础组件的文件夹根目录下新建componentRegister.js:

import Vue from 'vue'
/**
 * 首字母大写
 * @param str 字符串
 * @example heheHaha
 * @return {string} HeheHaha
 */
function capitalizeFirstLetter (str) {
  return str.charAt(0).toUpperCase() + str.slice(1)
}
/**
 * 对符合'xx/xx.vue'组件格式的组件取组件名
 * @param str fileName
 * @example abc/bcd/def/basicTable.vue
 * @return {string} BasicTable
 */
function validateFileName (str) {
  return /^\S+\.vue$/.test(str) &&
    str.replace(/^\S+\/(\w+)\.vue$/, (rs, $1) => capitalizeFirstLetter($1))
}
const requireComponent = require.context('./', true, /\.vue$/)
// 找到组件文件夹下以.vue命名的文件,如果文件名为index,那么取组件中的name作为注册的组件名
requireComponent.keys().forEach(filePath => {
  const componentConfig = requireComponent(filePath)
  const fileName = validateFileName(filePath)
  const componentName = fileName.toLowerCase() === 'index'
    ? capitalizeFirstLetter(componentConfig.default.name)
    : fileName
  Vue.component(componentName, componentConfig.default || componentConfig)
})

最后我们在main.js

import 'components/componentRegister.js'

我们就可以随时随地使用这些基础组件,无需手动引入了。

3. 页面性能调试:Hiper

我们写单页面应用,想看页面修改后性能变更其实挺繁琐的。有时想知道是「正优化」还是「负优化」只能靠手动刷新查看network。而Hiper很好解决了这一痛点(其实Hiper是后台静默运行Chromium来实现无感调试)。

Hiper官方文档

我们开发完一个项目或者给一个项目做完性能优化以后,如何来衡量这个项目的性能是否达标?

我们的常见方式是在Dev Tool中的performancenetwork中看数据,记录下几个关键的性能指标,然后刷新几次再看这些性能指标。

有时候我们发现,由于样本太少,受当前「网络」、「CPU」、「内存」的繁忙程度的影响很重,有时优化后的项目反而比优化前更慢。

如果有一个工具,一次性地请求N次网页,然后把各个性能指标取出来求平均值,我们就能非常准确地知道这个优化是「正优化」还是「负优化」。

并且,也可以做对比,拿到「具体优化了多少」的准确数据。这个工具就是为了解决这个痛点的。

全局安装

sudo npm install hiper -g
# 或者使用 yarn:
# sudo yarn global add hiper

性能指标

Key Value
DNS查询耗时 domainLookupEnd - domainLookupStart
TCP连接耗时 connectEnd - connectStart
第一个Byte到达浏览器的用时 responseStart - requestStart
页面下载耗时 responseEnd - responseStart
DOM Ready之后又继续下载资源的耗时 domComplete - domInteractive
白屏时间 domInteractive - navigationStart
DOM Ready 耗时 domContentLoadedEventEnd - navigationStart
页面加载总耗时 loadEventEnd - navigationStart

https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceTiming

用例配置

 # 当我们省略协议头时,默认会在url前添加`https://`

 # 最简单的用法
 hiper baidu.com
 # 如何url中含有任何参数,请使用双引号括起来
 hiper "baidu.com?a=1&b=2"
 #  加载指定页面100次
 hiper -n 100 "baidu.com?a=1&b=2"
 #  禁用缓存加载指定页面100次
 hiper -n 100 "baidu.com?a=1&b=2" --no-cache
 #  禁JavaScript加载指定页面100次
 hiper -n 100 "baidu.com?a=1&b=2" --no-javascript
 #  使用GUI形式加载指定页面100次
 hiper -n 100 "baidu.com?a=1&b=2" -H false
 #  使用指定useragent加载网页100次
 hiper -n 100 "baidu.com?a=1&b=2" -u "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36"

此外,还可以配置Cookie访问

module.exports = {
    ....
    cookies:  [{
        name: 'token',
        value: process.env.authtoken,
        domain: 'example.com',
        path: '/',
        httpOnly: true
    }],
    ....
}
# 载入上述配置文件(假设配置文件在/home/下)
hiper -c /home/config.json

# 或者你也可以使用js文件作为配置文件
hiper -c /home/config.js

4. Vue高阶组件封装

我们常用的<transition><keep-alive>就是一个高阶(抽象)组件。

export default {
  name: 'keep-alive',
  abstract: true,
  ...
}

所有的高阶(抽象)组件是通过定义abstract选项来声明的。高阶(抽象)组件不渲染真实DOM
一个常规的抽象组件是这么写的:

import { xxx } from 'xxx'
const A = () => {
    .....
}

export default {
    name: 'xxx',
    abstract: true,
    props: ['...', '...'],
    // 生命周期钩子函数
    created () {
      ....
    },
    ....
    destroyed () {
      ....
    },
    render() {
        const vnode = this.$slots.default
        ....
        return vnode
    },
})

4.1 防抖/节流 抽象组件

关于防抖和节流是啥就不赘述了。这里贴出组件代码:

改编自:Vue实现函数防抖组件

const throttle = function(fn, wait=50, isDebounce, ctx) {
  let timer
  let lastCall = 0
  return function (...params) {
    if (isDebounce) {
      if (timer) clearTimeout(timer)
      timer = setTimeout(() => {
        fn.apply(ctx, params)
      }, wait)
    } else {
      const now = new Date().getTime()
      if (now - lastCall < wait) return
      lastCall = now
      fn.apply(ctx, params)
    }
  }
}

export default {
    name: 'Throttle',
    abstract: true,
    props: {
      time: Number,
      events: String,
      isDebounce: {
        type: Boolean,
        default: false
      },
    },
    created () {
      this.eventKeys = this.events.split(',')
      this.originMap = {}
      this.throttledMap = {}
    },
    render() {
        const vnode = this.$slots.default[0]
        this.eventKeys.forEach((key) => {
            const target = vnode.data.on[key]
            if (target === this.originMap[key] && this.throttledMap[key]) {
                vnode.data.on[key] = this.throttledMap[key]
            } else if (target) {
                this.originMap[key] = target
                this.throttledMap[key] = throttle(target, this.time, this.isDebounce, vnode)
                vnode.data.on[key] = this.throttledMap[key]
            }
        })
        return vnode
    },
})

通过第三个参数isDebounce来控制切换防抖节流。
最后在main.js里引用:

import Throttle from '../Throttle'
....
Vue.component('Throttle', Throttle)

使用方式

<div id="app">
    <Throttle :time="1000" events="click">
        <button @click="onClick($event, 1)">click+1 {{val}}</button>
    </Throttle>
    <Throttle :time="1000" events="click" :isDebounce="true">
        <button @click="onAdd">click+3 {{val}}</button>
    </Throttle>
    <Throttle :time="3300" events="mouseleave" :isDebounce="true">
        <button @mouseleave.prevent="onAdd">click+3 {{val}}</button>
    </Throttle>
</div>
const app = new Vue({
    el: '#app',
    data () {
        return {
            val: 0
        }
    },
    methods: {
        onClick ($ev, val) {
            this.val += val
        },
        onAdd () {
            this.val += 3
        }
    }
})

抽象组件是一个接替Mixin实现抽象组件公共功能的好方法,不会因为组件的使用而污染DOM(添加并不想要的div标签等)、可以包裹任意的单一子元素等等

至于用不用抽象组件,就见仁见智了。

5. 性能优化:eventBus封装

**事件总线eventBus的实质就是创建一个vue实例,通过一个空的vue实例作为桥梁实现vue组件间的通信。它是实现非父子组件通信的一种解决方案。

eventBus实现也非常简单

import Vue from 'Vue'
export default new Vue

我们在使用中经常最容易忽视,又必然不能忘记的东西,那就是:清除事件总线eventBus

不手动清除,它是一直会存在,这样当前执行时,会反复进入到接受数据的组件内操作获取数据,原本只执行一次的获取的操作将会有多次操作。本来只会触发并只执行一次,变成了多次,这个问题就非常严重。

当不断进行操作几分钟后,页面就会卡顿,并占用大量内存。

所以一般在vue生命周期beforeDestroy或者destroyed中,需要用vue实例的$off方法清除eventBus

beforeDestroy(){
    bus.$off('click')
 }

可当你有多个eventBus时,就需要重复性劳动$off销毁这件事儿。
这时候封装一个 eventBus就是更优的解决方案。

5.1 拥有生命周期的 eventBus

我们从Vue源码Vue.init中可以得知:

 Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid vm实例唯一标识
    vm._uid = uid++
    // ....
    }

每个Vue实例有自己的_uid作为唯一标识,因此我们让EventBus_uid关联起来,并将其改造:

实现来自:让在Vue中使用的EventBus也有生命周期

class EventBus {
  constructor (vue) {
    if (!this.handles) {
      Object.defineProperty(this, 'handles', {
        value: {},
        enumerable: false
      })
    }
    this.Vue = vue
    // _uid和EventName的映射
    this.eventMapUid = {}
  }
  setEventMapUid (uid, eventName) {
    if (!this.eventMapUid[uid]) this.eventMapUid[uid] = []
    this.eventMapUid[uid].push(eventName) // 把每个_uid订阅的事件名字push到各自uid所属的数组里
  }
  $on (eventName, callback, vm) {
    // vm是在组件内部使用时组件当前的this用于取_uid
    if (!this.handles[eventName]) this.handles[eventName] = []
    this.handles[eventName].push(callback)
    if (vm instanceof this.Vue) this.setEventMapUid(vm._uid, eventName)
  }
  $emit () {
    let args = [...arguments]
    let eventName = args[0]
    let params = args.slice(1)
    if (this.handles[eventName]) {
      let len = this.handles[eventName].length
      for (let i = 0; i < len; i++) {
        this.handles[eventName][i](...params)
      }
    }
  }
  $offVmEvent (uid) {
    let currentEvents = this.eventMapUid[uid] || []
    currentEvents.forEach(event => {
      this.$off(event)
    })
  }
  $off (eventName) {
    delete this.handles[eventName]
  }
}
// 写成Vue插件形式,直接引入然后Vue.use($EventBus)进行使用
let $EventBus = {}

$EventBus.install = (Vue, option) => {
  Vue.prototype.$eventBus = new EventBus(Vue)
  Vue.mixin({
    beforeDestroy () {
      // 拦截beforeDestroy钩子自动销毁自身所有订阅的事件
      this.$eventBus.$offVmEvent(this._uid) 
    }
  })
}

export default $EventBus

使用:

// main.js中
...
import EventBus from './eventBus.js'
Vue.use(EnemtBus)
...

组件中使用:

 created () {
    let text = Array(1000000).fill('xxx').join(',')
    this.$eventBus.$on('home-on', (...args) => {
      console.log('home $on====>>>', ...args)
      this.text = text
    }, this) // 注意第三个参数需要传当前组件的this,如果不传则需要手动销毁
  },
  mounted () {
    setTimeout(() => {
      this.$eventBus.$emit('home-on', '这是home $emit参数', 'ee')
    }, 1000)
  },
  beforeDestroy () {
    // 这里就不需要手动的off销毁eventBus订阅的事件了
  }

6. webpack插件:真香

6.1 取代uglifyjsTerser Plugin

在二月初项目升级Vue-cli3时遇到了一个问题:uglifyjs不再支持webpack4.0。找了一圈,在Google搜索里查到Terser Plugin这个插件。

我主要用到了其中这几个功能:

  • cache,启用文件缓存。
  • parallel,使用多进程并行来提高构建速度。
  • sourceMap,将错误消息位置映射到模块(储存着位置信息)。
  • drop_console,打包时剔除所有的console语句
  • drop_debugger,打包时剔除所有的debugger语句

作为一个管小组前端的懒B,很多时候写页面会遗留console.log,影响性能。设置个drop_console就非常香。以下配置亲测有效。

const TerserPlugin = require('terser-webpack-plugin')
....
new TerserPlugin({
cache: true,
parallel: true,
sourceMap: true, // Must be set to true if using source-maps in production
terserOptions: {
  compress: {
    drop_console: true,
    drop_debugger: true
  }
}
})

更多的配置请看Terser Plugin

6.2 双端开启 gzip

开启gzip压缩的好处是什么?

可以减小文件体积,传输速度更快。gzip是节省带宽和加快站点速度的有效方法。

  • 服务端发送数据时可以配置 Content-Encoding:gzip,用户说明数据的压缩方式
  • 客户端接受到数据后去检查对应字段的信息,就可以根据相应的格式去解码。
  • 客户端请求时,可以用 Accept-Encoding:gzip,用户说明接受哪些压缩方法。

6.2.1 Webpack开启gzip

这里使用的插件为:CompressionWebpackPlugin

const CompressionWebpackPlugin = require('compression-webpack-plugin')
module.exports = { 
    “plugins”:[new CompressionWebpackPlugin] 
}

具体配置:

const CompressionWebpackPlugin = require('compression-webpack-plugin');

webpackConfig.plugins.push(
    new CompressionWebpackPlugin({
      asset: '[path].gz[query]',
      algorithm: 'gzip',
      test: new RegExp('\\.(js|css)$'),
      // 只处理大于xx字节 的文件,默认:0
      threshold: 10240,
      // 示例:一个1024b大小的文件,压缩后大小为768b,minRatio : 0.75
      minRatio: 0.8 // 默认: 0.8
      // 是否删除源文件,默认: false
      deleteOriginalAssets: false
    })
)

开启gzip前:

开启gzip前

开启gzip后 gzip后的大小从277KB到只有~91.2KB!

6.2.2 扩展知识:Nginxgzip设置

打开/etc/nginx/conf.d编写以下配置。

server {
    gzip on;
    gzip_static on;    
    gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
    gzip_proxied  any;
    gzip_vary on;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;    
    ...
}

Nginx尝试查找并发送文件/path/to/bundle.js.gz。如果该文件不存在,或者客户端不支持 gzip,Nginx则会发送该文件的未压缩版本。

保存配置后,重新启动Nginx:

$ sudo service nginx restart

开启gzip前

开启gzip后

6.2.3 如何验证gzip

通过使用curl测试每个资源的请求响应,并检查Content-Encoding

显示 Content-Encoding: gzip,即为配置成功

6.2.4 双端Gzip区别及其意义

不同之处在于:

  1. Webpack压缩会在构建运行期间一次压缩文件,然后将这些压缩版本保存到磁盘。

  2. nginx在请求时压缩文件时,某些包可能内置了缓存,因此性能损失只发生一次(或不经常),但通常不同之处在于,这将在响应 HTTP请求时发生。

  3. 对于实时压缩,让上游代理(例如 Nginx)处理 gzip和缓存通常更高效,因为它们是专门为此而构建的,并且不会遭受服务端程序运行时的开销(许多都是用C语言编写的) 。

  4. 使用 Webpack的好处是, Nginx每次请求服务端都要压缩很久才回返回信息回来,不仅服务器开销会增大很多,请求方也会等的不耐烦。我们在 Webpack打包时就直接生成高压缩等级的文件,作为静态资源放在服务器上,这时将 Nginx作为二重保障就会高效很多(请求其它目录资源时)。

  5. 注:具体是在请求时实时压缩,或在构建时去生成压缩文件,就要看项目业务情况。

求一份深圳的内推

本来还想谢谢动态配置表单相关,但篇幅太长也太难写了。

好了,又水完一篇,入正题:

目前本人在(又)准备跳槽,希望各位大佬和HR小姐姐可以内推一份靠谱的深圳前端岗位!996.ICU 就算了。

作者掘金文章总集

公众号


Typescript 的严格模式有多严格?

前言

"use strict" 指令在 JavaScript 1.8.5 (ECMAScript5) 中新增。

至今,前端er们基本都默认开启严格模式敲代码。

那么,你知道Typescript 其实也有属于自己的严格模式吗?

1. Typescript严格模式规则

Typescript严格模式设置为on时,它将使用 strict族下的严格类型规则对项目中的所有文件进行代码验证。规则是:

规则名称 解释
noImplicitAny 不允许变量或函数参数具有隐式any类型。
noImplicitThis 不允许this上下文隐式定义。
strictNullChecks 不允许出现nullundefined的可能性。
strictPropertyInitialization 验证构造函数内部初始化前后已定义的属性。
strictBindCallApply bind, call, apply 更严格的类型检测。
strictFunctionTypes 对函数参数进行严格逆变比较。

2. noImplicitAny

此规则不允许变量或函数参数具有隐式any类型。请看以下示例:

// Javascript/Typescript 非严格模式
function extractIds (list) {
  return list.map(member => member.id)
}

上述例子没有对list进行类型限制,map循环了item的形参member
而在Typescript严格模式下,会出现以下报错:

// Typescript 严格模式
function extractIds (list) {
  //              ❌ ^^^^
  //                 Parameter 'list' implicitly
  //                 has an 'any' type. ts(7006)
  return list.map(member => member.id)
  //           ❌ ^^^^^^
  //              Parameter 'member' implicitly
  //              has an 'any' type. ts(7006)
}

正确写法应是:

// Typescript 严格模式
interface Member {
  id: number
  name: string
}

function extractIds (list: Member[]) {
  return list.map(member => member.id)
}

1.1 浏览器自带事件该如何处理?

浏览器自带事件,比如e.preventDefault(),是阻止浏览器默认行为的关键代码。

这在Typescript 严格模式下是会报错的:

// Typescript 严格模式
function onChangeCheckbox (e) {
  //                    ❌ ^
  //                       Parameter 'e' implicitly
  //                       has an 'any' type. ts(7006)
  e.preventDefault()
  const value = e.target.checked
  validateCheckbox(value)
}


若需要正常使用这类Web API,就需要在全局定义扩展。比如:

// Typescript 严格模式
interface ChangeCheckboxEvent extends MouseEvent {
  target: HTMLInputElement
}

function onChangeCheckbox (e: ChangeCheckboxEvent) {
  e.preventDefault()
  const value = e.target.checked
  validateCheckbox(value)
}

1.2 第三方库也需定义好类型

请注意,如果导入了非Typescript库,这也会引发错误,因为导入的库的类型是any

// Typescript 严格模式
import { Vector } from 'sylvester'
//                  ❌ ^^^^^^^^^^^
//                     Could not find a declaration file 
//                     for module 'sylvester'.
//                     'sylvester' implicitly has an 'any' type. 
//                     Try `npm install @types/sylvester` 
//                     if it exists or add a new declaration (.d.ts)
//                     file containing `declare module 'sylvester';`
//                     ts(7016)

这可能是项目重构Typescript版的一大麻烦,需要专门定义第三方库接口类型

3. noImplicitThis

此规则不允许this上下文隐式定义。请看以下示例:

// Javascript/Typescript 非严格模式
function uppercaseLabel () {
  return this.label.toUpperCase()
}

const config = {
  label: 'foo-config',
  uppercaseLabel
}

config.uppercaseLabel()
// FOO-CONFIG

在非严格模式下,this指向config对象。this.label只需检索config.label

但是,this在函数上进行引用可能是不明确的

// Typescript严格模式
function uppercaseLabel () {
  return this.label.toUpperCase()
  //  ❌ ^^^^
  //     'this' implicitly has type 'any' 
  //     because it does not have a type annotation. ts(2683)
}

如果单独执行this.label.toUpperCase(),则会因为this上下文config不再存在而报错,因为label未定义。

解决该问题的一种方法是避免this在没有上下文的情况下使用函数:

// Typescript strict mode
const config = {
  label: 'foo-config',
  uppercaseLabel () {
    return this.label.toUpperCase()
  }
}

更好的方法是编写接口,定义所有类型,而不是Typescript来推断:

// Typescript strict mode
interface MyConfig {
  label: string
  uppercaseLabel: (params: void) => string
}

const config: MyConfig = {
  label: 'foo-config',
  uppercaseLabel () {
    return this.label.toUpperCase()
  }
}

4. strictNullChecks

此规则不允许出现nullundefined的可能性。请看以下示例:

// Typescript 非严格模式
function getArticleById (articles: Article[], id: string) {
  const article = articles.find(article => article.id === id)
  return article.meta
}

Typescript 非严格模式下,这样写不会有任何问题。但严格模式会非给你搞出点幺蛾子:

“你这样不行,万一find没有匹配到任何值呢?”:

// Typescript strict mode
function getArticleById (articles: Article[], id: string) {
  const article = articles.find(article => article.id === id)
  return article.meta
  //  ❌ ^^^^^^^
  //     Object is possibly 'undefined'. ts(2532)
}

“我星星你个星星!”

于是你会将改成以下模样:

// Typescript strict mode
function getArticleById (articles: Article[], id: string) {
  const article = articles.find(article => article.id === id)
  if (typeof article === 'undefined') {
    throw new Error(`Could not find an article with id: ${id}.`)
  }

  return article.meta
}

“真香!”

5. strictPropertyInitialization

此规则将验证构造函数内部初始化前后已定义的属性。

必须要确保每个实例的属性都有初始值,可以在构造函数里或者属性定义时赋值。

strictPropertyInitialization,这臭长的命名像极了React源码里的众多任性属性)

请看以下示例:

// Typescript non-strict mode
class User {
  username: string;
}

const user = new User();

const username = user.username.toLowerCase();

如果启用严格模式,类型检查器将进一步报错:

class User {
  username: string;
  //    ❌  ^^^^^^
  //     Property 'username' has no initializer
  //     and is not definitely assigned in the constructor
}

const user = new User();
/
const username = user.username.toLowerCase();
 //                 ❌         ^^^^^^^^^^^^
//          TypeError: Cannot read property 'toLowerCase' of undefined

解决方案有四种。

方案#1:允许undefined

username属性定义提供一个undefined类型:

class User {
  username: string | undefined;
}

const user = new User();

username属性可以为string | undefined类型,但这样写,需要在使用时确保值为string类型

const username = typeof user.username === "string"
  ? user.username.toLowerCase()
  : "n/a";

这也太不Typescript了。

方案#2:属性值显式初始化

这个方法有点笨,却挺有效:

class User {
  username = "n/a";
}

const user = new User();

// OK
const username = user.username.toLowerCase();

方案#3:在构造函数中赋值

最有用的解决方案是向username构造函数添加参数,然后将其分配给username属性。

这样,无论何时new User(),都必须提供默认值作为参数:

class User {
  username: string;

  constructor(username: string) {
    this.username = username;
  }
}

const user = new User("mariusschulz");

// OK
const username = user.username.toLowerCase();

还可以通过public修饰符进一步简化:

class User {
  constructor(public username: string) {}
}

const user = new User("mariusschulz");

// OK
const username = user.username.toLowerCase();

方案#4:显式赋值断言

在某些场景下,属性会被间接地初始化(使用辅助方法或依赖注入库)。

这种情况下,你可以在属性上使用显式赋值断言来帮助类型系统识别类型。

class User {
  username!: string;

  constructor(username: string) {
    this.initialize(username);
  }

  private initialize(username: string) {
    this.username = username;
  }
}

const user = new User("mariusschulz");

// OK
const username = user.username.toLowerCase();

通过向该username属性添加一个明确的赋值断言,我们告诉类型检查器:
username,即使它自己无法检测到该属性,也可以期望该属性被初始化。

6. strictBindCallApply

此规则将对 bind, call, apply 更严格地检测类型。

啥意思?请看以下示例:

// JavaScript
function sum (num1: number, num2: number) {
  return num1 + num2
}

sum.apply(null, [1, 2])
// 3

在你不记得参数类型时,非严格模式下不会校验参数类型和数量,运行代码时,Typescript 和环境(可能是浏览器)都不会引发错误:

// Typescript non-strict mode
function sum (num1: number, num2: number) {
  return num1 + num2
}

sum.apply(null, [1, 2, 3])
// 还是...3?

Typescript严格模式下,这是不被允许的:

// Typescript strict mode
function sum (num1: number, num2: number) {
  return num1 + num2
}

sum.apply(null, [1, 2, 3])
//           ❌ ^^^^^^^^^
//              Argument of type '[number, number, number]' is not 
//              assignable to parameter of type '[number, number]'.
//                Types of property 'length' are incompatible.
//                  Type '3' is not assignable to type '2'. ts(2345)

那怎么办? “...”扩展运算符和reduce老友来相救

// Typescript strict mode
function sum (...args: number[]) {
  return args.reduce<number>((total, num) => total + num, 0)
}

sum.apply(null, [1, 2, 3])
// 6

7. strictFunctionTypes

该规则将检查并限制函数类型参数是抗变(contravariantly)而非双变(bivariantly,即协变或抗变)的。

初看,内心OS: “这什么玩意儿?”,这里有篇介绍:

协变(covariance)和抗变(contravariance)是什么?

协变和逆变维基上写的很复杂,但是总结起来原理其实就一个。

  • 子类型可以隐性的转换为父类型

说个最容易理解的例子,intfloat两个类型的关系可以写成下面这样。
intfloat :也就是说intfloat的子类型。

这一更严格的检查应用于除方法或构造函数声明以外的所有函数类型。方法被专门排除在外是为了确保带泛型的类和接口(如 Array )总体上仍然保持协变。

请看下面这个 AnimalDogCat 的父类型的例子:

declare let f1: (x: Animal) => void;
declare let f2: (x: Dog) => void;
declare let f3: (x: Cat) => void;
f1 = f2;  // 启用 --strictFunctionTypes 时错误
f2 = f1;  // 正确
f2 = f3;  // 错误
  1. 第一个赋值语句在默认的类型检查模式中是允许的,但是在严格函数类型模式下会被标记错误。
  2. 而严格函数类型模式将它标记为错误,因为它不能 被证明合理。
  3. 任何一种模式中,第三个赋值都是错误的,因为它 永远不合理。

用另一种方式来描述这个例子则是,默认类型检查模式中 T在类型 (x: T) => void是 双变的,但在严格函数类型模式中 T是 抗变的:

interface Comparer<T> {
    compare: (a: T, b: T) => number;
}

declare let animalComparer: Comparer<Animal>;
declare let dogComparer: Comparer<Dog>;

animalComparer = dogComparer;  // 错误
dogComparer = animalComparer;  // 正确

写到此处,逼死了一个菜鸡前端。

总结&参考

参考文章:

  1. How strict is Typescript’s strict mode?
  2. 应该怎么理解编程语言中的协变逆变?
  3. TypeScript 严格函数类型

在面试的过程中,常被问到为什么TypescriptJavaScript好用?

从这些严格模式规则,你就可以一窥当中的奥秘,今日开严格,他日Bug秒甩锅,噢耶。

「React Hook」160行代码实现动态炫酷的可视化图表 - 排行榜

前言

某天在逛社区时看到一帖子:

react-dynamic-charts — A React Library for Visualizing Dynamic Data


这是一个国外大佬在其公司峰会的代码竞赛中写的一个库:react-dynamic-charts,用于根据动态数据创建动态图表可视化。

它的设计非常灵活,允许你控制内部的每个元素和事件。使用方法也非常简单,其源码也是非常精炼,值得学习。

但因其提供了不少API,不利于理解源码。所以以下实现有所精简:

1. 准备通用工具函数

1. getRandomColor:随机颜色

const getRandomColor = () => {
  const letters = '0123456789ABCDEF';
  let color = '#';
  for (let i = 0; i < 6; i++) {
    color += letters[Math.floor(Math.random() * 16)]
  }
  return color;
};

2. translateY:填充Y轴偏移量

const translateY = (value) => {
  return `translateY(${value}px)`;
}

2. 使用useState Hook声明状态变量

我们开始编写组件DynamicBarChart

const DynamicBarChart = (props) =>  {
  const [dataQueue, setDataQueue] = useState([]);
  const [activeItemIdx, setActiveItemIdx] = useState(0);
  const [highestValue, setHighestValue] = useState(0);
  const [currentValues, setCurrentValues] = useState({});
  const [firstRun, setFirstRun] = useState(false);
  // 其它代码...
  }

1. useState的简单理解:

const [属性, 操作属性的方法] = useState(默认值);

2. 变量解析

  • dataQueue:当前操作的原始数据数组
  • activeItemIdx: 第几“帧”
  • highestValue: “榜首”的数据值
  • currentValues: 经过处理后用于渲染的数据数组
  • firstRun: 第一次动态渲染时间

3. 内部操作方法和对应useEffect

请配合注释食用

// 动态跑起来~
function start () {
  if (activeItemIdx > 1) {
    return;
  }
  nextStep(true);
}
// 对下一帧数据进行处理
function setNextValues () {
  // 没有帧数时(即已结束),停止渲染
  if (!dataQueue[activeItemIdx]) {
    iterationTimeoutHolder = null;
    return;
  }
  //  每一帧的数据数组
  const roundData = dataQueue[activeItemIdx].values;
  const nextValues = {};
  let highestValue = 0;
  //  处理数据,用作最后渲染(各种样式,颜色)
  roundData.map((c) => {
    nextValues[c.id] = {
      ...c,
      color: c.color || (currentValues[c.id] || {}).color || getRandomColor()
    };

    if (Math.abs(c.value) > highestValue) {
      highestValue = Math.abs(c.value);
    }

    return c;
  });

  // 属性的操作,触发useEffect
  setCurrentValues(nextValues);
  setHighestValue(highestValue);
  setActiveItemIdx(activeItemIdx + 1);
}
// 触发下一步,循环
function nextStep (firstRun = false) {
  setFirstRun(firstRun);
  setNextValues();
}

对应useEffect:

// 取原始数据
useEffect(() => {
  setDataQueue(props.data);
}, []);
// 触发动态
useEffect(() => {
  start();
}, [dataQueue]);
// 设触发动态间隔
useEffect(() => {
  iterationTimeoutHolder = window.setTimeout(nextStep, 1000);
  return () => {
    if (iterationTimeoutHolder) {
      window.clearTimeout(iterationTimeoutHolder);
    }
  };
}, [activeItemIdx]);

useEffect示例:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

为什么要在 effect 中返回一个函数?

这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。

4. 整理用于渲染Dom的数据

const keys = Object.keys(currentValues);
const { barGapSize, barHeight, showTitle } = props;
const maxValue = highestValue / 0.85;
const sortedCurrentValues = keys.sort((a, b) => currentValues[b].value - currentValues[a].value);
const currentItem = dataQueue[activeItemIdx - 1] || {};
  • keys: 每组数据的索引
  • maxValue: 图表最大宽度
  • sortedCurrentValues: 对每组数据进行排序,该项影响动态渲染。
  • currentItem: 每组的原始数据

5. 开始渲染...

大致的逻辑就是:

  1. 根据不同Props,循环排列后的数据:sortedCurrentValues
  2. 计算宽度,返回每项的labelbarvalue
  3. 根据计算好的高度,触发transform
<div className="live-chart">
{
<React.Fragment>
  {
    showTitle &&
    <h1>{currentItem.name}</h1>
  }
  <section className="chart">
    <div className="chart-bars" style={{ height: (barHeight + barGapSize) * keys.length }}>
      {
        sortedCurrentValues.map((key, idx) => {
          const currentValueData = currentValues[key];
          const value = currentValueData.value
          let width = Math.abs((value / maxValue * 100));
          let widthStr;
          if (isNaN(width) || !width) {
            widthStr = '1px';
          } else {
            widthStr = `${width}%`;
          }

          return (
            <div className={`bar-wrapper`} style={{ transform: translateY((barHeight + barGapSize) * idx), transitionDuration: 200 / 1000 }} key={`bar_${key}`}>
              <label>
                {
                  !currentValueData.label
                    ? key
                    : currentValueData.label
                }
              </label>
              <div className="bar" style={{ height: barHeight, width: widthStr, background: typeof currentValueData.color === 'string' ? currentValueData.color : `linear-gradient(to right, ${currentValueData.color.join(',')})` }} />
              <span className="value" style={{ color: typeof currentValueData.color === 'string' ? currentValueData.color : currentValueData.color[0] }}>{currentValueData.value}</span>
            </div>
          );
        })
      }
    </div>
  </section>
</React.Fragment>
}
</div>

6. 定义常规propTypesdefaultProps:

DynamicBarChart.propTypes = {
  showTitle: PropTypes.bool,
  iterationTimeout: PropTypes.number,
  data: PropTypes.array,
  startRunningTimeout: PropTypes.number,
  barHeight: PropTypes.number,
  barGapSize: PropTypes.number,
  baseline: PropTypes.number,
};

DynamicBarChart.defaultProps = {
  showTitle: true,
  iterationTimeout: 200,
  data: [],
  startRunningTimeout: 0,
  barHeight: 50,
  barGapSize: 20,
  baseline: null,
};

export {
  DynamicBarChart
};

7. 如何使用

import React, { Component } from "react";

import { DynamicBarChart } from "./DynamicBarChart";

import helpers from "./helpers";
import mocks from "./mocks";

import "react-dynamic-charts/dist/index.css";

export default class App extends Component {
  render() {
    return (
      <DynamicBarChart
            barGapSize={10}
            data={helpers.generateData(100, mocks.defaultChart, {
              prefix: "Iteration"
            })}
            iterationTimeout={100}
            showTitle={true}
            startRunningTimeout={2500}
          />
      )
  }
}

1. 批量生成Mock数据


helpers.js:

function getRandomNumber(min, max) {
  return Math.floor(Math.random() * (max - min + 1) + min);
};

function generateData(iterations = 100, defaultValues = [], namePrefix = {}, maxJump = 100) {
  const arr = [];
  for (let i = 0; i <= iterations; i++) {
    const values = defaultValues.map((v, idx) => {
      if (i === 0 && typeof v.value === 'number') {
        return v;
      }
      return {
        ...v,
        value: i === 0 ? this.getRandomNumber(1, 1000) : arr[i - 1].values[idx].value + this.getRandomNumber(0, maxJump)
      }
    });
    arr.push({
      name: `${namePrefix.prefix || ''} ${(namePrefix.initialValue || 0) + i}`,
      values
    });
  }
  return arr;
};

export default {
  getRandomNumber,
  generateData
}

mocks.js:

import helpers from './helpers';
const defaultChart = [
  {
    id: 1,
    label: 'Google',
    value: helpers.getRandomNumber(0, 50)
  },
  {
    id: 2,
    label: 'Facebook',
    value: helpers.getRandomNumber(0, 50)
  },
  {
    id: 3,
    label: 'Outbrain',
    value: helpers.getRandomNumber(0, 50)
  },
  {
    id: 4,
    label: 'Apple',
    value: helpers.getRandomNumber(0, 50)
  },
  {
    id: 5,
    label: 'Amazon',
    value: helpers.getRandomNumber(0, 50)
  },
];
export default {
  defaultChart,
}

一个乞丐版的动态排行榜可视化就做好喇。

8. 完整代码

import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './styles.scss';

const getRandomColor = () => {
  const letters = '0123456789ABCDEF';
  let color = '#';
  for (let i = 0; i < 6; i++) {
    color += letters[Math.floor(Math.random() * 16)]
  }
  return color;
};

const translateY = (value) => {
  return `translateY(${value}px)`;
}

const DynamicBarChart = (props) => {
  const [dataQueue, setDataQueue] = useState([]);
  const [activeItemIdx, setActiveItemIdx] = useState(0);
  const [highestValue, setHighestValue] = useState(0);
  const [currentValues, setCurrentValues] = useState({});
  const [firstRun, setFirstRun] = useState(false);
  let iterationTimeoutHolder = null;

  function start () {
    if (activeItemIdx > 1) {
      return;
    }
    nextStep(true);
  }

  function setNextValues () {
    if (!dataQueue[activeItemIdx]) {
      iterationTimeoutHolder = null;
      return;
    }
    
    const roundData = dataQueue[activeItemIdx].values;
    const nextValues = {};
    let highestValue = 0;
    roundData.map((c) => {
      nextValues[c.id] = {
        ...c,
        color: c.color || (currentValues[c.id] || {}).color || getRandomColor()
      };

      if (Math.abs(c.value) > highestValue) {
        highestValue = Math.abs(c.value);
      }

      return c;
    });
    console.table(highestValue);

    setCurrentValues(nextValues);
    setHighestValue(highestValue);
    setActiveItemIdx(activeItemIdx + 1);
  }

  function nextStep (firstRun = false) {
    setFirstRun(firstRun);
    setNextValues();
  }

  useEffect(() => {
    setDataQueue(props.data);
  }, []);

  useEffect(() => {
    start();
  }, [dataQueue]);

  useEffect(() => {
    iterationTimeoutHolder = window.setTimeout(nextStep, 1000);
    return () => {
      if (iterationTimeoutHolder) {
        window.clearTimeout(iterationTimeoutHolder);
      }
    };
  }, [activeItemIdx]);

  const keys = Object.keys(currentValues);
  const { barGapSize, barHeight, showTitle, data } = props;
  console.table('data', data);
  const maxValue = highestValue / 0.85;
  const sortedCurrentValues = keys.sort((a, b) => currentValues[b].value - currentValues[a].value);
  const currentItem = dataQueue[activeItemIdx - 1] || {};

  return (
    <div className="live-chart">
      {
        <React.Fragment>
          {
            showTitle &&
            <h1>{currentItem.name}</h1>
          }
          <section className="chart">
            <div className="chart-bars" style={{ height: (barHeight + barGapSize) * keys.length }}>
              {
                sortedCurrentValues.map((key, idx) => {
                  const currentValueData = currentValues[key];
                  const value = currentValueData.value
                  let width = Math.abs((value / maxValue * 100));
                  let widthStr;
                  if (isNaN(width) || !width) {
                    widthStr = '1px';
                  } else {
                    widthStr = `${width}%`;
                  }

                  return (
                    <div className={`bar-wrapper`} style={{ transform: translateY((barHeight + barGapSize) * idx), transitionDuration: 200 / 1000 }} key={`bar_${key}`}>
                      <label>
                        {
                          !currentValueData.label
                            ? key
                            : currentValueData.label
                        }
                      </label>
                      <div className="bar" style={{ height: barHeight, width: widthStr, background: typeof currentValueData.color === 'string' ? currentValueData.color : `linear-gradient(to right, ${currentValueData.color.join(',')})` }} />
                      <span className="value" style={{ color: typeof currentValueData.color === 'string' ? currentValueData.color : currentValueData.color[0] }}>{currentValueData.value}</span>
                    </div>
                  );
                })
              }
            </div>
          </section>
        </React.Fragment>
      }
    </div>
  );
};

DynamicBarChart.propTypes = {
  showTitle: PropTypes.bool,
  iterationTimeout: PropTypes.number,
  data: PropTypes.array,
  startRunningTimeout: PropTypes.number,
  barHeight: PropTypes.number,
  barGapSize: PropTypes.number,
  baseline: PropTypes.number,
};

DynamicBarChart.defaultProps = {
  showTitle: true,
  iterationTimeout: 200,
  data: [],
  startRunningTimeout: 0,
  barHeight: 50,
  barGapSize: 20,
  baseline: null,
};

export {
  DynamicBarChart
};

styles.scss

.live-chart {
  width: 100%;
  padding: 20px;
  box-sizing: border-box;
  position: relative;
  text-align: center;
  h1 {
    font-weight: 700;
    font-size: 60px;
    text-transform: uppercase;
    text-align: center;
    padding: 20px 10px;
    margin: 0;
  }

  .chart {
    position: relative;
    margin: 20px auto;
  }

  .chart-bars {
    position: relative;
    width: 100%;
  }
  
  .bar-wrapper {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    position: absolute;
    top: 0;
    left: 0;
    transform: translateY(0);
    transition: transform 0.5s linear;
    padding-left: 200px;
    box-sizing: border-box;
    width: 100%;
    justify-content: flex-start;

    label {
      position: absolute;
      height: 100%;
      width: 200px;
      left: 0;
      padding: 0 10px;
      box-sizing: border-box;
      text-align: right;
      top: 50%;
      transform: translateY(-50%);
      font-size: 16px;
      font-weight: 700;
      display: flex;
      justify-content: flex-end;
      align-items: center;
    }

    .value {
      font-size: 16px;
      font-weight: 700;
      margin-left: 10px;
    }

    .bar {
      width: 0%;
      transition: width 0.5s linear;
    }
  }
}

原项目地址:react-dynamic-charts

结语

一直对实现动态排行榜可视化感兴趣,无奈多数都是基于D3echarts实现。
而这个库,不仅脱离图形库,还使用了React 16的新特性。也让我彻底理解了React Hook的妙用。

❤️ 看完三件事

如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

  • 点个「赞」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-)
  • 关注公众号「前端劝退师」,不定期分享原创&精品技术文章。
  • 添加微信:huab119,回复:加群。加入前端劝退师公众号交流群。


懒得clone项目的可以公众号后台回复「可视化」,直接拿核心代码,拖进项目用。

前端废材的自我劝退之路

1. 差不多前端的差不多自白

因为写博客加了许多同行粉丝,有些人会试探性地询问我哪高就,以及是否科班。

多数情况下我基本不回应。并非我高傲冷漠,而是真没可谈的:

  1. 和多数转行从事前端的盆友相似,我基底很浅:非本,无大厂经验。
  2. 入行初衷,只是面向工资择业,以及少许的兴趣(更多是因为找不到工作,被迫自学)。
  3. 履历上也没有拿得出手的项目经验,面试基本靠运气。

以下是稍微正式点的自我介绍:

我叫“笑妄”,16年地理信息系统专业专科毕业,自学的前端。
目前三年半的经验,前后工作过几家中小公司,做过Python爬虫,也曾在运维开发部混过。前两年的工作,都在为生计挣扎,做码农仅因自身一无所长,看这行工资高,就挤进来了。

微博时代起,我的网名“笑妄”就一直没变过,而今年则多了一个身份:“前端劝退师”。

从前端废材,到掘金/公众号 总阅读破百万的前端劝退师,我耗时10个月达成。

而我也从中获益良多,在不停变法子劝退自己的同时,写了一些对掘友还算有点帮助的文章。

2. 劝退自己,掌控知识

19年初,当我在某速运公司的运维开发部待了近大半年后,开始有了跳槽的念头:

  • 外因:公司各项福利和奇葩手段,使我觉得心理不适。
  • 内因:写了三年的后台系统,需要突破自己的技术瓶颈期。

于是我开始审视更新后的简历,简直惨不忍睹,堪称“三无”前端:无学历,无能力,无亮点。

心想完蛋,拿什么与人争。当时也没什么好的想法,就把面试相关基础补了一遍。而某一天在水群时,看了浪浪@浪里行舟 的一篇《写技术博客那点事》

“从某种意义上说,博客是我最好的学习笔记和个人名片。在IT行业内,技术博客是了解一个开发者最好的方式之一,特别是当你没有一张足够分量的文凭或者一段出彩的工作经历时,你就应该沉下心来好好打磨自己技术,打造自己的博客。往者不可谏,来者犹可追。”

喔吼,那就写吧。就在我着手博客事宜时,我迎来了自己的第一只猫:多多。

于是,我便开启了左手撸猫,右手敲文的快哉生活。

3. 平衡工作和学习(合理安排摸鱼时间)

自3月起,我开始利用攒下来的加班调休时间,陆续的请假去面试。而且当时部门处于空闲期,作为一个不起眼的小虾米,上班逛技术社区也没人理会。

于是我多进程操作:

  1. 每周约1~2次面试,详细记录每次面试的踩坑点。
  2. 将每次面试当成打怪升级,总结成博客大纲。
  3. medium,掘金等技术社区,寻求答案。

总结起来就是,前两天面试,中间一天整理,剩下三四天总结成文。

说来容易,过程却是很煎熬:既不想写堆叠资源list文,也不愿复刻一遍他人的知识。

那咋办?先仿写,再总结。 加上该有的引用,一篇热辣辣的文章就出炉了。

4. 劝退总结

三月面试时,面试吹嘘过自己的博客,当时说自己掘金阅读3W+,而到了今日,已经突破50W关卡,并晋级为掘金共建者。

30篇博客,每篇都是力求有趣,不乏味。涉猎的范围,也不至于前端:

1. Vue相关

2. JavaScript相关

3. 性能优化与前端调试

4. 全栈及网络原理

5. 数据结构与设计模式

6. 数据可视化与交互

这些多数篇章都在3000字以上,早期的更是8000字开外。碍于知识有限,文章多有勘误。在此也感谢各位掘友的指正与支持。

在写完这些文章后,我多多也长成下面模样:

5. 收获与进步

2019年应该是我从业以来技术进步最快的一年:

  1. React/Vue 涉猎源码和性能优化。
  2. 浏览器/计算机网络 原理。
  3. 数据结构与设计模式。
  4. JavaScript及各类库

而我已收获了不少:

  1. 积攒了一定的人气,微信好友从百人到千人。
  2. 工作之余还有副业收入。
  3. 被我带入前端行业的盆友愈多。
  4. 拥有了自己的猫。

6. 这一行烟尘滚滚

“有天若有人问,这一行烟尘滚滚。成败不论,我给自己打几分。我会说我很努力,
也祈祷好运气......一路上,要不是你们这些人,我不可能。” --- 李宗盛《你们》


劝退师的公众号头像是李宗盛,熟悉我的盆友也知道,我将李宗盛当作偶像。

除了他写的歌深入人心,还因为他的人生经历:瓦斯行老板的儿子,中专挂科读多了两年,后靠自己的天赋加努力成为一代音乐教父。

他前二十几岁的经历,像极了我们这些普普通通的转行前端er:

年少成绩不佳,待到出来社会,家中也无IT行业的带路人,只得独自摸索,磕磕碰碰的从事了编程这行。


而我走过不少弯路,但好在有这些人和事赏识/支撑我下去:

  • 感激我偶像李宗盛,谢谢你鼓励了我的灵魂。
  • 感恩我的第一位老大Carson(前IBM技术大拿),若非您从茫茫简历中捞起我来,我可能已经自卑转行了。
  • 感谢 掘金/奇舞周刊/前端大全 等平台的认可,撑起了“前端劝退师”的影响力。
  • 鸣谢各位前端同行的支持,是你们给我了写下去的动力与灵感。


最后,“期望风不伤你心雨不挡你路,甜梦布满冰冷漫漫长夜。还愿星光因你闪指引你路,期望你每天色彩里走过。”

「React Hook 」90行代码,15个元素实现无限滚动

前言

在本篇文章你将会学到:

  • IntersectionObserver API 的用法,以及如何兼容。
  • 如何在React Hook中实现无限滚动。
  • 如何正确渲染多达10000个元素的列表。

    无限下拉加载技术使用户在大量成块的内容面前一直滚动查看。这种方法是在你向下滚动的时候不断加载新内容。

当你使用滚动作为发现数据的主要方法时,它可能使你的用户在网页上停留更长时间并提升用户参与度。随着社交媒体的流行,大量的数据被用户消费。无线滚动提供了一个高效的方法让用户浏览海量信息,而不必等待页面的预加载。

如何构建一个体验良好的无限滚动,是每个前端无论是项目或面试都会碰到的一个课题。

1. 早期的解决方案

关于无限滚动,早期的解决方案基本都是依赖监听scroll事件:

function fetchData() {
  fetch(path).then(res => doSomeThing(res.data));
}

window.addEventListener('scroll', fetchData);

然后计算各种.scrollTop().offset().top等等。

手写一个也是非常枯燥。而且:

  • scroll事件会频繁触发,因此我们还需要手动节流。
  • 滚动元素内有大量DOM,容易造成卡顿。


后来出现交叉观察者IntersectionObserver API
,在与VueReact这类数据驱动视图的框架后,无限滚动的通用方案就出来了。

2. 交叉观察者:IntersectionObserver

const box = document.querySelector('.box');
const intersectionObserver = new IntersectionObserver((entries) => {
  entries.forEach((item) => {
    if (item.isIntersecting) {
      console.log('进入可视区域');
    }
  })
});
intersectionObserver.observe(box);

敲重点:
IntersectionObserver API是异步的,不随着目标元素的滚动同步触发,性能消耗极低。

2.1 IntersectionObserverEntry对象


这里我就粗略的介绍下需要用到的:

IntersectionObserve初试

IntersectionObserverEntry对象

callback函数被调用时,会传给它一个数组,这个数组里的每个对象就是当前进入可视区域或者离开可视区域的对象(IntersectionObserverEntry对象)

这个对象有很多属性,其中最常用的属性是:

  • target: 被观察的目标元素,是一个 DOM 节点对象
  • isIntersecting: 是否进入可视区域
  • intersectionRatio: 相交区域和目标元素的比例值,进入可视区域,值大于0,否则等于0

2.3 options

调用IntersectionObserver时,除了传一个回调函数,还可以传入一个option对象,配置如下属性:

  • threshold: 决定了什么时候触发回调函数。它是一个数组,每个成员都是一个门槛值,默认为[0],即交叉比例(intersectionRatio)达到0时触发回调函数。用户可以自定义这个数组。比如,[0, 0.25, 0.5, 0.75, 1]就表示当目标元素 0%、25%、50%、75%、100% 可见时,会触发回调函数。
  • root: 用于观察的根元素,默认是浏览器的视口,也可以指定具体元素,指定元素的时候用于观察的元素必须是指定元素的子元素
  • rootMargin: 用来扩大或者缩小视窗的的大小,使用css的定义方法,10px 10px 30px 20px表示top、right、bottom 和 left的值
const io = new IntersectionObserver((entries) => {
  console.log(entries);
}, {
  threshold: [0, 0.5],
  root: document.querySelector('.container'),
  rootMargin: "10px 10px 30px 20px",
});

2.4 observer

observer.observer(nodeone); //仅观察nodeOne 
observer.observer(nodeTwo); //观察nodeOne和nodeTwo 
observer.unobserve(nodeOne); //停止观察nodeOne
observer.disconnect(); //没有观察任何节点

3. 如何在React Hook中使用IntersectionObserver

在看Hooks版之前,来看正常组件版的:

class SlidingWindowScroll extends React.Component {
this.$bottomElement = React.createRef();
...
componentDidMount() {
    this.intiateScrollObserver();
}
intiateScrollObserver = () => {
    const options = {
      root: null,
      rootMargin: '0px',
      threshold: 0.1
    };
    this.observer = new IntersectionObserver(this.callback, options);
    this.observer.observe(this.$bottomElement.current);
}
render() {
    return (
    <li className='img' ref={this.$bottomElement}>
    )
}

众所周知,React 16.x后推出了useRef来替代原有的createRef,用于追踪DOM节点。那让我们开始吧:

4. 原理

实现一个组件,可以显示具有15个元素的固定窗口大小的n个项目的列表:
即在任何时候,无限滚动n元素上也仅存在15个DOM节点。

  • 采用relative/absolute 定位来确定滚动位置
  • 追踪两个ref: top/bottom来决定向上/向下滚动的渲染与否
  • 切割数据列表,保留最多15个DOM元素。

5. useState声明状态变量

我们开始编写组件SlidingWindowScrollHook:

const THRESHOLD = 15;
const SlidingWindowScrollHook = (props) =>  {
  const [start, setStart] = useState(0);
  const [end, setEnd] = useState(THRESHOLD);
  const [observer, setObserver] = useState(null);
  // 其它代码...
}

1. useState的简单理解:

const [属性, 操作属性的方法] = useState(默认值);

2. 变量解析

  • start:当前渲染的列表第一个数据,默认为0
  • end: 当前渲染的列表最后一个数据,默认为15
  • observer: 当前观察的视图ref元素

6. useRef定义追踪的DOM元素

const $bottomElement = useRef();
const $topElement = useRef();

正常的无限向下滚动只需关注一个dom元素,但由于我们是固定15个dom元素渲染,需要判断向上或向下滚动。

7. 内部操作方法和和对应useEffect

请配合注释食用:

useEffect(() => {
    // 定义观察
    intiateScrollObserver();
    return () => {
      // 放弃观察
      resetObservation()
  }
},[end])

// 定义观察
const intiateScrollObserver = () => {
    const options = {
      root: null,
      rootMargin: '0px',
      threshold: 0.1
    };
    const Observer = new IntersectionObserver(callback, options)
    // 分别观察开头和结尾的元素
    if ($topElement.current) {
      Observer.observe($topElement.current);
    }
    if ($bottomElement.current) {
      Observer.observe($bottomElement.current);
    }
    // 设初始值
    setObserver(Observer)    
}

// 交叉观察的具体回调,观察每个节点,并对实时头尾元素索引处理
const callback = (entries, observer) => {
    entries.forEach((entry, index) => {
      const listLength = props.list.length;
      // 向下滚动,刷新数据
      if (entry.isIntersecting && entry.target.id === "bottom") {
        const maxStartIndex = listLength - 1 - THRESHOLD;     // 当前头部的索引
        const maxEndIndex = listLength - 1;                   // 当前尾部的索引
        const newEnd = (end + 10) <= maxEndIndex ? end + 10 : maxEndIndex; // 下一轮增加尾部
        const newStart = (end - 5) <= maxStartIndex ? end - 5 : maxStartIndex; // 在上一轮的基础上计算头部
        setStart(newStart)
        setEnd(newEnd)
      }
      // 向上滚动,刷新数据
      if (entry.isIntersecting && entry.target.id === "top") {
        const newEnd = end === THRESHOLD ? THRESHOLD : (end - 10 > THRESHOLD ? end - 10 : THRESHOLD); // 向上滚动尾部元素索引不得小于15
        let newStart = start === 0 ? 0 : (start - 10 > 0 ? start - 10 : 0); // 头部元素索引最小值为0
        setStart(newStart)
        setEnd(newEnd)
        }
    });
}

// 停止滚动时放弃观察
const resetObservation = () => {
    observer && observer.unobserve($bottomElement.current); 
    observer && observer.unobserve($topElement.current);
}

// 渲染时,头尾ref处理
const getReference = (index, isLastIndex) => {
    if (index === 0)
      return $topElement;
    if (isLastIndex) 
      return $bottomElement;
    return null;
}

8. 渲染界面


  const {list, height} = props; // 数据,节点高度
  const updatedList = list.slice(start, end); // 数据切割
  
  const lastIndex = updatedList.length - 1;
  return (
    <ul style={{position: 'relative'}}>
      {updatedList.map((item, index) => {
        const top = (height * (index + start)) + 'px'; // 基于相对 & 绝对定位 计算
        const refVal = getReference(index, index === lastIndex); // map循环中赋予头尾ref
        const id = index === 0 ? 'top' : (index === lastIndex ? 'bottom' : ''); // 绑ID
        return (<li className="li-card" key={item.key} style={{top}} ref={refVal} id={id}>{item.value}</li>);
      })}
    </ul>
  );

9. 如何使用

App.js:

import React from 'react';
import './App.css';
import { SlidingWindowScrollHook } from "./SlidingWindowScrollHook";
import MY_ENDLESS_LIST from './Constants';

function App() {
  return (
    <div className="App">
     <h1>15个元素实现无限滚动</h1>
      <SlidingWindowScrollHook list={MY_ENDLESS_LIST} height={195}/>
    </div>
  );
}

export default App;

定义一下数据 Constants.js:

const MY_ENDLESS_LIST = [
  {
    key: 1,
    value: 'A'
  },
  {
    key: 2,
    value: 'B'
  },
  {
    key: 3,
    value: 'C'
  },
  // 中间就不贴了...
  {
    key: 45,
    value: 'AS'
  }
]

SlidingWindowScrollHook.js:

import React, { useState, useEffect, useRef } from "react";
const THRESHOLD = 15;

const SlidingWindowScrollHook = (props) =>  {
  const [start, setStart] = useState(0);
  const [end, setEnd] = useState(THRESHOLD);
  const [observer, setObserver] = useState(null);
  const $bottomElement = useRef();
  const $topElement = useRef();

  useEffect(() => {
    intiateScrollObserver();
    return () => {
      resetObservation()
  }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  },[start, end])

  const intiateScrollObserver = () => {
    const options = {
      root: null,
      rootMargin: '0px',
      threshold: 0.1
    };
    const Observer = new IntersectionObserver(callback, options)
    if ($topElement.current) {
      Observer.observe($topElement.current);
    }
    if ($bottomElement.current) {
      Observer.observe($bottomElement.current);
    }
    setObserver(Observer)    
  }

  const callback = (entries, observer) => {
    entries.forEach((entry, index) => {
      const listLength = props.list.length;
      // Scroll Down
      if (entry.isIntersecting && entry.target.id === "bottom") {
        const maxStartIndex = listLength - 1 - THRESHOLD;     // Maximum index value `start` can take
        const maxEndIndex = listLength - 1;                   // Maximum index value `end` can take
        const newEnd = (end + 10) <= maxEndIndex ? end + 10 : maxEndIndex;
        const newStart = (end - 5) <= maxStartIndex ? end - 5 : maxStartIndex;
        setStart(newStart)
        setEnd(newEnd)
      }
      // Scroll up
      if (entry.isIntersecting && entry.target.id === "top") {
        const newEnd = end === THRESHOLD ? THRESHOLD : (end - 10 > THRESHOLD ? end - 10 : THRESHOLD);
        let newStart = start === 0 ? 0 : (start - 10 > 0 ? start - 10 : 0);
        setStart(newStart)
        setEnd(newEnd)
      }
      
    });
  }
  const resetObservation = () => {
    observer && observer.unobserve($bottomElement.current);
    observer && observer.unobserve($topElement.current);
  }


  const getReference = (index, isLastIndex) => {
    if (index === 0)
      return $topElement;
    if (isLastIndex) 
      return $bottomElement;
    return null;
  }

  const {list, height} = props;
  const updatedList = list.slice(start, end);
  const lastIndex = updatedList.length - 1;
  
  return (
    <ul style={{position: 'relative'}}>
      {updatedList.map((item, index) => {
        const top = (height * (index + start)) + 'px';
        const refVal = getReference(index, index === lastIndex);
        const id = index === 0 ? 'top' : (index === lastIndex ? 'bottom' : '');
        return (<li className="li-card" key={item.key} style={{top}} ref={refVal} id={id}>{item.value}</li>);
      })}
    </ul>
  );
}
export { SlidingWindowScrollHook };

以及少许样式:

.li-card {
  list-style: none;
  box-shadow: 2px 2px 9px 0px #bbb;
  padding: 70px 10px;
  margin-bottom: 20px;
  border-radius: 10px;
  position: absolute;
  left: 30px;
  width: 80%;
}

然后你就可以慢慢耍了。。。

10. 兼容性处理

IntersectionObserver不兼容Safari?

莫慌,我们有polyfill


每周34万下载量呢,放心用

❤️ 看完三件事

如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

  1. 点赞,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)
  2. 关注公众号「前端劝退师」,不定期分享原创知识。
  3. 也看看其它文章

Chrome Devtools 高级调试指南(新)

前言

本文暂未涉及Performance面板的内容。
后续会单独出一篇,以下是目录:

  1. 常用命令和调试
  2. 黑盒脚本:Blackbox Script
  3. 控制台内置指令
  4. 远程调试WebView

1. Chrome Devtools 的用处

  • 前端开发:开发预览、远程调试、性能调优、bug跟踪、断点调试等
  • 后端开发:网络抓包、开发调试Response
  • 测试:服务端API数据是否正确、审查页面元素样式及布局、页面加载性能分析、自动化测试
  • 其他:安装扩展插件,如AdBlock、Gliffy、Axure

2. 菜单面板拆解

  • Elements - 页面dom元素
  • Console - 控制台
  • Sources - 页面静态资源
  • Network - 网络
  • Performance - 设备加载性能分析
  • Application - 应用信息,PWA/Storage/Cache/Frames
  • Security - 安全分析
  • Audits - 审计,自动化测试工具

3. 常用命令和调试

1. 呼出快捷指令面板:cmd + shift + p

Devtools打开的情况下,键入cmd + shift + p将其激活,然后开始在栏中键入要查找的命令或输入"?"号以查看所有可用命令。

  • ...: 打开文件
  • :: 前往文件
  • @:前往标识符(函数,类名等)
  • !: 运行脚本文件
  • >: 打开某菜单功能

1.性能监视器:> performance monitor


将显示性能监视器以及相关信息,例如CPU,JS堆大小和DOM节点。

2.FPS实时监控性能:> FPS选择第一项

3.截图单个元素:> screen 选择Capture node screenhot


2. DOM断点调试

当你要调试特定元素的DOM中的更改时,可以使用此选项。这些是DOM更改断点的类型:

  • Subtree modifications: 子节点删除或添加时
  • Attributes modifications: 属性修改时
  • Node Removal: 节点删除时

如上图:监听form标签,在input框获得焦点时,触发断点调试

3. 黑盒脚本:Blackbox Script

剔除多余脚本断点。

例如第三方(Javascript框架和库,广告等的堆栈跟踪)。

为避免这种情况并集中精力处理核心代码,在Sources或网络选项卡中打开文件,右键单击并选择Blackbox Script

4. 事件监听器:Event Listener Breakpoints

  1. 点击Sources面板
  2. 展开Event Listener Breakpoints
  3. 选择监听事件类别,触发事件启用断点


如上图:监听了键盘输入事件,就会跳到断点处。

5. 本地覆盖:local overrides

使用我们自己的本地资源覆盖网页所使用的资源。

类似的,使用DevTools的工作区设置持久化,将本地的文件夹映射到网络,在chrome开发者功能里面对css 样式的修改,都会直接改动本地文件,页面重新加载,使用的资源也是本地资源,达到持久化的效果。

  • 创建一个文件夹以在本地添加替代内容;
  • 打开Sources > Overrides > Enable local Overrides,选择本地文件夹
  • 打开Elements,编辑样式,自动生成本地文件。
  • 返回Sources,检查文件,编辑更改。

6. 扩展:local overrides模拟Mock数据

来自:chrome 开发者工具 - local overrides

对于返回json 数据的接口,可以利用该功能,简单模拟返回数据。

比如:

  • api 为: http://www.xxx.com/api/v1/list

  • 在根目录下,新建文件 www.xxx.com/api/v1/listlist 文件中的内容,与正常接口返回格式相同。

对象或者数组类型,从而覆盖掉原接口请求。

4. 控制台内置指令

可以执行常见任务的功能,例如选择DOM元素,触发事件,监视事件,在DOM中添加和删除元素等。

这像是Chrome自身实现的jquery加强版。

1. $(selector, [startNode]):单选择器

document.querySelector的简写
语法:

$('a').href;
$('[test-id="logo-img"]').src;
$('#movie_player').click();


控制台还会预先查询对应的标签,十分贴心。
还可以触发事件,如暂停播放:

此函数还支持第二个参数startNode,该参数指定从中搜索元素的“元素”或Node。此参数的默认值为document

2. $$(选择器,[startNode]):全选择器

document.querySelectorAll的简写,返回一个数组标签元素
语法:

$$('.button')


可以用过循环实现切换全选

或者打印属性


此函数还支持第二个参数startNode,该参数指定从中搜索元素的“元素”或Node。此参数的默认值为document
用法:

var images = $$('img', document.querySelector('.devsite-header-background'));
   for (each in images) {
       console.log(images[each].src);
}

3. $x(path, [startNode])xpath选择器

$x(path) 返回与给定xpath表达式匹配的DOM元素数组。

例如,以下代码返回<p>页面上的所有元素:

$x("//p")


以下代码返回<p>包含<a>元素的所有元素:

$x("//p[a]")

xpath多用于爬虫抓取,前端的同学可能不熟悉。

4. getEventListeners(object):获取指定对象的绑定事件

getEventListeners(object)返回在指定对象上注册的事件侦听器。返回值是一个对象,其中包含每个已注册事件类型(例如,clickkeydown)的数组。每个数组的成员是描述为每种类型注册的侦听器的对象。
用法:

getEventListeners(document);


相对于到监听面板里查事件,这个API便捷多了。

5. 花式console


除了不同等级的warnerror打印外

还有其它非常实用的打印。

1. 变量打印:%s%o%d、和%c

const text = "文本1"
console.log(`打印${text}`)

除了标准的ES6语法,实际上还支持四种字符串输出。
分别是:

console.log("打印 %s", text)
  • %s:字符串
  • %o:对象
  • %d:数字或小数

还有比较特殊的%c,可用于改写输出样式。

console.log('%c 文本1', 'font-size:50px; background: ; text-shadow: 10px 10px 10px blue')


当然,你也可以结合其它一起用(注意占位的顺序)。

const text = "文本1"
console.log('%c %s', 'font-size:50px; background: ; text-shadow: 10px 10px 10px blue', text)

你还可以这么玩:

console.log('%c Auth ', 
            'color: white; background-color: #2274A5', 
            'Login page rendered');
console.log('%c GraphQL ', 
            'color: white; background-color: #95B46A', 
            'Get user details');
console.log('%c Error ', 
            'color: white; background-color: #D33F49', 
            'Error getting user details');

2. 打印对象的小技巧

当我们需要打印多个对象时,经常一个个输出。且看不到对象名称,不利于阅读:

以前我的做法是这么打印:

console.log('hello', hello);
console.log('world', world);

这显然有点笨拙繁琐。其实,输出也支持对象解构:

console.log({hello, world});

3. 布尔断言打印:console.assert()

当你需要在特定条件判断时打印日志,这将非常有用。

  • 如果断言为false,则将一个错误消息写入控制台。
  • 如果断言是true,没有任何反应。

语法

console.assert(assertion,obj)

用法

const value = 1001
console.assert(value===1000,"value is not 1000")

4. 给console编组:console.group()

当你需要将详细信息分组或嵌套在一起以便能够轻松阅读日志时,可以使用此功能。

console.group('用户列表');
console.log('name: 张三');
console.log('job: 🐶前端');
// 内层
console.group('地址');
console.log('Street: 123 街');
console.log('City: 北京');
console.log('State: 在职');
console.groupEnd(); // 结束内层
console.groupEnd(); // 结束外层

5. 测试执行效率:console.time()

没有Performance API 精准,但胜在使用简便。

let i = 0;
console.time("While loop");
while (i < 1000000) {
  i++;
}
console.timeEnd("While loop");
console.time("For loop");
for (i = 0; i < 1000000; i++) {
  // For Loop
}
console.timeEnd("For loop");

6. 输出表格:console.table()

这个适用于打印数组对象。。。

let languages = [
    { name: "JavaScript", fileExtension: ".js" },
    { name: "TypeScript", fileExtension: ".ts" },
    { name: "CoffeeScript", fileExtension: ".coffee" }
];
console.table(languages);

7. 打印DOM对象节点:console.dir()

打印出该对象的所有属性和属性值.
console.dir()console.log()的作用区别并不明显。若用于打印字符串,则输出一摸一样。

console.log("Why,hello!");
console.dir("Why,hello!");


在输出对象时也仅是显示不同(log识别为字符串输出,dir直接打印对象。)。

唯一显著区别在于打印dom对象上:

console.log(document)
console.dir(document)


一个打印出纯标签,另一个则是输出DOM树对象。

6. 远程调试WebView

使用Chrome开发者工具在原生Android应用中调试WebView

  1. 配置WebViews进行调试。

    WebView类上调用静态方法setWebContentsDebuggingEnabled

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        WebView.setWebContentsDebuggingEnabled(true);
    }
    
  2. 手机打开usb调试,插上电脑。

  3. Chrome地址栏输入:Chrome://inspect


正常的话在App中打开WebView时,chrome中会监听到并显示你的页面。
4. 点击页面下的inspect,就可以实时看到手机上WebView页面的显示状态了。(第一次使用可能会白屏,这是因为需要去https://chrome-devtools-frontend.appspot.com那边下载文件)

除了inspect标签,还有 Focus tab:

  • 它会自动触发Android设备上的相同操作

其他应用里的WebView也可以,例如这是某个应用里的游戏,用的也是网页:

参考资料

  1. Practical Chrome Devtools — Common commands & Debugging
  2. Mobile web specialist — Remote Debugging
  3. Console Utilities API Reference
  4. Console API Reference

❤️ 看完三件事

如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

  1. 点赞,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)
  2. 关注公众号「前端劝退师」,不定期分享原创知识。
  3. 也看看其它文章

Web开发密码学指南

前言

密码学是各种安全应用程序所必需的,现代密码学旨在创建通过应用数学原理和计算机科学来保护信息的机制。但相比之下,密码分析旨在解密此类机制,以便获得对信息的非法访问。

密码学具有三个关键属性:

  • 机密性,为了防止未经授权的各方访问信息(换句话说,是要确保只有经过授权的人才能访问受限制的数据)。
  • 完整性,是指保护信息不被随意篡改
  • 真实性,与识别信息的所有者有关。

例如个人医疗数据:

  • 机密性,个人医疗数据需要保密,这意味着只有医生或医护人员才能访问它。
  • 完整性,还必须保护其完整性,因为篡改此类数据可能导致错误的诊断或治疗,并给患者带来健康风险。
  • 真实性,患者数据应与已识别的个人联系起来,且患者需要知道操作者(医生)是谁。

在本文中,我们将从加密,哈希,编码和混淆四种密码学基础技术来入门。

1. 什么是加密?

加密定义:以保证机密性的方式转换数据的过程。

为此,加密需要使用一个保密工具,就密码学而言,我们称其为“密钥”。

加密密钥和任何其他加密密钥应具有一些属性:

  • 为了保护机密性,密钥的值应难以猜测。
  • 应该在单个上下文中使用它,避免在不同上下文中重复使用(类比JS作用域)。密钥重用会带来安全风险,如果规避了其机密性,则影响更大,因为它“解锁”了更敏感的数据。

1.1 加密的分类:对称和非对称

加密分为两类:对称和非对称

对称加密:

用途:文件系统加密,Wi-Fi保护访问(WPA),数据库加密(例如信用卡详细信息)

非对称加密:


用途: TLSVPNSSH

其主要区别是:所需的密钥数量

  • 在对称加密算法中,单个密用于加密和解密数据。只有那些有权访问数据的人才能拥有单个共享密钥。
  • 在非对称加密算法中,使用了两个密钥:一个是公用密钥,一个是私有密钥。顾名思义,私钥必须保密,而每个人都可以知道公钥。
    • 应用加密时,将使用公钥,而解密则需要私钥。
    • 任何人都应该能够向我们发送加密数据,但是只有我们才能够解密和读取它。
  1. 通常使用非对称加密来在不安全的通道上进行通信时,两方之间会安全地建立公共密钥。
  2. 通过此共享密钥,双方切换到对称加密。
  3. 这种加密速度更快,更适合处理大量数据。

能被密码界承认的加密算法都是公开的:

  • 某些公司使用专有或“军事级”加密技术进行加密,这些技术是“私有的”。且基于“复杂“算法,但这不是加密的工作方式。
  • 密码界广泛使用和认可的所有加密算法都是公开的,因为它们基于数学算法,只有拥有密钥或先进的计算能力才能解决。
  • 公开算法是得到广泛采用,证明了其价值的。

2. 什么是哈希?

哈希算法定义:·一种只能加密,不能解密的密码学算法,可以将任意长度的信息转换成一段固定长度的字符串。

加密算法是可逆的(使用密钥),并且可以提供机密性(某些较新的加密算法也可以提供真实性),而哈希算法是不可逆的,并且可以提供完整性,以证明未修改特定数据。

哈希算法的前提很简单:给定任意长度的输入,输出特定长度的字节。在大多数情况下,此字节序列对于该输入将是唯一的,并且不会给出输入是什么的指示。换一种说法:

  1. 仅凭哈希算法的输出,是无法确定原始数据的。
  2. 取一些任意数据以及使用哈希算法输出,就可以验证此数据是否与原始输入数据匹配,从而无需查看原始数据。

为了说明这一点,请想象一个强大的哈希算法通过将每个唯一输入放在其自己的存储桶中而起作用。当我们要检查两个输入是否相同时,我们可以简单地检查它们是否在同一存储桶中。

散列文件的存储单位称为桶(Bucket)

2.1 例子一:资源下载

提供文件下载的网站通常会返回每个文件的哈希值,以便用户可以验证其下载副本的完整性。

例如,在Debian的图像下载服务中,您会找到其他文件,例如SHA256SUMS,其中包含可供下载的每个文件的哈希输出(在本例中为SHA-256算法)。

  • 下载文件后,可以将其传递给选定的哈希算法,输出一段哈希值
  • 用该哈希值来与校验和文件中列出的哈希值作匹配,以校验是否一致。

在终端中,可以用openssl来对文件进行哈希处理:

$ openssl sha256 /Users/hiro/Downloads/非对称.png
SHA256(/Users/hiro/Downloads/非对称.png)= 7c264efc9ea7d0431e7281286949ec4c558205f690c0df601ff98d59fc3f4f64

同一个文件采用相同的hash算法时,就可以用来校验是否同源。

在强大的哈希算法中,如果有两个不同的输入,则几乎不可能获得相同的输出。

而相反的,如果计算后的结果范围有限,就会存在不同的数据经过计算后得到的值相同,这就是哈希冲突。(两个不同的数据计算后的结果一样)

这种称为:哈希碰撞(哈希冲突)

如果两个不同的输入最终出现在同一个存储桶中,则会发生冲突。如MD5SHA-1,就会出现这种情况。这是有问题的,因为我们无法区分哪个碰撞的值匹配输入。

强大的哈希算法几乎会为每个唯一输入创建一个新存储桶。

2.2 例子二:网站登陆

web开发中,哈希算法使用最频繁的是在网站登陆应用上:

绝大多数的网站,在将登陆数据存入时,都会将密码哈希后存储。

  • 这是为了避免他人盗取数据库信息后,还原出你的初始输入。
  • 且下次登录时,Web应用程序将再次对你的密码进行哈希处理,并将此哈希与之前存储的哈希进行比较。
  • 如果哈希匹配,即使Web应用程序中没有实际的密码存储,Web应用程序也确信你知道密码。

注册:

登陆:

哈希算法的一个有趣的方面是:无论输入数据的长度如何,散列的输出始终是相同的长度。

从理论上讲,碰撞冲突将始终在可能性的范围之内,尽管可能性很小。

与之相反的是编码

3. 什么是编码?

编码定义:将数据从一种形式转换为另一种形式的过程,与加密无关

它不保证机密性,完整性和真实性这三种加密属性,因为:

  • 不涉及任何秘密且是完全可逆的。
  • 通常会输出与输入值成比例的数据量,并且始终是该输入的唯一值。
  • 编码方法被认为是公共的,普遍用于数据处理
  • 编码永远不适用于操作安全性相关

3.1 URL编码

又叫百分号编码,是统一资源定位(URL)编码方式。URL地址(常说网址)规定了:

  • 常用地数字,字母可以直接使用,另外一批作为特殊用户字符也可以直接用(/,:@等)
  • 剩下的其它所有字符必须通过%xx编码处理。

现在已经成为一种规范了,基本所有程序语言都有这种编码,如:

  • js:encodeURI、encodeURIComponent
  • PHP:urlencode、urldecode等。

编码方法很简单,在该字节ascii码的16进制字符前面加%. 如 空格字符,ascii码是32,对应16进制是'20',那么urlencode编码结果是:%20

# 源文本:
The quick brown fox jumps over the lazy dog

# 编码后:
#!shell
%54%68%65%20%71%75%69%63%6b%20%62%72%6f%77%6e%20%66%6f%78%20%6a%75%6d%70%73%20%6f%76%65%72%20%74%68%65%20%6c%61%7a%79%20%64%6f%67

3.2 HTML实体编码

HTML中,需要对数据进行HTML编码以遵守所需的HTML字符格式。转义避免XSS攻击也是如此。

3.3 Base64/32/16编码

base64base32base16可以分别编码转化8位字节为6位、5位、4位。

16,32,64分别表示用多少个字符来编码,

Base64常用于在通常处理文本数据的场合,表示、传输、存储一些二进制数据。包括MIMEemail,email via MIME,在XML中存储复杂数据。

编码原理:

  1. Base64编码要求把3个8位字节转化为4个6位的字节
  2. 之后在6位的前面补两个0,形成8位一个字节的形式
  3. 6位2进制能表示的最大数是2的6次方是64,这也是为什么是64个字符的原因
    • A-Z,a-z,0-9,+,/这64个编码字符,=号不属于编码字符,而是填充字符

Base64映射表,如下:


举个栗子:

引自:一篇文章彻底弄懂Base64编码原理

  • 第一步:“M”、“a”、"n"对应的ASCII码值分别为77,97,110,对应的二进制值是01001101、01100001、01101110。如图第二三行所示,由此组成一个24位的二进制字符串。
  • 第二步:如图红色框,将24位每6位二进制位一组分成四组。
  • 第三步:在上面每一组前面补两个0,扩展成32个二进制位,此时变为四个字节:00010011、00010110、00000101、00101110。分别对应的值(Base64编码索引)为:19、22、5、46。
  • 第四步:用上面的值在Base64编码表中进行查找,分别对应:T、W、F、u。因此“Man”Base64编码之后就变为:TWFu。

上面的示例旨在指出,编码的用例仅是数据处理,而不为编码的数据提供保护。

4. 什么是混淆?

混淆定义:将人类可读的字符串转换为难以理解的字符串

  • 与加密相反,混淆处理不包含加密密钥。
  • 与编码类似,混淆不能保证任何安全性,尽管有时会误将其用作加密方法

尽管不能保证机密性,但混淆仍有其它应用:

  • 用于防止篡改和保护知识产权。
  • APP源代码通常在打包之前就被混淆了
    • 因为源代码位于用户的设备中,可以从中提取代码。由于混淆后代码不友好,因此会阻止逆向工程,从而有助于保护知识产权。
    • 反过来,这可以防止篡改代码并将其重新分发以供恶意使用。

但是,如此存在许多有助于消除应用程序代码混淆的工具。那就是其它话题了。。。

4.1 例子一:JavaScript混淆

JavaScript源代码:

function hello(name) {
  console.log('Hello, ' + name);
}

hello('New user');

混淆后:

var _0xa1cc=["\x48\x65\x6C\x6C\x6F\x2C\x20","\x6C\x6F\x67","\x4E\x65\x77\x20\x75\x73\x65\x72"];
function hello(_0x2cc8x2){console[_0xa1cc[1]](_0xa1cc[0]+ _0x2cc8x2)}hello(_0xa1cc[2])

总结

从机密性,完整性,真实性分析四种密码技术:

加密 哈希 编码 混淆
机密性
完整性
真实性
  • 加密,虽然是为了保证数据的机密性,但某些现代加密算法还采用了其他策略来保证数据的完整性(有时通过嵌入式哈希算法)和真实性。
  • 哈希,只能保证完整性,但可以通过完整性对比来做权限控制,如:基于哈希的消息认证码(HMAC)和某些传输层安全性(TLS)方法。
  • 编码,过去曾被用来表示加密,并在技术领域之外仍具有这种含义,但在编程世界中,它仅是一种数据处理机制,从未提供任何安全措施
  • 混淆,可以用来提高抵御攻击的能力;但是,它永远不能保证数据的机密性。狡猾的对手最终将绕过混淆策略。与编码一样,永远不要将混淆视为可靠的安全控制

后记 & 引用

颜值即正义!这几个库颠覆你对数据交互的想象

1. 手绘风图表库:roughViz.js

基于D3(v5), roughjs, 和handy

1.1 衡量方式

有三种衡量方式:

粗糙度:

线条种类:

线条粗细:

1.2 多种搭配

简答CDN:

<script src="https://unpkg.com/[email protected]"></script>

npm:

npm install rough-viz

react/vue:

npm install react-roughviz
npm install vue-roughviz

甚至在Python中也可以:

pip install roughviz

1.3 简单使用

首先定义两个div

<div id="vis0"></div>
<div id="vis1"></div>

之后new两个实例:

new roughViz.BarH(
  {
    element: '#vis0',
    title: "Vehicles I've Had",
    titleFontSize: '1.5rem',
    legend: false,
    margin: {top: 50, bottom: 100, left: 160, right: 0},
    data: {
      labels: ['1992 Ford Aerostar Van', '2013 Kia Rio', '1980 Honda CB 125s', '1992 Toyota Tercel'],
      values: [8, 4, 6, 2]
    },
    xLabel: 'Time Owned (Years)',
    strokeWidth: 2,
    fillStyle: 'zigzag-line',
    highlight: 'gold',
  }
);

  new roughViz.BarH(
    {
      element: '#vis1',
      titleFontSize: '1.5rem',
      data: 'https://raw.githubusercontent.com/jwilber/random_data/master/owTanks.csv'
      color: 'tan',
      labels: 'name',
      values: 'health',
      title: "Overwatch Tank Health",
      roughness: 4,
    }
  );

整个的配置非常简洁,其中:

  • data: 数据源,支持简单对象或csv格式的文件
  • roughness: 线条粗糙混乱层级。如果调成10,就会变成这样:

线上体验demo: https://blockbuilder.org/jwilber/419fa6d878fe6c0f79a28f9fc72d7ec6

具体用法请参照官方文档:https://github.com/jwilber/roughViz

2. 抖音字体爆炸特效:react-three-fiber

Webreact-native都可用的高性能Threejs for react库。

可以在React外部驱动渲染循环,而不会产生额外开销。

最新版本采用了Hooks的写法,不像以往强行兼容的Threejs,写起来更加友好。

不止抖音字体爆炸特效,它能实现什么,源于你的技术和想象力。

以下一部分特效:

如果有人学会了...大佬带带?


抖音爆炸特效的实现:

其中用到一个库:react-spring,这是react最优秀的动画库,没有之一。

官方文档:https://github.com/react-spring/react-three-fiber

字体爆炸:https://codesandbox.io/s/y3j31r13zz

3. 播放器里的颜值担当:Mini Music Player - VueJS

国外友人写的一个Vue.js音乐播放器,好看的不得了。

其中的交互和逻辑,也是非常精炼。

源码:https://codepen.io/JavaScriptJunkie/pen/qBWrRyg

4. UI都夸好的卡片验证库:interactive-paycard

这个11月Vue新库一发布,就狂揽3k+star,过于优秀。

完整库名vue-interactive-paycard

React版的作者表示也即将发布了。

源码:https://github.com/muhammederdem/vue-interactive-paycard/issues

5. 真*动态可视化数据:SandDance

微软出品,必属精品

SandDance是使用Vega进行图表布局,使用Deck.gl进行WebGL渲染。

能在如此密集的数据量上保持动画流畅和美观的,也就微软爸爸能做到了。

我先跪了,你们随意。

此外,该库还有多种使用方式:

  1. Power BI软件内使用:
    • PowerBI是微软发布的一款数据可视化软件,可以在较短时间内生成各种报表。
  2. VSCode插件形式:
  3. 网页版和React:

官网:https://sanddance.js.org/

体验:https://sanddance.js.org/app/

6. 实现一个自己的AR: AR.js+Three.js+Autodesk 3D

这是个很有意思的实现,大致流程是:

  1. 手机开启浏览器
  2. AR.js程序开始
  3. ARToolKit识别到图片标记
  4. A-Frame.js开始调用Three.js渲染3D模型
  5. 在画面上显示

6.1 实现步骤

1. 查找模型

首先我们先到 https://sketchfab.com下载自己喜欢的3D模型

2. 下载3D模型

下载glTF格式(A框架提供glTFOBJ两个格式官网建议使用glTF

3. 创建index.html并把这些代码都贴上

<script src="https://aframe.io/releases/0.9.0/aframe.min.js"></script>
<script src="https://rawgit.com/jeromeetienne/ar.js/master/aframe/build/aframe-ar.js"></script>
<script>THREEx.ArToolkitContext.baseURL = 'https://rawgit.com/jeromeetienne/ar.js/master/three.js/'</script>

<body style='margin : 0px; overflow: hidden;'>
    <a-scene embedded arjs='sourceType: webcam; debugUIEnabled: false;'>
        <a-marker type='pattern' url='res/pattern-marker.patt'>
            <a-entity position='-3 2 0' text="width: 5; value:I am Psyduck. We are pokemon. We love you"></a-entity>
            <a-entity position='0 0 0' gltf-model="url(res/scene.gltf)"></a-entity>
        </a-marker>
    </a-scene>
</body>
  • 第1〜3行:把js套件都约会进来
  • 第6行:使用A-framehtml标签添加一个a-scene摄像头并把AR.js崁入
  • 第7行:标记Marker如果标记的Marker出现在摄像头里就会执行下方的事情
  • 第8行:新增你想要跟对方说的话
  • 第9行:新增3D模型

4. 部署你的应用。

5. 制作一个可供识别的二维码

6. 制作一张实体卡片

7. 扫一扫

原文:AR用AR.js做一個讓另對方 喔喔喔喔! 的小卡片吧!

请欣赏一个价值2199刀的模型

还有超赞的《这个杀手不太冷》女孩模型

这也太好看了吧。

4. 后记&引用

原本想凑齐十个再发的,但找了好久,都没什么开源库能入我法眼。

恳请大家,推荐几款*得不行的开源库,我来补充补充,谢谢喇。

❤️ 看完三件事

如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

  1. 点赞,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)
  2. 关注公众号「前端劝退师」,不定期分享原创知识。
  3. 也看看其它文章

也可以来我的GitHub博客里拿所有文章的源文件:

前端劝退指南https://github.com/roger-hiro/BlogFN

现代浏览器观察者API 指南

前言

前段时间在研究前端异常监控/埋点平台的实现。

在思考方案时,想到了浏览器自带的观察者以及页面生命周期API 。

于是在翻查资料时意外发现,原来现代浏览器支持多达四种不同类型的观察者:

  • Intersection Observer,交叉观察者。
  • Mutation Observer,变动观察者。
  • Resize Observer,视图观察者。
  • Performance Observer,性能观察者
IntersectionObserver MutationObserver ResizeObserver PerformanceObserver
用途 观察一个元素是否在视窗可见 观察DOM中的变化 观察视口大小的变化 监测性能度量事件
方法 observe()
disconnect()
takeRecords()
observe()
disconnect()
takeRecords()
unobserve()
observe()
disconnect()
unobserve()
observe()
disconnect()
takeRecords()
取代 Dom Mutation events getBoundingClientRect() 返回元素的大小及其相对于可视窗口的位置

Scroll 和 Resize 事件
Resize 事件 Performance 接口
用途 1. 无限滚动
2. 图片懒加载
3. 兴趣埋点
4. 控制动画/视频执行(性能优化)
1. 更高性能的数据绑定及响应
2. 实现视觉差滚动
3. 图片预加载
4. 实现富文本编辑器
1. 更智能的响应式布局(取代@media
2. 响应式组件
1. 更细颗粒的性能监控
2. 分析性能对业务的影响(交互快/慢是否会影响销量)

1. IntersectionObserver:交叉观察者

IntersectionObserver接口,提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法,祖先元素与视窗(viewport)被称为根(root)

1. 出现的意义

想要计算Web页面的元素的位置,非常依赖于DOM状态的显式查询。但这些查询是同步的,会导致昂贵的样式计算开销(重绘和回流),且不停轮询会导致大量的性能浪费。


于是便发展了以下的几种方案:

  • 构建DOM和数据的自定义预加载和延迟加载。
  • 实现了数据绑定的高性能滚动列表,该列表加载和呈现数据集的子集。
  • 通过scroll等事件或通过插件的形式,计算真实元素可见性。

而它们都有几项共同特点:

  1. 基本实现形式都是查询各个元素相对与某些元素(全局视口)的“被动查询”。
  2. 信息可以异步传递(例如从另一个线程传递),且没有统一捕获错误的处理。
  3. web平台支持匮乏,各有各家的处理。需要开发人员消耗大量精力兼容。

2. IntersectionObserver的优势

Intersection Observer API通过为开发人员提供一种新方法来异步查询元素相对于其他元素或全局视口的位置,从而解决了上述问题:

  • 异步处理消除了昂贵的DOM和样式查询,连续轮询以及使用自定义插件的需求。
  • 通过消除对这些方法的需求,可以使应用程序显着降低CPUGPU和资源成本。

3. IntersectionObserver基本使用

使用IntersectionObserver API主要需要三个步骤:

  1. 创建观察者
  2. 定义回调事件
  3. 定义要观察的目标对象

1.创建观察者

const options = {
    root: document.querySelector('.scrollContainer'),
    rootMargin: '0px',
    threshold: [0.3, 0.5, 0.8, 1] }
    
const observer = new IntersectionObserver(handler, options)

这几个参数用大白话解释就是:

  1. root:指定一个根元素
  2. rootMargin:使用类似于设置CSS边距的语法来指定根边距(根元素的观察影响范围)
  3. threshold:阈值,可以为数组。[0.3]意味着,当目标元素在根元素指定的元素内可见30%时,调用处理函数。

2. 定义回调事件

当目标元素与根元素通过阈值相交时,就会触发回调函数。

function handler (entries, observer) { 
    entries.forEach(entry => { 
    // 每个成员都是一个IntersectionObserverEntry对象。
    // 举例来说,如果同时有两个被观察的对象的可见性发生变化,entries数组就会有两个成员。
    // entry.boundingClientRect 
    // entry.intersectionRatio 
    // entry.intersectionRect 
    // entry.isIntersecting 
    // entry.rootBounds 
    // entry.target 
    // entry.time 
    }); 
}
  • time 时间戳
  • rootBounds 根元素的位置
  • boundingClientRect 目标元素的位置信息
  • intersectionRect 交叉部分的位置信息
  • intersectionRatio 目标元素的可见比例,看下图示
  • target。

3. 定义要观察的目标对象

任何目标元素都可以通过调用.observer(target)方法来观察。

const target = document.querySelector(“.targetBox”); 
observer.observe(target);

此外,还有两个方法:

停止对某目标的监听

observer.unobserve(target)

终止对所有目标的监听

observer.disconnect()

4. 例子1:图片懒加载

HTML:

<img src="placeholder.png" data-src="img-1.jpg">
<img src="placeholder.png" data-src="img-2.jpg">
<img src="placeholder.png" data-src="img-3.jpg">
<!-- more images -->

脚本:

let observer = new IntersectionObserver(
(entries, observer) => { 
entries.forEach(entry => {
    /* 替换属性 */
    entry.target.src = entry.target.dataset.src;
    observer.unobserve(entry.target);
  });
}, 
{rootMargin: "0px 0px -200px 0px"});

document.querySelectorAll('img').forEach(img => { observer.observe(img) });

上述例子表示 仅在到达视口距离底部200px视加载图片。

5. 例子2:兴趣埋点

关于兴趣埋点,一个比较通用的方案是:

来自:《超好用的API之IntersectionObserver》

const boxList = [...document.querySelectorAll('.box')]

var io = new IntersectionObserver((entries) =>{
  entries.forEach(item => {
    // intersectionRatio === 1说明该元素完全暴露出来,符合业务需求
    if (item.intersectionRatio === 1) {
      // 。。。 埋点曝光代码
      io.unobserve(item.target)
    }
  })
}, {
  root: null,
  threshold: 1, // 阀值设为1,当只有比例达到1时才触发回调函数
})

// observe遍历监听所有box节点
boxList.forEach(box => io.observe(box))

至于怎样评断用户是否感兴趣,记录方式就见仁见智了:

  • 位于屏幕中间,并停留时长大于2秒,计数一次。
  • 区域悬停,触发定时器记录时间。
  • PC端记录鼠标点击次数/悬停时间,移动端记录touch事件

这里就不展开写了(我懒)。

6. 控制动画/视频 执行

这里提供控制视频的版本

HTML:

<video src="OSRO-animation.mp4" controls=""></video>

js:

let video = document.querySelector('video');
let isPaused = false; /* Flag for auto-paused video */
let observer = new IntersectionObserver((entries, observer) => { 
  entries.forEach(entry => {
    if(entry.intersectionRatio!=1  && !video.paused){
      video.pause(); isPaused = true;
    }
    else if(isPaused) {video.play(); isPaused=false}
  });
}, {threshold: 1});
observer.observe(video);

效果:

2. Mutation Observer:变动观察者

接口提供了监视对DOM树所做更改的能力。它被设计为旧的MutationEvents功能的替代品,该功能是DOM3 Events规范的一部分。

1. 出现的意义


归根究底,是MutationEvents的功能不尽人意:

  1. MDN中也写到了,是被DOM Event承认在API上有缺陷,反对使用。
  2. 核心缺陷是:性能问题和跨浏览器支持。
  3. DOM添加 mutation 监听器极度降低进一步修改DOM文档的性能(慢1.5 - 7倍),此外, 移除监听器不会逆转的损害。

来自:《监听DOM加载完成及改变——MutationObserver应用》

MutationEvents的原理:通过绑定事件监听DOM

乍一看到感觉很正常,那列一下相关监听的事件:

DOMAttributeNameChanged
DOMCharacterDataModified
DOMElementNameChanged
DOMNodeInserted
DOMNodeInsertedIntoDocument
DOMNodeRemoved
DOMNodeRemovedFromDocument
DOMSubtreeModified

甭记,这么多事件,各内核各版本浏览器想兼容怕是要天荒地老。

2. MutationObserver的优势

Mutation Observer的优势在于:

  • MutationEvents事件是同步触发,也就是说,DOM 的变动立刻会触发相应的事件;
  • Mutation Observer 则是异步触发,DOM 的变动并不会马上触发,而是要等到当前所有 DOM 操作都结束才触发。
  • 可以通过配置项,监听目标DOM下子元素的变更记录

简单讲:异步**!

3. MutationObserver基本使用

使用MutationObserver API主要需要三个步骤:

  1. 创建观察者
  2. 定义回调函数
  3. 定义要观察的目标对象

1. 创建观察者

let observer = new MutationObserver(callback);

2. 定义回调函数

上面代码中的回调函数,会在每次 DOM 变动后调用。该回调函数接受两个参数,第一个是变动数组,第二个是观察器实例,下面是一个例子:

function callback (mutations, observer) {
  mutations.forEach(function(mutation) {
    console.log(mutation);
  });
});

其中每个mutation都对应一个MutationRecord对象,记录着DOM每次发生变化的变动记录

MutationRecord对象包含了DOM的相关信息,有如下属性:

属性 意义
type 观察的变动类型(attributecharacterData或者childList
target 发生变动的DOM节点
addedNodes 新增的DOM节点
removedNodes 删除的DOM节点
previousSibling 前一个同级节点,如果没有则返回null
nextSibling 下一个同级节点,如果没有则返回null
attributeName 发生变动的属性。如果设置了attributeFilter,则只返回预先指定的属性
oldValue 变动前的值。这个属性只对attributecharacterData变动有效,如果发生childList变动,则返回null

3. 定义要观察的目标对象

MutationObserver.observe(dom, options)

启动监听,接收两个参数。

  • 第一参数:被观察的DOM节点。
  • 第二参数:配置需要观察的变动项options
mutationObserver.observe(content, {
    attributes: true, // Boolean - 观察目标属性的改变
    characterData: true, // Boolean - 观察目标数据的改变(改变前的数据/值)
    childList: true, // Boolean - 观察目标子节点的变化,比如添加或者删除目标子节点,不包括修改子节点以及子节点后代的变化
    subtree: true, // Boolean - 目标以及目标的后代改变都会观察
    attributeOldValue: true, // Boolean - 表示需要记录改变前的目标属性值
    characterDataOldValue: true, // Boolean - 设置了characterDataOldValue可以省略characterData设置
    // attributeFilter: ['src', 'class'] // Array - 观察指定属性
});

优先级 :

  1. attributeFilter/attributeOldValue > attributes
  2. characterDataOldValue > characterData
  3. attributes/characterData/childList(或更高级特定项)至少有一项为true;
  4. 特定项存在, 对应选项可以忽略或必须为true

此外,还有两个方法:

停止观察。调用后不再触发观察器,解除订阅

MutationObserver.disconnect()

清除变动记录。即不再处理未处理的变动。该方法返回变动记录的数组,注意,该方法立即生效。

MutationObserver.takeRecords()

4. 例子1:MutationObserver监听文本变化

基本使用是:

const target = document.getElementById('target-id')

const observer = new MutationObserver(records => {
  // 输入变更记录
})

// 开始观察
observer.observe(target, {
  characterData: true
})

这里可以有几种处理。

  • 聊天的气泡框彩蛋,检测文本中的指定字符串/表情包,触发类似微信聊天的表情落下动画。
  • 输入框的热点话题搜索,当输入“#”号时,启动搜索框预检文本或高亮话题。

有个Vue的小型插件就是这么实现的:

来自:《vue-hashtag-textarea》

5. 例子2: 色块小游戏脚本

这个实现也是秀得飞起:

Hacking the color picker game — MutationObserver

游戏的逻辑很简单,当中间的色块颜色改变时,在时间限制内于底下的选项选择跟它颜色一样的选项就得分。难的点在于越后面的关卡选项越多,而且选项颜色也越相近,例如:

其实原理非常简单,就是观察色块的backgroundColor(属性变化attributes),然后触发点击事件e.click()

var targetNode = document.querySelector('#kolor-kolor');
var config = { attributes: true };
var callback = function(mutationsList, observer) {
    if (mutationsList[0].type == 'attributes') {
        console.log('attribute change!');
        let ans = document.querySelector('#kolor-kolor').style.backgroundColor;
        document.querySelectorAll('#kolor-options a').forEach( (e) => {
            if (e.style.backgroundColor == ans) {
                e.text = 'Ans!';
                e.click()
            }
        })
    }
};

var observer = new MutationObserver(callback);
observer.observe(targetNode, config);

3. ResizeObserver,视图观察者

ResizeObserver API是一个新的JavaScript API,与IntersectionObserver API非常相似,它们都允许我们去监听某个元素的变化。

1. 出现的意义

  • 开发过程当中经常遇到的一个问题就是如何监听一个 div 的尺寸变化。

  • 但众所周知,为了监听 div 的尺寸变化,都将侦听器附加到 window 中的 resize 事件。

  • 但这很容易导致性能问题,因为大量的触发事件。

  • 换句话说,使用
    window.resize 通常是浪费的,因为它告诉我们每个视窗大小的变化,而不仅仅是当一个元素的大小发生变化。

  • 而且resize事件会在一秒内触发将近60次,很容易在改变窗口大小时导致性能问题

比如说,你要调整一个元素的大小,那就需要在 resize 的回调函数 callback() 中调用 getBoundingClientRectgetComputerStyle。不过你要是不小心处理所有的读和写操作,就会导致布局混乱。比如下面这个小示例:

2. ResizeObserver的优势

ResizeObserver API 的核心优势有两点:

  • 细颗粒度的DOM元素观察,而不是window
  • 没有额外的性能开销,只会在绘制前或布局后触发调用

3. ResizeObserver基本使用

使用ResizeObserver API同样也是三个步骤:

  1. 创建观察者
  2. 定义回调函数
  3. 定义要观察的目标对象

1. 创建观察者

let observer = new ResizeObserver(callback);

2. 定义回调函数

const callback = entries => {
    entries.forEach(entry => {
        
    })
}

每一个entry都是一个对象,包含两个属性contentRecttarget

contentRect都是一些位置信息:

属性 作用
bottom top + height的值
height 元素本身的高度,不包含paddingborder
left padding-left的值
right left + width的值
top padidng-top的值
width 元素本身的宽度,不包含paddingborder
x 大小与top相同
y 大小与left相同

3. 定义要观察的目标对象

observer.observe(document.body)

unobserve方法:取消单节点观察

observer.unobserve(document.body)

disconnect方法:取消所有节点观察

observer.disconnect(document.body)

4. 例子1:缩放渐变背景

html

<div class="box">
    <h3 class="info"></h3>
</div>
<div class="box small">
    <h3 class="info"></h3>
</div>

添加点样式:

body {
    width: 100vw;
    height: 100vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding: 2vw;
    box-sizing: border-box;
}
.box {
    text-align: center;
    height: 20vh;
    border-radius: 8px;
    box-shadow: 0 0 4px rgba(0,0,0,.25);
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 1vw
}
.box h3 {
    color: #fff;
    margin: 0;
    font-size: 5vmin;
    text-shadow: 0 0 10px rgba(0,0,0,0.4);
}
.box.small {
    max-width: 550px;
    margin: 1rem auto;
}

JavaScript代码:

const boxes = document.querySelectorAll('.box');
let callbackFired = 0;
const myObserver = new ResizeObserver(entries => {
    for (let entry of entries) {
        callbackFired++
        const infoEl = entry.target.querySelector('.info');
        const width = Math.floor(entry.contentRect.width);
        const height = Math.floor(entry.contentRect.height);
        const angle = Math.floor(width / 360 * 100);
        const gradient = `linear-gradient(${ angle }deg, rgba(0,143,104,1) 50%, rgba(250,224,66,1) 50%)`;
        entry.target.style.background = gradient;
        infoEl.innerText = `
        I'm ${ width }px and ${ height }px tall
        Callback fired: ${callbackFired}
        `;
    }
});
boxes.forEach(box => {
    myObserver.observe(box);
});

当你拖动浏览器窗口,改变其大小时,看到的效果如下:

5. 例子2:响应式Vue组件

  • 假设你要创建一个postItem组件,在大屏上是这样的显示效果

  • 在手机上需要这样的效果:

简单的@media就可以实现:

@media only screen and (max-width: 576px) {
  .post__item {
    flex-direction: column;
  }
  
  .post__image {
    flex: 0 auto;
    height: auto;
  }
}
  • 但这就很容易出现 当你在超过预期的屏幕(过大)查看页面时,会出现以下的布局:

@media查询的最大问题是:

  • 组件响应度取决于屏幕尺寸,而不是响应自身的尺寸。

以下是指令版实现:


使用:

效果:

这是vue-responsive-components库的具体实现代码,还有组件形式的实现,感兴趣的可以去看看。

4. PerformanceObserver:性能观察者

这是一个浏览器和Node.js 里都存在的API,采用相同W3CPerformance Timeline规范

  • 在浏览器中,我们可以使用 window 对象取得window.performancewindow.PerformanceObserver
  • 而在 Node.js 程序中需要perf_hooks 取得性能对象,如下:
    const { PerformanceObserver, performance } = require('perf_hooks');
    

1. 出现的意义

首先来看Performance 接口:

  • 可以获取到当前页面中与性能相关的信息。它是 High Resolution Time API 的一部分,同时也融合了 Performance Timeline APINavigation Timing APUser Timing APIResource Timing API

  • Performance API 是大家熟悉的一个接口,他记录着几种性能指数的庞大对象集合。

  1. 若想获得某项页面加载性能记录,就需要调用performance.getEntries或者performance.getEntriesByName来获得。
  2. 而获得执行效率,也只能通过performance.now来计算。

为了解决上述的问题,在Performance Timeline Level 2中,除了扩展了Performance的基本定义以外,还增加了PerformanceObserver接口。

2. PerformanceObserver的优势

PerformanceObserver是浏览器内部对Performance实现的观察者模式,也是现代浏览器支持的几个 Observer 之一。

来自:《你了解 Performance Timeline Level 2 吗?》

它解决了以下3点问题:

  • 避免不知道性能事件啥时候会发生,需要重复轮训timeline获取记录。
  • 避免产生重复的逻辑去获取不同的性能数据指标
  • 避免其他资源需要操作浏览器性能缓冲区时产生竞态关系。

W3C官网文档鼓励开发人员尽可能使用PerformanceObserver,而不是通过Performance获取性能参数及指标。

3. PerformanceObserver的使用

使用PerformanceObserver API主要需要三个步骤:

  1. 创建观察者
  2. 定义回调函数事件
  3. 定义要观察的目标对象

1. 创建观察者

let observer = new PerformanceObserver(callback); 

2. 定义回调函数事件

const callback = (list, observer) => {
   const entries = list.getEntries();
   entries.forEach((entry) => {
    console.log(“Name: “ + entry.name + “, Type: “ + entry.entryType + “, Start: “ + entry.startTime + “, Duration: “ + entry.duration + “\n”); });
}

其中每一个list都是一个完整的PerformanceObserverEntryList对象:

包含三个方法getEntriesgetEntriesByTypegetEntriesByName

方法 作用
getEntries() 返回一个列表,该列表包含一些用于承载各种性能数据的对象,不做任何过滤
getEntriesByType() 返回一个列表,该列表包含一些用于承载各种性能数据的对象,按类型过滤
getEntriesByName() 返回一个列表,,该列表包含一些用于承载各种性能数据的对象,按名称过滤

3. 定义要观察的目标对象

observer.observe({entryTypes: ["entryTypes"]});

observer.observe(...)方法接受可以观察到的有效的入口类型。这些输入类型可能属于各种性能API,比如User tmingNavigation Timing API。有效的entryType值:

属性 别名 类型 描述
frame, navigation PerformanceFrameTiming, PerformanceNavigationTiming URL 文件的地址。
resource PerformanceResourceTiming URL 所请求资源的解析URL。
mark PerformanceMark DOMString 通过调用创建标记时使用的名称performance.mark()。
measure PerformanceMeasure DOMString 通过调用创建度量时使用的名称performance.measure()。
paint PerformancePaintTiming DOMString 无论是'first-paint'或'first-contentful-paint'。
longtask PerformanceLongTaskTiming DOMString 报告长任务的实例

4. 例子1:静态资源监控

来自:《资源监控》

function filterTime(a, b) {
  return (a > 0 && b > 0 && (a - b) >= 0) ? (a - b) : undefined;
}

let resolvePerformanceTiming = (timing) => {
  let o = {
    initiatorType: timing.initiatorType,
    name: timing.name,
    duration: parseInt(timing.duration),
    redirect: filterTime(timing.redirectEnd, timing.redirectStart), // 重定向
    dns: filterTime(timing.domainLookupEnd, timing.domainLookupStart), // DNS解析
    connect: filterTime(timing.connectEnd, timing.connectStart), // TCP建连
    network: filterTime(timing.connectEnd, timing.startTime), // 网络总耗时

    send: filterTime(timing.responseStart, timing.requestStart), // 发送开始到接受第一个返回
    receive: filterTime(timing.responseEnd, timing.responseStart), // 接收总时间
    request: filterTime(timing.responseEnd, timing.requestStart), // 总时间

    ttfb: filterTime(timing.responseStart, timing.requestStart), // 首字节时间
  };

  return o;
};

let resolveEntries = (entries) => entries.map(item => resolvePerformanceTiming(item));

let resources = {
  init: (cb) => {
    let performance = window.performance || window.mozPerformance || window.msPerformance || window.webkitPerformance;
    if (!performance || !performance.getEntries) {
      return void 0;
    }

    if (window.PerformanceObserver) {
      let observer = new window.PerformanceObserver((list) => {
        try {
          let entries = list.getEntries();
          cb(resolveEntries(entries));
        } catch (e) {
          console.error(e);
        }
      });
      observer.observe({
        entryTypes: ['resource']
      })
    } else {
        window.addEventListener('load', () => {
        let entries = performance.getEntriesByType('resource');
        cb(resolveEntries(entries));
      });
    }
  },
};

参考文章&总结

参考文章有点多:

且都有对应的Polyfills版实现。

网上的总结和文档都深浅不一,如果哪里有错误,欢迎指正。

❤️ 看完三件事

如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

  1. 点赞,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)
  2. 关注公众号「前端劝退师」,不定期分享原创知识。
  3. 也看看其它文章

也可以来我的GitHub博客里拿所有文章的源文件:

前端劝退指南https://github.com/roger-hiro/BlogFN

Vue3.0前的TypeScript 最佳实践.md

前言

其实Vue官方从2.6.X版本开始就部分使用Ts重写了。

我个人对更严格类型限制没有积极的看法,毕竟各类转类型的*写法写习惯了。

然鹅最近的一个项目中,是TypeScript+ Vue,毛计喇,学之...…真香!

注意此篇标题的“前”,本文旨在讲Ts混入框架的使用,不讲Class API

img

1. 使用官方脚手架构建

npm install -g @vue/cli
# OR
yarn global add @vue/cli

新的Vue CLI工具允许开发者 使用 TypeScript 集成环境 创建新项目。

只需运行vue create my-app

然后,命令行会要求选择预设。使用箭头键选择Manually select features

接下来,只需确保选择了TypeScriptBabel选项,如下图:

image-20190611163034679

完成此操作后,它会询问你是否要使用class-style component syntax

然后配置其余设置,使其看起来如下图所示。

image-20190611163127181

Vue CLI工具现在将安装所有依赖项并设置项目。

image-20190611163225739
接下来就跑项目喇。

image-20190611163245714

总之,先跑起来再说。

2. 项目目录解析

通过tree指令查看目录结构后可发现其结构和正常构建的大有不同。

image-20190611163812421

这里主要关注shims-tsx.d.tsshims-vue.d.ts 两个文件

两句话概括:

  • shims-tsx.d.ts,允许你以.tsx结尾的文件,在Vue项目中编写jsx代码
  • shims-vue.d.ts 主要用于 TypeScript 识别.vue 文件,Ts 默认并不支持导入 vue 文件,这个文件告诉 ts 导入.vue 文件都按VueConstructor<Vue>处理。

此时我们打开亲切的src/components/HelloWorld.vue,将会发现写法已大有不同

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <!-- 省略 -->
  </div>
</template>

<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';

@Component
export default class HelloWorld extends Vue {
  @Prop() private msg!: string;
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>

至此,准备开启新的篇章 TypeScript极速入门 和 vue-property-decorator

3. TypeScript极速入门

3.1 基本类型和扩展类型

image-20190611173126273

TypescriptJavascript共享相同的基本类型,但有一些额外的类型。

  • 元组 Tuple
  • 枚举 enum
  • AnyVoid

1. 基本类型合集

// 数字,二、八、十六进制都支持
let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;

// 字符串,单双引都行
let name: string = "bob";
let sentence: string = `Hello, my name is ${ name }.

// 数组,第二种方式是使用数组泛型,Array<元素类型>:
let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3];

let u: undefined = undefined;
let n: null = null;

2. 特殊类型

#####1. 元组 Tuple image-20190611174157121

想象 元组 作为有组织的数组,你需要以正确的顺序预定义数据类型。

const messyArray = [' something', 2, true, undefined, null];
const tuple: [number, string, string] = [24, "Indrek" , "Lasn"]

如果不遵循 为元组 预设排序的索引规则,那么Typescript会警告。

image-20190611174515658

​ (tuple第一项应为number类型)

2. 枚举 enum*

image-20190611174833904

enum类型是对JavaScript标准数据类型的一个补充。 像C#等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。

// 默认情况从0开始为元素编号,也可手动为1开始
enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;

let colorName: string = Color[2];
console.log(colorName);  // 输出'Green'因为上面代码里它的值是2

另一个很好的例子是使用枚举来存储应用程序状态。

image-20190611175542886

3. Void

image-20190611175724302

Typescript中,你必须在函数中定义返回类型。像这样:

image-20190611175858587

若没有返回值,则会报错:

image-20190611175932713

我们可以将其返回值定义为void:

image-20190611180043827

此时将无法 return

4. Any

image-20190611180255381

Emmm...就是什么类型都行,当你无法确认在处理什么类型时可以用这个。

但要慎重使用,用多了就失去使用Ts的意义。

let person: any = "前端劝退师"
person = 25
person = true

主要应用场景有:

  1. 接入第三方库
  2. Ts菜逼前期都用
5. Never

image-20190611180943940

用很粗浅的话来描述就是:"Never是你永远得不到的爸爸。"

具体的行为是:

  • throw new Error(message)
  • return error("Something failed")
  • while (true) {} // 存在无法达到的终点

image-20190611181410052

3. 类型断言

image-20190611182337690

简略的定义是:可以用来手动指定一个值的类型。

有两种写法,尖括号和as:

let someValue: any = "this is a string";

let strLength: number = (<string>someValue).length;
let strLength: number = (someValue as string).length;

使用例子有:

当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法:

function getLength(something: string | number): number {
    return something.length;
}

// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
//   Property 'length' does not exist on type 'number'.

如果你访问长度将会报错,而有时候,我们确实需要在还不确定类型的时候就访问其中一个类型的属性或方法,此时需要断言才不会报错:

function getLength(something: string | number): number {
    if ((<string>something).length) {
        return (<string>something).length;
    } else {
        return something.toString().length;
    }
}

3.2 泛型:Generics

软件工程的一个主要部分就是构建组件,构建的组件不仅需要具有明确的定义和统一的接口,同时也需要组件可复用。支持现有的数据类型和将来添加的数据类型的组件为大型软件系统的开发过程提供很好的灵活性。

C#Java中,可以使用"泛型"来创建可复用的组件,并且组件可支持多种数据类型。这样便可以让用户根据自己的数据类型来使用组件。

1. 泛型方法

在TypeScript里,声明泛型方法有以下两种方式:

function gen_func1<T>(arg: T): T {
    return arg;
}
// 或者
let gen_func2: <T>(arg: T) => T = function (arg) {
    return arg;
}

调用方式也有两种:

gen_func1<string>('Hello world');
gen_func2('Hello world'); 
// 第二种调用方式可省略类型参数,因为编译器会根据传入参数来自动识别对应的类型。

2. 泛型与Any

Ts 的特殊类型 Any 在具体使用时,可以代替任意类型,咋一看两者好像没啥区别,其实不然:

// 方法一:带有any参数的方法
function any_func(arg: any): any {
    console.log(arg.length);
		return arg;
}

// 方法二:Array泛型方法
function array_func<T>(arg: Array<T>): Array<T> {
	  console.log(arg.length);
		return arg;
}
  • 方法一,打印了arg参数的length属性。因为any可以代替任意类型,所以该方法在传入参数不是数组或者带有length属性对象时,会抛出异常。
  • 方法二,定义了参数类型是Array的泛型类型,肯定会有length属性,所以不会抛出异常。

3. 泛型类型

泛型接口:

interface Generics_interface<T> {
    (arg: T): T;
}
 
function func_demo<T>(arg: T): T {
    return arg;
}

let func1: Generics_interface<number> = func_demo;
func1(123);     // 正确类型的实际参数
func1('123');   // 错误类型的实际参数

3.3 自定义类型:Interface vs Type alias

Interface,国内翻译成接口。

Type alias,类型别名。

image-20190613192317416

以下内容来自:

Typescript 中的 interface 和 type 到底有什么区别

1. 相同点

都可以用来描述一个对象或函数:

interface User {
  name: string
  age: number
}

type User = {
  name: string
  age: number
};

interface SetUser {
  (name: string, age: number): void;
}
type SetUser = (name: string, age: number): void;

都允许拓展(extends):

interfacetype 都可以拓展,并且两者并不是相互独立的,也就是说interface可以 extends type, type 也可以 extends interface虽然效果差不多,但是两者语法不同

interface extends interface

interface Name { 
  name: string; 
}
interface User extends Name { 
  age: number; 
}

type extends type

type Name = { 
  name: string; 
}
type User = Name & { age: number  };

interface extends type

type Name = { 
  name: string; 
}
interface User extends Name { 
  age: number; 
}

type extends interface

interface Name { 
  name: string; 
}
type User = Name & { 
  age: number; 
}

2. 不同点

type 可以而 interface 不行

  • type 可以声明基本类型别名,联合类型,元组等类型
// 基本类型别名
type Name = string

// 联合类型
interface Dog {
    wong();
}
interface Cat {
    miao();
}

type Pet = Dog | Cat

// 具体定义数组每个位置的类型
type PetList = [Dog, Pet]
  • type 语句中还可以使用 typeof 获取实例的 类型进行赋值
// 当你想获取一个变量的类型时,使用 typeof
let div = document.createElement('div');
type B = typeof div
  • 其他*操作
type StringOrNumber = string | number;  
type Text = string | { text: string };  
type NameLookup = Dictionary<string, Person>;  
type Callback<T> = (data: T) => void;  
type Pair<T> = [T, T];  
type Coordinates = Pair<number>;  
type Tree<T> = T | { left: Tree<T>, right: Tree<T> };

interface 可以而 type 不行

interface 能够声明合并

interface User {
  name: string
  age: number
}

interface User {
  sex: string
}

/*
User 接口为 {
  name: string
  age: number
  sex: string 
}
*/

interface 有可选属性和只读属性

  • 可选属性

    接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。 例如给函数传入的参数对象中只有部分属性赋值了。带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个?符号。如下所示

interface Person {
  name: string;
  age?: number;
  gender?: number;
}
  • 只读属性

    顾名思义就是这个属性是不可写的,对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用 readonly来指定只读属性,如下所示:

interface User {
    readonly loginName: string;
    password: string;
}

上面的例子说明,当完成User对象的初始化后loginName就不可以修改了。

3.4 实现与继承:implementsvsextends

extends很明显就是ES6里面的类继承,那么implement又是做什么的呢?它和extends有什么不同?

implement,实现。与C#或Java里接口的基本作用一样,TypeScript也能够用它来明确的强制一个类去符合某种契约

implement基本用法

interface IDeveloper {
   name: string;
   age?: number;
}
// OK
class dev implements IDeveloper {
    name = 'Alex';
    age = 20;
}
// OK
class dev2 implements IDeveloper {
    name = 'Alex';
}
// Error
class dev3 implements IDeveloper {
    name = 'Alex';
    age = '9';
}

extends是继承父类,两者其实可以混着用:

 class A extends B implements C,D,E

搭配 interfacetype的用法有:

image-20190612003025759

3.5 声明文件与命名空间:declare namespace

前面我们讲到Vue项目中的shims-tsx.d.tsshims-vue.d.ts,其初始内容是这样的:

// shims-tsx.d.ts
import Vue, { VNode } from 'vue';

declare global {
  namespace JSX {
    // tslint:disable no-empty-interface
    interface Element extends VNode {}
    // tslint:disable no-empty-interface
    interface ElementClass extends Vue {}
    interface IntrinsicElements {
      [elem: string]: any;
    }
  }
}

// shims-vue.d.ts
declare module '*.vue' {
  import Vue from 'vue';
  export default Vue;
}

declare:当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。

这里列举出几个常用的:

declare var 声明全局变量
declare function 声明全局方法
declare class 声明全局类
declare enum 声明全局枚举类型
declare global 扩展全局变量
declare module 扩展模块

namespace:“内部模块”现在称做“命名空间”

module X { 相当于现在推荐的写法 namespace X {)

跟其他 JS 库协同

类似模块,同样也可以通过为其他 JS 库使用了命名空间的库创建 .d.ts 文件的声明文件,如为 D3 JS 库,可以创建这样的声明文件:

declare namespace D3{
    export interface Selectors { ... }
}
declare var d3: D3.Base;

所以上述两个文件:

  • shims-tsx.d.ts, 在全局变量 global中批量命名了数个内部模块。
  • shims-vue.d.ts,意思是告诉 TypeScript *.vue 后缀的文件可以交给 vue 模块来处理。

3.6 访问修饰符:privatepublicprotected

其实很好理解:

  1. 默认为public

  2. 当成员被标记为private时,它就不能在声明它的类的外部访问,比如:

    class Animal {
      private name: string;
    
      constructor(theName: string) {
        this.name = theName;
      }
    }
    
    let a = new Animal('Cat').name; //错误,‘name’是私有的
    
  3. protectedprivate类似,但是,protected成员在派生类中可以访问

    class Animal {
      protected name: string;
    
      constructor(theName: string) {
        this.name = theName;
      }
    }
    
    class Rhino extends Animal {
         constructor() {
              super('Rhino');
        }         
        getName() {
            console.log(this.name) //此处的name就是Animal类中的name
        }
    } 
    

3.7 可选参数 ( ?: )和非空断言操作符(!.)

可选参数

function buildName(firstName: string, lastName?: string) {
    return firstName + ' ' + lastName
}

// 错误演示
buildName("firstName", "lastName", "lastName")
// 正确演示
buildName("firstName")
// 正确演示
buildName("firstName", "lastName")

非空断言操作符:

能确定变量值一定不为空时使用。

与可选参数 不同的是,非空断言操作符不会防止出现 null 或 undefined。

let s = e!.name;  // 断言e是非空并访问name属性

4. Vue组件的Ts写法

从 vue2.5 之后,vue 对 ts 有更好的支持。根据官方文档,vue 结合 typescript ,有两种书写方式:

**Vue.extend **

  import Vue from 'vue'

  const Component = Vue.extend({
  	// type inference enabled
  })

vue-class-component

import { Component, Vue, Prop } from 'vue-property-decorator'

@Component
export default class Test extends Vue {
  @Prop({ type: Object })
  private test: { value: string }
}

理想情况下,Vue.extend 的书写方式,是学习成本最低的。在现有写法的基础上,几乎 0 成本的迁移。

但是Vue.extend 模式,需要与mixins 结合使用。在 mixin 中定义的方法,不会被 typescript 识别到

,这就意味着会出现丢失代码提示、类型检查、编译报错等问题。

菜鸟才做选择,大佬都挑最好的。直接讲第二种吧:

4.1 vue-class-component

image-20190613013846506

我们回到src/components/HelloWorld.vue

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <!-- 省略 -->
  </div>
</template>

<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';

@Component
export default class HelloWorld extends Vue {
  @Prop() private msg!: string;
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>

有写过python的同学应该会发现似曾相识:

  • vue-property-decorator这个官方支持的库里,提供了函数 **装饰器(修饰符)**语法

1. 函数修饰符 @

“@”,与其说是修饰函数倒不如说是引用、调用它修饰的函数。

或者用句大白话描述:@: "下面的被我包围了。"

举个栗子,下面的一段代码,里面两个函数,没有被调用,也会有输出结果:

test(f){
    console.log("before ...");
    f()
		console.log("after ...");
 }

@test
func(){
	console.log("func was called");
}

直接运行,输出结果:

before ...
func was called
after ...

上面代码可以看出来:

  • 只定义了两个函数: testfunc,没有调用它们。
  • 如果没有“@test”,运行应该是没有任何输出的。

但是,解释器读到函数修饰符“@”的时候,后面步骤会是这样:

  1. 去调用 test函数,test函数的入口参数就是那个叫“func”的函数;

  2. test函数被执行,入口参数的(也就是func函数)会被调用(执行);

换言之,修饰符带的那个函数的入口参数,就是下面的那个整个的函数。有点儿类似JavaScript里面的
function a (function () { ... });

�����鬼鬼��表�����并���个�穿�����鬼鬼��表��.jpg

2. vue-property-decoratorvuex-class提供的装饰器

vue-property-decorator的装饰器:

vuex-class的装饰器:

我们拿原始Vue组件模版来看:

import {componentA,componentB} from '@/components';

export default {
	components: { componentA, componentB},
	props: {
    propA: { type: Number },
    propB: { default: 'default value' },
    propC: { type: [String, Boolean] },
  }
  // 组件数据
  data () {
    return {
      message: 'Hello'
    }
  },
  // 计算属性
  computed: {
    reversedMessage () {
      return this.message.split('').reverse().join('')
    }
    // Vuex数据
    step() {
    	return this.$store.state.count
    }
  },
  methods: {
    changeMessage () {
      this.message = "Good bye"
    },
    getName() {
    	let name = this.$store.getters['person/name']
    	return name
    }
  },
  // 生命周期
  created () { },
  mounted () { },
  updated () { },
  destroyed () { }
}

以上模版替换成修饰符写法则是:

import { Component, Vue, Prop } from 'vue-property-decorator';
import { State, Getter } from 'vuex-class';
import { count, name } from '@/person'
import { componentA, componentB } from '@/components';

@Component({
    components:{ componentA, componentB},
})
export default class HelloWorld extends Vue{
	@Prop(Number) readonly propA!: number | undefined
  @Prop({ default: 'default value' }) readonly propB!: string
  @Prop([String, Boolean]) readonly propC!: string | boolean | undefined
  
  // 原data
  message = 'Hello'
  
  // 计算属性
	private get reversedMessage (): string[] {
  	return this.message.split('').reverse().join('')
  }
  // Vuex 数据
  @State((state: IRootState) => state . booking. currentStep) step!: number
	@Getter( 'person/name') name!: name
  
  // method
  public changeMessage (): void {
    this.message = 'Good bye'
  },
  public getName(): string {
    let storeName = name
    return storeName
  }
	// 生命周期
  private created ():void { },
  private mounted ():void { },
  private updated ():void { },
  private destroyed ():void { }
}

正如你所看到的,我们在生命周期 列表那都添加private XXXX方法,因为这不应该公开给其他组件。

而不对method做私有约束的原因是,可能会用到@Emit来向父组件传递信息。

4.2 添加全局工具

引入全局模块,需要改main.ts:

import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';

Vue.config.productionTip = false;

new Vue({
  router,
  store,
  render: (h) => h(App),
}).$mount('#app');

npm i VueI18n

import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
// 新模块
import i18n from './i18n';

Vue.config.productionTip = false;

new Vue({
    router, 
    store, 
    i18n, // 新模块
    render: (h) => h(App),
}).$mount('#app');

但仅仅这样,还不够。你需要动src/vue-shim.d.ts

// 声明全局方法
declare module 'vue/types/vue' {
  interface Vue {
        readonly $i18n: VueI18Next;
        $t: TranslationFunction;
    }
}

之后使用this.$i18n()的话就不会报错了。

4.3 Axios 使用与封装

Axios的封装千人千面

如果只是想简单在Ts里体验使用Axios,可以安装vue-axios
简单使用Axios

$ npm i axios vue-axios

main.ts添加:

import Vue from 'vue'
import axios from 'axios'
import VueAxios from 'vue-axios'

Vue.use(VueAxios, axios)

然后在组件内使用:

Vue.axios.get(api).then((response) => {
  console.log(response.data)
})

this.axios.get(api).then((response) => {
  console.log(response.data)
})

this.$http.get(api).then((response) => {
  console.log(response.data)
})

1. 新建文件request.ts

文件目录:

-api
    - main.ts   // 实际调用
-utils
    - request.ts  // 接口封装

2. request.ts文件解析

import * as axios from 'axios';
import store from '@/store';
// 这里可根据具体使用的UI组件库进行替换
import { Toast } from 'vant';
import { AxiosResponse, AxiosRequestConfig } from 'axios';
 
 /* baseURL 按实际项目来定义 */
const baseURL = process.env.VUE_APP_URL;

 /* 创建axios实例 */
const service = axios.default.create({
    baseURL,
    timeout: 0, // 请求超时时间
    maxContentLength: 4000,
});

service.interceptors.request.use((config: AxiosRequestConfig) => {
    return config;
}, (error: any) => {
    Promise.reject(error);
});

service.interceptors.response.use(
    (response: AxiosResponse) => {
        if (response.status !== 200) {
            Toast.fail('请求错误!');
        } else {
            return response.data;
        }
    },
    (error: any) => {
        return Promise.reject(error);
    });
    
export default service;

为了方便,我们还需要定义一套固定的 axios 返回的格式,新建ajax.ts

export interface AjaxResponse {
    code: number;
    data: any;
    message: string;
}

3. main.ts接口调用:

// api/main.ts
import request from '../utils/request';

// get
export function getSomeThings(params:any) {
    return request({
        url: '/api/getSomethings',
    });
}

// post
export function postSomeThings(params:any) {
    return request({
        url: '/api/postSomethings',
        methods: 'post',
        data: params
    });
}

5. 编写一个组件

为了减少时间,我们来替换掉src/components/HelloWorld.vue,做一个博客帖子组件:

<template>
	<div class="blogpost">
		<h2>{{ post.title }}</h2>
		<p>{{ post.body }}</p>
		<p class="meta">Written by {{ post.author }} on {{ date }}</p>
	</div>
</template>

<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';

// 在这里对数据进行类型约束
export interface Post {
	title: string;
	body: string;
	author: string;
	datePosted: Date;
}

@Component
export default class HelloWorld extends Vue {
	@Prop() private post!: Post;

	get date() {
		return `${this.post.datePosted.getDate()}/${this.post.datePosted.getMonth()}/${this.post.datePosted.getFullYear()}`;
	}
}
</script>

<style scoped>
h2 {
  text-decoration: underline;
}
p.meta {
  font-style: italic;
}
</style>

然后在Home.vue中使用:

<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png">
   	<HelloWorld v-for="blogPost in blogPosts" :post="blogPost" :key="blogPost.title" />
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import HelloWorld, { Post } from '@/components/HelloWorld.vue'; // @ is an alias to /src

@Component({
  components: {
    HelloWorld,
  },
})
export default class Home extends Vue {
    private blogPosts: Post[] = [
        {
          title: 'My first blogpost ever!',
          body: 'Lorem ipsum dolor sit amet.',
          author: 'Elke',
          datePosted: new Date(2019, 1, 18),
        },
        {
          title: 'Look I am blogging!',
          body: 'Hurray for me, this is my second post!',
          author: 'Elke',
          datePosted: new Date(2019, 1, 19),
        },
        {
          title: 'Another one?!',
          body: 'Another one!',
          author: 'Elke',
          datePosted: new Date(2019, 1, 20),
        },
      ];
}
</script>

这时候运行项目:

image-20190613191025585

这就是简单的父子组件

����.jpg

6. 参考文章

TypeScript — JavaScript with superpowers — Part II

VUE WITH TYPESCRIPT

TypeScript + 大型项目实战

Python修饰符 (一)—— 函数修饰符 “@”

Typescript 中的 interface 和 type到底有什么区别

7. 总结


而关于Class API撤销,其实还是挺舒服的。
class 来编写 Vue组件确实太奇怪了。
(所以我这篇Ts入门压根没写Class API)

作者掘金文章总集

需要转载到公众号的喊我加下白名单就行了。

公众号

「React Hooks」如何用120行代码,实现一个交互良好的拖拽上传组件?

前言

你将在该篇学到:

  • 如何将现有组件改写为 React Hooks函数组件
  • useStateuseEffectuseRef是如何替代原生命周期和Ref的。
  • 一个完整拖拽上传行为覆盖的四个事件:dragoverdragenterdropdragleave
  • 如何使用React Hooks编写自己的UI组件库。

逛国外社区时看到这篇:

How To Implement Drag and Drop for Files in React

文章讲了React拖拽上传的精简实现,但直接翻译照搬显然不是我的风格。

于是我又用React Hooks 重写了一版,除CSS的代码总数 120行。
效果如下:

1. 添加基本目录骨架

app.js

import React from 'react';
import PropTypes from 'prop-types';

import { FilesDragAndDrop } from '../components/Common/FilesDragAndDropHook';

export default class App extends React.Component {
    static propTypes = {};

    onUpload = (files) => {
        console.log(files);
    };

    render() {
        return (
            <div>
                <FilesDragAndDrop
                    onUpload={this.onUpload}
                />
            </div>
        );
    }
}

FilesDragAndDrop.js(非Hooks):

import React from 'react';
import PropTypes from 'prop-types';

import '../../scss/components/Common/FilesDragAndDrop.scss';

export default class FilesDragAndDrop extends React.Component {
    static propTypes = {
        onUpload: PropTypes.func.isRequired,
    };

    render() {
        return (
            <div className='FilesDragAndDrop__area'>
                传下文件试试?
                <span
                    role='img'
                    aria-label='emoji'
                    className='area__icon'
                >
                    &#128526;
                </span>
            </div>
        );
    }
}

1. 如何改写为Hooks组件?

请看动图:

2. 改写组件

Hooks版组件属于函数组件,将以上改造:

import React, { useEffect, useState, useRef } from "react";
import PropTypes from 'prop-types';
import classNames from 'classnames';
import classList from '../../scss/components/Common/FilesDragAndDrop.scss';
const FilesDragAndDrop = (props) => {
    return (
        <div className='FilesDragAndDrop__area'>
            传下文件试试?
            <span
                role='img'
                aria-label='emoji'
                className='area__icon'
            >
                &#128526;
            </span>
        </div>
    );
}

FilesDragAndDrop.propTypes = {
    onUpload: PropTypes.func.isRequired,
    children: PropTypes.node.isRequired,
    count: PropTypes.number,
    formats: PropTypes.arrayOf(PropTypes.string)
}

export { FilesDragAndDrop };

FilesDragAndDrop.scss

.FilesDragAndDrop {
  .FilesDragAndDrop__area {
    width: 300px;
    height: 200px;
    padding: 50px;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-flow: column nowrap;
    font-size: 24px;
    color: #555555;
    border: 2px #c3c3c3 dashed;
    border-radius: 12px;

    .area__icon {
      font-size: 64px;
      margin-top: 20px;
    }
  }
}

然后就可以看到页面:

2. 实现分析

从操作DOM、组件复用、事件触发、阻止默认行为、以及Hooks应用方面分析。

1. 操作DOM:useRef

由于需要拖拽文件上传以及操作组件实例,需要用到ref属性。

React Hooks 中 新增了useRef API
语法

const refContainer = useRef(initialValue);
  • useRef 返回一个可变的 ref 对象,。
  • .current 属性被初始化为传递的参数(initialValue
  • 返回的对象将存留在整个组件的生命周期中。
...
const drop = useRef();

return (
    <div
        ref={drop}
        className='FilesDragAndDrop'
    />
    ...
    )

2. 事件触发


完成具有动态交互的拖拽行为并不简单,需要用到四个事件控制:

  • 区域外:dragleave, 离开范围
  • 区域内:dragenter,用来确定放置目标是否接受放置。
  • 区域内移动:dragover,用来确定给用户显示怎样的反馈信息
  • 完成拖拽(落下):drop,允许放置对象。

这四个事件并存,才能阻止 Web 浏览器默认行为和形成反馈。

3. 阻止默认行为

代码很简单:

e.preventDefault() //阻止事件的默认行为(如在浏览器打开文件)
e.stopPropagation() // 阻止事件冒泡

每个事件阶段都需要阻止,为啥呢?举个🌰栗子:

const handleDragOver = (e) => {
    // e.preventDefault();
    // e.stopPropagation();
};

不阻止的话,就会触发打开文件的行为,这显然不是我们想看到的。

4. 组件内部状态: useState

拖拽上传组件,除了基础的拖拽状态控制,还应有成功上传文件或未通过验证时的消息提醒。
状态组成应为:

state = {
    dragging: false,
    message: {
        show: false,
        text: null,
        type: null,
    },
};

写成对应useState前先回归下写法:

const [属性, 操作属性的方法] = useState(默认值);

于是便成了:

const [dragging, setDragging] = useState(false);
const [message, setMessage] = useState({ show: false, text: null, type: null });

5. 需要第二个叠加层

除了drop事件,另外三个事件都是动态变化的,而在拖动元素时,每隔 350 毫秒会触发 dragover事件。

此时就需要第二ref来统一控制。

所以全部的`ref``为:

const drop = useRef(); // 落下层
const drag = useRef(); // 拖拽活动层

6. 文件类型、数量控制

我们在应用组件时,prop需要传入类型和数量来控制

<FilesDragAndDrop
    onUpload={this.onUpload}
    count={1}
    formats={['jpg', 'png']}
>
    <div className={classList['FilesDragAndDrop__area']}>
        传下文件试试?
<span
            role='img'
            aria-label='emoji'
            className={classList['area__icon']}
        >
            &#128526;
</span>
    </div>
</FilesDragAndDrop>
  • onUpload:拖拽完成处理事件
  • count: 数量控制
  • formats: 文件类型。

对应的组件Drop内部事件:handleDrop:

const handleDrop = (e) => {
    e.preventDefault();
    e.stopPropagation();
    setDragging(false)
    const { count, formats } = props;
    const files = [...e.dataTransfer.files];
    if (count && count < files.length) {
        showMessage(`抱歉,每次最多只能上传${count} 文件。`, 'error', 2000);
        return;
    }
    if (formats && files.some((file) => !formats.some((format) => file.name.toLowerCase().endsWith(format.toLowerCase())))) {
        showMessage(`只允许上传 ${formats.join(', ')}格式的文件`, 'error', 2000);
        return;
    }
    if (files && files.length) {
        showMessage('成功上传!', 'success', 1000);
        props.onUpload(files);
    }
};

.endsWith是判断字符串结尾,如:"abcd".endsWith("cd"); // true

showMessage则是控制显示文本:

const showMessage = (text, type, timeout) => {
    setMessage({ show: true, text, type, })
    setTimeout(() =>
        setMessage({ show: false, text: null, type: null, },), timeout);
};

需要触发定时器来回到初始状态

7. 事件在生命周期里的触发与销毁

原本EventListener的事件需要在componentDidMount添加,在componentWillUnmount中销毁:

componentDidMount () {
    this.drop.addEventListener('dragover', this.handleDragOver);
}

componentWillUnmount () {
    this.drop.removeEventListener('dragover', this.handleDragOver);
}

Hooks中有内部操作方法和对应useEffect来取代上述两个生命周期

useEffect示例:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

每个effect都可以返回一个清除函数。如此可以将添加(componentDidMount)和移除(componentWillUnmount) 订阅的逻辑放在一起。

于是上述就可以写成:

useEffect(() => {
    drop.current.addEventListener('dragover', handleDragOver);
    return () => {
        drop.current.removeEventListener('dragover', handleDragOver);
    }
})


这也太香了吧!!!

3. 完整代码:

FilesDragAndDropHook.js:

import React, { useEffect, useState, useRef } from "react";
import PropTypes from 'prop-types';
import classNames from 'classnames';
import classList from '../../scss/components/Common/FilesDragAndDrop.scss';

const FilesDragAndDrop = (props) => {
    const [dragging, setDragging] = useState(false);
    const [message, setMessage] = useState({ show: false, text: null, type: null });
    const drop = useRef();
    const drag = useRef();
    useEffect(() => {
        // useRef 的 drop.current 取代了 ref 的 this.drop
        drop.current.addEventListener('dragover', handleDragOver);
        drop.current.addEventListener('drop', handleDrop);
        drop.current.addEventListener('dragenter', handleDragEnter);
        drop.current.addEventListener('dragleave', handleDragLeave);
        return () => {
            drop.current.removeEventListener('dragover', handleDragOver);
            drop.current.removeEventListener('drop', handleDrop);
            drop.current.removeEventListener('dragenter', handleDragEnter);
            drop.current.removeEventListener('dragleave', handleDragLeave);
        }
    })
    const handleDragOver = (e) => {
        e.preventDefault();
        e.stopPropagation();
    };

    const handleDrop = (e) => {
        e.preventDefault();
        e.stopPropagation();
        setDragging(false)
        const { count, formats } = props;
        const files = [...e.dataTransfer.files];

        if (count && count < files.length) {
            showMessage(`抱歉,每次最多只能上传${count} 文件。`, 'error', 2000);
            return;
        }

        if (formats && files.some((file) => !formats.some((format) => file.name.toLowerCase().endsWith(format.toLowerCase())))) {
            showMessage(`只允许上传 ${formats.join(', ')}格式的文件`, 'error', 2000);
            return;
        }

        if (files && files.length) {
            showMessage('成功上传!', 'success', 1000);
            props.onUpload(files);
        }
    };

    const handleDragEnter = (e) => {
        e.preventDefault();
        e.stopPropagation();
        e.target !== drag.current && setDragging(true)
    };

    const handleDragLeave = (e) => {
        e.preventDefault();
        e.stopPropagation();
        e.target === drag.current && setDragging(false)
    };

    const showMessage = (text, type, timeout) => {
        setMessage({ show: true, text, type, })
        setTimeout(() =>
            setMessage({ show: false, text: null, type: null, },), timeout);
    };

    return (
        <div
            ref={drop}
            className={classList['FilesDragAndDrop']}
        >
            {message.show && (
                <div
                    className={classNames(
                        classList['FilesDragAndDrop__placeholder'],
                        classList[`FilesDragAndDrop__placeholder--${message.type}`],
                    )}
                >
                    {message.text}
                    <span
                        role='img'
                        aria-label='emoji'
                        className={classList['area__icon']}
                    >
                        {message.type === 'error' ? <>&#128546;</> : <>&#128536;</>}
                    </span>
                </div>
            )}
            {dragging && (
                <div
                    ref={drag}
                    className={classList['FilesDragAndDrop__placeholder']}
                >
                    请放手
                    <span
                        role='img'
                        aria-label='emoji'
                        className={classList['area__icon']}
                    >
                        &#128541;
                    </span>
                </div>
            )}
            {props.children}
        </div>
    );
}

FilesDragAndDrop.propTypes = {
    onUpload: PropTypes.func.isRequired,
    children: PropTypes.node.isRequired,
    count: PropTypes.number,
    formats: PropTypes.arrayOf(PropTypes.string)
}

export { FilesDragAndDrop };

App.js

import React, { Component } from 'react';
import { FilesDragAndDrop } from '../components/Common/FilesDragAndDropHook';
import classList from '../scss/components/Common/FilesDragAndDrop.scss';

export default class App extends Component {
    onUpload = (files) => {
        console.log(files);
    };
    render () {
        return (
            <FilesDragAndDrop
                onUpload={this.onUpload}
                count={1}
                formats={['jpg', 'png', 'gif']}
            >
                <div className={classList['FilesDragAndDrop__area']}>
                    传下文件试试?
            <span
                        role='img'
                        aria-label='emoji'
                        className={classList['area__icon']}
                    >
                        &#128526;
            </span>
                </div>
            </FilesDragAndDrop>
        )
    }
}

FilesDragAndDrop.scss

.FilesDragAndDrop {
  position: relative;

  .FilesDragAndDrop__placeholder {
    position: absolute;
    top: 0;

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.