“略偏但狠重要”的前端面试题,让我们举一反三儿~

2,053 阅读1小时+

+ 对SSR的理解?

SSR 全称为 Server-Side Rendering,即服务端渲染。它是一种将 React 组件在服务端渲染成 HTML 字符串并发送到浏览器的技术。

通常情况下,React 应用程序都是在浏览器中进行渲染的。当浏览器加载页面时,会下载 JavaScript 脚本,执行脚本,最终生成 DOM,并展示出来。然而,这种方式有一个明显的缺点,就是客户端需要等待 JavaScript 都下载和解析完成后才能看到页面内容,因此可能会导致首屏加载速度较慢,特别是在网络条件较差的情况下。

而使用 SSR 技术,可以在服务端生成完整的 HTML 页面,并将其直接发送给客户端,从而避免了客户端等待 JavaScript 下载和解析的过程。这样可以提高页面的首屏加载速度,也可以改善 SEO(Search Engine Optimization,搜索引擎优化)效果。

与传统的客户端渲染相比,这种渲染方式的优点是:

  • 提高页面首屏加载速度
  • 改善 SEO 效果,有利于搜索引擎爬虫抓取页面内容。

当然,SSR 也有一些缺点,例如:

  • SSR增加了服务器压力
  • 部分生态系统中的库需要进行不同的使用方式或适配,如 React Router 等。

因此,在使用 SSR 技术时,需要权衡其优缺点,根据具体场景做出选择。

追问:知道哪些常见的SSR框架?

在服务器端渲染 (Server-Side Rendering, SSR) 领域,有几个常见的框架和工具可供选择。以下是其中一些常见的 SSR 框架:

  1. Next.js:Next.js 是一个基于 React 的 SSR 框架,提供了简单的配置和默认约定,使得构建 SSR 应用变得非常容易。它支持热模块替换、文件系统路由、静态导出以及动态路由等功能。
  2. Nuxt.js:Nuxt.js 是一个基于 Vue 的 SSR 框架,类似于 Next.js。它为 Vue 应用提供了服务器端渲染的能力,并且还集成了许多有用的功能,如自动生成路由、代码拆分、静态导出等。
  3. Gatsby:Gatsby 是一个基于 React 的静态网站生成器,也可以用于 SSR。它使用 GraphQL 查询方式获取数据,并通过预渲染技术生成静态 HTML 文件,提供了快速加载和 SEO 优化的优势。
  4. Angular Universal:Angular Universal 是 Angular 官方提供的 SSR 解决方案。它允许在服务器端渲染 Angular 应用,并使其在客户端进行交互。Angular Universal 提供了预渲染、数据预取等功能,并与 Angular 框架紧密集成。

除了上述框架外,还有其他一些 SSR 相关的工具和库:

  • Express.js:Express.js 是一个流行的 Node.js Web 应用框架,它能够与 SSR 应用很好地配合使用。你可以使用 Express.js 构建一个简单的服务器,并将 SSR 应用挂载在其中
  • Koa.js:Koa.js 是另一个 Node.js Web 框架,它提供了一种更现代化和精简的方式来构建 Web 应用程序。类似于 Express.js,你可以使用 Koa.js 来创建一个服务器,并实现 SSR 功能

这些框架和工具各有特点,你可以根据自己的需求和技术栈选择适合的 SSR 解决方案。无论你选择哪个框架,SSR 可以提供更好的首次加载性能、SEO 优化以及更好的用户体验。

追问:Koa后台如何实现一个简单的SSR?

下面是一个使用 Koa 实现简单的 SSR 的例子:

const Koa = require('koa');
const Router = require('koa-router');
const ReactDOMServer = require('react-dom/server');
const React = require('react');

const app = new Koa();
const router = new Router();

// 定义一个 React 组件
function HelloWorld() {
  return React.createElement('h1', null, 'Hello World from SSR');
}

// 处理首页路由
router.get('/', async (ctx) => {
  // 将 React 组件渲染为 HTML 字符串
  const html = ReactDOMServer.renderToString(React.createElement(HelloWorld));
  
  // 返回渲染后的 HTML 字符串
  ctx.body = `
    <!DOCTYPE html>
    <html>
      <head>
        <title>SSR Example</title>
      </head>
      <body>
        <div id="root">${html}</div>
        <script src="/bundle.js"></script>
      </body>
    </html>
  `;
});

app.use(router.routes()).use(router.allowedMethods());

app.listen(3000, () => {
  console.log('Server is running on http://localhost:3000');
});

上述代码使用 Koa 创建了一个简单的服务器,并定义了一个路由处理根路径 /。当用户访问根路径时,服务器会将 React 组件 HelloWorld 渲染为 HTML 字符串,并将其返回给客户端。

需要注意的是,上述示例中使用了 ReactDOMServer.renderToString 方法来将 React 组件渲染为 HTML 字符串,这是 SSR 最关键的一步。在服务器端渲染中,我们需要将 React 组件渲染为静态 HTML,以便在客户端发送到浏览器之前就能够展示出来。

另外,还需要将生成的 HTML 字符串嵌入到一个完整的 HTML 页面中,并引入客户端 JavaScript 文件。这样客户端 JavaScript 文件可以在浏览器中重新挂载并处理事件交互。

此外,你还需要通过 Babel 或其他工具将 JSX 语法转换为普通的 JavaScript。以上代码只提供了 SSR 的基本实现思路,如果要在生产环境中使用,还需要进行更多的配置和优化。

image.png


+ Vue模版编译原理是怎样的?

Vue 的模板编译原理可以分为以下三个主要阶段:

  1. 解析:Vue 使用 HTML 解析器 将模板字符串解析成抽象语法树AST)。在 AST 中,每个节点表示模板中的一个标记,例如元素、文本和指令等。解析器还会对模板中的表达式进行静态分析和优化,生成具体的代码。
  2. 优化:AST 中的节点被进一步转换为可执行的渲染函数,并且在这个过程中,一些静态节点和属性将被标记为静态,以便在后续渲染中可以跳过它们的比较过程,从而提高渲染性能。
  3. 代码生成:通过将 AST 转换为 Render 函数,最终生成可在浏览器中执行的 JavaScript 代码。这个过程包括将静态节点和属性抽离出来,为动态节点和属性生成响应式代码,并将所有的生成代码合并为单个函数,以便在运行时被调用。

总的来说,Vue 的模板编译原理的主要目的是将模板编译为可执行的 JavaScript 代码,并将其与组件实例关联起来。Vue 通过这个过程实现了模板到渲染函数的转化,使得开发者可以更加方便地使用声明式的模板来描述用户界面,并且不用关注底层的数据和状态处理细节。

+ 追问:Vue中render函数和h函数有何异同

在 Vue 中,render 函数和 h 函数都是用来生成虚拟 DOM 的函数,但它们有一些不同之处。

h 函数是 Vue 提供的创建虚拟节点(VNode)的辅助函数,定义如下:

h(tag, props, children)

其中:

  • tag:表示标签名或组件;
  • props:表示属性对象;
  • children:表示子节点数组。

例如:

h("div", {class: "container"}, [
  h("h1", "Title"),
  h("p", "Paragraph")
])

以上代码生成的 VNode 就是一个 div 标签,其 class 为 container,其中包含了两个子节点,分别是一个 h1 标签和一个 p 标签。

而 render 函数则是开发者手动编写的用于生成虚拟节点的函数,其形式如下:

render(h) {
  return h("div", {class: "container"}, [
    h("h1", "Title"),
    h("p", "Paragraph")
  ])
}

可以看出,在 render 函数中,我们也可以使用 h 函数来生成虚拟节点。需要注意的是,在 render 函数中,我们通常会在其中对数据进行处理,并使用 JavaScript 表达式来生成虚拟节点,从而实现动态的渲染效果。而且,通过 render 函数,我们可以访问到组件的上下文和数据,可以更加灵活地控制组件的渲染过程。

综上所述,h 函数和 render 函数都用于生成虚拟节点,但 h 函数是 Vue 提供的辅助函数,而 render 函数是开发者手动编写的函数,可以更加灵活地控制组件的渲染过程。


+ Vue template 到 render 的过程?

在 Vue 中,从模板(Template)到渲染函数(Render Function)的过程涉即模板编译过程:

  • Vue 的编译器会将模板字符串解析成抽象语法树(AST),然后通过遍历 AST 的方式生成一个渲染函数。
  • 渲染函数是一个 JavaScript 函数,它描述了组件的渲染逻辑。渲染函数接收一个参数,即上下文对象(包含了组件的数据、方法等)。
  • 当渲染函数执行时,它会根据上下文对象的数据来生成虚拟 DOM。

整个过程可以概括为:模板字符串 -> AST -> 渲染函数 -> 虚拟DOM

需要注意的是,Vue 在首次渲染时会将模板编译为渲染函数并缓存起来,后续的渲染过程中会直接使用该渲染函数,而不需要再次进行模板编译。这样可以提高性能。

追问:模板是怎么变成AST的?

要将一个简单的模板字符串解析为 AST,可以使用 Vue 的编译器工具来实现。下面是一个示例:

const { compile } = require('@vue/compiler-dom');

const template = '<div>Hello, {{ name }}!</div>';
const ast = compile(template).ast;
console.log(ast);

在上述代码中,我们首先引入了 @vue/compiler-dom 模块,并获取了其中的 compile 函数。然后,我们定义了一个简单的模板字符串 template,其中包含了一个插值表达式 {{ name }}

接下来,我们调用 compile 函数,并将模板字符串作为参数传递给它。compile 函数会返回一个编译结果对象,其中的 ast 属性就是我们需要的抽象语法树(AST)。

最后,我们将 ast 打印输出,可以看到解析后的 AST 的结构。

对于上述示例,打印输出的结果可能如下所示:

{
  "type": 1,
  "tag": "div",
  "props": [],
  "children": [
    {
      "type": 2,
      "content": "Hello, ",
      "text": "Hello, "
    },
    {
      "type": 5,
      "content": "{{ name }}"
    },
    {
      "type": 2,
      "content": "!",
      "text": "!"
    }
  ],
  "loc": {
    "start": { "line": 1, "column": 0 },
    "end": { "line": 1, "column": 24 }
  }
}

这个 AST 表示了一个 <div> 元素,它包含了三个子节点:一个文本节点("Hello, ")、一个插值表达式节点("{{ name }}")和另一个文本节点("!")。

这只是一个简单的例子,Vue 的编译器还支持更复杂的模板语法,例如条件渲染、循环、事件处理等。通过将模板字符串传递给 compile 函数,并使用返回结果中的 ast 属性,我们可以获得相应的抽象语法树,以便于进一步的处理和分析。

追问:AST又是怎么变成渲染函数的呢?

在 Vue 中,编译器将模板字符串解析为抽象语法树(AST),然后通过遍历 AST 的方式生成渲染函数。渲染函数描述了组件的渲染逻辑,它是一个 JavaScript 函数。

下面是一个简单的示例,演示了如何将 AST 转换为渲染函数:

const { compile } = require('@vue/compiler-dom');

const template = '<div>Hello, {{ name }}!</div>';

// 将模板字符串编译为 AST
const ast = compile(template).ast;

// 递归遍历 AST,生成渲染函数
function generateRenderFn(ast) {
  if (ast.type === 1) {
    // 元素节点
    const code = `h('${ast.tag}', {}, [${generateChildren(ast.children)})]`;
    return `function() { return ${code}; }`;
  } else if (ast.type === 2) {
    // 文本节点
    const code = JSON.stringify(ast.content);
    return `function() { return ${code}; }`;
  } else if (ast.type === 3) {
    // 插值表达式节点
    const code = `this[name]`;
    return `function() { return ${code}; }`;
  }
}

// 递归遍历子节点,生成子节点的渲染函数代码
function generateChildren(children) {
  return children.map(child => generateRenderFn(child)).join(', ');
}

// 生成渲染函数
const renderFn = generateRenderFn(ast);

console.log(renderFn);

在上述代码中,我们首先定义了一个简单的模板字符串 template,其中包含一个 <div> 元素和一个插值表达式 {{ name }}

然后,我们调用 compile 函数将模板字符串编译为 AST,并将其保存在变量 ast 中。

接下来,我们定义了一个名为 generateRenderFn 的函数,它用于递归地遍历 AST 并生成渲染函数的代码。根据节点的类型(元素节点、文本节点、插值表达式节点),我们生成对应的渲染函数代码。

另外,我们还定义了一个辅助函数 generateChildren,用于遍历子节点并生成它们的渲染函数代码。

最后,我们调用 generateRenderFn 函数,传入根节点的 AST,即 ast,生成了最终的渲染函数代码,并将其存储在变量 renderFn 中。你可以打印输出 renderFn 来查看生成的渲染函数的代码。

大致如下:

function() {
  return h(
    'div',
    {},
    [
      function () {
        return "Hello, " + this[name] + "!";
      }
    ]
  );
}

这个渲染函数执行后得到的虚拟DOM大致如下:

{
  tag: 'div',
  props: {},
  children: [
    function () {
      return "Hello, " + this[name] + "!";
    }
  ]
}

虚拟DOM在后期经过简单解析就可以生成真实DOM了!

image.png


+ JS进程之间的通信方式是什么?

在 JavaScript 中,进程之间的通信主要有以下几种方式:

  1. SharedWorker API:SharedWorker 是一种单独的进程,可以同时为多个浏览器标签页提供服务。它允许不同标签页之间共享数据,通过 postMessage 方法实现通信。
  2. Web Workers API:Web Workers 是另一种单独的进程,用于在后台执行 JavaScript 代码,以避免主线程被阻塞。与 SharedWorker 不同,Web Worker 只能与创建它的页面通信,其他页面无法使用 postMessage 和 onmessage 事件直接访问它。
  3. MessageChannel API:MessageChannel 是一个通信渠道,可以在两个不同的进程之间进行通信。通过创建两个 MessageChannel 实例并相互交换,两个进程之间可以发送和接收消息。
  4. BroadcastChannel API:BroadcastChannel 是一种跨 tab 通信的方式,能够在同一浏览器内的不同标签页之间传递信息,使用广播(broadcast)的方式将消息传递给所有的监听者。
  5. LocalStorage:LocalStorage 是一个 HTML5 Web 存储 API,可以让浏览器存储键值对,以便在不同的标签页之间进行通信。但是,LocalStorage 的读写速度较慢,不适合高频数据交换的场景。

综上所述,JavaScript 进程之间的通信方式主要有 SharedWorker API、Web Workers API、MessageChannel API、BroadcastChannel API 和 LocalStorage 等,开发者需要根据具体情况选择合适的通信方式。


+ 简述HTTPS的特点?

HTTPS(Hyper Text Transfer Protocol Secure)是一种安全的 HTTP 协议,它通过 SSL/TLS 加密协议来保证数据的安全传输。相较于 HTTP,HTTPS 具有以下几个特点:

  1. 数据传输加密:HTTPS 使用 SSL/TLS 加密协议对数据进行加密传输,可以保证第三方无法获取敏感数据,确保数据传输的机密性。
  2. 身份认证:HTTPS 可以使用数字证书对服务器和用户进行身份认证,防止中间人攻击,确保通信双方的身份真实可信。
  3. 数据完整性校验:HTTPS 使用消息摘要算法和数字签名技术对数据进行实时完整性校验,确保在传输过程中数据没有被篡改或者损坏。
  4. 保证用户隐私:HTTPS 可以有效地防止黑客利用网络嗅探等手段窃取用户的敏感信息,比如密码等。

综上所述,HTTPS 在数据传输、身份认证、数据完整性校验和保护用户隐私等方面都具有强大的安全性能。在现代互联网中,HTTPS 已经成为保证网络信息安全最普遍且有效的方式之一。

追问:HTTPS协议握手的过程是怎样的?

HTTPS 协议的握手过程是建立安全通信的重要环节,它主要包括以下 4 个步骤:

  1. 客户端向服务器发送一个 SSL/TLS 版本号、加密算法优先级以及一个随机数作为客户端的“预备信封”。
  2. 服务器接收到客户端发送的信息后,向客户端发送一个 SSL/TLS 版本号、加密算法优先级、数字证书(公钥)以及一个随机数作为服务器的“预备信封”。
  3. 客户端接收到服务器发送的信息后,会根据服务器提供的数字证书向CA机构验证服务器的身份真实性。如果验证通过,则会生成一个新的随机数,然后使用服务器提供的公钥对该随机数进行加密并发送给服务器
  4. 服务器接收到客户端发来的加密随机数后,通过自己的私钥解密得到该随机数,并使用该随机数来生成一个加密密钥。然后,服务器再使用约定密钥加密算法将一个握手确认消息发送给客户端,客户端同样使用约定的密钥解密该消息,确保握手完成

总体来说,HTTPS 握手的过程是通过交换加密算法、数字证书和随机数等信息来建立起安全连接。这一过程可以防止中间人攻击、保护用户隐私等重要作用

追问:如何构建一个基于HTTPS通信的后台呢?

要构建一个基于 HTTPS 通信的后台,需要做以下几个步骤:

  1. 获取 SSL/TLS 证书:为了启用 HTTPS 连接,需要从可信的 SSL/TLS 证书颁发机构获取证书。可以选择购买或者使用免费的证书来保证通信的安全性。
  2. 配置服务器:在服务器上安装 SSL/TLS 证书,并通过服务器软件(比如 Apache 或 Nginx)来配置 HTTPS 连接。具体的步骤可以参考相应的服务器软件官方文档进行配置。
  3. 修改前端代码适配 HTTPS:在应用程序中将 HTTP 请求改为 HTTPS 请求,并确保所有资源都通过 HTTPS 加载,包括图片、样式文件等。
  4. 检查应用程序安全性:确保应用程序没有安全漏洞,以防止攻击者通过 HTTPS 连接获得敏感信息。
  5. 使用 CDN 进一步优化性能:使用 CDN(内容分发网络)可以进一步提高网站的性能和安全性。

总的来说,构建一个基于 HTTPS 通信的后台需要一定的技术知识和经验,需要处理证书、配置服务器、修改代码适配 HTTPS 等多个方面。但是,通过这些努力,可以进一步加强后台的安全性和稳定性。


+ HTTPS是如何保证安全的?

HTTPS 是基于 SSL/TLS 协议的安全传输协议,其主要通过以下几种方式来保证数据的安全性:

  1. 加密传输:HTTPS 使用 SSL/TLS 协议,对传输的数据进行加密,通过使用公钥和私钥,确保只有预期的接收方能够解密并读取数据。在数据传输过程中,黑客无法轻易地截获、查看或篡改通信内容。
  2. 身份验证:HTTPS 通过数字证书来验证服务器和客户端的身份。证书由受信任的第三方机构颁发,即 CA(Certificate Authority),确保服务器是可信的,并且服务器正在与正确的客户端进行通信。
  3. 完整性保护:HTTPS 使用消息认证码(Message Authentication Code,MAC)对数据完整性进行校验。当数据在传输过程中被篡改时,接收方会发现 MAC 校验失败,从而拒绝接受数据。
  4. 防止重放攻击:HTTPS 在握手过程中,会生成一个随机数,用于确保每次通信所传输的数据不同,从而避免了重放攻击。

综上所述,HTTPS 通过加密传输、身份验证、完整性保护和防止重放攻击等多种安全机制来保证数据的安全。虽然黑客和病毒可以通过一些手段攻破 HTTPS,但这需要对 SSL/TLS 协议有深入的了解,并掌握一些高级的攻击技术。因此,HTTPS 仍然是目前应用最广泛、安全性最高的传输协议之一。


追问:HTTPS如何防御中间人攻击呢?

HTTPS 通过使用 SSL/TLS 协议,可以防御中间人攻击。中间人攻击是指黑客冒充服务器或者客户端与对方进行通信的一种攻击方式。攻击者可以获取通信双方之间的信息并篡改这些信息,使得双方不知情。下面是 HTTPS 如何防御中间人攻击的原理:

  1. 数字证书验证:在 HTTPS 的握手阶段,客户端会向服务器发送请求,要求服务器提供数字证书。该数字证书是由第三方机构颁发的,可以用于验证服务器的身份。如果客户端发现服务器的证书无效,就会断开连接,从而避免了中间人攻击。
  2. 对称加密传输:在 HTTPS 握手成功之后,客户端和服务器会使用共同协商的对称加密算法对数据进行加密。这个对称加密算法是使用服务器公钥加密的,只有服务器才能解密。因此,即使攻击者截获了加密数据,也无法破解。
  3. 完整性保护:在 HTTPS 握手成功之后,客户端和服务器会通过 TLS 计算出一个摘要值(Message Authentication Code,MAC)来保证加密数据的完整性,如果数据被篡改,MAC 值就会变化,从而让客户端意识到通信中有篡改行为。这种机制可以有效避免中间人攻击,并保护通信双方的数据不被篡改。

综上所述,HTTPS 使用数字证书验证、对称加密传输和完整性保护等多种方式来防御中间人攻击。这些安全机制确保客户端与服务器之间的通信不会被黑客截获或篡改,从而保证了通信的安全性和可靠性。

image.png


+ React setState 调用的原理

React 的 setState 是用于更新 React 组件中 state 状态的方法,其调用的过程可以总结为以下几个步骤:

  1. 处理参数:React 首先会检查 setState 方法中传递的参数,如果参数不是一个对象或者函数,React 会直接返回并不执行。
  2. 添加到更新队列:如果参数是一个函数或者对象,React 会将其添加到组件的更新队列中。React 使用 Fiber 架构对组件的更新进行调度,并使用 Virtual DOM 进行快速比较和渲染。
  3. 批量更新:在同一周期内,如果组件的多个 state 发生了变化,React 会将这些变化合并成一个批量更新,减少重复渲染的次数。
  4. 触发 updating 生命周期函数:在更新队列中的 state 更新被执行后,React 会触发组件的 componentWillUpdate 生命周期函数。在这个生命周期方法中,开发者可以实现相关的操作,例如数据统计、性能监控等。
  5. 更新 UI:在 componentWillUpdate 生命周期函数执行完毕后,React 会根据新的 state 值重新渲染组件,并更新 UI。
  6. 触发 updated 生命周期函数:如果组件的 UI 更新成功,React 会触发 componentDidUpdate 生命周期函数。开发者可以在这个生命周期方法中实现相关的操作,例如自定义动画效果等。

