前端项目总结干货 + 渡一、coderwhy、黑马、尚硅谷实操笔记(第二版,持续更新中~~~)

前端学习笔记(温馨提示:最好根据目录查看笔记)

构建vite、vue项目

1.npm create vite@latest my-vue-app
2.vue create vue01

知识储备

响应式原理(渡一)

什么是数据响应式?

函数与数据的关联(重要)

数据变化后,会自动重新运行依赖该数据的函数(重要)

  1. 被监控的函数

    render、computed回调、watch、watchEffect

  2. 函数运行期间用到了响应式数据(响应式数据一定是个对象)

  3. 响应式数据变化会导致函数重新运行

defineProperty(渡一)

var obj = {
  b: 2,
};

// 得到属性描述符
// var desc = Object.getOwnPropertyDescriptor(obj, 'a');
// console.log(desc);

// 设置属性描述符
Object.defineProperty(obj, 'a', {
  value: 10,
  writable: false, // 不可重写
  enumerable: false, // 不可遍历
  configurable: false, // 不可修改描述符本身
});
// Object.defineProperty(obj, 'a', {
//   writable: true,
// });
obj.a = 'abc';
console.log(obj.a);
// for (var key in obj) {
//   console.log(key);
// }

// var keys = Object.keys(obj);
// console.log(keys);

// console.log(obj);

var aGoods = {
  pic: '.',
  title: '..',
  desc: `...`,
  sellNumber: 1,
  favorRate: 2,
  price: 3,
};

class UIGoods {
  get totalPrice() {
    return this.choose * this.data.price;
  }

  get isChoose() {
    return this.choose > 0;
  }

  constructor(g) {
    g = { ...g };
    Object.freeze(g);
    Object.defineProperty(this, 'data', {
      get: function () {
        return g;
      },
      set: function () {
        throw new Error('data 属性是只读的,不能重新赋值');
      },
      configurable: false,
    });
    var internalChooseValue = 0;
    Object.defineProperty(this, 'choose', {
      configurable: false,
      get: function () {
        return internalChooseValue;
      },
      set: function (val) {
        if (typeof val !== 'number') {
          throw new Error('choose属性必须是数字');
        }
        var temp = parseInt(val);
        if (temp !== val) {
          throw new Error('choose属性必须是整数');
        }
        if (val < 0) {
          throw new Error('choose属性必须大于等于 0');
        }
        internalChooseValue = val;
      },
    });
    console.log(this)
    this.a = 1;
    Object.seal(this);
  }
}

Object.freeze(UIGoods.prototype);

var g = new UIGoods(aGoods);
UIGoods.prototype.haha = 'abc';
// g.data.price = 100;

console.log(g.haha);

/**
 * 观察某个对象的所有属性
 * @param {Object} obj
 */
function observe(obj) {
  for (const key in obj) {
    let internalValue = obj[key];
    let funcs = [];
    Object.defineProperty(obj, key, {
      get: function () {
        //  依赖收集,记录:是哪个函数在用我
        if (window.__func && !funcs.includes(window.__func)) {
          funcs.push(window.__func);
        }
        return internalValue;
      },
      set: function (val) {
        internalValue = val;
        // 派发更新,运行:执行用我的函数
        for (var i = 0; i < funcs.length; i++) {
          funcs[i]();
        }
      },
    });
  }
}

function autorun(fn) {
  window.__func = fn;
  fn();
  window.__func = null;
}

constructor的使用(购物车案例)(渡一)

image-20230428110145906

以上代码等价于以下:

image-20230428110237123

// 单件商品的数据
class UIGoods {
  constructor(g) {
    this.data = g;
    this.choose = 0;
  }
  // 获取总价
  getTotalPrice() {
    return this.data.price * this.choose;
  }
  // 是否选中了此件商品
  isChoose() {
    return this.choose > 0;
  }
  // 选择的数量+1
  increase() {
    this.choose++;
  }
  //   选择的数量-1
  decrease() {
    if (this.choose === 0) {
      return;
    }
    this.choose--;
  }
}

// 整个界面的数据
class UIData {
  constructor() {
    var uiGoods = [];
    for (var i = 0; i < goods.length; i++) {
      var uig = new UIGoods(goods[i]);
      uiGoods.push(uig);
    }
    this.uiGoods = uiGoods;
    this.deliveryThreshold = 30;
    this.deliveryPrice = 5;
  }

  getTotalPrice() {
    var sum = 0;
    for (var i = 0; i < this.uiGoods.length; i++) {
      var g = this.uiGoods[i];
      sum += g.getTotalPrice();
    }
    return sum;
  }

  // 增加某件商品的选中数量
  increase(index) {
    this.uiGoods[index].increase();
  }
  // 减少某件商品的选中数量
  decrease(index) {
    this.uiGoods[index].decrease();
  }

  // 得到总共的选择数量
  getTotalChooseNumber() {
    var sum = 0;
    for (var i = 0; i < this.uiGoods.length; i++) {
      sum += this.uiGoods[i].choose;
    }
    return sum;
  }

  // 购物车中有没有东西
  hasGoodsInCar() {
    return this.getTotalChooseNumber() > 0;
  }

  // 是否跨过了起送标准
  isCrossDeliveryThreshold() {
    return this.getTotalPrice() >= this.deliveryThreshold;
  }

  isChoose(index) {
    return this.uiGoods[index].isChoose();
  }
}

// 整个界面
class UI {
  constructor() {
    this.uiData = new UIData();
    this.doms = {
      goodsContainer: document.querySelector('.goods-list'),
      deliveryPrice: document.querySelector('.footer-car-tip'),
      footerPay: document.querySelector('.footer-pay'),
      footerPayInnerSpan: document.querySelector('.footer-pay span'),
      totalPrice: document.querySelector('.footer-car-total'),
      car: document.querySelector('.footer-car'),
      badge: document.querySelector('.footer-car-badge'),
    };
    var carRect = this.doms.car.getBoundingClientRect();

    var jumpTarget = {
      x: carRect.left + carRect.width / 2,
      y: carRect.top + carRect.height / 5,
    };
    this.jumpTarget = jumpTarget;

    this.createHTML();
    this.updateFooter();
    this.listenEvent();
  }

  // 监听各种事件
  listenEvent() {
    this.doms.car.addEventListener('animationend', function () {
      this.classList.remove('animate');
    });
  }

  // 根据商品数据创建商品列表元素
  createHTML() {
    var html = '';
    for (var i = 0; i < this.uiData.uiGoods.length; i++) {
      var g = this.uiData.uiGoods[i];
      html += `<div class="goods-item">
      <img src="${g.data.pic}" alt="" class="goods-pic">
      <div class="goods-info">
        <h2 class="goods-title">${g.data.title}</h2>
        <p class="goods-desc">${g.data.desc}</p>
        <p class="goods-sell">
          <span>月售 ${g.data.sellNumber}</span>
          <span>好评率${g.data.favorRate}%</span>
        </p>
        <div class="goods-confirm">
          <p class="goods-price">
            <span class="goods-price-unit">¥</span>
            <span>${g.data.price}</span>
          </p>
          <div class="goods-btns">
            <i index="${i}" class="iconfont i-jianhao"></i>
            <span>${g.choose}</span>
            <i index="${i}" class="iconfont i-jiajianzujianjiahao"></i>
          </div>
        </div>
      </div>
    </div>`;
    }
    this.doms.goodsContainer.innerHTML = html;
  }

  increase(index) {
    this.uiData.increase(index);
    this.updateGoodsItem(index);
    this.updateFooter();
    this.jump(index);
  }

  decrease(index) {
    this.uiData.decrease(index);
    this.updateGoodsItem(index);
    this.updateFooter();
  }
  // 更新某个商品元素的显示状态
  updateGoodsItem(index) {
    var goodsDom = this.doms.goodsContainer.children[index];
    if (this.uiData.isChoose(index)) {
      goodsDom.classList.add('active');
    } else {
      goodsDom.classList.remove('active');
    }
    var span = goodsDom.querySelector('.goods-btns span');
    span.textContent = this.uiData.uiGoods[index].choose;
  }
  // 更新页脚
  updateFooter() {
    // 得到总价数据
    var total = this.uiData.getTotalPrice();
    // 设置配送费
    this.doms.deliveryPrice.textContent = `配送费¥${this.uiData.deliveryPrice}`;
    // 设置起送费还差多少
    if (this.uiData.isCrossDeliveryThreshold()) {
      // 到达起送点
      this.doms.footerPay.classList.add('active');
    } else {
      this.doms.footerPay.classList.remove('active');
      // 更新还差多少钱
      var dis = this.uiData.deliveryThreshold - total;
      dis = Math.round(dis);
      this.doms.footerPayInnerSpan.textContent = `还差¥${dis}元起送`;
    }
    // 设置总价
    this.doms.totalPrice.textContent = total.toFixed(2);
    // 设置购物车的样式状态
    if (this.uiData.hasGoodsInCar()) {
      this.doms.car.classList.add('active');
    } else {
      this.doms.car.classList.remove('active');
    }
    // 设置购物车中的数量
    this.doms.badge.textContent = this.uiData.getTotalChooseNumber();
  }

  // 购物车动画
  carAnimate() {
    this.doms.car.classList.add('animate');
  }
  // 抛物线跳跃的元素
  jump(index) {
    // 找到对应商品的加号
    var btnAdd = this.doms.goodsContainer.children[index].querySelector(
      '.i-jiajianzujianjiahao'
    );
    var rect = btnAdd.getBoundingClientRect();
    var start = {
      x: rect.left,
      y: rect.top,
    };
    // 跳吧
    var div = document.createElement('div');
    div.className = 'add-to-car';
    var i = document.createElement('i');
    i.className = 'iconfont i-jiajianzujianjiahao';
    // 设置初始位置
    div.style.transform = `translateX(${start.x}px)`;
    i.style.transform = `translateY(${start.y}px)`;
    div.appendChild(i);
    document.body.appendChild(div);
    // 强行渲染
    div.clientWidth;

    // 设置结束位置
    div.style.transform = `translateX(${this.jumpTarget.x}px)`;
    i.style.transform = `translateY(${this.jumpTarget.y}px)`;
    var that = this;
    div.addEventListener(
      'transitionend',
      function () {
        div.remove();
        that.carAnimate();
      },
      {
        once: true, // 事件仅触发一次
      }
    );
  }
}

var ui = new UI();

// 事件
ui.doms.goodsContainer.addEventListener('click', function (e) {
  if (e.target.classList.contains('i-jiajianzujianjiahao')) {
    var index = +e.target.getAttribute('index');
    ui.increase(index);
  } else if (e.target.classList.contains('i-jianhao')) {
    var index = +e.target.getAttribute('index');
    ui.decrease(index);
  }
});

window.addEventListener('keypress', function (e) {
  if (e.code === 'Equal') {
    ui.increase(0);
  } else if (e.code === 'Minus') {
    ui.decrease(0);
  }
});

浏览器是如何渲染页面的?(渡一)

当浏览器的网络线程收到 HTML 文档后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列。

在事件循环机制的作用下,渲染主线程取出消息队列中的渲染任务,开启渲染流程。


整个渲染流程分为多个阶段,分别是: HTML 解析、样式计算、布局、分层、绘制、分块、光栅化、画

每个阶段都有明确的输入输出,上一个阶段的输出会成为下一个阶段的输入。

这样,整个渲染流程就形成了一套组织严密的生产流水线。


渲染的第一步是解析 HTML

解析过程中遇到 CSS 解析 CSS,遇到 JS 执行 JS。为了提高解析效率,浏览器在开始解析前,会启动一个预解析的线程,率先下载 HTML 中的外部 CSS 文件和 外部的 JS 文件。

如果主线程解析到link位置,此时外部的 CSS 文件还没有下载解析好,主线程不会等待,继续解析后续的 HTML。这是因为下载和解析 CSS 的工作是在预解析线程中进行的。这就是 CSS 不会阻塞 HTML 解析的根本原因。

如果主线程解析到script位置,会停止解析 HTML,转而等待 JS 文件下载好,并将全局代码解析执行完成后,才能继续解析 HTML。这是因为 JS 代码的执行过程可能会修改当前的 DOM 树,所以 DOM 树的生成必须暂停。这就是 JS 会阻塞 HTML 解析的根本原因。

第一步完成后,会得到 DOM 树和 CSSOM 树,浏览器的默认样式、内部样式、外部样式、行内样式均会包含在 CSSOM 树中。


渲染的下一步是样式计算

主线程会遍历得到的 DOM 树,依次为树中的每个节点计算出它最终的样式,称之为 Computed Style。

在这一过程中,很多预设值会变成绝对值,比如red会变成rgb(255,0,0);相对单位会变成绝对单位,比如em会变成px

这一步完成后,会得到一棵带有样式的 DOM 树。


接下来是布局,布局完成后会得到布局树。

布局阶段会依次遍历 DOM 树的每一个节点,计算每个节点的几何信息。例如节点的宽高、相对包含块的位置。

大部分时候,DOM 树和布局树并非一一对应。

比如display:none的节点没有几何信息,因此不会生成到布局树;又比如使用了伪元素选择器,虽然 DOM 树中不存在这些伪元素节点,但它们拥有几何信息,所以会生成到布局树中。还有匿名行盒、匿名块盒等等都会导致 DOM 树和布局树无法一一对应。


下一步是分层

主线程会使用一套复杂的策略对整个布局树中进行分层。

分层的好处在于,将来某一个层改变后,仅会对该层进行后续处理,从而提升效率。

滚动条、堆叠上下文、transform、opacity 等样式都会或多或少的影响分层结果,也可以通过will-change属性更大程度的影响分层结果。


再下一步是绘制

主线程会为每个层单独产生绘制指令集,用于描述这一层的内容该如何画出来。


完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成。

合成线程首先对每个图层进行分块,将其划分为更多的小区域。

它会从线程池中拿取多个线程来完成分块工作。


分块完成后,进入光栅化阶段。

合成线程会将块信息交给 GPU 进程,以极高的速度完成光栅化。

GPU 进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块。

光栅化的结果,就是一块一块的位图


最后一个阶段就是

合成线程拿到每个层、每个块的位图后,生成一个个「指引(quad)」信息。

指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形。

变形发生在合成线程,与渲染主线程无关,这就是transform效率高的本质原因。

合成线程会把 quad 提交给 GPU 进程,由 GPU 进程产生系统调用,提交给 GPU 硬件,完成最终的屏幕成像。

image-20230426175916683

image-20230426175930373

image-20230426175946170

image-20230426175957615

image-20230426180018996

image-20230426180033746

image-20230426180044793

image-20230426180057617

image-20230426180105918 image-20230426180115334 image-20230426180130456
什么是 reflow?

image-20230426180144593

reflow 的本质就是重新计算 layout 树。

当进行了会影响布局树的操作后,需要重新计算布局树,会引发 layout。

为了避免连续的多次操作导致布局树反复计算,浏览器会合并这些操作,当 JS 代码全部完成后再进行统一计算。所以,改动属性造成的 reflow 是异步完成的。

也同样因为如此,当 JS 获取布局属性时,就可能造成无法获取到最新的布局信息。

浏览器在反复权衡下,最终决定获取属性立即 reflow。

什么是 repaint?

repaint 的本质就是重新根据分层信息计算了绘制指令。

当改动了可见样式后,就需要重新计算,会引发 repaint。

由于元素的布局信息也属于可见样式,所以 reflow 一定会引起 repaint。

为什么 transform 的效率高?

image-20230426180203897

因为 transform 既不会影响布局也不会影响绘制指令,它影响的只是渲染流程的最后一个「draw」阶段

由于 draw 阶段在合成线程中,所以 transform 的变化几乎不会影响渲染主线程。反之,渲染主线程无论如何忙碌,也不会影响 transform 的变化。

事件循环(渡一)

浏览器的进程模型
何为进程?

程序运行需要有它自己专属的内存空间,可以把这块内存空间简单的理解为进程

image-20220809205743532

每个应用至少有一个进程,进程之间相互独立,即使要通信,也需要双方同意。

何为线程?

有了进程后,就可以运行程序的代码了。

运行代码的「人」称之为「线程」。

一个进程至少有一个线程,所以在进程开启后会自动创建一个线程来运行代码,该线程称之为主线程。

如果程序需要同时执行多块代码,主线程就会启动更多的线程来执行代码,所以一个进程中可以包含多个线程。

image-20220809210859457

浏览器有哪些进程和线程?

浏览器是一个多进程多线程的应用程序

浏览器内部工作极其复杂。

为了避免相互影响,为了减少连环崩溃的几率,当启动浏览器后,它会自动启动多个进程。

image-20220809213152371

可以在浏览器的任务管理器中查看当前的所有进程

其中,最主要的进程有:

  1. 浏览器进程

    主要负责界面显示、用户交互、子进程管理等。浏览器进程内部会启动多个线程处理不同的任务。

  2. 网络进程

    负责加载网络资源。网络进程内部会启动多个线程来处理不同的网络任务。

  3. 渲染进程(本节课重点讲解的进程)

    渲染进程启动后,会开启一个渲染主线程,主线程负责执行 HTML、CSS、JS 代码。

    默认情况下,浏览器会为每个标签页开启一个新的渲染进程,以保证不同的标签页之间不相互影响。

    将来该默认模式可能会有所改变,有兴趣的同学可参见chrome官方说明文档

渲染主线程是如何工作的?

渲染主线程是浏览器中最繁忙的线程,需要它处理的任务包括但不限于:

  • 解析 HTML
  • 解析 CSS
  • 计算样式
  • 布局
  • 处理图层
  • 每秒把页面画 60 次
  • 执行全局 JS 代码
  • 执行事件处理函数
  • 执行计时器的回调函数

思考题:为什么渲染进程不适用多个线程来处理这些事情?

要处理这么多的任务,主线程遇到了一个前所未有的难题:如何调度任务?

比如:

  • 我正在执行一个 JS 函数,执行到一半的时候用户点击了按钮,我该立即去执行点击事件的处理函数吗?
  • 我正在执行一个 JS 函数,执行到一半的时候某个计时器到达了时间,我该立即去执行它的回调吗?
  • 浏览器进程通知我“用户点击了按钮”,与此同时,某个计时器也到达了时间,我应该处理哪一个呢?

渲染主线程想出了一个绝妙的主意来处理这个问题:排队

image-20220809223027806

  1. 在最开始的时候,渲染主线程会进入一个无限循环
  2. 每一次循环会检查消息队列中是否有任务存在。如果有,就取出第一个任务执行,执行完一个后进入下一次循环;如果没有,则进入休眠状态。
  3. 其他所有线程(包括其他进程的线程)可以随时向消息队列添加任务。新任务会加到消息队列的末尾。在添加新任务时,如果主线程是休眠状态,则会将其唤醒以继续循环拿取任务

这样一来,就可以让每个任务有条不紊的、持续的进行下去了。

整个过程,被称之为事件循环(消息循环)

若干解释
何为异步?

代码在执行过程中,会遇到一些无法立即处理的任务,比如:

  • 计时完成后需要执行的任务 —— setTimeoutsetInterval
  • 网络通信完成后需要执行的任务 – XHRFetch
  • 用户操作后需要执行的任务 – addEventListener

如果让渲染主线程等待这些任务的时机达到,就会导致主线程长期处于「阻塞」的状态,从而导致浏览器「卡死」

image-20220810104344296

渲染主线程承担着极其重要的工作,无论如何都不能阻塞!

因此,浏览器选择异步来解决这个问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-feFEX8td-1683379078101)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/202208101048899.png)]

使用异步的方式,渲染主线程永不阻塞

面试题:如何理解 JS 的异步?

参考答案:

JS是一门单线程的语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。

而渲染主线程承担着诸多的工作,渲染页面、执行 JS 都在其中运行。

如果使用同步的方式,就极有可能导致主线程产生阻塞,从而导致消息队列中的很多其他任务无法得到执行。这样一来,一方面会导致繁忙的主线程白白的消耗时间,另一方面导致页面无法及时更新,给用户造成卡死现象。

所以浏览器采用异步的方式来避免。具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。

在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行。

JS为何会阻碍渲染?

先看代码

<h1>Mr.Yuan is awesome!</h1>
<button>change</button>
<script>
  var h1 = document.querySelector('h1');
  var btn = document.querySelector('button');

  // 死循环指定的时间
  function delay(duration) {
    var start = Date.now();
    while (Date.now() - start < duration) {}
  }

  btn.onclick = function () {
    h1.textContent = '袁老师很帅!';
    delay(3000);
  };
</script>

点击按钮后,会发生什么呢?

<见具体演示>

image-20230426104307271

任务有优先级吗?

任务没有优先级,在消息队列中先进先出

消息队列是有优先级的

根据 W3C 的最新解释:

  • 每个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列。
    在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行。
  • 浏览器必须准备好一个微队列,微队列中的任务优先所有其他任务执行
    https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint

随着浏览器的复杂度急剧提升,W3C 不再使用宏队列的说法

在目前 chrome 的实现中,至少包含了下面的队列:

  • 延时队列:用于存放计时器到达后的回调任务,优先级「中」
  • 交互队列:用于存放用户操作后产生的事件处理任务,优先级「高」
  • 微队列:用户存放需要最快执行的任务,优先级「最高」

添加任务到微队列的主要方式主要是使用 Promise、MutationObserver

例如:

// 立即把一个函数添加到微队列
Promise.resolve().then(函数)

浏览器还有很多其他的队列,由于和我们开发关系不大,不作考虑

image-20230426104359281

面试题:阐述一下 JS 的事件循环

参考答案:

事件循环又叫做消息循环,是浏览器渲染主线程的工作方式。

在 Chrome 的源码中,它开启一个不会结束的 for 循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候将任务加入到队列末尾即可。

过去把消息队列简单分为宏队列和微队列,这种说法目前已无法满足复杂的浏览器环境,取而代之的是一种更加灵活多变的处理方式。

根据 W3C 官方的解释,每个任务有不同的类型,同类型的任务必须在同一个队列,不同的任务可以属于不同的队列。不同任务队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的任务。但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行。

面试题:JS 中的计时器能做到精确计时吗?为什么?

参考答案:

不行,因为:

  1. 计算机硬件没有原子钟,无法做到精确计时
  2. 操作系统的计时函数本身就有少量偏差,由于 JS 的计时器最终调用的是操作系统的函数,也就携带了这些偏差
  3. 按照 W3C 的标准,浏览器实现计时器时,如果嵌套层级超过 5 层,则会带有 4 毫秒的最少时间,这样在计时时间少于 4 毫秒时又带来了偏差
  4. 受事件循环的影响,计时器的回调函数只能在主线程空闲时运行,因此又带来了偏差

request.js

import axios from 'axios'
import vuex from '../store/index'
import { Message } from 'element-ui'

// 该项目所有请求均为 get请求
export function request(url, params) {
  // 请求超过30秒则判定为超时
  const instance = axios.create({
    baseURL: '/api',
    timeout: 30000,
    withCredentials: true
  })

  // axios拦截器
  // 请求拦截
  instance.interceptors.request.use(
    config => {
      return config
    },
    err => {
      console.log(err)
    }
  )

  // 响应拦截
  instance.interceptors.response.use(
    config => {
      const code = config.data.code
      if (code !== 200 && !(code >= 800 && code <= 803)) Message.error(config.data.message || '未知错误, 请打开控制台查看')
      return config
    },
    err => {
      console.log([err])
      if (err.response.data.msg === '需要登录') {
        // 修改当前的登录状态
        vuex.state.isLogin = false
      } else {
        Message.error(err.response.data.message || '未知错误, 请打开控制台查看')
      }
    }
  )

  instance.defaults.withCredentials = true

  if (params) {
    const query = { params }
    return instance.get(url, query)
  } else {
    return instance.get(url)
  }
}
************************************************************************************************************
import axios from 'axios'
import { getToken } from '@/composables/auth.js'
import {toast} from '@/composables/util.js'
import store from './store'
const service = axios.create({
  baseURL: import.meta.env.VITE_APP_BASE_API
})
// 添加请求拦截器
service.interceptors.request.use(
  function (config) {
    //往header头自动添加token
    const token = getToken()
    if (token) {
      config.headers['token'] = token
    }

    return config
  },
  function (error) {
    // 对请求错误做些什么
    return Promise.reject(error)
  }
)

// 添加响应拦截器
service.interceptors.response.use(
  function (response) {
    // 对响应数据做点什么
    return response.request.responseType == 'blob' ? response.data : response.data.data
  },
  function (error) {
    const msg = error.response.data.msg || '请求失败'

    if (msg == '非法token,请先登录!') {
      store.dispatch('logout').finally(() => location.reload())
    }

    toast(msg, 'error')

    return Promise.reject(error)
  }
)
export default service
************************************************************************************************************
import axios from 'axios'
import { Message } from 'element-ui'
import store from '@/store'
import router from '@/router'

const service = axios.create({
  // process.env 是nodejs内置的固定环境变量对象
  // npm run dev -> 启动开发服务, 项目根目录 .env.development 环境变量配置文件里值
  // 添加到process.env对象上
  // npm run build:prod -> 启动打包, 项目根目录 .env.production 环境变量配置文件里值
  // 添加到process.env对象上

  // 问题: 为何不直接写在这里?
  // 答案: 开发的时候, 用的是基地址1
  // 上线的时候, 用的是基地址2
  baseURL: process.env.VUE_APP_BASE_API,
  timeout: 20000
})

service.interceptors.request.use(
  config => {
    const token = store.getters.token
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}` // 后台解析的方法要求必须前面拼接一个Bearer 和空格的字符串
    }
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

service.interceptors.response.use(
  response => {
    // 因为后台成功/失败都是200, 如何区分成功和失败呢? 用success字段
    // response参数, 是axios响应对象(里面有config/header/status/data字段)
    // data字段里对应的才是后台返回的全部的数据 (也是一个对象)
    // 第一个data: 是axios自带的
    // 第二个data: 是后台返回数据对象的
    // console.log(response);//success  布尔值
    const { success, message } = response.data
    if (success) {
      return response.data // 返回给逻辑页面的直接是后台的完整数据对象, 不再是axios封装response对象(防止逻辑页面.2次data)
    } else {
      // 逻辑失败(把后台返回message提示文字返回到逻辑页面)
      // 返回Promise的reject拒绝状态(await无法接收, 如果有try+catch进catch里)
      // Message.error(message)
      return Promise.reject(message)
    }
  },
  error => {
    // 4xx/5xx的响应状态, 如果后台返回了响应数据, 我们就用一下, 如果没有, 就error对象本身message值
    // && 为了防止null.data报错    response存在就看response.data存在与否,response.data存在就取response.data.message,反之就只取error.message
    Message.error((error.response && error.response.data && error.response.data.message) || error.message)
    // 上面是报错就提示, 下面是具体的分析
    // 可以用过http状态码来判断 error.response.status === 401
    // 或者还可以用code逻辑码来判断 (10002 和后台商定的值, 代表token过期)
    // 知识点: ?. (可选链操作符) 新版的语法, 需要babel支持才能用(脚手架自带babel)
    // 左侧有值才会继续往下去点属性 (防止空值去.任意的属性报错)
    if (error?.response?.data?.code === 10002) {
      // 前端token过期, 要在前端做些什么(经验):
      // (1): 清除token(vuex和本地都得清除)
      // (2): 清除用户信息(vuex里存)
      store.dispatch('user/logoutActions')
      // (3): 返回login页面(也要被被动退出时, 所在页面的路由地址字符串传递给登录页面)
      //不是vue组件内不能用`/login?redirect=${this.$route.fullPath}`
      router.replace(`/login?redirect=${router.currentRoute.fullPath}`)
    }

    return Promise.reject(error)
  }
)

export default service
************************************************************************************************************
//对于axios进行二次封装
import axios from 'axios'
import nprogress from 'nprogress'
//引入进度条的样式
import 'nprogress/nprogress.css'
//在当前模块中引入store
import store from '@/store'
//start:进度条开始 done:进度条结束

//1:利用axios对象的方法create,去创建一个axios实例
//2:requests就是axios,只不过稍做配置一下
const requests = axios.create({
  //配置对象
  //基础路径,发请求URL携带api【发现:真实服务器接口都携带/api】
  baseURL: '/api',
  //超时的设置
  timeout: 3000
})
//请求拦截器:将来项目中【N个请求】,只要发请求,会触发请求拦截器!!!
requests.interceptors.request.use(config => {
  //请求拦截器:请求头【header】,请求头能否给服务器携带参数
  //请求拦截器:其实项目中还有一个重要的作用,给服务器携带请求们的公共的参数
  //config:配置对象,对象里面有一个属性很重要,headers请求头
  if (store.state.detail.uuid_token) {
    //请求头添加一个字段(userTempId):和后台老师商量好了
    config.headers.userTempId = store.state.detail.uuid_token
  }
  //需要携带token带给服务器
  if (store.state.user.token) {
    config.headers.token = store.state.user.token
  }
  //进度条开始动
  nprogress.start()
  return config
})
//响应拦截器:请求数据返回会执行
requests.interceptors.response.use(
  res => {
    //res:实质就是项目中发请求、服务器返回的数据

    //进度条结束
    nprogress.done()
    return res.data
  },
  err => {
    //温馨提示:某一天发请求,请求失败,请求失败的信息打印出来
    //终止Promise链
    return Promise.reject(new Error('failed'))
  }
)

//最后需要暴露:暴露的是添加新的功能的axios,即为requests
export default requests
************************************************************************************************************
// 基于axios封装网络请求
// 每个程序员的想法都不一样, 封装的地方和名字都不一样, 但是思想相同
import theAxios from 'axios'
import router from '@/router'
import { Notify } from 'vant'
import { getToken, removeToken } from '@/utils/token.js' // , setToken
// import { getNewTokenAPI } from '@/api'

const axios = theAxios.create({
  baseURL: 'http://toutiao.itheima.net', // 基地址   http://toutiao.itheima.net   http://geek.itheima.net
  timeout: 2000 // 2秒超时时间(请求2秒无响应直接判定超时)
})

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
  // console.log(config)就是发的请求,里面有请求头,请求携带的参数,请求方式等等
  // 在发送请求之前做些什么
  // 目标: 统一携带token
  // 判断本地有token再携带, 判断具体api/index.js里如果没有携带Authorization, 我在添加上去
  // 未定义叫undefined, null具体的值你得赋予才叫空
  // ?. 可选链操作符, 如果前面对象里没有length, 整个表达式原地返回undefined  undefined也不大于0,进不去判断
  // 如果getToken()在原地有值token字符串, 才能调用length获取长度

  //如果没请求头且有token
  if (getToken()?.length > 0 && config.headers.Authorization === undefined) {
    config.headers.Authorization = `Bearer ${getToken()}`
  }
  return config
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error)
})

// 添加响应拦截器
// 本质: 就是一个函数
axios.interceptors.response.use(function (response) {
  // http响应状态码为2xx, 3xx就进入这里
  // 对响应数据做点什么
  return response
}, async function (error) {
  // http响应状态码4xx, 5xx报错进入这里
  // 对响应错误做点什么
  // console.dir(error)
  // console.log(this) // undefined
  // 只有401才代表身份过期, 才需要跳转登录
  if (error.response.status === 401) {
    Notify({ type: 'danger', message: '用户身份过期,请重新登录' })
    // 不能使用this.$router (因为this不是vue组件对象无法调用$router)
    // 解决: this.$router为了拿到router路由对象, 所以直接去上面引入@/router下router对象
    removeToken() // 先清除token, 才能让路由守卫判断失效, 放行我去登录页
    // 方式1: 强制跳转到登陆, 用户有感知
    // router.currentRoute 相当于 在vue文件内this.$route -> 拿到当前路由对象信息
    // fullPath, 路由对象里完整路由路径  #后面的一切
    // router.replace('/login')
    router.replace(`/login?path=${router.currentRoute.fullPath}`)
    // 方式2: 使用refresh_token换回新的token再继续使用, JS代码实现, 用户无感知(效果好)
    //   const res = await getNewTokenAPI()
    //   // 新的token回来之后, 我们要做什么
    //   // 1. 更新token在本地
    //   setToken(res.data.data.token)
    //   // 2. 更新新的token在请求头里
    //   error.config.headers.Authorization = `Bearer ${res.data.data.token}`
    //   // 3. 未完成这次请求, 再一次发起
    //   // error.config就是上一次请求的配置对象
    //   // 结果我们要return回原本逻辑页面调用地方-还是return回去一个Promise对象
    //   return axios(error.config)
  }//else if (error.response.status === 500 && error.config.url === '/v1_0/authorizations' && error.config.method === 'put') {
  //   // 刷新的refresh_token也过期了
  //   localStorage.clear() // 清除localStorage里所有值
  //   // localStorage当前网页, 域名划分, 每个域名下有自己范围的localStorage
  //   Notify({ type: 'warning', message: '身份已过期' })
  //   router.replace('/login')
  // }
  return Promise.reject(error)
})
// 目标: token讲解
// 操作:
// 1. 手动修改localStorage里geek那个token改错(模拟过期)
// 2. 点击反馈/其他需要标明身份的接口(错误token携带给后台请求)
// 3. 反馈不感兴趣, 这次请求返回状态为 401, 进入错误响应拦截器

// 代码解决401问题
// 方式1: 清除token, 强制跳转回登录页面, 有感知重新登录, 拿到新token替换到本地
// 需要重新点击反馈按钮, 再次反馈 -> 感觉特别不好
// 方式2: 刷新token, 使用登录时保存的refresh_token, 调用另外一个接口, 换回来
// 新的token值, 替换到本地, 再次完成本次未完成的请求 -> 用户无感知体验好
// 1. 登录页面, localStorage.setItem('refresh_token', 存入refresh_token)
// 2. 401中, 注释掉跳转login的代码, 引入刷新token的api方法调用
// 3. 替换保存到本地新的token
// 4. error错误对象里headers替换成新的token
// 5. axios再次发起这次未完成请求, 返回Promise对象到最开始发请求的逻辑页面
// 注意: 调用刷新token的接口, 如果没携带refresh_token或者携带错误的都会导致500

// 401+500
// token和refresh_token都过期了 (前提, 是你手动把2个token改成错误的)
// 强制回到登录页

// 如果外面没有写值,就默认GET和空对象
export default ({ url, method = 'GET', params = {}, data = {}, headers = {} }) => {
  return axios({
    url,
    method,
    params,
    data,
    headers
  })
}
//   return new Promise((resolve, reject) => {
//     // 判断如果params有值, 需要自己写js代码, 把params对象里key和value拼接到url上
//     $.ajax({
//       url,
//       data,
//       headers,
//       type: method,
//       success: (res) => {
//         resolve(res)
//       },
//       error: err => {
//         reject(err)
//       }
//     })
//   })

// 但是上面有局限性
// 导出的axios方法在使用时
/*
// 我在逻辑页面调用时, 传入的这5个配置名字
    axios({
        url: '请求地址',
        method: '请求方式',
        params: {},
        data: {},
        headers: {}
    })
*/
// 问题来了, 万一将来我要更新request.js里封装网络请求的工具
// 把axios换成jquery的$.ajax
// import $ from 'jquery'
// export default $.ajax
/*
 $.ajax({
    url: '请求地址',
    type: '请求方式',
    data: {}, // 没有params
    headers: {}
 })
*/
************************************************************************************************************
// 1. 创建一个新的axios实例
// 2. 请求拦截器,如果有token进行头部携带
// 3. 响应拦截器:1. 剥离无效数据  2. 处理token失效
// 4. 导出一个函数,调用当前的axsio实例发请求,返回值promise

import axios from 'axios'
import store from '@/store'
import router from '@/router'

// 导出基准地址,原因:其他地方不是通过axios发请求的地方用上基准地址
export const baseURL = 'http://pcapi-xiaotuxian-front-devtest.itheima.net/'
const instance = axios.create({
  // axios 的一些配置,baseURL  timeout
  baseURL,
  timeout: 5000
})

instance.interceptors.request.use(config => {
  // 拦截业务逻辑
  // 进行请求配置的修改
  // 如果本地又token就在头部携带
  // 1. 获取用户信息对象
  const { profile } = store.state.user
  // 2. 判断是否有token
  if (profile.token) {
    // 3. 设置token
    config.headers.Authorization = `Bearer ${profile.token}`
  }
  return config
}, err => {
  return Promise.reject(err)
})

// res => res.data  取出data数据,将来调用接口的时候直接拿到的就是后台的数据
instance.interceptors.response.use(res => res.data, err => {
  // 401 状态码,进入该函数
  if (err.response && err.response.status === 401) {
    // 1. 清空无效用户信息
    // 2. 跳转到登录页
    // 3. 跳转需要传参(当前路由地址)给登录页码
    store.commit('user/setUser', {})
    // 当前路由地址
    // 组件里头:`/user?a=10` $route.path === /user  $route.fullPath === /user?a=10
    // js模块中:router.currentRoute.value.fullPath 就是当前路由地址,router.currentRoute 是ref响应式数据
    const fullPath = encodeURIComponent(router.currentRoute.value.fullPath)
    // encodeURIComponent 转换uri编码,防止解析地址出问题
    router.push('/login?redirectUrl=' + fullPath)
  }
  return Promise.reject(err)
})

// 请求工具函数
export default (url, method, submitData) => {
  // 负责发请求:请求地址,请求方式,提交的数据
  return instance({
    url,
    method,
    // 1. 如果是get请求  需要使用params来传递submitData   ?a=10&c=10
    // 2. 如果不是get请求  需要使用data来传递submitData   请求体传参
    // [] 设置一个动态的key, 写js表达式,js表达式的执行结果当作KEY
    // method参数:get,Get,GET  转换成小写再来判断
    // 在对象,['params']:submitData ===== params:submitData 这样理解
    [method.toLowerCase() === 'get' ? 'params' : 'data']: submitData
  })
}
************************************************************************************************************
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

// 导入自己UI组件库
import UI from '@/components/library'

// 1. 重置样式的库
import 'normalize.css'
// 2. 自己项目的重置样式和公用样式
import '@/assets/styles/common.less'

// mockjs
import '@/mock'

createApp(App).use(store).use(router).use(UI).mount('#app')
************************************************************************************************************import axios from 'axios'
import store from '@/store'
import router from '@/router'
// 创建一个自定的axios方法(比原axios多了个基地址)
// axios函数请求的url地址前面会被拼接基地址, 然后axios请求baseURL+url后台完整地址
export const baseURL = 'http://big-event-vue-api-t.itheima.net'
const myAxios = axios.create({
  baseURL
})
// 白名单: 不需要携带token的api地址
const whiteAPIList = ['/api/reg', '/api/login']

// 定义请求拦截器
myAxios.interceptors.request.use(function (config) {
  if (!whiteAPIList.includes(config.url)) {
    // 为请求头挂载 Authorization 字段
    config.headers.Authorization = store.state.token
  }
  //在请求前触发一次
  return config
}, function (error) {
  //请求有异常就报错
  return Promise.reject(error)
})
// 定义响应拦截器
myAxios.interceptors.response.use(function (response) {
  // 响应状态码为 2xx 时触发成功的回调,形参中的 response 是“成功的结果”
  return response
}, function (error) {
  // 响应状态码不是 2xx 时触发失败的回调,形参中的 error 是“失败的结果”
  if (error.response.status === 401) {
    // 无效的 token
    // 把 Vuex 中的 token 重置为空,并跳转到登录页面
    store.commit('Token', '')
    router.push('/login')
  }
  return Promise.reject(error)
})
// 导出自定义的axios方法, 供外面调用传参发请求
export default myAxios
************************************************************************************************************
import axios from 'axios';
import { useUserStore } from '@/store/user'

//1. 创建axios对象
const service = axios.create();

//2. 请求拦截器
service.interceptors.request.use(config => {
  const userStore = useUserStore();
  let token = userStore.token;
  if( token ){
  	config.headers['Authorization'] = token;
  }
  return config;
}, error => {
  Promise.reject(error);
});

//3. 响应拦截器
service.interceptors.response.use(response => {
  //判断code码
  return response.data;
},error => {
  return Promise.reject(error);
});

export default service;

插槽

image-20230217140202405

image-20230217140044954

image-20230217135858123

Vue笔记

中国地图实现
用到了地图和用户的JSON资料
在E盘
<template>
  <div class="geo" ref="geo"></div>
</template>

<script setup>
import * as echarts from 'echarts'
import { onMounted, ref } from 'vue'
const geo = ref(null)
const map = async () => {
  const myChart = echarts.init(geo.value)
  myChart.showLoading() //初始化,获得echarts实例
  const resp = await fetch('../../JSON/map.json').then(resp => resp.json()) //获取中国GEOJSON数据
  const users = await fetch('../../JSON/user.json').then(resp => resp.json()) //获取用户数据
  //注册地图
  echarts.registerMap('China', resp)
  myChart.setOption({
    title: {
      text: '注册用户分布图'
    },
    //配置了该项,鼠标指上去有提示
    tooltip: {
      formatter: '{b}注册用户{c}人'
    },
    visualMap: {
      //地图条
      //可视地图,一般用户设置不同颜色来展示数据差异
      left: 'left', //可视地图显示的位置
      top: 'center', //可视地图显示的位置
      min: 0, //区间的最小值
      max: 10000, //区间数据的最大值
      text: ['高', '低'],
      calculable: true //是否允许控制区间
    },
    series: [
      {
        type: 'map', //图标类型:地图
        map: 'China', //使用注册的地图
        roam: false, //是否开启鼠标缩放和平移
        scaleLimit: {
          min: 0.7, //最小缩放0.7倍
          max: 3 //最大放大3倍
        },
        data: users
      }
    ]
  })
  myChart.hideLoading()
}

onMounted(()=>{
  map()
})
</script>

<style>
.geo {
  width: 1000px;
  height: 1000px;
  position: fixed;
  display: flex;
  justify-content: center;
  align-items: center;
}
</style>
天气api封装
法一:
const params = {
	key: '92f30edb2bae4bad59fc50bdb3aeb3b3',
	adcode: '',
	baseUrl: 'https://restapi.amap.com/v3'
}

// 获取天气方法
async function getCityWeather(adcode, type = 'base') {
	const result = await axios.get(
		`${params.baseUrl}/weather/weatherInfo?key=${params.key}&city=${adcode}&extensions=${type}`
	)
	if (type === 'all') {
		return result.data.forecasts
	}
	return result.data.lives
}

// 根据用户ip地址获取城市编码
async function getCurrentCity(type = 'base') {
	const result = await axios.get(`${params.baseUrl}/ip?key=${params.key}`)
	const adcode = result.data.adcode
	const weather = await getCityWeather(adcode, type)
	return weather
}

// 根据用户输入的城市名获取对应的天气
async function getCityName(address, type = 'base') {
	const result = await axios.get(
		`${params.baseUrl}/geocode/geo?key=${params.key}&address=${address}`
	)
	const adcode = result.data.geocodes[0].adcode
	const weather = await getCityWeather(adcode, type)
	return weather
}

法二:
<template>
  <div class="weather">
    <h1>查看最近天气</h1>
    <form>
      <input type="text" v-model="city" placeholder="请输入城市" />
      <button type="submit" @click.prevent="getWeather">查询</button>
    </form>
    <div class="result" v-for="(item,index) in fulWeather" :key="index">
      <p>时间:{{ item.date }} - 星期{{ item.week }}</p>
      <p>白天天气:{{ item.dayweather }}</p>
      <p>晚上天气:{{ item.nightweather }}</p>
      <p>白天温度:{{ item.daytemp }}℃</p>
      <p>晚上温度:{{ item.nighttemp }}℃</p>
      <p>白天风向:向{{ item.daywind }}</p>
      <p>晚上风向:向{{ item.nightwind }}</p>
      <p>白天风力:{{ item.daypower }}级</p>
      <p>晚上风力:{{ item.nightpower }}级</p>
    </div>
  </div>
</template>

<script>
import axios from 'axios'
import { ElMessage } from 'element-plus'
export default {
  data() {
    return {
      city: '',
      fulWeather:null
    }
  },
  methods: {
    getWeather() {
      const fulUrl = `https://restapi.amap.com/v3/weather/weatherInfo?key=407cd13370e3e36bcb96759e9b08d958&city=${this.city}&output=json&extensions=all`
        //未来天气
      axios
        .get(fulUrl)
        .then(response => {
          const data = response.data
          console.log(data)
          if (data.status === '1') {
            const weatherData = data.forecasts[0]
            this.fulWeather = weatherData.casts
          } else if (!this.city) {
            ElMessage.error('请输入地点再查询')
          } else {
            ElMessage.error('获取天气信息失败')
          }
        })
        .catch(error => {
          console.log(error)
        })
    }
  }
}
</script>

