前端汇总 --006

172 阅读31分钟

一、 2023前端面试真题之JS篇

1.1 手写一个Promise

//Promise/A+规范的三种状态
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class MyPromise {
  // 构造方法接收一个回调
  constructor(executor) {
    this._status = PENDING     // Promise状态
    this._resolveQueue = []    // 成功队列, resolve时触发
    this._rejectQueue = []     // 失败队列, reject时触发

    // 由于resolve/reject是在executor内部被调用, 因此需要使用箭头函数固定this指向, 否则找不到this._resolveQueue
    let _resolve = (val) => {
      if(this._status !== PENDING) return// 对应规范中的"状态只能由pending到fulfilled或rejected"
      this._status = FULFILLED              // 变更状态

      // 这里之所以使用一个队列来储存回调,是为了实现规范要求的 "then 方法可以被同一个 promise 调用多次"
      // 如果使用一个变量而非队列来储存回调,那么即使多次p1.then()也只会执行一次回调
      while(this._resolveQueue.length) {
        const callback = this._resolveQueue.shift()
        callback(val)
      }
    }
    // 实现同resolve
    let _reject = (val) => {
      if(this._status !== PENDING) return// 对应规范中的"状态只能由pending到fulfilled或rejected"
      this._status = REJECTED               // 变更状态
      while(this._rejectQueue.length) {
        const callback = this._rejectQueue.shift()
        callback(val)
      }
    }
    // new Promise()时立即执行executor,并传入resolve和reject
    executor(_resolve, _reject)
  }

  // then方法,接收一个成功的回调和一个失败的回调
  then(resolveFn, rejectFn) {
    this._resolveQueue.push(resolveFn)
    this._rejectQueue.push(rejectFn)
  }
}

代码测试:

const p1 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('result')
  }, 1000);
})
p1.then(res =>console.log(res))
//一秒后输出result

1.2 手写一个迭代器

function makeIterator(array) {
  var nextIndex = 0;
  return {
    next: function() {
      return nextIndex < array.length
        ? { value: array[nextIndex++], done: false }
        : { value: undefined, done: true };
    },
  };
}

代码测试:

var it = makeIterator(["a", "b"]);

it.next(); // { value: "a", done: false }
it.next(); // { value: "b", done: false }
it.next(); // { value: undefined, done: true }

1.3 设计模式的分类

总体来说设计模式分为三大类:(C5S7B11)

  1. 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式
  2. 结构型模式,共七种:适配器模式装饰器模式代理模式、外观模式、桥接模式、组合模式、享元模式。
  3. 行为型模式,共十一种:策略模式、模板方法模式、观察者模式/发布订阅模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

1.31 手写单例模式(创建模式)

let CreateSingleton = (function(){
    let instance;
    return function(name) {
        if (instance) {
            return instance;
        }
        this.name = name;
        return instance = this;
    }
})();
CreateSingleton.prototype.getName = function() {
    console.log(this.name);
}

代码测试:

let Winner = new CreateSingleton('Winner');
let Looser = new CreateSingleton('Looser');

console.log(Winner === Looser); // true
console.log(Winner.getName());  // 'Winner'
console.log(Looser.getName());  // 'Winner'

1.32 手写观察者模式(行为模式)

// 定义observe
const queuedObservers = new Set();
const observe = fn => queuedObservers.add(fn);


const observable = obj => new Proxy(obj, {
  set(target, key, value, receiver) {
    const result = Reflect.set(target, key, value, receiver);
    // notify
    queuedObservers.forEach(observer => observer());
    return result;
  }
});

代码测试:

let obj = observable({
  name:'789'
})

observe(function test(){
  console.log('触发了')
})

obj.name ="前端alex"
// 触发了
// 前端alex

1.33 手写发布订阅 (行为模式)

class Observer {
  caches = {}; // 事件中心
  
  // eventName事件名-独一无二, fn订阅后执行的自定义行为
  on (eventName, fn){ 
    this.caches[eventName] = this.caches[eventName] || [];
    this.caches[eventName].push(fn);
  }
  
  // 发布 => 将订阅的事件进行统一执行
  emit (eventName, data) { 
    if (this.caches[eventName]) {
      this.caches[eventName]
      .forEach(fn => fn(data));
    }
  }
  // 取消订阅 => 若fn不传, 直接取消该事件所有订阅信息
  off (eventName, fn) { 
    if (this.caches[eventName]) {
      const newCaches = fn 
        ? this.caches[eventName].filter(e => e !== fn) 
        : [];
      this.caches[eventName] = newCaches;
    }
  }

}

代码测试:

// 发布订阅
let ob = new Observer();

let l1 = (data) => console.log(`l1_${data}`)
let l2 = (data) => console.log(`l2_${data}`)

ob.on('event1',l1)
ob.on('event1',l2)

//发布订阅
ob.emit('event1',789) 
// l1_789
// l2_789

// 取消,订阅l1
ob.off('event1',l1)

ob.emit('event1',567)
//l2_567

1.34 观察者模式 VS 发布订阅模式

  1. 从表面上看:

    • 观察者模式里,只有两个角色 —— 观察者 + 被观察者
    • 而发布订阅模式里,却不仅仅只有发布者和订阅者两个角色,还有一个经常被我们忽略的 —— {经纪人|Broker}
  2. 往更深层次讲:

    • 观察者和被观察者,是松耦合的关系
    • 发布者和订阅者,则完全不存在耦合
  3. 从使用层面上讲:

    • 观察者模式,多用于单个应用内部
    • 发布订阅模式,则更多的是一种{跨应用的模式|cross-application pattern} ,比如我们常用的消息中间件

1.4 WebGL和canvas的关系

  • Canvas就是画布,只要浏览器支持,可以在canvas上获取2D上下文3D上下文,其中3D上下文一般就是WebGL,当然WebGL也能用于2D绘制,并且WebGL提供硬件渲染加速,性能更好。
  • 但是 WEBGL 的支持性caniuse还不是特别好,所以在不支持 WebGL 的情况下,只能使用 Canvas 2D api,注意这里的降级不是降到 Canvas,它只是一个画布元素,而是降级使用 浏览器提供的 Canvas 2D Api,这就是很多库的兜底策略,如 Three.js, PIXI

1.5 JS 深浅复制

JS在语言层面仅支持浅复制,深复制需要手动实现

1.51 浅复制(3个)

  1. 扩展运算符
  2. Object.assign()
  3. Object.getOwnPropertyDescriptors()+Object.defineProperties()

1.511 扩展运算符(...)复制对象和数组

const copyOfObject = {...originalObject};
const copyOfArray = [...originalArray];

扩展运算符不足和特性。

不足&特性
不能复制普通对象的prototype属性
不能复制内置对象特殊属性(internal slots)
只复制对象的本身的属性(非继承)
只复制对象的可枚举属性(enumerable)
复制的数据属性都是可写的(writable)和可配置的(configurable)

1.512 Object.assign()

Object.assign()的工作方式和扩展运算符类似。

const copy1 = {...original};
const copy2 = Object.assign({}, original);

Object.assign()并非完全和扩展运算符等同,他们之间存在一些细微的差别。

  • 扩展运算符在副本中直接定义新的属性
  • Object.assign()通过赋值的方式来处理副本中对应属性

1.513 Object.getOwnPropertyDescriptors()Object.defineProperties()

JavaScript允许我们通过属性描述符来创建属性。

function copyAllOwnProperties(original) {
  return Object.defineProperties(
    {}, Object.getOwnPropertyDescriptors(original));
}
  1. 能够复制所有自有属性
  2. 能够复制非枚举属性

1.52 深复制

1.521 通过嵌套扩展运算符实现深复制

const original = {name: '789', work: {address: 'BeiJing'}};
const copy = {name: original.name, work: {...original.work}};

original.work !== copy.work // 指向不同的引用地址

1.522 使用JSON实现数据的深复制

先将普通对象,

  1. 先转换为JSON串(stringify)

  2. 然后再解析(parse)该串

function jsonDeepCopy(original) {
      return JSON.parse(JSON.stringify(original));
    }

而通过这种方式有一个很明显的缺点就是:

只能处理JSON所能识别的keyvalue。对于不支持的类型,会被直接忽略掉。

1.53 手动实现

1.531 递归函数实现深复制