总之,setState 方法的调用会触发 React 的生命周期方法和更新机制,最终实现 state 变量的更新,并更新组件的 UI。由于 React 使用了 Virtual DOM,因此可以极大地提高应用程序的性能并保证了渲染效率。


+ 对有状态组件和无状态组件的理解及使用场景?

在 React 中,组件可以分为两类:有状态组件和无状态组件。它们的区别主要体现在组件的状态管理、渲染方式和使用场景等方面。

  1. 有状态组件

有状态组件(Stateful Component)是指组件中包含状态变量和生命周期函数的组件。这类组件可以通过 setState 方法来更新自己的状态,并且可以响应生命周期函数来处理事件。有状态组件通常用来处理一些需要状态管理和复杂业务逻辑的场景,例如表单数据的处理、网络请求等。

  1. 无状态组件

无状态组件(Stateless Component)是指组件不包含状态变量和生命周期函数的组件。这类组件仅接受 props 作为参数,并根据 props 的值来渲染组件的 UI。无状态组件通常用来实现简单的 UI 组件或者展示组件,这类组件没有副作用,更容易进行测试和重构。

下面是有状态组件和无状态组件的使用场景:

  • 使用有状态组件的场景:
  1. 当需要使用组件内部状态来控制组件显示或者行为的时候,可以使用有状态组件。
  2. 当需要在组件生命周期内执行某些操作、访问 DOM 或者更新组件状态等复杂情况时,可以使用有状态组件。
  3. 当需要使用异步请求和事件处理等功能时,可以使用有状态组件。
  • 使用无状态组件的场景:
  1. 当需要实现简单的 UI 组件时,如按钮、链接等,可以使用无状态组件。
  2. 当需要展示数据但不需要与其交互时,如列表、卡片等,可以使用无状态组件。
  3. 当需要将数据格式化为 UI 展示给用户时,可以使用无状态组件。

综上所述,有状态组件和无状态组件各有其优缺点和适用场景。在实际开发中,根据项目需求,灵活地选择使用有状态组件和无状态组件,可以提高开发效率和代码质量。


+ 在React中如何避免不必要的render?

在 React 中,render 方法是实现组件 UI 渲染的核心方法,因为每次组件状态发生变化时都会触发 render 方法重新渲染组件,并生成新的 Virtual DOM。如果不注意优化,渲染操作可能会导致性能问题。下面介绍一些避免不必要的 render 的方法:

  1. 根据 shouldComponentUpdate 生命周期方法控制是否重新渲染:shouldComponentUpdate 方法可以用来控制组件是否需要重新渲染,如果 shouldComponentUpdate 返回 false,则该组件不会进行下一步的更新操作,提高了组件的渲染效率。
  2. 使用 React.memo 组件包裹有状态组件: React.memo 是一个高阶组件,它用于包裹函数式组件,可以帮助避免不必要的渲染。使用 React.memo 会对组件进行浅层比较,只有 props 发生变化时才会重新渲染。
  3. 使用 PureComponents 代替普通的组件:PureComponents 是 React 提供的内置组件,继承自 Component,它可以帮助我们实现“浅比较”,只有 props 和 state 发生变化时才会重新渲染组件。
  4. 使用 Immutable.js 等第三方库管理组件的 state 对象,从而更好地控制组件是否需要重新渲染。
  5. 合理利用 key 属性:在使用列表渲染等场景时,给每个子元素添加 key 属性可以帮助 React 更好地管理组件结构,从而提高组件的渲染效率。

综上所述,以上是一些避免不必要的 render 的方法。在实际开发中,需要根据具体情况选择合适的优化方式来提高组件渲染效率。

追问:用过memo和useMemo吗?

React.memo 和 useMemo 都是 React 提供的优化组件性能的钩子函数,但它们的使用场景略有不同。以下是一个带有示例代码的 React.memo 和 useMemo 对比的例子:

我们假设有两个组件:ParentComponent 和 ChildComponent。ParentComponent 会传递一个 num 参数给 ChildComponent,然后 ChildComponent 根据 num 参数渲染一个复杂的表格。同时,ParentComponent 中也有一个 count 参数,表示 ParentComponent 自身的状态。每次 count 变化时,ParentComponent 和 ChildComponent 都会重新渲染。

我们可以使用 React.memo 来避免无意义的 ChildComponent 的重复渲染。我们在使用 React.memo 包裹 ChildComponent 后,只有当 num 参数发生变化时,ChildComponent 才会重新渲染。

import React, { memo } from 'react';

const ChildComponent = (props) => {
  const renderTable = () => {
    // 一个根据 num 参数渲染表格的复杂函数
  }
  
  return (
    <div>
      {renderTable()}
    </div>
  )
}

export default memo(ChildComponent);

而使用 useMemo,我们可以缓存一个依赖于 num 参数的昂贵计算结果,从而避免 ChildComponent 重复执行这个计算过程。

import React, { useMemo } from 'react';

const ChildComponent = (props) => {
  const tableData = useMemo(() => {
    // 一个昂贵的计算,依赖于 num 参数
    // 返回的结果缓存在 tableData 中
  }, [props.num]);
 
  return (
    <div>
      {tableData}
    </div>
  )
}

综上所述,React.memo 主要用于避免因为父组件状态变化导致的不必要的子组件渲染,而 useMemo 则是用于避免重复计算一些昂贵的操作或函数,提升组件的性能表现。

追问:是否用过useCallback呢?

useCallback 是 React 提供的一个钩子函数,它用于优化函数组件中函数的性能表现。它的主要作用是缓存一个函数依赖的变量,并且只在这些变量发生改变时再创建新的函数。这样可以避免因为不必要的函数重复创建导致的性能问题。

以下是一个示例,假设有一个 ChildComponent 组件接收一个 handleClick 函数作为 props,然后在按钮被点击时调用它。由于 handleClick 函数可能需要依赖于大量的 props 变量,我们可以使用 useCallback 来避免不必要的函数创建和重复渲染:

import React, { useCallback } from 'react';

const ChildComponent = (props) => {
  const { num, text, handleClick } = props;

  const handleClickMemoized = useCallback(() => {
    // 在这里执行其他逻辑
    handleClick(num, text);
  }, [num, text, handleClick]);

  return (
    <div>
      <button onClick={handleClickMemoized}>Click me</button>
    </div>
  );
}

export default ChildComponent;

在上述示例中,handleClickMemoized 是使用 useCallback 缓存的一个事件处理函数,它会根据 num、text 和 handleClick 这三个参数来决定是否需要重新创建新的函数。而在 handleClickMemoized 函数中,我们可以执行其他的逻辑,但只要其依赖的 num、text 或 handleClick 发生变化时才会重新创建新的函数。

通过使用 useCallback,我们避免了不必要的函数重复创建和渲染带来的性能问题。

image.png


+ DNS完整的查询过程?

域名系统(DNS)是互联网上最为重要的基础设施之一,它负责将域名映射为 IP 地址,为用户提供快速的访问服务。域名解析的过程也是 DNS 的核心功能之一。

DNS 解析一个域名的详细过程如下:

  1. 用户在 Web 浏览器里输入域名,比如www.example.com,并按下回车键。
  2. 客户端本地的 DNS 缓存会首先查找该域名是否已经缓存,如果有,则直接返回对应的 IP 地址;如果没有,则向本地 DNS 服务器发送一个 DNS 查询请求。
  3. 本地 DNS 服务器接收到 DNS 查询请求后,会首先检查自身缓存是否有该域名对应的 IP 地址记录。如果有,则直接返回对应的 IP 地址;如果没有,则向根域名服务器发送一个 DNS 查询请求。
  4. 根域名服务器接收到 DNS 查询请求后,会返回下一级 DNS 服务器(顶级域名服务器)的地址。
  5. 本地 DNS 服务器接收到顶级域名服务器的地址后,会向其发送一个 DNS 查询请求。
  6. 顶级域名服务器接收到 DNS 查询请求后,会返回该域名对应的权威 DNS 服务器的地址。
  7. 本地 DNS 服务器接收到权威 DNS 服务器的地址后,会向其发送一个 DNS 查询请求。
  8. 权威 DNS 服务器接收到 DNS 查询请求后,会查询该域名对应的 IP 地址,并返回给本地 DNS 服务器。
  9. 本地 DNS 服务器接收到 IP 地址信息后,会将其缓存,并返回给客户端。
  10. 客户端收到 IP 地址信息后,就可以使用该地址与服务器建立 TCP 连接,获取网页信息并显示在浏览器中。

image.png

追问:这里的本地DNS服务器到底位于哪里?

本地 DNS 服务器指的是您连接到的本地网络中负责处理 DNS 查询的服务器,通常由 互联网服务提供商(ISP)组织内部网络管理员来管理和维护。它们通常位于 ISP 的数据中心或某些较大的组织中心办公室。当您连接到网络时,您的设备会自动从您的 ISP 中获取 DNS 服务器的地址,并使用该服务器进行查询。

可以通过以下方法查看您当前正在使用的 DNS 服务器:

在 Windows 操作系统下,在命令提示符或 PowerShell 中运行 ipconfig /all 命令,查找“DNS 服务器”字段并查看对应的 IP 地址。

在 MacOS 操作系统下,在终端中运行 networksetup -getdnsservers Wi-Fi 命令,查找输出中的 DNS 服务器 IP 地址。

在 Linux 操作系统下,可以通过 cat /etc/resolv.conf 文件查找 DNS 服务器地址。

需要注意的是,如果您使用的是公共 DNS 服务器(如 Google Public DNS 或 Cloudflare DNS),则您的 DNS 查询将不会经过本地 DNS 服务器。相反,您的设备将直接查询公共 DNS 服务器并返回结果。

PS:宁迭代,莫递归 image.png

image.png


+ 对 React和 Vue 的理解,它们的异同如何?

React 和 Vue 都是比较流行的前端框架,它们都具有将数据和视图进行关联、允许组件化开发等特点,但也具有一些不同之处。

React 是由 Facebook 开发的 JavaScript 库,它采用了虚拟 DOM 的概念,通过对虚拟 DOM 进行操作来更新实际的 DOM 元素,从而提高应用的性能。React 可以与其他库或框架进行整合,如 Redux 等,使其更加灵活和可扩展。React 的学习曲线可能相对较陡峭,需要掌握 JSX 语法、生命周期等概念,但也可以提高开发者对于组件化设计和函数式编程的理解。

Vue 是一款渐进式 JavaScript 框架,它允许将应用程序分解为多个组件,这些组件可以互相组成更高层次的组件。Vue 采用了指令的方式来操作 DOM,使得代码可读性更高,并且相对于 React 来说,它的学习曲线可能更加平缓。同时,Vue 还内置了 Vuex 管理库和 Vue 路由器,使得在大型应用开发中集成各种功能变得更加方便。

总体上来说,React 更加底层,需要学习和编写更多的 JavaScript 代码,但也可以更好地掌握底层实现细节Vue 更加注重开箱即用的体验提供了很多内置功能,使得开发变得更加轻松和快速。两者都各有优劣势,开发者可以根据具体情况选择适合自己的框架。

追问:为什么大企业往往更倾向于选择React而非Vue呢?