<style scoped>
.weather {
  max-width: 500px;
  margin: 0 auto;
  text-align: center;
}
.weather h1 {
  font-size: 24px;
  margin-bottom: 20px;
}
.weather form {
  display: flex;
  margin-bottom: 20px;
}
.weather input {
  padding: 10px;
  margin-right: 10px;
  border: 1px solid #ccc;
  border-radius: 5px;
}
.weather button {
  padding: 10px 20px;
  background-color: #2196f3;
  color: #fff;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  transition: background-color 0.3s;
}
.weather button:hover {
  background-color: #1976d2;
}
.weather .result {
  margin-top: 20px;
  background-color: #f2f2f2;
  padding: 20px;
  border-radius: 5px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
.weather p {
  font-size: 16px;
  margin: 5px 0;
  text-align: left;
  padding-left: 0;
}
</style>

pinia使用-持久化存储
cnpm install pinia
cnpm i pinia-plugin-persist --save
***main.js
import store from './store'
createApp(App).use(router).use(store).use(ElementPlus).mount('#app')
***index.js
import { createPinia } from 'pinia'
import piniaPluginPersist from 'pinia-plugin-persist'

const store = createPinia()
store.use(piniaPluginPersist)

export default store
***user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore({
  id: 'user',
  state: () => {
    return {
      token: '',
      userInfo:{}
    }
  },
  actions:{
    //设置token
  	setToken( token ){
  		this.token = token;
  	},
    //清除token
    clearToken(){
      this.token = '';
      //清除用户信息
      this.userInfo = {};
    }
  },
  // 开启数据缓存
  persist: {
    enabled: true,
    strategies: [{
      key: 'xiaoluxian_user',
      storage: localStorage,
      //paths: ['token']
    }]
  }
})
***组件中:
//pinia
import { storeToRefs } from 'pinia';
import { useCartStore } from '@/store/cart'
let cartStore = useCartStore();
let { cartList , isChecked , total  } = storeToRefs( cartStore );
//生命周期
onBeforeMount(()=>{
  getShopCarList().then(res=>{
    cartStore.addCart( res.data.list );
  })
})


//pinia
import { useUserStore } from '../store/user'
const userStore = useUserStore();
userStore.setToken(res.data.accessToken);
富文本编辑器vue-quill-editor的使用
***main.js
// 导入富文本编辑器
import VueQuillEditor from 'vue-quill-editor'
// 导入富文本编辑器的样式
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
// 全局注册富文本编辑器
Vue.use(VueQuillEditor)
***组件中:
<el-form-item label="文章内容" prop="content">
            <!-- 使用 v-model 进行双向的数据绑定 -->
            <quill-editor v-model="pubForm.content"></quill-editor>
</el-form-item>
vuex-persistedstate使用
import Vue from 'vue'
import Vuex from 'vuex'
//createPersistedState插件默认使用storage:localStorage存储,默认的存储键名key是“vuex”,而且提供了option参数来修改默认配置和个性化存储。
//这里介绍一个vuex的插件包叫做`vuex-persistedstate@3.2.1`版本(配合vue2使用, 默认最新版是配合vue3使用)
import createPersistedState from 'vuex-persistedstate'
import { getUserInfoAPI } from '@/api'
Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    token: '',
    userInfo: {}
  },
  getters: {
    nickname: state => state.userInfo.nickname, // 昵称
    username: state => state.userInfo.username, // 用户名
    user_pic: state => state.userInfo.user_pic // 用户头像
  },
  mutations: {
    // token 的 mutation 函数
    Token(state, token) {
      state.token = token
    },
    // 用户的信息
    USERINFO(state, userInfo) {
      state.userInfo = userInfo
    }
  },
  actions: {
    async getUserInfo({ commit }) {
      let res = await getUserInfoAPI()
      // console.log(res)
      if (res.data.code == 0) {
        commit('USERINFO', res.data.data)
      }
    }
  },
  // 配置为 vuex 的插件
  plugins: [createPersistedState()]
})

选择地址封装
***组件中:
<div class="g-service">
    <dl>
      <dt>促销</dt>
      <dd>12月好物放送,App领券购买直降120元</dd>
    </dl>
    <dl>
      <dt>配送</dt>
      <dd>至 <XtxCity @change="changeCity" :fullLocation="fullLocation" /></dd>
    </dl>
    <dl>
      <dt>服务</dt>
      <dd>
        <span>无忧退货</span>
        <span>快速退款</span>
        <span>免费包邮</span>
        <a href="javascript:;">了解详情</a>
      </dd>
    </dl>
  </div>
***city.js
<template>
  <div class="xtx-city" ref="target">
    <div class="select" @click="toggle()" :class="{active:visible}">
      <span v-if="!fullLocation" class="placeholder">{{placeholder}}</span>
      <span v-else class="value">{{fullLocation}}</span>
      <i class="iconfont icon-angle-down"></i>
    </div>
    <div class="option" v-if="visible">
      <div v-if="loading" class="loading"></div>
      <template v-else>
        <span class="ellipsis" @click="changeItem(item)" v-for="item in currList" :key="item.code">{{item.name}}</span>
      </template>
    </div>
  </div>
</template>
<script>
import { computed, reactive, ref } from 'vue'
import { onClickOutside } from '@vueuse/core'
import axios from 'axios'
export default {
  name: 'XtxCity',
  props: {
    fullLocation: {
      type: String,
      default: ''
    },
    placeholder: {
      type: String,
      defulat: '请选择配送地址'
    }
  },
  setup (props, { emit }) {
    // 显示隐藏数据
    const visible = ref(false)

    // 所有省市区数据
    const allCityData = ref([])
    // 正在加载数据
    const loading = ref(false)

    // 提供打开和关闭函数
    const open = () => {
      visible.value = true
      // 获取地区数据
      loading.value = true
      getCityData().then(data => {
        allCityData.value = data
        loading.value = false
      })
      // 清空之前选择
      for (const key in changeResult) {
        changeResult[key] = ''
      }
    }
    const close = () => {
      visible.value = false
    }
    // 提供一个切换函数给select使用
    const toggle = () => {
      visible.value ? close() : open()
    }
    // 实现点击组件外部元素进行关闭操作
    const target = ref(null)
    onClickOutside(target, () => {
      // 参数1:监听那个元素
      // 参数2:点击了该元素外的其他地方触发的函数
      close()
    })

    // 实现计算属性:获取当前显示的地区数组
    const currList = computed(() => {
      // 默认省一级
      let list = allCityData.value
      // 可能:市一级
      if (changeResult.provinceCode && changeResult.provinceName) {
        list = list.find(p => p.code === changeResult.provinceCode).areaList
      }
      // 可能:县地区一级
      if (changeResult.cityCode && changeResult.cityName) {
        list = list.find(c => c.code === changeResult.cityCode).areaList
      }
      return list
    })

    // 定义选择的 省市区 数据
    const changeResult = reactive({
      provinceCode: '',
      provinceName: '',
      cityCode: '',
      cityName: '',
      countyCode: '',
      countyName: '',
      fullLocation: ''
    })
    // 当你点击按钮的时候记录
    const changeItem = (item) => {
      if (item.level === 0) {
        // 省
        changeResult.provinceCode = item.code
        changeResult.provinceName = item.name
      }
      if (item.level === 1) {
        // 市
        changeResult.cityCode = item.code
        changeResult.cityName = item.name
      }
      if (item.level === 2) {
        // 地区
        changeResult.countyCode = item.code
        changeResult.countyName = item.name
        // 完整路径
        changeResult.fullLocation = `${changeResult.provinceName} ${changeResult.cityName} ${changeResult.countyName}`
        // 这是最后一级,选完了,关闭对话框,通知父组件数据
        close()
        emit('change', changeResult)
      }
    }

    return { visible, toggle, target, loading, currList, changeItem }
  }
}
// 获取省市区数据函数
const getCityData = () => {
  // 数据:https://yjy-oss-files.oss-cn-zhangjiakou.aliyuncs.com/tuxian/area.json
  // 1. 当本地没有缓存,发请求获取数据
  // 2. 当本地缓存,取出本地数据
  // 返回promise在then获取数据,这里可能是异步操作可能是同步操作
  return new Promise((resolve, reject) => {
    // 约定:数据存储在window上的cityData字段
    if (window.cityData) {
      resolve(window.cityData)
    } else {
      const url = 'https://yjy-oss-files.oss-cn-zhangjiakou.aliyuncs.com/tuxian/area.json'
      axios.get(url).then(res => {
        // 缓存
        window.cityData = res.data
        resolve(res.data)
      })
    }
  })
}
</script>
<style scoped lang="less">
.xtx-city {
  display: inline-block;
  position: relative;
  z-index: 400;
  .select {
    border: 1px solid #e4e4e4;
    height: 30px;
    padding: 0 5px;
    line-height: 28px;
    cursor: pointer;
    &.active {
      background: #fff;
    }
    .placeholder {
      color: #999;
    }
    .value {
      color: #666;
      font-size: 12px;
    }
    i {
      font-size: 12px;
      margin-left: 5px;
    }
  }
  .option {
    width: 542px;
    border: 1px solid #e4e4e4;
    position: absolute;
    left: 0;
    top: 29px;
    background: #fff;
    min-height: 30px;
    line-height: 30px;
    display: flex;
    flex-wrap: wrap;
    padding: 10px;
    > span {
      width: 130px;
      text-align: center;
      cursor: pointer;
      border-radius: 4px;
      padding: 0 3px;
      &:hover {
        background: #f5f5f5;
      }
    }
    .loading {
      height: 290px;
      width: 100%;
      background: url(../../assets/images/loading.gif) no-repeat center;
    }
  }
}
</style>

mockjs包使用
***mock.js
//先引入mockjs模块
import Mock from 'mockjs'
//把JsON数据格式引入进来[JSON数据格式根本没有对外暴露,但是可以引入]
//webpack默认对外暴露的:图片、JSON数据格式
import banner from './banner.json'
import floor from './floor.json'
//mock数据:第一个参数请求地址第二个参数:请求数据
Mock.mock('/mock/banner', { code: 200, data: banner }) //模拟首页大的轮播图的数据
Mock.mock('/mock/floor', { code: 200, data: floor })
************************************************************************************************************
***banner.json
[
  {
    "id": "1",
    "imgUrl": "/images/banner1.jpg"
  },
  {
    "id": "2",
    "imgUrl": "/images/banner2.jpg"
  },
  {
    "id": "3",
    "imgUrl": "/images/banner3.jpg"
  },
  {
    "id": "4",
    "imgUrl": "/images/banner4.jpg"
  }
]
************************************************************************************************************
***floor.json
[
  {
    "id": "001",
    "name": "家用电器",
    "keywords": ["节能补贴", "4K电视", "空气净化器", "IH电饭煲", "滚筒洗衣机", "电热水器"],
    "imgUrl": "/images/floor-1-1.png",
    "navList": [
      {
        "url": "#",
        "text": "热门"
      },
      {
        "url": "#",
        "text": "大家电"
      },
      {
        "url": "#",
        "text": "生活电器"
      },
      {
        "url": "#",
        "text": "厨房电器"
      },
      {
        "url": "#",
        "text": "应季电器"
      },
      {
        "url": "#",
        "text": "空气/净水"
      },
      {
        "url": "#",
        "text": "高端电器"
      }
    ],
    "carouselList": [
      {
        "id": "0011",
        "imgUrl": "/images/floor-1-b01.png"
      },
      {
        "id": "0012",
        "imgUrl": "/images/floor-1-b02.png"
      },
      {
        "id": "0013",
        "imgUrl": "/images/floor-1-b03.png"
      }
    ],
    "recommendList": ["/images/floor-1-2.png", "/images/floor-1-3.png", "/images/floor-1-5.png", "/images/floor-1-6.png"],
    "bigImg": "/images/floor-1-4.png"
  },
  {
    "id": "002",
    "name": "手机通讯",
    "keywords": ["节能补贴2", "4K电视2", "空气净化器2", "IH电饭煲2", "滚筒洗衣机2", "电热水器2"],
    "imgUrl": "/images/floor-1-1.png",
    "navList": [
      {
        "url": "#",
        "text": "热门2"
      },
      {
        "url": "#",
        "text": "大家电2"
      },
      {
        "url": "#",
        "text": "生活电器2"
      },
      {
        "url": "#",
        "text": "厨房电器2"
      },
      {
        "url": "#",
        "text": "应季电器2"
      },
      {
        "url": "#",
        "text": "空气/净水2"
      },
      {
        "url": "#",
        "text": "高端电器2"
      }
    ],
    "carouselList": [
      {
        "id": "0011",
        "imgUrl": "/images/floor-1-b01.png"
      },
      {
        "id": "0012",
        "imgUrl": "/images/floor-1-b02.png"
      },
      {
        "id": "0013",
        "imgUrl": "/images/floor-1-b03.png"
      }
    ],
    "recommendList": ["/images/floor-1-2.png", "/images/floor-1-3.png", "/images/floor-1-5.png", "/images/floor-1-6.png"],
    "bigImg": "/images/floor-1-4.png"
  }
]
************************************************************************************************************
***mockajax.js
//对于axios进行二次封装
import axios from 'axios'
import nprogress from 'nprogress'
//引入进度条的样式
import 'nprogress/nprogress.css'
//start:进度条开始 done:进度条结束

//1:利用axios对象的方法create,去创建一个axios实例
//2:requests就是axios,只不过稍做配置一下
const requests = axios.create({
  //配置对象
  //基础路径,发请求URL携带api【发现:真实服务器接口都携带/api】
  baseURL: '/mock',
  //超时的设置
  timeout: 3000
})
//请求拦截器:将来项目中【N个请求】,只要发请求,会触发请求拦截器!!!
requests.interceptors.request.use(config => {
  //请求拦截器:请求头【header】,请求头能否给服务器携带参数
  //请求拦截器:其实项目中还有一个重要的作用,给服务器携带请求们的公共的参数
  //config:配置对象,对象里面有一个属性很重要,headers请求头

  //进度条开始动
  nprogress.start()
  return config
})
//响应拦截器:请求数据返回会执行
requests.interceptors.response.use(
  res => {
    //res:实质就是项目中发请求、服务器返回的数据

    //进度条结束
    nprogress.done()
    return res.data
  },
  err => {
    //温馨提示:某一天发请求,请求失败,请求失败的信息打印出来
    //终止Promise链
    return Promise.reject(new Error('failed'))
  }
)

//最后需要暴露:暴露的是添加新的功能的axios,即为requests
export default requests
************************************************************************************************************
***api.js
import mockRequests from './mockAjax'
//获取banner (Home首页轮播图接口)获取banner和floor的假数据
export const reqGetBannerList = () => mockRequests.get('/banner')

//获取floor数据
export const reqFloorList = () => mockRequests.get('/floor')
amfe-flexible自适应包
npm install amfe-flexible --save
npm install postcss-pxtorem --save

    "amfe-flexible": "^2.2.1",
	"postcss": "^8.4.1",
    "postcss-pxtorem": "5.1.1",
***main.js
// rem(root em)是一个相对单位,基准是相对于html元素的字体大小(浏览器默认字体大小是16px)。如:根元素(html)设置font-size: 12px;  非根元素设置width: 2rem; 则换成px表示就是24px。
// amfe-flexible:原理是把当前设备宽度划分为10等份,动态设置html元素的字体大小为一份。如:当前屏幕宽度为360px,html元素的字体大小为36px。
// postcss-pxtorem:不同设备下,元素占比是一定的,即rem的值不变。此工具自动将px转成rem。
import 'amfe-flexible' // 引入flexible.js -> 设置根标签字体大小(移动端适配)
nextTick案例

image-20230416161804679

image-20230416161858230

清除默认css样式的包
cnpm install --save normalize.css
import 'normalize.css/normalize.css' // 清除默认css样式的包
处理字符串高亮关键字
<!--v-html 解析标签 `<span style="color: red;">${match}</span>`-->
      <div
        class="sugg-item"
        v-for="(str, index) in suggestList"
        :key="index"
        v-html="lightFn(str, kw)"
        @click="suggestClickFn(str)"
	></div>

// 专门处理字符串高亮关键字
    lightFn (originStr, target) {
      // orginStr: 原来字符串
      // target: 关键字
      // 字符串.replace() -> 替换第一个符合条件
      // 字符串.replaceAll() -> 替换全部
      // 例如: "好同志, 都是招募来的", 关键字是: "好"
      // 返回值: 替换后的完整字符串

      // 查文档
      // 如果你要使用变量, 作为正则的匹配条件, 不能用语法糖简化写法
      const reg = new RegExp(target, 'ig') // i忽略大小写, g全局都匹配
      // 替换后的值不能用target
      // 例如: 输入框里是 java, 匹配回来的联想菜单Java, JAVA, jAVA, 如果用target,都被你replace换成输入框target值java
      return originStr.replace(reg, (match) => {
        // match就是匹配中时, 原字符串里的那个字符, 用人家原来的, 只不过我们给个颜色即可
        return `<span style="color: red;">${match}</span>`
      })
    }
socket.io-client使用
npm i socket.io-client

<template>
  <div class="container">
    <!-- 固定导航 -->
    <van-nav-bar fixed left-arrow @click-left="$router.back()" title="小思同学"></van-nav-bar>

    <!-- 聊天主体区域 -->
    <div class="chat-list">
      <div v-for="(obj, index) in list"
      :key="index"
      >
          <!-- 左侧是机器人小思 -->
          <div class="chat-item left" v-if="obj.name !== 'me'">
            <van-image fit="cover" round src="https://img.yzcdn.cn/vant/cat.jpeg" />
            <div class="chat-pao">{{ obj.msg }}</div>
          </div>

          <!-- 右侧是当前用户 -->
          <div class="chat-item right" v-else>
            <div class="chat-pao">{{ obj.msg }}</div>
            <van-image fit="cover" round :src="$store.state.userPhoto" />
          </div>
       </div>
    </div>

    <!-- 对话区域 -->
    <div class="reply-container van-hairline--top">
      <van-field v-model="word"  placeholder="说点什么..." @keyup.enter="sendFn">
        <template #button>
          <span  style="font-size:12px;color:#999" @click="sendFn">提交</span>
        </template>
      </van-field>
    </div>
  </div>
</template>

<script>
// 1. 下包, 引入io
import { io } from 'socket.io-client'
import { getToken } from '@/utils/token.js'
export default {
  name: 'Chat',
  data () {
    return {
      word: '', // 输入框的内容
      list: [ // 所有的聊天消息
        // 只根据 name 属性,即可判断出这个消息应该渲染到左侧还是右侧
        { name: 'xs', msg: 'hi,你好!我是小思' },
      ],
      socket: null // 客户端和服务器端建立链接的socket对象
    }
  },
  created () {
    // 注意: io是建立socket链接, 和axios一毛钱关系也没有  ws和http协议都行
    this.socket = io('http://geek.itheima.net', {
      query: {
        token: getToken()
      },
      transports: ['websocket']
    })

    // 测试下是否建立链接成功
    this.socket.on('connect', () => {
      // console.log('链接建立成功')
    })

    // 接收后端传回来的消息
    this.socket.on('message', obj => {
      // 立刻组织相同字段对象放到数组里 -> v-for更新
      this.list.push({
        name: 'xs',
        msg: obj.msg
      })

      // 最后一条聊天消息滚动到屏幕范围
      // 数据变化->DOM更新是异步的, 所以获取到的是上一条div
      this.$nextTick(() => {
        const theDiv = document.querySelector('.chat-list>div:last-child')
        theDiv.scrollIntoView({
          behavior: 'smooth'
        })
      })
    })
  },
  methods: {
    // 发送span->点击事件
    sendFn () {
      if (this.word.trim().length === 0) return
      // 用socket链接对象.emit('后端接收消息的事件名', 值)
      this.socket.emit('message', {
        msg: this.word,
        timestamp: new Date().getTime()
      })

      // 把消息显示到页面上
      this.list.push({
        msg: this.word,
        name: 'me'
      })
      //最后一条聊天消息滚动到屏幕范围
      //数据变化->DOM更新是异步的,所以获取到的是上一条div
      this.$nextTick(() => {
        const theDiv = document.querySelector('.chat-list>div:last-child')
        theDiv.scrollIntoView({
          behavior: 'smooth'
        })
      })

      // 清空输入框
      this.word = ''
    }
  },
  destroyed () {
    this.socket.close()
    this.socket = null
  }
}
</script>

<style lang="less" scoped>
.container {
  height: 100%;
  width: 100%;
  position: absolute;
  left: 0;
  top: 0;
  box-sizing: border-box;
  background: #fafafa;
  padding: 46px 0 50px 0;
  .chat-list {
    height: 100%;
    overflow-y: scroll;
    .chat-item {
      padding: 10px;
      .van-image {
        vertical-align: top;
        width: 40px;
        height: 40px;
      }
      .chat-pao {
        vertical-align: top;
        display: inline-block;
        min-width: 40px;
        max-width: 70%;
        min-height: 40px;
        line-height: 38px;
        border: 0.5px solid #c2d9ea;
        border-radius: 4px;
        position: relative;
        padding: 0 10px;
        background-color: #e0effb;
        word-break: break-all;
        font-size: 14px;
        color: #333;
        &::before {
          content: '';
          width: 10px;
          height: 10px;
          position: absolute;
          top: 12px;
          border-top: 0.5px solid #c2d9ea;
          border-right: 0.5px solid #c2d9ea;
          background: #e0effb;
        }
      }
    }
  }
}
.chat-item.right {
  text-align: right;
  .chat-pao {
    margin-left: 0;
    margin-right: 15px;
    &::before {
      right: -6px;
      transform: rotate(45deg);
    }
  }
}
.chat-item.left {
  text-align: left;
  .chat-pao {
    margin-left: 15px;
    margin-right: 0;
    &::before {
      left: -5px;
      transform: rotate(-135deg);
    }
  }
}
.reply-container {
  position: fixed;
  left: 0;
  bottom: 0;
  height: 44px;
  width: 100%;
  background: #f5f5f5;
  z-index: 9999;
}
</style>

滚动页面
法一:
// 评论按钮 -> 点击事件-> 把第一条评论滑动到最上面
    commentClickFn () {
      // 看扩展里ppt, 只要设置window.scrollTo(0, 文章内容高度)
      // JS代码是在html+css运行后, 真实DOM已经在网页上了, 从document往下获取标签是ok的
      //   const articleHeight = document.querySelector('.article-container').scrollHeight
      //   // window.scrollTo() 使网页进行滚动, 设置相应的坐标, 就可以让网页滚动到屏幕的最顶端
      //   // 如果底下没有内容了, 则不再滚动
      //   window.scrollTo(0, articleHeight)

      // css可以做动画: 例如轮播图, CSS3位移, 旋转, 动画 (你必须修改css属性才能触发css动画)
      // 使用: animation (配合帧动画), transition (过渡动画)
      // js也可以做动画: 滚动条滚动....
      // 使用: 计时器间隔时间, 修改目标状态, (动画片一样)
      // 动画实现的JS原生代码, 在配置资料扩展-> txt文档里

      // 比较方便的方法(既能滚动也能带动画)
      // 原生标签.scrollIntoView(), 让原生的标签滚动到屏幕的最上面
      // 为何选择like-box不选择第一条评论, 因为头部导航会挡住
      // 注意: 有的人的电脑不支持这个方法, 没有滑动的效果 -> 只能用原生JS写(兼容性好)
      document.querySelector('.like-box').scrollIntoView({
        behavior: 'smooth' // 设置出现的滑动效果->平滑的动画
      })
    }
法二:
// 实现歌词滚动
    lyricScroll(currentLyric) {
      let placeholderHeight = 0;
      // 获取歌词item
      let lyricsArr = document.querySelectorAll(".lyricsItem");
      // 获取歌词框
      let lyrics = document.querySelector(".lyrics");
      // console.log(lyrics.offsetTop)//123
      // console.log(lyricsArr[0].offsetTop)//123
      // placeholder的高度
      if (placeholderHeight == 0) {
        placeholderHeight = lyricsArr[0].offsetTop - lyrics.offsetTop;//123-123
        // console.log(placeholderHeight)//0
      }
      //   歌词item在歌词框的高度 = 歌词框的offsetTop - 歌词item的offsetTop
        // console.log(currentLyric);//歌词索引
        // console.log(lyricsArr[currentLyric - 1])//歌词第一句打印的是全部歌词,后面打印的是上一句歌词的div
      if (lyricsArr[currentLyric - 1]) {
        let distance = lyricsArr[currentLyric - 1].offsetTop - lyrics.offsetTop;
        // console.log(lyricsArr[currentLyric - 1].offsetTop)
        // console.log(lyrics.offsetTop)//123
        // console.log(distance)
        //   lyricsArr[currentLyric].scrollIntoView();
        lyrics.scrollTo({
          behavior: "smooth",
          top: distance - placeholderHeight,
        });
      }
    }
法三:
移动端保存切换页面的滚动条位置
在路由中设置meta
{
    path: '/layout',
    component: () => import('@/views/Layout'),
    children: [
      {
        path: 'home',
        component: () => import('@/views/Home'),
        meta: {
          scrollT: 0 // 保存首页离开时, 滚动条位置
        }
      },
      {
        path: 'user',
        component: () => import('@/views/User')
      }
    ]
}

channelScrollTObj: {} // 保存每个频道的滚动位置

// tabs切换的事件  ->  获取文章列表数据
    channelChangeFn () {
      // tab切换后, 设置滚动条位置
      // tab切换时, 这个组件内部, 会把切走的容器height设置为0, 滚动条因为没有高度回到了顶部
      // 切回来的一瞬间, 没有高度, 滚动事件从底下上来也被触发了, 所以才把数据里设置为0
      // 切换来的一瞬间, 高度为0, 你设置滚动位置也无用
      //先让dom更新
      this.$nextTick(() => {
        document.documentElement.scrollTop = this.channelScrollTObj[this.channelId]
        document.body.scrollTop = this.channelScrollTObj[this.channelId]
      })
    },
// 监听网页滚动事件
    scrollFn () {
      // 谷歌浏览器内核, 和安卓手机内置浏览器的内核不是同一个
      // 获取scrollTop方式不同
      // 谷歌浏览器用的html的scrollTop
      // 有的浏览器用的body的scrollTop
      // Notify({
      //   message: document.body.scrollTop
      // })
      this.$route.meta.scrollT = document.documentElement.scrollTop || document.body.scrollTop
      // 同时保存当前频道的滚动距离  //滚动位置存到对应的频道
      this.channelScrollTObj[this.channelId] = document.documentElement.scrollTop || document.body.scrollTop
    }
    
    
// 只有使用keep-alive的组件才有这2个生命周期
activated () { // 切回来
    // console.log(this.$route)   实时监听滚动事件
    window.addEventListener('scroll', this.scrollFn)
    // window和document, 监听网页滚动的事件   监听scroll事件
    // html标签获取scrollTop, 滚动的距离, 和设置滚动的位置
    // 立刻设置, 滚动条位置
    document.documentElement.scrollTop = this.$route.meta.scrollT
    document.body.scrollTop = this.$route.meta.scrollT
  },
  deactivated () { // 切走
    window.removeEventListener('scroll', this.scrollFn)
  }
  // 先切走了, 滚动条回到顶部, 才触发deactivated失焦, 所以拿不到滚动位置了
封装本地存储的方式
// 封装本地存储的方式
// 整个项目使用localStorage, sessionStorage, 还是cookie
// 只需要改变这里即可
// 封装: 为了统一管理, 方便以后替换和修改
export const setStorage = (key, value) => {
  localStorage.setItem(key, value)
}

export const getStorage = (key) => {
  return localStorage.getItem(key)
}

export const removeStorage = (key) => {
  localStorage.removeItem(key)
}
二次封装axios
install插件、全局注册组件
法一:
***index.js
这些组件在其他组件中用到时,不再需要import和注册,直接在组件中用

import PageTools from '@/components/PageTools'
import UploadImg from '@/components/UploadImg'
import ImageHolder from '@/components/ImageHolder'
import Lang from '@/components/Lang'
import ScreenFull from '@/components/ScreenFull'

export default { // 导出插件对象
  install(Vue) {
    Vue.component('PageTools', PageTools)
    Vue.component('UploadImg', UploadImg)
    Vue.component('ImageHolder', ImageHolder)
    Vue.component('Lang', Lang)
    Vue.component('ScreenFull', ScreenFull)
  }
}

// 导出的方式
// export 变量声明 -> import { 名一致 }

// export default 导出
// import 变量名
************************************************************************************************************
法二:
// 扩展vue原有的功能:全局组件,自定义指令,挂载原型方法,注意:没有全局过滤器。
// 这就是插件
// vue2.0插件写法要素:导出一个对象,有install函数,默认传入了Vue构造函数,Vue基础之上扩展
// vue3.0插件写法要素:导出一个对象,有install函数,默认传入了app应用实例,app基础之上扩展

import defaultImg from '@/assets/images/200.png'
// import XtxSkeleton from './xtx-skeleton.vue'
// import XtxCarousel from './xtx-carousel.vue'
// import XtxMore from './xtx-more.vue'
// import XtxBread from './xtx-bread.vue'
// import XtxBreadItem from './xtx-bread-item.vue'

// 使用 `require` 提供的函数 `context`  加载某一个目录下的所有 `.vue` 后缀的文件。
// 然后 `context` 函数会返回一个导入函数 `importFn`
// - 它又一个属性 `keys() `  获取所有的文件路径
// 通过文件路径数组,通过遍历数组,再使用 `importFn`  根据路径导入组件对象
// 遍历的同时进行全局注册即可

import Message from './Message'
import Confirm from './Confirm'

// context(目录路径,是否加载子目录,加载文件的匹配正则)
const importFn = require.context('./', false, /\.vue$/)

export default {
  install (app) {
    // 在app上进行扩展,app提供 component directive 函数
    // 如果要挂载原型 app.config.globalProperties 方式
    // app.component(XtxSkeleton.name, XtxSkeleton)
    // app.component(XtxCarousel.name, XtxCarousel)
    // app.component(XtxMore.name, XtxMore)
    // app.component(XtxBread.name, XtxBread)
    // app.component(XtxBreadItem.name, XtxBreadItem)

    // 根据keys批量注册
    importFn.keys().forEach(path => {
      // 导入组件
      const component = importFn(path).default
      // 进行注册
      app.component(component.name, component)
    })

    // 定义指令
    defineDirective(app)

    // 定义一个原型函数
    app.config.globalProperties.$message = Message
    app.config.globalProperties.$confirm = Confirm
  }
}

// 定义指令
const defineDirective = (app) => {
  // 1. 图片懒加载指令 v-lazy
  // 原理:先存储图片地址不能在src上,当图片进入可视区,将你存储图片地址设置给图片元素即可。
  app.directive('lazy', {
    // vue2.0 监听使用指令的DOM是否创建好,钩子函数:inserted
    // vue3.0 的指令拥有的钩子函数和组件的一样,使用指令的DOM是否创建好,钩子函数:mounted
    mounted (el, binding) {
      // 2. 创建一个观察对象,来观察当前使用指令的元素
      const observe = new IntersectionObserver(([{ isIntersecting }]) => {
        if (isIntersecting) {
          // 停止观察
          observe.unobserve(el)
          // 3. 把指令的值设置给el的src属性 binding.value就是指令的值
          // 4. 处理图片加载失败 error 图片加载失败的事件 load 图片加载成功
          el.onerror = () => {
            // 加载失败,设置默认图
            el.src = defaultImg
          }
          el.src = binding.value
        }
      }, {
        threshold: 0
      })
      // 开启观察
      observe.observe(el)
    }
  })
}

************************************************************************************************************
法三:
***main.js
import directiveObj from './utils/directive'//自己封装的全局插件
Vue.use(directiveObj) // 执行目标对象里install方法并传入Vue类
***组件中:
<!-- 组件内会给原生input标签, 绑定keypress-按键事件
        $emit('search')
       -->
      <van-search
        v-model.trim="kw"
        v-fofo
        placeholder="请输入搜索关键词"
        background="#007BFF"
        shape="round"
        @input="inputFn"
        @search="searchFn"
      />
      
***.js中
// 对Vue的全局指令, 进行封装
// 封装中间件函数插件
const directiveObj = {
  install (Vue) {
    Vue.directive('fofo', {
      // el代表指令所在标签
      // 指令所在标签, 被插入到真实DOM时才触发, 如果标签用display:none隐藏再出现, 不会再触发inserted的
      inserted (el) {
        // 指令所在van-search组件
        // 组件根标签是div, input在内部
        // 以上都是原生标签对象
        // 搜索页面 el是div
        // 文章评论 el是textarea
        // 以后el还可能是input呢
        // 知识点: 原生DOM.nodeName 拿到标签名字 (注意: 大写的字符串)
        if (el.nodeName === 'TEXTAREA' || el.nodeName === 'INPUT') {
          el.focus()
        } else {
          // el本身不是输入框, 尝试往里获取一下
          setTimeout(() => {
            const theInput = el.querySelector('input')
            const theTextArea = el.querySelector('textarea')
            // 判断: 不一定能获取得到, 需要加判断, 有值了, 再执行.focus()才不报错
            if (theInput) theInput.focus()
            if (theTextArea) theTextArea.focus()
          })
        }
      },
      update (el) { // 指令所在标签, 被更新时触发,用于编辑姓名的input自动聚焦
        if (el.nodeName === 'TEXTAREA' || el.nodeName === 'INPUT') {
          el.focus()
        } else {
          // el本身不是输入框, 尝试往里获取一下
          setTimeout(() => {
            const theInput = el.querySelector('input')
            const theTextArea = el.querySelector('textarea')
            // 判断: 不一定能获取得到, 需要加判断, 有值了, 再执行.focus()才不报错
            if (theInput) theInput.focus()
            if (theTextArea) theTextArea.focus()
          })
        }
      }
    })
  }
}

export default directiveObj
************************************************************************************************************
法三:
***index.js
// 扩展vue原有的功能:全局组件,自定义指令,挂载原型方法,注意:没有全局过滤器。
// 这就是插件
// vue2.0插件写法要素:导出一个对象,有install函数,默认传入了Vue构造函数,Vue基础之上扩展
// vue3.0插件写法要素:导出一个对象,有install函数,默认传入了app应用实例,app基础之上扩展

import defaultImg from '@/assets/images/200.png'
// import XtxSkeleton from './xtx-skeleton.vue'
// import XtxCarousel from './xtx-carousel.vue'
// import XtxMore from './xtx-more.vue'
// import XtxBread from './xtx-bread.vue'
// import XtxBreadItem from './xtx-bread-item.vue'

// 使用 `require` 提供的函数 `context`  加载某一个目录下的所有 `.vue` 后缀的文件。
// 然后 `context` 函数会返回一个导入函数 `importFn`
// - 它又一个属性 `keys() `  获取所有的文件路径
// 通过文件路径数组,通过遍历数组,再使用 `importFn`  根据路径导入组件对象
// 遍历的同时进行全局注册即可

import Message from './Message'
import Confirm from './Confirm'

// context(目录路径,是否加载子目录,加载文件的匹配正则)
const importFn = require.context('./', false, /\.vue$/)

export default {
  install (app) {
    // 在app上进行扩展,app提供 component directive 函数
    // 如果要挂载原型 app.config.globalProperties 方式
    // app.component(XtxSkeleton.name, XtxSkeleton)
    // app.component(XtxCarousel.name, XtxCarousel)
    // app.component(XtxMore.name, XtxMore)
    // app.component(XtxBread.name, XtxBread)
    // app.component(XtxBreadItem.name, XtxBreadItem)

    // 根据keys批量注册
    importFn.keys().forEach(path => {
      // 导入组件
      const component = importFn(path).default
      // 进行注册
      app.component(component.name, component)
    })

    // 定义指令
    defineDirective(app)

    // 定义一个原型函数
    app.config.globalProperties.$message = Message
    app.config.globalProperties.$confirm = Confirm
  }
}

// 定义指令
const defineDirective = (app) => {
  // 1. 图片懒加载指令 v-lazy
  // 原理:先存储图片地址不能在src上,当图片进入可视区,将你存储图片地址设置给图片元素即可。
  app.directive('lazy', {
    // vue2.0 监听使用指令的DOM是否创建好,钩子函数:inserted
    // vue3.0 的指令拥有的钩子函数和组件的一样,使用指令的DOM是否创建好,钩子函数:mounted
    mounted (el, binding) {
      // 2. 创建一个观察对象,来观察当前使用指令的元素
      const observe = new IntersectionObserver(([{ isIntersecting }]) => {
        if (isIntersecting) {
          // 停止观察
          observe.unobserve(el)
          // 3. 把指令的值设置给el的src属性 binding.value就是指令的值
          // 4. 处理图片加载失败 error 图片加载失败的事件 load 图片加载成功
          el.onerror = () => {
            // 加载失败,设置默认图
            el.src = defaultImg
          }
          el.src = binding.value
        }
      }, {
        threshold: 0
      })
      // 开启观察
      observe.observe(el)
    }
  })
}
***Confirm.js
import { createVNode, render } from 'vue-demi'
import XtxConfirm from './xtx-confirm.vue'

// 准备一个DOM
const div = document.createElement('div')
div.setAttribute('class', 'xtx-confirm-container')
document.body.appendChild(div)

// 返回的是promise对象,点取消销毁组件,点确认销毁组件
export default ({ title, text }) => {
  // 1. 导入被创建的组件
  // 2. 使用createVNode创建虚拟节点
  // 3. 准备一个dom容器装载组件
  // 4. 使用render函数渲染组件到容器
  return new Promise((resolve, reject) => {
    // 点击取消触发的函数
    const cancelCallback = () => {
      render(null, div)
      reject(new Error('点击取消'))
    }
    // 点击确认触发的函数
    const submitCallback = () => {
      render(null, div)
      resolve()
    }

    const vn = createVNode(XtxConfirm, { title, text, cancelCallback, submitCallback })
    render(vn, div)
  })
}
***Message.js
// 提供一个能够显示xtx-message组件的函数
// 这个函数将来:导入直接使用,也可以挂载在vue实例原型上
// import Message from 'Message.js' 使用 Message({type:'error',text:'提示文字'})
// this.$message({type:'error',text:'提示文字'})

import { createVNode, render } from 'vue'
import XtxMessage from './xtx-message.vue'

// DOM容器
const div = document.createElement('div')
div.setAttribute('class', 'xtx-msssage-container')
document.body.appendChild(div)

// 定时器标识
let timer = null

export default ({ type, text }) => {
  // 渲染组件
  // 1. 导入消息提示组件
  // 2. 将消息提示组件编译为虚拟节点(dom节点)
  // createVNode(组件,属性对象(props))
  const vnode = createVNode(XtxMessage, { type, text })
  // 3. 准备一个装载消息提示组件的DOM容器
  // 4. 将虚拟节点渲染再容器中
  // render(虚拟节点,DOM容器)
  render(vnode, div)
  // 5. 3s后销毁组件
  clearTimeout(timer)
  timer = setTimeout(() => {
    render(null, div)
  }, 3000)
}
***Message.vue
<template>
  <Transition name="down">
    <div class="xtx-message" :style="style[type]" v-show="visible">
      <!-- 上面绑定的是样式 -->
      <!-- 不同提示图标会变 :class="{'icon-warning':true}" :class="['icon-warning']" -->
      <i class="iconfont" :class="[style[type].icon]"></i>
      <span class="text">{{text}}</span>
    </div>
  </Transition>
</template>
<script>
import { onMounted, ref } from 'vue'
export default {
  name: 'XtxMessage',
  props: {
    type: {
      type: String,
      default: 'warn'
    },
    text: {
      type: String,
      default: ''
    }
  },
  setup () {
    // 定义一个对象,包含三种情况的样式,对象key就是类型字符串
    const style = {
      warn: {
        icon: 'icon-warning',
        color: '#E6A23C',
        backgroundColor: 'rgb(253, 246, 236)',
        borderColor: 'rgb(250, 236, 216)'
      },
      error: {
        icon: 'icon-shanchu',
        color: '#F56C6C',
        backgroundColor: 'rgb(254, 240, 240)',
        borderColor: 'rgb(253, 226, 226)'
      },
      success: {
        icon: 'icon-queren2',
        color: '#67C23A',
        backgroundColor: 'rgb(240, 249, 235)',
        borderColor: 'rgb(225, 243, 216)'
      }
    }
    // 控制元素显示隐藏
    const visible = ref(false)
    onMounted(() => {
      visible.value = true
    })
    return { style, visible }
  }
}
</script>
<style scoped lang="less">
.down {
  &-enter {
    &-from {
      transform: translate3d(0,-75px,0);
      opacity: 0;
    }
    &-active {
      transition: all 0.5s;
    }
    &-to {
      transform: none;
      opacity: 1;
    }
  }
}
.xtx-message {
  width: 300px;
  height: 50px;
  position: fixed;
  z-index: 9999;
  left: 50%;
  margin-left: -150px;
  top: 25px;
  line-height: 50px;
  padding: 0 25px;
  border: 1px solid #e4e4e4;
  background: #f5f5f5;
  color: #999;
  border-radius: 4px;
  i {
    margin-right: 4px;
    vertical-align: middle;
  }
  .text {
    vertical-align: middle;
  }
}
</style>
***Confirm.js
<template>
  <div class="xtx-confirm" :class="{fade}">
    <div class="wrapper" :class="{fade}">
      <div class="header">
        <h3>{{title}}</h3>
        <a @click="cancel" href="JavaScript:;" class="iconfont icon-close-new"></a>
      </div>
      <div class="body">
        <i class="iconfont icon-warning"></i>
        <span>{{text}}</span>
      </div>
      <div class="footer">
        <XtxButton @click="cancel" size="mini" type="gray">取消</XtxButton>
        <XtxButton @click="submit" size="mini" type="primary">确认</XtxButton>
      </div>
    </div>
  </div>
</template>
<script>
import { onMounted, ref } from 'vue'
import XtxButton from './xtx-button'
export default {
  name: 'XtxConfirm',
  components: { XtxButton },
  props: {
    title: {
      type: String,
      default: '温馨提示'
    },
    text: {
      type: String,
      default: ''
    },
    cancelCallback: {
      type: Function
    },
    submitCallback: {
      type: Function
    }
  },
  setup (props) {
    // 对话框默认隐藏
    const fade = ref(false)
    // 组件渲染完毕后
    onMounted(() => {
      // 过渡效果需要在元素创建完毕后延时一会加上才会触发
      setTimeout(() => {
        fade.value = true
      }, 0)
    })
    // 取消
    const cancel = () => {
      // 其他事情
      props.cancelCallback()
    }
    // 确认
    const submit = () => {
      // 其他事情
      props.submitCallback()
    }
    return { cancel, submit, fade }
  }
}
</script>
<style scoped lang="less">
.xtx-confirm {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  z-index: 8888;
  background: rgba(0,0,0,0);
  &.fade {
    transition: all 0.4s;
    background: rgba(0,0,0,.5);
  }
  .wrapper {
    width: 400px;
    background: #fff;
    border-radius: 4px;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%,-60%);
    opacity: 0;
    &.fade {
      transition: all 0.4s;
      transform: translate(-50%,-50%);
      opacity: 1;
    }
    .header,.footer {
      height: 50px;
      line-height: 50px;
      padding: 0 20px;
    }
    .body {
      padding: 20px 40px;
      font-size: 16px;
      .icon-warning {
        color: @priceColor;
        margin-right: 3px;
        font-size: 16px;
      }
    }
    .footer {
      text-align: right;
      .xtx-button {
        margin-left: 20px;
      }
    }
    .header {
      position: relative;
      h3 {
        font-weight: normal;
        font-size: 18px;
      }
      a {
        position: absolute;
        right: 15px;
        top: 15px;
        font-size: 20px;
        width: 20px;
        height: 20px;
        line-height: 20px;
        text-align: center;
        color: #999;
        &:hover {
          color: #666;
        }
      }
    }
  }
}
</style>
***组件中使用:
import Message from '@/components/library/Message'
import Confirm from '@/components/library/Confirm'
// 删除
    const deleteCart = (skuId) => {
      Confirm({ text: '亲,您是否确认删除商品' }).then(() => {
        store.dispatch('cart/deleteCart', skuId).then(() => {
          Message({ type: 'success', text: '删除成功' })
        })
      }).catch(e => {})
    }
    // 批量删除选中商品,也支持清空无效商品
    const batchDeleteCart = (isClear) => {
      Confirm({ text: `亲,您是否确认删除${isClear ? '失效' : '选中'}的商品` }).then(() => {
        store.dispatch('cart/batchDeleteCart', isClear)
      }).catch(e => {})
    }
取消掉项目发布后的console提示
***console.js
// 1.开发环境, 生产环境, 是2套不同的环境
// 开发环境需要console.log使用
// 生产环境不需console.log使用
// 让一套代码, 在2个环境自动生效
// nodejs打包时执行main.js代码时, node内全局内置变量process(固定)
// console.log(process.env)
// 2.服务器根目录下, 可以新建环境变量配置文件(文件名固定)
// 脚手架环境webpack内置配好的, 文件名(可以修改的但是要改配置-自行百度)
// .env.development
// .env.production
// 3.环境变量文件中, 定义变量名NODE_ENV(固定), BASE_URL(固定), 自定义变量名VUE_APP_开头(规定)
// key名必须一致, 写代码一套代码.key名, 会自动匹配环境变量值
// 4. yarn serve启动项目, .env.development内变量挂在到process.env属性上
// yarn build打包项目, .env.production内变量挂在到process.env属性上
if (process.env.NODE_ENV === 'production') {
  console.log = function () {} // 覆盖所有打印语句
  console.warning = function () {}
  console.dir = function () {}
  console.error = function () {}
}
***.env.production
NODE_ENV = production
VUE_APP_NUM = pro
vant封装toast
// 基于vant进行二次封装 / 你可以自己封装一个.vue文件组件(弹窗)
// 封装通知的"方法"
// import { Notify } from 'vant'
// export default Notify

import { Toast } from 'vant'
// 外面逻辑页面传入的字段, 我用自定义函数解构赋值形参中转接收
// 内部如何使用和传值, 在这个函数体里自己决定
export default ({ type, message }) => {
  if (type === 'danger') {
    type = 'fail' // Toast失败图标类型叫fail才行
  }
  Toast({
    type,
    message
  })
}
使用:
import { Notify } from 'vant'
Notify({ type: 'danger', message: '用户身份过期,请重新登录' })
lodash中throttle使用
npm install --save lodash
法一:
<h3 @mouseenter="changeIndex(index)">
                  <a :data-categoryName="c1.categoryName" :data-category1Id="c1.categoryId" href="javascript:;">{{ c1.categoryName }}</a>
</h3>

methods: {
    //鼠标进入修改响应式数据currentIndex属性
    //throttle回调函数别用箭头函数,可能出现上下文this问题
    changeIndex: throttle(function (index) {
      //index:鼠标移上某一个一级分类的元素的索引值
      //正常情况(用户慢慢的操作):鼠标进入,每一个一级分类h3,都会触发鼠标进入事件
      //非正常情况(用户操作很快):本身全部的一级分类都应该触发鼠标进入事件,但是经过测试,只有部分h3触发了
      //就是由于用户行为过快,导致浏览器反应不过来。如果当前回调函数中有一些大量业务,有可能出现卡顿现象。
      this.currentIndex = index
    }, 20),
}

法二:
<li class="cart-list-con5">
            <a href="javascript:void(0)" class="mins" @click="handler('minus', -1, cart)">-</a>
            <input autocomplete="off" type="text" minnum="1" class="itxt" :value="cart.skuNum" @change="handler('change', $event.target.value * 1, cart)" />
            <a href="javascript:void(0)" class="plus" @click="handler('add', 1, cart)">+</a>
</li>
          
//修改某一个产品的个数【节流】
    handler: throttle(async function (type, disNum, cart) {
      //type:为了区分这三个元素
      // disNum形参:+变化量(1)-变化量(-1) input最终的个数(并不是变化量)
      //cart:哪一个产品【身上有id】
      //向服务器发请求,修改数量
      switch (type) {
        //加号
        case 'add':
          disNum = 1
          break
        case 'minus':
          //判断产品的个数大于1,才可以传递给服务器-1
          //如果出现产品的个数小于等于1,传递给服务器个数o(原封不动)
          disNum = cart.skuNum > 1 ? -1 : 0
          break
        case 'change':
          //用户输入进来的最终量,非法的(带有汉字|负数),带给服务器数字0
          if (isNaN(disNum) || disNum < 1) {
            disNum = 0
          } else {
            //属于正常情况(小数:取整),带给服务器变化的量 用户输入进来的 -产品的起始个数
            disNum = parseInt(disNum) - cart.skuNum
          }
          break
      }
      //派发action
      try {
        //代表的是修改成功
        await this.$store.dispatch('addOrUpdateShopCart', { skuId: cart.skuId, skuNum: disNum })
        //再一次获取服务器最新的数据进行展示
        this.getData()
      } catch (error) {}
    }, 2000),
    //删除某一个产品的操作
    async deleteCartById(cart) {
      try {
        //如果删除成功再次发请求获取新的数据进行展示
        await this.$store.dispatch('deleteCartListBySkuId', cart.skuId)
        this.getData()
      } catch (error) {
        alert(error.message)
      }
    },
nprogress使用
npm i nprogress -S
基本上都是在对axios进行二次封装、前置守卫路由或者封装成工具函数的.js文件中用到
import nprogress from 'nprogress'
//引入进度条的样式
import 'nprogress/nprogress.css'
//start:进度条开始 done:进度条结束

//进度条开始动
  nprogress.start()
//进度条结束
  nprogress.done()
法一:
// 显示全屏loading
export function showFullLoading(){
  nprogress.start()
}

// 隐藏全屏loading
export function hideFullLoading(){
  nprogress.done()
}
法二:
import router, { asyncRoutes } from './router'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import store from '@/store'
import getPageTitle from '@/utils/get-page-title'

NProgress.configure({ showSpinner: false }) // NProgress Configuration

const whiteList = ['/login', '/404'] // 白名单: 无需登录, 可以跳转查看的路由地址(在路由表里)

// 问题: 为何动态路由添加后, 在动态路由地址上刷新会404?
// 前提1: 刷新时, 所有代码重新执行, 回归初始化
// 前提2: 刷新时, 路由会从/ 跳转到浏览器地址栏所在的路由地址 (走一次路由守卫代码)
// 动态的还未添加, 所以404了

// 问题: 右上角退出登录+重新登录, 进入到首页时, 网页刷新不? (不刷新)
// 网页本身是不刷新的, 完全依赖路由业务场景的切换 (单页面应用好处: 用户的体验更好, 切换业务场景更快)
// 内存里路由表, 之前添加的筛选后路由规则对象还在不? (在)
// 问题2: 为何重新登录, 路由定义重复了?
// 退出登录的时候, 把token和用户信息清除了
// 登录的时候, 先获取到token保存到vuex和本地, 然后才是跳转路由, 才执行路由守卫(所以判断token有值)
// 但是用户信息没有, 重新请求, 再添加一遍筛选后的路由对象, 所以导致了路由重复

// 解决: 退出登录的时候, 让路由也回归初始化

// 问题: 什么是路由(导航)守卫?
// 答案: 当路由发生跳转的时候, 会触发一个钩子"函数", 在函数中可以通过跳转或取消或强制切换跳转地址来守卫导航
// 路由守卫里必须要有一个next()调用, 出口, 让路由页面跳转
router.beforeEach(async(to, from, next) => {
  NProgress.start()
  const token = store.getters.token
  // 登录了->不能去登录页
  // 非登录->只能去登录页
  if (token) { // 登陆了
    if (to.path === '/login') { // 去登录页
      // 中断要跳转/login这次导航, 重新跳转到/(首页)
      next('/')
      NProgress.done()
    } else { // 去别的页面
      next() // 如果手动让cookie里token改错误, 刷新以后, vuex才会从本地取出错误token
      // 刷新时, 路由守卫会从 / 跳转到地址栏里路由地址, 所以先让页面跳转进去
      // 执行下面请求会401, 被动退出时, 才能拿到跳转后的路由地址(未遂地址给登录页面, 否则next在下面, 未遂地址一直是/)
      if (!store.getters.name) {
        await store.dispatch('user/getUserInfoActions')
        // const menus = await store.dispatch('user/getUserInfoActions')
        // 用menus权限点英文字符串, 和路由规则对象name匹配
        // 把所有准备好的8个路由规则对象, 取出, 看看名字和menus里是否匹配, 匹配就证明
        // 此登录的用户有这个页面的访问权限, 让filter收集此路由规则对象到新数组里
        // const filterList = asyncRoutes.filter(routeObj => {
        //   const routeName = routeObj.children[0].name.toLowerCase()
        //   return menus.includes(routeName)
        // })

        // filterList.push({ path: '*', redirect: '/404', hidden: true })

        // 始终都动态添加先8个路由规则对象
        // 知识点: 路由切换匹配的路由规则对象数组存在于内存中的
        // new Router时, 有一些初始的路由规则对象
        // addRoutes, 会给路由表, 再额外的增加一个规则对象
        // 现象: 路由规则对象添加成功, 但是左侧的导航不见了
        const filterList = asyncRoutes
        router.addRoutes(filterList)

        // 给vuex也同步一份
        store.commit('permission/setRoutes', filterList)

        // 路由再跳转一次, 因为上面next() 会导致白屏(因为放行时, 动态路由还没有加入到内存中路由表里)
        // 添加完, 立刻再跳转一次
        next({
          path: to.path,
          replace: true // 不让回退 类似于this.$router.replace() 防止进入刚才的白屏
        })
      }
    }
  } else { // 没有登录
    if (whiteList.includes(to.path)) { // 要去的路由地址字符串, 是否在白名单数组里出现过, 出现过就放行
      next()
    } else { // 去别的页面(内部项目, 不登录别的页面不能去)
      next('/login')
      NProgress.done()
    }
  }
})
// 验证: 把本地cookie里token手动删除掉, 刷新, 看看是否走最后一个else内
router.afterEach((to, from) => {
  // 正常next()放行了跳转了, 才会走后置守卫, 关闭正常流程进度条
  //动态改变title
  document.title = getPageTitle(to.meta.title)
  NProgress.done()
})
法三:
//对于axios进行二次封装
import axios from 'axios'
import nprogress from 'nprogress'
//引入进度条的样式
import 'nprogress/nprogress.css'
//start:进度条开始 done:进度条结束

//1:利用axios对象的方法create,去创建一个axios实例
//2:requests就是axios,只不过稍做配置一下
const requests = axios.create({
  //配置对象
  //基础路径,发请求URL携带api【发现:真实服务器接口都携带/api】
  baseURL: '/mock',
  //超时的设置
  timeout: 3000
})
//请求拦截器:将来项目中【N个请求】,只要发请求,会触发请求拦截器!!!
requests.interceptors.request.use(config => {
  //请求拦截器:请求头【header】,请求头能否给服务器携带参数
  //请求拦截器:其实项目中还有一个重要的作用,给服务器携带请求们的公共的参数
  //config:配置对象,对象里面有一个属性很重要,headers请求头

  //进度条开始动
  nprogress.start()
  return config
})
//响应拦截器:请求数据返回会执行
requests.interceptors.response.use(
  res => {
    //res:实质就是项目中发请求、服务器返回的数据

    //进度条结束
    nprogress.done()
    return res.data
  },
  err => {
    //温馨提示:某一天发请求,请求失败,请求失败的信息打印出来
    //终止Promise链
    return Promise.reject(new Error('failed'))
  }
)

//最后需要暴露:暴露的是添加新的功能的axios,即为requests
export default requests
dayjs使用
法一:
import dayjs from 'dayjs'
<el-table-column prop="timeOfEntry" label="入职时间" :formatter="timeFormatter" />
// 时间格式化
    // 后台返回的时间格式不一定是什么?(后端没有做数据的验证, 录入新员工不同的同学, 录入的时间格式不同)
    timeFormatter(row) {
      return dayjs(row.timeOfEntry).format('YYYY-MM-DD')
},
法二:
// 封装专门处理时间的 方法
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime' // 到指定时间需要的插件
import 'dayjs/locale/zh' // 集成中文
// 一个/和两个**就能打出多行注释
// JSDOC注释, 文档注释
/**
 * .....多久之前
 * @param {*} 之前的时间
 * @returns 系统时间到之前指定时间的距离值
 */
export const timeAgo = (targetTime) => {
  // 格式化时间
  dayjs.extend(relativeTime)
  dayjs.locale('zh')
  var a = dayjs()
  var b = dayjs(targetTime)
  return a.to(b) // 返回多久之前...
}

export const formatDate = (dateObj) => {
  return dayjs(dateObj).format('YYYY-MM-DD')
}
vue2实现上传本地照片
***npm i cos-js-sdk-v5 --save
	"core-js": "3.6.5",
    "cos-js-sdk-v5": "^1.3.5",

<template>
  <div class="user-info">
    <!-- 个人信息 -->
    <el-form label-width="220px">
      <!-- 工号 入职时间 -->
      <el-row class="inline-info">
        <el-col :span="12">
          <el-form-item label="工号">
            <el-input v-model="userInfo.workNumber" class="inputW" />
          </el-form-item>
        </el-col>

        <el-col :span="12">
          <el-form-item label="入职时间">
            <!--
              数据 "2018-01-01" -> 影响视图显示
              视图选择 -> 默认绑定日期对象 -> v-model变量

              type="date" (选择年-月-日) 控制选择日期格式 (组件渲染内容)
              value-format 选择的值绑定格式(默认不写, v-model绑定的是日期对象)

             -->
            <el-date-picker
              v-model="userInfo.timeOfEntry"
              style="width: 300px"
              type="date"
              class="inputW"
            />
          </el-form-item>
        </el-col>
      </el-row>
      <!-- 姓名 部门 -->
      <el-row class="inline-info">
        <el-col :span="12">
          <el-form-item label="姓名">
            <el-input v-model="userInfo.username" class="inputW" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="部门">
            <el-input v-model="userInfo.departmentName" class="inputW" readonly />
          </el-form-item>
        </el-col>
      </el-row>
      <!--手机 聘用形式  -->
      <el-row class="inline-info">
        <el-col :span="12">
          <el-form-item label="手机">
            <el-input v-model="userInfo.mobile" style="width: 300px" readonly />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="聘用形式">
            <el-select v-model="userInfo.formOfEmployment" class="inputW">
              <el-option
                v-for="item in EmployeeEnum.hireType"
                :key="item.id"
                :label="item.value"
                :value="item.id"
              />
            </el-select>
          </el-form-item>
        </el-col>
      </el-row>
      <!-- 员工照片 -->
      <el-row class="inline-info">
        <el-col :span="12">
          <el-form-item label="员工头像">
            <!-- 放置上传图片 -->
            <upload-img ref="uploadImg" />

          </el-form-item>
        </el-col>
      </el-row>
      <!-- 保存个人信息 -->
      <el-row class="inline-info" type="flex" justify="center">
        <el-col :span="12">
          <el-button type="primary" @click="saveUser">保存更新</el-button>
          <el-button @click="$router.back()">返回</el-button>
        </el-col>
      </el-row>
    </el-form>
  </div>
</template>

<script>
import { getUserPhotoAPI, updateEmployeesAPI } from '@/api'
import EmployeeEnum from '@/api/constant'
export default {
  name: 'UserInfo',
  data() {
    return {
      userInfo: {}, // 个人信息-对象(提前声明属性为了见名知意)
      EmployeeEnum
      // 知识点: v-model="userInfo.workNumber"
      // 当输入框有值的时候
      // 如果对象里有这个属性, 则赋值
      // 如果对象里无这个属性, 则会添加属性并赋值
    }
  },
  created() {
    // 请求-个人信息
    this.getUserInfoFn()
  },
  methods: {
    async getUserInfoFn() {
      const res = await getUserPhotoAPI(this.$route.query.id)
      this.userInfo = res.data
      this.$refs.uploadImg.imageUrl = res.data.staffPhoto
      // 额外加入一个聘用形式
      // 问题: 下面这样写, 为何点击页面下拉菜单, 标签里显示的值不变, vue里数据名里值变了
      // 问题: 视图 -> 数据(v), 但是数据 -> 响应没有更新给视图
      // Vue框架原理: 响应式原理
      // Vue内部会检测data里每个变量(如果变量本身改变了->上面那句话, 响应式更新视图所有)
      // 检测userInfo里每个属性(检测到变化, 会更新数据+视图)
      // 上面数据劫持已经绑定完毕

      // 走到这句话的时候, 数据->视图 (但是没有绑定数据劫持)
      // 给"对象后续添加一个属性"的时候, "还想双向绑定好用" 不会应该对象本身的响应式触发
      // this.userInfo.formOfEmployment = parseInt(this.$route.query.form)
      // 解决: 如果你要后续给对象添加属性
      // $set() Vue内部提供的一个专门添加数组/对象某个值的(并且额外添加数据劫持)
      // 参数1: 数组/对象 目标
      // 参数2: 下标/属性名
      // 参数3: 值
      this.$set(this.userInfo, 'formOfEmployment', parseInt(this.$route.query.form))
    },

    // 保存更新按钮->点击事件
    async saveUser() {
      // 把头像地址保存到userInfo里一起带给后台
      this.userInfo.staffPhoto = this.$refs.uploadImg.imageUrl

      const res = await updateEmployeesAPI(this.userInfo)
      this.$message.success(res.message)
      this.$router.back()
    }
  }
}
</script>

<style lang="scss" scoped></style>
***组件中:
<!-- 放置上传图片 -->
<upload-img ref="uploadImg" />

this.$refs.uploadImg.imageUrl = res.data.staffPhoto
uuid使用
****uuid.js
//利用uuid生成未登录用户临时标识符
import { v4 as uuidv4 } from 'uuid'
//封装函数:只能生成一次用户临时身份
export const getUUID = () => {
  let uuid_token = localStorage.getItem('UUIDTOKEN')
  //如果没有
  if (!uuid_token) {
    //生成一个随机的临时身份
    uuid_token = uuidv4()
    //本地存储一次
    localStorage.setItem('UUIDTOKEN', uuid_token)
  }
  return uuid_token
}

***store.js
//封装游客身份模块uuid  生成一个随机字符串(不能在变了)
import { getUUID } from '@/utils/uuid_token'
const state = {
  //游客临时身份
  uuid_token: getUUID()
}
VueRouter中重写push和replace方法
在vue中如果我们使用编程是跳转路由,然后跳转的还是同一个路由页面,那么控制台会出现报错
//先把VueRouter原型对象的push,先保存一份
let originPush = VueRouter.prototype.push
let originReplace = VueRouter.prototype.replace
//重写push|replace
//第一个参数:告诉原来push方法,你往哪里跳转(传递哪些参数)
VueRouter.prototype.push = function (location, resolve, reject) {
  if (resolve && reject) {
    //call||apply区别:
    // 相同点,都可以调用函数一次,都可以篡改函数的上下文一次
    //不同点: call与apply传递参数: call传递参数用逗号隔开,apply方法执行,传递数组
    originPush.call(this, location, resolve, reject)
  } else {
    originPush.call(
      this,
      location,
      () => {},
      () => {}
    )
  }
}
VueRouter.prototype.replace = function (location, resolve, reject) {
  if (resolve && reject) {
    //call||apply区别:
    // 相同点,都可以调用函数一次,都可以篡改函数的上下文一次
    //不同点: call与apply传递参数: call传递参数用逗号隔开,apply方法执行,传递数组
    originReplace.call(this, location, resolve, reject)
  } else {
    originReplace.call(
      this,
      location,
      () => {},
      () => {}
    )
  }
}
全局前置守卫
import { router, addRoutes } from '@/router'
import { getToken } from '@/composables/auth'
import { toast, showFullLoading, hideFullLoading } from '@/composables/util'
import store from './store'
// 全局前置守卫
let hasGetInfo = false
router.beforeEach(async (to, from, next) => {
  //显示loading
  showFullLoading()
  const token = getToken()
  // 没有登录,强制跳转回登录页
  if (!token && to.path != '/login') {
    toast('请先登录', 'error')
    return next({ path: '/login' })
  }
  // 防止重复登录
  if (token && to.path == '/login') {
    toast('请勿重复登录', 'error')
    return next({ path: from.path ? from.path : '/' })
  }
  // 如果用户登录了,自动获取用户信息,并存储在vuex当中
  let hasNewRoutes = false
  if (token && !hasGetInfo) {
    let { menus } = await store.dispatch('getInfo')
    hasGetInfo = true
    //动态添加路由
    hasNewRoutes = addRoutes(menus)
  }
  // 设置页面标题
  let title = (to.meta.title ? to.meta.title : '') + '-帝莎编程商城后台'
  document.title = title

  hasNewRoutes ? next(to.fullPath) : next()
})

// 全局后置守卫
router.afterEach((to, from) => hideFullLoading())
************************************************************************************************************
import router, { asyncRoutes } from './router'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import store from '@/store'
import getPageTitle from '@/utils/get-page-title'

NProgress.configure({ showSpinner: false }) // NProgress Configuration

const whiteList = ['/login', '/404'] // 白名单: 无需登录, 可以跳转查看的路由地址(在路由表里)

// 问题: 为何动态路由添加后, 在动态路由地址上刷新会404?
// 前提1: 刷新时, 所有代码重新执行, 回归初始化
// 前提2: 刷新时, 路由会从/ 跳转到浏览器地址栏所在的路由地址 (走一次路由守卫代码)
// 动态的还未添加, 所以404了

// 问题: 右上角退出登录+重新登录, 进入到首页时, 网页刷新不? (不刷新)
// 网页本身是不刷新的, 完全依赖路由业务场景的切换 (单页面应用好处: 用户的体验更好, 切换业务场景更快)
// 内存里路由表, 之前添加的筛选后路由规则对象还在不? (在)
// 问题2: 为何重新登录, 路由定义重复了?
// 退出登录的时候, 把token和用户信息清除了
// 登录的时候, 先获取到token保存到vuex和本地, 然后才是跳转路由, 才执行路由守卫(所以判断token有值)
// 但是用户信息没有, 重新请求, 再添加一遍筛选后的路由对象, 所以导致了路由重复

// 解决: 退出登录的时候, 让路由也回归初始化

