前端最新场景题,不来看一看吗?(四)

121 阅读9分钟

1.script标签上有那些属性,有什么作用?

src:

  • 用于指定外部脚本文件的 URL。
  • 浏览器会下载并执行该脚本

type:

  • 定义脚本的 MIME 类型。
  • 在 HTML 5 中,如果 type 未指定,或者其值为 text/javascript,则浏览器会默认使用 JavaScript.

async:

  • 表示脚本可以异步加载,不会阻塞页面的渲染。
  • 适用于那些不依赖于其他脚本的脚本文件。

defer:

  • 表示脚本可以延迟执行直到文档解析完成后。
  • 适用于那些依赖于完整文档结构的脚本。
  • 与 async 不同,defer 脚本会按照它们在文档中出现的顺序执行.

integrity:

  • 用于验证资源的完整性。
  • 包含一个加密签名,浏览器会验证下载的脚本与签名是否匹配.

charset:

  • 定义脚本的字符集。
  • 在 HTML 5 中,推荐使用 <meta charset="UTF-8"> 来定义字符集,而不是在 <script> 标签中。

text:

  • 定义脚本的内容。
  • 通常与 type="module" 一起使用,用于定义模块脚本的内容.

nomodule:

  • 用于定义不支持模块的浏览器的回退脚本。
  • 当浏览器不支持 JavaScript 模块时,会执行带有 nomodule 属性的脚本。
<script type="module" src="module.js"></script>
<script nomodule src="fallback.js"></script>

2.为什么spa应用都会提供一个hash路由,有什么好处?

  1. 无页面刷新

    • 使用 hash 可以实现页面的无刷新更新,提供更流畅的用户体验。
  2. 浏览器历史兼容

    • 浏览器原生支持 hash 变化监听,当 hash 改变时,可以触发 hashchange 事件,这使得 SPA 能够在不刷新页面的情况下,响应 URL 的变化。
  3. 易于实现

    • 相比于 HTML5 的 History API,hash 路由更容易实现,因为它不需要服务器端的特殊配置来处理 404 错误页面并重定向回入口 HTML 文件。
  4. SEO 友好

    • 尽管服务器不解析 hash 部分的 URL,但 SPA 可以通过一些策略(如服务端渲染或预渲染)来生成 SEO 友好的页面。
  5. 易于分享

    • 用户可以分享通过 hash 路由生成的 URL,其他用户访问这个 URL 时,SPA 会加载相同的视图状态。
  6. 后退/前进按钮支持

    • 浏览器的后退和前进按钮可以正常工作,因为 hash 的变化会改变浏览器的历史记录。
  7. 前端控制

    • SPA 完全控制路由逻辑,可以根据应用的需求灵活地设计 URL 结构。
  8. 安全性

    • 由于 hash 不会被发送到服务器,因此可以避免一些安全问题,比如泄露敏感信息。
  9. 兼容性

    • 几乎所有的浏览器都支持 hash 路由,因此它具有很好的兼容性

3.单点登录是什么,具体流程?

简单地理解为通过单点登录可以让用户只需要登录一次软件或者系统,那么同系统下的平台都可以免去再次注册、验证、访问权限的麻烦程序,通俗易懂的理解为一次性登录也可以一次性下线。

1、一个系统登录流程:用户进入系统——未登录——跳转登录界面——用户名和密码发送——服务器端验证后,设置cookie发送浏览器,设置一个session存放在服务器——用户再次请求(带上cookie)——服务器验证cookie和session匹配后,就可以进行业务了。

2、多个系统登录:如果一个大公司有很多系统,a.seafile.com, b.seafile.com,c.seafile.com。这些系统都需要登录,如果用户在不同系统间登录需要多次输入密码,用户体验很不好。所以使用 SSO (single sign on) 单点登录实现。