实现逻辑就是(FHT

  1. 利用 for-in对对象的属性进行遍历(自身属性+继承属性)
  2. source.hasOwnProperty(i)判断是否是非继承可枚举属性
  3. typeof source[i] === 'object'判断值的类型,如果是对象,递归处理
function clone(source) {
    let target = {};
    for(let i in source) {
        if (source.hasOwnProperty(i)) {
            if (typeof source[i] === 'object') {
                target[i] = clone(source[i]); // 递归处理
            } else {
                target[i] = source[i];
            }
        }
    }

    return target;
}

1.6 闭包

1.61 函数即对象

在JS中,一切皆对象。那从语言的设计层面来讲,函数是一种特殊的对象

image.png

1.62 code 属性

当执行到,一个函数调用语句时,V8 便会从函数对象中取出 code 属性值(也就是函数代码),然后再解释执行这段函数代码。

在解释执行函数代码的时候,又会生成该函数对应的执行上下文,并被推入到调用栈里

1.63 闭包是如何产生的?

产生闭包的核心两步:

  1. 预扫描内部函数
  2. 内部函数引用的外部变量保存到堆中
function test() {
    var myName = "fn_outer"
    let age = 78;
    var innerObj = {
        getName:function(){
            console.log(age);
            return myName
        },
        setName:function(newName){
            myName = newName
        }
    }
    return innerObj
}
var t = test();

V8 执行test 函数时

  • 首先会编译,并创建一个空执行上下文。

    • 编译过程中,遇到内部函数 setNameV8还要对内部函数做一次快速的词法扫描(预扫描) 发现该内部函数引用了 外部函数(test)中的 myName 变量
    • 由于是内部函数引用了外部函数的变量,所以 V8 判断这是一个闭包
    • 于是在堆空间创建换一个closure(test)的对象 (这是一个内部对象,JavaScript 是无法访问的),用来保存 myName 变量
  • test 函数执行结束之后,返回的 getNamesetName 方法都引用clourse(test)对象。

    • 即使 test 函数退出了,clourse(test)依然被其内部的 getNamesetName 方法引用。
  • 所以在下次调用t.setName或者t.getName时,在进行变量查找时候,根据作用域链来查找。

1.7 his 的设计缺陷以及应对方案

1.72 嵌套函数中的 内部函数this 不会从外层函数中继承

var myObj = {
  name : " 北宸南蓁 ", 
  showThis: function(){
    console.log(this)
    function inner(){console.log(this)}
    inner()
  }
}
myObj.showThis()
  • 函数 inner 中的 this 指向的是全局 window 对象
  • 函数 showThis 中的 this 指向的是 myObj 对象

1.72 解决方案

1.721 把 this 体系转换为了作用域的体系

var myObj = {
  name : " 北宸 ", 
  showThis: function(){
    console.log(this)
    var self = this
    function inner(){
      self.name = " 南蓁 "
    }
    inner()
  }
}
myObj.showThis()
console.log(myObj.name)
console.log(window.name)

在 showThis 函数中声明一个变量 self 用来保存 this,然后在 inner 函数中使用 self

1.722 使用 ES6 中的箭头函数

var myObj = {
  name : " 北宸 ", 
  showThis: function(){
    console.log(this)
    var inner = ()=>{
      this.name = " 南蓁 "
      console.log(this)
    }
    inner()
  }
}
myObj.showThis()
console.log(myObj.name)
console.log(window.name)

ES6 中的箭头函数并不会创建其自身的执行上下文,所以箭头函数中的 this 取决于它的外部函数

1.73 普通函数中的 this 默认指向全局对象 window

通过设置 JavaScript 的“严格模式”来解决

在严格模式下,默认执行一个函数,其函数的执行上下文中的 this 值是 undefined

1.8 函数式编程,柯里化,redux 中间件

1.81 柯里化

const curry = (fn,arity=fn.length,...args) => 
  arity<=args.length
  ? fn(...args)
  : curry.bind(null,fn,arity,...args)

测试代码:

const add = (a,b,c) => a+b+c; 
curry(add)(1,2,3) // 结果为6

1.82 pplyMiddleware

function applyMiddleware(...middlewares){
  return function(createStore){
    return function(reducer,initialState){
      var store = createStore(reducer,initialState);
      var dispatch = store.dispatch;
      var chain = [];

      var middlewareAPI = {
        getState: store.getState,
        dispatch: (action) => dispatch(action)
      };

      chain = middlewares.map(
          middleware => middleware(middlewareAPI)
          );

      dispatch = compose(...chain)(store.dispatch);
      return { ...store, dispatch };
    }
  }
}

applyMiddleware 函数是一个三级柯里化函数

1.9 图片懒加载

利用JavaScript实现懒加载的3种方式,原理都是判断图片是否出现在可视区后给图片赋值src属性。

1.91 利用HTML提供的 data- 属性来嵌入自定义数据

自定义数据存放这个标签原本的图片地址。

1.911 利用offsetTop计算位置

JavaScript实现当滚动滚动条时,如果图片出现在可视区,那么加载图片。加载图片其实就是给img标签src属性赋值为本来的地址,那么此时图片便会请求加载渲染出来

//获取全部img标签
var images = document.getElementsByTagName("img");
 
 window.addEventListener("scroll", (e) => {
    //当发生滚动事件时调用loadPic事件
    loadPic();
  });
  
function loadPic() {
  // 遍历每一张图
  for (let i of images) {
    //判断当前图片是否在可视区内
    if (i.offsetTop <= window.innerHeight + window.scrollY) {
        //获取自定义data-src属性的值
        let trueSrc = i.getAttribute("data-src");
        //把值赋值给图片的src属性
        i.setAttribute("src", trueSrc);
    }
  }
}
//没发生滚动事件时也要先执行一次
loadPic();
  • offsetTop 为元素距离顶部的距离;
  • window.innerHeight 为当前窗口的高度;
  • window.scrollY 为滚动距离

i.offsetTop <= window.innerHeight + window.scrollY时图片就处于窗口可视区了。

1.912 利用getBoundingClientRect().top 计算位置

window.addEventListener("scroll", (e) => {
      loadPic();
    });
    
function loadPic() {
  for (let i of images) {
    //计算方式和第一种方式不同
    if (i.getBoundingClientRect().top < window.innerHeight) {
      let trueSrc = i.getAttribute("data-src");
      i.setAttribute("src", trueSrc);
    }
  }
}

loadPic();
  • getBoundingClientRect().top 为元素相对于窗口的位置;
  • window.innerHeight 为当前窗口的高度;

当元素对于窗口的位置小于当前窗口的高度时,那自然处于了窗口可视区了。

1.92 Intersection Observer

Intersection Observer 构造函数的作用是它能够观察可视窗口与目标元素产生的交叉区域。简单来说就是当用它观察我们的图片时,当图片出现或者消失在可视窗口,它都能知道并且会执行一个特殊的回调函数,我们就利用这个回调函数实现我们的操作

var images = document.getElementsByTagName("img");
function callback(entries) {
   for (let i of entries) {
     if (i.isIntersecting) {
         let img = i.target;
         let trueSrc = img.getAttribute("data-src");
         img.setAttribute("src", trueSrc);
         observer.unobserve(img);
     }
   } 
}
 
const observer = new IntersectionObserver(callback);

for (let i of images) {
 observer.observe(i);
}

1.10 PromiseQueue

class PromiseQueue{
    constructor(tasks,concurrentCount=1){
        this.totals = tasks.length;
        this.todo =tasks;
        this.count = concurrentCount;
        this.running =[];
        this.complete =[];
        
    }

    runNext(){
        return (
            this.running.length < this.count
            && this.todo.length
        )
    }

    run(){
        while(this.runNext()){
            let promise = this.todo.shift();
            promise.then(()=>{
                this.complete.push(this.running.shift());
                this.run();
            })

            this.running.push(promise)
        }
    }
}

测试用例

// 接收一个promise数组,定义窗口大小为3
const taskQueue = new PromiseQueue(tasks, 3); 
taskQueue.run();

1.11 数组常用方法

1.111 改变原数组(7)

  1. push
  2. pop
  3. shift
  4. unshift
  5. reverse
  6. sort
  7. splice

1.112 不会改变(7)

  1. concat
  2. join
  3. slice
  4. filter
  5. reduce
  6. find
  7. findIndex

二、 前端部分CSS技巧

2.1 object-fit

使用object-fit属性,可以指定替换元素如何调整其大小以适应其容器。它有以下几个值:

  • fill:默认值,替换元素会拉伸以填充容器,可能会导致元素的宽高比发生变化。
  • contain:替换元素会缩放以适应容器,保持其宽高比,可能会留有空白区域。
  • cover:替换元素会缩放以填充容器,保持其宽高比,可能会被裁剪。
  • none:替换元素将保持其本来的尺寸和宽高比,可能会溢出容器。
  • scale-down:替换元素会根据容器的大小进行缩放,但不会超过其原始大小,可能会留有空白区域。

2.2. 解决图片5px间距问题

是否经常遇到图片底部多出 5px 间距的问题?有4种方法可以解决:

  • 解决方案 1:将其父元素的 font-size:0px
  • 解决方案 2:将 img 的样式增加 display:block
  • 解决方案 3:将 img 的样式增加 vertical-align:bottom
  • 解决方案 4:将父元素的样式增加 line-height:5px

2.3. 修改输入框 placeholder 样式

这个是表单输入框占位符属性,如何来修改默认样式,如下:

input::-webkit-input-placeholder {
  color: #babbc1;
  font-size: 12px;
}

2.4. 使用 :not 选择器

除了最后一个元素之外的所有元素都需要一些样式,使用 not 选择器可以非常容易实现。

例如实现一个列表,最后一个元素不需要下划线,如下:

li:not(:last-child) {
  border-bottom: 1px solid #ebedf0;
}

2.5. 使用 caret-color 修改光标颜色

有时需要修改光标的颜色。现在是插入符号颜色显示时间。

.caret-color {
  width: 300px;
  padding: 10px;
  margin-top: 20px;
  border-radius: 10px;
  border: solid 1px #ffd476;
  box-sizing: border-box;
  background-color: transparent;
  outline: none;
  color: #ffd476;
  font-size: 14px;
  /* 关键样式 */
  caret-color: #ffd476;
}

.caret-color::-webkit-input-placeholder {
  color: #4f4c5f;
  font-size: 14px;
}

2.6. 使用 flex 布局将元素智能地固定在底部

当内容不够时,按钮应该在页面底部。当有足够的内容时,按钮应该跟随内容。当遇到类似问题时,可以使用flex实现智能布局!

<div class="container">
  <div class="main">这里为内容</div>
  <div class="footer">按钮</div>
</div>

CSS 代码如下:

.container {
  height: 100vh;
  /* 关键样式 */
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

.main {
  /* 关键样式 */
  flex: 1;
  background-image: linear-gradient(
    45deg,
    #ff9a9e 0%,
    #fad0c4 99%,
    #fad0c4 100%
  );
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
}

.footer {
  padding: 15px 0;
  text-align: center;
  color: #ff9a9e;
  font-size: 14px;
}

2.7. 去掉 type="number" 末尾的箭头

默认情况下,input 类型为 type="number" 的末尾会出现一个小箭头,但有时需要将其去掉,可以用一下样式:

css
复制代码
input {
  width: 300px;
  padding: 10px;
  margin-top: 20px;
  border-radius: 10px;
  border: solid 1px #ffd476;
  box-sizing: border-box;
  background-color: transparent;
  outline: none;
  color: #ffd476;
  font-size: 14px;
  caret-color: #ffd476;
  display: block;
}

input::-webkit-input-placeholder {
  color: #4f4c5f;
  font-size: 14px;
}
/* 关键样式 */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
  -webkit-appearance: none; 
}

2.8. 使用 outline:none 删除输入状态行

当输入框被选中时,默认会有一个蓝色的状态行,可以使用 outline:none 将其去掉。

2.9. 解决iOS滚动条卡住的问题

在苹果手机上,经常会出现滚动时元素卡住的情况,这个时候只要一行CSS会支持弹性滚动。

body,html{
  -webkit-overflow-scrolling: touch;
}

2.10. 画三角形

.triangle {
  display: inline-block;
  margin-right: 10px;
  /* 基础样式 */
  border: solid 10px transparent;
}
/* 向下三角形 */
.triangle.bottom {
  border-top-color: #0097a7;
}
/* 向上三角形 */
.triangle.top {
  border-bottom-color: #b2ebf2;
}
/* 向左三角形 */
.triangle.left {
  border-right-color: #00bcd4;
}
/* 向右三角形 */
.triangle.right {
  border-left-color: #009688;
}

2.11 input密码框password在ie/edge浏览器下默认的icon去除

::-ms-clear,::-ms-reveal{display:none;}
// ie
input::-ms-reveal {
  display: none;
}
input::-ms-clear{
  display: none;
}
// edge
input::-o-clear{
  display: none;
}

2.12. 自定义选定的文本样式

可以通过样式自定义文本选择的颜色和样式,关键样式如下:

::selection {
  color: #ffffff;
  background-color: #ff4c9f;
}

2.13. 不允许选择的文本

使用样式 user-select: none;

2.14 使用 filter:grayscale(1) 使页面处于灰色模式

一行代码将使页面处于灰色模式。

body{
  filter: grayscale(1);
}

2.15 毛玻璃的效果

image.png

.navbar-wrapper {
  position: relative;
  border-bottom: 1px solid var(--border-color);
  height: var(--header-height);
  padding: 0 12px 0 24px;
  background-image: radial-gradient(transparent 1px,var(--bg-color) 1px);
  background-size: 4px 4px;
  backdrop-filter: saturate(50%) blur(4px);
  -webkit-backdrop-filter: saturate(50%) blur(4px);
  top: 0;
}

2.16 可用 CSS 完全取代 JS

2.161 颜色选择器

直接通过设置 input 标签属性,就自带这个颜色选择功能了~

<input type="color" value="#FF99FF" />

code.juejin.cn/pen/7164305…

2.162. 打字效果

纯 CSS 就能实现以下“打字效果”

code.juejin.cn/pen/7164307…

@keyframes typing { from { width: 0; } }
@keyframes blink-caret { 50% { border-color: transparent; } }

.typing { 
    font: bold 300% Consolas, Monaco, monospace;
    border-right: .1em solid black;
    width: 16.5em; 
    width: 21ch;
    margin: 2em 1em;
    white-space: nowrap;
    overflow: hidden;
    animation: typing 7s steps(21, end),
    blink-caret .5s step-end infinite alternate;
}

2.163. 滑顶/触底效果

我们通常是这样实现点击按钮,滚动条立即触顶/触底效果的:

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

现在不必了,CSS 一行代码就可解决了:

code.juejin.cn/pen/7164310…

2.164. 顺滑切图

原来 CSS scroll-snap-type: x mandatory; 可以让你顺滑切图。当你滑动横向滚动条到一定位置时,会自动为你切图,这样显得流畅;

code.juejin.cn/pen/7164311…

是否更顺滑,实际效果可以在码上掘金上去掉 scroll-snap-type: x mandatory 这一行,前后对比一试便知。

三、定时任务

3.1 定时任务高效触发

3.11 环形队列处理

数据结构:

  • 环形队列ListLoop,例如可以创建一个包含0-30的slot环形队列(本质是个数组);
  • 每个环上的任务集合Slot,环上每一个slot是一个Set
  • 记录每个Task对应落到Slot的Map集合;

执行过程
第一步: 启动一个timer,每隔1s,在上述环形队列中移动一格,0->1->2->3…->29->30->0…有一个CurrentSlotIndex指针来标识刚检测过的slot ;
第二步: 当有某用户uid有请求包到达时,从Map结构中,查找出这个uid存储在哪一个slot里;
第三步: 如果存在,从这个slot的Set结构中,删除这个uid,否则跳过该步骤;
第四步: 将uid重新加入到新的slot中(CurrentSlotIndex指针所指向的上一个slot)因为这个slot,会被timer在30s之后扫描到
第五步: 更新Map,重新设置该uid对应slot的index值

// new Array(31).fill(new Set())
// No,数组中所有Set集合为同一个

let listLoop = new Array(31),
    map = new Map(),  // 记录每个uid的slotIndex
    currentSlotIndex = 1; // 当前要检测的slot

function doAction(uid) {
    // 如果循环队列中已存在该uid,需要先干掉,重新计时
    let slotIndex = map.get(uid);
    slotIndex && listLoop[slotIndex].delete(uid);
    // 将该uid重现添加到循环队列中
    // 周期31,新插入的置入当前的后一个(即,30s后可以扫描到它)
    // 更新map中这个uid的最新slotIndex
    slotIndex = currentSlotIndex - 1;
    listLoop[slotIndex] = listLoop[slotIndex] ? 
        listLoop[slotIndex].add(uid) : new Set().add(uid);
    map.set(uid, slotIndex);
}

// 每秒钟移动一个slot,这个slot对应的set集合中所有uid都为超时
// 如果所有slot对应的set集合都为空,则表示没有uid超时
setInterval(function() {
    var slotSet = listLoop[currentSlotIndex];
    if(slotSet && slotSet.size > 0) {
        for(let uid of slotSet.values()) {
            // 执行完的uid从map集合中剔除
            map.delete(uid);
            console.log(`<${uid}>超过30s未做任何操作,设置为离线!`);
        }
        // 置空该集合
        slotSet.clear();
    }
    // 指标继续+1
    currentSlotIndex = (++currentSlotIndex) % 31;
}, 1000);

// 思路、注意Map集合的内心移除情况。

方案的优点:

  • 无需再轮询全部订单,效率高
  • 无重复执行,一个订单,任务只执行一次
  • 效性好,精确到秒(控制timer移动频率可以控制精度)

四、 灵活运用CSS开发技巧

CSS开发技巧整理一下,起个易记的名字。

  •  Layout Skill:布局技巧
  •  Behavior Skill:行为技巧
  •  Color Skill:色彩技巧
  •  Figure Skill:图形技巧
  •  Component Skill:组件技巧

4.1 Layout Skill

4.11 使用vw定制rem自适应布局

  • 要点:移动端使用rem布局需要通过JS设置不同屏幕宽高比的font-size,结合vw单位和calc()可脱离JS的控制
  • 场景:rem页面布局(不兼容低版本移动端系统)
  • 兼容:vwcalc()

4.12 使用writing-mode排版竖文

  • 要点:通过writing-mode调整文本排版方向
  • 场景:竖行文字文言文诗词
  • 兼容:writing-mode
  • 代码:在线演示

4.13 使用text-align-last对齐两端文本

  • 要点:通过text-align-last:justify设置文本两端对齐
  • 场景:未知字数中文对齐
  • 兼容:text-align-last
  • 代码:在线演示

4.14 使用:not()去除无用属性

  • 要点:通过:not()排除指定元素不使用设置样式
  • 场景:符号分割文字边界元素排版(首元素、尾元素、左右两边元素)
  • 兼容::not()
  • 代码:在线演示

4.15 使用object-fit规定图像尺寸

  • 要点:通过object-fit使图像脱离background-size的约束,使用<img>来标记图像背景尺寸
  • 场景:图片尺寸自适应
  • 兼容:object-fit
  • 代码:在线演示

4.16 使用overflow-x排版横向列表

  • 要点:通过flexboxinline-block的形式横向排列元素,对父元素设置overflow-x:auto横向滚动查看
  • 场景:横向滚动列表元素过多但位置有限的导航栏
  • 兼容:overflow-x
  • 代码:在线演示

4.17 使用transform描绘1px边框

  • 要点:分辨率比较低的屏幕下显示1px的边框会显得模糊,通过::before::aftertransform模拟细腻的1px边框
  • 场景:容器1px边框
  • 兼容:transform
  • 代码:在线演示

4.18 使用transform翻转内容

  • 要点:通过transform:scale3d()对内容进行翻转(水平翻转、垂直翻转、倒序翻转)
  • 场景:内容翻转
  • 兼容:transform
  • 代码:在线演示

4.2 Behavior Skill

4.21 使用overflow-scrolling支持弹性滚动

  • 要点:iOS页面非body元素的滚动操作会非常卡(Android不会出现此情况),通过overflow-scrolling:touch调用Safari原生滚动来支持弹性滚动,增加页面滚动的流畅度
  • 场景:iOS页面滚动
  • 兼容:iOS自带-webkit-overflow-scrolling
body {
    -webkit-overflow-scrolling: touch;
}
.elem {
    overflow: auto;
}

4.22 使用transform启动GPU硬件加速

  • 要点:有时执行动画可能会导致页面卡顿,可在特定元素中使用硬件加速来避免这个问题
  • 场景:动画元素(绝对定位、同级中超过6个以上使用动画)
  • 兼容:transform
.elem {
    transform: translate3d(0, 0, 0); /* translateZ(0)亦可 */
}

4.22 使用pointer-events禁用事件触发

  • 要点:通过pointer-events:none禁用事件触发(默认事件、冒泡事件、鼠标事件、键盘事件等),相当于<button>disabled
  • 场景:限时点击按钮(发送验证码倒计时)、事件冒泡禁用(多个元素重叠且自带事件、a标签跳转)
  • 兼容:pointer-events
  • 代码:在线演示

4.24 使用+或~美化选项框

  • 要点:<label>使用+~配合for绑定radiocheckbox的选择行为
  • 场景:选项框美化选中项增加选中样式
  • 兼容:+~
  • 代码:在线演示

4.25 使用transform模拟视差滚动

4.3 Color Skill

4.31 使用filter开启悼念模式

  • 要点:通过filter:grayscale()设置灰度模式来悼念某位去世的仁兄或悼念因灾难而去世的人们
  • 场景:网站悼念
  • 兼容:filter
  • 代码:在线演示

4.32 使用linear-gradient控制背景渐变

  • 要点:通过linear-gradient设置背景渐变色并放大背景尺寸,添加背景移动效果
  • 场景:主题化彩虹背景墙
  • 兼容:gradientanimation
  • 代码:在线演示

4.33 使用::scrollbar改变滚动条样式

  • 要点:通过scrollbarscrollbar-trackscrollbar-thumb等属性来自定义滚动条样式
  • 场景:主题化页面滚动
  • 兼容:::scrollbar
  • 代码:在线演示

4.34 使用filter模拟Instagram滤镜

  • 要点:通过filter的滤镜组合起来模拟Instagram滤镜
  • 场景:图片滤镜
  • 兼容:filter
  • 代码:在线演示css-gram

4.4 Figure Skill

4.41 使用div描绘各种图形

  • 要点:<div>配合其伪元素(::before::after)通过cliptransform等方式绘制各种图形
  • 场景:各种图形容器
  • 兼容:cliptransform
  • 代码:在线演示

4.42 使用conic-gradient描绘饼图

  • 要点:通过conic-gradient绘制多种色彩的饼图
  • 场景:项占比饼图
  • 兼容:gradient
  • 代码:在线演示

4.42 使用box-shadow描绘单侧投影

4.44 使用filter描绘头像彩色阴影

  • 要点:通过filter:blur() brightness() opacity()模拟阴影效果
  • 场景:头像阴影
  • 兼容:filter
  • 代码:在线演示

4.45 使用box-shadow裁剪图像

  • 要点:通过box-shadow模拟蒙层实现中间镂空
  • 场景:图片裁剪新手引导背景镂空投射定位
  • 兼容:box-shadow
  • 代码:在线演示

4.5 Component Skill

4.51 迭代计数器

4.52 下划线跟随导航栏

  • 要点:下划线跟随鼠标移动的导航栏
  • 场景:动态导航栏
  • 兼容:+
  • 代码:在线演示

4.53 故障文本

4.54 换色器

  • 要点:通过拾色器改变图像色相的换色器
  • 场景:图片色彩变换
  • 兼容:mix-blend-mode
  • 代码:在线演示

4.55 商城票券

  • 要点:边缘带孔和中间折痕的票劵
  • 场景:电影票代金券消费卡
  • 兼容:gradient
  • 代码:在线演示

4.56 倒影加载条

4.57 三维立方体

在线演示转存失败,建议直接上传图片文件

4.58 动态边框

  • 要点:鼠标悬浮时动态渐变显示的边框
  • 场景:悬浮按钮边框动画
  • 兼容:gradient
  • 代码:在线演示

4.59 星级评分

  • 要点:点击星星进行评分的按钮
  • 场景:评分
  • 兼容:~
  • 代码:在线演示

4.510 加载指示器

4.511 螺纹进度条

4.512 蛇形边框

4.513 自动打字

  • 要点:逐个字符自动打印出来的文字
  • 场景:代码演示文字输入动画
  • 兼容:chanimation
  • 代码:在线演示

五、 灵活运用JS开发技巧

目录

JS开发技巧整理一下:

  • String Skill:字符串技巧
  • Number Skill:数值技巧
  • Boolean Skill:布尔技巧
  • Array Skill:数组技巧
  • Object Skill:对象技巧
  • Function Skill:函数技巧
  • DOM Skill:DOM技巧

5.1 String Skill

5.11 格式化金钱

const ThousandNum = num => num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
const money = ThousandNum(20190214);
// money => "20,190,214"

5.12 生成随机ID

const RandomId = len => Math.random().toString(36).substr(3, len);
const id = RandomId(10);
// id => "jg7zpgiqva"

5.13 生成随机HEX色值

const RandomColor = () => "#" + Math.floor(Math.random() * 0xffffff).toString(16).padEnd(6, "0");
const color = RandomColor();
// color => "#f03665"

5.14 生成星级评分

const StartScore = rate => "★★★★★☆☆☆☆☆".slice(5 - rate, 10 - rate);
const start = StartScore(3);
// start => "★★★"

5.15 操作URL查询参数

const params = new URLSearchParams(location.search.replace(/?/ig, "")); // location.search = "?name=young&sex=male"
params.has("young"); // true
params.get("sex"); // "male"

5.2 Number Skill

5.21 补零 padStart以及padEnd

const FillZero = (num, len) => num.toString().padStart(len, "0");
const num = FillZero(169, 5);
const FillZeroEnd = (num, len) => num.toString().padEnd(len, "0");
const numEnd = FillZeroEnd(169, 5);
// num => "00169"
// numEnd => "16900"

5.22 转数值

只对null、""、false、数值字符串有效

const num1 = +null;
const num2 = +"";
const num3 = +false;
const num4 = +"169";
// num1 num2 num3 num4 => 0 0 0 169

5.23 精确小数(向上取整)

const RoundNum = (num, decimal) => Math.round(num * 10 ** decimal) / 10 ** decimal;
const num = RoundNum(1.69, 1);
// num => 1.7

5.24 判断奇偶

const OddEven = num => !!(num & 1) ? "odd" : "even";
const num = OddEven(2);
// num => "even"

5.25 取最小最大值

const arr = [0, 1, 2];
const min = Math.min(...arr);
const max = Math.max(...arr);
// min max => 0 2

5.3 Boolean Skill

5.3 短路运算符

const a = d && 1; // 满足条件赋值:取假运算,从左到右依次判断,遇到假值返回假值,后面不再执行,否则返回最后一个真值
const b = d || 1; // 默认赋值:取真运算,从左到右依次判断,遇到真值返回真值,后面不再执行,否则返回最后一个假值
const c = !d; // 取假赋值:单个表达式转换为true则返回false,否则返回true

5.31 判断数据类型

可判断类型:undefined、null、string、number、boolean、array、object、symbol、date、regexp、function、asyncfunction、arguments、set、map、weakset、weakmap

function DataType(tgt, type) {
    const dataType = Object.prototype.toString.call(tgt).replace(/[object (\w+)]/, "$1").toLowerCase();
    return type ? dataType === type : dataType;
}
DataType("young"); // "string"
DataType(20190214); // "number"
DataType(true); // "boolean"
DataType([], "array"); // true
DataType({}, "array"); // false

5.32 是否为空数组

const arr = [];
const flag = Array.isArray(arr) && !arr.length;
// flag => true

5.33 是否为空对象

const obj = {};
const flag = DataType(obj, "object") && !Object.keys(obj).length;
// flag => true

5.4 Array Skill

5.41 去重数组

const arr = [...new Set([0, 1, 1, null, null])];
// arr => [0, 1, null]

5.42 混淆数组

const arr = [0, 1, 2, 3, 4, 5].slice().sort(() => Math.random() - .5);
// arr => [3, 4, 0, 5, 1, 2]

5.43 截断数组

const arr = [0, 1, 2];
arr.length = 2;
// arr => [0, 1]

5.44 交换赋值

let a = 0;
let b = 1;
[a, b] = [b, a];
// a b => 1 0

5.45 过滤空值

空值:undefined、null、""、0、false、NaN

const arr = [undefined, null, "", 0, false, NaN, 1, 2].filter(Boolean);
// arr => [1, 2]

5.46 异步累计

async function Func(deps) {
    return deps.reduce(async(t, v) => {
        const dep = await t;
        const version = await Todo(v);
        dep[v] = version;
        return dep;
    }, Promise.resolve({}));
}
const result = await Func(); // 需在async包围下使用

5.47 统计数组成员个数

const arr = [0, 1, 1, 2, 2, 2];
const count = arr.reduce((t, v) => {
	t[v] = t[v] ? ++t[v] : 1;
	return t;
}, {});
// count => { 0: 1, 1: 2, 2: 3 }

5.48 解构数组成员嵌套

const arr = [0, 1, [2, 3, [4, 5]]];
const [a, b, [c, d, [e, f]]] = arr;
// a b c d e f => 0 1 2 3 4 5

5.49 解构数组成员默认值

const arr = [0, 1, 2];
const [a, b, c = 3, d = 4] = arr;
// a b c d => 0 1 2 4

5.410 创建指定长度且值相等的数组

const arr = new Array(3).fill(0);
// arr => [0, 0, 0]

5.411 reduce代替map和filter

const _arr = [0, 1, 2];

// map
const arr = _arr.map(v => v * 2);
const arr = _arr.reduce((t, v) => {
    t.push(v * 2);
    return t;
}, []);
// arr => [0, 2, 4]

// filter
const arr = _arr.filter(v => v > 0);
const arr = _arr.reduce((t, v) => {
    v > 0 && t.push(v);
    return t;
}, []);
// arr => [1, 2]

// map和filter
const arr = _arr.map(v => v * 2).filter(v => v > 2);
const arr = _arr.reduce((t, v) => {
    v = v * 2;
    v > 2 && t.push(v);
    return t;
}, []);
// arr => [4]

5.5 Object Skill

5.51 克隆对象(浅复制)

const _obj = { a: 0, b: 1, c: 2 }; // 以下方法任选一种
const obj = { ..._obj };
const obj = JSON.parse(JSON.stringify(_obj));
// obj => { a: 0, b: 1, c: 2 }

5.52 对象字面量

获取环境变量时必用此方法

const env = "prod";
const link = {
    dev: "Development Address",
    test: "Testing Address",
    prod: "Production Address"
}[env];
// link => "Production Address

5.52 创建纯空对象

const obj = Object.create(null);
Object.prototype.a = 0;
// obj => {}

5.54 删除对象无用属性

const obj = { a: 0, b: 1, c: 2 }; // 只想拿b和c
const { a, ...rest } = obj;
// rest => { b: 1, c: 2 }

5.55 解构对象属性嵌套

const obj = { a: 0, b: 1, c: { d: 2, e: 3 } };
const { c: { d, e } } = obj;
// d e => 2 3

5.56 解构对象属性别名

const obj = { a: 0, b: 1, c: 2 };
const { a, b: d, c: e } = obj;
// a d e => 0 1 2

5.57 解构对象属性默认值

const obj = { a: 0, b: 1, c: 2 };
const { a, b = 2, d = 3 } = obj;
// a b d => 0 1 3

5.6 Function Skill

5.61 函数自执行

const Func = function() {}(); // 常用

(function() {})(); // 常用
(function() {}()); // 常用
[function() {}()];

+ function() {}();
- function() {}();
~ function() {}();
! function() {}();

new function() {};
new function() {}();
void function() {}();
typeof function() {}();
delete function() {}();

1, function() {}();
1 ^ function() {}();
1 > function() {}();

5.62 隐式返回值

只能用于单语句返回值箭头函数,如果返回值是对象必须使用()包住

const Func = function(name) {
    return "I Love " + name;
};
// 换成
const Func = name => "I Love " + name;

5.63 一次性函数

适用于运行一些只需执行一次的初始化代码

function Func() {
    console.log("x");
    Func = function() {
        console.log("y");
    }
}

5.64 惰性载入函数

函数内判断分支较多较复杂时可大大节约资源开销

function Func() {
    if (a === b) {
        console.log("x");
    } else {
        console.log("y");
    }
}
// 换成
function Func() {
    if (a === b) {
        Func = function() {
            console.log("x");
        }
    } else {
        Func = function() {
            console.log("y");
        }
    }
    return Func();
}

5.65 检测非空参数

function IsRequired() {
    throw new Error("param is required");
}
function Func(name = IsRequired()) {
    console.log("I Love " + name);
}
Func(); // "param is required"
Func("You"); // "I Love You"

5.66 优雅处理Async/Await参数

function AsyncTo(promise) {
    return promise.then(data => [null, data]).catch(err => [err]);
}
const [err, res] = await AsyncTo(Func());

5.67 优雅处理多个函数返回值

function Func() {
    return Promise.all([
        fetch("/user"),
        fetch("/comment")
    ]);
}
const [user, comment] = await Func(); // 需在async包围下使用

5.7 DOM Skill

5.71 自适应页面

页面基于一张设计图但需做多款机型自适应,元素尺寸使用rem进行设置

function AutoResponse(width = 750) {
    const target = document.documentElement;
    target.clientWidth >= 600
        ? (target.style.fontSize = "80px")
        : (target.style.fontSize = target.clientWidth / width * 100 + "px");
}

5.72 过滤XSS

function FilterXss(content) {
    let elem = document.createElement("div");
    elem.innerText = content;
    const result = elem.innerHTML;
    elem = null;
    return result;
}

5.73 存取LocalStorage

反序列化取,序列化存

const love = JSON.parse(localStorage.getItem("love"));
localStorage.setItem("love", JSON.stringify("I Love You"));

六、 性能优化的9大策略和6大指标

所有的性能优化都围绕着两大层面两小层面实现,核心层面是网络层面渲染层面,辅助层面是时间层面体积层面,而辅助层面则充满在核心层面里。于是笔者通过本文整理出关于前端性能优化九大策略六大指标。(笔者自己定义)

image.png

6.1 九大策略

6.11 网络层面

网络层面的性能优化,无疑是如何让资源体积更小加载更快

  • 构建策略:基于构建工具(Webpack/Rollup/Parcel/Esbuild/Vite/Gulp)
  • 图像策略:基于图像类型(JPG/PNG/SVG/WebP/Base64)
  • 分发策略:基于内容分发网络(CDN)
  • 缓存策略:基于浏览器缓存(强缓存/协商缓存)

上述四方面都是一步接着一步完成,充满在整个项目流程里。构建策略图像策略处于开发阶段,分发策略缓存策略处于生产阶段,因此在每个阶段都可检查是否按顺序接入上述策略。通过这种方式就能最大限度增加性能优化应用场景。

6.111 构建策略

该策略主要围绕webpack做相关处理,同时也是接入最普遍的性能优化策略。其他构建工具的处理也是大同小异,可能只是配置上不一致。说到webpack性能优化,无疑是从时间层面体积层面入手。

⏱表示减少打包时间,📦表示减少打包体积

  • 减少打包时间缩减范围缓存副本定向搜索提前构建并行构建可视结构
  • 减少打包体积分割代码摇树优化动态垫片按需加载作用提升压缩资源

⏱缩减范围

配置include/exclude缩小Loader对文件的搜索范围,好处是避免不必要的转译

include/exclude通常在各大Loader里配置,src目录通常作为源码目录,可做如下处理。当然include/exclude可根据实际情况修改。

export default {
    // ...
    module: {
        rules: [{
            exclude: /node_modules/,
            include: /src/,
            test: /.js$/,
            use: "babel-loader"
        }]
    }
};

⏱缓存副本

配置cache缓存Loader对文件的编译副本,好处是再次编译时只编译修改过的文件

大部分Loader/Plugin都会提供一个可使用编译缓存的选项,通常包含cache字眼。以babel-loadereslint-webpack-plugin为例。

import EslintPlugin from "eslint-webpack-plugin";

export default {
    // ...
    module: {
        rules: [{
            // ...
            test: /.js$/,
            use: [{
                loader: "babel-loader",
                options: { cacheDirectory: true }
            }]
        }]
    },
    plugins: [
        new EslintPlugin({ cache: true })
    ]
};

⏱定向搜索

配置resolve提高文件的搜索速度,好处是定向指定必须文件路径。若某些第三方库以常规形式引入可能报错或希望程序自动索引特定类型文件都可通过该方式解决。

alias映射模块路径,extensions表明文件后缀,noParse过滤无依赖文件。通常配置aliasextensions就足够。

export default {
    // ...
    resolve: {
        alias: {
            "#": AbsPath(""), // 根目录快捷方式
            "@": AbsPath("src"), // src目录快捷方式
            swiper: "swiper/js/swiper.min.js"
        }, // 模块导入快捷方式
        extensions: [".js", ".ts", ".jsx", ".tsx", ".json", ".vue"] // import路径时文件可省略后缀名
    }
};

⏱并行构建

配置Thread将Loader单进程转换为多进程,好处是释放CPU多核并发的优势。在使用webpack构建项目时会有大量文件需解析和处理,构建过程是计算密集型的操作,随着文件增多会使构建过程变得越慢。

运行在Node里的webpack是单线程模型,简单来说就是webpack待处理的任务需一件件处理,不能同一时刻处理多件任务。

文件读写计算操作无法避免,能不能让webpack同一时刻处理多个任务,发挥多核CPU电脑的威力以提升构建速度呢?thread-loader来帮你,根据CPU个数开启线程。

在此需注意一个问题,若项目文件不算多就不要使用该性能优化建议,毕竟开启多个线程也会存在性能开销。

import Os from "os";

export default {
    // ...
    module: {
        rules: [{
            // ...
            test: /.js$/,
            use: [{
                loader: "thread-loader",
                options: { workers: Os.cpus().length }
            }, {
                loader: "babel-loader",
                options: { cacheDirectory: true }
            }]
        }]
    }
};

⏱可视结构

配置BundleAnalyzer分析打包文件结构,好处是找出导致体积过大的原因。从而通过分析原因得出优化方案减少构建时间。BundleAnalyzerwebpack官方插件,可直观分析打包文件的模块组成部分、模块体积占比、模块包含关系、模块依赖关系、文件是否重复、压缩体积对比等可视化数据。

可使用webpack-bundle-analyzer配置,有了它,我们就能快速找到相关问题。

import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer";

export default {
    // ...
    plugins: [
        // ...
        BundleAnalyzerPlugin()
    ]
};

📦分割代码

分割各个模块代码,提取相同部分代码,好处是减少重复代码的出现频率webpack v4使用splitChunks替代CommonsChunksPlugin实现代码分割。

splitChunks配置较多,详情可参考官网,在此笔者贴上常用配置。

export default {
    // ...
    optimization: {
        runtimeChunk: { name: "manifest" }, // 抽离WebpackRuntime函数
        splitChunks: {
            cacheGroups: {
                common: {
                    minChunks: 2,
                    name: "common",
                    priority: 5,
                    reuseExistingChunk: true, // 重用已存在代码块
                    test: AbsPath("src")
                },
                vendor: {
                    chunks: "initial", // 代码分割类型
                    name: "vendor", // 代码块名称
                    priority: 10, // 优先级
                    test: /node_modules/ // 校验文件正则表达式
                }
            }, // 缓存组
            chunks: "all" // 代码分割类型:all全部模块,async异步模块,initial入口模块
        } // 代码块分割
    }
};

📦摇树优化

删除项目中未被引用代码,好处是移除重复代码和未使用代码摇树优化首次出现于rollup,是rollup的核心概念,后来在webpack v2里借鉴过来使用。

摇树优化只对ESM规范生效,对其他模块规范失效。摇树优化针对静态结构分析,只有import/export才能提供静态的导入/导出功能。因此在编写业务代码时必须使用ESM规范才能让摇树优化移除重复代码和未使用代码。

webpack里只需将打包环境设置成生产环境就能让摇树优化生效,同时业务代码使用ESM规范编写,使用import导入模块,使用export导出模块。

export default {
    // ...
    mode: "production"
};

📦动态垫片

通过垫片服务根据UA返回当前浏览器代码垫片,好处是无需将繁重的代码垫片打包进去。每次构建都配置@babel/preset-envcore-js根据某些需求将Polyfill打包进来,这无疑又为代码体积增加了贡献。

@babel/preset-env提供的useBuiltIns可按需导入Polyfill

  • false:无视target.browsers将所有Polyfill加载进来
  • entry:根据target.browsers将部分Polyfill加载进来(仅引入有浏览器不支持的Polyfill,需在入口文件import "core-js/stable")
  • usage:根据target.browsers和检测代码里ES6的使用情况将部分Polyfill加载进来(无需在入口文件import "core-js/stable")

在此推荐大家使用动态垫片动态垫片可根据浏览器UserAgent返回当前浏览器Polyfill,其思路是根据浏览器的UserAgentbrowserlist查找出当前浏览器哪些特性缺乏支持从而返回这些特性的Polyfill。对这方面感兴趣的同学可参考polyfill-librarypolyfill-service的源码。

在此提供两个动态垫片服务,可在不同浏览器里点击以下链接看看输出不同的Polyfill。相信IExplore还是最多Polyfill的,它自豪地说:我就是我,不一样的烟火

使用html-webpack-tags-plugin在打包时自动插入动态垫片

import HtmlTagsPlugin from "html-webpack-tags-plugin";

export default {
    plugins: [
        new HtmlTagsPlugin({
            append: false, // 在生成资源后插入
            publicPath: false, // 使用公共路径
            tags: ["https://polyfill.alicdn.com/polyfill.min.js"] // 资源路径
        })
    ]
};

📦按需加载

将路由页面/触发性功能单独打包为一个文件,使用时才加载,好处是减轻首屏渲染的负担。因为项目功能越多其打包体积越大,导致首屏渲染速度越慢。

首屏渲染时只需对应JS代码而无需其他JS代码,所以可使用按需加载webpack v4提供模块按需切割加载功能,配合import()可做到首屏渲染减包的效果,从而加快首屏渲染速度。只有当触发某些功能时才会加载当前功能的JS代码

webpack v4提供魔术注解命名切割模块,若无注解则切割出来的模块无法分辨出属于哪个业务模块,所以一般都是一个业务模块共用一个切割模块的注解名称。

const Login = () => import( /* webpackChunkName: "login" */ "../../views/login");
const Logon = () => import( /* webpackChunkName: "logon" */ "../../views/logon");

运行起来控制台可能会报错,在package.jsonbabel相关配置里接入@babel/plugin-syntax-dynamic-import即可。

{
    // ...
    "babel": {
        // ...
        "plugins": [
            // ...
            "@babel/plugin-syntax-dynamic-import"
        ]
    }
}

📦作用提升

分析模块间依赖关系,把打包好的模块合并到一个函数中,好处是减少函数声明和内存花销作用提升首次出现于rollup,是rollup的核心概念,后来在webpack v3里借鉴过来使用。

在未开启作用提升前,构建后的代码会存在大量函数闭包。由于模块依赖,通过webpack打包后会转换成IIFE,大量函数闭包包裹代码会导致打包体积增大(模块越多越明显)。在运行代码时创建的函数作用域变多,从而导致更大的内存开销。

在开启作用提升后,构建后的代码会按照引入顺序放到一个函数作用域里,通过适当重命名某些变量以防止变量名冲突,从而减少函数声明和内存花销。

webpack里只需将打包环境设置成生产环境就能让作用提升生效,或显式设置concatenateModules

export default {
    // ...
    mode: "production"
};
// 显式设置
export default {
    // ...
    optimization: {
        // ...
        concatenateModules: true
    }
};

📦压缩资源

压缩HTML/CSS/JS代码,压缩字体/图像/音频/视频,好处是更有效减少打包体积。极致地优化代码都有可能不及优化一个资源文件的体积更有效。

针对HTML代码,使用html-webpack-plugin开启压缩功能。

import HtmlPlugin from "html-webpack-plugin";

export default {
    // ...
    plugins: [
        // ...
        HtmlPlugin({
            // ...
            minify: {
                collapseWhitespace: true,
                removeComments: true
            } // 压缩HTML
        })
    ]
};

针对CSS/JS代码,分别使用以下插件开启压缩功能。其中OptimizeCss基于cssnano封装,UglifyjsTerser都是webpack官方插件,同时需注意压缩JS代码需区分ES5ES6

import OptimizeCssAssetsPlugin from "optimize-css-assets-webpack-plugin";
import TerserPlugin from "terser-webpack-plugin";
import UglifyjsPlugin from "uglifyjs-webpack-plugin";

const compressOpts = type => ({
    cache: true, // 缓存文件
    parallel: true, // 并行处理
    [`${type}Options`]: {
        beautify: false,
        compress: { drop_console: true }
    } // 压缩配置
});
const compressCss = new OptimizeCssAssetsPlugin({
    cssProcessorOptions: {
        autoprefixer: { remove: false }, // 设置autoprefixer保留过时样式
        safe: true // 避免cssnano重新计算z-index
    }
});
const compressJs = USE_ES6
    ? new TerserPlugin(compressOpts("terser"))
    : new UglifyjsPlugin(compressOpts("uglify"));

export default {
    // ...
    optimization: {
        // ...
        minimizer: [compressCss, compressJs] // 代码压缩
    }
};

针对字体/音频/视频文件,还真没相关Plugin供我们使用,就只能拜托大家在发布项目到生产服前使用对应的压缩工具处理了。针对图像文件,大部分Loader/Plugin封装时均使用了某些图像处理工具,而这些工具的某些功能又托管在国外服务器里,所以导致经常安装失败。具体解决方式可回看笔者曾经发布的《聊聊NPM镜像那些险象环生的坑》一文寻求答案。

鉴于此,笔者花了一点小技巧开发了一个Plugin用于配合webpack压缩图像,详情请参考tinyimg-webpack-plugin

import TinyimgPlugin from "tinyimg-webpack-plugin";

export default {
    // ...
    plugins: [
        // ...
        TinyimgPlugin()
    ]
};

上述构建策略都集成到笔者开源的bruce-cli里,它是一个React/Vue应用自动化构建脚手架,其零配置开箱即用的优点非常适合入门级、初中级、快速开发项目的前端同学使用,还可通过创建brucerc.js文件覆盖其默认配置,只需专注业务代码的编写无需关注构建代码的编写,让项目结构更简洁。详情请戳这里

6.112 图像策略

该策略主要围绕图像类型做相关处理,同时也是接入成本较低的性能优化策略。只需做到以下两点即可:

  • 图像选型:了解所有图像类型的特点及其何种应用场景最合适
  • 图像压缩:在部署到生产环境前使用工具或脚本对其压缩处理
6.1121图像选型一定要知道每种图像类型的体积/质量/兼容/请求/压缩/透明/场景等参数相对值,这样才能迅速做出判断在何种场景使用何种类型的图像。
类型体积质量兼容请求压缩透明场景
JPG有损不支持背景图、轮播图、色彩丰富图
PNG无损支持图标、透明图
SVG无损支持图标、矢量图
WebP兼备支持看兼容情况
Base64看情况无损支持图标
6.1122 图像压缩可在上述构建策略-压缩资源里完成,也可自行使用工具完成。由于现在大部分webpack图像压缩工具不是安装失败就是各种环境问题(你懂的),所以笔者还是推荐在发布项目到生产服前使用图像压缩工具处理,这样运行稳定也不会增加打包时间。

好用的图像压缩工具无非就是以下几个,若有更好用的工具麻烦在评论里补充喔!

工具开源收费API免费体验
QuickPicture✖️✔️✖️可压缩类型较多,压缩质感较好,有体积限制,有数量限制
ShrinkMe✖️✖️✖️可压缩类型较多,压缩质感一般,无数量限制,有体积限制
Squoosh✔️✖️✔️可压缩类型较少,压缩质感一般,无数量限制,有体积限制
TinyJpg✖️✔️✔️可压缩类型较少,压缩质感很好,有数量限制,有体积限制
TinyPng✖️✔️✔️可压缩类型较少,压缩质感很好,有数量限制,有体积限制
Zhitu✖️✖️✖️可压缩类型一般,压缩质感一般,有数量限制,有体积限制

若不想在网站里来回拖动图像文件,可使用笔者开源的图像批处理工具img-master代替,不仅有压缩功能,还有分组功能、标记功能和变换功能。目前笔者负责的全部项目都使用该工具处理,一直用一直爽!

图像策略也许处理一张图像就能完爆所有构建策略,因此是一种很廉价但极有效的性能优化策略

6.113 缓存策略

该策略主要围绕浏览器缓存做相关处理,同时也使接入成本最低的性能优化策略。其显著减少网络传输所带来的损耗,提升网页访问速度,是一种很值得使用的性能优化策略

通过下图可知,为了让浏览器缓存发挥最大作用,该策略尽量遵循以下五点就能发挥浏览器缓存最大作用。

  • 考虑拒绝一切缓存策略Cache-Control:no-store
  • 考虑资源是否每次向服务器请求Cache-Control:no-cache
  • 考虑资源是否被代理服务器缓存Cache-Control:public/private
  • 考虑资源过期时间Expires:t/Cache-Control:max-age=t,s-maxage=t
  • 考虑协商缓存Last-Modified/Etag

缓存判断机制

缓存策略通过设置HTTP报文实现,在形式上分为强缓存/强制缓存协商缓存/对比缓存。为了方便对比,笔者将某些细节使用图例展示,相信你有更好的理解。

强缓存.png

协商缓存.png

整个缓存策略机制很明了,先走强缓存,若命中失败才走协商缓存。若命中强缓存,直接使用强缓存;若未命中强缓存,发送请求到服务器检查是否命中协商缓存;若命中协商缓存,服务器返回304通知浏览器使用本地缓存,否则返回最新资源

有两种较常用的应用场景值得使用缓存策略一试,当然更多应用场景都可根据项目需求制定。

  • 频繁变动资源:设置Cache-Control:no-cache,使浏览器每次都发送请求到服务器,配合Last-Modified/ETag验证资源是否有效
  • 不常变化资源:设置Cache-Control:max-age=31536000,对文件名哈希处理,当代码修改后生成新的文件名,当HTML文件引入文件名发生改变才会下载最新文件

6.112 渲染层面

渲染层面的性能优化,无疑是如何让代码解析更好执行更快。因此笔者从以下五方面做出建议。

  • CSS策略:基于CSS规则
  • DOM策略:基于DOM操作
  • 阻塞策略:基于脚本加载
  • 回流重绘策略:基于回流重绘
  • 异步更新策略:基于异步更新

渲染层面性能优化更多表现在编码细节上,而并非实体代码。简单来说就是遵循某些编码规则,才能将渲染层面性能优化发挥到最大作用。

回流重绘策略渲染层面性能优化里占比较重,也是最常规的性能优化之一。上年笔者发布的掘金小册《玩转CSS的艺术之美》使用一整章讲解回流重绘,本章已开通试读,更多细节请戳这里

6.1121 CSS策略

  • 避免出现超过三层的嵌套规则
  • 避免为ID选择器添加多余选择器
  • 避免使用标签选择器代替类选择器
  • 避免使用通配选择器,只对目标节点声明规则
  • 避免重复匹配重复定义,关注可继承属性

6.1122 DOM策略

  • 缓存DOM计算属性
  • 避免过多DOM操作
  • 使用DOMFragment缓存批量化DOM操作

6.1123 阻塞策略

  • 脚本与DOM/其它脚本的依赖关系很强:对<script>设置defer
  • 脚本与DOM/其它脚本的依赖关系不强:对<script>设置async

6.1124 回流重绘策略

  • 缓存DOM计算属性
  • 使用类合并样式,避免逐条改变样式
  • 使用display控制DOM显隐,将DOM离线化

6.1125 异步更新策略

  • 异步任务中修改DOM时把其包装成微任务

6.2、 六大指标

六大指标基本囊括大部分性能优化细节,可作为九大策略的补充。笔者根据每条性能优化建议的特征将指标划分为以下六方面。

  • 加载优化:资源在加载时可做的性能优化
  • 执行优化:资源在执行时可做的性能优化
  • 渲染优化:资源在渲染时可做的性能优化
  • 样式优化:样式在编码时可做的性能优化
  • 脚本优化:脚本在编码时可做的性能优化
  • V8引擎优化:针对V8引擎特征可做的性能优化

6.21 加载优化

六大指标-加载优化.png

6.22 执行优化

六大指标-执行优化.png

6.23 渲染优化

六大指标-渲染优化.png

6.24 样式优化

六大指标-样式优化.png

6.25 脚本优化

六大指标-脚本优化.png

6.26 V8引擎优化

image.png

七、移动端H5坑位指南

  • 提及的安卓系统包括Android和基于Android开发的系统

  • 提及的苹果系统包括iOSiPadOS

  • 本文针对的开发场景是移动端浏览器,因此大部分坑位的解决方案在桌面端浏览器里不一定有效

  • 解决方案若未提及适用系统就默认在安卓系统和苹果系统上都适用,若提及适用系统则会详细说明

  • Webkit及其衍生内核在移动端浏览器市场占有率里达到惊人的97%,因此无需太过担心CSS3ES6浏览器新特性的兼容性

  • 真正的开发环境都是基于webpack构建,因此代码演示都不会带上CSS前缀,除非该属性是Webkit独有才会带上-webkit-

7.1 HTML方向

7.11 调用系统功能

使用<a>能快速调用移动设备的电话/短信/邮件三大通讯功能,使用<input>能快速调用移动设备的的图库/文件

这些功能方便了页面与系统的交互,关键在于调用格式一定要准确,否则会被移动端浏览器忽略。

<!-- 拨打电话 -->
<a href="tel:10086">拨打电话给10086小姐姐</a>

<!-- 发送短信 -->
<a href="sms:10086">发送短信给10086小姐姐</a>

<!-- 发送邮件 -->
<a href="mailto:young.joway@aliyun.com">发送邮件给JowayYoung</a>

<!-- 选择照片或拍摄照片 -->
<input type="file" accept="image/*">

<!-- 选择视频或拍摄视频 -->
<input type="file" accept="video/*">

<!-- 多选文件 -->
<input type="file" multiple>

7.12 忽略自动识别

有些移动端浏览器会自动将数字字母符号识别为电话/邮箱并将其渲染成上述调用系统功能里的<a>。虽然很方便却有可能违背需求。

<!-- 忽略自动识别电话 -->
<meta name="format-detection" content="telephone=no">

<!-- 忽略自动识别邮箱 -->
<meta name="format-detection" content="email=no">

<!-- 忽略自动识别电话和邮箱 -->
<meta name="format-detection" content="telephone=no, email=no">

7.13 弹出数字键盘

使用<input type="tel">弹起数字键盘会带上#*,适合输入电话。推荐使用<input type="number" pattern="\d*">弹起数字键盘,适合输入验证码等纯数字格式。

<!-- 纯数字带#和* -->
<input type="tel">

<!-- 纯数字 -->
<input type="number" pattern="\d*">

7.14 唤醒原生应用

通过location.href与原生应用建立通讯渠道,这种页面与客户端的通讯方式称为URL Scheme,其基本格式为scheme://[path][?query],笔者曾经发表过《H5与App的通讯方式》讲述URL Scheme的使用。

  • scheme:应用标识,表示应用在系统里的唯一标识
  • path:应用行为,表示应用某个页面或功能
  • query:应用参数,表示应用页面或应用功能所需的条件参数

URL Scheme一般由前端与客户端共同协商。唤醒原生应用的前提是必须在移动设备里安装了该应用,有些移动端浏览器即使安装了该应用也无法唤醒原生应用,因为它认为URL Scheme是一种潜在的危险行为而禁用它,像Safari微信浏览器。还好微信浏览器可开启白名单让URL Scheme有效。

若在页面引用第三方原生应用的URL Schema,可通过抓包第三方原生应用获取其URL

<!-- 打开微信 -->
<a href="weixin://">打开微信</a>

<!-- 打开支付宝 -->
<a href="alipays://">打开支付宝</a>

<!-- 打开支付宝的扫一扫 -->
<a href="alipays://platformapi/startapp?saId=10000007">打开支付宝的扫一扫</a>

<!-- 打开支付宝的蚂蚁森林 -->
<a href="alipays://platformapi/startapp?appId=60000002">打开支付宝的蚂蚁森林</a>

7.15 禁止页面缩放

在智能手机的普及下,很多网站都具备桌面端移动端两种浏览版本,因此无需双击缩放查看页面。禁止页面缩放可保障移动端浏览器能无遗漏地展现页面所有布局。

<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, minimum-scale=1, maximum-scale=1">

7.16 禁止页面缓存

Cache-Control指定请求和响应遵循的缓存机制,不想使用浏览器缓存就禁止呗!

<meta http-equiv="Cache-Control" content="no-cache">

7.17 禁止字母大写

有时在输入框里输入文本会默认开启首字母大写纠正,就是输入首字母小写会被自动纠正成大写。直接声明autocapitalize=off关闭首字母大写功能和autocorrect=off关闭纠正功能。

<input autocapitalize="off" autocorrect="off">

7.18 针对Safari配置

一些Safari较零散且少用的配置。

<!-- 设置Safari全屏,在iOS7+无效 -->
<meta name="apple-mobile-web-app-capable" content="yes">

<!-- 改变Safari状态栏样式,可选default/black/black-translucent,需在上述全屏模式下才有效 -->
<meta name="apple-mobile-web-app-status-bar-style" content="black">

<!-- 添加页面启动占位图 -->
<link rel="apple-touch-startup-image" href="pig.jpg" media="(device-width: 375px)">

<!-- 保存网站到桌面时添加图标 -->
<link rel="apple-touch-icon" sizes="76x76" href="pig.jpg">

<!-- 保存网站到桌面时添加图标且清除默认光泽 -->
<link rel="apple-touch-icon-precomposed" href="pig.jpg">

7.19 针对其他浏览器配置

贴一些其他浏览器较零散且少用的配置,主要是常用的QQ浏览器UC浏览器360浏览器。从网易MTL的测试数据得知,新版的QQ浏览器UC浏览器已不支持以下<meta>声明了。

<!-- 强制QQ浏览器竖屏 -->
<meta name="x5-orientation" content="portrait">

<!-- 强制QQ浏览器全屏 -->
<meta name="x5-fullscreen" content="true">

<!-- 开启QQ浏览器应用模式 -->
<meta name="x5-page-mode" content="app">

<!-- 强制UC浏览器竖屏 -->
<meta name="screen-orientation" content="portrait">

<!-- 强制UC浏览器全屏 -->
<meta name="full-screen" content="yes">

<!-- 开启UC浏览器应用模式 -->
<meta name="browsermode" content="application">

<!-- 开启360浏览器极速模式 -->
<meta name="renderer" content="webkit">

7.110 让:active有效,让:hover无效

有些元素的:active可能会无效,而元素的:hover在点击后会一直处于点击状态,需点击其他位置才能解除点击状态。给<body>注册一个空的touchstart事件可将两种状态反转。

<body ontouchstart></body>

7.2 CSS方向

7.21 自动适应布局

参考文献: