2
头图

前言

这是一个每天更新一题的文章,欢迎评论区网友提供题目和答案,不断更新中,记录下来,方便学习,参考答案如有问题,敬请批评指正,废话不多说,开始进入正文。

第1题

题目

  1. 判断一个对象是否为空,包含了其原型链上是否有自定义数据或者方法。 该如何判定?

参考解答

参考解答:

要判断一个对象是否为空,并且考虑到对象自身及其原型链上是否有自定义数据或方法,您可以使用以下的 JavaScript 函数来实现:

function isObjectEmpty(obj) {
  // 首先获取对象自身的属性
  const ownProperties = Object.getOwnPropertyNames(obj);
  // 遍历自身属性
  for (const property of ownProperties) {
    const descriptor = Object.getOwnPropertyDescriptor(obj, property);
    // 如果属性是数据属性并且有值,或者是方法(可调用函数),则对象不为空
    if (
      (descriptor.value && descriptor.value !== null && descriptor.value !== undefined) ||
      typeof descriptor.value === "function"
    ) {
      return false;
    }
  }
  // 获取对象的原型
  const prototype = Object.getPrototypeOf(obj);
  // 如果有原型并且原型不是 `Object.prototype`(避免误判普通对象的默认方法)
  while (prototype && prototype !== Object.prototype) {
    const prototypeProperties = Object.getOwnPropertyNames(prototype);
    // 遍历原型的属性
    for (const property of prototypeProperties) {
      const descriptor = Object.getOwnPropertyDescriptor(prototype, property);
      // 如果原型上的属性是数据属性并且有值,或者是方法(可调用函数),则对象不为空
      if (
        (descriptor.value && descriptor.value !== null && descriptor.value !== undefined) ||
        typeof descriptor.value === "function"
      ) {
        return false;
      }
    }
    // 继续沿着原型链向上查找
    prototype = Object.getPrototypeOf(prototype);
  }
  // 如果以上检查都没有找到非空属性或方法,则对象为空
  return true;
}

可以使用这个函数来判断对象是否为空,例如:

function MyClass() {}
MyClass.prototype.myMethod = function () {};
const instance = new MyClass();
console.log(isObjectEmpty(instance));

第2题

题目

  1. 为何通常在微前端应用隔离,不选择 iframe 方案?

参考解答

在微前端架构中,虽然iframe能提供很好的应用隔离(包括 JavaScript 和 CSS 隔离),确保微前端应用之间不会相互干扰,但一般不把它作为首选方案,原因包括:

  1. 性能开销

iframe会创建一个全新的浏览器上下文环境,每个iframe都有自己的文档对象模型(DOM)树、全局执行环境等。如果一个页面中嵌入了多个iframe,就会导致额外的内存和 CPU 资源消耗,特别是在性能有限的设备上更为显著。

  1. 应用集成和交互问题

iframe自然隔离了父子页面的环境,这虽然提供了隔离,但同时也使得主应用与子应用之间的通信难度增加。虽然可以通过postMessage等 API 实现跨iframe通信,但这种方式相比于直接 JavaScript 调用来说,更为复杂,交互效率也较低。

  1. UI 体验一致性

在iframe中运行的应用在视觉上可能与主应用难以实现无缝集成。iframe内外的样式、字体等一致性需要额外的处理。此外,iframe可能带来额外的滚动条,影响用户体验。

  1. SEO 问题

如果微前端的某些内容是通过iframe呈现的,那么这部分内容对于搜索引擎是不可见的,这可能会对应用的
SEO 产生负面影响。

  1. 安全问题

虽然iframe可以提供一定程度的隔离,但它也可能引入点击劫持等安全风险。此外,过多地使用iframe也可能增加网站被恶意脚本攻击的风险。

因此,虽然iframe是一种可行的应用隔离方法,它的这些局限性使得开发者在选择微前端技术方案时,往往会考虑其他提供更轻量级隔离、更好集成与交互体验的方案,如使用 JavaScript 沙箱、CSS 隔离技术、Web Components 等。这些方法虽然隔离性可能不如iframe彻底,但在整体的应用性能、用户体验和开发效率上通常会有更好的表现。

第3题

题目

  1. Webpack 为何不支持 CMD 模块化?

参考解答

Webpack 本身并不直接“不支持”CMD(CommonJS Modules/1.x 规范)模块化,因为 CMD 模块化规范在前端开发中并不常见,且 Webpack 实际上可以处理多种模块化规范,包括 CommonJS。