3、相同域名,不同子域下的单点登录:在浏览器端,根据同源策略,不同子域名的cookie不能共享。所以设置SSO的域名为根域名。SSO登录验证后,子域名可以访问根域名的 cookie,即可完成校验。在服务器端,可以设置多个子域名session共享(Spring-session)

4、不同域名下的单点登录:CAS流程:用户登录子系统时未登录,跳转到 SSO 登录界面,成功登录后,SSO 生成一个 ST (service ticket )。用户登录不同的域名时,都会跳转到 SSO,然后 SSO 带着 ST 返回到不同的子域名,子域名中发出请求验证 ST 的正确性(防止篡改请求)。验证通过后即可完成不同的业务。

一般情况下,用户的登录状态是记录在 Session 中的,要实现共享登录状态,就要先共享 Session,但是由于不同的应用系统有着不同的域名,尽管 Session 共享了,但是由于 SessionId 是往往保存在浏览器 Cookie 中的,因此存在作用域的限制,无法跨域名传递。

也就是说当用户在 a.com 中登录后,Session Id 仅在浏览器访问 a.com 时才会自动在请求头中携带,而当浏览器访问 b.com 时,Session Id 是不会被带过去的。实现单点登录的关键在于,如何让 Session Id(或 Token)在多个域中共享。

第一个方法: 只需要将 Cookie 的 domain 属性设置为父域的域名(主域名),同时将 Cookie 的 path 属性设置为根路径,这样所有的子域应用就都可以访问到这个 Cookie 了。不过这要求应用系统的域名需建立在一个共同的主域名之下,如 tieba.baidu.com 和 map.baidu.com,它们都建立在 baidu.com 这个主域名之下,那么它们就可以通过这种方式来实现单点登录。

第二个方法: 部署一个认证中心,认证中心就是一个专门负责处理登录请求的独立的 Web 服务。 应用系统检查当前请求有没有 Token,如果没有,说明用户在当前系统中尚未登录,那么就将页面跳转至认证中心进行登录。由于这个操作会将认证中心的 Cookie 自动带过去,因此,认证中心能够根据 Cookie 知道用户是否已经登录过了。如果认证中心发现用户尚未登录,则返回登录页面,等待用户登录,如果发现用户已经登录过了,就不会让用户再次登录了,而是会跳转回目标 URL ,并在跳转前生成一个 Token,拼接在目标 URL 的后面,回传给目标应用系统

第三个方法: 单点登录的关键在于,如何让 Session Id(或 Token)在多个域中共享。但是 Cookie 是不支持跨主域名的,而且浏览器对 Cookie 的跨域限制越来越严格。

在前后端分离的情况下,完全可以不使用 Cookie,我们可以选择将 Session Id (或 Token )保存到浏览器的 LocalStorage 中,让前端在每次向后端发送请求时,主动将 LocalStorage 的数据传递给服务端。这些都是由前端来控制的,后端需要做的仅仅是在用户登录成功后,将 Session Id (或 Token )放在响应体中传递给前端。

在这样的场景下,单点登录完全可以在前端实现。前端拿到 Session Id (或 Token )后,除了将它写入自己的 LocalStorage 中之外,还可以通过特殊手段将它写入多个其他域下的 LocalStorage 中

4.web网页如何禁止别人修改水印?

这个之前水印文章弄过。监听移除样式事件。

 
/**
 * @param text  水印文字
 * @param font  水印字体
 * @param color 水印颜色
 * @param deg   水印倾斜角度,负数上坡正数下坡
 * @param gap   相邻两个水印的间距
 */
 
const config = {
  attributes: true, // 属性的变动
  attributeOldValue: true, // 布尔值,表示观察attributes变动时,是否需要记录变动前的属性值
  attributeFilter: ['class', 'id', 'style'], // 数组,表示需要观察的特定属性(比如['class','src']
  characterData: true, // 节点内容或节点文本的变动
  childList: true, // 子节点的变动(指新增,删除或者更改)
  subtree: true, // 布尔值,表示是否将该观察器应用于该节点的所有后代节点。
  characterDataOldValue: true // 布尔值,表示观察characterData变动时,是否需要记录变动前的值。
}
 
