前端面试题详解整理15|算法 版本号比较  vue23 dif深拷贝jwt和cookie比较 session存在哪,通用级联框组件且带权限判断丶vue响应式原理

178 阅读18分钟

腾讯健康前端

一面

1丶算法 版本号比较  深拷贝(存在循环引用也要拷贝出来,weakmap出现>2就终止递归)

实现深拷贝时需要考虑循环引用的情况,可以使用 WeakMap 来解决。WeakMap 是一种弱引用的数据结构,可以避免循环引用导致的内存泄漏。以下是一个实现深拷贝并处理循环引用的 JavaScript 函数示例:

function deepClone(obj, map = new WeakMap()) {
    // 如果是基本类型或者 null,则直接返回xc
    if (obj === null || typeof obj !== 'object') {
        return obj;
    }
    
    // 如果已经拷贝过该对象,则直接返回其拷贝后的对象
    if (map.has(obj)) {
        return map.get(obj);
    }
    
    // 根据类型创建新对象
    const newObj = Array.isArray(obj) ? [] : {};
    
    // 将新对象添加到 WeakMap 中
    map.set(obj, newObj);
    
    // 递归拷贝每个属性
    for (let key in obj) {
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
            newObj[key] = deepClone(obj[key], map);
        }
    }
    
    return newObj;
}

这个函数接受一个对象作为参数,以及一个 WeakMap 对象用于存储已经拷贝过的对象。它首先检查参数是否为基本类型或者 null,如果是则直接返回原始值。然后检查是否已经拷贝过该对象,如果是则直接返回其拷贝后的对象。接着根据对象类型创建新对象,并将原对象和新对象存入 WeakMap 中。最后递归拷贝对象的每个属性,并返回拷贝后的对象。

通过这样的方式,可以实现深拷贝并处理循环引用的情况。

版本号比较是一个常见的问题,通常用于判断软件版本的新旧或者进行软件升级。版本号通常采用 x.y.z 的形式,其中 x 为主版本号,y 为次版本号,z 为修订版本号。对于每个部分,数字越大表示版本越新。

下面是一个简单的算法来比较两个版本号的大小:

  1. 将版本号字符串分割为数字数组。
  2. 从左向右逐个比较对应位置的数字。
  3. 如果两个版本号对应位置的数字相等,则继续比较下一个位置的数字。
  4. 如果两个版本号对应位置的数字不相等,则较大的数字对应的版本号更高。
  5. 如果一个版本号的所有位置都比另一个版本号大,则该版本号更高。

这个算法可以通过编写函数来实现,下面是一个示例 JavaScript 实现:

function compareVersion(version1, version2) {
    const v1 = version1.split('.').map(Number);
    const v2 = version2.split('.').map(Number);
    
    for (let i = 0; i < Math.max(v1.length, v2.length); i++) {
        const num1 = i < v1.length ? v1[i] : 0;
        const num2 = i < v2.length ? v2[i] : 0;
        
        if (num1 < num2) {
            return -1;
        } else if (num1 > num2) {
            return 1;
        }
    }
    
    return 0;
}

这个函数接受两个版本号字符串作为参数,然后将它们分割为数字数组,并逐个比较数字。最终返回一个数字,表示两个版本号的大小关系:-1 表示 version1 小于 version2,1 表示 version1 大于 version2,0 表示它们相等。

2丶登陆方案,jwt和cookie比较 session存在哪,为什么存redis,除了并发量和读取速度的考虑还有什么。token存哪,如果存vuex刷新还存在吗。

登陆方案通常涉及到用户认证和身份验证,而 JWT(JSON Web Token)和 Cookie 是两种常见的身份验证机制。

  1. JWT(JSON Web Token): JWT 是一种基于 JSON 的开放标准(RFC 7519),用于在网络上安全地传输声明。JWT 可以使用 HMAC 算法或 RSA 算法对令牌进行签名,以确保令牌的完整性。优点是无状态、跨域、可扩展,适合用于无状态的分布式身份验证场景。令牌包含了用户的一些声明信息,例如用户 ID、权限等,客户端在收到令牌后可以解析其中的信息并进行身份验证。

  2. Cookie: Cookie 是一种在客户端存储的小型文本文件,由服务器发送给客户端,并保存在客户端的浏览器中。Cookie 可以包含用户的身份信息或会话信息,用于在客户端和服务器之间进行状态管理。优点是简单易用,广泛支持,但缺点是在跨域请求中可能会受到限制,且可能存在 CSRF(跨站请求伪造)等安全风险。

Session 是服务器端存储用户会话信息的一种机制,通常存储在服务器的内存中或者数据库中。为了提高性能和可扩展性,有些应用选择将 Session 存储在 Redis 等分布式缓存中,这样可以实现集群环境下的 Session 共享和持久化存储。

关于 token 存储的问题,通常有以下几种方案:

  • 存储在前端(例如 Vuex): 前端将 token 存储在本地存储(LocalStorage)或者 Vuex 等状态管理库中,每次请求时将 token 携带在请求头中。这种方式的缺点是容易受到 XSS(跨站脚本攻击)攻击,且存在安全风险。

  • 存储在 Cookie 中: 将 token 存储在 Cookie 中,每次请求时浏览器会自动携带 Cookie,不需要手动设置请求头。但是需要注意安全性,防止 CSRF 攻击。

  • 存储在后端的 Session 中: 将 token 存储在后端的 Session 中,每次请求时服务器端会验证 Session 中的 token。这种方式相对安全,但需要维护 Session 状态,不适合分布式环境。