然而,当我们讨论 Webpack 是否“支持”CMD 时,可能是在对比 Webpack 对不同模块化规范的支持程度和默认行为。Webpack 主要关注于将模块打包成浏览器可以理解的格式,而 CommonJS(以及 AMD、UMD 等)模块化规范在 Node.js 环境中更为常见。

Webpack 默认支持 ES Module(ECMAScript Modules,ES6 引入的模块系统)和 CommonJS 模块化规范。对于 CommonJS 模块,Webpack 能够识别 require() 和 module.exports 语法,并将这些模块按照依赖关系打包成一个或多个文件。

至于 CMD 模块化规范,它主要是 Sea.js 这类模块加载器所使用的规范,并不是 JavaScript 官方标准或广泛支持的模块化规范。因此,Webpack 没有内置对 CMD 的直接支持,也不需要将 CMD 模块化规范作为其主要关注点。

在实际开发中,如果你在使用 Webpack 并且遇到了需要处理 CMD 模块化规范的情况,这通常意味着你可能需要:

转换模块规范:使用 Babel 或其他转换工具将 CMD 模块化规范转换为 Webpack 支持的规范,如 CommonJS 或 ES Module。
使用插件或加载器:查看是否有现成的 Webpack 插件或加载器可以处理 CMD 模块化规范,尽管这样的插件可能不太常见。
重新考虑模块化策略:评估是否有可能将项目中的模块迁移到更广泛支持的模块化规范上,如 CommonJS 或 ES Module。
总的来说,Webpack 本身不直接“不支持”CMD 模块化规范,但它更关注于支持 ECMAScript 标准和广泛使用的模块化规范,如 CommonJS 和 ES Module。如果你需要使用 CMD 模块化规范,可能需要通过一些额外的步骤来适配 Webpack 的工作流程。

第4题

题目

  1. Webpack 如何将一些通用的依赖打包成一个独立的 bundle?

参考解答

在 Webpack 中,将一些通用的依赖,如 React、React DOM、React Router 等库和框架,打包成一个独立的 bundle,通常是为了长期缓存和减少每次部署更新的下载量。这可以通过 "代码分割" (code splitting) 和 "优化" (optimization) 配置来实现。
以下是 Webpack 中分离通用依赖的几个步骤:

1、使用 entry 来定义不同的入口点: 可以通过配置一个额外的入口来创建一个只包含通用库的 bundle,也就是所谓的 "vendor" bundle。

module.exports = {
  entry: {
    main: "./src/index.js", // 你的应用代码
    vendor: ["react", "react-dom", "react-router"], // 指定共享库
  },
  // ...
};

2、使用 SplitChunksPlugin: 这个插件可以将共享代码分割成不同的 chunks,并可以通过配置将其从业务代码中分离出来。在 Webpack 4 及之后的版本中,默认内置了 optimization.splitChunks,就是这个插件的配置方法。

module.exports = {
  // ...
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/, // 指定是 node_modules 下的第三方包
          name: "vendors", // 打包后的文件名,任意命名
          chunks: "all", // 对所有的 chunk 生效
        },
      },
    },
  },
};

3、配置 output: 虽然不是必须的,你还可以在 output 中定义 filename 和 chunkFilename,来控制主入口和非主入口 chunks 的文件名。

output: {
  filename: '[name].[contenthash].js',
  chunkFilename: '[name].[contenthash].js'
}

通过这样的配置,Webpack 在打包时会自动将 node_modules 中的依赖和业务代码分离开来,业务代码会被打包到 main chunk 中,而第三方库则会打包到 vendors chunk。

第5题

题目

  1. 如何统计长任务时间、长任务执行次数?

参考解答

在 JavaScript 中,可以使用 Performance API 中的 PerformanceObserver 来监视和统计长任务(Long Task)。长任务是指那些执行时间超过 50 毫秒的任务,这些任务可能会阻塞主线程,影响页面的交互性和流畅性。

// 创建一个性能观察者实例来订阅长任务
let observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log("Long Task detected:");
    console.log(`Task Start Time: ${entry.startTime}, Duration: ${entry.duration}`);
  }
});

// 开始观察长任务
observer.observe({ entryTypes: ["longtask"] });

// 启动长任务统计数据的变量
let longTaskCount = 0;
let totalLongTaskTime = 0;