function addWaterfall(el, {
  text = '我是水印',
  font = 'bold 34px STHeiti',
  color = 'rgba(180,180,180,0.8)',
  deg = -20,
  gap = 200
}) {
  // MutationObserver监听元素阻止用户操作dom
  const ob = new MutationObserver(changeOb)
  // 开始监听
  function beginWatch() {
    ob.observe(el, config)
  }
 
  // 停止监听,可通过observe重新监听
  function stopWatch() {
    ob.disconnect()
  }
 
  // 监听回调
  function changeOb(mutationList) {
    stopWatch()
    console.log('修改了dom')
    mutationList.forEach(mutation => {
      switch (mutation.type) {
        case 'attributes':
        {
          const attr = mutation.attributeName
          if (attr === 'style') {
            mutation.target.style = mutation.oldValue
          } else if (attr === 'id') {
            mutation.target.id = mutation.oldValue
          }
        }
      }
    })
    beginWatch()
  }
 
  function createWaterfall() {
    // 停止监听,否则会无限循环卡死
    stopWatch()
    // 创建canvas生成base64图片
    const box_w = parseFloat(document.defaultView.getComputedStyle(el).width)
    const box_h = parseFloat(document.defaultView.getComputedStyle(el).height)
    const canvas = document.createElement('canvas')
    canvas.id = 'canvas'
    canvas.width = box_w
    canvas.height = box_h
    const ctx = canvas.getContext('2d')
    ctx.rotate((deg * Math.PI) / 180)
    ctx.font = font
    ctx.fillStyle = color
    // ctx.textAlign = 'left'
    ctx.textBaseline = 'middle'
    // 暴力渲染,在最多的行数或列数的基础上前后各多渲染二分之根号二的数量
    const MAX_NUM = parseInt(Math.max(box_w, box_h) / gap) + 1
    const LEFT = Math.floor(MAX_NUM / Math.sqrt(2) * -1)
    const RIGHT = Math.ceil(MAX_NUM + MAX_NUM / Math.sqrt(2))
    console.log(LEFT, RIGHT)
    for (let i = LEFT; i <= RIGHT; i++) {
      for (let j = LEFT; j <= RIGHT; j++) {
        ctx.fillText(text, gap * i, gap * j)
      }
    }
    el.style.background = `url(${canvas.toDataURL('image/png')})`
    // 继续监听
    beginWatch()
  }
 
  return createWaterfall
}
 
export default addWaterfall
// 全局自定义指令  waterfall
Vue.directive('waterfall', {
  inserted(el, binding) {
    let width = ''
    let height = ''
    const createWaterfall = addWaterfall(el, binding.value)
    // 监听元素宽高发生改变,重新渲染水印
    function resize() {
      const style = document.defaultView.getComputedStyle(el)
      if (width !== style.width || height !== style.height) {
        console.log('resize')
        createWaterfall()
      }
      width = style.width
      height = style.height
    }
    resize()
    el.__VueSetInterval = setInterval(resize, 300)
  },
  unbind(el) {
    clearTimeout(el.__VueSetInterval)
  }
})
<div v-waterfall="{
    text: '我是水印', // 各种水印相关的配置
}">
    <span v-for="item in 50" :key="item" class="PdR50" style="line-height: 34px;"> 文本内容文本内容文本内容文本内容文本内容文本内容    </span>
</div>

5.用户访问白屏,如何排查解决?

1.前端原因: 某些文件,函数方法调用异常,接口调用异常等。 或者:工程文件过大,加载缓慢。 eg: vue首页白屏的原因是打包后的js和css文件过大,浏览器初始访问网站时,会先加载该项目的js和css文件,加载完成后才会进行页面渲染。如果打包的文件过大,加载时间就会变长,出现视觉上的页面白屏. 优化方法:组件懒加载,cdn加速,gzip压缩,预加载,loading效果都可。

