feng xiaohan
Wujie 简介

由腾讯开发的微前端框架,帮助开发者将复杂的前端项目拆分为多个微前端应用,实现分治开发、独立部署、统一集成的目标。

拥有以下几个特点(官网):

极速

页面切换时速度快,无白屏。(主要是通过开启预加载实现)

预加载

预加载指的是在应用空闲的时候requestIdleCallback将所需要的静态资源提前从网络中加载到内存中。

主流的 PC 屏幕刷新率(FPS)大多在 60Hz,即 1 秒钟对屏幕进行 60 次刷新,平均每次刷新耗时大概是 16.6ms。

requestidlecallback触发的时机有两种:

  • 在一帧的输入渲染合成完成后才会有空闲时间触发requestidlecallback;

    一帧内所做的事情如下:

    • 输入事件处理:浏览器会处理用户的输入事件,如鼠标点击、键盘输入等,并将这些事件放入事件队列中;
    • JavaScript 执行:浏览器会从事件队列中取出事件并执行对应的 JavaScript 代码,如事件处理函数、定时器等;
    • 执行 requestAnimationFrame;
    • 执行 dom 的回流与重绘;
    • 计算更新图层的绘制指令(浏览器会将布局信息转换成绘制指令,并使用 GPU 进行绘制渲染,生成对应的位图);
    • 绘制指令合并主线程(浏览器会将多个位图进行合成,生成最终的页面图像),如果有空余时间会执行 requestidlecallback

    在这个过程中,浏览器会尽可能地优化渲染流程,以提高页面的渲染性能和用户体验。实际上就是在 16.6ms 之内完成输入渲染合成,空闲的时间才会留给 requestidlecallback

  • 没有任务执行浏览器会有 50ms 空闲时间,这个时间段也会执行 requestidlecallback

    例如直接在控制台输出。

requestidlecallback(function (deadline) {
  console.log(deadline.timeRemaining());
});

由于子应用提前渲染可能会导致阻塞主应用的线程,所以无界提供了类似于react-fiber方式来防止阻塞线程:

react16: postMessage + requestAnimationFrame来实现类似requestidlecallback()的功能;

  • 使用postMessage + requestAnimationFrame是因为 react 开发人员经过测试发现requestidlecallback()可能会超过 16ms,超过 16ms 绘制就会看起来很卡,所以 react16 是用postMessage + requestAnimationFrame实现的。
  • 使用postMessage 来代替setTimeOut()是因为后者即使为 0

react18 :使用MessageChannel实现requestidlecallback()

强大

支持子应用保活、内嵌、去中心化通信、多应用激活。

子应用保活

简单

框架封装,保持普通组件使用体验一致。

原生隔离

基于 WebComponent 和 iframe,原生物理隔离。

WebComponent

window.onload = () => {
  class Wujie extends HTMLElement {
    constructor() {
      super();
      // shadowdom 样式隔离
      let dom = this.attachShadow({ mode: "open" });
      let template = document.querySelector("#wujie") as HTMLTemplateElement;

      dom.appendChild(template.content.cloneNode(true));

      console.log(this.getAttr("name"), this.getAttr("url"));
    }
    private getAttr(attr: string) {
      return this.getAttribute(attr);
    }

    // webComponents的生命周期
    connectedCallback() {
      console.log("类似于vue的mounted");
    }
    disconnectedCallback() {
      console.log("类似于vue的destory");
    }
    attributeChangedCallback(name: any, oldVal: any, newVal: any) {
      console.log("类似于vue的watch");
    }
  }
  // 使用原生js挂载一个组件
  window.customElements.define("wu-jie", Wujie);
};
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./index.js"></script>
  </head>
  <body>
    <wu-jie name="fdsf" url="dsafas"></wu-jie>
    <div>我是外层的div</div>

    <template id="wujie">
      <style>
        div {
          background: red;
        }
      </style>
      <div>我是template里面的div</div>
    </template>
  </body>
</html>

iframe

将 js 单独存放在 iframe 中。

原生性能

避免 with 语句运行代码,整体的运行性能接近原生。

开箱即用

主、子应用无需做任何适配,开箱即用。

小结

缺陷

  • 隔离 js 使用一个空的 iframe 进行隔离。
  • 子应用 axios 需要自行适配。
  • iframe 沙箱的 src 设置了主应用的 host,初始化 iframe 的时候需要等待 iframe 的 location.orign 从’about:blank’初始化为主应用的 host,这个采用的计时器去等待的不是很优雅。

底层原理:使用 shadowDom 隔离 css,js 使用空的 iframe 隔离,通讯使用的是 proxy。

js 事件

事件的冒泡和捕获

js 事件流大体分为三个阶段,描述了事件在 dom 文档中传播的顺序:

  1. 捕获阶段:事件从文档的根节点开始,向目标元素传播。(由外向内)
  2. 目标阶段:事件到达目标元素。
  3. 冒泡阶段:事件从目标元素向根节点传播。(由内向外)