大企业在选择前端框架时,需要考虑各种因素,包括代码质量可维护性可扩展性应用性能等等。在这些因素中,React 框架通常比 Vue 更受欢迎的原因如下:

  1. 全球社区支持度高:React 拥有庞大的社区,并受到全球范围内的广泛关注。无论是开发工具、插件还是各类技术文档,React 的社区都给予了很大的支持,这使得在使用 React 时可以更加轻松地获得帮助和解决问题。
  2. 可定制性更强:React 可以与其他库或框架进行整合,整合起来会更加灵活,同时可以满足更多不同需求的组件构建。这也为大企业提供了更好的模块化开发体验,能够更好地对应复杂多变的业务场景。
  3. 更加规范的生态圈:React 的生态圈更为成熟,相关的技术标准和最佳实践也更为统一。这也意味着,大型企业采用 React 可以更方便地规范化开发和维护,防止代码风格和程度管理方面的混乱。

当然,这并不意味着 Vue 不适合大型企业,事实上,Vue 也具备可扩展性、组件化构建等优点,并且它在性能和易用性方面的表现也非常出色。具体来说,选择前端框架需要根据公司实际情况来做决策,这包括开发团队的技能水平、希望使用的技术栈、项目规模和业务需求等因素。

追问:Angular有死灰复燃之势,你怎么看?

Angular 是由 Google 所推出的一款前端框架,它拥有完整的解决方案,包括了模板语法、组件化构建、依赖注入等诸多功能。虽然在 React 和 Vue 的激烈竞争下,Angular 相对来说显得有些落后,但实际上 Angular 也正逐渐复兴,并迎来了一些新的发展机会。

值得一提的是,Angular 在某些特定的场景下仍然具有非常优秀的表现。比如,大规模应用程序的开发、多人协作开发等方面,Angular 独特的依赖注入强类型化等特性可以帮助企业更好地进行代码组织和代码管理。并且,Angular 可以同时支持 Web 应用和移动应用的开发,使得开发者可以一次编写,多平台使用。

此外,在公共利益领域,例如政府机构、学校、医院等机构的信息化系统中,安全稳定和可靠性往往比较重要。Angular 的严格的类型检查强制性的编码规范等特点,可以在这些领域中体现出优越的表现,并且和前两个更加灵活的框架相比,Angular 更容易被落地推广使用。

综上,虽然目前在前端开发中,React 和 Vue 占据了相当一部分市场份额,但是 Angular 作为一款经过验证的框架,仍然有自己独特的优势和发展机会。

image.png

+ 虚拟DOM的解析过程是怎样的?

Vue 中的虚拟 DOM 部分和 React 的类似,但是在具体实现上有一些不同。下面是 Vue 中虚拟 DOM 的解析过程:

  1. Vue 组件中的模板代码会被编译成渲染函数(render function),这个函数返回一个虚拟 DOM 树
  2. 实例化过程中,Vue 会把这个虚拟 DOM 树转换成一份真正的、响应式的 DOM 树,并在之后对它进行操作。
  3. 数据发生变化时,Vue 通过内部的依赖追踪系统来监听这些变化,并触发重新渲染的操作。
  4. 在重新渲染的过程中,Vue 会生成一个新的虚拟 DOM 树,并和之前的虚拟 DOM 树进行比较,找出差异部分,即diff算法
  5. 接着,Vue 会根据差异部分进行更新操作,将更新后的结果渲染到浏览器中,即差量渲染

需要注意的是,在 Vue 中,由于采用了模板语法,所以在模板中使用的各种指令和表达式也需要被翻译成相应的虚拟 DOM 树结构。另外,Vue 中还引入了一些优化手段,例如 vnode 缓存和异步更新等,可以更加高效地进行虚拟 DOM 的更新和渲染。

追问:这个过程中会用到AST吗,具体是哪里?

在 Vue 的编译过程中,会将模板代码转换成抽象语法树(AST),然后再将 AST 转为渲染函数。所以,Vue 中的虚拟 DOM 解析过程和 AST 是密切相关的。

具体来说,在 Vue 的编译过程中,会涉及以下几个步骤:

  1. 解析:将模板代码解析成一个抽象语法树(AST),包括语法分析词法分析等。
  2. AST优化:对 AST 进行一些优化操作,例如静态节点优化、事件处理函数优化等。
  3. 生成渲染函数:将优化后的 AST 转换成渲染函数,即生成可执行的 JavaScript 代码。在这个过程中,会通过递归遍历 AST 的各个节点,并生成相应的虚拟 DOM 树节点。
  4. 缓存:为了避免每次都需要重新编译模板代码,Vue 还提供了编译结果缓存机制,即将编译结果缓存在内存中或者浏览器本地存储中,下次使用时可以直接从缓存中读取。

需要注意的是,虽然 AST 在 Vue 的编译过程中起到了很重要的作用,但是在虚拟 DOM 的解析过程中并没有直接用到 AST,而是将 AST 转换成了渲染函数,然后再通过渲染函数生成虚拟 DOM 树

追问:虚拟DOM树转真实DOM操作代码时会不会再次用到AST?

在虚拟 DOM 树转真实 DOM 的过程中,不会再次用到 AST。因为在 Vue 或 React 中,虚拟 DOM 树和真实 DOM 树之间的映射关系已经建立好了,所以只需要根据两个树之间的差异,对真实 DOM 进行必要的更新即可。

具体来说,当虚拟 DOM 树和真实 DOM 树不一致时,Vue 或 React 会进行一些算法计算,例如 diff 算法和 Reconcilation 算法等,来找出两个树之间具体的不同点。然后根据这些不同点,对真实 DOM 进行必要的添加、修改和删除等操作,以达到视图更新的目的。

需要注意的是,在生成真实 DOM 树的过程中,可能会使用一些底层的 DOM 操作函数,例如 document.createElement()element.appendChild() 等。但是这些函数并不涉及 AST 解析,而是直接对 DOM 进行操作。


+ 什么是CSRF? 如何防御?

CSRF(Cross-Site Request Forgery,跨站请求伪造)攻击是一种利用用户在其他网站已经登录的身份,来进行攻击目标网站的一种网络攻击方式。

攻击者会在第三方网站(通常是恶意网站)上设置一个钓鱼页面,在页面上植入 JavaScript 代码,向目标网站发送 HTTP 请求,该请求中包含了目标网站需要的参数,如登录凭证(Cookie 等)、用户信息等。由于用户已经在目标网站登陆过,并且浏览器可能自动保存了 Cookie 等凭证信息用于后续请求,所以目标网站无法判断该请求是否是合法用户产生的,从而执行请求,达到攻击者想要的恶意操作,例如转账、删除数据等,危害极大。

为了防御 CSRF 攻击,可以采取以下措施:

  1. 使用 Token:在网站中加入一个随机的 Token 字符串,并将其与用户的会话相关联。每次发送请求时,都需要将该 Token 值一同提交给服务端,如果 Token 值不匹配,服务端就可以拒绝该请求。
  2. 检查来源:对发送请求的来源进行检查,只接受来自信任来源的请求。可以使用 Referer 头部和 Origin 头部来判断请求的来源。
  3. 添加验证码:在关键操作前添加验证码验证,例如发邮件、保存数据等,需要先输入正确的验证码才能执行操作。
  4. 不要使用 cookie 验证:不要使用 cookie 作为验证用户身份的唯一标识,可以使用类似 JWTOAuth 等更加安全的方式进行验证。

需要注意的是,以上措施并不能完全解决 CSRF 攻击,只能提高攻击的难度和限制可受害范围。因此,对于需要保护用户隐私和安全的网站或应用程序,需要不断加强安全意识,及时更新补丁和升级设备软件等措施。

追问:举一个oauth的场景例子

一个常见的 OAuth 场景是用户使用第三方平台(例如微信)登录某个网站或应用程序。

具体流程如下:

  1. 用户打开网站或应用程序,并选择使用第三方平台登录
  2. 网站或应用程序将用户重定向到第三方平台的认证服务器,并带上一个回调地址
  3. 第三方平台认证服务器要求用户提供登录凭证(例如帐号密码、指纹识别等),验证用户身份后,颁发一个授权码给网站或应用程序。
  4. 网站或应用程序通过回调地址将授权码发送到服务端
  5. 服务端拿到授权码后向第三方平台认证服务器请求许可令牌
  6. 第三方平台认证服务器验证授权码的合法性,并向服务端颁发许可令牌
  7. 服务端使用许可令牌向第三方平台资源服务器请求用户数据,并将用户数据返回给前端网站或应用程序。
  8. 网站或应用程序收到用户数据后,进行相关业务处理,完成用户登录

此流程实现了从第三方平台获取用户数据的过程,便于网站或应用程序实现快速的用户注册和登录。同时,用户无需在每个网站或应用程序上注册新帐户,只需要使用自己已经有的第三方平台帐户即可登录。

以微信登录为例,在下图中资源服务器即微信后台,而第三方服务器即需要接入微信登录服务的“我们自己的网站后台”

image.png


+ OPTIONS请求方法及使用场景

OPTIONS 请求方法是 HTTP/1.1 协议中定义的一种预检请求,它用于获取 Web 服务器的性能指标以及判断某些请求是否可用

具体来说,OPTIONS 请求方法发出后,服务器会返回 Allow 响应头,它包含了该资源所支持的所有 HTTP 方法列表 + 允许跨域访问的前端源 + 允许的特殊请求头。这样客户端就可以在发送真正的请求前,先弄清服务器接受哪些 HTTP 请求方法,以及支持哪些请求头等。

OPTIONS 请求方法的使用场景:

  1. CORS 支持查询:当进行跨域请求时,浏览器会先发送 OPTIONS 预检请求,并根据服务器返回的 Allow 响应头来判断是否可以发送真实请求。如果服务器支持跨域访问,则会在响应头中添加 Access-Control-Allow-Origin 头部,允许来自特定域名下的跨域请求。
  2. RESTful API 可用方法列表查询:在 RESTful API 中,OPTIONS 方法通常用于获取可用的操作列表,以及验证客户端请求的操作是否在服务器端允许。例如,对于 GET /users 路径,OPTIONS 请求可能返回一个允许客户端使用的列表,如 GET、POST、PUT、DELETE 等方法,而不允许使用 PATCH 方法。
  3. 文件元数据与访问权限查询 :WebDAV 是一个基于 HTTP 扩展的文件管理和发布协议,CalDAV 则是基于 WebDAV 和 iCalendar 标准的日历共享协议。这些协议支持 OPTIONS 方法用于获取资源的元数据,并允许客户端检查服务器端对资源的操作权限。

+ HTTP 1.1和 HTTP 2.0 的区别?

HTTP 1.1 和 HTTP 2.0 是两个不同版本的 HTTP 协议,它们在性能、安全和功能方面有着很大的差异,以下是它们之间的主要区别:

  1. 性能:HTTP 2.0 使用二进制协议而不是文本协议,可以更高效地传输数据。
  2. 安全:HTTP 2.0 强制使用 TLS 加密协议传输数据,即 强制开启HTTPS,可以有效地保障数据的安全性。
  3. 功能:HTTP 2.0 支持服务器推送(Server push)功能,也就是说服务器可以在客户端没有请求的情况下主动推送资源给客户端。
  4. 报头压缩:HTTP 2.0 引入了 HPACK 算法实现报头压缩,有效地减少了请求和响应报头的大小。
  5. 多路复用:HTTP 2.0 的一大特点是采用了多路复用,允许客户端和服务器同时通过同一个 TCP 连接传输多个请求和响应数据流,从而避免了 HTTP/1.1 的队头阻塞问题,提高传输效率。