// 更新之前的性能观察者实例,以增加统计逻辑
observer = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    longTaskCount++; // 统计长任务次数
    totalLongTaskTime += entry.duration; // 累加长任务总耗时
    // 可以在这里添加其他逻辑,比如记录长任务发生的具体时间等
  });
});

// 再次开始观察长任务
observer.observe({ entryTypes: ["longtask"] });
在上面的代码中,我们创建了一个PerformanceObserver对象来订阅长任务。每当检测到长任务时,它会向回调函数传递一个包含长任务性能条目的列表。在这个回调中,我们可以统计长任务的次数和总耗时。
注意:PerformanceObserver需要在支持该 API 的浏览器中运行。截至到我所知道的信息(2023 年 4 月的知识截点),所有现代浏览器都支持这一 API,但在使用前你应该检查用户的浏览器是否支持这个特性。
以下是如何在实际使用中停止观察和获取当前的统计数据:
// 停止观察能力
observer.disconnect();

// 统计数据输出
console.log(`Total number of long tasks: ${longTaskCount}`);
console.log(`Total duration of all long tasks: ${totalLongTaskTime}ms`);

使用这种方法,你可以监控应用程序中的性能问题,并根据长任务的发生频率和持续时间进行优化。

第6题

题目

  1. forwardsRef 作用是啥, 有哪些使用场景?

参考解答

在 React 中,forwardRef 是一个用来传递 ref 引用给子组件的技术。通常情况下,refs 是不会透传给子组件的,因为 refs 并不是像 props 那样的属性。forwardRef 提供了一种机制,可以将 ref 自动地通过组件传递到它的子组件。

forwardRef 的作用:

访问子组件的 DOM 节点: 当需要直接访问子组件中的 DOM 元素(例如,需要管理焦点或测量尺寸)时,可以使用 forwardRef。

在高阶组件(HOC)中转发 refs: 封装组件时,通过 forwardRef 可以将 ref 属性透传给被封装的组件,这样父组件就能够通过 ref 访问到实际的子组件实例或 DOM 节点。

在函数组件中使用 refs(React 16.8+): 在引入 Hook 之前,函数组件不能直接与 refs 交互。但是,引入了 forwardRef 和 useRef 之后,函数组件可以接受 ref 并将它透传给子节点。

使用场景举例:

  1. 访问子组件的 DOM 节点

假设你有一个 FancyButton 组件,你想从父组件中直接访问这个按钮的 DOM 节点。

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// 现在你可以从父组件中直接获取DOM引用
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
  1. 在高阶组件中转发 refs

一个常见的模式是为了抽象或修改子组件行为的高阶组件(HOC)。forwardRef可以用来确保 ref 可以传递给包装组件:

function logProps(Component) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log("old props:", prevProps);
      console.log("new props:", this.props);
    }

    render() {
      const { forwardedRef, ...rest } = this.props;

      // 将自定义的 prop 属性 "forwardedRef" 定义为 ref
      return <Component ref={forwardedRef} {...rest} />;
    }
  }

  // 注意:React.forwardRef 回调的第二个参数 "ref" 传递给了LogProps组件的props.forwardedRef
  return React.forwardRef((props, ref) => {
    return <LogProps {...props} forwardedRef={ref} />;
  });
}
  1. 在函数组件中使用 ref

在 Hook 出现之前,函数组件不能够直接与 ref 通信。现在可以这样做:

const MyFunctionalComponent = React.forwardRef((props, ref) => {
  return <input type="text" ref={ref} />;
});

const ref = React.createRef();
<MyFunctionalComponent ref={ref} />;

当你需要在父组件中控制子组件中的 DOM 元素或组件实例的行为时,forwardRef 是非常有用的工具。不过,如果可行的话,通常最好通过状态提升或使用 context 来管理行为,只在没有其他替代的情况下才选择使用 refs。

第7题

题目

  1. 如何一次性渲染十万条数据还能保证页面不卡顿?

参考解答

原理其实就是通过 requestAnimationFrame 实现分块儿加载。

requestAnimationFrame + fragment(时间分片)

既然定时器的执行时间和浏览器的刷新率不一致,那么我就可以用requestAnimationFrame来解决
requestAnimationFrame也是个定时器,不同于setTimeout,它的时间不需要我们人为指定,这个时间取决于当前电脑的刷新率,如果是 60Hz ,那么就是 16.7ms 执行一次,如果是 120Hz 那就是 8.3ms 执行一次。

