前端面试题汇总2

前端面试题汇总2前端面试题汇总 prefetch 断点续传

大家好,欢迎来到IT知识分享网。

什么是csp,具体是如何操作的?

  • 可以从浏览器的执行进行预防,一种是使用纯前端的方式,不用服务器端拼接后返回。另一种是对需要插入到HTML中的代码做好充分的转义。对于DOM型的攻击,主要是前端脚本的不可靠。对于数据获取渲染和字符串拼接的时候应该对可能出现的恶意代码情况进行判断。
  • 使用csp。本质就是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行。规则需要开发人员去配置。有两种方式开启CSP,一种是设置HTTP首部的Content-Security-Policy,一种是设置meta标签的方式<meta http-equiv=“Content-Security-Policy”>
  • 对一些敏感信息进行保护,如cookie使用http-only使得脚本无法获取。

内容安全策略(CSP),其核心思想十分简单:网站通过发送一个 CSP 头部,来告诉浏览器什么是被授权执行的与什么是需要被禁止的。它的实现和执行全部由浏览器完成,开发者只需提供配置。CSP 大大增强了网页的安全性。攻击者即使发现了漏洞,也没法注入脚本,除非还控制了一台列入了白名单的可信主机。

1、使用CSP的两种方式

(1)、在HTTP Header上使用(首选)
"Content-Security-Policy":策略;
(配置好并启用后,不符合CSP的外部资源就会被阻止加载。)
"Content-Security-Policy-Only":策略;
(表示不执行限制选项,只是记录违反限制的行为。它必须与resport-uri选项配合使用)
(2)、在HTML上使用
<meta http-equiv="content-security-policy" content="策略">
<meta http-equiv="content-security-policy-report-only" content="策略">

2、策略应该怎么写

// 限制所有的外部资源,都只能从当前域名加载 Content-Security-Policy: default-src 'self' // default-src 是 CSP 指令,多个指令之间用英文分号分割;多个指令值用英文空格分割 Content-Security-Policy: default-src https://host1.com https://host2.com; frame-src 'none'; object-src 'none' //JS的加载策略,会覆盖default-src中的策略 Content-Security-Policy: script-src https://host1.com https://host2.com 

有时,我们不仅希望防止 XSS,还希望记录此类行为。report-uri就用来告诉浏览器,应该把注入行为报告给哪个网址。

// 通过report-uri指令指示浏览器发送JSON格式的拦截报告到某个url地址 Content-Security-Policy: default-src 'self'; ...; report-uri /my_amazing_csp_report_parser; // 报告看起来会像下面这样 { 
    "csp-report": { 
    "document-uri": "http://example.org/page.html", "referrer": "http://evil.example.com/", "blocked-uri": "http://evil.example.com/evil.js", "violated-directive": "script-src 'self' https://apis.google.com", "original-policy": "script-src 'self' https://apis.google.com; report-uri http://example.org/my_amazing_csp_report_parser" } } 

使用过cookie吗?有哪些API,浏览器可以修改cookie的值吗?有哪些属性?

1、如何设置Cookie

  • 服务器端设置:response header中有一项叫set-cookie,是服务端专门用来设置cookie的。
    • HTTP 响应报文通过 Set-Cookie 头字段给浏览器给当前网站设置 cookie:
    • 一个set-Cookie字段只能设置一个cookie,当你要想设置多个 cookie,需要添加同样多的set-Cookie字段。
HTTP/1.1 200 OK Set-Cookie: lang=en-US; Path=/ Set-Cookie: token=abcd; Max-Age=6000; Path=/; Expires=Thu, 28 Apr 2022 16:31:36 GMT; HttpOnly 
  • 浏览器用 JS 脚本通过 document.cookie 或 CookieStore 来设置 cookie。(这种方法只能获取非HttpOnly类型的cookie)。
    • 客户端可以设置cookie 的下列选项:expires、domain、path、secure(有条件:只有在https协议的网页中,客户端设置secure类型的 cookie 才能成功),但无法设置HttpOnly选项。

2、有如下的属性