综上所述,HTTP 2.0 相较于 HTTP 1.1 在性能、安全和功能方面均有明显的提升。但是,需要注意的是,由于一些其它原因,如浏览器兼容性等,部分网站仍在使用 HTTP 1.1 协议。

多路复用原理图

*下图为连接中往返数据帧的实际状态(往来杂乱无章)

由于每个数据帧都带有流ID+帧序号,逻辑上实现了上图连接虚拟为流的状态 image.png

追问:那末(缅怀一下从语文课本消失20年的鲁迅先生), HTTP 2.0 和 HTTP 3.0 又有何区别呢?

HTTP/3 是一个全新的协议,它基于 底层采用UDP的QUIC协议,而非 TCP。因此,与 HTTP/2 相比,HTTP/3 的区别主要表现在以下几个方面:

  1. UDP传输:HTTP/3 使用 底层采用UDP的QUIC 作为传输协议,而 HTTP/2 则基于 TCP。QUIC 把消息分割成较小的数据包,可以避免 TCP 队头阻塞的问题,同时也更适合运行在高丢包、高延迟的网络环境中
  2. 加密升级:像 HTTP/2 一样,HTTP/3 也是强制加密的协议,需要通过 TLS 进行加密。但是与 HTTP/2 不同的是,HTTP/3 不再依赖 TCP 套接字来完成数据的传输加密,而是使用 QUIC 内置的安全套接字(SSL/TLS 1.3) 来进行数据传输的加密,这使得 HTTP/3 可以更好地保护隐私和安全。
  3. 多路复用:与 HTTP/2 类似,HTTP/3 仍然支持多路复用,但由于采用了 QUIC 协议,因此多路复用实现方式有所不同。
  4. 0轮握手:HTTP/3 相对于 HTTP/2 在连接建立时有更低的延迟,这主要得益于 QUIC 支持 “0-RTT”连接,即使用“0轮”往返时间来建立连接。

综上所述,HTTP/3 相比 HTTP/2 在性能和安全方面有着更加优秀的表现。因此,未来随着网络技术的发展,HTTP/3 有可能会逐渐取代 HTTP/2 成为最主流的 Web 传输协议。


+ 说说你对git rebase 和 git merge的理解,区别是什么?

Git rebase 和 Git merge 是 Git 中两种常用的代码版本合并方式。它们的作用都是将不同分支上的修改合并到一起,但实现的方式有一定差异。

Git merge 的作用是将两个分支上的修改合并到一个新的分支上。在执行 merge 操作时,Git 会先找到两个分支的共同祖先,然后将两个分支各自的修改合并到新的分支上,并生成一个新的提交记录,该记录包含了两个原始分支的代码。

相比之下,Git rebase 的作用是将当前分支的修改“衍合”(rebase)到目标分支上。这意味着,Git 会将当前分支的修改挪到目标分支的最新提交之后,这样就可以看到一个更加线性的提交历史。在执行 rebase 操作时,Git 会找到当前分支和目标分支的共同祖先,然后将当前分支的修改逐个应用到目标分支上,并生成新的提交记录。

Git rebase 和 Git merge 的区别在于合并后生成的提交历史不同。Git merge 生成的是一个新的合并提交,包含了两个原始分支的修改;而 Git rebase 则是在目标分支的基础上应用当前分支的修改,并在目标分支上生成新的提交记录。因此,在一些情况下,使用 Git rebase 可以使代码提交历史更加简洁、清晰

换底为目标分支.jpg↓↓ image.png

注意:在rebase的时候,是可以有选择地跳过任意节点的!↑↑

追问:那么到底rebase还是merge呢?
  • 如果你希望分支的提交历史尽可能详实,建议使用 rebase
  • 如果你讨厌频繁处理冲突,觉得总分支的历史记录大条一点也蛮好,那么merge无疑更简单粗暴一些!
  • 人少或者一个人玩时rebase详实一点挺好,越多人越应该merge为主,化零为整以简化管理!
  • merge失败破坏当前分支,rebase失败破坏目标分支,无论单位要求你merge还是rebase,破坏哪个分支都不合适,提前备可能被破坏的分支——小心驶得万年船!

+ 什么时候需要自定义Webpack loader或plugin?

当我们在项目中遇到以下情况时,就需要考虑自定义 Webpack Loader 或 Plugin:

  1. 需要处理非 JavaScript 类型的文件

Webpack 默认只能处理 JavaScript 的模块,如果需要处理非 JavaScript 的文件类型,例如 CSS、LESS、SASS、Markdown 等,就需要使用对应的 Loader 将这些文件转换成可以被 Webpack 识别的模块。

  1. 需要自定义业务逻辑

有时在项目开发中,我们可能需要对某些文件进行自定义的处理逻辑,例如使用自己的模板解析引擎来处理 HTML 文件或者为图片文件添加水印等等。

  1. 需要在构建过程中动态生成代码

使用 Loader 可以在构建过程中动态生成代码。例如,在开发环境中,可以使用 style-loader 将 CSS 样式直接注入到 HTML 中,而不是生成单独的 CSS 文件。在生产环境中,也可以借助 Loader 将小图片转化成 base64 编码并直接嵌入到 CSS 文件中,从而减少 HTTP 请求次数和页面加载时间。

  1. 需要扩展 Webpack 的功能

Plugin 可以扩展 Webpack 的功能,例如自动生成 HTML 页面、压缩 JavaScript 代码、提取 CSS 到单独的文件等等。如果我们的需求没有被 Webpack 官方提供的插件满足,那么就需要自定义 Plugin 来满足我们的需求。

  1. 需要自定义输出文件名

在构建项目时,我们可能需要自定义输出文件的名称或路径,例如为文件添加 hash 值、生成多个入口文件等。这时可以通过插件来实现。

总之,当我们遇到一些特殊的需求,并且 Webpack 默认的功能无法满足时,就需要考虑自定义 Loader 或 Plugin 来达成目标。

追问:请写一个自定义loader的例子

好的,这里给出一个简单的自定义 Loader 示例来解释如何实现。

假设我们有一个名为 example.txt 的文本文件,它的内容是一组数字,每行一个。我们希望在读取该文件时,将每行数字加上 1 并返回一个数组。

首先,我们需要创建一个 JavaScript 文件作为我们的 Loader 入口文件。在这个文件中,我们会导出一个函数,这个函数会接收一个字符串类型的参数,即被处理的文件内容,然后对其进行转换并返回结果。

// loader.js

module.exports = function(source) {
  const lines = source.split('\n').filter(Boolean); // 获取每行数字
  const numbers = lines.map(Number); // 转换成数字
  const result = numbers.map(n => n + 1); // 对每个数字加 1
  return `module.exports = ${JSON.stringify(result)}`; // 导出结果
};

接着,在 webpack 配置文件中,我们需要配置使用这个 Loader 来处理 .txt 文件类型:

// webpack.config.js

module.exports = {
  // ...other options
  module: {
    rules: [
      {
        test: /.txt$/,
        use: [
          {
            loader: path.resolve(__dirname, 'loader.js'),
          },
        ],
      },
    ],
  },
};

最后,我们就可以在代码中通过 require() 导入 .txt 文件并获取经过 Loader 处理后的结果了:

const numbers = require('./example.txt');
console.log(numbers); // [2, 3, 4, 5, 6, 7]

这就是一个简单的自定义 Loader 示例,它可以帮助你了解如何编写和使用 Loader。实际的 Loader 可能需要更加复杂的逻辑,但总的来说,它们都遵循着类似的结构和规范。

追问:请写一个自定义plugin的例子

好的,这里给出一个简单的自定义 Plugin 示例来解释如何实现。

假设我们有一个需要在每个构建后向控制台输出一条消息的需求。我们可以通过自定义一个 Plugin 来实现这个功能。

首先,在项目中创建一个 JavaScript 文件,作为我们的自定义 Plugin 入口。在这个文件中,我们需要导出一个带有 apply 方法的对象:

// plugin.js

class ConsoleLogOnBuildWebpackPlugin {

  apply(compiler) {
    compiler.hooks.done.tap(
        'ConsoleLogOnBuildWebpackPlugin', 
        stats => {
          console.log('************Build done!************');
        }
     );
  }
  
}

module.exports = ConsoleLogOnBuildWebpackPlugin;

接着,在 webpack 配置文件中,我们需要将这个自定义 Plugin 实例化并添加到插件列表中:

// webpack.config.js

const ConsoleLogOnBuildWebpackPlugin = require('./plugin');

module.exports = {
  // ...other options
  plugins: [
    new ConsoleLogOnBuildWebpackPlugin(),
  ],
};

最后,运行 webpack 命令进行构建即可在控制台看到输出的信息:

************Build done!************

这就是一个简单的自定义 Plugin 示例,它可以帮助你了解如何编写和使用 Plugin。实际的 Plugin 可能需要更加复杂的逻辑,但总的来说,它们都遵循着类似的结构和规范。

再介绍一个我开发过的Webpack4 插件(实测WP5也能使用!):SizePlugin。该插件可以在 Webpack 构建完成后,输出每个打包文件的大小和 gzip 压缩后的大小,以帮助我们更好地了解项目的体积情况,从而进行优化。

以下是 SizePlugin 的代码实现:

const zlib = require('zlib');

class SizePlugin {
  apply(compiler) {
    compiler.hooks.done.tap('SizePlugin', stats => {
      const assets = stats.toJson().assets;
      console.log('File\t\tSize\t\tGzip Size');
      console.log('----------------------------------');
      assets.forEach(asset => {
        const filename = asset.name.padEnd(16);
        const size = formatBytes(asset.size).padEnd(10);
        const gzipped = formatBytes(zlib.gzipSync(asset.source()).length);
        console.log(`${filename}${size}${gzipped}`);
      });
      console.log('----------------------------------');
    });
  }
}

function formatBytes(bytes, decimals = 2) {
  if (bytes === 0) return '0 Bytes';
  const k = 1024;
  const dm = decimals < 0 ? 0 : decimals;
  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}

使用该插件也非常简单,只需要在 webpack.config.js 文件中配置即可:

const SizePlugin = require('./path/to/SizePlugin');

module.exports = {
  // ...
  plugins: [
    new SizePlugin()
  ]
};

当 Webpack 构建完成后执行 build 命令时,会输出类似下面的信息:

File            Size            Gzip Size
----------------------------------
main.js         169.56 KB       28.48 KB  
vendor.js       1.19 MB         398.19 KB 
----------------------------------

image.png

从中,我们可以看到打包文件的大小和 gzip 压缩后的大小,从而对文件大小进行优化。

追问:webpack4自定义plugin常用生命周期