这么一来,每次电脑屏幕 16.7ms 后刷新一下,定时器就会产生 20 个li,dom结构的出现和屏幕的刷新保持了一致。

const total = 100000;
let ul = document.getElementById("container");
let once = 20;
let page = total / once;

function loop(curTotal) {
  if (curTotal <= 0) return;

  let pageCount = Math.min(curTotal, once);

  window.requestAnimationFrame(() => {
    for (let i = 0; i < pageCount; i++) {
      let li = document.createElement("li");
      li.innerHTML = ~~(Math.random() * total);
      ul.appendChild(li);
    }
    loop(curTotal - pageCount);
  });
}

loop(total);

其实目前这个代码还可以优化一下,每一次appendChild都是新增一个新的li,也就意味着需要回流一次,总共十万条数据就需要回流十万次。

此前讲回流的时候提出过虚拟片段fragment来解决这个问题。

fragment是虚拟文档碎片,我们一次for循环产生 20 个li的过程中可以全部把真实dom挂载到fragment上,然后再把fragment挂载到真实dom上,这样原来需要回流十万次,现在只需要回流100000 / 20次。

const total = 100000;
let ul = document.getElementById("container");
let once = 20;
let page = total / once;

function loop(curTotal) {
  if (curTotal <= 0) return;

  let pageCount = Math.min(curTotal, once);

  window.requestAnimationFrame(() => {
    let fragment = document.createDocumentFragment(); // 创建一个虚拟文档碎片
    for (let i = 0; i < pageCount; i++) {
      let li = document.createElement("li");
      li.innerHTML = ~~(Math.random() * total);
      fragment.appendChild(li); // 挂到fragment上
    }
    ul.appendChild(fragment); // 现在才回流
    loop(curTotal - pageCount);
  });
}

loop(total);

第8题

题目

  1. jsBridge 是什么?原理是啥?

参考解答

jsBridge是一种在 Web 开发中常用的技术,通常指的是 JavaScript Bridge 的缩写,它是一种在 Web 视图(如 WebView)和原生应用之间进行通信的机制。jsBridge 使得原生代码(如 Android 的 Java/Kotlin 或 iOS 的 Objective-C/Swift)能够与嵌入到 WebView 中的 JavaScript 代码相互调用和通信。

在具体实现上,jsBridge 的原理可能因平台而异,但大致的原理如下:

从 JavaScript 调用原生代码:
注册原生函数:首先,原生应用会在 WebView 中注册一些可以供 JavaScript 调用的方法或函数。
调用原生函数:然后,JavaScript 可以通过特定的接口调用这些注册的原生方法。这通常是通过注入对象(例如,在 Android 中可以使用addJavascriptInterface方法)或监听特定的 URL scheme。
消息传递:当 JavaScript 需要与原生应用通信时,它会发送消息(或调用方法),这个消息包含必要的指令和数据。
原生处理:原生代码接收到这个消息后,会执行对应的指令,并将结果返回给 JavaScript(如果需要)。

从原生代码调用 JavaScript:
执行 JavaScript 代码:原生应用可以执行 WebView 中的 JavaScript 代码。例如,通过 WebView 的evaluateJavaScript(iOS)或loadUrl("javascript:...")(Android)方法。
回调 JavaScript:原生应用还可以通过执行回调函数的方式,将数据或结果传递回 JavaScript。

jsBridge 在移动应用开发中尤为重要,因为它提供了一种方式来整合 Web 技术和原生应用功能,让开发者能够利用 Web 技术来编写跨平台的应用,同时还能够访问设备的原生功能,如相机、GPS 等。
这种机制特别适合于混合应用的开发,在这些应用中,部分界面和逻辑使用 Web 技术实现,而另一部分则利用原生代码以获取更好的性能和更丰富的设备功能支持。通过 jsBridge,两种不同的代码和技术可以互相协作,提供统一的用户体验。

第9题

题目

  1. ts 项目中,如何使用 node_modules 里面定义的全局类型包到自己项目 src 下面使用?

参考解答

在 TypeScript 项目中导入 node_modules 中定义的全局包,并在你的 src 目录下使用它,通常遵循以下步骤:
安装包: 使用包管理器如 npm 或 yarn 来安装你需要的全局包。

npm install <package-name>
# 或者
yarn add <package-name>

类型声明: 确保该全局包具有类型声明。如果该全局包包含自己的类型声明,则 TypeScript 应该能够自动找到它们。如果不包含,则可能需要安装对应的 DefinitelyTyped 声明文件。