// 问题: 什么是路由(导航)守卫?
// 答案: 当路由发生跳转的时候, 会触发一个钩子"函数", 在函数中可以通过跳转或取消或强制切换跳转地址来守卫导航
// 路由守卫里必须要有一个next()调用, 出口, 让路由页面跳转
router.beforeEach(async(to, from, next) => {
  NProgress.start()
  const token = store.getters.token
  // 登录了->不能去登录页
  // 非登录->只能去登录页
  if (token) { // 登陆了
    if (to.path === '/login') { // 去登录页
      // 中断要跳转/login这次导航, 重新跳转到/(首页)
      next('/')
      NProgress.done()
    } else { // 去别的页面
      next() // 如果手动让cookie里token改错误, 刷新以后, vuex才会从本地取出错误token
      // 刷新时, 路由守卫会从 / 跳转到地址栏里路由地址, 所以先让页面跳转进去
      // 执行下面请求会401, 被动退出时, 才能拿到跳转后的路由地址(未遂地址给登录页面, 否则next在下面, 未遂地址一直是/)
      if (!store.getters.name) {
        await store.dispatch('user/getUserInfoActions')
        // const menus = await store.dispatch('user/getUserInfoActions')
        // 用menus权限点英文字符串, 和路由规则对象name匹配
        // 把所有准备好的8个路由规则对象, 取出, 看看名字和menus里是否匹配, 匹配就证明
        // 此登录的用户有这个页面的访问权限, 让filter收集此路由规则对象到新数组里
        // const filterList = asyncRoutes.filter(routeObj => {
        //   const routeName = routeObj.children[0].name.toLowerCase()
        //   return menus.includes(routeName)
        // })

        // filterList.push({ path: '*', redirect: '/404', hidden: true })

        // 始终都动态添加先8个路由规则对象
        // 知识点: 路由切换匹配的路由规则对象数组存在于内存中的
        // new Router时, 有一些初始的路由规则对象
        // addRoutes, 会给路由表, 再额外的增加一个规则对象
        // 现象: 路由规则对象添加成功, 但是左侧的导航不见了
        const filterList = asyncRoutes
        router.addRoutes(filterList)

        // 给vuex也同步一份
        store.commit('permission/setRoutes', filterList)

        // 路由再跳转一次, 因为上面next() 会导致白屏(因为放行时, 动态路由还没有加入到内存中路由表里)
        // 添加完, 立刻再跳转一次
        next({
          path: to.path,
          replace: true // 不让回退 类似于this.$router.replace() 防止进入刚才的白屏
        })
      }
    }
  } else { // 没有登录
    if (whiteList.includes(to.path)) { // 要去的路由地址字符串, 是否在白名单数组里出现过, 出现过就放行
      next()
    } else { // 去别的页面(内部项目, 不登录别的页面不能去)
      next('/login')
      NProgress.done()
    }
  }
})
// 验证: 把本地cookie里token手动删除掉, 刷新, 看看是否走最后一个else内
router.afterEach((to, from) => {
  // 正常next()放行了跳转了, 才会走后置守卫, 关闭正常流程进度条
  //动态改变title
  document.title = getPageTitle(to.meta.title)
  NProgress.done()
})
************************************************************************************************************
//配置路由的地方
import Vue from 'vue'
import VueRouter from 'vue-router'
//使用插件
Vue.use(VueRouter)
import routes from './routes'
//引入仓库
import store from '@/store'
//先把VueRouter原型对象的push,先保存一份
let originPush = VueRouter.prototype.push
let originReplace = VueRouter.prototype.replace
//重写push|replace
//第一个参数:告诉原来push方法,你往哪里跳转(传递哪些参数)
VueRouter.prototype.push = function (location, resolve, reject) {
  if (resolve && reject) {
    //call||apply区别:
    // 相同点,都可以调用函数一次,都可以篡改函数的上下文一次
    //不同点: call与apply传递参数: call传递参数用逗号隔开,apply方法执行,传递数组
    originPush.call(this, location, resolve, reject)
  } else {
    originPush.call(
      this,
      location,
      () => {},
      () => {}
    )
  }
}
VueRouter.prototype.replace = function (location, resolve, reject) {
  if (resolve && reject) {
    //call||apply区别:
    // 相同点,都可以调用函数一次,都可以篡改函数的上下文一次
    //不同点: call与apply传递参数: call传递参数用逗号隔开,apply方法执行,传递数组
    originReplace.call(this, location, resolve, reject)
  } else {
    originReplace.call(
      this,
      location,
      () => {},
      () => {}
    )
  }
}
//配置路由
let router = new VueRouter({
  //配置路由
  //第一:路径的前面需要有/(不是二级路由)
  //路径中单词都是小写的
  // component右侧v别给我加单引号【字符串:组件是对象(VueComponent类的实例)】
  routes,
  //滚动行为
  scrollBehavior(to, from, savedPosition) {
    //y代表滚动条在最上方
    return { y: 0 }
  }
})
//全局守卫:前置守卫(在路由跳转之间进行判断)
//全局守卫:只要项目中有任何路由变化,全局守卫都会进行拦截【符合条件走你,不符合条件不能访问】
//全局守卫:全局前置守卫【访问之前进行触发】
router.beforeEach(async (to, from, next) => {
  //to:可以获取到你要跳转到哪个路由信息
  //from:可以获取到你从哪个路由而来的信息
  //next:放行函数  next()放行
  //第一种:next(),放行函数,全部放行!!!
  //第二种:next(path),守卫指定放行到那个路由去
  //token
  //用户登录了,才会有token,未登录一定不会有token
  let hasToken = store.state.user.token
  //用户信息
  let hasName = store.state.user.userInfo.name
  //用户登录
  if (hasToken) {
    //用户登录了,不能去login
    if (to.path == '/login') {
      next('/home')
    } else {
      //用户登陆了,而且还有用户信息【去的并非是login】
      //登陆,去的不是login 去的是【home |search|detail|shopcart】
      //如果用户名已有
      if (hasName) {
        next()
      } else {
        //用户登陆了,但是没有用户信息 派发action让仓库存储用户信息在跳转
        try {
          //发请求获取用户信息以后在放行
          await store.dispatch('getUserInfo')
          next()
        } catch (error) {
          //用户没有信息,还携带token发请求获取用户信息【失败】
          //token【*****失效了】
          //token失效:本地清空数据、服务器的token通知服务器清除
          await store.dispatch('userLogout')
          //回到登录页,重新获取一个新的学生证
          next('/login')
        }
      }
    }
  } else {
    //用户未登录||目前的判断都是放行.将来这里会'回手掏'增加一些判断
    //用户未登录:不能进入/trade、/pay、/paysuccess、/center、/center/myorder  /center/grouporder
    let toPath = to.path
    //要去的路由存在
    if (toPath.indexOf('/trade') != -1 || toPath.indexOf('/pay') != -1 || toPath.indexOf('/center') != -1) {
      //把未登录的时候想去而没有去成的路由地址,存储于地址栏中【路由】
      next('/login?redirect=' + toPath)
    } else {
      next()
    }
  }
})
export default router
************************************************************************************************************
import { createRouter, createWebHashHistory } from 'vue-router'
import store from '@/store'
import { h } from 'vue'

const Layout = () => import('@/views/Layout')
const Home = () => import('@/views/home')
const TopCategory = () => import('@/views/category/index')
const SubCategory = () => import('@/views/category/sub')
const Goods = () => import('@/views/goods/index')
const Cart = () => import('@/views/cart/index')

const Login = () => import('@/views/login/index')
const LoginCallback = () => import('@/views/login/callback')

const Checkout = () => import('@/views/member/pay/checkout')
const Pay = () => import('@/views/member/pay/index')
const PayResult = () => import('@/views/member/pay/result')

const MemberLayout = () => import('@/views/member/Layout')
const MemberHome = () => import('@/views/member/home')
const MemberOrder = () => import('@/views/member/order')
const MemberOrderDetail = () => import('@/views/member/order/detail')

// 路由规则
const routes = [
  // 一级路由布局容器
  {
    path: '/',
    component: Layout,
    children: [
      { path: '/', component: Home },
      { path: '/category/:id', component: TopCategory },
      { path: '/category/sub/:id', component: SubCategory },
      { path: '/product/:id', component: Goods },
      { path: '/cart', component: Cart },
      { path: '/member/checkout', component: Checkout },
      { path: '/member/pay', component: Pay },
      { path: '/pay/callback', component: PayResult },
      {
        path: '/member',
        component: MemberLayout,
        children: [
          { path: '/member', component: MemberHome },
          // { path: '/member/order', component: MemberOrder },
          // { path: '/member/order/:id', component: MemberOrderDetail }
          {
            path: '/member/order',
            // 创建一个RouterView容器形成嵌套关系
            component: { render: () => h(<RouterView />) },
            children: [
              { path: '', component: MemberOrder },
              { path: ':id', component: MemberOrderDetail }
            ]
          }
        ]
      }
    ]
  },
  { path: '/login', component: Login },
  { path: '/login/callback', component: LoginCallback }
]

// vue2.0 new VueRouter({}) 创建路由实例
// vue3.0 createRouter({}) 创建路由实例
const router = createRouter({
  // 使用hash的路由模式
  history: createWebHashHistory(),
  routes,
  // 每次切换路由的时候滚动到页面顶部
  scrollBehavior () {
    // vue2.0  x  y  控制
    // vue3.0  left  top 控制
    return { left: 0, top: 0 }
  }
})

// 前置导航守卫
router.beforeEach((to, from, next) => {
  // 需要登录的路由:地址是以 /member 开头
  const { profile } = store.state.user
  if (!profile.token && to.path.startsWith('/member')) {
    return next('/login?redirectUrl=' + encodeURIComponent(to.fullPath))
  }
  next()
})

export default router
************************************************************************************************************
import Vue from 'vue'
import VueRouter from 'vue-router'
import store from '@/store'
Vue.use(VueRouter)

const routes = [
  {
    path: '/register',
    component: () => import('@/views/register')
  },
  {
    path: '/login',
    component: () => import('@/views/login')
  },
  {
    path: '/',
    component: () => import('@/views/layout'),
    redirect: '/home', // 默认显示首页的二级路由
    children: [
      {
        path: 'home',
        component: () => import('@/views/home')
      },
      {
        path: 'user-info', // 这里必须叫user-info, 因为侧边栏导航切换的是它
        component: () => import('@/views/user/userInfo')
      },
      {
        path: 'user-avatar', // 必须用这个值
        component: () => import('@/views/user/changePhoto')
      },
      {
        path: 'user-pwd', // 必须用这个值
        component: () => import('@/views/user/userPwd')
      },
      {
        path: 'art-cate', // 文章分类
        component: () => import('@/views/article/artCate')
      },
      {
        path: 'art-list', // 文章列表
        component: () => import('@/views/article/artList')
      }
    ]
  }
]

const router = new VueRouter({
  routes
})

// 无需验证token的页面
const whiteList = ['/login', '/register']

router.beforeEach((to, from, next) => {
  const token = store.state.token
  if (token) {
    // 如果有token, 证明已登录
    if (!store.state.userInfo.username) {
      // 有token但是没有用户信息, 才去请求用户信息保存到vuex里
      // 调用actions里方法请求数据
      store.dispatch('getUserInfo')
      // 下次切换页面vuex里有用户信息数据就不会重复请求用户信息
    }
    next() // 路由放行
  } else {
    // 如果无token
    // 如果去的是白名单页面, 则放行
    if (whiteList.includes(to.path)) {
      next()
    } else {
      // 如果其他页面请强制拦截并跳转到登录页面
      next('/login')
    }
  }
})
export default router
************************************************************************************************************
import { createRouter, createWebHistory } from "vue-router";
//pinia
import { useUserStore } from '@/store/user'
//首页
import Home from "../views/Home.vue";
const routes = [
  {
    path: "/",
    name: "Home",
    component: Home,
  },
  {
    path: "/course",
    name: "Course",
    component: () =>
      import(/* webpackChunkName: "course" */ "../views/Course.vue"),
  },
  {
    path: "/course-info/:id",
    name: "CourseInfo",
    component: () =>
      import(/* webpackChunkName: "courseInfo" */ "../views/CourseInfo.vue"),
  },
  { 
    path:'/course-play/:courseId/:chapterId',
    name:'course-play',
    component: () =>
      import(/* webpackChunkName: "CoursePlay" */ "../views/CoursePlay.vue"),
  },
  {
    path: "/login",
    name: "Login",
    component: () =>
      import(/* webpackChunkName: "login" */ "../views/Login.vue"),
  },
  {
    path: "/cart",
    name: "Cart",
    component: () =>
      import(/* webpackChunkName: "cart" */ "../views/Cart.vue"),
    beforeEnter:(to, from, next)=>{
      if( useUserStore().userInfo.id ){
        next();
      }else{
        next('/login');
      }
    }
  },
  {
    path: "/confirmOrder",
    name: "ConfirmOrder",
    component: () =>
      import(/* webpackChunkName: "confirmOrder" */ "../views/ConfirmOrder.vue"),
    beforeEnter:(to, from, next)=>{
      if( useUserStore().userInfo.id ){
        next();
      }else{
        next('/login');
      }
    }
  },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;
swiper插件使用
***main.js
npm i swiper
//引入swiper样式
import 'swiper/css/swiper.css'
***组件中:
<template>
  <!-- 轮播图 -->
  <div class="swiper-container" ref="cur">
    <div class="swiper-wrapper">
      <div class="swiper-slide" v-for="carousel in list" :key="carousel.id">
        <img :src="carousel.imgUrl" />
      </div>
    </div>
    <!-- 如果需要分页器 -->
    <div class="swiper-pagination"></div>

    <!-- 如果需要导航按钮 -->
    <div class="swiper-button-prev"></div>
    <div class="swiper-button-next"></div>
  </div>
</template>

<script>
//引入Swiper
import Swiper from 'swiper'
export default {
  name: 'Carousel',
  props: ['list'],
  watch: {
    list: {
      //立即监听:不管你数据有没有变化,我上来立即监听一次
      //为什么watch监听不到list:因为这个数据从来没有发生变化《数据是父亲给的,父亲给的时候就是一个对象,对象里面该有的数据都是有的)
      immediate: true,
      handler() {
        //只能监听到数据已经有了,但是v-for动态渲染结构我们还是没有办法确定的,因此还是需要用nextTick
        this.$nextTick(() => {
          var mySwiper = new Swiper(this.$refs.cur, {
            loop: true,
            autoplay: true,
            //如果需要分页器
            pagination: {
              el: '.swiper-pagination',
              //点击小球的时候也切换图片
              clickable: true
            },
            //如果需要前进后退按钮
            navigation: {
              nextEl: '.swiper-button-next',
              prevEl: '.swiper-button-prev'
            }
          })
        })
      }
    }
  }
}
</script>

<style scoped></style>

支付代码、QRCode插件使用、ElementUI注册组件
***main.js
import { Button, MessageBox,Message } from 'element-ui'
//注册全局组件
Vue.component(Button.name, Button)
//ElementUI注册组件的时候,还有一种写法,挂在原型上
Vue.prototype.$msgbox = MessageBox
Vue.prototype.$alert = MessageBox.alert
Vue.prototype.$confirm = MessageBox.confirm
Vue.prototype.$message = Message

***$alert、$msgbox使用:
npm install --save qrcode
import QRCode from 'qrcode'
//支付弹出框函数
    async open() {
      //生成二维码地址
      let url = await QRCode.toDataURL(this.payInfo.codeUrl)
      this.$alert(`<img src=${url} />`, '请您微信支付', {
        //是否将 message属性作为HTML片段处理
        dangerouslyUseHTMLString: true,
        //居中
        center: true,
        //显示取消按钮
        showCancelButton: true,
        //取消按钮的文本内容
        cancelButtonText: '支付遇见问题',
        //确定按钮的文本
        confirmButtonText: '已支付成功',
        //右上角的叉子
        showClose: true,
        //关闭弹出框的配置值
        beforeClose: (type, instance, done) => {
          //type:区分取消|确定按钮
          //instance:当前组件实例
          //done:关闭弹出框的方法
          if (type == 'cancel') {
            // alert('请联系管理员')
            //清除定时器
            clearInterval(this.timer)
            this.timer = null
            //关闭弹出框
            done()
          } else {
            //判断是否真的支付了
            //开发人员后门
            if (this.code == 200) {
            	clearInterval(this.timer)
            	this.timer = null
            	done()
            	//跳转到下一路由
            	this.$router.push('/paysuccess')
            }
          }
        }
      }).catch(() => {}) //没有进行错误捕获,就会提示Uncaught (in promise) cancel错误。
      //需要知道支付成功与否   每隔1s就判断支付成功没
      //支付成功,路由的跳转,如果支付失败,提示信息
      //定时器没有,开启一个新的定时器
      if (!this.timer) {
        // console.log(this.timer)  null
        // console.log(Boolean(this.timer))   false
        // console.log(Boolean(!this.timer))   true
        this.timer = setInterval(async () => {
          //发请求获取用户支付状态
          let result = await this.$API.reqPayStatus(this.orderId)
          if (result.code == 200) {
            //第一步:停止定时器
            clearInterval(this.timer)
            this.timer = null
            //保存支付成功返回的code
            this.code = result.code
            //关闭弹出框
            this.$msgbox.close()
            //跳转到下一路由
            this.$router.push('/paysuccess')
          }
        }, 1000)
      }
    }
***$confirm使用:
// 点击退出登录的回调
    logout() {
      this.$confirm('要退出登录吗?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(async() => {
          let res = await this.$request("/logout",{ timestamp: getTimeStamp() });
          // console.log(res);
          if (res.data.code != 200) {
            this.$message("退出登录失败, 请稍后重试!");
            return;
          }
          // 清空data和localstorage中的数据,以及cookie
          // window.localStorage.setItem("userInfo", "");
          // this.clearAllCookie();
          // 删除localstoarge的userId
          window.localStorage.removeItem("userId");
          //   在vuex中更新登录状态
          this.$store.commit("updataLoginState", false);
          this.$message.success("退出成功!");
          this.isCurrentUser = false;
        }).catch(() => {
          this.$message({
            type: 'info',
            message: '已取消登录'
          });          
        });
    },
***$message使用:
this.$message.error("请先进行登录操作");
b u s 、 bus、 busAPI全局绑定
***main.js
//统一接口api文件夹里面全部请求函数
//统一引入
import * as API from '@/api'

new Vue({
  render: h => h(App),
  //全局时间总线$bus配置
  // beforeCreate 函数就是 Vue 实例被创建出来之前,会执行它。在 beforeCreate 生命周期函数执行的时候,Vue实例中的 data 和 methods 中的数据都还没有被初始化。
  beforeCreate() {
    //向外共享Vue的实例对象  固定写法
    Vue.prototype.$bus = this
    // console.log(Vue.prototype.$bus)
    Vue.prototype.$API = API
    // console.log(Vue.prototype.$API)里面是reqAddOrUpdateShopCart,reqAddressInfo等接口
  },
  //注册路由:底下的写法KV一致省略V[router小写]
  //注册路由信息:当这里书写router的时候,组件身上都拥有$route,$router属性
  router,
  //注册仓库:组件实例的身上会多一个属性$store属性
  store
}).$mount('#app')
***$bus使用场景:
//通知兄弟组件:当前的索引值为几
      this.$bus.$emit('getIndex', this.currentIndex)
//全局事件总线:获取兄弟组件传递过来的索引值
    this.$bus.$on('getIndex', index => {
      //修改当前响应式数据
      this.currentIndex = index
    })
***$API使用场景:
//获取我的订单方法
    async getData() {
      //结构出参数
      const { page, limit } = this
      //好处:不用引入reqMyOrderList接口
      let result = await this.$API.reqMyOrderList(page, limit)
      if (result.code == 200) {
        this.myOrder = result.data
      }
},
vee-validate以及validate使用
npm install vee-validate --save
法一:
***main.js
//引入表单检验插件
import '@/plugins/validate'
***validate.js
//vee-validate插件:表单验证区域
import Vue from 'vue'
import VeeValidate from 'vee-validate'
//中文提示信息
import zh_CN from 'vee-validate/dist/locale/zh_CN'
Vue.use(VeeValidate)

//表单验证
VeeValidate.Validator.localize('zh_CN', {
  messages: {
    ...zh_CN.messages,
    is: field => `${field}必须与密码相同` //修改内置规则的message,让确认密码和密码相同
  },
  attributes: {
    //给校验的-field-属性名映射中文名称
    //给每个字段转为中文
    phone: '手机号',
    code: '验证码',
    password: '密码',
    password1: '确认密码',
    agree: '协议'
  }
})
//自定义校验规则
VeeValidate.Validator.extend('agree', {
  validate: value => {
    return value
  },
  getMessage: field => field + '必须同意'
})
***组件中:
<div class="content">
        <label>手机号:</label>
        <!-- has('field') – 当前filed是否有错误(true/false) -->
        <!-- errors.first('field') – 获取关于当前field的第一个错误信息 -->
        <input placeholder="请输入你的手机号" v-model="phone" name="phone" v-validate="{ required: true, regex: /^1\d{10}$/ }" :class="{ invalid: errors.has('phone') }" />
        <span class="error-msg">{{ errors.first('phone') }}</span>
      </div>
      <div class="content">
        <label>验证码:</label>
        <input placeholder="请输入你的验证码" v-model="code" name="code" v-validate="{ required: true, regex: /^\d{6}$/ }" :class="{ invalid: errors.has('code') }" />
        <button style="width: 100px; height: 38px" @click="getCode">获取验证码</button>
        <span class="error-msg">{{ errors.first('code') }}</span>
      </div>
      <div class="content">
        <label>登录密码:</label>
        <input type="password" placeholder="请输入你的密码" v-model="password" name="password" v-validate="{ required: true, regex: /^[0-9A-Za-z]{8,20}$/ }" :class="{ invalid: errors.has('password') }" />格式:8-20位密码,禁止输入符号
        <span class="error-msg">{{ errors.first('password') }}</span>
      </div>
      <div class="content">
        <label>确认密码:</label>
        <input type="password" placeholder="请输入确认密码" v-model="password1" name="password1" v-validate="{ required: true, is: password }" :class="{ invalid: errors.has('password1') }" />
        <span class="error-msg">{{ errors.first('password1') }}</span>
      </div>
      <div class="controls">
        <input type="checkbox" v-model="agree" name="agree" v-validate="{ required: true, agree: true }" :class="{ invalid: errors.has('agree') }" />
        <span>同意协议并注册《尚品汇用户协议》</span>
        <span class="error-msg">{{ errors.first('agree') }}</span>
      </div>
      
<script>
data() {
    return {
      //收集表单数据--手机号
      phone: '',
      //验证码
      code: '',
      //密码
      password: '',
      //确认密码
      password1: '',
      //确认协议
      agree: true
    }
  },
	//注册信息全部合格后才能注册
      const success = await this.$validator.validateAll()
      // console.log(success);//布尔值
      //全部表单验证成功,在向服务器发请求,进行注册
      //只要有一个表单没有成功,不会发请求
</script>

************************************************************************************************************
法二:
***vee-validate-schema.js
import { userAccountCheck } from '@/api/user'

// 给vee-validate提供校验规则函数
export default {
  // 用户名校验
  account (value) {
    if (!value) return '请输入用户名'
    // 规则:字母开头6-20字符之间
    if (!/^[a-zA-Z]\w{5,19}$/.test(value)) return '字母开头且6-20个字符'
    return true
  },
  // 用户校验且校验唯一性
  async accountApi (value) {
    if (!value) return '请输入用户名'
    if (!/^[a-zA-Z]\w{5,19}$/.test(value)) return '字母开头且6-20个字符'
    // 服务器端校验
    const data = await userAccountCheck(value)
    if (data.result.valid) return '用户名已存在'
    return true
  },
  // 密码校验
  password (value) {
    if (!value) return '请输入密码'
    // 规则:密码格式6-24个字符
    if (!/^\w{6,24}$/.test(value)) return '密码6-24个字符'
    return true
  },
  // 密码校验
  rePassword (value, { form }) {
    if (!value) return '请输入密码'
    if (!/^\w{6,24}$/.test(value)) return '密码6-24个字符'
    // form表单数据对象
    if (value !== form.password) return '需要和密码保持一致'
    return true
  },
  mobile (value) {
    if (!value) return '请输入手机号'
    // 规则:1开头 3-9 之间  9个数字
    if (!/^1[3-9]\d{9}$/.test(value)) return '手机号格式不对'
    return true
  },
  code (value) {
    if (!value) return '请输入短信验证码'
    // 规则: 6个数字
    if (!/^\d{6}$/.test(value)) return '短信验证码6个数字'
    return true
  },
  isAgree (value) {
    if (!value) return '请勾选登录协议'
    return true
  }
}
***组件中:
<Form ref="formCom" class="form" :validation-schema="schema" v-slot="{errors}" autocomplete="off">
      <template v-if="!isMsgLogin">
        <div class="form-item">
          <div class="input">
            <i class="iconfont icon-user"></i>
            <Field :class="{error:errors.account}" v-model="form.account" name="account" type="text" placeholder="请输入用户名" />
          </div>
          <div class="error" v-if="errors.account">
            <i class="iconfont icon-warning" />
            {{errors.account}}
          </div>
        </div>
        <div class="form-item">
          <div class="input">
            <i class="iconfont icon-lock"></i>
            <Field :class="{error:errors.password}" v-model="form.password" name="password" type="password" placeholder="请输入密码" />
          </div>
          <div class="error" v-if="errors.password">
            <i class="iconfont icon-warning" />
            {{errors.password}}
          </div>
        </div>
      </template>
      <template v-else>
        <div class="form-item">
          <div class="input">
            <i class="iconfont icon-user"></i>
            <Field :class="{error:errors.mobile}" v-model="form.mobile" name="mobile" type="text" placeholder="请输入手机号" />
          </div>
          <div class="error" v-if="errors.mobile">
            <i class="iconfont icon-warning" />
            {{errors.mobile}}
          </div>
        </div>
        <div class="form-item">
          <div class="input">
            <i class="iconfont icon-code"></i>
            <Field :class="{error:errors.code}" v-model="form.code" name="code" type="text" placeholder="请输入验证码" />
            <span @click="send()" class="code">
              {{time===0?'发送验证码':`${time}秒后发送`}}
            </span>
          </div>
          <div class="error" v-if="errors.code">
            <i class="iconfont icon-warning" />
            {{errors.code}}
          </div>
        </div>
      </template>
      <div class="form-item">
        <div class="agree">
          <Field as="XtxCheckbox" name="isAgree" v-model="form.isAgree" />
          <span>我已同意</span>
          <a href="javascript:;">《隐私条款》</a>
          <span>和</span>
          <a href="javascript:;">《服务条款》</a>
        </div>
        <div class="error" v-if="errors.isAgree">
          <i class="iconfont icon-warning" />
          {{errors.isAgree}}
        </div>
      </div>
      <a @click="login()" href="javascript:;" class="btn">登录</a>
    </Form>

import { Form, Field } from 'vee-validate'
import schema from '@/utils/vee-validate-schema'

// vee-validate 校验基本步骤
    // 1. 导入 Form Field 组件 将 form 和 input 进行替换,需要加上name用来指定将来的校验规则函数的
    // 2. Field 需要进行数据绑定,字段名称最好和后台接口需要的一致
    // 3. 定义Field的name属性指定的校验规则函数,Form的validation-schema接受定义好的校验规则是对象
    // 4. 自定义组件需要校验必须先支持v-model 然后Field使用as指定为组件名称
    const mySchema = {
      // 校验函数规则:返回true就是校验成功,返回一个字符串就是失败,字符串就是错误提示
      account: schema.account,
      password: schema.password,
      mobile: schema.mobile,
      code: schema.code,
      isAgree: schema.isAgree
    }
    
// 1. 发送验证码
    // 1.1 绑定发送验证码按钮点击事件
    // 1.2 校验手机号,如果成功才去发送短信(定义API),请求成功开启60s的倒计时,不能再次点击,倒计时结束恢复
    // 1.3 如果失败,失败的校验样式显示出来
    const send = async () => {
      const valid = mySchema.mobile(form.mobile)
      if (valid === true) {
        // 通过
        if (time.value === 0) {
        // 没有倒计时才可以发送
          await userMobileLoginMsg(form.mobile)
          Message({ type: 'success', text: '发送成功' })
          time.value = 60
          resume()
        }
      } else {
        // 失败,使用vee的错误函数显示错误信息 setFieldError(字段,错误信息)
        formCom.value.setFieldError('mobile', valid)
      }
    }
************************************************************************************************************
法三:
组件中:
<el-form :model="regForm" :rules="regRules" ref="regRef">
        <!-- 用户名 -->
        <el-form-item prop="username">
          <el-input v-model="regForm.username" placeholder="请输入用户名"></el-input>
        </el-form-item>
        <!-- 密码 -->
        <el-form-item prop="password">
          <el-input v-model="regForm.password" type="password" placeholder="请输入密码"></el-input>
        </el-form-item>
        <!-- 确认密码 -->
        <el-form-item prop="repassword">
          <el-input v-model="regForm.repassword" type="password" placeholder="请再次确认密码"></el-input>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" class="btn-reg" @click="regNewUserFn">注册</el-button>
          <el-link type="info" @click="$router.push('/login')">去登录</el-link>
        </el-form-item>
</el-form>

data() {
    const samePwd = (rule, value, callback) => {
      if (value !== this.regForm.password) {
        // 如果验证失败,则调用 回调函数时,指定一个 Error 对象。
        callback(new Error('两次输入的密码不一致!'))
      } else {
        // 如果验证成功,则直接调用 callback 回调函数即可。
        callback()
      }
    }
    return {
      // 注册表单的数据对象
      regForm: {
        username: '',
        password: '',
        repassword: ''
      },
      // 注册表单的验证规则对象
      regRules: {
        username: [
          { required: true, message: '请输入用户名', trigger: 'blur' },
          {
            pattern: /^[a-zA-Z0-9]{1,10}$/,
            message: '用户名必须是1-10的大小写字母数字',
            trigger: 'blur'
          }
        ],
        password: [
          { required: true, message: '请输入密码', trigger: 'blur' },
          {
            pattern: /^\S{6,15}$/,
            message: '密码必须是6-15的非空字符',
            trigger: 'blur'
          }
        ],
        repassword: [
          { required: true, message: '请再次输入密码', trigger: 'blur' },
          { pattern: /^\S{6,15}$/, message: '密码必须是6-15的非空字符', trigger: 'blur' },
          { validator: samePwd, trigger: 'blur' }
        ]
      }
    }
  },
  methods: {
    regNewUserFn() {
      // 进行表单预验证
      this.$refs.regRef.validate(async valid => {
        if (!valid) return false
        // 尝试拿到用户输入的内容
        // console.log(this.regForm)
        const res = await registerAPI(this.regForm)
        //这样写下面就不用.data
        // const { data: res } = await registerAPI(this.regForm)
        // console.log(res)
        // 2. 注册失败,提示用户
        if (res.data.code !== 0) return this.$message.error(res.data.message)
        // 3. 注册成功,提示用户
        this.$message.success(res.data.message)
        // 4. 跳转到登录页面
        this.$router.push('/login')
      })
    }
  }
懒加载插件使用
npm i vue-lazyload -S
//引入图片懒加载插件
import VueLazyload from 'vue-lazyload'
import atm from '@/assets/logo.png'
Vue.use(VueLazyload, {
  // 放入懒加载的图片,就是atm
  loading: atm
})
***组件中:
<img v-lazy="good.defaultImg" />
i18n包使用
***main.js
import ELEMENT from 'element-ui'
// 引入语言对象
import i18n from '@/lang'
Vue.use(ELEMENT, {
  // 配置 ELEMENT 语言转换关系
  // 每个组件都会调用一次
  i18n: (key, value) => {
    // 组件内容处, 使用的相关参数和值↓
    // key: el.pagination.total (好比是要查找语言包的属性路径)
    // value: 对应要传入的值 {total: 10}
    // i18n.t 好比 Vue组件$t
    // key就是去语言包环境找到对应的中文/英文值
    // value就是要传入的值 会替换掉{} 位置, 换成对应值在原地显示
    return i18n.t(key, value)
  }
}) // 只是为了注册elementUI组件(语言切换, 一会儿和Vuei18n集成)

new Vue({
  el: '#app',
  router,
  store,
  i18n,
  render: h => h(App)
})
***组件中:
<template>
  <!--
      trigger 是下拉菜单的触发时机
      @command 自定义事件 (检测菜单项的点击行为)
   -->
  <el-dropdown trigger="click" @command="changeLanguage">
    <!-- 第一个子标签是上来就显示的标签 -->
    <div>
      <svg-icon style="color:#fff;font-size:20px" icon-class="language" />
    </div>
    <!-- 就会出现真正的下拉菜单项 -->
    <el-dropdown-menu slot="dropdown">
      <!-- command 点击时, 传入给@command事件里参数
        $i18n 是Vue.use(Vuei18n)添加给Vue原型的全局属性, 通过它可以拿到i18n里locale环境的英文标识('zh'/'en')
       -->
      <el-dropdown-item command="zh" :disabled="'zh'=== $i18n.locale ">中文</el-dropdown-item>
      <el-dropdown-item command="en" :disabled="'en'=== $i18n.locale ">English</el-dropdown-item>
    </el-dropdown-menu>
  </el-dropdown>
</template>

<script>
export default {
  methods: {
    // 下拉菜单项的点击事件
    // lang的值就是 "zh" "en"
    changeLanguage(lang) {
      this.$i18n.locale = lang // 设置给本地的i18n插件
      this.$message.success('切换多语言成功')
    }
  }
}
</script>

***lang.js
import Vue from 'vue'
import VueI18n from 'vue-i18n'
// ElementUI的中英文语言包引入
// 语言包:对象
// 相同的key(键)名, 对应的对象(值, 不同的语言包, 对应值不同)
import enLocale from 'element-ui/lib/locale/lang/en'
import zhLocale from 'element-ui/lib/locale/lang/zh-CN'

Vue.use(VueI18n)

// 通过选项创建 VueI18n 实例
const i18n = new VueI18n({
  // 隐藏警告
  silentTranslationWarn: true,
  locale: 'zh-CN', // 设置地区
  messages: {
    en: {
      navbar: {
        companyName: 'Jiangsu Chuanzhi podcast Education Technology Co., Ltd',
        name: '{name}'
      },
      sidebar: {
        dashboard: 'Dashboard',
        approvals: 'Approvals',
        departments: 'Departements',
        employees: 'Employees',
        permission: 'Permission',
        attendances: 'Attendances',
        salarys: 'Salarys',
        setting: 'Company-Settings',
        social: 'Social'
      },
      ...enLocale,
      message: {
        hello: 'hello world'
      }
    },
    zh: {
      navbar: {
        companyName: '江苏传智播客教育科技股份有限公司',
        name: '{name}'
      },
      sidebar: {
        dashboard: '首页',
        approvals: '审批',
        departments: '组织架构',
        employees: '员工',
        permission: '权限',
        attendances: '考勤',
        salarys: '工资',
        setting: '公司设置',
        social: '社保'
      },
      ...zhLocale,
      message: {
        hello: '你好, 世界'
      }
    }
  } // 设置地区信息
})

// vuei18n内部的工作原理
// 1. 会给Vue原型上添加$t方法
// 2. 我们自己业务vue文件中, 文字部分都要换成$t方法, 然后在方法中传入要获取的对象的属性值路径字符串
// 3. $t方法内, 会根据locale的值, 去messages里面取出对应环境的语言对象, 然后再拼接本次寻找值对象属性的路径, 找到 对应的值返回到$t函数位置

export default i18n

map使用
1.案例一:取给定数组的某一字段组成新数组
的后台传来的数据 data(json):
[   //data的数据
	{"txt":"09:00-12:00","codId":"1","flgDel":"0","id":1},
	{"txt":"13:00-16:00","codId":"1","flgDel":"0","id":2},
	{"txt":"18:00-20:00","codId":"1","flgDel":"0","id":3}
]
前台使用要为:
['09:00-12:00', '13:00-16:00', '18:00-20:00']
用到map()只需一行。快捷方法出来了学去吧。
let time = data.map(item =>(item.txt))
console.log(time) 
//控制台输出如下
//['09:00-12:00', '13:00-16:00', '18:00-20:00']
2.案例二:取给定数组的某些字段重命名并组成新数组
新的接口传来data(json):
[  //新data数据
{"txt":"拜访","flgDel":"0","id":1},
{"txt":"面试","flgDel":"0","id":2},
{"txt":"其他","flgDel":"0","id":3}
]
前台使用数组结构:
[{ name: '拜访' }, { name: '面试' }, { name: '其他' }]
//这里看到相比于案例一有字段了,还新命名了
//只需一行map()
let resion = data.map(item =>({name: item.txt}))
console.log(resion) 
//控制台输出
//[{ name: '拜访' }, { name: '面试' }, { name: '其他' }]
当然,或许你要的这样⬇ :
[{ name: '拜访',id:'1' }, { name: '面试',id:'2' }, { name: '其他',id:'3'}]
//要两个字段的数据
let resion2 = data.map(item =>({name: item.txt, id: item.id}))
console.log(resion2) 
//控制台输出
//[{ name: '拜访',id:'1' }, { name: '面试',id:'2' }, { name: '其他',id:'3'}]
又或许你想要这样⬇ :
[{ name: '拜访1' }, { name: '面试2' }, { name: '其他3'}]
//要拼接的数据
let resion3 = data.map(item =>({name: item.txt + item.id}))
console.log(resion3) 
//控制台输出
//[{ name: '拜访1' }, { name: '面试2' }, { name: '其他3'}]
深拷贝,浅拷贝