综上所述,选择合适的身份验证方案取决于应用的需求和安全考虑。JWT 适用于无状态的分布式身份验证,适合前后端分离的应用;而 Cookie 和 Session 则适用于传统的 Web 应用,根据应用场景和安全要求选择合适的方案。

3丶让你实现一个通用级联框组件且带权限判断,你有什么思路(不会,有没有佬说一下)。面试官追问你如何让嵌套的多层组件获取到权限判断的信息(多层嵌套父子组件通信?provide inject 事件总线)。vue父子组件生命周期

以下是一个简单的示例代码,实现了一个通用级联框组件(CascadeSelect),并带有权限判断功能。该组件接受一个级联数据(options)和用户权限信息(userPermissions),根据权限判断是否显示或禁用选项。

<template>
  <div>
    <select v-for="(option, index) in options" :key="index" v-model="selected[index]" @change="handleChange(index)">
      <option v-for="item in option.items" :key="item.value" :value="item.value" :disabled="!checkPermission(item)">{{ item.label }}</option>
    </select>
  </div>
</template>

<script>
export default {
  props: {
    options: {
      type: Array,
      required: true
    },
    userPermissions: {
      type: Object,
      required: true
    }
  },
  data() {
    return {
      selected: []
    };
  },
  methods: {
    handleChange(index) {
      this.selected = this.selected.slice(0, index + 1);
      this.$emit('change', this.selected);
    },
    checkPermission(item) {
      // 在实际应用中根据用户权限信息判断是否显示或禁用选项
      // 这里简单演示,假设只有当用户权限中包含该选项的权限时才显示
      return this.userPermissions.includes(item.value);
    }
  }
};
</script>

在使用该组件时,可以这样传入级联数据和用户权限信息:

<template>
  <div>
    <CascadeSelect :options="options" :userPermissions="userPermissions" @change="handleCascadeChange"></CascadeSelect>
  </div>
</template>

<script>
import CascadeSelect from '@/components/CascadeSelect.vue';

export default {
  components: {
    CascadeSelect
  },
  data() {
    return {
      options: [
        { label: 'Option 1', items: [{ label: 'Item 1', value: 'item1' }, { label: 'Item 2', value: 'item2' }] },
        { label: 'Option 2', items: [{ label: 'Item 3', value: 'item3' }, { label: 'Item 4', value: 'item4' }] }
      ],
      userPermissions: ['item1', 'item3'] // 假设用户拥有 item1 和 item3 的权限
    };
  },
  methods: {
    handleCascadeChange(selected) {
      // 处理级联选择变化的逻辑
      console.log('Selected:', selected);
    }
  }
};
</script>

这样,当用户拥有 item1 和 item3 的权限时,级联框中只会显示这两个选项,其他选项将被禁用。

要实现一个通用的级联框组件,并带有权限判断功能,可以考虑以下思路:

  1. 组件结构设计: 设计组件的结构,包括级联框的布局、样式、交互等。考虑到级联框通常是多层级的结构,可以采用递归组件的方式来实现。

  2. 数据结构设计: 设计级联框所需的数据结构,包括选项数据、级联关系等。可以采用树形结构或者多维数组来表示级联关系,以便于组件的渲染和交互。

  3. 权限判断功能: 在组件中添加权限判断功能,根据用户的权限来决定是否显示或禁用某些选项。可以通过 props 来传递用户权限信息给组件,然后在组件内部进行判断并作出相应的显示或隐藏。

  4. 父子组件通信: 如果希望多层嵌套的子组件也能获取到权限判断的信息,可以使用 Vue 的 provide 和 inject 或者事件总线等方式来实现。通过在父组件中提供权限信息,然后在子组件中通过 inject 来获取,从而实现跨层级的权限传递。

至于 Vue 中父子组件的生命周期,父组件的生命周期包括 beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy、destroyed;子组件的生命周期与父组件类似,也包括 beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy、destroyed。这些生命周期钩子函数在组件的不同阶段被调用,可以用于执行相应的逻辑操作。