这些属性规定了什么时候失效,发送到哪个域名,哪个路径 。

  • cookieName=cookieValue:cookie的名字和值,必填项。
  • Http-only:设置之后,只能通过HTTP响应报文的Set-Cookie来新增或更新Cookie,客户端无法通过js脚本的方式来读写cookie。这样的话如果攻击者成功实施了XSS攻击,就会因为无法读取cookie而拿不到敏感信息。
  • Expires:用于设置cookie的国企时间点,使用了GMT时间格式的字符串。
    <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT 
  • Max-Age:cookie的有效时间长度,单位为秒。通过设置小于等于0的数字,可以让一个cookie失效。如果Max-Age和Expires同时存在,以Max-Age为准。
  • Path:设置cookie的路径作用域。
    Set-Cookie: lang=en-US; Path=/user 

    浏览器会将名为lang的cookie保存在当前域名下。
    但之后请求时,会判断路径是否匹配/user,来决定是否待上cookie。

  • Domain:设置cookie的domain的作用域。
    Set-Cookie: lang=en-US; Domain=.a.com 

    在不提供该属性的情况下,会使用请求时的domain,比如http://www.a.com/api/v1/books,
    不设置Domain时,拿到的cookie的Domain属性会设置为www.a.com
    如果Domain不能覆盖当前域名,该cookie会被认为是无效,然后丢弃掉。
    domain和path2个选项共同决定了cookie何时被浏览器自动添加到请求头部中发送出去。如果没有设置这两个选项,则会使用默认值

  • Secure:本身没有值,属性本身存在就代表安全模式。请求必须是HTTPS,cookie才会被保存下来。
    Set-Cookie: token=en-US; Secure 
  • SameSite:cookie在跨域时是否应该被发送。
    • Strict:跨域请求严禁携带本站cookie
    • Lax:使用顶级导航的方式并使用GET请求时可以携带
    • None:会携带cookie。但前提是Secure设置为true。
    Set-Cookie: token=abcd; SameSite=Strict 

3、前端如何操作cookie

因为cookie没有自己封装好的getItem、setItem等方法,因此前端人员需要手动封装。

 // 如果要设置过期时间以秒为单位 function setCookie(c_name, value, expireseconds) { 
    var exdate = new Date(); exdate.setTime(exdate.getTime() + expireseconds * 1000); document.cookie = c_name + "=" + escape(value) + ((expireseconds == null) ? "" : ";expires=" + exdate.toGMTString()) } // setCookie('name', 'zzyn', 3600); //cookie过期时间为一个小时 console.log(document.cookie) document.cookie = 'name=张三' function getcookie(key) { 
    var str = document.cookie; var startIndex = str.indexOf(key); var value = ""; if (startIndex == -1) { 
   //找不到 return value; } var endIndex = str.indexOf(";", startIndex); if (endIndex == -1) { 
   //找不到 value = str.substring(startIndex + key.length + 1) } else { 
    value = str.substring(startIndex + key.length + 1, endIndex) } return value; } 

cookie推荐博客

什么是断点续传,如何操作的

1、如何上传大文件:

如何实现大文件上传。当我们将文件切片,除了需要给文件对应的值(可以使用md5)外,要还给每个小文件对应的编号,将md5和编号同时发送给后端,后端根据md5和编号进行小文件存储,当发生文件上传一半终止上传,可以获取后端已经上传的小文件编号发送到前端,前端检测到后端发送的小文件编号,从对应编号的切片进行上传。

2、断点续传:

就是从文件已经下载的地方开始继续下载。HTTP/1.1就开始支持了。一般断点下载时才需要用到Range和Content-Range请求头。

  • Range用于请求头,指定第一个字节的位置和最后一个字节的位置。
  • Content-Range用于响应头,指定整个实体中的一部分的插入位置。

3、一个简单的断点续传的实现大概如下:

1.客户端下载一个1024K的文件,已经下载了其中512K
2. 网络中断,客户端请求续传,因此需要在HTTP头中申明本次需要续传的片段:Range:bytes=- 这个头通知服务端从文件的512K位置开始传输文件
3. 服务端收到断点续传请求,从文件的512K位置开始传输,并且在HTTP头中增加:Content-Range:bytes -/
并且此时服务端返回的HTTP状态码应该是206,而不是200。

cookie中可以放置一些用户的信息。
理解Cookie、Session和token机制,及其安全问题,可以参考博客:
http://t.zoukankan.com/yunmuq-p-15799113.html