image-20230414190438291

image-20230414190450368

image-20230414190457404

配置动态路由
*********************************************************************法一:
import { createRouter, createWebHashHistory } from 'vue-router'
import Index from '@/pages/index.vue'
import Login from '@/pages/login.vue'
import NotFound from '@/pages/404.vue'
import Admin from '@/layouts/admin.vue'
import GoodList from '@/pages/goods/list.vue'
import CategoryList from '@/pages/category/list.vue'
import UserList from '@/pages/user/list.vue'
import OrderList from '@/pages/order/list.vue'
import CommentList from '@/pages/comment/list.vue'
import ImageList from '@/pages/image/list.vue'
import NoticeList from '@/pages/notice/list.vue'
import SettingBase from '@/pages/setting/base.vue'
import CouponList from '@/pages/coupon/list.vue'
import ManagerList from '@/pages/manager/list.vue'
import AccessList from '@/pages/access/list.vue'
import RoleList from '@/pages/role/list.vue'
import SkusList from '@/pages/skus/list.vue'
import LevelList from '@/pages/level/list.vue'
import SettingBuy from '@/pages/setting/buy.vue'
import SettingShip from '@/pages/setting/ship.vue'
import DistributionIndex from '@/pages/distribution/index.vue'
import DistributionSetting from '@/pages/distribution/setting.vue'
//默认路由,所有用户共享
const routes = [
  {
    path: '/',
    name: 'admin',
    component: Admin
  },
  {
    path: '/login',
    component: Login,
    meta: {
      title: '登录页'
    }
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: NotFound
  }
]
//动态路由
const asyncRoutes = [
  {
    path: '/',
    name: '/',
    component: Index,
    meta: {
      title: '后台首页'
    }
  },
  {
    path: '/goods/list',
    name: '/goods/list',
    component: GoodList,
    meta: {
      title: '商品管理'
    }
  },
  {
    path: '/category/list',
    name: '/category/list',
    component: CategoryList,
    meta: {
      title: '分类列表'
    }
  },
  {
    path: '/user/list',
    name: '/user/list',
    component: UserList,
    meta: {
      title: '用户列表'
    }
  },
  {
    path: '/order/list',
    name: '/order/list',
    component: OrderList,
    meta: {
      title: '订单列表'
    }
  },
  {
    path: '/comment/list',
    name: '/comment/list',
    component: CommentList,
    meta: {
      title: '评价列表'
    }
  },
  {
    path: '/image/list',
    name: '/image/list',
    component: ImageList,
    meta: {
      title: '图库列表'
    }
  },
  {
    path: '/notice/list',
    name: '/notice/list',
    component: NoticeList,
    meta: {
      title: '公告列表'
    }
  },
  {
    path: '/setting/base',
    name: '/setting/base',
    component: SettingBase,
    meta: {
      title: '配置'
    }
  },
  {
    path: '/coupon/list',
    name: '/coupon/list',
    component: CouponList,
    meta: {
      title: '优惠券列表'
    }
  },
  {
    path: '/manager/list',
    name: '/manager/list',
    component: ManagerList,
    meta: {
      title: '管理员管理'
    }
  },
  {
    path: '/access/list',
    name: '/access/list',
    component: AccessList,
    meta: {
      title: '菜单权限管理'
    }
  },
  {
    path: '/role/list',
    name: '/role/list',
    component: RoleList,
    meta: {
      title: '角色管理'
    }
  },
  {
    path: '/skus/list',
    name: '/skus/list',
    component: SkusList,
    meta: {
      title: '规格管理'
    }
  },
  {
    path: '/level/list',
    name: '/level/list',
    component: LevelList,
    meta: {
      title: '会员等级'
    }
  },
  {
    path: '/setting/buy',
    name: '/setting/buy',
    component: SettingBuy,
    meta: {
      title: '支付设置'
    }
  },
  {
    path: '/setting/ship',
    name: '/setting/ship',
    component: SettingShip,
    meta: {
      title: '物流设置'
    }
  },
  {
    path: '/distribution/index',
    name: '/distribution/index',
    component: DistributionIndex,
    meta: {
      title: '分销员管理'
    }
  },
  {
    path: '/distribution/setting',
    name: '/distribution/setting',
    component: DistributionSetting,
    meta: {
      title: '分销设置'
    }
  }
]

export const router = createRouter({
  history: createWebHashHistory(),
  routes
})

// 动态添加路由的方法
export function addRoutes(menus) {
  // 是否有新的路由
  let hasNewRoutes = false
  const findAndAddRoutesByMenus = arr => {
    arr.forEach(e => {
      let item = asyncRoutes.find(o => o.path == e.frontpath)
      if (item && !router.hasRoute(item.path)) {
        //添加到名字叫admin路由的子路由
        router.addRoute('admin', item)
        hasNewRoutes = true
      }
      if (e.child && e.child.length > 0) {
        findAndAddRoutesByMenus(e.child)
      }
    })
  }
  findAndAddRoutesByMenus(menus)
  return hasNewRoutes
}

***全局前置守卫.js:
  // 如果用户登录了,自动获取用户信息,并存储在vuex当中
  let hasNewRoutes = false
  if (token && !hasGetInfo) {
    let { menus } = await store.dispatch('getInfo')
    hasGetInfo = true
    //动态添加路由
    hasNewRoutes = addRoutes(menus)
  }
  
  
*********************************************************************法二:
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
import Layout from '@/layout'
import approvalsRouter from './modules/approvals'
import departmentsRouter from './modules/departments'
import employeesRouter from './modules/employees'
import permissionRouter from './modules/permission'
import attendancesRouter from './modules/attendances'
import salarysRouter from './modules/salarys'
import settingRouter from './modules/setting'
import socialRouter from './modules/social'

// 动态路由表,项目中不同的用户可以访问不同的功能
// 暂时让所有人都看到这8个页面(最后2天再去做筛选)
// 动态路由规则  异步路由
//只做了前4个。后4个自己扩展
export const asyncRoutes = [
  departmentsRouter,
  settingRouter,
  employeesRouter,
  permissionRouter,
  approvalsRouter,
  attendancesRouter,
  salarysRouter,
  socialRouter
]

export const constantRoutes = [
  {
    path: '/login',
    component: () => import('@/views/login/index'),
    hidden: true
  },

  {
    path: '/404',
    component: () => import('@/views/404'),
    hidden: true
  },

  {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    children: [{
      path: 'dashboard',
      name: 'Dashboard',
      component: () => import('@/views/dashboard/index'),
      meta: { title: '首页', icon: 'dashboard' }
    }]
  },

  {
    path: '/excel',
    component: Layout,
    children: [
      {
        path: '',
        component: () => import('@/views/excel')
      }
    ]
  }

  // { path: '*', redirect: '/404', hidden: true }
]

const createRouter = () => new Router({
  // mode: 'history', // require service support
  scrollBehavior: () => ({ y: 0 }),
  // routes: [...constantRoutes, ...asyncRoutes]
  routes: [...constantRoutes]
})

const router = createRouter()

// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
  const newRouter = createRouter()
  router.matcher = newRouter.matcher // reset router
  // 重置的是路由对象内部match方法(匹配routes选项中的路由规则的)
  // match里会使用newRouter里routes一起代替掉了
}

export default router

***路由守卫.js
import router, { asyncRoutes } from './router'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import store from '@/store'
import getPageTitle from '@/utils/get-page-title'

NProgress.configure({ showSpinner: false }) // NProgress Configuration

const whiteList = ['/login', '/404'] // 白名单: 无需登录, 可以跳转查看的路由地址(在路由表里)

// 问题: 为何动态路由添加后, 在动态路由地址上刷新会404?
// 前提1: 刷新时, 所有代码重新执行, 回归初始化
// 前提2: 刷新时, 路由会从/ 跳转到浏览器地址栏所在的路由地址 (走一次路由守卫代码)
// 动态的还未添加, 所以404了

// 问题: 右上角退出登录+重新登录, 进入到首页时, 网页刷新不? (不刷新)
// 网页本身是不刷新的, 完全依赖路由业务场景的切换 (单页面应用好处: 用户的体验更好, 切换业务场景更快)
// 内存里路由表, 之前添加的筛选后路由规则对象还在不? (在)
// 问题2: 为何重新登录, 路由定义重复了?
// 退出登录的时候, 把token和用户信息清除了
// 登录的时候, 先获取到token保存到vuex和本地, 然后才是跳转路由, 才执行路由守卫(所以判断token有值)
// 但是用户信息没有, 重新请求, 再添加一遍筛选后的路由对象, 所以导致了路由重复

// 解决: 退出登录的时候, 让路由也回归初始化

// 问题: 什么是路由(导航)守卫?
// 答案: 当路由发生跳转的时候, 会触发一个钩子"函数", 在函数中可以通过跳转或取消或强制切换跳转地址来守卫导航
// 路由守卫里必须要有一个next()调用, 出口, 让路由页面跳转
router.beforeEach(async(to, from, next) => {
  NProgress.start()
  const token = store.getters.token
  // 登录了->不能去登录页
  // 非登录->只能去登录页
  if (token) { // 登陆了
    if (to.path === '/login') { // 去登录页
      // 中断要跳转/login这次导航, 重新跳转到/(首页)
      next('/')
      NProgress.done()
    } else { // 去别的页面
      next() // 如果手动让cookie里token改错误, 刷新以后, vuex才会从本地取出错误token
      // 刷新时, 路由守卫会从 / 跳转到地址栏里路由地址, 所以先让页面跳转进去
      // 执行下面请求会401, 被动退出时, 才能拿到跳转后的路由地址(未遂地址给登录页面, 否则next在下面, 未遂地址一直是/)
      if (!store.getters.name) {
        await store.dispatch('user/getUserInfoActions')
        // const menus = await store.dispatch('user/getUserInfoActions')
        // 用menus权限点英文字符串, 和路由规则对象name匹配
        // 把所有准备好的8个路由规则对象, 取出, 看看名字和menus里是否匹配, 匹配就证明
        // 此登录的用户有这个页面的访问权限, 让filter收集此路由规则对象到新数组里
        // const filterList = asyncRoutes.filter(routeObj => {
        //   const routeName = routeObj.children[0].name.toLowerCase()
        //   return menus.includes(routeName)
        // })

        // filterList.push({ path: '*', redirect: '/404', hidden: true })

        // 始终都动态添加先8个路由规则对象
        // 知识点: 路由切换匹配的路由规则对象数组存在于内存中的
        // new Router时, 有一些初始的路由规则对象
        // addRoutes, 会给路由表, 再额外的增加一个规则对象
        // 现象: 路由规则对象添加成功, 但是左侧的导航不见了
        const filterList = asyncRoutes
        router.addRoutes(filterList)

        // 给vuex也同步一份
        store.commit('permission/setRoutes', filterList)

        // 路由再跳转一次, 因为上面next() 会导致白屏(因为放行时, 动态路由还没有加入到内存中路由表里)
        // 添加完, 立刻再跳转一次
        next({
          path: to.path,
          replace: true // 不让回退 类似于this.$router.replace() 防止进入刚才的白屏
        })
      }
    }
  } else { // 没有登录
    if (whiteList.includes(to.path)) { // 要去的路由地址字符串, 是否在白名单数组里出现过, 出现过就放行
      next()
    } else { // 去别的页面(内部项目, 不登录别的页面不能去)
      next('/login')
      NProgress.done()
    }
  }
})
// 验证: 把本地cookie里token手动删除掉, 刷新, 看看是否走最后一个else内
router.afterEach((to, from) => {
  // 正常next()放行了跳转了, 才会走后置守卫, 关闭正常流程进度条
  //动态改变title
  document.title = getPageTitle(to.meta.title)
  NProgress.done()
})

mixins使用
法一:
***lyricScroll.js
export default {
  props: {
    lyric: {
      type: Array,
      default: [],
    },
  },
  data() {
    return {
      // 当前歌词索引
      lyricsIndex: 0,
    };
  },
  methods: {
    // 实现歌词滚动
    lyricScroll(currentLyric) {
      let placeholderHeight = 0;
      // 获取歌词item
      let lyricsArr = document.querySelectorAll(".lyricsItem");
      // 获取歌词框
      let lyrics = document.querySelector(".lyrics");
      // console.log(lyrics.offsetTop)//123
      // console.log(lyricsArr[0].offsetTop)//123
      // placeholder的高度
      if (placeholderHeight == 0) {
        placeholderHeight = lyricsArr[0].offsetTop - lyrics.offsetTop;//123-123
        // console.log(placeholderHeight)//0
      }
      //   歌词item在歌词框的高度 = 歌词框的offsetTop - 歌词item的offsetTop
        // console.log(currentLyric);//歌词索引
        // console.log(lyricsArr[currentLyric - 1])//歌词第一句打印的是全部歌词,后面打印的是上一句歌词的div
      if (lyricsArr[currentLyric - 1]) {
        let distance = lyricsArr[currentLyric - 1].offsetTop - lyrics.offsetTop;
        // console.log(lyricsArr[currentLyric - 1].offsetTop)
        // console.log(lyrics.offsetTop)//123
        // console.log(distance)
        //   lyricsArr[currentLyric].scrollIntoView();
        lyrics.scrollTo({
          behavior: "smooth",
          top: distance - placeholderHeight,
        });
      }
    },
    //获取当前歌词索引
    getCurrentLyricsIndex(currentTime) {
      let lyricsIndex = 0;
      this.lyric.some((item) => {
        if (lyricsIndex < this.lyric.length - 1) {
          if (currentTime > item[0]) {
            lyricsIndex += 1;
          }
          return currentTime <= item[0];
        }
      });
      // console.log(lyricsIndex);
      this.lyricsIndex = lyricsIndex;
    },
  },
  watch: {
    // 监听当前播放时间
    "$store.state.currentTime"(currentTime, lastTime) {
      // 如果两个时间间隔有1秒,则可得知进度条被拖动 需要重新校准歌词index
      // 当歌词数量大于1并且索引为零时,可能歌词位置差距较大,走这个if进行快速跳转
      if (
        (lastTime && Math.abs(currentTime - lastTime) >= 1) ||
        (this.lyricsIndex == 0 && this.lyric.length > 1)
      ) {
        // 处理播放时间跳转时歌词位置的校准
        if (this.lyric.length > 1) {
          this.getCurrentLyricsIndex(currentTime);
          // 滑动到当前歌词
          this.lyricScroll(this.lyricsIndex);
        }
      }
      // 根据实时播放时间实现歌词滚动
      if (this.lyricsIndex < this.lyric.length) {
        if (currentTime >= this.lyric[this.lyricsIndex][0]) {
          this.lyricsIndex += 1;
          this.lyricScroll(this.lyricsIndex);
        }
      }
    },
    // 监听vuex中的musicId 重置歌词索引
    "$store.state.musicId"(musicId) {
      this.lyricsIndex = 0;
    },
    lyric(current) {
      // console.log("获取了歌词");
      // 大于一秒,说明歌词在1秒后才请求成功 歌词可能不能马上跳转到当前时间 这里进行校准
      if (this.$store.state.currentTime > 1) {
        // 处理播放时间跳转时歌词位置的校准
        if (this.lyric.length > 1) {
          this.getCurrentLyricsIndex(this.$store.state.currentTime);
          this.$nextTick(() => {
            // 滑动到当前歌词
            this.lyricScroll(this.lyricsIndex);
          });
        }
      }
    },
  },
};
***组件1
<script>
import LyricsScroll from './LyricsScroll.js'
export default {
  mixins: [LyricsScroll]
}
</script>
***组件2
<script>
import LyricsScroll from './LyricsScroll.js'
export default {
  mixins: [LyricsScroll]
}
</script>

法二:
***courseType.js
export default function(){

	let courseTypeFn = ( type )=>{
		let val = '';
		switch (type) {
	        case 1:
	          val = '初级';
	          break;
	        case 2:
	         val = '中级';
	          break;
	        case 3:
	          val = '高级';
	          break;
	        default:
	          val = '';
	    }
	    return val;
	}

	return {
		courseTypeFn
	}

}
***组件中:
<div class="courseDegree">{{ courseTypeFn(item.courseLevel) }} · {{ item.purchaseCounter + item.purchaseCnt }}人购买</div>
//mixin
import mixin from '../../mixins/courseType.js'
let { courseTypeFn } = mixin();
后台工具函数
import { ref, reactive, computed } from "vue"
import { toast } from "@/composables/util"
// 列表,分页,搜索,删除,修改状态
export function useInitTable(opt = {}) {
    let searchForm = null
    let resetSearchForm = null
    if (opt.searchForm) {
        searchForm = reactive({ ...opt.searchForm })
        resetSearchForm = () => {
            for (const key in opt.searchForm) {
                searchForm[key] = opt.searchForm[key]
            }
            getData()
        }
    }

    const tableData = ref([])
    const loading = ref(false)

    // 分页
    const currentPage = ref(1)
    const total = ref(0)
    const limit = ref(10)

    // 获取数据
    function getData(p = null) {
        if (typeof p == "number") {
            currentPage.value = p
        }

        loading.value = true
        opt.getList(currentPage.value, searchForm)
            .then(res => {
                if (opt.onGetListSuccess && typeof opt.onGetListSuccess == "function") {
                    opt.onGetListSuccess(res)
                } else {
                    tableData.value = res.list
                    total.value = res.totalCount
                }
            })
            .finally(() => {
                loading.value = false
            })
    }

    getData()

    // 删除
    const handleDelete = (id) => {
        loading.value = true
        opt.delete(id).then(res => {
            toast("删除成功")
            getData()
        }).finally(() => {
            loading.value = false
        })
    }


    // 修改状态
    const handleStatusChange = (status, row) => {
        row.statusLoading = true
        opt.updateStatus(row.id, status)
            .then(res => {
                toast("修改状态成功")
                row.status = status
            })
            .finally(() => {
                row.statusLoading = false
            })
    }

    // 多选选中ID
    const multiSelectionIds = ref([])
    const handleSelectionChange = (e) => {
        multiSelectionIds.value = e.map(o => o.id)
    }
    // 批量删除
    const multipleTableRef = ref(null)
    const handleMultiDelete = () => {
        loading.value = true
        opt.delete(multiSelectionIds.value)
            .then(res => {
                toast("删除成功")
                // 清空选中
                if (multipleTableRef.value) {
                    multipleTableRef.value.clearSelection()
                }
                getData()
            })
            .finally(() => {
                loading.value = false
            })
    }

    // 批量修改状态
    const handleMultiStatusChange = (status) => {
        loading.value = true
        opt.updateStatus(multiSelectionIds.value,status)
            .then(res => {
                toast("修改状态成功")
                // 清空选中
                if (multipleTableRef.value) {
                    multipleTableRef.value.clearSelection()
                }
                getData()
            })
            .finally(() => {
                loading.value = false
            })
    }

    return {
        searchForm,
        resetSearchForm,
        tableData,
        loading,
        currentPage,
        total,
        limit,
        getData,
        handleDelete,
        handleStatusChange,
        handleSelectionChange,
        multipleTableRef,
        handleMultiDelete,
        handleMultiStatusChange,
        multiSelectionIds
    }
}

// 新增,修改
export function useInitForm(opt = {}) {
    // 表单部分
    const formDrawerRef = ref(null)
    const formRef = ref(null)
    const defaultForm = opt.form
    const form = reactive({})
    const rules = opt.rules || {}
    const editId = ref(0)
    const drawerTitle = computed(() => editId.value ? "修改" : "新增")

    const handleSubmit = () => {
        formRef.value.validate((valid) => {
            if (!valid) return

            formDrawerRef.value.showLoading()

            let body = {}
            if(opt.beforeSubmit && typeof opt.beforeSubmit == "function"){
                body = opt.beforeSubmit({ ...form })
            } else {
                body = form
            }

            const fun = editId.value ? opt.update(editId.value, body) : opt.create(body)

            fun.then(res => {
                toast(drawerTitle.value + "成功")
                // 修改刷新当前页,新增刷新第一页
                opt.getData(editId.value ? false : 1)
                formDrawerRef.value.close()
            }).finally(() => {
                formDrawerRef.value.hideLoading()
            })

        })
    }

    // 重置表单
    function resetForm(row = false) {
        if (formRef.value) formRef.value.clearValidate()
        for (const key in defaultForm) {
            form[key] = row[key]
        }
    }

    // 新增
    const handleCreate = () => {
        editId.value = 0
        resetForm(defaultForm)
        formDrawerRef.value.open()
    }

    // 编辑
    const handleEdit = (row) => {
        editId.value = row.id
        resetForm(row)
        formDrawerRef.value.open()
    }

    return {
        formDrawerRef,
        formRef,
        form,
        rules,
        editId,
        drawerTitle,
        handleSubmit,
        resetForm,
        handleCreate,
        handleEdit
    }
}
import { ElNotification,ElMessageBox } from 'element-plus'
import nprogress from 'nprogress'

//提示
export function toast(message,type="success",dangerouslyUseHTMLString=true){
  ElNotification({
    message,
    type,
    duration:3000,
    dangerouslyUseHTMLString
  })
}
export function showModal(content = "提示内容",type = "warning",title = ""){
  return ElMessageBox.confirm(
      content,
      title,
      {
        confirmButtonText: '确认',
        cancelButtonText: '取消',
        type,
      }
    )
}
// 显示全屏loading
export function showFullLoading(){
  nprogress.start()
}

// 隐藏全屏loading
export function hideFullLoading(){
  nprogress.done()
}
// 弹出输入框
export function showPrompt(tip,value = ""){
  return ElMessageBox.prompt(tip, '', {
    confirmButtonText: '确认',
    cancelButtonText: '取消',
    inputValue:value
  })
}
// 将query对象转成url参数
export function queryParams(query){
  let q = []
  for (const key in query) {
      if(query[key]){
          q.push(`${key}=${encodeURIComponent(query[key])}`)
      }
  }
  // console.log(q)//['limit=10', 'keyword=ceshi']
  let r = q.join("&")// limit=10&keyword=ceshi
  r = r ? ("?"+r) : ""
  return r
}
// 上移
export function useArrayMoveUp(arr,index){
  swapArray(arr,index,index - 1)
}

// 下移
export function useArrayMoveDown(arr,index){
  swapArray(arr,index,index + 1)
}

function swapArray(arr,index1,index2){
  arr[index1] = arr.splice(index2,1,arr[index1])[0]
  return arr
}

// sku排列算法
export function cartesianProductOf() {
  return Array.prototype.reduce.call(arguments, function (a, b) {
      var ret = [];
      a.forEach(function (a) {
          b.forEach(function (b) {
              ret.push(a.concat([b]));
          });
      });
      return ret;
  }, [
      []
  ]);
}
import {useCookies} from '@vueuse/integrations/useCookies'
const TokenKey = "admin-token"
const cookie = useCookies()
export function getToken(){
  return cookie.get(TokenKey)
}
export function setToken(token){
  return cookie.set(TokenKey,token)
}
export function removeToken(){
  return cookie.remove(TokenKey)
}
接口工具函数
// 将query对象转成url参数
export function queryParams(query){
  let q = []
  for (const key in query) {
      if(query[key]){
          q.push(`${key}=${encodeURIComponent(query[key])}`)
      }
  }
  // console.log(q)//['limit=10', 'keyword=ceshi']
  let r = q.join("&")// limit=10&keyword=ceshi
  r = r ? ("?"+r) : ""
  return r
}
导出文件
const onSubmit = () => {
  if (!form.tab) return toast('订单类型不能为空', 'error')
  loading.value = true
  let starttime = null
  let endtime = null
  if (form.time && Array.isArray(form.time)) {
    starttime = form.time[0]
    endtime = form.time[1]
  }
  exportOrder({
    tab: form.tab,
    starttime,
    endtime
  })
    .then(data => {
      let url = window.URL.createObjectURL(new Blob([data]))
      //定义一个a标签
      let link = document.createElement('a')
      //隐藏掉a标签
      link.style.display = 'none'
      link.href = url
      //文件命名
      let filename = new Date().getTime() + '.xlsx'
      link.setAttribute('download', filename)
      document.body.appendChild(link)
      link.click()
      close()
    })
    .finally(() => {
      loading.value = false
    })
}
可选链、??
先来看两个场景:
场景1

我需要判断数组对象中的某个值是否存在进而去做其他事情:

let title;
if(data&&data.children&&data.children[0]&&data.children[0].title) {
        title = data.children[0].title
}
场景2

我需要判断某个值是否有效进而去做其他事情

let isMan,text,person = {
    name: 'zhangsan',
    hasCount: 0,
    isMan: false
};
if(person.hasCount || person.hasCount === 0) {
    text = person.hasCount
} else {
    text = '暂无数据'
}
上面两个场景我在开发中经常用到,后来在公众号得知js的新语法可选链"?."以及双问号"??"能使这两个场景操作变得简单。


优化如下

//场景1
let title = data?.children?.[0]?.title
//场景2 
let {hasCount} = person;
text = hasCount ?? '暂无数据'
 
 
 
 
//除此之外,"??"还有其他应用场景
let a;
a ??= 6;
console.log(a); // 6
 
可选链的语法允许开发者访问嵌套得更深的对象属性,而不用担心属性是否真的存在。也就是说,如果可选链在挖掘过程遇到了null或undefined的值,就会通过短路(short-circuit)计算,返回undefined,而不会报错。

逻辑空分配运算符仅在空值或未定义(null  or undefined)时才将值分配给a
富文本编辑器

第一步:

cnpm i tinymce
cnpm i @tinymce/tinymce-vue

第二步:

image-20230405183528985

image-20230405183819682

第三步:创建editor组件

<template>
    <editor v-model="content" tag-name="div" :init="init" />
    <ChooseImage :preview="false" ref="ChooseImageRef" :limit="9"/>
</template>
<script setup>
import tinymce from "tinymce/tinymce";
import Editor from "@tinymce/tinymce-vue";
import ChooseImage from "@/components/ChooseImage.vue"
import { ref, watch } from "vue"
import "tinymce/themes/silver/theme"; // 引用主题文件
import "tinymce/icons/default"; // 引用图标文件
import 'tinymce/models/dom'
// tinymce插件可按自己的需要进行导入
// 更多插件参考:https://www.tiny.cloud/docs/plugins/
import "tinymce/plugins/advlist"
import "tinymce/plugins/anchor"
import "tinymce/plugins/autolink"
import "tinymce/plugins/autoresize"
import "tinymce/plugins/autosave"
import "tinymce/plugins/charmap" // 特殊字符
import "tinymce/plugins/code" // 查看源码
import "tinymce/plugins/codesample" // 插入代码
import "tinymce/plugins/directionality"
import "tinymce/plugins/emoticons"
import "tinymce/plugins/fullscreen" //全屏
import "tinymce/plugins/help"
import "tinymce/plugins/image" // 插入上传图片插件
import "tinymce/plugins/importcss" //图片工具
import "tinymce/plugins/insertdatetime" //时间插入
import "tinymce/plugins/link"
import "tinymce/plugins/lists" // 列表插件
import "tinymce/plugins/media" // 插入视频插件
import "tinymce/plugins/nonbreaking"
import "tinymce/plugins/pagebreak" //分页
import "tinymce/plugins/preview" // 预览
import "tinymce/plugins/quickbars"
import "tinymce/plugins/save" // 保存
import "tinymce/plugins/searchreplace" //查询替换
import "tinymce/plugins/table" // 插入表格插件
import "tinymce/plugins/template" //插入模板
import "tinymce/plugins/visualblocks"
import "tinymce/plugins/visualchars"
import "tinymce/plugins/wordcount" // 字数统计插件
// v-model
const props = defineProps({
    modelValue: String,
})
const emit = defineEmits(["update:modelValue"])
const ChooseImageRef = ref(null)
// 配置
const init = {
    language_url: '/tinymce/langs/zh-Hans.js', // 中文语言包路径
    language: "zh-Hans",
    skin_url: '/tinymce/skins/ui/oxide', // 编辑器皮肤样式
    content_css: "/tinymce/skins/content/default/content.min.css",
    menubar: false, // 隐藏菜单栏
    autoresize_bottom_margin: 50,
    max_height: 500,
    min_height: 400,
    // height: 320,
    toolbar_mode: "none",
    plugins:
        'wordcount visualchars visualblocks template searchreplace save quickbars preview pagebreak nonbreaking media insertdatetime importcss image help fullscreen directionality codesample code charmap link code table lists advlist anchor autolink autoresize autosave',
    toolbar:
        "formats undo redo fontsizeselect fontselect ltr rtl searchreplace media imageUpload | outdent indent aligncenter alignleft alignright alignjustify lineheight underline quicklink h2 h3 blockquote numlist bullist table removeformat forecolor backcolor bold italic strikethrough hr link preview fullscreen help ",
    content_style: "p {margin: 5px 0; font-size: 14px}",
    fontsize_formats: "12px 14px 16px 18px 24px 36px 48px 56px 72px",
    font_formats:
        "微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方 = PingFang SC, Microsoft YaHei, sans- serif; 宋体 = simsun, serif; 仿宋体 =  FangSong, serif; 黑体 = SimHei, sans - serif; Arial = arial, helvetica, sans - serif;Arial Black = arial black, avant garde;Book Antiqua = book antiqua, palatino; ",
    branding: false,
    elementpath: false,
    resize: false, // 禁止改变大小
    statusbar: false, // 隐藏底部状态栏
    setup:(editor)=>{
        editor.ui.registry.addButton("imageUpload",{
            tooltip:"插入图片",
            icon:"image",
            onAction(){
                ChooseImageRef.value.open((data)=>{
                    data.forEach(url=>{
                        editor.insertContent(`<img src="${url}" style="width:100%;"/>`)
                    })
                })
            }
        })
    }
};
tinymce.init; // 初始化
const content = ref(props.modelValue)
watch(props, (newVal) => content.value = newVal.modelValue)
watch(content, (newVal) => emit("update:modelValue", newVal))
</script>
<style>
.tox-tinymce-aux {
    z-index: 9999 !important;
}
</style>
上传图片
***UploadFile.vue
<template>
  <!-- uploadImageAction接口地址 -->
  <el-upload
    drag
    :action="uploadImageAction"
    multiple
    :headers="{
        token
    }"
    name="img"
    :data="data"
    :on-success="uploadSuccess"
    :on-error="uploadError"
  >
    <el-icon class="el-icon--upload"><upload-filled /></el-icon>
    <div class="el-upload__text">
      在此放置文件或者 <em>点击上传</em>
    </div>
    <template #tip>
      <div class="el-upload__tip">
        大小小于500kb的jpg/png文件
      </div>
    </template>
  </el-upload>
</template>
<script setup>
import { uploadImageAction } from "@/api/image"
import { getToken } from "@/composables/auth"
import { toast } from "@/composables/util"
const token = getToken()

defineProps({
    data:Object,
})

const emit = defineEmits(["success"])

const uploadSuccess = (response, uploadFile, uploadFiles)=>{
    emit("success",{
        response, uploadFile, uploadFiles
    })
    toast("上传成功!")
}

const uploadError = (error, uploadFile, uploadFiles)=>{
    let msg = JSON.parse(error.message).msg || "上传失败"
    toast(msg,"error")
}
</script>

***组件中:
<el-drawer v-model="drawer" title="上传图片">
    <UploadFile :data="{ image_class_id }" @success="handleUploadSuccess" />
</el-drawer>
自定义指令(用于设置有权限的用户才能看见的模块)
***main.js
import permission from "@/directives/permission.js"
app.use(permission)
***permission.js
import store from "@/store"
function hasPermission(value,el = false){
    if(!Array.isArray(value)){
        throw new Error(`需要配置权限,例如 v-permission="['getStatistics3,GET']"`)
    }
    const hasAuth = value.findIndex(v=>store.state.ruleNames.includes(v)) != -1
    if(el && !hasAuth){
        el.parentNode && el.parentNode.removeChild(el)
    }
    return hasAuth
}

export default {
    install(app){
        app.directive("permission",{
            mounted(el,binding){
                hasPermission(binding.value,el)
            }
        })
    }
}
***组件中:
    <el-row :gutter="20" class="mt-5">
      <el-col :span="12" :offset="0">
        <IndexChart v-permission="['getStatistics3,GET']" />
      </el-col>
      <el-col :span="12" :offset="0" v-permission="['getStatistics2,GET']">
        <IndexCard title="店铺及商品提示" tip="店铺及商品提示" :btns="goods" class="mb-3" />
        <IndexCard title="交易提示" tip="需要立即处理的交易订单" :btns="order" />
      </el-col>
    </el-row>
Echarts使用
cnpm i echarts

***IndexChart.vue
<template>
  <el-card shadow="never">
    <template #header>
      <div class="flex justify-between">
        <span class="text-sm">订单统计</span>
        <div>
          <el-check-tag v-for="(item, index) in options" :key="index" :checked="current == item.value" style="margin-right: 8px" @click="handleChoose(item.value)">{{ item.text }}</el-check-tag>
        </div>
      </div>
    </template>
    <div ref="el" id="chart" style="width: 100%; height: 300px"></div>
  </el-card>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import * as echarts from 'echarts'
import { useResizeObserver } from '@vueuse/core'

import { getStatistics3 } from '@/api/index.js'

const current = ref('week')
const options = [
  {
    text: '近1个月',
    value: 'month'
  },
  {
    text: '近1周',
    value: 'week'
  },
  {
    text: '近24小时',
    value: 'hour'
  }
]

const handleChoose = type => {
  current.value = type
  getData()
}

var myChart = null
onMounted(() => {
  var chartDom = document.getElementById('chart')
  if (chartDom) {
    myChart = echarts.init(chartDom)
    getData()
  }
})

onBeforeUnmount(() => {
  if (myChart) echarts.dispose(myChart)
})

function getData() {
  let option = {
    xAxis: {
      type: 'category',
      data: []
    },
    yAxis: {
      type: 'value'
    },
    series: [
      {
        data: [],
        type: 'bar',
        showBackground: true,
        backgroundStyle: {
          color: 'rgba(180, 180, 180, 0.2)'
        }
      }
    ]
  }

  myChart.showLoading()
  getStatistics3(current.value)
    .then(res => {
      option.xAxis.data = res.x
      option.series[0].data = res.y

      myChart.setOption(option)
    })
    .finally(() => {
      myChart.hideLoading()
    })
}
//图标等比例缩小和放大
const el = ref(null)
if(myChart){
  useResizeObserver(el, entries => myChart.resize())
}
</script>

***index.vue
<el-row :gutter="20" class="mt-5">
      <el-col :span="12" :offset="0">
        <IndexChart/>
      </el-col>
</el-row>
通过gsap库实现数字滚动变化
cnpm i gsap


<template>
  {{ d.num.toFixed(0) }}
</template>
<script setup>
import { reactive,watch } from "vue"
import gsap from "gsap"

const props = defineProps({
  value:{
      type:Number,
      default:0
  }
})

const d = reactive({
  num:0
})

function AnimateToValue(){
  gsap.to(d,{
      duration:0.5,
      num:props.value
  })
}

AnimateToValue()

watch(()=>props.value,()=>AnimateToValue())

</script>

vue3暴露给父组件props、方法、事件
    import { ref } from "vue"

    const showDrawer = ref(false)

    const props = defineProps({
        title:String,
        size:{
            type:String,
            default:"45%"
        },
        destroyOnClose:{
            type:Boolean,
            default:false
        },
        confirmText:{
            type:String,
            default:"提交"
        }
    })

    const loading = ref(false)
    const showLoading = ()=>loading.value = true
    const hideLoading = ()=>loading.value = false

    // 打开
    const open = ()=> showDrawer.value = true

    // 取消
    const close = ()=>showDrawer.value = false

    // 提交,传事件给父组件
    const emit = defineEmits(["submit"])
    const submit = ()=> emit("submit")

    // 向父组件暴露以下方法
    defineExpose({
        open,
        close,
        showLoading,
        hideLoading
    })
vue3实现全屏显示
cnpm i @vueuse/core
import { useFullscreen } from '@vueuse/core'
const {
  // 是否全屏状态
  isFullscreen,
  // 切换全屏
  toggle
} = useFullscreen()
***组件中
<el-icon class="icon-btn" @click="toggle">
          <full-screen v-if="!isFullscreen" />
          <aim v-else />
</el-icon>
进度条nprogress实现
cnpm i nprogress
***main.js
import "nprogress/nprogress.css"
actions中的写法
法一:
async deleteCartListBySkuId({ commit }, skuId) {
    let result = await reqDeleteCartById(skuId)
    if (result.code == 200) {
      return 'ok '
    } else {
      return Promise.reject(new Error('faile'))
    }
},
法二:
getInfo({commit}){
      return new Promise((resolve,reject)=>{
        getInfo().then(res=>{
          commit("SET_USERINFO",res)
          resolve(res)
        }).catch(err=>reject(err))
      })
}
通过VueUse使用cookie,封装token
法一:
cnpm i @vueuse/integrations
cnpm i universal-cookie