在 Webpack 4 中,自定义插件可以通过监听和绑定到不同的生命周期钩子(lifecycle hooks)来执行特定的功能。以下是一些常用的生命周期钩子:

  1. beforeRun:在 Webpack Compiler 开始读取记录之前触发。通常在该钩子中执行一些预处理的任务。
  2. emit:在生成资源到 output 目录之前触发。可以在该钩子中对生成的资源进行最后的修改或优化。
  3. afterEmit:在生成资源到 output 目录后触发。可以在该钩子中执行与生成的资源相关的其他任务,如上传到服务器或通知等操作。
  4. done:当编译完成并且输出了静态资源时触发。通常在该钩子中执行一些与编译结果相关的任务,如输出构建统计信息或显示成功信息。
  5. compilation:在每次创建新的 compilation 对象时触发。可以在该钩子中对特定的 compilation 对象进行修改,如添加额外的资源、变更编译选项等。
  6. optimize:在模块优化过程开始之前触发。可以在该钩子中对模块进行修改或添加额外的优化处理。
  7. afterPlugins:在所有插件的 apply 方法被调用完毕之后触发。通常在该钩子中执行与插件之间的协调和整合操作。
  8. environment:在设置好 compilation 的环境后触发。可以在该钩子中进行与环境相关的配置或修改。
  9. beforeCompile:在开始编译之前触发。可以在该钩子中执行一些预备编译的任务。
  10. afterCompile:在编译完成之后触发。可以在该钩子中获取编译结果,并进行处理或分析。

这些是 Webpack 4 中常用的一些生命周期钩子,你可以根据自己的需求选择合适的钩子来自定义插件功能。每个钩子都有不同的特点和用途,可以根据具体情况决定使用哪些钩子。

追问:能不能再在其它生命周期里来一个Plugin呢?

我们可以创建一个自定义插件来动态生成一个名为 version.js 的文件,文件中包含构建的版本号信息。这对于在前端应用中展示版本号非常有用。

const fs = require('fs');

class VersionPlugin {
  constructor(options) {
    this.options = options;
  }

  apply(compiler) {
    compiler.hooks.emit.tapAsync('VersionPlugin', (compilation, callback) => {
      const { version } = this.options;

      const content = `export const appVersion = '${version}';`;

      fs.writeFileSync('./dist/version.js', content);

      callback();
    });
  }
}

module.exports = VersionPlugin;

在这个示例中,我们创建了一个名为 VersionPlugin 的自定义插件,并接收一个 version 参数作为构建的版本号。在 emit 生命周期钩子中,我们通过 fs.writeFileSync 方法,将生成的版本信息写入到 version.js 文件中。

要使用这个自定义插件,需要将其配置到 Webpack 的插件列表中,并传递版本号作为参数。假设你已经有一个 Webpack 配置文件(例如 webpack.config.js),可以像下面这样使用这个插件:

const VersionPlugin = require('./VersionPlugin');

module.exports = {
  // ...其他配置选项...
  plugins: [
    new VersionPlugin({ version: '1.0.0' })
  ]
};

现在,当你运行 Webpack 命令进行构建时,会在 dist 目录下生成一个名为 version.js 的文件,其中包含构建的版本号信息。

image.png

image.png


+ HTTP响应报文的是什么样的?

HTTP 响应报文是由客户端接收的服务器端返回的数据,其通常包含以下三部分:

  1. 状态行:

    状态行包含了 HTTP 协议版本号、状态码和状态码的原因短语。

    示例:

    HTTP/1.1 200 OK
    
  2. 响应头:

    响应头用来描述服务器返回的资源的一些基本信息,例如内容类型、内容长度、缓存策略等。

    示例:

    Content-Type: text/html; charset=UTF-8
    Content-Length: 1024
    Cache-Control: no-cache
    
  3. 响应正文:

    响应正文是服务器返回的实际数据,可以是任意类型的数据(文本、二进制等)。例如:

    <!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="UTF-8">
            <title>Example Page</title>
        </head>
        <body>
            <p>Hello, World!</p>
        </body>
    </html>
    

以上是 HTTP 响应报文的基本组成成分,需要注意的是,每个部分之间必须使用 CRLF(回车换行)符号进行隔开。HTTP 响应报文的格式是固定的,浏览器和 Web 服务器都是根据这种格式进行解析和处理的。


+ 回流与重绘的概念及触发条件?

回流(reflow)和重绘(repaint)是 Web 页面性能优化中常提到的概念。

回流指的是当 DOM 的尺寸、结构或某些属性发生变化时,浏览器需要重新计算元素的几何属性并重新布局(layout),从而称为回流。这个过程往往比较耗费性能,因为每次回流都会导致整个页面的重新渲染

重绘则是在元素的某些样式属性发生变化时(如背景色、文本颜色等),浏览器只需要将更改的部分重新绘制即可,不需要重新计算元素的几何属性,因此性能开销比回流要小

一般来说,回流的触发条件包括:

  • 添加、删除、修改 DOM 节点尺寸
  • 显示与隐藏元素(display:none)
  • 盒子模型相关属性的改变(width、height、padding、margin、border 等)
  • 页面第一次加载或者重新调整窗口大小

而重绘的触发条件主要是:

  • 颜色变化,包括文本颜色,背景色等
  • 不影响元素布局的属性变化,例如边框样式、边框颜色、outline 等

在实际开发中,我们应该避免频繁触发回流和重绘,以提高页面性能。可以使用以下方式来优化:

  • 尽量避免频繁添加、删除、修改 DOM 节点
  • 避免过多使用 display:none,可以使用 visibility:hidden 来代替
  • 使用 translate、opacity 等属性代替 top、left 等属性
  • 避免在 JavaScript 中频繁读取样式值,而应该将其缓存下来
  • 避免设置多层内嵌标签,尽量减少 DOM 层级,优化布局
追问:visibility:hidden会导致相邻元素的位置改变吗?

不会。这是因为 visibility: hidden 会将元素隐藏起来,但是不会改变元素的布局或占用空间,也就是说,元素仍然会占据原来的位置。相邻的元素在计算位置时会将该元素视为存在。


+ Cookie有哪些字段,作用分别是什么?

以下是一个属性全面的 Cookie 例子:

Set-Cookie: name=value; domain=example.com; path=/; expires=Tue, 22 Jun 2023 04:22:12 GMT; secure; HttpOnly

该 Cookie 的属性和作用如下:

  • 名称和值

名称为 “name”,值为 “value”。这是 Cookie 的基本属性,用于保存具体信息。

  • 域名

域名为 “example.com”。表示该 Cookie 可以在 example.com 及其子域名下使用。

  • 路径

路径为 “/”。表示该 Cookie 可以在整个网站的根目录下使用。

  • 过期时间

过期时间为 “Tue, 22 Jun 2023 04:22:12 GMT”。表示该 Cookie 将在该日期之后失效。

  • 安全标志

安全标志为 “secure”。表示该 Cookie 只能通过 HTTPS 发送。这可以提高 Cookie 的安全性。

  • HttpOnly 标志

HttpOnly 标志为 “HttpOnly”。表示该 Cookie 无法通过 JavaScript 访问,这可以防止跨站点脚本攻击(XSS)。

这是一个非常完整的 Cookie 示例,几乎涵盖了所有可能用到的属性。需要注意的是,不是所有网站都需要使用所有属性,具体使用哪些属性需要根据实际情况来确定。通常情况下,只需要设置基本的名称、值和过期时间即可。

追问:如何设置一个可被跨域访问的cookie

为了设置可被跨域访问的 Cookie,需要在设置 Cookie 时指定 SameSite 属性为 None,并且同时设置 Secure 属性为 true,表示仅通过 HTTPS 协议发送 Cookie。同时,需要将 Domain 属性设置为允许访问该 Cookie 的跨域域名。

下面是一个设置可被跨域访问的 Cookie 的示例:

Set-Cookie: name=value; domain=.example.com; path=/; expires=Tue, 22 Jun 2023 04:22:12 GMT; secure; SameSite=None

其中,Domain 属性以点号开头,表示该 Cookie 可以被所有以 example.com 结尾的子域名访问。例如,在 www.example.comtest.example.comblog.example.com 等网站中都可以访问该 Cookie。

需要注意的是,为了保护用户隐私和安全,浏览器限制了跨域 Cookie 的使用。默认情况下,同源策略会阻止跨域访问其他网站的 Cookie。同时,Chrome 浏览器等一些主流浏览器也针对 Cookie 进行了更加严格的限制,会对未设置 SameSite 属性的 Cookie 进行拦截,以防止 CSRF 攻击等安全问题。

为了使跨域 Cookie 生效,需要同时满足以下条件:

  1. 必须在跨域源服务器返回 Access-Control-Allow-Credentials: true 头部,表示允许跨域请求携带 Cookie。
  2. 必须在设置 Cookie 时,将 SameSite 属性设置为 None,并将 Secure 属性设置为 true,表示仅通过 HTTPS 协议发送 Cookie。
  3. 必须将 Domain 属性设置为允许访问该 Cookie 的跨域域名,而且只能设置为当前域名的父域名或者其子域名。

需要注意的是,不是所有的浏览器都支持跨域 Cookie,例如,IE6、IE7 和 Safari 等浏览器不支持跨域 Cookie。因此,在实际应用中,需要根据实际情况选择合适的技术方案,以兼容不同的浏览器和平台。同时,需要注意设置 Cookie 时的安全性和隐私保护,以确保用户的安全和隐私不受侵犯。

追问:跨域cookie的使用场景

跨域 Cookie 的使用场景比较广泛,例如:

  1. 单点登录系统的用户信息共享:在多个子域名下使用相同的账号和密码进行登录,通过 Cookie 在多个子域名下实现用户状态的共享
  2. 跨域请求时的用户信息共享:在跨域的情况下,使用 CORS 或 JSONP 等技术进行数据交换时,可以使用 Cookie 传递用户身份信息和权限信息,并保持用户的登录状态。
追问:那么cookie的domain可以为多个吗?

不能,需要针对每个域名分别设置 Cookie!

或者当Domain属性以点号(".")开头,表示允许所有以该域名结尾的子域名访问该 Cookie。例如,如果将 Domain 属性设置为 ".example.com",则该 Cookie 可以被 "www.example.com"、"test.example.com"、"abc.example.com" 等任何以 "example.com" 结尾的子域名访问,但无法被其他域名访问。

追问:两个父级域名不同的网站能共享cookie吗?

不能!


+ for...in和for...of的区别?

for...in 和 for...of 都是 JavaScript 中的循环语句,但它们用法和作用略有不同:

  1. for...in 循环遍历对象的可枚举属性包括对象本身和继承自原型链的属性。例如:

    const obj = { a: 1, b: 2 };
    for (let prop in obj) {
      console.log(prop); // 输出 "a" 和 "b"
    }
    

    注意,for...in 循环遍历顺序不一定是按照属性定义的顺序进行的,且可能会出现意料之外的情况。

  2. for...of 循环遍历可迭代对象(Iterable),包括数组、字符串、Set、Map 等数据结构中的元素。例如:

    const arr = [1, 2, 3];
    for (let value of arr) {
      console.log(value); // 输出 1、2 和 3
    }
    

    注意,for...of 循环只能遍历值,而不能遍历对象的键名或键值,因为它只能访问对象的迭代器方法。

因此,for...in 循环和 for...of 循环适用于不同的场景。如果要遍历对象中的属性,用 for...in 循环;如果要遍历数组等可迭代对象中的元素,用 for...of 循环。如果不确定要循环的对象是何种类型,可以使用 typeof 运算符或者 Array.isArray() 方法来进行判断。

追问:何以知道某个属性(例如toString)是否可枚举呢?

可以通过以下代码来证明 toString 方法是不可枚举属性:

console.log(Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable);
// 输出 false

在这个示例中,我们使用 Object.getOwnPropertyDescriptor() 方法获取 Object.prototype 对象的 toString 属性描述符,并输出其 enumerable 属性的值。由于 toString 方法默认不可枚举,因此输出结果为 false。