npm install @types/<package-name>
# 或者,如果它是一个流行的库,一些库可能已经带有自己的类型定义。
导入包: 在 TypeScript 文件中,使用 import 语句导入全局包。
import * as PackageName from "<package-name>";
// 或者
import PackageName from "<package-name>";

tsconfig.json 配置:

确保你的 tsconfig.json 文件配置得当,以便 TypeScript 能够找到 node_modules 中的声明文件。

如果包是模块形式的,确保 "moduleResolution" 设置为 "node"。
确保 compilerOptions 中的 "types" 和 "typeRoots" 属性没有配置错误。

使用全局包:
现在你可以在你的 src 目录中的任何文件里使用这个全局包。

记住,最好的做法是不要把包当成全局包来使用,即使它们是全局的。通过显式地导入所需的模块,可以有助于工具如 linters 和 bundlers 更好地追踪依赖关系,并可以在以后的代码分析和维护中发挥重要作用。

此外,全局变量或全局模块通常指的是在项目的多个部分中无需导入就可以直接使用的变量或模块。如果你确实需要将某些模块定义为全局可用,并且无法通过导入来使用,你可能需要更新你的 TypeScript 配置文件(tsconfig.json)来包括这些全局声明。但这通常不是一个推荐的做法,因为它可能会导致命名冲突和代码可维护性问题。

第10题 ---2024-08-07

题目

  1. React为何要自己实现调度器, 而不是直接使用 requestIdleCallback ?

参考解答

React 在性能优化方面的一个关键组件是调度器(Scheduler),它负责在渲染的过程中合理安排工作,以减少用户的等待时间以及避免单个任务占用过多的主线程时间,从而提高渲染性能。React 在 18.0 版本后引入了新的调度器机制,提供了更好的性能体验。

那么,为什么 React 不直接使用 requestIdleCallback 而要自己实现调度器呢?

1、控制精细度: React 需要比 requestIdleCallback 更高的控制精细度。requestIdleCallback 是基于浏览器的空闲时间进行调度的,而 React 调度器可以根据组件优先级、更新的紧急程度等信息,更精确地安排渲染的工作。

2、跨浏览器兼容性: requestIdleCallback 直到 2018 年才是浏览器中较普遍支持的 API。React 需要一个能够跨各个版本或框架的解决方案,以实现一致的性能体验。

3、时间切片: React 使用一种称为“时间切片”(time slicing)的技术,允许组件分布 多个帧中渲染以维持流畅的 UI。这依赖于 React 自己对任务和帧的精确控制,而不是依赖浏览器的 requestIdleCallback。

4、更丰富的特性: React 调度器提供了比 requestIdleCallback 更丰富的特性和更加详细的调度策略,这包括:

  • Immediate 模式,用于同步渲染,当它是必需的时候。
  • User-blocking 模式,用于任务需要尽快完成,但能够容忍一定延迟,比如交互动画。
  • Normal 和 Low 模式,用于不同优先级的更新。

5、复杂功能的实现: React 使用调度器实现某些特定的特性,比如:
Fiber 架构,允许 React 在类组件上实现 Concurrent 特性。
在客户端渲染和服务器端渲染之间实现一致性。

6、优化生态工具: 对于 React 生态中的其他工具和实现(如 react-native、fast-refresh 等),它们可能需要特定或不同的调度策略。

7、未来兼容性: React 团队可以更好地在自己控制的调度器中实现未来的优化和特性,而不受浏览器 API 变更的影响。

最后,调度器是 React 架构中的一个重要部分,它让 React 能够实现更丰富和灵活的用户界面渲染逻辑。尽管 requestIdleCallback 可以被用来实现一些调度器的特性,但是完全使用它将限制 React 进一步优化的可能性,并迫使 React 依赖于浏览器的调度行为,这可能不符合 React 的长期发展和技术策略。

第11题 ---2024-08-08

题目

  1. 如何检测网页空闲状态(一定时间内无操作)?

参考解答

如何判断页面是否空闲?首先,我们要知道什么是空闲?用户一定时间内,没有对网页进行任何操作,则当前网页为空闲状态。
用户操作网页,无非就是通过鼠标、键盘两个输入设备(暂不考虑手柄等设备)。因而我们可以监听相应的输入事件,来判断网页是否空闲(用户是否有操作网页)。