import {useCookies} from '@vueuse/integrations/useCookies'
const TokenKey = "admin-token"
const cookie = useCookies()
export function getToken(){
  return cookie.get(TokenKey)
}
export function setToken(token){
  return cookie.set(TokenKey,token)
}
export function removeToken(){
  return cookie.remove(TokenKey)
}
法二:
//对外暴露一个函数
//存储token
export const setToken = token => {
  localStorage.setItem('TOKEN', token)
}
//获取token
export const getToken = () => {
  return localStorage.getItem('TOKEN')
}
//清除本地token
export const removeToken = () => {
  localStorage.removeItem('TOKEN')
}
vite配置@地址、windi css、跨域
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import WindiCSS from 'vite-plugin-windicss'
import path from "path"
// https://vitejs.dev/config/
export default defineConfig({
  resolve:{
    alias:{
      "@":path.resolve(__dirname,"src")
    }
  },
  server:{
    proxy:{
      '/api': {
        target: 'http://ceshi13.dishait.cn',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      },
    }
  },
  plugins: [vue(),WindiCSS()],
})

项目重难点

image-20230316222745566

image-20230217143756040

image-20230217143643814

image-20230217143709989

image-20230217143508572

*image-20230217143445689

image-20230217143231190

image-20230217143126850

image-20230217143042420

image-20230217142834941

*image-20230217142755717 *

image-20230217142655637

image-20230217142451290

image-20230217142216146

image-20230217142143039

image-20230217141900766

image-20230217141907421

image-20230217141914653

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KxLfrvxP-1683379078111)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230217141928687.png)]

image-20230217141955980

image-20230217141454682

image-20230217141304503

image-20230217141222070

image-20230217141151975

image-20230217141111114

image-20230217141103384

image-20230217141057742

image-20230217135637415

image-20230217135549004

image-20230217135446088

image-20230219200733482

JS高级

闭包
闭包的终极定义
(掌握)JS中函数是一等公民
  • 在 js 中,函数是非常重要的,并且是一等公民

    1. 那么就意味着函数的使用非常灵活
    2. 函数可以作为另一个函数的参数,也可以作为一个函数的返回值来使用
  • 自己编写高阶函数

  • 使用内置的高阶函数

  • 高阶函数:一个函数可以接收另外一个函数作为参数,或者该函数会返回另外一个函数作为返回值的函数,那么这个函数就称之为是一个高阶函数

  • Tips:一般来说,函数 function 和 方法 method 是同一个东西,但是也有区别

    • 函数:当这个函数是一个独立的函数,称之为一个函数
    • 方法:当我们一个函数属于某一个对象时,我们称这个函数是这个对象的方法,比如 var obj = { foo: function(){} }
(掌握)JS 闭包的定义
  • 这里我们来看一下闭包的定义,分成两个,在计算机科学中和 JavaScript中

  • 计算机科学科学中对于闭包的定义(维基百科):

    1. 闭包 (英语:Closure),又称词法闭包(Lexical Closure)闭包函数( function closure)
    2. 是在支持头等函数(即一等公民)的编程语言中,实现词法绑定的一种技术
    3. 闭包的实现是一个结构体(这个概念是在C语言中的,如果换成 JS,其实就是一个对象),也就是说这个结构体里面存储了两个东西,一个是函数,一个是关联的环境(相当于是一个符号查找表)
    4. 闭包跟函数最大的区别在于,当捕捉闭包的时候,他的自由变量会在捕捉时被确定,这样即使脱离了捕捉的上下文,也会照常运行
  • JavaScript中对于闭包的解释:

    1. 一个函数和其周围状态( Lexical environment词法环境 ) 的引用捆绑在一起的(或者说函数被引用包围),这样的组合就是闭包( Closure )
    2. 也就是说,闭包可以让你在一个个内层函数中访问到其外层函数的作用域
    3. 在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来
(掌握)高阶函数的执行过程
  • 先分析一段代码的执行过程:

    function foo() {
      function bar() {
        console.log('bar')
      }
      return bar
    }
    
    var fn = foo()
    fn()
    
  1. 首先依旧是创建 GO 对象,将 String、window、setTimeout等等加入 GO, 同时做一个编译提升,全局变量为 undefined,遇见函数创建函数对象空间,并返回在内存中的地址,如下:

    // GO
    {
      String: "类",
      setTimeout: "函数",
      window: GlobalObject,
      fn: undefined, // 定义的变量是在 GEC 开始执行的时候才会加入
      foo: 0xa00 // 内存中引用的地址
    }
    
    // 函数对象空间存储
    parentScope 和 函数体
    
  2. 编译完成之后,就会在 ECStack 栈中为 GO 创建 GEC,开始执行 var fn = foo() ,就会为函数 foo 创建函数执行上下文,将 VO 指向 AO, AO 检测到包含 bar 函数,就会为 bar 创建一个函数对象空间,返回地址 0xb00,然后开始执行 foo 函数的后续代码,将 bar 的地址 返回出去

  3. 此时的 foo bar 函数已经执行完毕,而 var fn = foo() 也会变成了var fn = 0xb00,开始执行 fn(),fn也会在ECStack 调用栈中创建函数执行上下文,而其引用的是 bar 函数的地址,所以 fn 函数的 AO 对象也是空,bar 函数体只是打印一句话,所以最后输出 ‘bar’,当然 fn 执行完毕后,函数上下文也会被销毁

  4. 图解如下:

    image-20221026223500294
(掌握)闭包的执行过程
  1. 现在我们将上面的代码稍微增加一点,在看看执行过程

    function foo() {
      var name = 'foo'
      function bar() {
        console.log('bar', name)
      }
      return bar
    }
    
    var fn = foo()
    fn()
    
  2. 首先在执行形参var fn = foo()代码的时候,会在调用栈里面创建函数的执行上下文, foo 函数的 AO 的时候会多生成一个 name,如下:

    // foo AO
    {
      name: undefined, 
      bar: 0xb00
    }
    
  3. 然后在创建 AO 之后,就会开始执行foo 函数内的第一行代码,var name = ‘foo’,就将 AO 中的 name 赋值为了 ‘foo’,随后将执行 return bar,将引用的 bar 函数地址返回出去

  4. 随后 fn 在执行引用了 bar 的函数对象空间地址 0xb00 后,执行代码 var fn = 0xb00,此时 foo 和 bar 的函数执行上下文就被销毁了,但是他们的 AO 对象还是存在在堆内存中,然后执行 fn(),为 fn 创建函数执行上下文,开始在 bar 函数中的 AO 找 name,没有找到,就去父级作用域 foo 函数中找 name, 发现 name 为 ‘foo’

  5. 最后 console 输出的语句就是 bar, foo

  6. 那这里的闭包是在哪里形成的呢,正常来说,在执行第九行代码 var fn = foo()的时候,foo 函数将 abr 的地址返回之后已经执行完毕了,按理来说里面创建的 name 也会被销毁掉,为什么还依然可以对象 foo 函数内的 AO 进行访问呢?这里本该销毁的变量在 JS 内部帮我们保存了下来,因此可以进行访问,所以当我们函数执行完毕,本该销毁的变量却还能通过调用 bar 去访问,这时候这个变量他就形成了

  7. 结合开篇所讲的定义,闭包是有有两部分组成的,比如这里 bar函数本身 + 可以访问到外部的 name 这个自由变量,两者结合在一起,就是闭包

  8. 总结:闭包就是函数 + 自由变量,现在就可以解释计算机科学上对闭包的定义是什么:捕捉闭包就是捕捉 bar 这个函数的时候,将外面 name 这个自由变量会一起被捕捉,让本该销毁的保存了下来,所以在脱离了上下文之后,还可以被执行,JavaScript中对于闭包的解释也是大同小异:一个函数就代表 bar 这个函数, 周围状态就表示被绑定的变量,那为什么又说在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来,来看下面一段代码,这也是一个闭包,因为他可以访问外部的变量,只不过是全局变量

    var uname = 'zs'
    function fun(){
        console.log(uname)
    }
    fun()
    
  9. 在来看一个特殊情况,如下:这算一个闭包吗,其实都有争论;如果从可以访问的角度出发,那么这个函数就可以访问外部的变量,只是没有访问而已,那就是闭包;如果从必须访问的角度出发,那么这个函数没有访问外面的变量,那就不是闭包

    function demo(){
        
    }
    demo()
    
(掌握)闭包的总结
  1. 一个普通的函数 function,如果它可以访问外层作用域的自由变量,那么这个函数就是一个闭包
  2. 从广义的角度来说:JavaScript 中的函数都是闭包
  3. 从狭义的角度来说:JavaScript 中一个函数,如果访问了外层作用域的变量,那么它是一个闭包

image-20230424194334181

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JTGvYbmk-1683379078115)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230424200022549.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wegQqDQt-1683379078115)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230424203750395.png)]

函数的执行过程的内存

先解析:

image-20230424211256805

执行:

image-20230424214353331

(进阶版)foo执行前先解析:

image-20230424224007224

执行foo:

image-20230424225317085

内存泄漏
例一:

image-20230424225536615

解决内存泄漏的方法:
function foo(){
var name = "foo"
var age = 18
function bar(){
	console.log(name)
	console.log(age)
}
	return bar
}
var fn = foo()
fn()
fn = null//将fn赋值为null
//fn()
//fn()
//fn()
//fn()
//fn()

image-20230424230933814

例二:
创建了 var arrayFns = []
这一整个都不会销毁,占用内存很大,内存泄漏严重

image-20230425132533535

image-20230425135231497

image-20230425135024240

(了解)开篇问题
  • 实例代码如下:

    function foo() {
      var name = 'foo'
    function bar() {
      console.log('bar', name)
    }
      return bar
    }
    var fn = foo()
    fn()
    
  • 为什么在 foo 函数已经执行完毕之后,AO 没有被销毁,还能保存下来,并且会被 bar 所访问到呢

  • 同时内存泄露又是什么呢?

(掌握)函数执行过程中的内存
  1. 实例代码如下:

    var message = 'Hello world'
    function foo() {
      var name = 'zs'
      var age = 18
    }
    
    function test() {
      console.log('test')
    }
    
    foo()
    
    test()
    
  2. 第一步还是创建 GO 里面存储着 String、Date、setTimeout、window等等的全局方法,然后在ECStack调用栈里面创建 GEC,GEC 开始执行的时候就会去定义全局 JS 代码的全局变量或函数,如上述代码中的 message变量和 foo 、test 函数,此时 message 赋值为 undefined,两个函数分别引用函数对象在内存中的地址,而两个函数当中我们知道会存储一个 parentScope 和 函数体,那么这个 parentScope 我们一直只是说保存的是父级作用域,父级作用域也就是 GO,此时我们可以衍生出一点之前没有谈到的,GO 本质上也是内存中开辟的一块空间,所以自然也是存在地址,所以两个函数父级作用域实际保存的是 GO 的内存引用地址

  3. 而当执行到 foo() 的时候,就是调用函数,会在调用栈中创建一个 FEC,这个 FEC 开始执行阶段会有一个 VO 指向 AO,AO 中会解析 foo 函数,name 和 age 赋值为undefined,开始解析完成之后,就会开始执行 foo 函数中的代码,将 name 赋值为 zs,age 赋值为18,等全部执行完成之后,foo 函数的执行上下文就会在调用栈中销毁,调用栈函数的执行上下文被销毁,那内存中的 AO 对象也自然而然被销毁了,test 函数也是一致的

(掌握)闭包为什么可以保存变量
  1. 实例代码如下:

    var message = 'Hello world'
    function foo() {
      var name = 'zs'
      var age = 18
      function bar() {
          console.log(name)
          console.log(age)
      }
      return bar
    }
    
    var fn = foo()
    fn()
    
  2. 前面的执行步骤就不在过多赘述了,直接从 foo 函数执行开始,既然是执行那肯定就是会有 FEC,AO 中 name 和 age 赋值为 undefined,bar 是函数又会去创建一个函数对象,返回一个地址0xb00,在创建 bar 函数的函数对象的时候,就会创建一个自身的 parentScope,就是 foo 函数

  3. 在解析完成之后,就要开始执行,name 赋值为 zs,age 赋值为 18,bar 的地址被返回,而执行完毕后 foo 执行上下文就会被销毁,正常流程内存中创建的 foo 的AO 也会被销毁,但是没有,就是因为 bar 函数中还存在着 foo 函数AO对象的引用地址,而这个引用地址还返回给了 fn,所以 fn 也还在引用着,var fn = 0xb00

  4. 此时 fn()执行,也就代表着会在 ECStack 中创建 FEC,首先 bar 中没有定义变量,所以 AO 是一个空对象,然后开始执行代码,输出 name 和 age ,自身作用域中找不到,就会去父级作用域中查找,然后在 foo 函数的 AO 中找到了 name 和 age ,直接输出打印,执行完毕后就销毁了

(掌握)执行完毕未销毁的变量
  1. 上述的叙述中,fn 执行完毕之后, bar 和 foo 的在内存中函数对象还是存在,bar 函数对象存在就会一直指着 foo 创建的 AO 对象,所以一直没有销毁掉,但是如果我后续不在调用 fn 了呢,拿在这里是不是就是浪费了呢
  2. 这种该销毁的东西一直没有销毁,就称之为内存泄露
  3. 如果想要解决这个内存泄露也很简单,直接将 fn = null,取消指向就可以了,让 fn 赋值为 null,实际上是在把 bar 函数的指向从指向 foo 函数的 AO 对象变成了 null
  4. 然后根据 JS 引擎采用的标记清除 GC 算法,从根对象 (GO) 开始查找,没有指向的对象就会被销毁,此时 fn 不在指向 bar函数对象,所以 bar 函数对象就会被销毁,而 bar 函数对象被销毁后,就没有函数对象指向 foo 函数对象所创建的 AO 了,所以也会被销毁,但是此时 GO 中的 foo 还是引用着 foo 函数对象的地址,所以还是会存在,如果不需要了,也可以将 foo = null,更换指向
函数执行-作用域链-面试题-内存管理
函数执行-作用域链

image-20230424131049704

image-20230424132101105

image-20230424135727647

/*
  为什么是 hello world?
  因为 foo 的 parentScope 为 window,编译阶段创建 go 的时候遇到函数
会在内存中开辟一个函数空间对象
  这个在编译的时候就被确定了,同时保存自身函数foo的 parentScope 和 函数体
  因为在编译时候就确定了 parentScope,所以执行阶段没有在 foo函数自身的 AO 中找到message
  就会往 parentScope 作用域查找
  所以输出为 hello world
*/

image-20230424132552976

JS 全局代码执行的过程
(掌握)写一段全局代码分析执行过程
  • 假设我们现在有一段代码

    var name = 'zs'
    var num1 = 10
    var num2 = 20
    var result = num1 + num2
    console.log(result)
    
  1. 第一步解析代码:

    1. 代码被解析,V8引擎内部会帮我们创建一个对象GlobalObject

    2. Global全局 Object对象,顾名思义,创建一个全局对象,GlobalObject 也称之为 go

    3. 这一步实际上就是 js源代码被解析成 AST 树之间的这一步过程,首先会把所有全局对象都放进 GlobalObject 里面,比如 String,Data,setTimeout,window等等全局下就可以使用的,下面写一段伪代码说明一下

      var globalObject = {
            String: "类", // 为什么这里是一个类呢,因为都是可以被 new 创建使用的
            Data: "类",
            window: this, // 这里的this也就是指向 globalObject
            // 还有其余的....
            // 那我们书写的代码呢,也是需要被解析的
            // 会将我们的属性也放到全局对象中,但是因为代码还没有被执行,还没有赋值,所以是undefined
            name: undefined,
            num1: undefined,
            num2: undefined,
            result: undefined
      
  2. 第二步运行代码:

    1. V8引擎为了执行代码,会在内部有一个执行上下文栈(英文名称 Execution Context Stack 简称为 ECStack 或 ECS )也叫作函数调用栈

    2. 在运行的时候会有一个 ECStack 栈,代码想过执行都需要放在 ECStack 栈里面,一般 ECStack 栈里面是放函数的,但是我们的示例代码里面没有函数,那应该怎么运行呢

    3. 因为我们现在执行的都是全局代码,为了我们的全局代码能够正常执行,需要创建一个全局执行上下文(全局代码需要被执行时才会被创建)( 英文名称是 Global Execution Context 简称 GEC ),全局上下文只有一个

    4. 全局执行上下文创建后会将这个 GEC 放入到 ECStack 栈中,还记得我们在解析的时候创建一个全局对象 GlobalObject 吗,全局上下文中有一个 VO(variable Object 即 变量对象),这个 VO 指向的就是我们的GO(全局对象),当然,如果是函数的话,这个 VO 指向的其他对象,这里就暂且不谈,现在开始结构都有了,就会开始执行代码了

    5. 那怎么执行代码呢,就是按照 js 自上而下的执行方式执行,比如执行 var name = ‘zs’,执行这一步的时候会通过我们的 VO 找到 GO,找到 GO 里面的属性 name,将 undefined 改为 ‘zs’,其余执行过程一样,然后在输出 console.log(result),就会在 go 里面找到 result 打印

    6. 但是如果我们在第二行之前,如下图 的位置,此时打印 num1 因为还没有执行下面 var num1 = 10,将 go 里面的 num1赋值为 10,还是 undefined,所以找到 num1的值为 undefined,打印的结果就是 undefined ,也就是我们通常所说的作用域提升,作用域提升主要就是将变量放到 go 里面

      var name = 'zs'
      console.log(num1)
      var num1 = 10
      
    7. 图解如下:

      image-20221024232509018
(掌握)全局函数代码的执行过程
  • 上述可以看到 var定义的变量,在定义前打印输出,会输出 undefined

  • 现在来进行测试一段代码:

    var name = 'zs'
    
    foo() // console.log('123')
    function foo(num){
        console.log(a)
        var a = 10
        var b = 20
        console.log('123')
    }
    
  • 为什么输出不是 undefined呢,下面来一起看一下执行的过程;

    1. 前面步骤的执行与变量没有太大的区别,遇到 foo() 这句代码的时候,是在解析,没有在执行,所以对于这句代码没有处理,函数比较特殊,当遇到函数 foo的时候,不会像变量一样定义一个值为 undefined存入 go,在存入 go 的时候,它会在内存里面创建另外一个函数对象空间存储 foo 函数,在编译阶段,这个新创建的函数对象主要包含两个参数,父级作用域函数执行体(代码块),就会生成一个如下的 go 和 函数对象

      var glabalObject = {
          name: undefined,
          foo: 0xa00 // 所以这里是引用内存地址
      }
          
      // 函数对象空间 存储函数,同理,既然在内存中开辟了一块空间,就有对应的内存地址 这个地址一般是十六进制
      // 假设这里内存地址为 0xa00
          scope: parent scope
          函数执行体:代码块
      
    2. 开始运行:

      1. name的变量一样进行赋值为 ‘zs’

      2. 执行 foo() 的时候去go里面找,找到 go 里面的 foo 其实是一个内存地址,根据内存地址找到函数对象空间,小括号就表示调用,这时候就会把我们这个函数给他放入到函数的调用栈ECStack里面,但是放入的不是一个函数,而是在 ECStack 里创建一个函数上下执行文(Functional Execution Context 简称 FEC)

      3. 创建玩 FEC 之后也不会立即执行,而是会在 FEC 之中也会创建一个 VO,不过对应的对象从 GO 变成了 AO(Activation Object),只不过和 GO 的区别是,AO 会被销毁

      4. AO 在执行之前会做一些什么事情呢,上方我们的 foo 函数中有 a 和 b 两个变量,和一个参数 num,会生成一段如下 AO

        {
            num: undefined,
            a: undefined,
            b: undefined
        }
        
      5. 开始执行代码,首先对 foo 函数代码进行改造,如下,首先 num 赋值为 ‘test’,a 在查找的时候会在 VO 里面去找,VO 对应的就是 AO,AO 里面的 a 为 undefined,因为 a 还没进行赋值,后续为继续赋值,最后输出 ‘123’

        var name = 'zs'
        
        foo('test') 
        function foo(num){
            console.log(a)
            var a = 10
            var b = 20
            console.log('123')
        }
        
      6. 而当函数执行完毕之后,这个 FEC 就会被弹出 ECStack 栈,也就是被销毁掉,同时 AO 在没有对象指向的情况下,也会被销毁,如果在调用,会把这个流程在重复一次

作用域原理详解
(掌握)初识作用域链
  • 我们先来看一段示例代码

    var name = 'zs'
    
    foo(123)
    function foo(num){
        console.log(m)
        var m = 10
        var n = 20
        var name = 'ls'
        console.log(name) 
    }
    // 上述这段代码可以得出 name 输出为 ls
    
    var name = 'zs'
    
    foo(123)
    function foo(num){
        console.log(m)
        var m = 10
        var n = 20
        //  var name = 'ls' 当函数内部的name被注释时
        console.log(name) 
    }
    // 这段代码 name 输出为 zs
    
  • 我们都知道 name 输出为 ‘zs’,那为什么输出为 ‘zs’ 呢

  • 先来看看查找这个 name 时所执行的过程

    1. 当我们查找一个变量的时候,真实的路径是沿着作用域链进行查找
    2. 而这个作用域不仅有自身会创建的一个作用域,还有一个父级作用域(ParentScope),也就是说这个作用域链不仅包含函数自身创建的 AO 还包含函数的父级作用域,而函数的父级作用域就是全局对象 go
    3. 所以在自身作用域 AO 里面无法找到时,就会一层一层的往上寻找,所以输出的结果就是 go 里面的 name
(掌握)函数嵌套下的作用域链查找规则
  • 示例代码展示:

    var name = 'zs'
    
    foo(123)
    function foo (num) {
      console.log(m)
      var m = 10
      var n = 20
    
      function bar () {
        console.log(name)
      }
      bar()
    }
    
  1. 现在我们知道作用域链的规则是一层一层往上查找的,我们在来简单论述一遍流程

  2. 第一步创建 go,,name解析阶段没有赋值,函数 foo 会创建一个函数空间对象,在内存中开辟一块空间,foo 引用的就是内存地址,此时 go存储的内容如下:

    // go 存储
    {
      name: undefined,
      foo: 0xa00 
    }
    
  3. 为什么 bar 函数还没有创建呢,因为此时的 foo 并没有被调用,所以是 foo 函数体内的一部分,还未开始运行,解析完成开始运行的时候,go 中会在 ECStack调用栈创建全局上下文执行文,foo 函数会在 ECStack调用栈中创建一个函数上下执行文文

  4. 此时全局上下执行文中的VO(变量对象)指向 go,而函数上下执行文中的 VO 指向 AO(可以理解为一个活跃的对象,因为执行结束会被回收),同时还保存着一个作用域链,就是自身的 AO + GO,此时 AO 存储的内容为如下所示:

    // foo AO
    {
      num: undefined,
      m: undefined,
      n: undefined,
      bar: 0xb00
    }
    
  5. 既然是函数,bar 就会创建一个 函数对象空间,空间地址假设为 0xb00,同时也会在 ECStack栈中创建一个函数上下文,bar 在函数上下执行文中,也会有一个 VO 指向 AO,和一条作用域链,自身的 AO + AO(FOO) + GO,再来看一下 bar 中 AO 存储的内容:

    {
      // bar 内没有变量,所以是空对象
    }
    
  6. 最后开始真正的执行,依次将 GO 中的变量和 foo 中 AO 的存储的变量进行赋值,如下:

    // GO
    {
      name: 'zs',
      foo: 0xa00  // 开始执行foo
    }
    
    // foo 内的 AO
    {
      num: 124,
      m: 10,
      n: 10,
      bar: 0xb00 // 开始执行 bar
    }
    
    // bar 内的 AO 为空
    
  7. 而 bar 函数内部的 name 无法在自身的 AO 和 foo 函数中的 AO 找到值,就会在往上一层的 GO 进行查找,最后输出 GO 中存储的 name

  8. 图解如下:

    image-20221025230404635

  9. 特别提示:如果将 GO 中的name 去掉也没用报错 undefined,是因为 window 对象本身就带有 name 属性,所以尽量避免使用 name 作为变量名

关于新本规则对于命名的变化
  • 在 ES5 之前命令是: variable object 也就是变量对象,简称 VO

  • 在最新的ECMA规范中,对这个定义做出了一些修改,更加的严谨,而变量对象也就是VO修改成了变量环境(variable Environment)也就是VE,为什么会做出这个修改呢?因为这个变量环境不一定用对象实现,还能是Map等等,所以用变量环境来描述更加严谨些

面试题
面试题一:
/* 
  为什么输出为 200 呢
  按照内部的执行过程就是,在函数上下文执行阶段,扫描的时候,要执行 n=200这行代码,
  但是发现函数自身的AO里面没有n这个变量属性,就会去父级作用域查找,
  然后父级作用找到了n,就把n赋值成了200
*/

image-20230424123629532

面试题二:
// 结果:undefined 200
/* 
  解析1:
  按照变量提升的角度
  var n 
  function foo(){
    var n
    conosle.log(n)
    n = 200
    conosle.log(n)
  }
  n = 100
  foo()

  解析2:
  在函数准备执行阶段 AO 中有一个属性 n 需要定义,所以存储为 {n: undefined}
  正式执行阶段,输出第一个 n,可以在AO中找到 n,不过是 undefined
  在按照顺序将 n 赋值为 200
  打印第二个 n 就是200
*/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CylWcrrj-1683379078119)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230424123647436.png)]

面试题三:
/* 
  var a
  function foo(){
    var a
    console.log(a)
    return
    a = 100
  }
  a = 100
  foo()
*/

image-20230424123736668

面试题四:
/* 
  // undefined 100
  var b = 100
  function foo(){
    var a
    a = 100
  }

  foo()
  console.log(a)
*/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EHCT6g4J-1683379078120)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230424123945410.png)]

内存管理

image-20230424125356486

垃圾回收

image-20230424125732050

image-20230424130135921

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ywdbQhdt-1683379078121)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230424130642674.png)]

浏览器工作原理和v8引擎

image-20230506203842886

image-20230506203934149

this指向
(理解)this 的作用
  1. 实例代码:

    var obj = {
      name: 'zs',
      eating: function () {
        console.log(obj.name + '在吃东西')
      },
      runing: function () {
        console.log(obj.name + '在跑步')
      },
      studying: function () {
        console.log(obj.name + '在学习')
      }
    }
    
    obj.eating()
    obj.runing()
    obj.studying()
    
  2. 上述可以看到,很多时候好像没有 this 我们也是可以实现我们的需求,所以为什么需要呢?

  3. 来看一段代码

    var obj = {
      name: 'zs',
      eating: function () {
        console.log(obj.name + '在吃东西')
      },
      runing: function () {
        console.log(obj.name + '在跑步')
      },
      studying: function () {
        console.log(obj.name + '在学习')
      }
    }
    
    obj.eating()
    obj.runing()
    obj.studying()
    
    var obj1 = {
      name: 'zs',
      eating: function () {
        console.log(obj1.name + '在吃东西')
      },
      runing: function () {
        console.log(obj1.name + '在跑步')
      },
      studying: function () {
        console.log(obj1.name + '在学习')
      }
    }
    
    obj1.eating()
    obj1.runing()
    obj1.studying()
    
  4. 首先可以看到,一段赋值的代码,只是对象的名字不同, copy 后需要一一修改对象名,或者有一天,对象名称改变了,比如 obj1 改成了 obj2,下面的方法是不是也需要修改呢,如果用的是 this 就方便很多了

    var obj = {
      name: 'zs',
      eating: function () {
        console.log(this.name + '在吃东西')
      },
      runing: function () {
        console.log(this.name + '在跑步')
      },
      studying: function () {
        console.log(this.name + '在学习')
      }
    }
    
    obj.eating()
    obj.runing()
    obj.studying()
    
  5. 所以没有 this 也可以实现,但是会让我们编写的收比较繁杂,说到这,就可以知道 this 的作用了–指向

(掌握)this 在全局作用域下的指向
  • 在大多数情况下,this 都是出现在函数中,在全局作用域下,浏览器中 this 绑定的是 window,在 node 环境下,this 绑定的是一个空对象,创建一个 js 文件,输出 this,使用 node 运行,输出的是一个空对象

  • 但是在开发中,很少直接将 this 在全局作用域下去使用 this,通常都是在函数中使用

    1. 所有的函数被调用时,都会直接创建一个执行上下文
    2. 这个上下文中记录着函数的调用栈, AO 对象等
    3. this 也是其中的一条记录
  • 我们知道所有函数在创建的时候都会创建一个函数的执行上下文,这个 FEC 里面包含着 VO 和 作用域链,其实还包含着一个 this,只要这个函数里面打印 this 或者使用 this,FEC 都会来看一下这个 this 的指向到底是谁

  • 这个 this 是动态绑定的,为什么呢,因为只有在执行的时候,才会真正的绑定上,而不是在代码编译的时候确定的,正式因为这种动态的绑定,所以有很多的绑定规则

(掌握)this 的绑定规则
  • 实例代码

    function foo() {
      console.log(this)
    }
    // 1、直接调用
    foo() // window
    
    // 2、创建一个对象,对象中的函数指向 foo
    var obj = {
      name: 'zs',
      foo: foo
    }
    obj.foo() // obj
    
    // 3、apply 调用
    foo.apply('abc') // string: {'abc'}
    
  • 通过这个案例可以看出一下几点:

    1. 函数在调用时,JavaScript 会默认给 this 绑定一个值
    2. this 的绑定和定义的位置(编写的位置)没有关系
    3. this 的绑定和调用方式以及调用的位置有关系
    4. this 是在运行时被绑定的
  • 下面来一起看一下 this 的四种绑定规则:

    • 默认绑定、隐式绑定、显示绑定、new 绑定
(掌握)this 绑定规则之默认绑定
  1. 什么情况下会默认绑定呢,独立函数调用的时候

  2. 独立函数调用可以理解成函数没有被绑定到某个对象上调用

  3. 代码案例如下:

    /**
     * 案例1:
     */
    // function foo() {
    //   console.log(this)
    // }
    
    // foo()
    // 在全局下指向 window,但是不一定要在全局内调用,
    // 只要不是通过什么对象调用的方式,是一个独立的函数
    
    /**
     * 案例2:
     */
    // function foo1() {
    //   console.log(this)
    // }
    
    // function foo2() {
    //   console.log(this)
    //   foo1()
    // }
    
    // function foo3() {
    //   console.log(this)
    //   foo2()
    // }
    // foo3()
    // 会发现都是指向 window,因为都是独立调用,并没有通过指定谁来对这些函数进行调用
    
    /**
     * 案例3:
     */
    // var obj = {
    //   name: 'zs',
    //   foo: function () {
    //     console.log(this)
    //   }
    // }
    // var bar = obj.foo
    // bar()
    // 也是 window,为什么,我们仔细看一下,这个函数被调用的时候,有其他的主题吗
    // 没有,是直接将值赋予然后调用,所以不要看定义的时候有没有主题,而是看调用的时候
    // 有没有主题
    
    /**
     * 案例4:
     */
    // function foo() {
    //   console.log(this)
    // }
    // var obj = {
    //   name: 'zs',
    //   foo: foo
    // }
    
    // var bar = obj.foo
    // bar()
    // 还是 window,因为调用的时候是独立调用,不需要注意如何定义的,只需要
    // 注意调用的时候是不是独立调用的
    
    /**
     * 案例5:
     */
    function foo() {
      function bar() {
        console.log(this)
      }
      return bar
    }
    var fn = foo()
    fn()
    // 同理,调用时没有主题调用,window
    
  4. 根据以上我们可以得出结论,只要调用的时候是独立调用,都是指向 window

(掌握)this 绑定规则之隐式绑定
  1. 通过某个对象调用,也就是他的调用的时候,是通过某个对象发起的函数调用

  2. 隐式绑定:对象调用函数的时候(object.fn()),object 对象会被 js 引擎绑定到 fn 函数中的 this 里面,这个过程是自动的,我们看不到这个过程,所以叫做隐式绑定

  3. 案例如下:

    /**
     * 案例1:
     */
    // function foo() {
    //   console.log(this)
    // }
    
    // var obj = {
    //   name: 'zs',
    //   foo: foo
    // }
    
    // obj.foo() // obj对象
    // 为什么是 obj,因为调用的时候,是通过 obj 被调用的,
    // 就是在ECStack调用栈,里面运行FEC的时候,会执行 obj.foo()这句代码
    // 执行这句代码的时候,js引擎就会把 obj 绑定成执行上下文里面的this
    // 所以打印 this 的时候就会取到这个 obj
    
    /**
     * 案例2:
     */
    var obj = {
      name: 'zs',
      eating: function () {
        console.log(this.name + '在吃饭')
      },
      runing: function () {
        console.log(this.name + '在跑步')
      }
    }
    obj.eating()
    obj.runing()
    // 这里this指向原理和案例1一样
    
    /* 稍微修改 */
    var fn = obj.eating
    // 注意这里的 eating 不能加括号调用,否则就是执行了,返回的就是undefined
    // 因为没有 return 的函数,默认返回是 undefined
    fn() // 在吃饭
    // 会发现 name 的值没有打印出来,现在是独立调用,
    // 因为此时的 this 已经指向 window 了
    
    /**
     * 案例3:
     */
    var obj1 = {
      foo: function () {
        console.log(this)
      }
    }
    
    var obj2 = {
      name: 'obj2',
      bar: obj1.foo
    }
    
    obj2.bar() // obj2 调用者是 obj2
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LJnHTnrz-1683379078122)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230426191755193.png)]

(掌握)this 绑定规则之显示绑定
  1. 如果我们不希望在对象内部包含这个函数的引用,同时又希望这个对象上进行强制调用,该怎么做呢?

    1. JavaScript 所有的函数都可以使用 call 和 apply 方法 (这个 Prototype 有关)

      • 这里先不对 Prototype 进行叙述

      • call 和 apply 有什么不同呢,传入的第一个参数是相同的,call第二个参数需要传入参数列表,而apply 第二个参数需要传入数组

    2. 这两个函数的第一个参数都要求是一个对象,这个对象的作用是什么呢?就是给 this 准备的

    3. 在调用这个函数时,会将 this 绑定到这个传入的对象上

  2. 先来认识一下 apply 和 call 的基本使用

    /* 
      案例1
    */
    function foo() {
      console.log('被调用了', this)
    }
    
    foo() // 可以直接调用
    foo.call()
    // 也可以通过 call 调用,可以使用 call 是因为函数原型上有一个 call 方法
    foo.apply()
    // 也可以通过 apply 调用
    // 为什么都可以调用,还需要 call 和 apply 呢?因为 this 的绑定是不同的
    // foo 直接调用时执行 window
    var obj = {
      name: 'zs'
    }
    // 如果想要 foo 指向 obj,又不想在 obj 里面引用,不像用隐式绑定实现,如下:
    // var obj = {
    //   name: 'zs',
    //   foo: foo
    // }
    // obj.foo() // 这样是可以调用,且指向 obj 的
    
    // 但是我是要指向 obj 又不想使用使用上面这种方式呢,此时就可以使用 call 和 apply
    foo.call(obj)
    foo.apply(obj)
    
  3. apply 和 call 的区别

    function sum(num1, num2) {
      console.log(num1 + num2, this)
    }
    // 现在我们知道可以通过 call 和 apply改变指向
    // 那如果需要参数应该怎么传入
    // call 传入参数列表
    sum.call('call', 1, 1)
    // apply 传入数组
    sum.apply('apply', [1, 2])
    // 所以二者之间的区别只是在于传递参数的方式不一样
    
  4. call 和 apply 在执行函数的时候,是可以明确确定 this 的指向的,所以这种绑定也称之为显示绑定

  5. 显示绑定:bind

    1. 示例代码:

      function foo(){
          console.log(this)
      }
      var newFoo = foo.bind('aaa')
      newFoo()
      
    2. 通过bind修改 this 指向,会返回一个新的函数,通过这个返回新的函数,就算独立调用 newFoo 函数也不会指向 window,在 newFoo 调用之前,被 bind 显示的绑定了一个 aaa,所以以后 newFoo 调用的时候,会一直是指向 aaa,就相当于默认绑定和显示绑定冲突了,两个绑定规则冲突,就会有一个优先级了,通过实例证明 显示绑定优先级大于 默认绑定,所以后续调用就不需要每次都 newFoo.call(‘aaa’),比如:

        newFoo.call('aaa')
        newFoo.call('aaa')  
        newFoo.call('aaa')
        // 或者
        newFoo.apply('aaa')
        newFoo.apply('aaa')
        newFoo.apply('aaa')
        // 但是前面我们已经用bind绑定了,所以就需要这么繁杂了,直接调用即可
        newFoo()
        newFoo()
        newFoo()
      
    3. 那如何传递参数呢?如下:

      function foo(name, age) {
        console.log('我叫' + name + age + '岁了', this)
      }
      
      var newFoo = foo.bind('aaa')
      newFoo('张三', '18') // 我叫张三18岁了 [String: 'aaa']
      