如何让嵌套的多层组件获取到权限判断的信息(多层嵌套父子组件通信?

在 Vue 中,要让嵌套的多层组件获取到权限判断的信息,可以使用 provide 和 inject 或者事件总线等方式来实现跨层级的组件通信。

  1. 使用 provide 和 inject: 在父组件中通过 provide 提供权限信息,然后在子组件中通过 inject 来获取。这样子组件就可以在任何层级上访问父组件提供的权限信息。

    <!-- ParentComponent.vue -->
    <template>
      <div>
        <ChildComponent></ChildComponent>
      </div>
    </template>
    
    <script>
    export default {
      provide: {
        userPermissions: {
          // 权限信息
        }
      }
    }
    </script>
    
    <!-- ChildComponent.vue -->
    <template>
      <div v-if="userPermissions.canAccess">
        <!-- 根据权限判断显示内容 -->
      </div>
    </template>
    
    <script>
    export default {
      inject: ['userPermissions']
    }
    </script>
    
  2. 使用事件总线: 创建一个全局的事件总线,让任何组件都可以通过事件总线来进行通信。父组件在渲染子组件时,将权限信息作为参数传递给子组件,子组件通过监听事件总线来获取权限信息。

    // EventBus.js
    import Vue from 'vue';
    export const EventBus = new Vue();
    
    <!-- ParentComponent.vue -->
    <template>
      <div>
        <ChildComponent :userPermissions="userPermissions"></ChildComponent>
      </div>
    </template>
    
    <script>
    import { EventBus } from './EventBus.js';
    export default {
      data() {
        return {
          userPermissions: {
            // 权限信息
          }
        }
      },
      created() {
        EventBus.$on('getUserPermissions', () => {
          EventBus.$emit('sendUserPermissions', this.userPermissions);
        });
      }
    }
    </script>
    
    <!-- ChildComponent.vue -->
    <template>
      <div v-if="userPermissions.canAccess">
        <!-- 根据权限判断显示内容 -->
      </div>
    </template>
    
    <script>
    import { EventBus } from './EventBus.js';
    export default {
      data() {
        return {
          userPermissions: {}
        }
      },
      created() {
        EventBus.$emit('getUserPermissions');
        EventBus.$on('sendUserPermissions', (permissions) => {
          this.userPermissions = permissions;
        });
      }
    }
    </script>
    

这些方法都可以实现跨层级的组件通信,让嵌套的多层组件获取到权限判断的信息。

4丶vue响应式原理,系统如何判断需要更新页面了(利用事件循环),页面更新是同步吗。

Vue 的响应式原理是通过 Object.defineProperty 或 Proxy 来劫持 JavaScript 对象的访问和修改,从而实现对数据的监听和响应。当数据发生变化时,Vue 能够自动检测到变化,并且触发相关的更新操作,更新页面视图。

系统如何判断需要更新页面了呢?在 Vue 中,当数据发生变化时,Vue 会通过事件循环(Event Loop)的机制来异步地进行更新。具体来说,当数据变化时,Vue 会将需要更新的任务放入任务队列中,然后在下一个事件循环中执行这些任务,这样可以确保更新操作在数据变化之后,但在当前 JavaScript 执行栈执行完毕之前进行。

页面更新是同步的吗?虽然 Vue 的更新操作是通过异步的方式进行的,但是在页面更新时,Vue 会同步地执行更新操作,确保页面在同一个事件循环内更新,这样可以避免页面出现不一致的情况。因此,页面更新可以看作是同步的,但实际更新操作是在下一个事件循环中执行的。

5丶vue23 diff过程。key的作用是什么,假如key相同会做什么操作

Vue 中的 diff 过程是 Virtual DOM 的 diff 算法的实现,用于比较前后两个虚拟 DOM 树的差异,并将差异应用到实际 DOM 上,以实现高效的 DOM 更新。

diff 过程主要包括以下步骤:

  1. 新旧节点比较: 首先比较两棵树的根节点,如果根节点相同,则比较它们的子节点;如果根节点不同,则直接替换旧节点为新节点。

  2. 子节点比较: 对比两棵树中相同位置的子节点,分以下情况处理:

    • 如果子节点类型不同,则直接替换旧节点为新节点。
    • 如果子节点类型相同,则进入递归比较子节点的过程。
  3. 列表节点更新: 对比列表节点时,需要根据每个节点的 key 属性进行匹配,key 的作用是帮助 Vue 识别列表中的每个节点,从而减少节点的重新排序和重新创建。如果 key 相同,则认为是同一个节点,尝试复用已存在的节点;如果 key 不同,则将旧节点替换为新节点。

  4. 节点属性更新: 对比节点的属性,将新节点的属性与旧节点的属性进行比较,更新发生改变的属性。

如果两个节点的 key 相同,Vue 会尝试复用已存在的节点,并更新其属性,而不是直接替换节点。这样做可以减少不必要的 DOM 操作,提高性能。如果两个节点的 key 不同,则会将旧节点替换为新节点,从而确保新节点的正确渲染。

6丶react一个组件发生更新,它的子组件也会更新吗,如何避免,vue呢 在 React 中,当一个组件发生更新时,它的子组件也会被重新渲染。这是因为 React 中的组件树是单向数据流的,父组件的状态变化会向下传递给子组件,触发子组件的重新渲染。

要避免不必要的子组件重新渲染,可以通过以下方式:

  1. 使用 PureComponents 或 React.memo: PureComponent 和 React.memo 可以帮助优化组件的性能,它们会对 props 进行浅比较,只有当 props 发生变化时才会重新渲染组件。

  2. 使用 shouldComponentUpdate 或 useMemo 避免不必要的渲染: 在组件中可以通过 shouldComponentUpdate 生命周期函数或 useMemo 钩子函数来判断是否需要重新渲染组件,从而避免不必要的渲染。

  3. 使用 React Context 或 Redux 等状态管理工具: 可以将状态提升到更高层级的组件中管理,然后通过 React Context 或 Redux 等状态管理工具来共享状态,避免将不必要的状态传递给子组件。

在 Vue 中,默认情况下,当一个父组件发生更新时,它的所有子组件也会被重新渲染。但是 Vue 提供了一些优化策略来避免不必要的重新渲染:

  1. 使用 v-once 指令: 可以在组件或元素上使用 v-once 指令来确保只渲染一次,不再进行响应式侦听。

  2. 使用 v-if 和 key 控制组件的显示与隐藏: 可以通过 v-if 指令控制组件的显示与隐藏,以及通过 key 属性来告诉 Vue 在相同组件的不同状态之间进行复用或重新渲染。

  3. 使用 keep-alive 组件缓存组件状态: 可以使用 keep-alive 组件来缓存组件的状态,当组件被切换时,会将组件保存在缓存中,以避免重新渲染和销毁组件。

总之,无论是在 React 还是 Vue 中,都可以通过合适的优化策略来避免不必要的组件重新渲染,提高应用的性能和用户体验。

7丶react fiber架构说一下,为什么用fiber,中断diff如何实现,时间点如何把握,浏览器和react如何合作,交换控制权(requestIdleCallback,postmessage?)

非常抱歉,我误解了您的意思。以下是对您提出的问题的详细回答:

对于 React Fiber 架构,它是 React 中的一种新的架构设计,旨在改善 React 的性能和用户体验。传统的 React 渲染和更新是同步进行的,即一旦开始渲染或更新,就会一直执行完毕,中途无法中断,可能导致页面卡顿和响应速度下降的问题。

为了解决这个问题,React Fiber 架构引入了一种可中断的渲染机制。它将渲染过程拆分为多个小任务(称为 Fiber 节点),并通过调度器(Scheduler)动态调整任务的执行顺序和优先级。这样就可以在执行任务时根据需要中断当前任务,执行其他高优先级的任务,再回来继续执行之前中断的任务,从而实现任务的中断和恢复。

中断 diff 的实现是通过在每个 Fiber 节点上设置标志位来实现的。在执行 diff 过程中,可以根据任务的优先级和时间点来判断是否需要中断当前任务,然后标记当前 Fiber 节点为暂停状态,并将控制权交给调度器。调度器会根据任务的优先级和时间点来重新安排任务的执行顺序,保证高优先级的任务能够及时执行。

React 与浏览器之间是通过调度器和协调器进行交互的。调度器负责调度任务的执行顺序和优先级,协调器根据任务的优先级和时间点来决定任务的执行方式。在浏览器空闲时,React 可以利用浏览器提供的原生 API(如 requestIdleCallback、requestAnimationFrame 等)来执行任务,以确保不会阻塞主线程。另外,React 还可以使用 postMessage 等 API 将任务交给其他线程执行,以提高性能和流畅度。

在整个过程中,React 与浏览器是协同合作的,通过浏览器提供的原生 API 和 React 的调度器来实现任务的调度和执行,从而提高页面的性能和用户体验。

React Fiber 架构是 React 中用于实现异步渲染的一种架构。它的设计目标是提高 React 应用的性能和用户体验,使得在页面加载和交互时能够更加流畅。

  1. 为什么使用 Fiber 架构:

    • 在 React 之前,React 是采用栈调用方式进行组件的渲染和更新。这种方式在大型应用中可能会导致性能问题,因为更新操作是同步的,一旦开始就无法中断,可能会阻塞主线程,导致页面卡顿。
    • Fiber 架构将渲染过程分解为多个小任务,并且引入了优先级概念,可以根据优先级动态调整任务的执行顺序,使得页面的渲染和交互更加流畅。
  2. 中断 diff 如何实现:

    • Fiber 架构中,每个任务被分为多个小任务,每个小任务称为 Fiber 节点。当 React 需要中断当前任务时,可以将当前任务中正在执行的 Fiber 节点标记为暂停状态,并且将控制权交给浏览器或其他高优先级任务。
  3. 时间点如何把握:

    • React 使用浏览器提供的requestIdleCallbackrequestAnimationFrame等浏览器原生 API 来控制任务的执行时间。通过这些 API,React 可以在浏览器空闲时执行任务,或者在下一帧动画之前执行任务,以保证不会阻塞主线程。
  4. 浏览器和 React 如何合作:

    • React 与浏览器之间通过协调器(Scheduler)和调度器(Reconciler)进行交互。调度器负责调度任务的优先级,协调器根据任务的优先级和时间点来决定任务的执行顺序。
    • 在浏览器空闲时,React 可以使用requestIdleCallback来执行任务,以确保不会阻塞主线程。同时,React 也可以使用其他浏览器提供的原生 API,如postMessage等,来与浏览器进行通信。
  5. 交换控制权:

    • React 可以利用浏览器提供的requestIdleCallbackrequestAnimationFrame等 API,将控制权交给浏览器。当浏览器空闲时,React 可以利用这些 API 来执行任务,以确保不会阻塞主线程。另外,React 还可以使用postMessage等 API,将任务交给其他线程执行,以提高性能和流畅度。

8丶pnpm优势,如何减少磁盘空间占用,符号链接在里面发生什么作用 pnpm 是一个基于 npm 客户端的替代品,它有一些优势和特点:

  1. 共享依赖: pnpm 通过符号链接(symlink)的方式将依赖共享在项目之间,而不是像 npm 和 yarn 那样每个项目都有一份完整的依赖树。这样可以减少磁盘空间的占用,尤其是对于大型项目或者多个项目存在时,可以显著减少磁盘空间的使用量。

  2. 快速安装: 由于依赖是共享的,并且通过符号链接进行链接,因此安装依赖的速度会更快,尤其是当有多个项目使用相同的依赖时,只需下载一份依赖即可。

  3. 本地缓存: pnpm 会在本地缓存已经下载的依赖包,避免了重复下载,提高了安装速度,并且可以离线安装。

为了进一步减少磁盘空间的占用,可以采取以下措施:

  1. 清理缓存: 定期清理 pnpm 的本地缓存,可以使用 pnpm prune 命令清理缓存,删除不再使用的依赖包。

  2. 使用软链接(symlink): pnpm 默认使用符号链接的方式进行依赖共享,可以通过软链接的方式将项目的 node_modules 目录链接到一个共享的位置,进一步减少磁盘空间的占用。

符号链接在 pnpm 中的作用是通过创建一个链接指向真实的依赖包,而不是像 npm 和 yarn 那样将依赖包复制到每个项目的 node_modules 目录下。这样,多个项目可以共享同一份依赖,减少了重复存储,节省了磁盘空间,并且在安装和更新依赖时也更加高效。

9丶vite和webpack区别,vite开发环境首屏速度如何加快(面试官说和http2差不多?并发?),vite构建有哪些阶段,cjs的依赖如何处理。

Vite 和 Webpack 是两种常用的前端构建工具,它们在实现原理和使用方式上有一些区别:

  1. Vite:

    • Vite 是一种基于现代浏览器原生 ES 模块的快速构建工具。它利用了 ES 模块的特性,在开发环境下以原生 ES 模块的方式直接导入文件,无需打包成一个或多个文件,因此具有非常快的启动和热更新速度。
    • Vite 的热更新速度得益于其使用了浏览器原生的 ES 模块特性,以及借助了 HTTP/2 协议的服务端推送功能,在开发环境下利用 HTTP/2 并发请求的特性,可以快速加载各个模块,加速首屏加载速度。
  2. Webpack:

    • Webpack 是一个功能强大的静态模块打包工具。它通过加载器(Loader)和插件(Plugin)的方式对各种资源进行处理,并将它们打包成静态资源文件。
    • Webpack 在开发环境下需要先将所有模块打包成一个或多个文件,因此在启动和热更新速度上相对较慢,尤其是当项目规模较大时。

由于 Vite 在开发环境下利用了 HTTP/2 的并发请求特性,因此可以实现类似于 HTTP/2 加载的快速首屏加载效果,但并不是直接由 HTTP/2 实现的。Vite 的构建过程相对简单,一般包括解析、编译、优化等几个阶段。

关于 CommonJS(CJS)的依赖处理,Vite 使用了 Rollup 来处理模块的打包工作。Rollup 默认支持 ES 模块,对于 CommonJS 模块,Vite 使用了 Rollup 的插件 @rollup/plugin-commonjs 来进行转换,使其可以在浏览器中执行。

10丶webpack,loader是什么作用,有什么类的loader,loader的处理顺序,插件是什么, 在Webpack中,Loader和Plugin是两个核心概念,它们分别起到不同的作用:

  1. Loader:

    • 作用: Loader 用于对模块的源代码进行转换,从而使得Webpack能够处理非 JavaScript 文件,并将它们转换为可被添加到依赖图中的有效模块。
    • 类别: Loader可以分为很多种类,比如处理样式的 CSS Loader、Sass Loader,处理图片的 File Loader、URL Loader,处理 TypeScript 的 TypeScript Loader 等等。根据不同的需求,可以使用不同的Loader。
    • 处理顺序: Loader的处理顺序是从后往前,即从右往左执行。在配置中,Loader的执行顺序是由配置数组中的顺序来决定的。
  2. Plugin:

    • 作用: Plugin用于扩展Webpack的功能,在Webpack构建过程中执行一些额外的任务,比如压缩代码、提取公共代码、生成HTML文件等。
    • 原理: Plugin通过在Webpack生命周期中的钩子函数来实现对Webpack的控制和影响。在Webpack的不同阶段,Plugin可以注册不同的钩子函数,并在执行这些钩子函数时,执行自定义的任务。
    • 使用: 在Webpack配置中,通过plugins字段来配置需要使用的Plugin,通常是一个Plugin的实例或者是一个使用了Plugin的构造函数。

总结来说,Loader用于对模块的源代码进行转换,Plugin用于扩展Webpack的功能。在Webpack的构建过程中,Loader主要负责文件的转换,而Plugin主要负责对Webpack构建过程的控制和影响。它们共同作用于Webpack的构建过程,为项目的打包和优化提供了丰富的可能性。

complier和complation区别,有没有写过插件,让你实现同时往所有vue文件中添加一个相同的代码片段如何实现,比如添加一个copyright。知道splitchunk的有哪些规则吗 在Webpack中,Complier 和 Compilation 是两个重要的概念,它们之间存在一些区别:

  1. Compiler:

    • Compiler 是整个 Webpack 构建过程的核心对象,负责将用户配置的参数传递给 Webpack,并调用相应的插件执行编译任务。
    • Compiler 只在 Webpack 启动时创建一次,代表了整个构建过程,包含了 Webpack 配置、选项和资源等信息,它负责管理整个编译过程。
  2. Compilation:

    • Compilation 对象代表了一次新的编译过程,每当 Webpack 从入口开始解析依赖并且生成最终结果时,就会创建一个新的 Compilation 对象。
    • Compilation 对象包含了当前模块资源、编译生成的资源、编译生成资源之间的依赖关系等信息,它负责管理一次编译过程中的所有信息。

写插件可以通过实现 Webpack 提供的插件接口来实现。如果要往所有的 Vue 文件中添加相同的代码片段,可以编写一个自定义插件,在 Compilation 阶段监听 Vue 文件的解析,并在解析完成后往每个 Vue 文件的 AST(抽象语法树)中添加相应的代码片段。

class AddCodePlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap('AddCodePlugin', (compilation) => {
      compilation.hooks.finishModules.tap('AddCodePlugin', (modules) => {
        modules.forEach((module) => {
          if (module.resource && module.resource.endsWith('.vue')) {
            // 在 Vue 文件中添加代码片段
            module._source._value += `\n/* Your code here */`;
          }
        });
      });
    });
  }
}