追问:我想把某个属性设置为不可枚举,怎么设?

可以使用 Object.defineProperty() 或者 Object.defineProperties() 方法来将一个属性设置为不可枚举。

Object.defineProperty() 方法用于在一个对象上定义一个新属性或修改现有属性,并返回该对象。它接受三个参数:要添加或修改属性的对象,要添加或修改的属性名,以及一个包含属性描述符的对象。其中,属性描述符对象中有 enumerable 属性,它决定了属性是否可枚举。如果值为 false,则该属性是不可枚举的。示例如下:

const obj = {};
Object.defineProperty(obj, 'id', {
    value: 123,
    enumerable: false
});

for(let prop in obj) {
    console.log(prop); //不会输出任何内容
}

console.log(Object.keys(obj)); //输出 []

在这个示例中,我们首先定义了一个空对象 obj,然后使用 Object.defineProperty() 方法为其添加一个名为 id 的属性。我们将 enumerable 属性设置为 false,使得该属性变成了不可枚举属性。最后使用 for...in 循环和 Object.keys() 方法分别检查对象的可枚举属性和属性名,可以看到不会输出我们所定义的 id 属性。

除了 Object.defineProperty() 方法,您还可以使用 Object.defineProperties() 方法来批量操作对象的属性,并将它们设置为不可枚举。示例如下:

const obj = {
  foo: 'bar',
  baz: 'qux'
};

Object.defineProperties(obj, {
  'foo': {
    enumerable: false
  },
  'baz': {
    enumerable: false
  }
});

for(let prop in obj) {
    console.log(prop); //不会输出任何内容
}

console.log(Object.keys(obj)); //输出 []

这个示例中,我们首先定义了一个对象 obj,然后使用 Object.defineProperties() 方法为其批量操作属性,并将其设置为不可枚举属性。最后使用 for...in 循环和 Object.keys() 方法分别检查对象的可枚举属性和属性名,可以看到不会输出我们所定义的 foo 和 baz 属性。

追问:那么,写一个可迭代类来看看!

实现一个可迭代的类时,需要提供一个方法作为该类的 @@iterator 属性,这个方法返回一个对象,它包含一个 next 方法。每次调用 next() 方法都会返回一个包含 value 和 done 两个属性的对象。其中,value 表示每次迭代返回的值,done 表示是否已经迭代完毕。

下面是一个简单的可迭代类的示例:

class MyIterable {
  constructor(values) {
    this.values = values;
  }

  *[Symbol.iterator]() {
    for (let i = 0; i < this.values.length; i++) {
      yield this.values[i];
    }
  }
}

const myIterable = new MyIterable([1, 2, 3, 4, 5]);

for (let value of myIterable) {
  console.log(value);
}

// 输出:
// 1
// 2
// 3
// 4
// 5

这里我们定义了一个名为 MyIterable 的类,它包含一个构造函数和一个 @@iterator 方法。在构造函数中,我们将传入的参数保存到自己的 values 属性中。而在 @@iterator 方法中,我们使用了 ES6 中的 Generator 函数来生成一个可迭代的序列,然后通过 yield 语句在每次迭代中返回一个值

最后,我们创建了一个 MyIterable 类的实例 myIterable,并通过 for...of 循环遍历了这个实例。在循环中,我们可以依次访问 MyIterable 类中所有的值。

追问:如果我想动态地定义next返回什么呢!

如果要动态地定义 next() 方法的返回值,可以在迭代器对象中定义一个变量来保存下一个返回值,然后在每次调用 next() 方法时返回这个变量,并在需要的时候更新该变量的值。下面是一个示例:

class MyIterable {
  constructor(values) {
    this.values = values;
    this.nextValue = 0;
  }

  [Symbol.iterator]() {
    let iterator = {
      next: () => {
        if (this.nextValue >= this.values.length) {
          return {done: true};
        } else {
          let value = this.values[this.nextValue++];
          if (typeof value === 'function') {
              value = value();
          }
          return {value, done: false};
        }
      }
    };
    
    return iterator;
  }
}

const myIterable = new MyIterable([1, 2, () => 'hello', 4, 5]);

for (let value of myIterable) {
  console.log(value);
}

// 输出:
// 1
// 2
// hello
// 4
// 5

这里我们在定义 MyIterable 类的时候,除了保存了用户传入的值(values),还定义了一个 nextValue 变量,用于保存下一个要返回的值。在 @@iterator 方法中,我们创建了一个包含 next() 方法的对象 iterator,并在这个方法中根据 nextValue 和 values 数组的长度来决定是返回下一个值还是结束迭代。

在返回下一个值时,我们检查它是否为函数,如果是,则调用它获取实际的返回值。这样就可以动态地定义 next() 方法的返回值了。

最后,我们创建了一个 MyIterable 类的实例 myIterable,并通过 for...of 循环遍历了这个实例。在循环中,我们可以依次访问 MyIterable 类中所有的值。

追问:@@iterator 属性? 名字这么奇怪吗?

是的,@@iterator 属性名字看起来比较奇怪,这是因为它是 ES6 中引入的一个特殊属性,其命名方式是为了避免与用户定义的属性重名。

追问:可以在迭代器中玩异步吗?

必须可以!ES6 中支持在迭代器中使用异步操作。可以通过 Generator 函数的特性实现这一点。

下面是一个示例,介绍如何在迭代器中使用异步操作:

function* asyncGenerator() {
  yield new Promise(resolve => setTimeout(() => resolve('Hello'), 1000));
  yield 'World';
}

async function printValues() {
  const generator = asyncGenerator();
  for await (let value of generator) {
    console.log(value);
  }
}

printValues(); // 输出:Hello, World

这里我们定义了一个名为 asyncGenerator 的 Generator 函数,其中包含两个 yield 表达式。第一个 yield 表达式返回一个 Promise 对象,在 1 秒后会被 resolve,第二个 yield 表达式直接返回字符串 'World'。

接着我们定义了一个名为 printValues 的异步函数,它创建了一个迭代器并使用 for...of 循环遍历迭代器中的值。注意这里使用了 for-await-of 循环,而不是普通的 for...of 循环,原因是 asyncGenerator 中包含了异步操作。

在循环中,我们使用了 await 关键字来等待异步操作完成,然后输出相应的值。由于第一个值是在 1 秒后才返回的,因此在执行 printValues() 函数时,控制台并没有立即输出 'Hello',而是先输出了 'World'。

总结来说,Generator 函数的特性使得在迭代器中使用异步操作变得容易,您可以在 yield 表达式中返回 Promise,然后在 for...of 循环中使用 await 关键字来等待异步操作完成。

image.png


+ 对CSSprites 的理解?

CSS Sprites 中文又名雪碧图技术。这种前端性能优化技术可以将多张小图片合成一张大图,通过 CSS 切割和定位技术,在网页中展示需要的小图片。这样做的好处在于可减少 HTTP 请求次数,从而提高页面加载速度和性能。


+ 对媒体查询的理解?

媒体查询是一种 CSS 技术,它可以根据不同的屏幕尺寸、设备类型以及显示方向等条件来为网页应用提供不同的样式。

在实际的开发中,我们经常遇到需要针对不同设备或显示情况进行适配的情况。例如,我们可能需要调整字体大小、布局方式、图片尺寸等元素,以便在不同的手机、平板电脑和桌面电脑等设备上获得更好的用户体验。这时候,媒体查询就可以发挥重要作用。

使用媒体查询时,我们可以在样式表中使用 @media 规则,在其中指定一组 CSS 属性和值,这些属性和值将只会在满足特定媒体查询条件时才生效。例如,以下代码段表示在屏幕宽度小于等于 480px 时,修改 HTML 元素的字体大小为 12px:

@media screen and (max-width: 480px) {
  html {
    font-size: 12px;
  }
}

在上述代码中,@media 规则指定了特定的媒体查询条件:screen 表示是屏幕 media 类型,max-width: 480px 表示屏幕宽度小于等于 480px。当页面被渲染在满足此条件的设备上时,其中的样式规则才会生效。

总的来说,媒体查询是一种重要的 CSS 技术,可以帮助我们实现对不同设备和显示情况的适配,从而提高网页应用的用户体验和可用性。

追问:你平时怎么做大屏适配呢?

使用 CSS3 的媒体查询功能。

通过在 CSS 文件中使用媒体查询,我们可以根据屏幕大小应用不同的样式规则。譬如(再次怀念鲁迅先生):

/* 共用样式规则 */
.container {
  margin: 20px auto;
}
.box {
  width: 100%;
  height: 200px;
  background-color: #ccc;
  margin-bottom: 20px;
}

/* 小屏幕样式规则 */
@media (max-width: 767px) {
  .container {
    padding: 10px;
  }
  .box {
    height: 150px;
    margin-bottom: 10px;
  }
}

/* 大屏幕样式规则 */
@media (min-width: 768px) {
  .container {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
  }
  .box {
    width: calc(50% - 10px);
    margin-right: 20px;
  }
}

在上面的代码中,我们首先编写了共用的样式规则,然后通过 @media 媒体查询指令对不同屏幕大小应用不同的样式规则。

在小屏幕下,我们应用 .container.box 的另一组规则,这些规则会覆盖共用规则。在大屏幕下,我们应用了另一组规则,同样会覆盖共用规则。

最后,只需要在 HTML 页面中引入一个 CSS 文件即可。例如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>My Page</title>
    <link rel="stylesheet" href="styles.css">
  </head>
  <body>
    <!-- 页面内容 -->
  </body>
</html>

在上面的代码中,我们在 head 标签中引入了一个名为 styles.css 的 CSS 文件。这个文件将包含所有的样式规则,而不会影响页面的性能或加载时间。


+ 对BFC的理解,如何创建BFC?

BFC(Block Formatting Context)即块级格式化上下文。简单来说,BFC 为一个独立的区域,具有一定的规则,内部元素的布局不受外部元素的影响,同时内部元素也不能影响外部元素的布局。这种隔离机制可以帮助避免各种异常情况,提高页面的可靠性和可维护性。

BFC 的创建有以下几种方式:

  1. float 属性不为 none
  2. position 属性值为 fixed 或 absolute
  3. display 属性值为 inline-block、table-cell、table-caption、flex、inline-flex
  4. overflow 属性值不为 visible

BFC 可以帮助我们避免一些布局问题,例如浮动元素重叠、父元素高度塌陷等,它也可以帮助我们实现自适应布局,提升页面质量和交互效果。

追问:如何消除浮动?

浮动元素会脱离文档流,有时会对周围的元素造成影响,例如高度塌陷或者内容被覆盖等问题。为了解决这些问题,可以通过清除浮动来消除其影响。以下是一些消除浮动的方法:

  1. 使用空标签清除浮动

在浮动元素的末尾添加一个空的 <div style="clear:both;"></div> 标签,其 clear 属性设置为 both,即可清除浮动。

  1. 使用伪元素清除浮动

可以使用 CSS 伪元素 ::after 来在浮动元素后面插入一个空的块级元素,并将其 clear 属性设置为 both。例如:

.clearfix::after {
  content: "";
  display: block;
  clear: both;
}

在使用这个类的元素上添加 clearfix 类即可清除浮动。

  1. 父元素触发 BFC