2.后端原因: 调用服务异常,服务器异常,nginx配置异常等。

6.如何实现大对象深度对比?

1.loadsh的isEqual()。

2.自定义函数:

function isDeepEqual(obj1, obj2) {
  if (obj1 === obj2) {
    return true;
  }

  if (typeof obj1 !== 'object' || typeof obj2 !== 'object' || obj1 == null || obj2 == null) {
    return false;
  }

  const keys1 = Object.keys(obj1);
  const keys2 = Object.keys(obj2);

  if (keys1.length !== keys2.length) {
    return false;
  }

  for (const key of keys1) {
    if (!keys2.includes(key) || !isDeepEqual(obj1[key], obj2[key])) {
      return false;
    }
  }

  return true;
}

7.如何理解数据驱动视图,有什么核心要素?

视图是由数据驱动生成的,我们对视图的修改,不需要直接操作DOM,而可以通过修改预先设定好的数据达到修改视图的目的. 当Data对象中的变量有变化时,会触发当前对象中的setter方法,setter方法会通知getter方法收集的订阅者当前变量发生的变化,重新触发渲染函数,更新页面视图。

数据驱动的中心思想,是通过重写Data中数据的get和set属性方法,让数据在被渲染时把所有用到自己的订阅者存放在自己的订阅者列表中,当数据发生变化时将该变化通知到所有订阅了自己的订阅者,达到重新渲染的目的

派发更新,依赖收集,重新渲染视图。

vue2是通过defineProperty监听:

// 验证更新是否触发
function updateView(){
  console.log('视图更新')
}
// 重新定义属性,监听起来
function defineReactive(target, key, value){
  Object.defineProperty(target, key, {
    get(){
      return value
    },
    set(newVal){
      // value 一直在闭包中,此处设置完成后,下次get能够获取最新设置的值
      // 这里有个小优化,若相同则不触发更新
      if(newVal !== value){
        value = newVal
        // 触发更新
        updateView()
      }
    }
  })
}

// 重新定义属性,监听起来
function defineReactive(target, key, value){
  // 再次用value嵌套调用 observe 深,若为对象,则进行进一步监听,若非value非对象则直接返回
  observe(value)
  Object.defineProperty(target, key, {
    get(){
      return value
    },
    set(newVal){
      // value 一直在闭包中,此处设置完成后,下次get能够获取最新设置的值
      if(newVal !== value){
        value = newVal
        // 触发更新
        updateView()
      }
    }
  })
}
// 测试数据
const data = {
  name: 'Yimwu',
  id: 001,
  information: {
    tel: '135xxxxx354',
    email: '15xxxxx@xx.com' 
  }
}

// 监听数据
observe(data)

// 测试
data.name = 'YI' // (监听成功)输出 --> 数据更新
data.information.tel = '00000000000' (监听成功)输出 --> 数据更新


而vue3则是使用Proxy代理来实现:

// 判断是否是一个对象,是就用 reactive 来代理
const convert = val => (isObject(val) ? reactive(val) : val)

class RefImpl {
    constructor(_rawValue) {
        this._rawValue = _rawValue
        this.__v_isRef = true
        // 判断 _rawValue 是否是一个对象
        // 如果是对象调用reactive使用 proxy来代理
        // 不是返回 _rawValue 本身
        this._value = convert(_rawValue)
    }
    // 使用get/set 存取器,来进行追踪和触发
    get value() {
        // 追踪依赖
        track(this, 'value')
        // 当然 get 得返回 this._value
        return this._value
    }
    set value(newValue) {
        // 判断旧值和新值是否一直
        if (newValue !== this._value) {
            this._rawValue = newValue
            // 设置新值的时候也得使用 convert 处理一下,判断新值是否是对象
            this._value = convert(this._rawValue)
            // 触发依赖
            trigger(this, 'value')
        }
    }
}