对前端监控的理解

  • 数据监控
  • 性能监控
  • 异常监控

1、数据监控:就是监听用户的行为,常见的监控项有:

  • 页面浏览量或点击量
  • 用户在一个页面的停留时间
  • 用户通过什么入口来访问该网页
  • 用户在相应的页面中触发的行为

2、性能监控:监听前端的性能,主要包括监听页面或者说产品在用户端的体验。(可以利用chrome插件Lighthouse来去监控)

  • 不同用户,不同机型和不同系统下的首屏加载时间
  • 白屏时间
  • http 等请求的响应时间
  • 静态资源整体下载时间
  • 页面渲染时间
  • 页面交互动画完成时间

3、异常监控:

由于产品的前端代码在执行的时候也会发生异常,因此需要引入异常监控。比如通过window.onerror采集到所有的未捕获的异常。

4、如何实现前端控制?

收集监控数据是通过前端埋点来实现,常见的前端埋点方法有三种:手动埋点、可视化埋点和无埋点。
对于上报的方式无外乎两种:
一种是Ajax的方式上报;另一种是通过Image的形式进行上报。目前很多大厂采用的上报方式均是通过一个1*1像素的的gif图片进行上报。
为什么采用Image的方式上报?
没有跨域问题。因为数据服务器和后端服务器大概率是不同的域名,若采用Ajax的方式进行处理还要处理跨域问题,否则数据会被浏览器拦截。
不会阻塞页面加载,只需new Image对象即可。

有哪些首屏优化的方式

  • 路由懒加载:component:()=>import(“view/home/Home.vue”)
  • vue脚手架默认开启了preload和prefetch,当我们项目很大的时候,这个首屏加载就成了最大的元凶了。
    – preload与prefetch都是资源预加载机制
    – preload是预先加载资源,但并不执行,只有需要时才执行
    – prefetch是意图获取一些资源,以备下一个导航
    – preload的优先级高于prefetch
    //vue.config.js chainWebpack(config) { 
          config.plugins.delete('preload') // 删除默认的preload config.plugins.delete('prefetch') // 删除默认的prefetch } 
  • 使用gzip对资源进行压缩:压缩一些静态资源,让用户请求这些资源的大小会降低。
  • CDN优化:指把第三方库比如(vue、vue-router、vuex、axios)通过cdn的方式引入到项目中,减少打包后的文件体积。

prefetch和preload的区别和实用场景

1、prefetch:

元素的ref属性的属性值preload能够在html页面中书写一些声明式的资源获取请求,指明哪些资源是在页面加载完成之后立刻需要的。可以让浏览器提前加载指定资源,需要执行的时候再执行。

2、prefetch:

  • preload 是告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源。
  • prefetch 是告诉浏览器页面可能需要的资源,浏览器不一定会加载这些资源。
  • 在VUE SSR生成的页面中,首页的资源均使用preload,而路由对应的资源,则使用prefetch。
  • 对于当前页面很有必要的资源使用 preload,对于可能在将来的页面中使用的资源使用 prefetch。

参考博客:https://www.jianshu.com/p/8920dc078689

CORS中,如何将cookie发送到服务器

在跨域的时候,浏览器默认情况下无法主动跨域向后端发送cookie,如果想要发送的话,就要将withCredentials设置为true。
同时服务器也不能随便就能接受并返回新的cookie给你。需要在服务器端设置:Access-Control-Allow-Credentials:true。这样服务器端才能返回新的cookie给你。

说一下Babel是如何实现的

  1. 解析parse:将代码解析生成抽象语法书(AST)。
  2. 转换Transform:对AST进行变换的一些操作。进行一些对节点进行添加、更新以及移除的一些操作。
  3. 生成Generator:将变换后的AST再转换为JS代码。

如何制作一个插件并上传到网上进行下载