module.exports = AddCodePlugin;

SplitChunksPlugin 是用于提取公共模块的插件,它通过配置可以实现不同的规则,包括:

  • Chunks 分割策略: 通过配置chunks选项,可以指定哪些类型的块需要被优化成共享模块。
  • 缓存组(Caching Groups): 通过配置cacheGroups选项,可以定义缓存组,根据不同的规则将模块分配到不同的组中。
  • 文件名: 可以通过配置filename选项来指定生成的文件名。
  • 最小尺寸: 可以通过配置minSize选项来指定提取的模块的最小尺寸。
  • 最小块数: 可以通过配置minChunks选项来指定一个模块至少被引用的次数才会被提取。
  • 最大并发请求数: 可以通过配置maxAsyncRequestsmaxInitialRequests选项来限制最大并发请求数。
  • 优先级: 可以通过配置priority选项来指定缓存组的优先级。

以上这些规则可以根据项目的具体需求进行配置,以实现对公共模块的优化。

要向所有的 Vue 文件中添加相同的代码片段,可以编写一个自定义的 Webpack 插件,在编译阶段监听 Vue 文件的解析,并在解析完成后向每个 Vue 文件的 AST(抽象语法树)中添加相应的代码片段。

下面是一个简单的示例插件实现:

class AddCopyright {
  constructor(options) {
    // 可以接受传入的选项,比如版权信息等
    this.options = options;
  }