(掌握)this 绑则之 new 绑定
  1. JavaScript 中的函数可以当做一个类的构造函数来使用,也就是使用 new 关键字,

  2. 使用 new 关键字来调用函数时,会执行如下操作:

    1. 创建一个新的对象
    2. 这个新的对象会被执行 prototype 链接
    3. 这个新对象会绑定到调用函数的 this 身上(this 的绑定在这个步骤完成)
    4. 如果函数没有返回其他对象,表达式会返回这个新对象
  3. 示例代码:

    function Person() {}
    
    // new Person()
    // 在使用new 关键字调用的时候会自动在函数内部生成一个对象,
    // 而且会把这个新生成的对象赋值给 this,到最后会把这个对象返回
    // 也就是相当于把这个 this 返回了
    
    // 所以我们可以将他赋值给 p,
    // 这里的 p 就相当于是拿到了 function Person() { return this }
    var p = new Person()
    // 我们通过 new 关键字调用一个函数时,其实是把这个函数当成了一个构造器,
    // 这个时候我们的 this 是在调用这个构造器时创建出来的对象,
    // this = 创建出来的对象,这个绑定的过程就是 new 绑定
    

    image-20230426194542965

  4. 我们通过 new 关键字调用一个函数时,其实是把这个函数当成了一个构造器,这个时候我们的 this 是在调用这个构造器时创建出来的对象,this = 创建出来的对象,这个绑定的过程就是 new 绑定

this补充说明
(掌握)一些其他函数中 this 指向
// 1、setTimeout 的this 指向
// setTimeout(function () {
//   console.log(this) // 指向window,说明内部是独立函数调用
// }, 2000)

// 2、绑定事件的 this 指向
var div = document.querySelector('div')
div.onclick = function () {
  console.log(this) // 指向div
}
// 这里我们是将 this 作为 div 的一个属性,一个属性内部的方法
// 执行就是对象本身,就类似于内部做了一个 div.onclick(),这样的调用

// 那通过 addEventListener 绑定事件呢,因为 addEventListener是可以
// 绑定多个点击事件的
div.addEventListener('click', function () {
  console.log(this) // 也是指向 div
})
div.addEventListener('click', function () {
  console.log(this) // 也是指向 div
})
div.addEventListener('click', function () {
  console.log(this) // 也是指向 div
})
// 通过 addEventListener 调用会把函数手机起来,放入一个数组[fn1, fn2, fn3]
// 调用的时候在将其遍历出来,然后拿到这个函数通过 call的方式绑定上去
// 如:fn1.call(div)

// 3、数组 foreach/map等高阶内置函数
var arr = ['abc', '123', '321']
arr.forEach(function (item) {
  console.log(item, this) // 指向也都是 window。所以猜测这种情况下也是独立函数调用
})

// forEach 第一个参数是传入一个函数,第二个参数就可以传入一个this
arr.forEach(function (item) {
  console.log(item, this) // 此时指向就是 999
}, '999')
(掌握)规则的优先级
  1. 默认规则的优先级最低

  2. 显示绑定的优先级高于隐式绑定,代码测试如下:

    // 显示绑定高于隐私绑定
    var obj = {
      name: 'zs',
      foo: function () {
        console.log(this)
      }
    }
    
    obj.foo() // obj
    obj.foo.call('aaa') // aaa
    obj.foo.apply('aaa') // aaa
    // 可以看到分别输出不同的结果,第二种同时存在显示和隐式绑定
    // 最后输出的结果是显示绑定的this.所以显示的优先级高于隐式的优先级
    
    // 那bind呢,我们来看第二个案例
    //结论:bind优先级高于隐式绑定
    function fun() {
      console.log(this)
    }
    var obj1 = {
      name: 'ls',
      foo: fun.bind('eee')
    }
    
    obj1.foo() // eee
    // 此时也是同时存在,输出依然是 eee ,所以显示高于隐式
    
  3. new 绑定高于隐式绑定,代码测试如下:

    var obj = {
      name: 'obj',
      foo: function () {
        console.log(this)
      }
    }
    
    var f = new obj.foo() // foo,
    // 通过测试发现打印的是 foo 这个函数对象,而不是 obj
    // 因此可以得出 new 大于 隐式
    
  4. new 绑定高于显示绑定,代码测试如下:

    // new 关键字不能和 apply/call 一起s使用
    // 因为两者都是主动调用一个函数
    // 所以比较只能比较 new 和 bind 之间谁的优先级更高
    function foo() {
      console.log(this)
    }
    
    // 先将 foo 使用 bind 绑定
    var bar = foo.bind('aaa')
    var fn = new bar()
    // 最后打印的是 foo 这个函数对象,而不是 aaa
    // 所以 new 绑定高于 显示绑定
    
    foo.bind('abc').call('vvv') // 输出abc ,所以bind 优先级高于 call
    
  5. 总结:new > 显示 > 隐式 > 默认

(掌握)this绑定规则之外-忽略显示绑定
function foo() {
  console.log(this)
}
foo.apply('abc')
foo.call({})
// 上述正常绑定到 abc 和 {}
// 但是如果传入的是 null 和 undefined
foo.apply(null)
foo.call(undefined)
var fn = foo.bind(null)
fn()
// 两者都指向 window
// apply/call/bind 当传入的是 null 或者 undefined 的时候
// 会自动绑定到 window
(掌握)this绑定规则之外-间接函数引用.js
var obj1 = {
  name: 'obj1',
  foo: function () {
    console.log(this)
  }
}

var obj2 = {
  name: 'obj2'
}
// obj2.bar = obj1.foo
// obj2.bar() // 正常的隐式绑定输出obj2
;(obj2.bar = obj1.foo)() // 指向就是window
// 所以通过这种间接的方式拿到函数调用,也属于独立调用
// 因为存在赋值表达式

;(obj2.bar = obj1.foo)() 
// 等价于
;(obj2.bar = obj1.foo) // 执行完毕返回一个bar 
bar() // 独立函数调用,指向window
(掌握)箭头函数 this 获取规则
  1. 箭头函数是 ES6 之后增加的一种编写函数的方法,并且它比函数表达式要更加简洁:

    1. 箭头函数不会绑定 this 和 arguments 属性
    2. 箭头函数不能作为构造函数来使用(不能和 new 一起来使用,会抛出错误)
  2. 之前学习的 this 四种绑定规则都不适用箭头函数(也就是不绑定this),而是根据外层作用域来决定 this,示例如下:

    // var uname = 'zs'
    
    // var foo = () => {
    //   console.log(this)
    // }
    
    // var obj = {
    //   foo: foo
    // }
    
    // foo()
    // obj.foo()
    // foo.call('aaa')
    // 会发现输出都 window,证明了是不绑定this的,所以回去上层作用域去找
    
    // 应用场景
    var obj = {
      data: [],
      getData: function () {
        // 假设这里发起网络请求,将请求的数据放入data属性中
        // setTimeout(function () {
        //   var result = ['123', '321']
        //   // 如果直接使用 tihs.data = result,就不正确了,这时候的 this 指向window
        //   // 之前的话需要在外层存储 this,var _that = this,然后在进行赋值
        // }, 2000)
        // 现在就可以使用箭头函数来完成
        setTimeout(() => {
          var result = ['123', '321']
          this.data = result
          console.log(this.data)
        }, 2000)
        // 因为箭头函数会去上层作用域查找this,obj.getData隐式绑定
        // 所以this就指向obj
      }
    }
    
    obj.getData()
    
    
this 面试题案例
(掌握)面试题
  1. 题1:

    var name = 'window'
    var person = {
      name: 'person',
      sayName: function () {
        console.log(this.name)
      }
    }
    function sayName() {
      var sss = person.sayName
      sss() // window
      person.sayName() // person
      ;(person.sayName)() // person 这里没有赋值表达式,所以取出来还是person.sayName()
      ;(b = person.sayName)() // window 赋值表达式,所以获取的函数赋值给b,b()
    }
    
    sayName()
    
  2. 题2:

    var name = 'window'
    var person1 = {
      name: 'person1',
      foo1: function () {
        console.log(this.name)
      },
      foo2: () => console.log(this.name),
      foo3: function () {
        return function () {
          console.log(this.name)
        }
      },
      foo4: function () {
        return () => {
          console.log(this.name)
        }
      }
    }
    
    var person2 = { name: 'person2' }
    
    person1.foo1() // person1 隐式绑定
    person1.foo1.call(person2) // person2 显示绑定高于隐式绑定
    
    person1.foo2() // window this不绑定作用域,输出window
    person1.foo2.call(person2) // window 不绑定this输出window
    
    person1.foo3()() // window
    /* 
      window 因为执行person返回的就是function () {console.log(this.name)},
      所以这里实际执行的是,返回的函数直接调用,是没有调用主题的
    */
    person1.foo3.call(person2)() // window
    /* 
      这里首先在person1.foo3隐式绑定的时候,直接被call覆盖了,所以指向是person2
      然后还是一样,返回一个独立函数在调用,也是输出window
    */
    person1.foo3().call(person2) // person2
    /* 
      这里person1.foo3()已经执行了,相当于直接将函数返回出来了,然后将这个返回
      的独立函数绑定到了person2,在执行就输出 person2
    */
    
    person1.foo4()() // person1
    /* 
      这里person1.foo4执行后就得到了一个箭头函数,而箭头函数是没有this的,他就要去
      上层作用域查找,而上层作用域的函数是有tihs,他的this是person1隐式调用的,所以这
      里输出 person1
    */
    person1.foo4.call(person2)() // person2
    /* 
      这里首先将作用域显绑到person2,然后this向上层查找this指向,就输出person2的name
    */
    person1.foo4().call(person2) // person1
    /* 
      这里person1.foo4()执行之后,上层作用域直接绑定person1,箭头函数里面的this存储的就是
      person1,所以在修改箭头函数指向就无效了
    */
    
  3. 题3:Tip:{}是代码块有作用域,而 var obj = {}中的{}是字面量语法,没有作用域

    var name = 'window'
    
    function Person(name) {
      console.log(this)
      this.name = name
      this.foo1 = function () {
        console.log(this.name)
      }
      this.foo2 = () => console.log(this.name)
      this.foo3 = function () {
        return function () {
          console.log(this.name)
        }
      }
      this.foo4 = function () {
        return () => {
          console.log(this.name)
        }
      }
    } 
    
    var person1 = new Person('person1')
    var person2 = new Person('person2')
    
    // person1.foo1() // person1
    // person1.foo1.call(person2) // person2
    
    // person1.foo2() // person1
    /* 
      这里是函数,不是对象,函数是有作用域的,所以上层作用域就是这个 person1 这个调用的函数
    */
    // person1.foo2.call(person2) //  person1
    /* 
      箭头函数不受四种绑定规则影响,person1调用foo2的时候,箭头函数上层作用域就已经确定了
    */
    
    person1.foo3()() // window
    /* 
      因为person1.foo3()已经返回了一个函数,这个函数在调用就是独立调用
    */
    person1.foo3.call(person2)() //  window
    /* 
      这和上面的原理一样,只是在调用的过程中,person1调用变成了person12,
      但是都独立函数调用,所以指向 window
    */
    person1.foo3().call(person2) // person2
    /* 
      person1.foo3()执行完毕后就返回一个函数,在通过 call 把函数的指向改成person2
    */
    
    person1.foo4()() // person1
    /* 
      person1.foo4()执行完毕返回一个箭头函数,此时箭头函数查找发现是person1在调用,
      所以输出 person1
    */
    person1.foo4.call(person2)() // person2
    /* 
      person1.foo4执行的就是在引用这个函数,但是没有执行,所以里面的箭头函数没有输出,
      然后指向被换成了 person2,因为call转变指向后会立即执行,所以等价于person2.foo4(),
      此时里面的箭头函数返回,发现上层作用域是 person2
    */
    person1.foo4().call(person2) // person1
    /* 
      person1.foo4()执行完毕直接将箭头函数的作用域改为了person1,上层作用域已经确定了,
      所以箭头函数不绑定 this,所以call无效
    */
    
  4. Tip:此时 foo 函数的上层作用域是全局

    var name = 'window'
    var obj = {
        name:'zs',
        foo: function(){
            console.log(name)
        }
    }
    
  5. 题4:

    var name = 'window'
    
    function Person(name) {
      this.name = name
      this.obj = {
        name: 'obj',
        foo1: function () {
          return function () {
            console.log(this.name)
          }
        },
        foo2: function () {
          return () => {
            console.log(this.name)
          }
        }
      }
    }
    var person1 = new Person('person1')
    var person2 = new Person('person2')
    
    person1.obj.foo1()() // window
    /* 
      person1.obj.foo1()调用之后这里就返回了一个 匿名函数,然后匿名函数独立调用,
      符合默认绑定规则,所以指向 window
    */
    person1.obj.foo1.call(person2)() // window
    /* 
      这里显示绑定大于隐式绑定,所以执行的时候,person1调用obj,obj调用foo1的时候被
      显示绑定转为了person2,但是依然是返回一个函数,然后独立调用
    */
    person1.obj.foo1().call(person2) // person2
    /* 
      这里先执行person1.obj.foo1(),返回一个匿名函数,然后使用 call 改变匿名函数的指向,
      所以指向 proson2,因此输出 person2
    */
    
    person1.obj.foo2()() // obj
    /* 
      这是在执行person1.obj.foo2()的时候,返回的是一个箭头函数,这个箭头函数是没有绑定this的,
      所以要去上层作用域里面查找,所以这里的this实际上绑定的是他上面的函数中的this,而函数中的this
      实际上绑定的是 这个obj,所以输出obj
    */
    person1.obj.foo2.call(person2)() // person2
    /* 
      这一步是在执行到调用 foo2的时候,这个普通匿名函数的指向,被call改成了指向 person2,所以
      箭头函数来到普通匿名函数这里的时候,查找到指向是 person2,所以输出 person2
    */
    person1.obj.foo2().call(person2) // obj
    /* 
      person1.obj.foo2()执行的时候,普通匿名函数中存储的this绑定的是 obj,同时箭头函数无法绑定
      this,所以后面的绑定规则无效,直接在obj内超找name
    */
    
call、apply、bind案例
/* 
  案例1
*/
function foo() {
  console.log('被调用了', this)
}

foo() // 可以直接调用
foo.call()
// 也可以通过 call 调用,可以使用 call 是因为函数原型上有一个 call 方法
foo.apply()
// 也可以通过 apply 调用
// 为什么都可以调用,还需要 call 和 apply 呢?因为 this 的绑定是不同的
// foo 直接调用时执行 window
var obj = {
  name: 'zs'
}
// 如果想要 foo 指向 obj,又不想在 obj 里面引用,不像用隐式绑定实现,如下:
// var obj = {
//   name: 'zs',
//   foo: foo
// }
// obj.foo() // 这样是可以调用,且指向 obj 的

// 但是我是要指向 obj 又不想使用使用上面这种方式呢,此时就可以使用 call 和 apply
foo.call(obj)
foo.apply(obj)

/* 
  案例2
*/
//call和apply在执行函数时,是可以明确的绑定this,这个绑定规则称之为显示绑定
function sum(num1, num2) {
  console.log(num1 + num2, this)
}
// 现在我们知道可以通过 call 和 apply改变指向
// 那如果需要参数应该怎么传入
// call 传入参数列表
sum.call('call', 1, 1)
// apply 传入数组
sum.apply('apply', [1, 2])
// 所以二者之间的区别只是在于传递参数的方式不一样

/* 
  案例3
*/
// function foo() {
//   console.log(this)
// }
// 通过bind修改 this 指向,会返回一个新的函数
// var newFoo = foo.bind('aaa')
// 通过这个返回新的函数,就算独立调用 newFoo 函数也不会指向 window
// 在 newFoo 调用之前,被 bind 显示的绑定了一个 aaa
// 所以以后 newFoo 调用的时候,会一直是指向 aaa
// newFoo()
// 就相当于默认绑定和显示绑定冲突了,两个绑定规则冲突
// 就会有一个优先级了,通过实例证明 显示绑定优先级大于默认绑定
// 所以后续调用就不需要每次都 newFoo.call('aaa'),比如:
/**
  newFoo.call('aaa')
  newFoo.call('aaa')  
  newFoo.call('aaa')
  或者
  newFoo.apply('aaa')
  newFoo.apply('aaa')
  newFoo.apply('aaa')
 */

// 传递参数
function foo(name, age) {
  console.log('我叫' + name + age + '岁了', this)
}

var newFoo = foo.bind('aaa')
newFoo('张三', '18') // 我叫张三18岁了 [String: 'aaa']

call-apply-bind的实现
  • 因为这些实现源码是 C++ 编写的,我们使用 JS 进行模拟实现
  • 我们实现为练习函数、this、调用关系,不会过渡考虑一个边界情况
  • 边界情况(edge case):比如一个vue框架实现后,更多的情况是考虑使用者传入的值时候合法,比如需要一个整数,用户输入一个字符串,就要进行判断
(掌握)call的实现
  1. 首先我们先看一下内置 call 的执行结果:

    function foo() {
      console.log('foo函数被执行', this)
    }
    
    function sum(num1, num2) {
      console.log(num1 + num2, 'sum函数被执行', this)
    }
    
    foo.call('aaa')
    foo.call('bbb', 1, 2)
    
    • 输出结果如图:

image-20221102010227863

  1. 那么我们应该如何实现这样一个 call 方法呢?

    1. 首先我们需要定义一个方法名,如:myCall,那如何才能让其余的函数可以使用这个方法呢

    2. 我们可以通过 Function的原型上进行绑定一个 myCall 方法,并将这个方法赋值一个函数,如下:

      Function.prototype.myCall = function () {}
      
    3. 接下来我们可以测试一个这个方法是有有效,测试如下:

      Function.prototype.myCall = function () {
          console.log('myCall被调用了')
      }
      
      function foo() {
        console.log('foo函数被执行')
      }
      
      function sum(num1, num2) {
        console.log('sum函数被执行')
      }
      
      foo.myCall()
      sum.myCall()
      
      • 输出结果如下:

        image-20221102011149808
    4. 现在可以实现基本调用的时候,我们就需要进一步实现,如果foo sun 或者更多的函数都在调用这个 myCall,应该如果判断是谁在调用这个 myCall 方法呢?我们可以发现 foo.myCall(),这种方式为隐式绑定,那我们函数内部的 this 就是指向这个调用者,所以可以通过 this 获取,具体实现如下:

      Function.prototype.myCall = function () {
          // 将 this 赋值,在进行调用
          var fn = this
           fn()
      }
      
      function foo() {
        console.log('foo函数被执行', this)
      }
      
      function sum(num1, num2) {
        console.log(num1 + num2, 'sum函数被执行', this)
      }
      
      foo.myCall()
      sum.myCall()
      
      • 输出结果如下:

        image-20221102011947432
    5. 现在可以看到可以分别执行对应的函数调用了,现在我们可以看看内置的 call 中第一个参数可以指定 this 对象,如果打印的信息是 console.log(‘foo函数被执行’, this),那这个地方的thsi就可以打印出用户传入的指定this对象,但是目前我们的myCall是一个 fn(),是独立函数调用,打印的是一个 window,那这个 指定 this 对象应该如何实现呢?我们可以在接收一个参数 thisArg(当然名字可以自己定义),用来接收用户传入的 this,具体实现如下:

      Function.prototype.myCall = function (thisArg) {
          //此时this还是指向foo,sum
        var fn = this
        //将this的指向赋值给一个函数fn
        thisArg.fn = fn
          //隐式调用fn函数,这时候this指向thisArg
        thisArg.fn()
        /*
      	此时在外面传入一个空对象就会打印出来,不过打印的结果或多一个fn,如:
         	foo函数被执行 {fn: ƒ},因为内置的call是c++实现的,我们使用js模拟会出现这个情况
         */
        // 可以在调用完成之后删除即可
        delete thisArg.fn
      }
      
      function foo() {
        console.log('foo函数被执行', this)
      }
      
      // 现在我们暂时先忽略传值的影响
      function sum(num1, num2) {
        console.log('sum函数被执行', this)
      }
      
      foo.myCall({})
      sum.myCall({})
      
      • 输出结果如下:

        image-20221102012622899
    6. 除了这个情况我们还需要考虑传入的参数,如果传入的是字符串,那不一个函数,又应该怎么办呢?比如传入一个字符串,就会报错 thisArg.fn is not a function,因为如果是数字或者字符串thisArg.fn = fn就会变成 bbb.fn = fn,但是字符串这样加属性是加不上去的,所以我们需要进行一个转化,具体实现如下:

      Function.prototype.myCall = function (thisArg) {
        var fn = this
        thisArg.fn = fn
        thisArg.fn()
      }
      
      function foo() {
        console.log('foo函数被执行', this)
      }
      
      // 现在我们暂时先忽略传值的影响
      function sum(num1, num2) {
        console.log('sum函数被执行', this)
      }
      
      foo.myCall({})
      sum.myCall(123)
      
      • 输出结果如下:

        image-20221102013736329

    7. 所以我们就需要对传入的值进行一个判断,和转化,如下:

      Function.prototype.myCall = function (thisArg) {
        var fn = this
        // 但是不仅会是数字,也有可能是字符串或者Boolean,所以可以使用 Object()进行转化
        thisArg = Object(thisArg)
        thisArg.fn = fn
        thisArg.fn()
      }
      
      function foo() {
        console.log('foo函数被执行', this)
      }
      
      // 现在我们暂时先忽略传值的影响
      function sum(num1, num2) {
        console.log('sum函数被执行', this)
      }
      
      foo.myCall('aaa')
      sum.myCall(123)
      
      • 输出结果如下:

        image-20221102013901833
    8. 此时还是存在一些小问题,如果我们传入 null 或者 undefined 内置的会执行 window,而我们的会指向一个空对象,那应该如何避免呢?实现如下:

      Function.prototype.myCall = function (thisArg) {
        var fn = this
        // 如果传入的是 null 和 undefined 类型会转为一个空对象,那如果传入的是这两个,我们
        // 不希望是 空对象,而是一个 window
        // 如果有值转为 对象,没有转为 window
        //同时还要防止将this指向给0
        thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window
        thisArg.fn = fn
        thisArg.fn()
      }
      
      function foo() {
        console.log('foo函数被执行', this)
      }
      
      // 现在我们暂时先忽略传值的影响
      function sum(num1, num2) {
        console.log('sum函数被执行', this)
      }
      
      foo.myCall(undefined)
      sum.myCall(null)
      
      • 输出结果如下:

        image-20221102014335001

    9. 此时我们的 myCall方法指向 传入的this对象就差不多了,现在再来看看如何接收参数,我们可以用到一个 restParameters(剩余参数),以及展开运算符?

      // 如果接收的参数个数不确定的话,就无法正常使用形参
      // restParameters 是一个语法,既然是形参,自然名字是可以自己定义的
      /* 
        ...nums 是什么?会将传入的参数按照数组的形式存储
        存入一个参数就是 var nums = [10]
        存入两个参数就是 var nums = [10, 20]
        ...以此内推
      */
      function sum(...nums) {
        console.log(nums)
      }
      
      // 开始测试
      sum(10)
      sum(10, 20)
      sum(10, 20, 30)
      
      
      // 展开运算符 spread
      var names = ['aaa', 'bbb', 'ccc']
      console.log(...names)
      // 展开运算符会将里面的数组遍历,并直接取出
      function foo(name1, name2, name3) {
        console.log(name1 + name2 + name3) // aaabbbccc
      }
      
      foo(...names)
      
    10. 所以就可以根据上述的剩余参数和展开运算符进行实现

    Function.prototype.myCall = function (thisArg, ...args) {
    var fn = this
    thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window
    thisArg.fn = fn
    thisArg.fn(...args)
    }
    
    function foo() {
    console.log('foo函数被执行', this)
    }
    
    // 现在我们暂时先忽略传值的影响
    function sum(num1, num2) {
    console.log('sum函数被执行', this, num1, num2)
    }
    
    foo.myCall(undefined)
    // 这里传入参数测试
    sum.myCall('bbb', 1, 2)
    
    • 执行结果如下:

    image-20221102215558447

    1. 我们现在在对比一下内置的 call 和 自定义的 myCall 有什么区别:

      Function.prototype.myCall = function (thisArg, ...args) {
        var fn = this
        thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window
        thisArg.fn = fn
        thisArg.fn(...args)
      }
      
      function foo() {
        console.log('foo函数被执行', this)
      }
      
      // 现在我们暂时先忽略传值的影响
      function sum(num1, num2) {
        // console.log('sum函数被执行', this, num1, num2)
        return num1 + num2
      }
      
      // 这里使用系统内置的call进行调用
      var result = sum.call('bbb', 1, 2)
      console.log('内置的call调用:' + result)
      
      var result = sum.myCall('bbb', 1, 2)
      console.log('自定义的myCall调用:' + result)
      
      • 获取输出结果如下:可以看到我们定义的 myCall 没有返回值

        image-20221102221147324
    2. 那应该怎么实现呢?

      Function.prototype.myCall = function (thisArg, ...args) {
        var fn = this
        // thisArg = thisArg ? Object(thisArg) : window
        // 这里需要精准判断一下,不然传入 0 会指向 window
        thisArg = thisArg !== null && this !== undefined ? Object(thisArg) : window
        thisArg.fn = fn
        var result = result = thisArgs.fn(...args)
        delete thisArgs.fn
        return result
      }
      
      function foo() {
        console.log('foo函数被执行', this)
      }
      
      // 现在我们暂时先忽略传值的影响
      function sum(num1, num2) {
        // console.log('sum函数被执行', this, num1, num2)
        return num1 + num2
      }
      
      // 这里使用系统内置的call进行调用
      var result = sum.call('bbb', 1, 2)
      console.log('内置的call调用:' + result)
      
      var result = sum.myCall('bbb', 1, 2)
      console.log('自定义的myCall调用:' + result)
      
      • 获取输出结果如下:

        image-20221102221351769
(掌握)apply的实现
  1. 第一步先在原型上面添加 myApply

    // 还是一样在原型上面添加自己的 myApply 方法
    Function.prototype.myApply = function () {
      var fn = this
      fn()
    }
    
    function foo() {
      console.log('foo函数被调用')
    }
    
    function sum() {
      console.log('sum函数被调用')
    }
    
    foo.myApply()
    sum.myApply()
    
    • 输出如图:

      image-20221102222136498
  2. 第二步实现输入指定的this,以及判断输入的参数类型

    // 还是一样在原型上面添加自己的 myApply 方法
    Function.prototype.myApply = function (thisArgs) {
      var fn = this
      // 判断参数是否为null或undefined,是转为window,不是则转为对象
      // 这里需要精准判断一下,不然传入 0 会指向 window
      thisArgs = (thisArg !== null && thisArg !== undefined) ? Object(thisArgs) : window
      thisArgs.fn = fn
      thisArgs.fn()
    }
    
    function foo() {
      console.log('foo函数被调用', this)
    }
    
    function sum() {
      console.log('sum函数被调用', this)
    }
    
    foo.myApply(null)
    sum.myApply('abc')
    
    • 获取结果如图:

      image-20221102222537579

  3. 再实现一下参数的传递:

    // 还是一样在原型上面添加自己的 myApply 方法
    Function.prototype.myApply = function (thisArgs, args) {
     // 这里为什么不使用剩余参数了呢,因为展开运算符会把参数列表转为数组,不符合apply必须传入数组的规则
      var fn = this
      // 判断参数是否为null或undefined,是转为window,不是则转为对象
      thisArgs = (thisArg !== null && thisArg !== undefined) ? Object(thisArgs) : window
      thisArgs.fn = fn
      // 需要注意这里,如果我们没有传参的话,那就会是一个undefined,undefined是不能
      // 被迭代的,所以会报错,所以需要判断校验一下
      // 如果不是,就把这个数组使用展开运算符
      var result
      if (!args) {
        result = thisArgs.fn()
      } else {
        result = thisArgs.fn(...args)
      }
      // 当然if判断也可以使用 三元简化或者 使用 || 运算 args = args || []
      delete thisArgs.fn
      return result
    }
    
    function foo() {
      console.log('foo函数被调用', this)
    }
    
    function sum(num1, num2) {
      console.log('sum函数被调用', this, num1, num2)
      return num1 + num2
    }
    
    foo.myApply(null)
    var res = sum.myApply('abc', [1, 2])
    console.log(res)
    
    • 获取结果如图:

      image-20221102224253095

(掌握)bind的实现
  1. 先来看一下内置的bind如何传参

    function foo() {
      console.log('foo被执行', this)
    }
    
    function sum(num1, num2, num3, num4) {
      console.log(num1, num2, num3, num4)
    }
    
    // 自带的bind参数传参
    var bar = sum.bind('aaa', 10, 20, 30, 40)
    bar()
    
    var newBar = sum.bind('aaa')
    bar(10, 20, 30, 40)
    
    // 也可以选择分开发送
    var newBar = sum.bind('aaa', 10, 20)
    bar(30, 40)
    
    • 获取输出结果如下:

      image-20221102234554111
  2. 实现自定义 myBind 方法和其传参

    Function.prototype.myBind = function (thisArg, ...args1) {
      // 获取真实需要调用的函数
      var fn = this
      thisArg = thisArg !== null && thisArg !== undefined ? Object(thisArg) : window
      // bind 需要返回一个函数
      // 同时这里越需要形参 ...args,这样可以接收分开传送的参数
      function proxFn(...args2) {
        thisArg.fn = fn
        var findArgs = [...args1, ...args2]
        var result = thisArg.fn(...findArgs)
        delete thisArg.fn
        return result
      }
    
      return proxFn
    }
    
    function foo() {
      console.log('foo被执行', this)
    }
    
    function sum(num1, num2, num3, num4) {
      console.log(this, num1, num2, num3, num4)
      return 20
    }
    
    // 使用自己定义的 bind
    var bar = sum.myBind('abc', 10, 20, 30)
    var res = bar(40)
    console.log(res)
    
    • 输出结果如下:

      image-20221103001327292
(掌握)浅谈一下关闭 Object()
  • 可能对于刚刚使用的 Object() 进行转化,有一些陌生,这里就简单介绍一下:

    1. 在 JavaScript 中,几乎所有的对象都是 Object 的实例,它会从 Object.prototype继承属性

    2. Object 是 JavaScript 的一个内置对象,他是一个构造函数,但是也可以用作普通的函数

  1. 构造函数:Object 作为构造函数,可以使用 new 一个关键字来生成一个新的对象

    • Object 可以接收一个参数:
      1. 若参数为 null 或 undefined,则返回一个空对象
      2. 若参数是一个对象,则返回这个对象
      3. 若参数是一个原始类型值,则返回该值的包装对象
  2. 普通函数:Object 作为普通函数,他的作用是将任意值转为对象,处理语义和构造函数不同,其效果是一样的。

split

image-20230219174228260

例子 1

在本例中,我们将按照不同的方式来分割字符串:

<script type="text/javascript">

var str="How are you doing today?"

document.write(str.split(" ") + "<br />")
document.write(str.split("") + "<br />")
document.write(str.split(" ",3))

</script>

输出:

How,are,you,doing,today?
H,o,w, ,a,r,e, ,y,o,u, ,d,o,i,n,g, ,t,o,d,a,y,?
How,are,you

例子 2

在本例中,我们将分割结构更为复杂的字符串:

"2:3:4:5".split(":")	//将返回["2", "3", "4", "5"]
"|a|b|c".split("|")	//将返回["", "a", "b", "c"]

slice 从索引start开始(包括start)至end(不包括end)

image-20230219175005394

例子 1

在本例中,我们将提取从位置 6 开始的所有字符:

<script type="text/javascript">

var str="Hello happy world!"
document.write(str.slice(6))

</script>

输出:

happy world!

例子 2

在本例中,我们将提取从位置 6 到位置 11 的所有字符:

<script type="text/javascript">

var str="Hello happy world!"
document.write(str.slice(6,11))

</script>

输出:

happy

substr

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ff0RpPBS-1683379078124)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230217142457838.png)]

例子 1

在本例中,我们将使用 substr() 从字符串中提取一些字符:

<script type="text/javascript">

var str="Hello world!"
document.write(str.substr(3))

</script>

输出:

lo world!

例子 2

在本例中,我们将使用 substr() 从字符串中提取一些字符:

<script type="text/javascript">

var str="Hello world!"
document.write(str.substr(3,7))

</script>

输出:

lo worl

parseInt

image-20230219195520362

parseFloat

image-20230219200310937

image-20230219200421739

小程序笔记

登录逻辑以及实施

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PXFDdhe6-1683379078125)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230308202253669.png)]

后台播放
const audioContext = wx.getBackgroundAudioManager()
//需要设置后台播放的歌名
audioContext.title = res.songs[0].name
顺序播放、随机播放、单曲循环
changeNewMusicAction(ctx, isNext = true) {
      // 1.获取当前索引
      let index = ctx.playListIndex

      // 2.根据不同的播放模式, 获取下一首歌的索引
      switch(ctx.playModeIndex) {
        case 0: // 顺序播放
          index = isNext ? index + 1: index -1
          if (index === -1) index = ctx.playListSongs.length - 1
          if (index === ctx.playListSongs.length) index = 0
          break
        case 1: // 单曲循环
          break
        case 2: // 随机播放
          index = Math.floor(Math.random() * ctx.playListSongs.length)
          break
      }

      console.log(index)

      // 3.获取歌曲
      let currentSong = ctx.playListSongs[index]
      if (!currentSong) {
        currentSong = ctx.currentSong
      } else {
        // 记录最新的索引
        ctx.playListIndex = index
      }

      // 4.播放新的歌曲
      this.dispatch("playMusicWithSongIdAction", { id: currentSong.id, isRefresh: true })
    }
var,let,const三者的特点和区别
https://blog.csdn.net/xiewenhui111/article/details/113133330
歌词滚动
<swiper-item class="lyric">
		<scroll-view class="lyric-list" scroll-y scroll-top="{{lyricScrollTop}}" scroll-with-animation>
			<block wx:for="{{lyricInfos}}" wx:key="index">
				<view class="item {{currentLyricIndex === index ? 'active': ''}}" style="padding-top: {{index === 0 ? (contentHeight/2-80): 0}}px; padding-bottom: {{index === lyricInfos.length - 1 ? (contentHeight/2+80): 0}}px;">
					{{item.text}}
				</view>
			</block>
		</scroll-view>
</swiper-item>
目标歌词展示
//获取当前时间
			const currentTime = audioContext.currentTime * 1000
			//根据当前时间修改currentTime/sliderValue
			if(!this.data.isSliderChanging){	
				const sliderValue = currentTime / this.data.durationTime * 100
				this.setData({sliderValue,currentTime})
			}
			//根据当前时间去查找播放的歌词
			let i = 0
			for (; i < this.data.lyricInfos.length; i++) {
				const lyricInfo = this.data.lyricInfos[i]
				if (currentTime < lyricInfo.time) {
					// 设置当前歌词的索引和内容
					//此处i为上面循环结束后拿到的后一句的i,要i-1才是当前的
					const currentIndex = i - 1
					if (this.data.currentLyricIndex !== currentIndex) {
						const currentLyricInfo = this.data.lyricInfos[currentIndex]
						console.log(currentLyricInfo.text);
						this.setData({ currentLyricText: currentLyricInfo.text, currentLyricIndex: currentIndex })
					}
					break
				}
			}
歌词转换格式化
法一:
// 正则(regular)表达式(expression): 字符串匹配利器

// [00:58.65]  \是对[]和.转义
const timeRegExp = /\[(\d{2}):(\d{2})\.(\d{2,3})\]/

export function parseLyric(lyricString) {
  const lyricStrings = lyricString.split("\n")

  const lyricInfos = []
  for (const lineString of lyricStrings) {
    // [00:58.65]他们说 要缝好你的伤 没有人爱小丑
    const timeResult = timeRegExp.exec(lineString)
    if (!timeResult) continue
    // 1.获取时间
    const minute = timeResult[1] * 60 * 1000
    const second = timeResult[2] * 1000
    const millsecondTime = timeResult[3]
    const millsecond = millsecondTime.length === 2 ? millsecondTime * 10: millsecondTime * 1
    const time = minute + second + millsecond

    // 2.获取歌词文
    const text = lineString.replace(timeRegExp, "")
    lyricInfos.push({ time, text })
  }
  return lyricInfos
}

法二:
// 实现歌词滚动
    lyricScroll(currentLyric) {
      let placeholderHeight = 0;
      // 获取歌词item
      let lyricsArr = document.querySelectorAll(".lyricsItem");
      // 获取歌词框
      let lyrics = document.querySelector(".lyrics");
      // console.log(lyrics.offsetTop)//123
      // console.log(lyricsArr[0].offsetTop)//123
      // placeholder的高度
      if (placeholderHeight == 0) {
        placeholderHeight = lyricsArr[0].offsetTop - lyrics.offsetTop;//123-123
        // console.log(placeholderHeight)//0
      }
      //   歌词item在歌词框的高度 = 歌词框的offsetTop - 歌词item的offsetTop
        // console.log(currentLyric);//歌词索引
        // console.log(lyricsArr[currentLyric - 1])//歌词第一句打印的是全部歌词,后面打印的是上一句歌词的div
      if (lyricsArr[currentLyric - 1]) {
        let distance = lyricsArr[currentLyric - 1].offsetTop - lyrics.offsetTop;
        // console.log(lyricsArr[currentLyric - 1].offsetTop)
        // console.log(lyrics.offsetTop)//123
        // console.log(distance)
        //   lyricsArr[currentLyric].scrollIntoView();
        lyrics.scrollTo({
          behavior: "smooth",
          top: distance - placeholderHeight,
        });
      }
    }