export function ref(rawValue) {
    // __v_isRef 用来标识是否是 一个 ref 如果是直接返回,不用再转
    if (isObject(rawValue) && rawValue.__v_isRef) return rawValue

    return new RefImpl(rawValue)
}


// activeEffect 表示当前正在走的 effect
let activeEffect = null
export function effect(callback) {
    activeEffect = callback
    callback()
    activeEffect = null
}

// targetMap 表里每个key都是一个普通对象 对应他们的 depsMap
let targetMap = new WeakMap()
export function track(target, key) {
    // 如果当前没有effect就不执行追踪
    if (!activeEffect) return
    // 获取当前对象的依赖图
    let depsMap = targetMap.get(target)
    // 不存在就新建
    if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()))
    }
    // 根据key 从 依赖图 里获取到到 effect 集合
    let dep = depsMap.get(key)
    // 不存在就新建
    if (!dep) {
        depsMap.set(key, (dep = new Set()))
    }
    // 如果当前effectc 不存在,才注册到 dep里
    if (!dep.has(activeEffect)) {
        dep.add(activeEffect)
    }
}

// trigger 响应式触发
export function trigger(target, key) {
    // 拿到 依赖图
    const depsMap = targetMap.get(target)
    if (!depsMap) {
        // 没有被追踪,直接 return
        return
    }
    // 拿到了 视图渲染effect 就可以进行排队更新 effect 了
    const dep = depsMap.get(key)

    // 遍历 dep 集合执行里面 effect 副作用方法
    if (dep) {
        dep.forEach(effect => {
            effect()
        })
    }
}



响应式顺序:effect > track > trigger > effect

在组件渲染过程中,一个 effect 会会触发get,从而对值进行 track,当值发生改变,就会进行 trigge,执行 effect 来完成一个响应。

8.vue-cli做了哪些事?有什么功能?

是一个简单的Vue.js工程命令行脚手架工具。 快速生成项目的基础结构,包括目录结构、初始化文件。 持不同环境(开发、生产等)的配置,如环境变量和不同环境的入口文件。包括初始化vue/react等的项目。

  1. 项目初始化:当我们使用vue-cli创建一个新项目时,它会自动完成项目的初始化工作。这包括创建项目的基本文件夹结构、配置文件以及初始的依赖项。
  2. 本地开发服务器:vue-cli还提供了一个本地开发服务器,可以在开发过程中用于实时预览和调试。这个服务器使用webpack-dev-server来实现,支持热模块替换(HMR)和自动刷新等功能,大大提高了开发效率。
  3. 项目打包:在项目开发完成后,我们可以使用vue-cli提供的命令将项目打包成静态文件。这个过程会自动优化和压缩代码,并生成可部署的静态文件,以便最终在生产环境中使用。
  4. 插件和配置:vue-cli还提供了一系列插件和配置选项,用于定制化项目的行为和特性。例如,可以选择使用ESLint代码规范检查工具、添加路由管理器、集成CSS预处理器、配置代理服务器等。

9.执行100万个任务,如何保证浏览器不卡顿?

  • 使用 Web Workers 将任务放在后台线程执行,避免阻塞主线程。
// worker.js
self.addEventListener('message', function(e) {
  const data = e.data; // 接收主线程发送的数据
  let processedData = data.sort((a, b) => b - a); // 示例:对数据进行排序
  self.postMessage(processedData); // 将处理后的数据发送回主线程
});

// main.js
const worker = new Worker('worker.js');

// 假设这是我们需要处理的大量数据
let largeDataSet = generateLargeDataSet(1000000);

// 发送数据到 Web Worker
worker.postMessage(largeDataSet);

// 接收 Web Worker 处理后的数据
worker.onmessage = function(e) {
  const processedData = e.data;
  performRendering(processedData);
};

function generateLargeDataSet(size) {
  // 根据需要生成或加载大量数据
  return new Array(size).fill().map((_, index) => index + 1);
}