将父元素设置为触发 BFC(块级格式化上下文),可以清除浮动。可以通过以下方式触发 BFC:

  • 设置 overflow 属性为 hidden 或 auto
  • 设置 display 属性为 table-celltable-captioninline-blockflex 或 inline-flex
  • 设置 position 属性为 absolute 或 fixed
  • 使用 float: left/right 属性

以上几种方法都可以用来清除浮动,选择哪一种方法要根据具体情况而定。在实际应用中,可以根据需求和习惯来选择最佳方法。


+ Vue如何保存页面的当前的状态?

在 Vue 中,要保存页面的状态,可以采用以下两种方法:

  1. 使用 Vuex 管理状态

Vuex 是 Vue 的官方状态管理库,它提供了一种集中式存储管理应用的所有组件的状态的方式。如果将页面状态保存到 Vuex 中,那么可以在不同的组件之间共享状态,以及在需要时方便地修改状态。

例如,可以定义一个状态模块来保存页面状态:

Copy Code
const state = {
  count: 0,
  message: ''
}

const mutations = {
  increment(state) {
    state.count++
  },
  setMessage(state, message) {
    state.message = message
  }
}

export default {
  namespaced: true,
  state,
  mutations
}

然后在需要使用此状态的组件中,可以通过 mapStatemapMutations 等辅助函数来访问和修改状态,如下所示:

import { mapState, mapMutations } from 'vuex'

export default {
  computed: {
    ...mapState('page', {
      count: state => state.count,
      message: state => state.message
    })
  },
  methods: {
    ...mapMutations('page', ['increment', 'setMessage'])
  }
}
  1. 使用浏览器本地存储

除了使用 Vuex 管理状态之外,还可以将页面状态保存到浏览器本地存储中,比如使用 localStorage 或 sessionStorage。这样可以让用户在关闭页面或刷新页面后,仍然可以保留之前的状态。

例如,可以在页面加载时将状态从本地存储中读取出来,然后在页面关闭或刷新之前将状态保存回本地存储中,如下所示:

export default {
  data() {
    return {
      count: parseInt(localStorage.getItem('count')) || 0,
      message: localStorage.getItem('message') || ''
    }
  },
  methods: {
    saveState() {
      localStorage.setItem('count', this.count)
      localStorage.setItem('message', this.message)
    }
  },
  beforeDestroy() {
    this.saveState()
  }
}

以上两种方法都可以用来保存页面的状态,选择哪一种方法要根据具体的需求和场景而定。通常来说,如果需要多个组件之间共享状态,或者需要方便地管理状态,那么使用 Vuex 可能更为简便和适合;如果只需要保存一些简单的状态,或者状态不需要同时被多个组件访问和修改,那么使用本地存储可能更为实用。

3.使用keep-alive

使用 keep-alive 组件保存页面状态时,只需要将需要缓存的组件用 keep-alive 包裹起来即可,该组件的状态就会被自动缓存下来。例如:

<keep-alive>
  <!--底层原理为切换动态组件的is属性-->
  <router-view></router-view>
</keep-alive>

在这个例子中,<router-view> 是通过 Vue Router 动态加载的路由组件,它会根据当前路由匹配到相应的组件并渲染出来。而当用户离开该组件所在的路由时,该组件不会被销毁,而是被缓存下来,以便在用户返回时可以直接从缓存中读取该组件的状态。

需要注意的是,如果要将组件状态保存到 keep-alive 组件中,那么组件的生命周期钩子函数会被触发多次(包括 createdmountedactivated 等),因为组件在首次渲染和从缓存中激活时都会触发这些钩子函数。这就要求开发者在编写组件时,要考虑这些钩子函数被多次调用的情况,确保组件状态的正确性。

总之,keep-alive 组件是一种非常方便的保存页面状态的方式,可以避免对状态进行手动保存和恢复。如果需要缓存的是整个路由页面,那么将该页面包裹在 keep-alive 中即可;如果需要缓存的是某个组件,那么将该组件用 keep-alive 包裹起来即可。

追问:React怎么实现一个keep-alive效果呢?

在 React 中,可以使用自定义组件和状态管理来实现类似 keep-alive 的效果。下面是一个示例的实现方式:

import React, { useState, useEffect } from 'react';

const KeepAlive = ({ children }) => {
  const [isAlive, setIsAlive] = useState(true);

  useEffect(() => {
    return () => {
      setIsAlive(false);
    };
  }, []);

  return isAlive ? <div>{children}</div> : null;
};

export default KeepAlive;

在上述代码中,我们定义了一个名为 KeepAlive 的自定义组件。它通过 useState 来管理一个名为 isAlive 的状态,默认为 true

useEffect 钩子中,我们添加了一个清理函数,该函数在组件卸载时会被调用,将 isAlive 状态设置为 false,表示该组件不再活跃。

然后,在返回结果中,我们根据 isAlive 的值来决定是否渲染子组件。如果 isAlivetrue,则渲染子组件;否则,返回 null

通过使用 KeepAlive 组件包裹需要缓存的组件,就可以实现类似 keep-alive 的效果了。例如:

import React from 'react';
import KeepAlive from './KeepAlive';

const Home = () => {
  // Home 组件的内容...
};

const App = () => {
  return (
    <div>
      <h1>My App</h1>
      <KeepAlive>
        <Home />
      </KeepAlive>
    </div>
  );
};

在这个例子中,Home 组件被 KeepAlive 组件包裹起来,所以当 Home 组件被卸载后,实际上并没有被彻底销毁,而是通过 isAlive 状态判断是否活跃。这样,当再次需要渲染 Home 组件时,直接使用缓存的组件即可,无需重新创建和初始化。

需要注意的是,React 中没有像 Vue 的 activated 钩子函数那样的生命周期钩子来判断组件是否从缓存中激活。如果需要在组件从缓存中激活时执行特定的操作,可以通过其他方式来实现,例如监听状态变化或使用 useEffect 钩子函数。

总结起来,通过自定义组件和状态管理,我们可以在 React 中实现类似 keep-alive 的效果,用于缓存和复用组件,提高应用的性能和用户体验。

image.png


+最后来一波笔试!

题干和答案如下,请各位看官自行领会,如下:

// 手写 Promise.all
function fn1() {
  function myPromiseAll(ps) {
    return new Promise((resolve, reject) => {
      const results = new Array(ps.length).fill(null);

      for (let i = 0; i < ps.length; i++) {
        ps[i]
          .then((value) => {
            results[i] = value;
            if (results.indexOf(null) === -1) {
              return resolve(results);
            }
          })
          .catch((err) => {
            return reject(err);
          });
      }
    });
  }

  myPromiseAll([
    Promise.resolve(1),
    new Promise((resolve, reject) => setTimeout(reject, 2000, "bad luck")),
    new Promise((resolve, reject) => setTimeout(resolve, 3000, 3)),
  ])
    .then((values) => console.log("success", values))
    .catch((err) => console.log("failed", err));
}

// 手写apply 函数
function fn2() {
  Function.prototype.myApply = function (thisArg, args) {
    thisArg = thisArg || window;
    thisArg.fn = this;
    const result = thisArg.fn(...args);
    delete thisArg.fn;
    return result;
  };

  function add(a, b) {
    console.log("add", this);
    return a + b;
  }

  const obj = { name: "张三" };
  console.log(add.myApply(obj, [2, 3]));
  console.log(obj);
}

// 实现 add(1)(2,3,4)(5,6)
function fn3() {
  function curry(fn) {
    let totalArgs = [];

    return function cfn(...args) {
      totalArgs = totalArgs.concat(args);
      console.log("totalArgs/fn.length", totalArgs, fn.length);

      if (totalArgs.length === fn.length) {
        return fn.apply(null, [...totalArgs]);
      }

      return function (...moreArgs) {
        return cfn(...moreArgs);
      };
    };
  }

  function add(a, b, c, d, e, f) {
    return a + b + c + d + e + f;
  }

  const cadd = curry(add);
  console.log(cadd(1)(2, 3, 4)(5, 6));
}

// 循环打印红黄绿,红3秒-黄1秒-绿3秒,循环往复
function fn4() {
  const red = () => console.log("red");
  const yellow = () => console.log("yellow");
  const green = () => console.log("green");

  const loop = async (tasks) => {
    for (let { task, delay } of tasks) {
      task();
      await new Promise((resolve) => setTimeout(resolve, delay));
    }

    loop(tasks);
  };

  loop([
    { task: red, delay: 3000 },
    { task: yellow, delay: 1000 },
    { task: green, delay: 3000 },
    { task: yellow, delay: 1000 },
  ]);
}

// 查找文章中出现频率最高的单词
function fn5() {
  const str = `
    when i was young i'd listen to the radio, 
    waiting for my favorite songs, 
    when it played i'd sing along, 
    it made me smile.
    it was such a happy time and not so long ago,
    how i wondered where they'd gone,
    but they're back again just like a long lost friend,
    all the songs i loved so well.
    every sha la la la every woo woo woo oh still shine
    `;

  const regWord = /[\w']+/g;
  const arr = str.match(regWord);
  //   console.log(arr);

  const map = new Map();
  arr.forEach((w, i) =>
    map.get(w) ? map.set(w, map.get(w) + 1) : map.set(w, 1)
  );
  //   console.log(map);

  //   Array.from(map.entries()).forEach(([k, v]) => console.log(k, v));
  Array.from(map.entries())
    .sort((a, b) => b[1] - a[1])
    .forEach((e) => console.log(e));
}

// 实现日期格式化函数
function fn6() {
  const dateFormat = (dateInput, format) => {
    var day = dateInput.getDate();
    var month = dateInput.getMonth() + 1;
    var year = dateInput.getFullYear();
    format = format.replace(/yyyy/, year);
    format = format.replace(/MM/, month);
    format = format.replace(/dd/, day);
    return format;
  };

  console.log(dateFormat(new Date("2020-12-01"), "yyyy/MM/dd"));
  console.log(dateFormat(new Date("2020-04-01"), "yyyy/MM/dd"));
  console.log(dateFormat(new Date("2020-04-01"), "yyyy年MM月dd日"));
}

// 实现负大整数相加
function fn7() {
  function bigAdd(a = "", b = "") {
    a = a.slice(1);
    b = b.slice(1);

    const len = a.length >= b.length ? a.length : b.length;
    let retStr = "";

    let cplus = 0;
    for (let i = 1; i <= len; i++) {
      const at = a.length - i >= 0 ? Number(a[a.length - i]) : 0;
      const bt = b.length - i >= 0 ? Number(b[b.length - i]) : 0;

      //   const ct = at + bt < 10 ? at + bt : (at + bt) % 10 ;
      let ct;
      if (at + bt + cplus < 9) {
        ct = at + bt + cplus;
        cplus = 0;
      } else {
        ct = (at + bt + cplus) % 10;
        cplus = 1;
      }
      console.log(at, bt, cplus, ct);

      retStr = ct + retStr;
    }

    return `-${cplus || ""}${retStr}`;
  }

  console.log(bigAdd("-99", "-999")); //-1098
  console.log(bigAdd("-12", "-34")); //-46
  console.log(bigAdd("-99", "-34")); //-133
  console.log(bigAdd("-0", "-0")); //-0
  console.log(bigAdd("-0", "-26")); //-26
}

(function main() {
  //   fn1();
  //   fn2();
  //   fn3();
  //   fn4();
  //   fn5();
  //   fn6();
  //   fn7();
})();