当点击一个子元素触发某个事件时,如果其父元素也绑定了相同的事件,那么子元素和父元素都会触发相同的事件函数。如果只想触发子元素,则可以在子元素中使用e.stopPropagation()阻止事件传播(无论是冒泡阶段还是捕获阶段)。

阻止了之后点击子元素时后续元素都不会触发该事件了。

addEventListener(type, listener, options)
options: bool,该事件是否在捕获或冒泡阶段执行,
默认不会在捕获阶段执行(false)
;如果设置为 true,会在捕获阶段执行这个事件,这会导致事件再还没到达指定元素时就先被外层设置了为 true 的父元素上的监听器捕获到。

小结

  • e.stopPropagation()阻止事件传播(既可以阻止捕获阶段又可以阻止冒泡阶段);
  • adddEventListener(type, listener, options)`:中的第三个设置是否在事件捕获阶段执行;

其他事件

  • unhandledrejection:当有 Promise 返回 reject 或抛出异常,且该异常没有被 catch 时触发。
  • error:抛出异常时触发(img、script、css、and jsonp)。

    但是在 Promise 中抛出异常则不会触发该事件,可能是因为 Promise 中的异常被 catch 了,就不会触发该事件了吧(猜测)。
    解答:该事件是针对资源加载时的异常(img、script、css、and jsonp)等出错时才会触发。

vite

vite 加载文件原理

  • vite 在启动时会以 ESModule 模块化规范来加载文件,所以会先将所有文件转换为 ES Module 的 js 文件,再执行。

    包括但不限于.css.vue等。

  • 对所有打包资源文件进行依赖预构建,将其统放在node_modules/.vite下,这样方便浏览器引用资源路径。

  • 浏览器请求资源文件时,对返回的文件内容中的资源请求路径继续拦截预处理,将请求路径统一替换为node_modules/.vite下的路径。

  • 资源能正确访问。

.vue 文件处理

对于.vue的文件会先用对应的编译工具将其转换为 esm 模块,,然后以 js(application/javascript)的形式返回给浏览器,这样浏览器就能正确识别到了。

math

辗转相除法(欧几里得算法)

用于求两个正整数的最大公约数(Greatest Common Divisor,GCD)。

基本思想:
除数余数反复做除法运算,当余数为 0 时,取当前算式除数为最大公约数。

$$
gcd(a, b) = gcd(b, a mod b)
$$

// 递归
function gcd(a, b) {
  return a % b ? gcd(b, a % b) : b;
}

// 迭代
function gcd(a, b) {
  while (a % b) {
    [a, b] = [b, a % b];
  }
  return b;
}

补充:扩展的欧几里得算法可用于 RSA 加密算法等领域。

最小公倍数(Leatest Common Multiple,LCM)

可以用最大公约数求得。

$$
lcm(a, b) = \frac{ab}{gcd(a, b)}
$$

二分搜索

二分间值

最小化最大值

二分答案求最小。

最大化最小值

二分答案求最大。

第 K 小/大

  • 第 k 小等价于:求最小的 x,满足 <= x 的数的个数至少有 k 个
  • 第 k 大等价于:求最大的 x,满足 >= x 的数的个数至少有 k 个

注意事项:

  • 数组去重问题:一般不会去重;
  • 规定 k 从 1 开始,而不是数组下标 0;
排序算法

快速排序

基本思想:

  1. 找到一个基准值,将数组分成两个部分,左边的都比基准值小,右边的都比基准值大。

    基准值一般选用第一个数,然后与最后一个数开始比较

  2. 左右两个部分又可以进行上述过程,直到左右两个部分都只有一个元素。

可以看出将排序过程采用了分治思想,将问题分解成更小的子问题,然后递归求解,最后合并结果。

function quickSort(arr, start = 0, end = arr.length - 1) {
  if (start < end) {
    const base = arr[start]; // 基准值
    let left = start,
      right = end;
    while (left < right) {
      while (left < right && base <= arr[right]) {
        right--;
      }
      arr[left] = arr[right];
      while (left < right && base >= arr[left]) {
        left++;
      }
      arr[right] = arr[left];
    }
    arr[left] = base;
    quickSort(arr, start, left - 1); // 分治
    quickSort(arr, left + 1, end); // 分治
  }
}

特性

  • 不稳定
  • 原地排序
  • 平均时间复杂度:O(nlogn);平均空间复杂度:O(logn)

归并排序

基本思想:

  1. 分:将组数分成两个子数组,直到不能再分。
  2. 治:将两个子数组合并成一个有序数组。创建一个新数组,再分别为两个子数组设置标志位同向双指针移动比较,将较小的值放入新数组中。

典型分治思想。

function mergeSort(arr) {
  if (arr.length <= 1) return arr;
  const mid = arr.length >> 1;
  const left = arr.slice(0, mid);
  const right = arr.slice(mid);
  return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right) {
  const result = [];
  let l = 0,
    r = 0;
  while (l < left.length && r < right.length) {
    if (left[l] < right[r]) {
      result.push(left[l++]);
    } else {
      result.push(right[r++]);
    }
  }
  result.push(...left.slice(l), ...right.slice(r));
  return result;
}

特性

  • 稳定
  • “治”时需要额外的数组空间,并且最终也返回这个数组(非原地排序)
  • 平均时间复杂度:O(nlogn);平均空间复杂度:O(n)
tips

前缀和

  • 快速查找某个区间内相加(减)、相乘(除)、异或等运算结果,有结合律和交换律的一些运算。
  • 计算子数组的个数一般会用 map 来记录某个值出现的次数。

两数相加之后判断能否被一个数整除

相加后判断能否被 24 整除。
原理:利用 hash 表记录之前遍历的数的模数,然后对比当前所需要的模数是否在 hash 表中出现过,如果出现过,则说明存在两个数相加后能被 24 整除。

// 哈希表记录
const hours = [12, 12, 30, 24, 24];
const cnt = new Map();
let ans = 0;
for (const h of hours) {
  const cur = (24 - (h % 24)) % 24; // 寻找凑对后能被整除的凑对数的模数
  if (cnt.has(cur)) {
    ans += cnt.get(cur);
  }
  cnt.set(h % 24, (cnt.get(h % 24) || 0) + 1);
}
return ans;
分包策略-性能优化

原因

由于浏览器的缓存策略,在请求静态资源时只要静态资源的名称不变就不会重新请求。所以使用 vite 打包后的文件带有哈希值,只要有一点代码的变动文件的哈希值就会变换,以此来保证每次请求获取的都是最新代码。

但是有很多包是不需要经常更新的(第三方资源包,例如 lodash 等),每次重新请求对浏览器来说就是浪费资源,我们可以将这部分的包单独进行打包处理。

output.manualChunks

利用 rollup 本身的配置来实现。当该选项值为函数形式时,每个被解析的模块都会经过该函数处理。如果函数返回字符串,那么该模块及其所有依赖将被添加到以返回字符串命名的自定义 chunk 中。

export default defineConfig({
  // ...
  build: {
    rollupOptions: {
      output: {
        manualChunks: (id) => {
          console.log("id----", id); // id 对象所有需要打包的文件路径
          // 路径包含 node_modules 的包,一般为第三方包,不会变动的
          if (id.includes("node_modules")) {
            return "vendor"; // 会分出一个前缀为 vender的包
          }
        },
      },
    },
  },
});
一些有用的 css 属性

columns

将一个元素拆分成多列,并设置每列的宽度。

.content {
  columns: 200px 3; /* 每列的宽度为 200px,共 3 列 */
  column-gap: 12px;
}

break-inside

在浏览器中,列表项将不会被拆分到不同的列中。每个列表项将作为一个整体出现在同一列中。

.menu-list {
  break-inside: avoid;
}

使用伪元素通过 border 绘制一条直线

使用伪元素中的 content 在元素前/后添加直线时如果手动添加并不能使其自动填满父元素的剩余空间。
所以可以使用 border 配合 flex 来实现更好的效果:

<div class="title">首页</div>
.title {
  width: 100%;
  display: inline-flex; /* 设置该元素为行内flex元素 */
  padding-left: 4px;
}
.title::after {
  content: "";
  flex: 1 1 0%; /* 使用flex填满剩余空间 */
  border-top: 1px solid #ebebeb; /* 设置border作为分割线 */
  margin: 16px;
}
css 浏览器兼容

css 在不同的浏览器兼容性是不同的,有些特殊的样式在不同的浏览器前需要加上特殊性的前缀,比如:

// 只有chrome和safari支持,且需要添加webkit前缀
-webkit-margin-before
-webkit-margin-after

postcss

postcss 是一个用 JavaScript 工具和插件转换 CSS 代码的工具,postcss 自身没有什么功能,只是一个平台,可以下载各种插件,从而实现一些功能!

  • Autoprefixer:自动获取浏览器的流行度和能够支持的属性,并根据这些数据帮你自动为 CSS 规则添加前缀。
  • PostCSS Preset Env:将最新的 CSS 语法转换成大多数浏览器都能兼容的语法。
  • CSS module:解决 css 命名冲突。

vite 中配置 postcss

在 vite 中配置主要使用postcss-preset-env插件,它支持 css 变量和一些未来 css 语法以及自动补全(autoprefixer)。

  • 安装插件:

    npm i postcss-preset-env -D
    
  • 在 vite.config.js 中配置:

    import { defineConfig } from "vite";
    const postcssPresetEnv = require("postcss-preset-env");
    export default defineConfig({
      css: {
        // modules
        // devSourcemap
        // ...
        postcss: {
          plugins: [postcssPresetEnv()],
        },
      },
    });