  apply(compiler) {
    compiler.hooks.compilation.tap('AddCopyright', (compilation) => {
      compilation.hooks.finishModules.tap('AddCopyright', (modules) => {
        modules.forEach((module) => {
          // 只处理 Vue 文件
          if (module.resource && module.resource.endsWith('.vue')) {
            // 在 Vue 文件中添加代码片段
            const sourceCode = module._source._value;
            // 在模板部分添加版权信息
            const updatedSourceCode = sourceCode.replace('<template>', `<template>\n<!-- ${this.options.text} -->`);
            module._source._value = updatedSourceCode;
          }
        });
      });
    });
  }
}

module.exports = AddCopyright;

然后在 webpack 配置文件中引入并使用这个插件:

const AddCopyrightPlugin = require('AddCopyright');

module.exports = {
  // 其他配置...
  plugins: [
    new AddCopyright({
      text: 'Copyright (c) 2022'
    })
  ]
};

这样,在编译过程中,所有的 Vue 文件的模板部分都会添加上相同的版权信息。

11丶https和http区别,假如我通过https传输一个账号密码会被看见吗,这时候是对称加密还是非对称,4次握手过程,3个密钥前2个是干嘛的,后一个是干啥(面试官说前2个握手,后一个加密)。 HTTP 和 HTTPS 的主要区别在于安全性和数据传输方式:

  1. 安全性: HTTPS 是在 HTTP 的基础上添加了 SSL/TLS 协议进行加密通信的一种协议。HTTPS 使用 SSL/TLS 协议对数据进行加密,使得数据在传输过程中更加安全,可以有效防止中间人攻击和窃听等安全问题。

  2. 数据传输方式: HTTP 使用明文传输,数据不经过加密处理,可能会被窃听和篡改;而 HTTPS 使用 SSL/TLS 协议对数据进行加密,保证了数据在传输过程中的机密性和完整性。