监听鼠标移动事件mousemove;
监听键盘按下事件mousedown;
在用户进入网页后,设置延时跳转,如果触发以上事件,则移除延时器,并重新开始。

网页空闲检测实现
实现点:

需要使用防抖方式实现,避免性能问题
监听visibilitychange事件,在页面隐藏时移除延时器,然后页面显示时继续计时,从而解决这个问题。

/**
 * 网页空闲检测
 * @param {() => void} callback 空闲时执行,即一定时长无操作时触发
 * @param {number} [timeout=15] 时长,默认15s,单位:秒
 * @param {boolean} [immediate=false] 是否立即开始,默认 false
 * @returns
 */
const onIdleDetection = (callback, timeout = 15, immediate = false) => {
  let pageTimer;
  let beginTime = 0;
  const onClearTimer = () => {
    pageTimer && clearTimeout(pageTimer);
    pageTimer = undefined;
  };
  const onStartTimer = () => {
    const currentTime = Date.now();
    if (pageTimer && currentTime - beginTime < 100) {
      return;
    }

    onClearTimer();
    beginTime = currentTime;
    pageTimer = setTimeout(() => {
      callback();
    }, timeout * 1000);
  };

  const onPageVisibility = () => {
     // 页面显示状态改变时,移除延时器
     onClearTimer();

     if (document.visibilityState === 'visible') {
       const currentTime = Date.now();
       // 页面显示时,计算时间,如果超出限制时间则直接执行回调函数
       if (currentTime - beginTime >= timeout * 1000) {
         callback();
         return;
       }
       // 继续计时
       pageTimer = setTimeout(() => {
         callback();
       }, timeout * 1000 - (currentTime - beginTime));
     }
  };

  const startDetection = () => {
    onStartTimer();
    document.addEventListener('mousedown', onStartTimer);
    document.addEventListener('mousemove', onStartTimer);
    document.addEventListener('visibilitychange', onPageVisibility);
  };

  const stopDetection = () => {
    onClearTimer();
    document.removeEventListener('mousedown', onStartTimer);
    document.removeEventListener('mousemove', onStartTimer);
    document.removeEventListener('visibilitychange', onPageVisibility);
  };

  const restartDetection = () => {
      onClearTimer();
      onStartTimer();
  };

  if (immediate) {
    startDetection();
   }
}

第12题 ---2024-08-09

题目

  1. 浏览器对队头阻塞有什么优化?

参考解答

队头阻塞(Head-of-Line Blocking,缩写 HoLB)问题主要发生在网络通信中,特别是在使用 HTTP/1.1 和以前版本时,在一个 TCP 连接中同一时间只能处理一个请求。即使后续的请求已经准备好在客户端,它们也必须等待当前处理中的请求完成后才能被发送。这会延迟整个页面或应用的网络请求,降低性能。

现代浏览器和协议已经实施了多种优化措施来减少或解决队头阻塞问题:

1、HTTP/2:

为了解决 HTTP/1.x 的诸多问题,包括队头阻塞问题,HTTP/2 引入了多路复用(multiplexing)功能。这允许在同一 TCP 连接上同时传输多个独立的请求-响应消息。与 HTTP/1.1 相比,HTTP/2 在同一个连接上可以并行处理多个请求,大大减少了队头阻塞的问题。

2、服务器推送:

HTTP/2 还引入了服务器推送(server push)功能,允许服务器主动发送多个响应到客户端,而不需要客户端明确地为每个资源提出请求。这提高了页面加载的速度,因为相关资源可以被预先发送而无需等待浏览器请求。

3、域名分散(Domain Sharding):

这种技术常用于 HTTP/1.1 中,通过创建多个子域,使得浏览器可以同时开启更多的 TCP 连接来加载资源。虽然这种方法可以在一定程度上减轻队头阻塞,但它增加了复杂性,并且在 HTTP/2 中由于多路复用功能变得不再必要。

4、连接重用(Connection Reuse):

这是 HTTP/1.1 中的一个特性,即持久连接(Persistent Connections),允许在一次 TCP 连接中发送和接收多个 HTTP 请求和响应,而无需开启新的连接,从而减少了 TCP 握手的开销并提升了效率。

5、资源优化:

减少资源的大小通过压缩(如 GZIP),优化图片,减少 CSS 和 JavaScript 文件的大小等,可以减少队头阻塞的影响,因为小资源文件传输更快。

6、优先级设置:

HTTP/2 允许设置资源的加载优先级,使得关键资源(如 HTML,CSS,JavaScript)可以比不那么重要的资源(如图片,广告)更早加载。

7、预加载:

浏览器可以通过使用<link rel="preload">标签预加载关键资源,例如字体文件和关键脚本,这样可以确保它们在主要内容加载之前已经准备好。

8、HTTP/3 和 QUIC 协议:

HTTP/3 是未来的推进方向,它基于 QUIC 协议,一个在 UDP 之上的新传输层协议,旨在进一步减少延迟,解决 TCP/IP 协议的队头阻塞问题。

总的来说,HTTP/2 的特性如多路复用、服务器推送和优先级设置都有助于减少队头阻塞。而 HTTP/3 的引入可能会在未来为网络通信带来根本性的变化。在使用 HTTP/2、HTTP/3 和浏览器级别的优化时,网页开发者也需注意资源加载优化的最佳实践,以更全面地应对队头阻塞问题。

第13题 ---2024-08-14

题目

  1. 日志监控问题:可有办法将请求的调用源码地址包括代码行数也上报上去?

参考解答

在使用了代码混淆(例如 Webpack 的 mina-hash、chunkhash 或 contenthash)的前端代码中,即使执行了混淆,依然可以通过以下方法在日志监控时提供足够的上下文信息,主要包括被请求的源代码地址以及代码行数:

源码映射(Source Maps)

  1. 生成 Source Maps:在构建过程中生成功能强大的源码映射(Source Maps)文件是标准做法。Source Maps 主要用于将混淆、压缩后的 JavaScript 代码映射回到其原始版本,允许在浏览器调试工具中查看原始代码和追踪错误。

保存映射文件: 在生产版本中生成如.map的 Source Map 文件,并确保它们正常处理(通常是将它们放置在服务器上的一个公开但安全的位置)。

反映在 Source Maps 中的映射: Source Maps 文件应将原始的源文件路径和行号映射到构建后的代码中对应的位置。

  1. 错误跟踪系统集成:使用错误跟踪工具(也常被称为 Error Monitoring 平台, 如 Sentry、LogRocket、Bugsnag 等),这些工具通常兼容并支持 Source Maps:

    自动和源码追踪:漏洞和崩溃报告将自动包含被未混淆的源码引用,您只需确保生产版本的 Source Maps 配置正确。

    代码行号报告:用户报告的堆栈跟踪信息将包括对应底层源文件,而非混淆后的行号。

自定义错误日志逻辑

  1. 覆盖全局的错误处理器:对于更高级的错误追踪,你可能需要在前端代码中维护自定义的错误处理逻辑。

使用Window.onerrortry...catch:在Window.onerror中捕捉到运行时错误时,或者在自定义函数内try...catch捕获的错误,你可以从错误的堆栈跟踪中提取当前运行代码的位置,并尝试将符号化的堆栈信息发送到后端服务器。

  1. 在后端查阅符号化堆栈:为了安全和性能的考虑,源码映射通常不包括在客户端的部署中。因此固体堆栈信息需要在服务器端符号化,这是针对转换后的堆栈轨迹进行处理,将反向转换为源代码行。

注意:

确保 Source Maps 不公开到客户端以避免潜在的安全风险。应该将它们存放于受控的服务器环境,以避免源码泄露或不当使用。

以上方案更适合于开发或测试环境提供详细调试信息,确保在最终部署产品之前只公开给授权的人员。

第14题 ---2024-08-15

题目

  1. cookie 可以实现不同域共享吗?

参考解答

默认情况下,Cookie 不能在不同的顶级域名之间共享数据。

但是,如果两个域名属于同一主域名下的子域名,并且您设置了正确的 Domain 属性,那么在这些子域名之间是可以共享 Cookie 的。

例如,对于 sub1.example.com 和 sub2.example.com 这样的子域名,如果设置 Cookie 的 Domain 属性为 .example.com ,那么在这两个子域名之间,这个 Cookie 是可以共享和访问的。
然而,如果是完全不同的顶级域名,如 example.com 和 anotherdomain.com 之间,Cookie 是不能直接共享的。

此外,还需要注意 Cookie 的 Path 属性、安全属性(Secure)、HttpOnly 属性等,这些属性也会影响 Cookie 的使用范围和方式。

第15题 ---2024-08-19

题目

  1. 浏览器有同源策略, 但是为何 cdn 请求资源的时候不会有跨域限制?

参考解答

同源策略是啥