let index = 0; // 用于记录当前渲染到数组中的索引

function performRendering(data) {
  const chunkSize = 1000; // 每次渲染的条目数
  requestAnimationFrame(function renderChunk() {
    if (index < data.length) {
      // 执行渲染逻辑,例如将数据添加到 DOM 中
      renderDataChunk(data.slice(index, index + chunkSize));
      index += chunkSize;
      performRendering(data); // 递归调用以继续渲染
    }
  });
}

function renderDataChunk(chunk) {
  // 在这里将数据块添加到 DOM,可以使用 Document Fragment 来优化性能
  const fragment = document.createDocumentFragment();
  chunk.forEach(item => {
    const element = document.createElement('div');
    element.textContent = `Item ${item}`;
    fragment.appendChild(element);
  });
  document.body.appendChild(fragment);
}
  • 使用个定时器,一次定时器加载一点.
<script>
    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) // 最后一次渲染一定少于20条,因此取最小

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

    loop(total)
</script>
但是容易导致闪屏,定时器的执行需要等待前面的渲染队列执行完毕,
而定时器的执行又恰好是创建li,这才导致一个非预期时间产生li导致的闪屏问题
  • requestAnimationFrame + fragment(时间分片)
<script>
    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)
</script>


  • 虚拟列表

vue-virtual-scroller组件即可实现。 手写:

<template>

  <div ref="list" class="infinite-list-container" @scroll="scrollEvent">

    <div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }"></div>

    <div class="infinite-list" :style="{ transform: getTransform }">

      <slot :visibleData="visibleData" :itemSize="itemSize" />

    </div>

  </div>

</template>

  


<script>

export default {

  name: 'MyVirtualList',

  props: {

    items: {

      type: Array,

      default: () => []

    },

    itemSize: {

      type: Number,

      default: 50

    },


  },

  computed: {

    listHeight() {

      return this.items.length * this.itemSize

    },

    visibleCount() {

      return Math.ceil(this.screenHeight / this.itemSize)

    },

    getTransform() {

      return `translate3d(0,${this.startOffset}px,0)`

    },

    visibleData() {

      return this.items.slice(this.start, Math.min(this.end, this.items.length))

    }

  },

  mounted() {

    this.screenHeight = this.$refs.list.clientHeight

    this.start = 0

    this.end = this.start + this.visibleCount

    this.loading = false // 记录是否正在加载下一页

  },

  data() {

    return {

      screenHeight: 0,

      startOffset: 0,

      start: 0,

      end: 0,

      nextLoading: 1

    }

  },

  methods: {

    scrollEvent() {

      let scrollTop = this.$refs.list.scrollTop

      if (scrollTop + this.screenHeight >= this.listHeight) {

        // 判断是否已经触发过下一页的方法,避免重复触发

        if (this.nextLoading) {

          this.nextLoading = true

          this.$emit('load-next-page',this.nextLoading)

        }

      }

      this.start = Math.floor(scrollTop / this.itemSize)

      this.end = this.start + this.visibleCount

      this.startOffset = scrollTop - (scrollTop % this.itemSize)

    }

  }

}

</script>

  


<style lang="less" scoped>

.infinite-list-container {

  height: 100%;

  overflow: auto;

  position: relative;

}

  


.infinite-list-phantom {

  position: absolute;

  left: 0;

  top: 0;

  right: 0;

  z-index: -1;

}

  


.infinite-list {

  left: 0;

  right: 0;

  top: 0;

  position: absolute;

}

  


.infinite-list-item {

  line-height: 50px;

  text-align: center;

  color: #555;

  border: 1px solid #ccc;

  box-sizing: border-box;

  display: flex;

  align-items: center;

  


  .dialogWarnItem {

    height: 20px;

    display: flex;

    justify-content: center;

    align-items: center;

  }

  


  .infinite-list-container::-webkit-scrollbar-thumb {

    background: rgb(113, 179, 149);

  }

}

</style>