步骤如下:

  1. 创建包结构
  2. 创建src文件夹(代码编写的地方)、package.json(包管理配置文件)、index.js(包的入口文件)、README.md(包的说明文档)。
  3. 如下图所示。
    在这里插入图片描述
  4. 在src文件夹下创建:dateFormat.js(用于时间格式化处理)、htmlEscape.js(用于转义html字符函数)。
  5. 创建index.js(包入口文件)
  6. package.json,包含以下部分
    – 包的名称name,注意文件夹的名称和 name名称没关系。
    – 包的版本version
    – 包的入口文件main
    – 包的描述信息description
    – 搜索关键字keywords
    – 包遵守的开源许可协议license,npm官方推荐ISC
  7. README.md:是包的说明文档。
  8. 发布:
    – 首先要查看 https://www.npmjs.com/ 网站,自己是否注册过,因为下面要用到用户名以及密码
    – 打开终端,运行npm login。有的可能会报错。解决方案:npm set registry https://registry.npmjs.org/
    – 输入用户名和密码
    – 在要发布的包的根目录中,运行npm publish命令,就可以发布到npm上了。
    在这里插入图片描述
    在这里插入图片描述
  9. 删除可以执行npm unpublish 包名 --force
  10. 下载自己的包:npm i (名字)
  11. 正常导入使用就可以了。
let xixitool = require('xixi-tools') const dataStr = xixitool.dateFormat('2022-10-26 10:5:5') console.log(dataStr) 

虚拟列表

应用场景:如果后端给你的数据量很大有十万条,且后台又没有提供分页的接口,这时候应该如何操作才能减轻浏览器的压力。有两种解决方案:分片渲染和虚拟列表

1、分片渲染

这里其实就是对大数据列表进行切片,结合定时器依次加载到列表里面。

  1. 首先定义了四个变量,start(开始位置)、size(一次性加载的数据量大小)、originList(真实列表里存放的数据)、dealList(要渲染的列表数据)、
  2. 在mounted里面模拟要加载的数据量
  3. load方法里面渲染出来。根据start对originList中的数据切片,if判断是否还有数据,如果有的话开启定时器,将新切出来的数据和之前的数据进行合并,赋值给dealList。改变start的值。然后再次调用load()方法,直到dealList包含了originList中所有的数据。

这里采用的思想其实就是依次加载,而不是一下子全部加载完。
下面是分片渲染的代码实现。

<div class="" v-for="(item, index) in dealList" :key="index"> { 
  { item }} </div> 
 data() { 
    //这里存放数据 return { 
    start: 1, //开始的位置 size: 20, //一次加载20条数据 originList: [], //数组中存放的值 dealList: [], //真实渲染列表中的数据 }; }, //在mounted钩子里面。产生数据 mounted() { 
    for (let i = 0; i < 10000; i++) { 
    this.originList.push("我是第" + i + "条数据"); } this.load(); }, methods: { 
    load() { 
    let tempList = this.originList.slice(this.start, this.start + this.size); if (tempList && tempList.length > 0) { 
    setTimeout(() => { 
    this.dealList = [...this.dealList, ...tempList]; this.start = this.start + this.size; this.load(); }, 100); } }, }, 

2、虚拟列表

就是是根据滚动条移动的距离来去加载元素。用到的是scrollTop这个位置元素。

  1. 跟分块渲染的页面布局不同的是,虚拟列表有一个空白div,可用户显示滚动条。空白div的高度就是真实列表的高度。可视区域就是真实的渲染页面所有的列表数据。如下图所示,灰色是空白div。
    在这里插入图片描述
  2. 定义变量:start(开始的位置)、size(每页渲染的列表大小)、height(每条数据的高度)、totalList(真实的列表数据)、scropTop(滚动的条离顶部的距离)
  3. 跟分片渲染一样,在Mounted钩子里面生成要渲染的数据。将父div盒子的高度固定。不然的话显示不出滚动条。
  4. 采用computed计算属性,监听start的变化,然后切割totalList数组,从而获得真实的渲染列表
  5. 这里就是如何改变start了。通过监听virtualList身上的滚动方法,获得滚动高度,赋值给scropTop,scropTop滚动高度除以每一个列表的高度,就是start的值。
  6. 在页面样式上也需要注意一下,采用的是子绝父相,将滚动盒子固定到父元素的左上角。并沾满整个父盒子,父盒子设置overflow: auto,固定父盒子高度。同时用空的div撑大父盒子,因此就出现了滚动条。

下面是虚拟列表的的代码实现:

<template> <div class="wrapper"> <div id="virtualList" ref="virtualList" @scroll="handleScroll"> <!-- 空白div --> <div :style="{ height: totalList.length * height + 'px' }"></div> <!-- 可视区域 --> <div class="container" :style="{ top: this.scropTop + 'px' }"> <div id="dataItem" v-for="(item, index) in needRenderList" :key="index" :style="{ height: height + 'px' }" > { 
  { item }} </div> </div> </div> </div> </template> <script> export default { 
      name: "VirtualList", data() { 
      return { 
      start: 1, //开始的位置 size: 20, //每页渲染的大小 height: 40, //每条高度 totalList: [], scrollTop: 0, }; }, computed: { 
      needRenderList() { 
      return this.totalList.slice(this.start, this.start + this.size); }, }, mounted() { 
      for (let i = 0; i < 10000; i++) { 
      this.totalList.push("我是第" + i + "条数据"); } // this.$refs.virtualList.style.height = this.size * this.height + "px"; }, methods: { 
      // 滚动方法 handleScroll() { 
      this.scrollTop = this.$refs.virtualList.scrollTop; this.start = this.scrollTop / this.height; }, }, }; </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> #virtualList { 
      overflow: auto; position: relative; width: 200px; } .container { 
      position: absolute; left: 0; top: 0; width: 100px; } </style> 
总结:虚拟列表跟分片渲染的思想是不同的,分片渲染是结合定时器,一部分一部分的加载列表的数据。 虚拟列表是通过滚动的高度来加载数据,也就是说你不滚动他就不加载了,还是那些数据。但是分片渲染,过一会数据会自动加载完成。 

用户上传了一张图片,如何实现预览功能(代码题)