法三:
/**
 * 解析歌词字符串
 * 得到一个歌词对象的数组
 * 每个歌词对象:
 * {time:开始时间, words: 歌词内容}
 */
function parseLrc() {
  var lines = lrc.split('\n');
  var result = []; // 歌词对象数组
  for (var i = 0; i < lines.length; i++) {
    var str = lines[i];
    var parts = str.split(']');
    var timeStr = parts[0].substring(1);
    var obj = {
      time: parseTime(timeStr),
      words: parts[1],
    };
    result.push(obj);
  }
  return result;
}

/**
 * 将一个时间字符串解析为数字(秒)
 * @param {String} timeStr 时间字符串
 * @returns
 */
function parseTime(timeStr) {
  var parts = timeStr.split(':');
  return +parts[0] * 60 + +parts[1];
}

var lrcData = parseLrc();

// 获取需要的 dom
var doms = {
  audio: document.querySelector('audio'),
  ul: document.querySelector('.container ul'),
  container: document.querySelector('.container'),
};

/**
 * 计算出,在当前播放器播放到第几秒的情况下
 * lrcData数组中,应该高亮显示的歌词下标
 * 如果没有任何一句歌词需要显示,则得到-1
 */
function findIndex() {
  // 播放器当前时间
  var curTime = doms.audio.currentTime;
  for (var i = 0; i < lrcData.length; i++) {
    if (curTime < lrcData[i].time) {
      return i - 1;
    }
  }
  // 找遍了都没找到(说明播放到最后一句)
  return lrcData.length - 1;
}

// 界面

/**
 * 创建歌词元素 li
 */
function createLrcElements() {
  var frag = document.createDocumentFragment(); // 文档片段,这是在进行优化,直接操作dom树比parseHTML效率高
  for (var i = 0; i < lrcData.length; i++) {
    var li = document.createElement('li');
    li.textContent = lrcData[i].words;
    frag.appendChild(li); // 改动了 dom 树
  }
  doms.ul.appendChild(frag);
}

createLrcElements();

// 容器高度
var containerHeight = doms.container.clientHeight;
// 每个 li 的高度
var liHeight = doms.ul.children[0].clientHeight;
// 最大偏移量
var maxOffset = doms.ul.clientHeight - containerHeight;
/**
 * 设置 ul 元素的偏移量
 */
function setOffset() {
  var index = findIndex();
  var offset = liHeight * index + liHeight / 2 - containerHeight / 2;
  if (offset < 0) {
    offset = 0;
  }
  if (offset > maxOffset) {
    offset = maxOffset;
  }
  doms.ul.style.transform = `translateY(-${offset}px)`;
  // 去掉之前的 active 样式
  var li = doms.ul.querySelector('.active');
  if (li) {
    li.classList.remove('active');
  }

  li = doms.ul.children[index];
  if (li) {
    li.classList.add('active');//classList是元素的类样式列表,如果元素类样式过多,就不适合用li.className('active')
  }
}
//时间变化的函数
doms.audio.addEventListener('timeupdate', setOffset);

音乐播放器
// pages/music-player/index.js
import {getSongDetail,getSongLyric} from '../../services/api_player'
import {audioContext} from '../../store/player-store'
import {parseLyric} from '../../utils/parse-lyric'
Page({
	data: {
		id:0,
		currentSong:{},
		currentPage:0,
		contentHeight:0,
		//显示歌词
		isMusicLyric:true,
		//总时长
		durationTime:0,
		//当前时间
		currentTime:0,
		//滑动到的时间(百分比)
		sliderValue:0,
		//是否在滑动
		isSliderChanging:false,
		//歌词
		lyricInfos:[],
		//当前播放歌词
		currentLyricText:"",
		//当前播放歌词索引
		currentLyricIndex:0,
		//要滚动的距离
		lyricScrollTop:0
	},

	onLoad(options) {
		const id = options.id
		this.setData({id})
		this.getPageData(id)
		//动态计算高度,宽度
		const screenHeight = getApp().globalData.screenHeight
		const statusBarHeight = getApp().globalData.statusBarHeight
		const navBarHeight = getApp().globalData.navBarHeight
		const contentHeight = screenHeight - statusBarHeight - navBarHeight
		const deviceRadio = getApp().globalData.deviceRadio
		this.setData({contentHeight,isMusicLyric:deviceRadio >= 2})
		//创建播放器
		audioContext.stop()
		audioContext.src = `https://music.163.com/song/media/outer/url?id=${id}.mp3`
		audioContext.autoplay = true
		//audioContext事件监听
		this.setupAudioContextListener()
	},
// ========================   网络请求   ======================== 
	getPageData(id){
		getSongDetail(id).then(res => {
			this.setData({currentSong:res.songs[0],durationTime: res.songs[0].dt})
		})
		getSongLyric(id).then(res => {
			const lyricString = res.lrc.lyric
			const lyric = parseLyric(lyricString)
			this.setData({lyricInfos:lyric})
		})
	},
// ========================   事件处理   ======================== 
	handleSwiperChange(event){
		const current = event.detail.current
		this.setData({currentPage:current})
	},
	handleSliderChange(event){
		// 1.获取slider变化值(百分比)
		const value = event.detail.value
		// 2.计算需要播放的currentTime
		const currentTime = this.data.durationTime * value / 100
		// 3.设置context播放currentTime位置的音乐
		audioContext.pause()
		audioContext.seek(currentTime / 1000)
		// 4.记录最新的sliderValue, 并且需要讲isSliderChaning设置回false
		this.setData({ sliderValue: value, isSliderChanging: false })
	},
	handleSliderChanging(event){
		const value = event.detail.value
    	const currentTime = this.data.durationTime * value / 100
    	this.setData({ isSliderChanging: true, currentTime})
	},
	//事件监听
	setupAudioContextListener(){
		audioContext.onCanplay(() => {
			audioContext.play()
		})
		audioContext.onTimeUpdate(() => {
			//获取当前时间
			const currentTime = audioContext.currentTime * 1000
			//根据当前时间修改currentTime/sliderValue
			if(!this.data.isSliderChanging){	
				const sliderValue = currentTime / this.data.durationTime * 100
				this.setData({sliderValue,currentTime})
			}
			//根据当前时间去查找播放的歌词
			let i = 0
			for (; i < this.data.lyricInfos.length; i++) {
				const lyricInfo = this.data.lyricInfos[i]
				if (currentTime < lyricInfo.time) {
					// 设置当前歌词的索引和内容
					//此处i为上面循环结束后拿到的后一句的i,要i-1才是当前的
					const currentIndex = i - 1
					if (this.data.currentLyricIndex !== currentIndex) {
						const currentLyricInfo = this.data.lyricInfos[currentIndex]
						this.setData({ currentLyricText: currentLyricInfo.text, currentLyricIndex: currentIndex ,lyricScrollTop:currentIndex * 35})
					}
					break
				}
			}
		})
	}
})
image mode的属性
mode 有效值:

mode 有 13 种模式,其中 4 种是缩放模式,9 种是裁剪模式。

模式 值 说明
缩放 scaleToFill 不保持纵横比缩放图片,使图片的宽高完全拉伸至填满 image 元素
缩放 aspectFit 保持纵横比缩放图片,使图片的长边能完全显示出来。也就是说,可以完整地将图片显示出来。
缩放 aspectFill 保持纵横比缩放图片,只保证图片的短边能完全显示出来。也就是说,图片通常只在水平或垂直方向是完整的,另一个方向将会发生截取。
缩放 widthFix 宽度不变,高度自动变化,保持原图宽高比不变
裁剪 top 不缩放图片,只显示图片的顶部区域
裁剪 bottom 不缩放图片,只显示图片的底部区域
裁剪 center 不缩放图片,只显示图片的中间区域
裁剪 left 不缩放图片,只显示图片的左边区域
裁剪 right 不缩放图片,只显示图片的右边区域
裁剪 top left 不缩放图片,只显示图片的左上边区域
裁剪 top right 不缩放图片,只显示图片的右上边区域
裁剪 bottom left 不缩放图片,只显示图片的左下边区域
裁剪 bottom right 不缩放图片,只显示图片的右下边区域
使用多个插槽,要设置
//想使用多个插槽,要设置
Component({
	 options: {
  		multipleSlots: true
 	},
})
搜索关键字的高亮以及rich-text的使用
****string2nodes.js

export default function stringToNodes(keyword, value) {
	const nodes = []
	if (keyword.toUpperCase().startsWith(value.toUpperCase())) {
	  const key1 = keyword.slice(0, value.length)
	  const node1 = {
		name: "span",
		attrs: { style: "color: #26ce8a; font-size: 14px;" },
		children: [ { type: "text", text: key1 } ]
	  }
	  nodes.push(node1)
  
	  const key2 = keyword.slice(value.length)
	  const node2 = {
		name: "span",
		attrs: { style: "color: #000000; font-size: 14px;" },
		children: [ { type: "text", text: key2 } ]
	  }
	  nodes.push(node2)
	} else {
	  const node = {
		name: "span",
		attrs: { style: "color: #000000; font-size: 14px;" },
		children: [ { type: "text", text: keyword } ]
	  } 
	  nodes.push(node)
	}
	return nodes
  }
  
  ****组件内
  import stringToNodes from '../../utils/string2nodes'
  
  handleSearch(event){
		//获取输入的关键字
		const searchValue = event.detail
		this.setData({searchValue})
		if (!searchValue.length){
			this.setData({suggestSongs:[]})
			return
		} 
		debounceGetSearchSuggest(searchValue).then(res => {
			const suggestSongs = res.result.allMatch || []
			this.setData({suggestSongs})
			// 转成nodes节点
			const suggestKeywords = suggestSongs.map(item => item.keyword)
			const suggestSongsNodes = []
			for( const item of suggestKeywords){
				const nodes = stringToNodes(item,searchValue)
				suggestSongsNodes.push(nodes)
			}
			this.setData({suggestSongsNodes})
		})
	}
防抖使用(搜索框)
**组件内
import debounce from '../../utils/debounce'
const debounceGetSearchSuggest = debounce(getSearchSuggest,600)

debounceGetSearchSuggest(searchValue).then(res => {
			this.setData({suggestSongs:res.result.allMatch})
})

**debounce.js
export default function debounce(fn, delay = 500, immediate = false, resultCallback) {
	// 1.定义一个定时器, 保存上一次的定时器
	let timer = null
	let isInvoke = false
  
	// 2.真正执行的函数
	//...args是searchValue,输入的文字
	const _debounce = function(...args) {
	  return new Promise((resolve, reject) => {
		// 取消上一次的定时器
		if (timer) clearTimeout(timer)
  
		// 判断是否需要立即执行
		if (immediate && !isInvoke) {
		  const result = fn.apply(this, args)
		  if (resultCallback) resultCallback(result)
		  resolve(result)
		  isInvoke = true
		} else {
		  // 延迟执行
		  timer = setTimeout(() => {
			// 外部传入的真正要执行的函数
			const result = fn.apply(this, args)
			if (resultCallback) resultCallback(result)
			resolve(result)
			isInvoke = false
			timer = null
		  }, delay)
		}
	  })
	}
  
	// 封装取消功能
	_debounce.cancel = function() {
	  console.log(timer)
	  if (timer) clearTimeout(timer)
	  timer = null
	  isInvoke = false
	}
  
	return _debounce
  }
export和export default的区别

export和export default的区别 - 知乎 (zhihu.com)

data-使用以及动态key数组
	<block>
      <ranking-area-item item="{{originalRankingsongs}}" bindtap="handleMoreClickBtn" data-id="1"></ranking-area-item>
      <ranking-area-item item="{{newRankingsongs}}" bindtap="handleMoreClickBtn" data-id="2"></ranking-area-item>
      <ranking-area-item item="{{soarRankingsongs}}" bindtap="handleMoreClickBtn" data-id="3"></ranking-area-item>
    </block>
    
    
    **data-id  id为自定义名称,为下面事件event参数中添加一个id属性,rankingMap[id]动态id获取rankingMap数组中的value
    
    //排行榜点击事件
	handleMoreClickBtn(event){
		const rankingMap = {1:"originalRanking",2:"newRanking",3:"soarRanking"}
		const id = event.currentTarget.dataset.id
		const rankingName = rankingMap[id]
		this.navigateToDeatail(rankingName)
	}
引入hy-event-store,达到vuex效果
cnpm i hy-event-store

**index.js
import {rankingStore} from './ranking-store'
export {rankingStore}

**ranking-store.js
import {
	HYEventStore
} from 'hy-event-store'
import {
	getRankings
} from '../services/api_music'
const rankingStore = new HYEventStore({
	state: {
		hotRanking: {}
	},
	actions: {
		getRankingDataAction(ctx) {
			getRankings(3778678).then((res) => {
				console.log(res);
				ctx.hotRanking = res.playlist
			})
		}
	}
})
export {
	rankingStore
}

**组件
import {rankingStore} from '../../store/index'

onLoad(options) {
		this.getPageData()
		// 获取推荐音乐数据
		rankingStore.dispatch('getRankingDataAction')
		// 从store中获取数据
		rankingStore.onState("hotRanking",(res) =>{
			//刚开始的hotRanking为空对象
			if(!res.tracks) return
			const recommendSongs = res.tracks.slice(0,7)
			this.setData({recommendSongs})
		})
	},
小程序解决插槽动态显示方案

image-20230227104038541

  **header.wxss
  
  .header .slot:empty + .default {
	display: flex;
  }
  .header .default {
	display: none;
	align-items: center;
	font-size: 28rpx;
	color: #777;
  }
  **header.wxml
  
  <!--components/area-header/index.wxml-->
<view class="header">
  <view class="title">{{title}}</view>
  <view class="right" wx:if="{{showRight}}" bindtap="handleRightClick">
    <view class="slot"><slot></slot></view>
    <view class="default">
      <text>{{rightText}}</text>
      <image class="icon" src="/assets/images/icons/arrow-right.png"></image>
    </view>
  </view>
</view>
**home.wxml

<!-- 推荐歌曲 -->
<view class="recommend-song">
	<header title="推荐歌曲"></header>
</view>
setdata(同步是不那么好的)

image-20230227094106634

节流(规定时间内只能调用一次,普攻)
**throttle.js

export default function throttle(fn, interval = 1000, options = { leading: true, trailing: false }) {

  // 1.记录上一次的开始时间

  const { leading, trailing, resultCallback } = options

  let lastTime = 0

  let timer = null

 

  // 2.事件触发时, 真正执行的函数

  const _throttle = function(...args) {

   return new Promise((resolve, reject) => {

​    // 2.1.获取当前事件触发时的时间

​    const nowTime = new Date().getTime()

​    if (!lastTime && !leading) lastTime = nowTime

 

​    // 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数

​    const remainTime = interval - (nowTime - lastTime)

​    if (remainTime <= 0) {

​     if (timer) {

​      clearTimeout(timer)

​      timer = null

​     }

 

​     // 2.3.真正触发函数

​     const result = fn.apply(this, args)

​     if (resultCallback) resultCallback(result)

​     resolve(result)

​     // 2.4.保留上次触发的时间

​     lastTime = nowTime

​     return

​    }

 

​    if (trailing && !timer) {

​     timer = setTimeout(() => {

​      timer = null

​      lastTime = !leading ? 0: new Date().getTime()

​      const result = fn.apply(this, args)

​      if (resultCallback) resultCallback(result)

​      resolve(result)

​     }, remainTime)

​    }

   })

  }

 

  _throttle.cancel = function() {

   if(timer) clearTimeout(timer)

   timer = null

   lastTime = 0

  }
  return _throttle
 }
 
 
组件引用:

import throttle from '../../utils/throttle'
const throttleQueryRect = throttle(queryRect)

//动态计算swiper高度,防止手机不同样式不同
	handleSwiperHeight() {
		throttleQueryRect(".swiper-image").then(res =>{
			const rect = res[0]
			this.setData({swiperHeight:rect.height})
		})
}
导入vant weapp
npm init -y
cnpm i @vant/weapp@1.3.3 -S--production
将 app.json 中的 "style": "v2" 去除,小程序的新版基础组件强行加上了许多样式,难以覆盖,不关闭将造成部分组件样式混乱。
{
	"usingComponents": {
		"van-search": "@vant/weapp/search/index"
	}
}

打开微信开发者工具,点击 工具 -> 构建 npm,并勾选 使用 npm 模块 选项,构建完成后,即可引入组件。
动态计算swiper高度,防止手机不同样式不同
<swiper-item class="swiper-item">
			<image src="{{item.pic}}" mode="widthFix" class="swiper-image" bindload="handleSwiperHeight"/>
</swiper-item>
  
  
  swiperHeight:0
  
  handleSwiperHeight() {

​    //获取图片的高度

​    const query = wx.createSelectorQuery()

​    query.select('.swiper-image').boundingClientRect()

​    query.exec((res) =>{

​      const rect = res[0]

​      this.setData({swiperHeight:rect.height})

​    })

  }
二次封装接口

image-20230225223116952

image-20230225223018460

image-20230225224553962

(.wxs)日期和数量格式化
function formatCount(count) {
	var counter = parseInt(count)
	if (counter>100000000) {
		return (counter/100000000).toFixed(1) + '亿'
	}else if (counter>10000) {
		return (counter/10000).toFixed(1) + '万'
	}else{
		return counter + ''
	}
}
function addZero(time) {
	time = time + ''
	return ('00' + time).slice(time.length)
}
function formatDuration(time) {
	time = time/1000
	var minut = Math.floor(time/60)
	var second = Math.floor(time) % 60
	return addZero(minut) + ':' + addZero(second)
}
// commonjs
module .exports={
	formatCount:formatCount,
	formatDuration:formatDuration
}
<wxs src="../../utils/format.wxs" module="format"/>

<view class="count">
​     {{format.formatCount(item.playCount)}}
</view>
组件抽离

image-20230226104344554

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iJJcQS4e-1683379078128)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230226104407306.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-17leKaPc-1683379078128)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230226104439901.png)]

data-xxx的使用
data-xx 的作用是在事件中可以获取这些自定义的节点数据,用于事件的逻辑处理

比如 写一个list列表 想知道点击的list列表的那一个item ,比如获取点击的图片,等等

使用data-xx 需要注意的 xx 是自己取的名字, 后面跟着的渲染一定要是使用的值,否则无效

比如点击的是list 后面跟的是list的值,如果是图片后面就要是图片url的地址,
————————————————
版权声明:本文为CSDN博主「胡小牧」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_33210042/article/details/91983464

<view wx:for="{{topMvs}}" wx:key="id" class="item">
		<video-item-v1 item='{{item}}' bindtap="VideoItemBtn" data-item="{{item}}"></video-item-v1>
</view>


VideoItemBtn(event){
		const id = event.currentTarget.dataset.item.id
		//页面跳转
		wx.navigateTo({
		  url: `/pages/detail-video/index?id=${id}`,
		})
	}

安装tailwindcss

image-20230225153341363

image-20230225153815117

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OssTjcnV-1683379078129)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230225160644483.png)]

报错解决方案

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wdYkCwdw-1683379078129)(https://gitee.com/zjh1816298537/front-end-drawing-bed/raw/master/imgs/image-20230225160435943.png)]

CSS

文字彩色下划线动画实现
span {
  color: #337ab7;
  text-decoration: none;
  background: linear-gradient(to right,#ec695c,#61c454) no-repeat right bottom;
  background-size: 0px 3px;
  transition: background-size 1300ms;
}
span:hover {
  background-position-x: left;
  color: #23527c;
  background-size: 100% 3px;
}
css属性计算过程

你是否了解 CSS 的属性计算过程呢?

有的同学可能会讲,CSS属性我倒是知道,例如:

p{
  color : red;
}

上面的 CSS 代码中,p 是元素选择器,color 就是其中的一个 CSS 属性。

但是要说 CSS 属性的计算过程,还真的不是很清楚。

没关系,通过此篇文章,能够让你彻底明白什么是 CSS 属性的计算流程。

首先,不知道你有没有考虑过这样的一个问题,假设在 HTML 中有这么一段代码:

<body>
  <h1>这是一个h1标题</h1>
</body>

上面的代码也非常简单,就是在 body 中有一个 h1 标题而已,该 h1 标题呈现出来的外观是如下:

image-20220813140724136

目前我们没有设置该 h1 的任何样式,但是却能看到该 h1 有一定的默认样式,例如有默认的字体大小、默认的颜色。

那么问题来了,我们这个 h1 元素上面除了有默认字体大小、默认颜色等属性以外,究竟还有哪些属性呢?

答案是**该元素上面会有 CSS 所有的属性。**你可以打开浏览器的开发者面板,选择【元素】,切换到【计算样式】,之后勾选【全部显示】,此时你就能看到在此 h1 上面所有 CSS 属性对应的值。

image-20220813141516153

换句话说,我们所书写的任何一个 HTML 元素,实际上都有完整的一整套 CSS 样式。这一点往往是让初学者比较意外的,因为我们平时在书写 CSS 样式时,往往只会书写必要的部分,例如前面的:

p{
  color : red;
}

这往往会给我们造成一种错觉,认为该 p 元素上面就只有 color 属性。而真实的情况确是,任何一个 HTML 元素,都有一套完整的 CSS 样式,只不过你没有书写的样式,大概率可能会使用其默认值。例如上图中 h1 一个样式都没有设置,全部都用的默认值。

但是注意,我这里强调的是“大概率可能”,难道还有我们“没有设置值,但是不使用默认值”的情况么?

嗯,确实有的,所以我才强调你要了解“CSS 属性的计算过程”。

总的来讲,属性值的计算过程,分为如下这么 4 个步骤:

  • 确定声明值
  • 层叠冲突
  • 使用继承
  • 使用默认值
确定声明值

首先第一步,是确定声明值。所谓声明值就是作者自己所书写的 CSS 样式,例如前面的:

p{
  color : red;
}

这里我们声明了 p 元素为红色,那么就会应用此属性设置。

当然,除了作者样式表,一般浏览器还会存在“用户代理样式表”,简单来讲就是浏览器内置了一套样式表。

image-20220813143500066

在上面的示例中,作者样式表中设置了 color 属性,而用户代理样式表(浏览器提供的样式表)中设置了诸如 display、margin-block-start、margin-block-end、margin-inline-start、margin-inline-end 等属性对应的值。

这些值目前来讲也没有什么冲突,因此最终就会应用这些属性值。

层叠冲突

在确定声明值时,可能出现一种情况,那就是声明的样式规则发生了冲突。

此时会进入解决层叠冲突的流程。而这一步又可以细分为下面这三个步骤:

  • 比较源的重要性
  • 比较优先级
  • 比较次序

来来来,我们一步一步来看。

比较源的重要性

当不同的 CSS 样式来源拥有相同的声明时,此时就会根据样式表来源的重要性来确定应用哪一条样式规则。

那么问题来了,咱们的样式表的源究竟有几种呢?

整体来讲有三种来源:

  • 浏览器会有一个基本的样式表来给任何网页设置默认样式。这些样式统称用户代理样式
  • 网页的作者可以定义文档的样式,这是最常见的样式表,称之为页面作者样式
  • 浏览器的用户,可以使用自定义样式表定制使用体验,称之为用户样式

对应的重要性顺序依次为:页面作者样式 > 用户样式 > 用户代理样式

更详细的来源重要性比较,可以参阅 MDNhttps://developer.mozilla.org/zh-CN/docs/Web/CSS/Cascade

我们来看一个示例。

例如现在有页面作者样式表用户代理样式表中存在属性的冲突,那么会以作者样式表优先。

p{
  color : red;
  display: inline-block;
}

image-20220813144222152

可以明显的看到,作者样式表和用户代理样式表中同时存在的 display 属性的设置,最终作者样式表干掉了用户代理样式表中冲突的属性。这就是第一步,根据不同源的重要性来决定应用哪一个源的样式。

比较优先级

那么接下来,如果是在在同一个源中有样式声明冲突怎么办呢?此时就会进行样式声明的优先级比较。

例如:

<div class="test">
  <h1>test</h1>
</div>
.test h1{
  font-size: 50px;
}

h1 {
  font-size: 20px;
}

在上面的代码中,同属于页面作者样式,源的重要性是相同的,此时会以选择器的权重来比较重要性。

很明显,上面的选择器的权重要大于下面的选择器,因此最终标题呈现为 50px

image-20210916151546500

可以看到,落败的作者样式在 Elements>Styles 中会被划掉。

有关选择器权重的计算方式,不清楚的同学,可以进入此传送门:https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity

比较次序

经历了上面两个步骤,大多数的样式声明能够被确定下来。但是还剩下最后一种情况,那就是样式声明既是同源,权重也相同。

此时就会进入第三个步骤,比较样式声明的次序。

举个例子:

h1 {
  font-size: 50px;
}

h1 {
  font-size: 20px;
}

在上面的代码中,同样都是页面作者样式选择器的权重也相同,此时位于下面的样式声明会层叠掉上面的那一条样式声明,最终会应用 20px 这一条属性值。

image-20220823183928330

至此,样式声明中存在冲突的所有情况,就全部被解决了。

使用继承

层叠冲突这一步完成后,解决了相同元素被声明了多条样式规则究竟应用哪一条样式规则的问题。

那么如果没有声明的属性呢?此时就使用默认值么?

No、No、No,别急,此时还有第三个步骤,那就是使用继承而来的值。

例如:

<div>
  <p>Lorem ipsum dolor sit amet.</p>
</div>
div {
  color: red;
}

在上面的代码中,我们针对 div 设置了 color 属性值为红色,而针对 p 元素我们没有声明任何的属性,但是由于 color 是可以继承的,因此 p 元素从最近的 div 身上继承到了 color 属性的值。

image-20220813145102293

这里有两个点需要同学们注意一下。

首先第一个是我强调了是最近的 div 元素,看下面的例子:

<div class="test">
  <div>
    <p>Lorem ipsum dolor sit amet.</p>
  </div>
</div>
div {
  color: red;
}
.test{
  color: blue;
}

image-20220813145652726

因为这里并不涉及到选中 p 元素声明 color 值,而是从父元素上面继承到 color 对应的值,因此这里是谁近就听谁的,初学者往往会产生混淆,又去比较权重,但是这里根本不会涉及到权重比较,因为压根儿就没有选中到 p 元素。

第二个就是哪些属性能够继承?

关于这一点的话,大家可以在 MDN 上面很轻松的查阅到。例如我们以 text-align 为例,如下图所示:

image-20220813150147885

使用默认值

好了,目前走到这一步,如果属性值都还不能确定下来,那么就只能是使用默认值了。

如下图所示:

image-20220813150824752

前面我们也说过,一个 HTML 元素要在浏览器中渲染出来,必须具备所有的 CSS 属性值,但是绝大部分我们是不会去设置的,用户代理样式表里面也不会去设置,也无法从继承拿到,因此最终都是用默认值。

好了,这就是关于 CSS 属性计算过程的所有知识了。

image-20220814234654914
一道面试题

好了,学习了今天的内容,让我来用一道面试题测试测试大家的理解程度。

下面的代码,最终渲染出来的效果,a 元素是什么颜色?p 元素又是什么颜色?

<div>
  <a href="">test</a>
  <p>test</p>
</div>
div {
  color: red;
}

大家能说出为什么会呈现这样的结果么?

解答如下:

image-20220813151941113

实际上原因很简单,因为 a 元素在用户代理样式表中已经设置了 color 属性对应的值,因此会应用此声明值。而在 p 元素中无论是作者样式表还是用户代理样式表,都没有对此属性进行声明,然而由于 color 属性是可以继承的,因此最终 p 元素的 color 属性值通过继承来自于父元素。

你不知道的 CSS 之包含块

一说到 CSS 盒模型,这是很多小伙伴耳熟能详的知识,甚至有的小伙伴还能说出 border-box 和 content-box 这两种盒模型的区别。

但是一说到 CSS 包含块,有的小伙伴就懵圈了,什么是包含块?好像从来没有听说过这玩意儿。

好吧,如果你对包含块的知识一无所知,那么系好安全带,咱们准备出发了。

包含块英语全称为containing block,实际上平时你在书写 CSS 时,大多数情况下你是感受不到它的存在,因此你不知道这个知识点也是一件很正常的事情。但是这玩意儿是确确实实存在的,在 CSS 规范中也是明确书写了的:

https://drafts.csswg.org/css2/#containing-block-details

image-20220814222458695

并且,如果你不了解它的运作机制,有时就会出现一些你认为的莫名其妙的现象。

那么,这个包含块究竟说了什么内容呢?

说起来也简单,就是元素的尺寸和位置,会受它的包含块所影响。对于一些属性,例如 width, height, padding, margin,绝对定位元素的偏移值(比如 position 被设置为 absolute 或 fixed),当我们对其赋予百分比值时,这些值的计算值,就是通过元素的包含块计算得来。

来吧,少年,让我们从最简单的 case 开始看。

<body>
  <div class="container">
    <div class="item"></div>
  </div>
</body>
.container{
  width: 500px;
  height: 300px;
  background-color: skyblue;
}
.item{
  width: 50%;
  height: 50%;
  background-color: red;
}

请仔细阅读上面的代码,然后你认为 div.item 这个盒子的宽高是多少?

image-20220814223451349

相信你能够很自信的回答这个简单的问题,div.item 盒子的 width 为 250px,height 为 150px。

这个答案确实是没有问题的,但是如果我追问你是怎么得到这个答案的,我猜不了解包含块的你大概率会说,因为它的父元素 div.container 的 width 为 500px,50% 就是 250px,height 为 300px,因此 50% 就是 150px。

这个答案实际上是不准确的。正确的答案应该是,div.item 的宽高是根据它的包含块来计算的,而这里包含块的大小,正是这个元素最近的祖先块元素的内容区。

因此正如我前面所说,很多时候你都感受不到包含块的存在。

包含块分为两种,一种是根元素(HTML 元素)所在的包含块,被称之为初始包含块(initial containing block)。对于浏览器而言,初始包含块的的大小等于视口 viewport 的大小,基点在画布的原点(视口左上角)。它是作为元素绝对定位和固定定位的参照物。

另外一种是对于非根元素,对于非根元素的包含块判定就有几种不同的情况了。大致可以分为如下几种:

  • 如果元素的 positiion 是 relative 或 static ,那么包含块由离它最近的块容器(block container)的内容区域(content area)的边缘建立。
  • 如果 position 属性是 fixed,那么包含块由视口建立。
  • 如果元素使用了 absolute 定位,则包含块由它的最近的 position 的值不是 static (也就是值为fixed、absolute、relative 或 sticky)的祖先元素的内边距区的边缘组成。

前面两条实际上都还比较好理解,第三条往往是初学者容易比较忽视的,我们来看一个示例:

<body>
    <div class="container">
      <div class="item">
        <div class="item2"></div>
      </div>
    </div>
  </body>
.container {
  width: 500px;
  height: 300px;
  background-color: skyblue;
  position: relative;
}
.item {
  width: 300px;
  height: 150px;
  border: 5px solid;
  margin-left: 100px;
}
.item2 {
  width: 100px;
  height: 100px;
  background-color: red;
  position: absolute;
  left: 10px;
  top: 10px;
}

首先阅读上面的代码,然后你能在脑海里面想出其大致的样子么?或者用笔和纸画一下也行。

公布正确答案:

image-20220814233548188

怎么样?有没有和你所想象的对上?

其实原因也非常简单,根据上面的第三条规则,对于 div.item2 来讲,它的包含块应该是 div.container,而非 div.item。

如果你能把上面非根元素的包含块判定规则掌握,那么关于包含块的知识你就已经掌握 80% 了。

实际上对于非根元素来讲,包含块还有一种可能,那就是如果 position 属性是 absolute 或 fixed,包含块也可能是由满足以下条件的最近父级元素的内边距区的边缘组成的:

  • transform 或 perspective 的值不是 none
  • will-change 的值是 transform 或 perspective
  • filter 的值不是 none 或 will-change 的值是 filter(只在 Firefox 下生效).
  • contain 的值是 paint (例如: contain: paint;)

我们还是来看一个示例:

<body>
  <div class="container">
    <div class="item">
      <div class="item2"></div>
    </div>
  </div>
</body>
.container {
  width: 500px;
  height: 300px;
  background-color: skyblue;
  position: relative;
}
.item {
  width: 300px;
  height: 150px;
  border: 5px solid;
  margin-left: 100px;
  transform: rotate(0deg); /* 新增代码 */
}
.item2 {
  width: 100px;
  height: 100px;
  background-color: red;
  position: absolute;
  left: 10px;
  top: 10px;
}

我们对于上面的代码只新增了一条声明,那就是 transform: rotate(0deg),此时的渲染效果却发生了改变,如下图所示:

image-20220814234347149

可以看到,此时对于 div.item2 来讲,包含块就变成了 div.item。

好了,到这里,关于包含块的知识就基本讲完了。

image-20220814234654914

我们再把 CSS 规范中所举的例子来看一下。

<html>
  <head>
    <title>Illustration of containing blocks</title>
  </head>
  <body id="body">
    <div id="div1">
      <p id="p1">This is text in the first paragraph...</p>
      <p id="p2">
        This is text
        <em id="em1">
          in the
          <strong id="strong1">second</strong>
          paragraph.
        </em>
      </p>
    </div>
  </body>
</html>

上面是一段简单的 HTML 代码,在没有添加任何 CSS 代码的情况下,你能说出各自的包含块么?

对应的结果如下:

元素包含块
htmlinitial C.B. (UA-dependent)
bodyhtml
div1body
p1div1
p2div1
em1p2
strong1p2

首先 HTML 作为根元素,对应的包含块就是前面我们所说的初始包含块,而对于 body 而言,这是一个 static 定位的元素,因此该元素的包含块参照第一条为 html,以此类推 div1、p1、p2 以及 em1 的包含块也都是它们的父元素。

不过 strong1 比较例外,它的包含块确实 p2,而非 em1。为什么会这样?建议你再把非根元素的第一条规则读一下:

  • 如果元素的 positiion 是 relative 或 static ,那么包含块由离它最近的**块容器(block container)**的内容区域(content area)的边缘建立。

没错,因为 em1 不是块容器,而包含块是离它最近的块容器的内容区域,所以是 p2。

接下来添加如下的 CSS:

#div1 { 
  position: absolute; 
  left: 50px; top: 50px 
}

上面的代码我们对 div1 进行了定位,那么此时的包含块会发生变化么?你可以先在自己思考一下。

答案如下:

元素包含块
htmlinitial C.B. (UA-dependent)
bodyhtml
div1initial C.B. (UA-dependent)
p1div1
p2div1
em1p2
strong1p2

可以看到,这里 div1 的包含块就发生了变化,变为了初始包含块。这里你可以参考前文中的这两句话:

  • 初始包含块(initial containing block)。对于浏览器而言,初始包含块的的大小等于视口 viewport 的大小,基点在画布的原点(视口左上角)。它是作为元素绝对定位和固定定位的参照物。
  • 如果元素使用了 absolute 定位,则包含块由它的最近的 position 的值不是 static (也就是值为fixed、absolute、relative 或 sticky)的祖先元素的内边距区的边缘组成。

是不是一下子就理解了。没错,因为我们对 div1 进行了定位,因此它会应用非根元素包含块计算规则的第三条规则,寻找离它最近的 position 的值不是 static 的祖先元素,不过显然 body 的定位方式为 static,因此 div1 的包含块最终就变成了初始包含块。

接下来我们继续修改我们的 CSS:

#div1 { 
  position: absolute; 
  left: 50px; 
  top: 50px 
}
#em1  { 
  position: absolute; 
  left: 100px; 
  top: 100px 
}

这里我们对 em1 同样进行了 absolute 绝对定位,你想一想会有什么样的变化?

没错,聪明的你大概应该知道,em1 的包含块不再是 p2,而变成了 div1,而 strong1 的包含块也不再是 p2 了,而是变成了 em1。

如下表所示:

元素包含块
htmlinitial C.B. (UA-dependent)
bodyhtml
div1initial C.B. (UA-dependent)
p1div1
p2div1
em1div1(因为定位了,参阅非根元素包含块确定规则的第三条)
strong1em1(因为 em1 变为了块容器,参阅非根元素包含块确定规则的第一条)

好了,这就是 CSS 规范中所举的例子。如果你全都能看明白,以后你还能跟别人说你是看过这一块知识对应的 CSS 规范的人。

image-20220815093518833

另外,关于包含块的知识,在 MDN 上除了解说了什么是包含块以外,也举出了很多简单易懂的示例。

具体你可以移步到:https://developer.mozilla.org/zh-CN/docs/Web/CSS/Containing_block

  • 4
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值