假如你通过 HTTPS 传输一个账号密码,即使在网络传输过程中被截获,黑客也无法直接看见账号密码,因为 HTTPS 使用了加密技术。HTTPS 使用了非对称加密和对称加密两种加密方式:

  • 非对称加密(Asymmetric Encryption): 在 HTTPS 握手阶段使用非对称加密,用于交换对称加密所需的密钥。在非对称加密中,公钥用于加密数据,私钥用于解密数据。

  • 对称加密(Symmetric Encryption): 在建立安全连接后,HTTPS 使用对称加密算法对传输的数据进行加密。对称加密使用相同的密钥进行加密和解密,速度更快,适合大量数据的加密。

HTTPS 握手过程包括四次握手:

  1. 客户端发送请求: 客户端向服务器端发送连接请求。

  2. 服务器响应并发送公钥证书: 服务器收到请求后,向客户端返回公钥证书。

  3. 客户端验证证书并生成对称密钥: 客户端验证服务器证书的合法性,并生成用于加密通信的对称密钥。

  4. 服务器收到密钥并确认: 服务器收到客户端发送的密钥后,使用自己的私钥解密,并发送确认消息。

在这个过程中,前两次握手主要是建立连接和验证身份,第三次握手则是为了协商对称密钥,后面的数据传输就是使用对称加密的方式进行的。