1、 通过一种异步文件读取机制FileReader实现。具体步骤是:

  1. 创建FileReader对象
  2. 调用readAsDataURL函数读取文件内容
  3. 监听FileReader创建的实例对象的onload事件
  4. 通过e.target.result获取读取到的结果,值是base64格式的字符串
  5. 将读取到的结果赋值给img标签的src属性
 <input type="file"> <img src="./avatar.jpg" alt=""> <script> const img = document.querySelector('img') document.querySelector('input').addEventListener('change',function(e){ 
    // 获取选择的文件 const fileList = e.target.files // 创建FileReader实例对象 const reader = new FileReader() // 调用readAsDataURL函数读取文件内容 reader.readAsDataURL(fileList[0]) // 监听FileReader创建的实例对象的onload事件 reader.addEventListener('load', () => { 
    // 通过e.target.result获取读取到的结果,值是base64格式的字符串 console.log(reader.result) img.src = reader.result }) }) </script> 

2、进阶版:使用内置的URL对象的createObjectURL

通过将图片对象存储到内存里面,并返回一个地址。

  • 定义一个变量接收URL.createObjectURL,返回url
  • 将url地址赋值给img标签的src属性

(与上一个不同的是,上一个调用的是FileReader对象的readAsDataURL函数来读取文件内容)

 <input type="file"> <img src="./avatar.jpg" alt=""> <script> const img = document.querySelector('img') document.querySelector('input').addEventListener('change',function(e){ 
    // 获取选择的文件 const fileList = e.target.files // 定义一个变量接收URL.createObjectURL(图片对象)返回的url地址 const url = URL.createObjectURL(fileList[0]) // 将这个变量存储的url地址赋值给img标签的src属性 img.src = url }) </script> 

怎样在原生的JS中写Vue-Router(代码题)

前端的路由一般有两种,一种是Hash路由,另一种是History路由。

1、History路由:

  • History.back():前往上一页, 用户可点击浏览器左上角的返回按钮模拟此方法. 等价于 history.go(-1)。
  • History.forward():在浏览器历史记录里前往下一页,用户可点击浏览器左上角的前进按钮模拟此方法. 等价于 history.go(1)。
  • History.go(n):通过当前页面的相对位置从浏览器历史记录中加载页面。
  • history.pushState()和history.replaceState():都可以操作浏览器的历史记录,而不会引起页面的刷新。不同之处在于: pushState 会增加一条新的历史记录,而 replaceState 则会替换当前的历史记录。

2、Hash路由

当hash发生变化的时候,可以通过hashchange事件监听到,从而在回调函数里面触发某些方法。

3、简单版-单页面路由:

采用的是hash模式。

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="author" content=""> <title>原生模拟 Vue 路由切换</title> <style type="text/css"> .router_box, #router-view { 
      max-width: 1000px; margin: 50px auto; padding: 0 20px; } .router_box>a { 
      padding: 0 10px; color: #42b983; } </style> </head> <body> <div class="router_box"> <a href="/home" class="router">主页</a> <a href="/news" class="router">新闻</a> <a href="/team" class="router">团队</a> <a href="/about" class="router">关于</a> </div> <!-- 内容呈现的地方 --> <div id="router-view"></div> <script type="text/javascript"> //这里是定义了一个路由数组,里面包裹着一个个的路由对象。每个路由对象由路径path以及component组成。path跟页面上a标签中的href属性对应,component则是每个子组件的内容。 new Vue({ 
      routes: [{ 
      path: '/home', component: "<h1>这里是主页内容区</h1>" }, { 
      path: '/news', component: "<h1>这里是新闻内容区</h1>" }, { 
      path: '/team', component: '<h1>这里是团队内容区</h1>' }, { 
      path: '/about', component: '<h1>这里是关于内容区</h1' }, { 
      path: '*', redirect: '/home' }] }); //构造函数vue,parameters是传入的参数 function Vue(parameters) { 
      let vue = { 
     }; vue.routes = parameters.routes || [];//判断是否为空 vue.init = function() { 
      //获得菜单标签对象 document.querySelectorAll(".router").forEach((item, index) => { 
      //绑定点击事件 item.addEventListener("click", function(e) { 
      let event = e || window.event; event.preventDefault();//阻止默认事件 //将菜单身上的href属性值赋值给window.location.hash window.location.hash = this.getAttribute("href"); }, false); }); //绑定监听事件,监听hashchange事件,一旦window.location.hash属性发生变化,此事件就会触发 window.addEventListener("hashchange", () => { 
      //路由跳转方法 vue.routerChange(); }); vue.routerChange(); }; vue.routerChange = () => { 
      let nowHash = window.location.hash; //在刚new出来的vue列表中寻找对应的下标。 let index = vue.routes.findIndex((item, index) => { 
      return nowHash == ('#' + item.path); }); //如果index找到的话,就采用innerHTML将内容呈现出来 if (index >= 0) { 
      document.querySelector("#router-view").innerHTML = vue.routes[index].component; } else { 
      //如果没有找到,则找到路由中默认加载的地址。 let defaultIndex = vue.routes.findIndex((item, index) => { 
      return item.path == '*'; }); //如果有的话,就再次对window.location.hash进行赋值。 //在vue构造函数的hashChange事件监听下来,又调用了一次routerChange事件,应该也是为了防止没有找到路由吧。完成了一次重定向。 if (defaultIndex >= 0) { 
      window.location.hash = vue.routes[defaultIndex].redirect; } } }; vue.init(); } </script> </body> </html> 

步骤:

  1. 页面中设置点击跳转的地方,用的是a标签,href中定义要跳转的地址。
  2. js中定义路由数组,里面包裹着路由对象。每个路由对象由路径path以及component组成。path跟页面上a标签中的href属性对应,component则是每个子组件的内容。
  3. 构造函数vue,parameters是传入的参数,这里的parameters就是传入的路由数组对象,new已经在第二步发生了。
  4. 路由初始化。在初始化中要做两件事情。
    • 为菜单标签绑定监听事件。这里要获取菜单标签,为每一个标签绑定点击事件。在点击的过程中,要讲菜单身上的href属性赋值给window.location.hash属性
    • 为window绑定监听事件hashchange事件。一旦window.location.hash属性发生变化,此事件就会触发,事件里面编写的是路由跳转方法
  5. 根据window.location.hash获得当前的路由,在路由对象数组中找到下标,根据下标找到对应的component对应的子组件内容。然后将内容赋值到路由应该显示的容器上的innerHTML中。
  6. 如果没有找到对应的下标的话,就找到路由中默认加载的地址。再次对window.location.hash进行赋值。在vue构造函数的hashChange事件监听下来,又调用了一次routerChange事件,应该也是为了防止没有找到路由吧。完成了一次重定向。
    参考博客:

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/148289.html

(0)
上一篇 2025-04-01 14:20
下一篇 2025-04-01 14:33

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信