浏览器的同源策略(Same-origin policy)是一种重要的安全机制,用于限制不同源的文档或脚本之间的交互操作。

“源”(origin)由三部分组成:协议(protocol)、域名(domain)和端口(port)。如果两个 URL 的协议、域名和端口都完全相同,那么它们就是同源的;否则,就是不同源的。

同源策略的主要作用和规则如下:

  1. 阻止文档或脚本读取来自不同源的文档内容。比如,在 https://example.com/page1.html 页面中的 JavaScript 脚本,不能读取 https://anotherdomain.com/page2.html 的文档内容。
  2. 限制不同源的脚本之间的交互。不同源的脚本不能相互调用方法、访问对象属性或修改对方的 DOM(文档对象模型)。
  3. 限制跨源的网络请求。例如,使用 XMLHttpRequest 或 Fetch API 发起的网络请求,如果目标地址与当前页面的源不同,会受到同源策略的限制。不过,一些跨源请求可以通过 CORS(跨源资源共享,Cross-Origin Resource Sharing)机制来实现合法的跨源访问。

同源策略的目的是防止恶意网站窃取用户在其他网站上的敏感信息或进行未经授权的操作,保护用户数据的安全性和隐私性。

举个例子,如果没有同源策略,恶意网站 malicious-site.com 可能会在其页面中嵌入脚本,尝试读取用户正在访问的银行网站 bank-site.com 的页面内容,获取用户的账户信息、交易记录等敏感数据,这将给用户带来极大的安全风险。

浏览器有同源策略, 但是为何 cdn 请求资源的时候不跨域?

一些 CDN 资源不受同源策略限制,是因为同源策略主要是浏览器的一种安全机制,用于限制不同源的文档或脚本之间的交互操作,但对于某些特定类型的资源访问,浏览器会有一些例外情况。

在 HTML 中,<script><img><iframe><link>等标签的 src 属性所指向的资源(如 JavaScript 文件、图片、CSS 文件等)通常是可以跨域访问的。

例如,可以在自己的网站中通过<script src="https://cdn.example.com/vue.min.js"></script>加载来自 CDN 的 Vue.js 库。

这样做的主要原因是为了保证 Web 的开放性和可扩展性。如果这些资源也严格受到同源策略的限制,那么将所有相关资源都部署在同一个服务器下会违背 Web 开放的初衷,并且不利于资源的分发和缓存。

然而,虽然浏览器允许这些资源的跨域加载,但在加载 JavaScript 时,会限制对返回内容的读写权限,以防止恶意脚本获取或修改其他域的信息。

另外,CDN 服务提供商通常也会采取一些措施来确保资源的安全性和合法性。他们会对资源进行管理和配置,只允许合法的请求访问资源,并防止恶意使用或滥用 CDN 资源。

需要注意的是,同源策略仍然是非常重要的安全机制,它能有效防止恶意网站窃取用户在其他网站上的敏感信息或进行未经授权的操作。在涉及到敏感数据的交互或需要更严格安全控制的情况下,仍然需要遵循同源策略或采用适当的跨域解决方案,如 CORS(跨域资源共享)等。CORS 通过在服务器端设置响应头,明确允许哪些源可以访问资源,从而在保证安全的前提下实现跨域数据交互。

img src 指向的任何资源都是可以跨域访问吗?

一般情况下,<img> 元素的 src 属性指向的资源是可以进行跨域访问的。

当您在网页中使用 <img> 标签加载图片资源时,浏览器会向指定的 URL 发送请求获取图片数据,并将其显示在页面上,即使该资源的源与当前页面的源不同。

然而,也存在一些特殊情况和限制:

  1. 如果图片资源所在的服务器设置了严格的访问控制策略(例如通过服务器端的配置限制某些来源的访问),那么可能会导致您的请求被拒绝。
  2. 对于一些受版权保护或有特殊访问权限要求的图片资源,如果您没有相应的授权或访问权限,即使浏览器本身允许跨域访问,您也无法获取和显示这些图片。

总的来说,在大多数常规情况下,<img> 元素的 src 属性指向的图片资源能够实现跨域访问,但需要考虑服务器端的配置和资源本身的权限要求等因素。


夕水
5.2k 声望5.7k 粉丝

问之以是非而观其志,穷之以辞辩而观其变,资之以计谋而观其识,告知以祸难而观其勇,醉之以酒而观其性,临之以利而观其廉,期之以事而观其信。