12http2和1.1区别,http2这些都为了什么(你的项目可以考虑升级成2)

HTTP/2 相对于 HTTP/1.1 的主要改进和优势包括以下几点:

  1. 多路复用(Multiplexing): HTTP/2 允许在同一个 TCP 连接上并行发送多个请求和响应,从而避免了 HTTP/1.1 中的队头阻塞问题,提高了网络利用率和页面加载速度。

  2. 二进制传输: HTTP/2 将 HTTP 报文从文本格式转换为二进制格式传输,减少了不必要的解析成本,提高了传输效率。

  3. 头部压缩(Header Compression): HTTP/2 使用 HPACK 算法对 HTTP 头部进行压缩,减少了头部大小,降低了网络流量消耗,加快了页面加载速度。

  4. 服务器推送(Server Push): HTTP/2 允许服务器在客户端发起请求之前主动推送资源给客户端,提前满足客户端的请求,减少了客户端的等待时间,加快了页面加载速度。

  5. 流优先级(Stream Priority): HTTP/2 支持流优先级,允许客户端指定请求的优先级,从而更精细地控制资源的加载顺序,提高了页面的渲染性能。

  6. 安全性: HTTP/2 推荐使用加密传输(TLS),因此默认情况下,HTTP/2 在安全连接(HTTPS)上运行,提高了通信的安全性和隐私性。

以上这些改进和优势使得 HTTP/2 相比于 HTTP/1.1 更加高效、快速和安全,能够更好地满足现代 Web 应用的需求。

如果你的项目还在使用 HTTP/1.1,考虑升级到 HTTP/2 可以带来显著的性能提升和用户体验改善,特别是对于需要大量请求的页面或者对网络速度敏感的应用场景,升级到 HTTP/2 是一个不错的选择。

2面

1丶一个多表单的场景,用一个草稿去缓存的时候,假如表单根据不同权限有不同的展示,那你如何实现第二次进入展示草稿时怎么实现这种差异(有没有佬解答一波) 在这种场景下,可以考虑在缓存中保存表单的草稿信息,并且保存表单的权限信息。当用户第二次进入展示草稿时,根据用户的权限信息动态展示表单内容。

具体实现步骤如下:

  1. 保存草稿及权限信息到缓存: 当用户第一次保存草稿时,将表单的草稿内容以及用户的权限信息保存到缓存中,例如使用 localStorage 或 sessionStorage。

  2. 进入页面时读取缓存并根据权限展示表单: 用户再次进入展示草稿的页面时,首先从缓存中读取保存的草稿信息和权限信息。根据用户的权限信息,动态展示表单的各个部分或字段。

  3. 根据权限动态调整表单内容: 根据用户的权限信息,动态调整表单的展示内容。例如,对于管理员权限可以展示所有字段,而对于普通用户只展示部分字段。

  4. 用户交互时更新缓存: 用户在表单中进行交互或修改时,更新对应的草稿信息,并保存到缓存中。确保用户的操作都能及时地反映到缓存中。

通过以上步骤,可以实现根据用户权限动态展示表单内容,并且确保用户再次进入页面时能够看到正确的表单展示。

2丶babel转换如何实现,语法树是什么数据结构 Babel 是一个 JavaScript 编译器,它可以将 ECMAScript 2015+(ES6+)的代码转换为向后兼容的 JavaScript 代码,以便在当前和旧版的浏览器或环境中运行。Babel 的转换过程主要分为以下几个步骤:

  1. 解析(Parsing): Babel 首先会将输入的 JavaScript 代码解析成抽象语法树(Abstract Syntax Tree,AST),AST 是表示代码结构的树状数据结构,它可以准确地描述代码的语法结构。

  2. 转换(Transformation): Babel 会对 AST 进行遍历和修改,应用各种转换规则(plugins)来对代码进行转换。转换规则可以添加、移除或修改 AST 节点,以实现对代码的转换操作,例如转换新特性、优化性能等。

  3. 生成(Code Generation): 经过转换后的 AST 被重新生成为 JavaScript 代码,这些代码可能与原始代码不同,但在语义上等价。生成的 JavaScript 代码可以在目标环境中运行,兼容性更好或具有特定的优化效果。

Babel 本身提供了一系列插件(plugins),每个插件都对应着一种转换规则,可以根据需要选择性地使用插件来进行代码转换。同时,Babel 还支持自定义插件,允许开发者根据自己的需求编写定制化的转换规则。

关于语法树(AST)的数据结构,它是一个树状结构,由多个节点组成,每个节点代表代码中的一个语法单元,例如变量声明、函数调用、条件语句等。AST 中的每个节点都包含了与其对应的代码片段的详细信息,例如节点的类型、位置、子节点等,这样可以方便地对代码进行分析、修改和转换。AST 是编译器中重要的数据结构之一,用于表示源代码的抽象语法结构,被广泛应用于编程语言的静态分析、代码优化和转换等领域。

3丶nodejs和其他语言相比有什么优势(高并发适合io密集,不适合cpu密集) Node.js 在与其他语言相比具有一些明显的优势,尤其在处理高并发和 I/O 密集型任务时表现突出。以下是一些 Node.js 的优势:

  1. 事件驱动和非阻塞 I/O: Node.js 基于事件驱动和非阻塞 I/O 模型,使得它非常适合处理大量并发连接和 I/O 密集型任务。通过单线程和事件循环机制,Node.js 能够高效地处理大量请求,同时保持较低的资源消耗。

  2. 高性能: Node.js 底层使用了 Chrome V8 引擎,它是一个高性能的 JavaScript 引擎,能够将 JavaScript 代码快速编译成本地机器码,从而提高执行效率。另外,非阻塞 I/O 模型也使得 Node.js 在处理大量并发请求时能够保持高性能。

  3. 统一的语言: 使用 Node.js,开发者可以使用 JavaScript 来编写服务器端和客户端的代码,这样就能够实现前后端统一,减少了开发和维护的复杂性。此外,JavaScript 的灵活性和广泛应用也使得 Node.js 生态系统非常丰富。

  4. 模块化和包管理: Node.js 提供了强大的模块化机制,允许开发者将代码分割成小的、可重用的模块。同时,Node.js 的包管理工具 npm 提供了海量的开源模块和库,可以方便地集成到项目中,提高开发效率。

  5. 跨平台: Node.js 支持跨平台运行,可以在 Windows、Linux 和 macOS 等多种操作系统上运行,这样就能够实现代码的无缝迁移和部署。

尽管 Node.js 在处理高并发和 I/O 密集型任务方面表现出色,但它并不适合处理 CPU 密集型任务,因为 Node.js 是单线程的,无法充分利用多核 CPU 的优势。因此,在需要大量计算的情况下,其他语言如 Python、Java 或 Go 等可能会更适合。

4丶你要怎么封装虚拟列表,封装成一个通用组件或者hooks 封装虚拟列表(Virtual List)作为一个通用组件或者自定义 Hooks,可以提高代码的复用性,并使得在不同场景下使用虚拟列表更加方便。下面是封装虚拟列表的一般步骤和考虑因素:

  1. 确定需求和接口: 首先要确定虚拟列表的需求和所需的接口,包括列表数据、每项数据的高度、可视区域的高度等。

  2. 实现虚拟滚动: 使用 CSS 实现列表项的虚拟滚动,即只渲染可视区域内的列表项,超出可视区域的部分不渲染,从而提高性能。

  3. 计算列表项高度: 如果列表项高度不固定,需要提供计算列表项高度的方法或者自定义 Hooks,以便获取列表项的高度信息。

  4. 监听滚动事件: 监听滚动事件,动态计算当前可视区域内的列表项范围,从而实现虚拟滚动。

  5. 数据分页: 如果列表数据较大,可以考虑分页加载数据,当滚动到列表底部时自动加载下一页数据。

  6. 性能优化: 对列表项进行重用,避免频繁创建和销毁列表项;使用 memoization 技术优化性能。

  7. 提供扩展接口: 根据需要,提供自定义的接口或者配置项,例如加载更多数据的回调函数、自定义渲染列表项的方法等。

  8. 文档和示例: 编写清晰的文档说明虚拟列表的使用方法和接口,提供示例代码以供参考。

综上所述,封装虚拟列表作为一个通用组件或者自定义 Hooks,需要考虑到性能、灵活性和易用性等方面,并提供清晰的文档和示例,以便开发者能够快速使用和定制虚拟列表组件。

5丶怎么获取海内外用户的打开页面的时间,埋点,怎么发送埋点数据,sendBeacon

要获取海内外用户的打开页面的时间并进行埋点,可以通过以下步骤实现:

  1. 客户端埋点: 在页面加载时,使用 JavaScript 代码获取当前时间,并将该时间作为页面打开时间的标记。可以在页面的 onload 事件中执行此操作。

  2. 发送埋点数据: 使用发送数据的方式将页面打开时间的信息发送到服务器端进行记录。可以使用异步请求(Ajax)或者 navigator.sendBeacon 方法发送数据。navigator.sendBeacon 方法可以在页面关闭时仍然发送数据,适用于页面关闭时发送的埋点数据。

  3. 处理埋点数据: 在服务器端接收到埋点数据后,可以根据业务需求进行相应的处理,例如记录用户打开页面的时间、用户的地理位置等信息,并存储到数据库或者日志文件中。

以下是使用 sendBeacon 方法发送埋点数据的示例代码:

// 获取页面打开时间
let pageOpenTime = new Date().getTime();

// 监听页面关闭事件,在页面关闭时发送埋点数据
window.addEventListener('unload', function(event) {
    // 构造要发送的埋点数据
    let eventData = {
        pageOpenTime: pageOpenTime,
        // 其他需要发送的埋点数据
    };

    // 将埋点数据转换为字符串
    let eventDataString = JSON.stringify(eventData);

    // 使用 sendBeacon 方法发送埋点数据
    navigator.sendBeacon('http://example.com/track', eventDataString);
});

在上面的示例中,navigator.sendBeacon 方法会在页面关闭时发送埋点数据到指定的 URL,其中 eventDataString 是要发送的埋点数据,http://example.com/track 是接收埋点数据的服务器端地址。服务器端需要相应的接口来处理接收到的埋点数据。

作者:已坠机
链接:www.nowcoder.com/discuss/541…
来源:牛客网